-
Notifications
You must be signed in to change notification settings - Fork 135
Refactor config attributes loading #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ class SATOSAConfig(object): | |
A configuration class for the satosa proxy. Verifies that the given config holds all the | ||
necessary parameters. | ||
""" | ||
|
||
sensitive_dict_keys = ["STATE_ENCRYPTION_KEY"] | ||
mandatory_dict_keys = ["BASE", "BACKEND_MODULES", "FRONTEND_MODULES", | ||
"INTERNAL_ATTRIBUTES", "COOKIE_STATE_NAME"] | ||
|
@@ -30,11 +31,12 @@ def __init__(self, config): | |
:param config: Can be a file path or a dictionary | ||
:return: A verified SATOSAConfig | ||
""" | ||
parsers = [self._load_dict, self._load_yaml] | ||
for parser in parsers: | ||
self._config = parser(config) | ||
if self._config is not None: | ||
break | ||
self._config = self._parse_config(config) | ||
|
||
if not self._config: | ||
raise SATOSAConfigurationError( | ||
"Missing configuration or unknown format" | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be part of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope. This statement used to be in the beginning of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me rephrase the question. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not answering what I am asking. Whether those are functions or methods is irrelevant to the question. What I am asking is who is responsible for checking if the result of an action is an error. You do not need to think about this particular case at all; see the example bellow. You have two choices, the first is what is being done now - the caller is checking for errors
and a second choice, where the callee is checking for the error
How can we compare those two and choose one over the other? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It depends is the answer. It depends on what kind of error is this. Is it an evaluation error or it is a logic error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so what we do here is we parse the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed in this case, it is a parsing error and the parsing function should throw this |
||
|
||
# Load sensitive config from environment variables | ||
for key in SATOSAConfig.sensitive_dict_keys: | ||
|
@@ -44,26 +46,49 @@ def __init__(self, config): | |
|
||
self._verify_dict(self._config) | ||
|
||
self._load_plugins() | ||
self._load_internal_attributes() | ||
|
||
def _parse_config(self, config): | ||
return next( | ||
filter( | ||
lambda conf: conf is not None, | ||
map( | ||
lambda parser: parser(config), | ||
(self._load_dict, self._load_yaml), | ||
), | ||
), | ||
None, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would write this, as: value = next(
(
conf
for parser in [self._load_dict, self._load_yaml]
for conf in [parser(config)]
if conf
),
None,
)
return value it is cleaner, and more "pythonic" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For "pythonic" (whatever this vague term means) I agree. For cleaner I think it's more subjective. I always found nested comprehensions to be less clear than nested maps (especially when nesting > 2) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I should put a debugger there I'm sure that there will be problems. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would you put the debugger inside the comprehension to debug it? On the contrary I think it's easier to debug filter & map since each map call can be debugged on it's own, whereas a nested comprehension must be run fully or rewritten in smaller parts to be debugged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You simply can't! This is why it would be better to avoid that kind of generalization. This is just a personal view, I like the others code style, I also imitate these if needed and with curiosity. I'm jut getting older, that's all. Because of this I'd prefer a simple and very debuggable code. I've get into this thread to exchange some words about my personal view, now I've done, you can simply ignore me and go ahead 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You don't have to convert anything in the map/filter, you simply evaluate each part (the map part, or/and the filter part). There is no need to write any for loop. Map/filter power is composition which cannot be achieved with comprehensions. Try having a 4 times nested comprehension (for complex constructs) to see how readable it is (I'm not sure even if it is achievable). Whereas map/filter does not change at all, you compose each map as an argument to the next map. List comprehensions have a limit since they are a syntactic construct whereas map is a higher order function that can be composed with much less limitation. Readability for this specific discussion, depends on the experience one has with map/filter usage and concepts and it is a personal taste as well. If we had this conversation 10 years ago, mutating everything like crazy and having 10 levels deep for loops could be a normal thing that one would consider "readable". There is a reason concepts like map/filter and other functional concepts are being introduced and used more and more today even (if Guido wants to rant about how hard he finds map/filter/reduce and prefers comprehensions). The same thing can be said about the usage of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
you cannot evaluate each part, as the two are nested. I would understand this if the code was in the form intermediate_result = map(foo, bar)
final_result = filter(baz, intermediate_result) which directly translates to comprehensions intermediate_result = (foo(x) for x in bar)
final_result = (x for x in intermediate_result if baz(x))
we agree on that.
I can't see why and I'm interested in understanding this.
what is the order of the for statement? For this specific snippet, there is nothing being composed. Creating lambdas on the fly is not composition. You would be composing if these were functions that were being reused to achieve a greater effect. Instead, this is just hardcoded behaviour in a map/filter construct. Just because someone is using map/filter/reduce, it does not mean that they're composing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
in order to debug the following
you have to evaluate its parts. to evaluate them you have to write a in order to evaluate this:
you evaluate
I actually meant functional decomposition (splitting a problem into a set of functions). Composition is another feature that is not builtin in python (you have to create a function that will create composite function whereas in other languages there is a operator to do that) For example:
I want to flatten a list of integers
reading from right to left, I flatten the list, then I get the string representation of them, then I filter out those that are even, then I add the star character With list comprehensions and if statements:
Now how do I read this to describe what is done? Can I read from right to left? No. Can I read from left to right? Probably, let's try: I add a star character to a variable that is the one that is contained in a list of other variables that the string representation of that sub-variable when applied the remainder_of_two function, returns != 0. Is this narrative correct? Not exactly because I never mentioned the flattening of the list of lists explicitly. That kind of processing with list comprehensions needs extra variables. map/filter/reduce are higher order functions and don't need those (unless you pass a lambda). (sidenote: until python 3 list comprehensions were leaking scope of their variables (that is, they were accessing outside variables instead of creating a local scope).
As I said before, narrating the operation in one's head, feeding results in functions is simpler than having to keep variables in brain memory. In our case:
How do we express this above? I have a result called conf that -skip to line 3- when applied to a variable called parser -skip to line 2- where parser is a loop over a list of functions, -skipt to line 4- if conf is truthish. What I mean by this extremely naive narration, is that in comprehensions you have to read the whole expression to understand what's happening. Whereas in map/filter in our case:
Reading for right to left: I have a list of functions, and I apply them to a config, and then the results I feed them to a check to see if they are truthish and keep those that are.
Indeed there is no composition here since there aren't any composite functions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yes.
no. As with map/filter you evaluate the different parts.
Which means that you evaluate its parts. I think that you are assuming that the way map/filter work in python, is the same as in most other languages, but this is not the case. The This works exactly as comprehensions work, and it is the reason you need to wrap the outermost function (filter in this case) with a pull-function, as you do with comprehensions. In most other languages, map and filter do not return an iterator, but a complete result. That used to be the case for python2; but for python3 the effect is the same as with comprehensions.
yes. The reason I mentioned the transformation to the for loop, was to be able to invoke a debugger inside the loop. If you're saying there is not reason to look at the The only good thing with map/reduce is that you can replace them with versions that can parallelize the operation safely. (which is not needed here and in the majority of the cases)
Reading from right to left is the opposite of what people do. To be precise, this reads inside-out (or bottom-top if you will).
There is no need for this nesting. This should be written as
or even
Reading is fine, from top to bottom.
What you're doing here is the same as this:
(which, again, is not true, because in practice map and filter return iterators.) Likewise, I can read the comprehension as: for each parser, apply it to config and generate a conf, if that is truthish keep it I doubt that you find it easier to locate the inner-most function, and immediately reason about it, while you find it hard to read that comprehension. In general, comprehensions read from top to bottom, left to right, and accumulate results in the very first line. Instead map/filter read inside-out, and demand understanding of the arguments. Having said all this, I do not think that there is any other place where you would nest functions one inside the other like you do with map and filter. You would not do:
instead you would split this into multiple lines that feed results into each other.
so, I cannot understand how this changes with map and filter, and how it is helpful to anyone to read it nested like that. Do people really write nested code like that with map/filter/reduce? Do they try to convert their whole programs into a one-liner? Where does this come from?
The only reason you do not have variables is because of this nesting. Variables is not a bad thing that we should avoid. Instead, variables are pretty helpful to document in-code the intermediate results. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Well it depends. In Clojure for example maps are lazy (realized in chunks to be exact) whereas in Haskell they are not (they are non-strict which is not the same) although they act like the same
I said it to point the fact that you simply evaluate each part without rewriting it whereas in list comprehensions you have to rewrite it if it has many parts (i.e nested comprehension with if statement)
Well from top to bottom in the example I read a v and the declaration for what
They don't I'm simply mentioning this to underline why I think map/filter is more powerful; because it chains better
Variables is not a bad thing indeed. I simply mentioned them that you have to remember variables when reading a comprehension I think we're beating a dead horse here, we will probably not reach an agreement. I think this ends up to personal taste. People using mostly Python might find comprehensions more intuitive because they are more exposed to them and the community & BDFL supports them. I started this discussion to underline the fact that "clearer" and "pythonic" are not objective terms and should be taken with a grain of salt. Especially "pythonic" can lead to assumptions that are counter-intuitive for other languages (i.e by default ordered dicts, assignment expressions as in pep572 etc) |
||
|
||
def _load_plugins(self): | ||
def load_plugin_config(config): | ||
plugin_config = self._parse_config(config) | ||
if not plugin_config: | ||
raise SATOSAConfigurationError( | ||
"Failed to load plugin config '{}'".format(config) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this message different than the one on line 38? |
||
) | ||
else: | ||
return plugin_config | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should try to have a straight codepath. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thought that "explicit is better than implicit" for the eye here, but sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
def foo(x):
value = command1(x)
if has_error(value):
generate_error()
else:
command2(value)
command3(value)
command4(value) or def foo(x):
value = command1(x)
if has_error(value):
generate_error()
command2(value)
command3(value)
command4(value) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It depends is the answer again. I would probably have a validation function call to throw the error and hide the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I feel we're saying the same thing. To keep one logical path you would not put the rest of the code under the else branch. |
||
|
||
# Read plugin configs from dict or file path | ||
for key in ["BACKEND_MODULES", "FRONTEND_MODULES", "MICRO_SERVICES"]: | ||
plugin_configs = [] | ||
for config in self._config.get(key, []): | ||
for parser in parsers: | ||
plugin_config = parser(config) | ||
if plugin_config: | ||
plugin_configs.append(plugin_config) | ||
break | ||
else: | ||
raise SATOSAConfigurationError('Failed to load plugin config \'{}\''.format(config)) | ||
self._config[key] = plugin_configs | ||
|
||
for parser in parsers: | ||
_internal_attributes = parser(self._config["INTERNAL_ATTRIBUTES"]) | ||
if _internal_attributes is not None: | ||
self._config["INTERNAL_ATTRIBUTES"] = _internal_attributes | ||
break | ||
self._config[key] = list( | ||
map( | ||
lambda x: load_plugin_config(x), | ||
self._config.get(key, []) or [], | ||
) | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is cleaner with a comprehension # generate
pconfs = {
ptype: [
load_plugin_config(pconf)
for pconf self._config.get(ptype) or []
]
for ptype in ["BACKEND_MODULES", "FRONTEND_MODULES", "MICRO_SERVICES"]:
}
# mutate
self._config.update(pconfs) |
||
|
||
def _load_internal_attributes(self): | ||
self._config["INTERNAL_ATTRIBUTES"] = self._parse_config( | ||
self._config["INTERNAL_ATTRIBUTES"] | ||
) | ||
|
||
if not self._config["INTERNAL_ATTRIBUTES"]: | ||
raise SATOSAConfigurationError("Could not load attribute mapping from 'INTERNAL_ATTRIBUTES.") | ||
raise SATOSAConfigurationError( | ||
"Could not load attribute mapping from 'INTERNAL_ATTRIBUTES." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A third different message for the same action. Can we make those messages similar? |
||
) | ||
|
||
def _verify_dict(self, conf): | ||
""" | ||
|
@@ -76,12 +101,14 @@ def _verify_dict(self, conf): | |
:param conf: config to verify | ||
:return: None | ||
""" | ||
if not conf: | ||
raise SATOSAConfigurationError("Missing configuration or unknown format") | ||
|
||
for key in SATOSAConfig.mandatory_dict_keys: | ||
if key not in conf: | ||
raise SATOSAConfigurationError("Missing key '%s' in config" % key) | ||
if not conf.get(key, None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in |
||
raise SATOSAConfigurationError( | ||
"Missing key {key} or value for {key} in config".format( | ||
key=key | ||
) | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I have three keys missing, should I have to try three times to get the complete information? |
||
|
||
for key in SATOSAConfig.sensitive_dict_keys: | ||
if key not in conf and "SATOSA_{key}".format(key=key) not in os.environ: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we know which of the two it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We didn't and we don't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we provide this information?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uknown format means parsing failed or returned empty dict whereas missing configuration could mean that either an empty config argument was given to the constructor or that the argument was a file that couldn't be opened. The first case was/is only indirectly checked. The second will be displayed in the logs (if
_load_yaml
cannot open the file for example). I'm not saying that this is the way it should be done, I'm just saying how it is currently implementedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know; and what I'm saying is that we should make clear what went wrong. How we make it clearer is what we are discussing.
Ideas to improve this:
config
as part of the message and let the user figure out which of the two it is implicitlyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should probably be written in a new issue, otherwise in a PR they will be lost