On Thu, May 07, 2020 at 06:20:46PM +0200, Andrea Bolognani wrote:
On Wed, 2020-05-06 at 14:05 +0200, Erik Skultety wrote:
> class Config:
>
> + def __init__(self):
> +
> + # Load the template config containing the defaults first, this must
> + # always succeed.
> + # NOTE: we should load this from /usr/share once we start packaging
> + # lcitool
> + base = Util.get_base()
> + with open(os.path.join(base, "config.yaml"), "r") as
fp:
> + self.values = yaml.load(fp, Loader=yaml.Loader)
We're use yaml.safe_load(fp) everywhere else. Any reason why we
should need the non-safe version? Why is it necessary to explicitly
provide it with a Loader?
"YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated"
...but this was a completely wrong choice of API on my end, since rather then
grepping the code base and copy-pasting, I read the documentation and missed
the safe_load API which also doesn't suffer from the deprecation warning.
> + try:
> + with open(self._get_config_file("config.yaml"),
"r") as fp:
> + user_config = yaml.load(fp, Loader=yaml.Loader)
> + except Exception as e:
> + print("Missing or invalid config.yaml file: {}".format(e),
> + file=sys.stderr)
> + sys.exit(1)
As mentioned last time, this is not the correct place or way to
handle this exception. I know you don't like how we process failures
right now :) but the way to address that is to post a patch that
eradicates the current implementation and replaces it with a better
one in one fell swoop, not to introduce alternative ways to do error
handling in a few select places.
I have posted a patch[1] that implements the suggestion I gave last
time, and that allows you to simply raise an Exception here.
> + # Validate the user provided config and use it to override the default
> + # settings
> + self._validate(user_config)
> + self._update(user_config)
Incidentally, since these two functions can also fail, with your
approach you'd have reported a nice error message if the
configuration file is missing altogether but *not* if it exists but
is invalid.
> + @staticmethod
> + def _validate_section(config, section, keys):
> +
> + if config.get(section, None) is None:
> + raise KeyError("Section '{}' not
found".format(section))
This will raise the same error message both when the section is
actually missing and when it's present but doesn't contain anything,
so the error handling should be
raise Exception("Missing or empty section '{}'.format(section))
Note the use of Exception instead of KeyError - again, if you want
to change the way we do error handling by all means post patches
towards that goal, but until then please stick with the current
approach.
As for the check itself, I think I would like it to look like
if section not in config or config[section] is None:
Well, if it was only about key existence, the first part of your boolean
predicate would be preferable, but once you need to check both existence and
value, .get() is the recommended way.
I'm no Pythonista, but this it seems to follow the mantra "explicit
As it turned out, I even was explicit :P since 'None' is the default value
returned on key non-existence
is better than implicit" more closely... You've written more
Python
than me, though, so if you think using get() is superior I don't have
a problem leaving the code as is :)
> + for key in keys:
> + if config.get(section).get(key, None) is None:
> + raise KeyError("Missing mandatory key
'{}.{}'".format(section,
> + key))
The very same considerations apply here.
One additional thing: since YAML is quite flexible, I think it would
be sensible (if a little paranoid) to also have something like
if not (instanceof(config[section][key], str) or
instanceof(config[section][key], int)):
raise Exception(
"Invalid type for key '{}.{}'".format(section, key)
)
Yes, this is a good idea, I'll incorporate that into the next revision,
although I'll do it a bit differently.
otherwise we'd happily accept something like
---
install:
root_password:
- let
- me
- in
as valid.
Yep, that's bad.
> + @staticmethod
> + def _validate(config):
The first thing you need to check here is whether config is not None,
otherwise an empty configuration file will result in
Traceback (most recent call last):
File "./lcitool", line 1090, in <module>
Application().run()
File "./lcitool", line 452, in __init__
self._config = Config()
File "./lcitool", line 156, in __init__
self._validate(user_config)
File "./lcitool", line 195, in _validate
Config._validate_section(config, "install", ["root_password"])
File "./lcitool", line 183, in _validate_section
if config.get(section, None) is None:
AttributeError: 'NoneType' object has no attribute 'get'
Damn, this is bad too, I'll fix it.
> + # verify the [install] section and its mandatory options
> + Config._validate_section(config, "install",
["root_password"])
> +
> + # we need to check flavor here, because later validations depend on it
> + flavor = config.get("install").get("flavor",
"test")
This hardcodes the default value for flavor in the script instead of
reading it from the default configuration file. It should be
.get("flavor", self.values["install"]["flavor"])
I'm not hardcoding anything. What's actually being done is we're only
verifying
the user-provided data, so we should not interact with the default values at
this point yet, so instead, either '' or None should be returned from the
.get() method.
--
Erik Skultety