From b63746dafcde53f295fdecd663108c3783792f93 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 17 Oct 2022 18:51:51 +0200 Subject: [PATCH 01/26] API-change: refactor of state-dependency handling --- src/miniflask/miniflask.py | 206 ++++++++++++++++++------------------- src/miniflask/state.py | 115 ++++++++++++++++++++- src/miniflask/util.py | 2 +- 3 files changed, 211 insertions(+), 112 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 8f79ae1c..771a026f 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -17,7 +17,7 @@ # package modules from .exceptions import save_traceback, format_traceback_list, RegisterError, StateKeyError from .event import event, event_obj -from .state import state, as_is_callable, optional as optional_default +from .state import state, as_is_callable, optional as optional_default, state_node from .dummy import miniflask_dummy from .util import getModulesAvail, EnumAction, get_relative_id from .util import highlight_error, highlight_name, highlight_module, highlight_loading, highlight_loading_default, highlight_loaded_default, highlight_loading_module, highlight_loaded_none, highlight_loaded, highlight_event, str2bool, get_varid_from_fuzzy @@ -106,11 +106,7 @@ def __init__(self, module_dirs, debug=False): # arguments from cli-stdin self.settings_parser = ArgumentParser(usage=sys.argv[0] + " modulelist [optional arguments]") - self._settings_parse_later = {} - self._settings_parse_later_overwrites_list = [] - self._settings_parse_later_overwrites = {} - self._settings_parser_tracebacks = {} - self.settings_parser_required_arguments = [] + self.cli_required_arguments = [] self.default_modules = [] self.default_modules_overwrites = [] self.bind_events = True @@ -123,7 +119,8 @@ def __init__(self, module_dirs, debug=False): self.event = event(self, optional=False) self.event.optional = event(self, optional=True) self.state = {} - self.state_default = {} + self.state_registrations = {} # saves the information of the respective variables (initial value, registration, etc.) + self._state_overwrites_list = [] self.modules_loaded = {} self.modules_ignored = [] self.modules_avail = getModulesAvail(self.module_dirs) @@ -754,6 +751,12 @@ class TESTENUM(Enum): if not caller_traceback: caller_traceback = save_traceback() + # ensures that self.state is also usable for miniflask-wrapper + if hasattr(self.state, "all"): + local_state = self.state.all + else: + local_state = self.state + # now register every key-value pair for key, val in defaults.items(): @@ -770,27 +773,37 @@ class TESTENUM(Enum): varname = module_id + "." + key # overwrite parsefn for as_is_callable object - if isinstance(val, as_is_callable): + if isinstance(val, (as_is_callable, optional_default)): parsefn = False - # actual initialization is done when all modules have been parsed + # pre-initialize variables using an intermediate representation + # note: + # - some registrations need to be deferred, because we do not want overwrite-registrations to be dependent on module-call-orderings + # thus, this enables to have base-settings loaded before the actual to-be-overwritten module is loaded by the user + # - we need to remember overwrites for later because we need to check if the variables to overwrite actually exist + # (we can only be sure, that the varname is a unique varid if we are not overwriting here) + # - actual initialization is done when all modules have been parsed + # - design decision is here to have the dependency nodes in a seperate dict and only the state actually store data + # - we also save all tracebacks implicitly in case we need this information due to an error + is_dependency = callable(val) and not isinstance(val, type) and not isinstance(val, EnumMeta) and parsefn + node = state_node( + varid=varname, + mf=self, + caller_traceback=caller_traceback, + cliargs=cliargs, + parsefn=parsefn, + is_ovewriting=overwrite, + missing_argument_message=missing_argument_message, + fn=val if is_dependency else None + ) if overwrite: - self._settings_parse_later_overwrites_list.append((varname, val, cliargs, parsefn, caller_traceback, self)) + self._state_overwrites_list.append((varname, val, node)) else: + local_state[varname] = val - # pre-initialize variable for possible lambda expressions in second pass - # (we can only be sure, that the varname is the unique varid if we are not overwriting) - if hasattr(self.state, "all"): - self.state.all[varname] = val - else: - self.state[varname] = val - - self._settings_parse_later[varname] = (val, cliargs, parsefn, caller_traceback, self) - - # save all tracebacks in case we need this information due to an error - if varname not in self._settings_parser_tracebacks: - self._settings_parser_tracebacks[varname] = [] - self._settings_parser_tracebacks[varname].append(("overwrite" if overwrite else "definition", caller_traceback, missing_argument_message)) + if varname not in self.state_registrations: + self.state_registrations[varname] = [] + self.state_registrations[varname].append(node) def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, default=None, is_optional=False): # noqa: C901 too-complex @@ -802,8 +815,7 @@ def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, defau else: val_type, start_del, end_del = "tuple", "(", ",)" raise RegisterError(f"Variable '%s' is registered as {val_type} of length 0 (see exception below), however it is required to define the type of the {val_type} arguments for it to become accessible from cli.\n\nYour options are:\n\t- define a default {val_type}, e.g. {start_del}\"a\", \"b\", \"c\"{end_del}\n\t- define the {val_type} type, e.g. {start_del}str{end_del}\n\t- define the variable as a helper using register_helpers(...)" % (fg('red') + varname + attr('reset')), traceback=caller_traceback) - self._settings_parser_add(varname, val[0], caller_traceback, nargs="*" if isinstance(val, list) else len(val), default=val, is_optional=is_optional) - return + return self._settings_parser_add(varname, val[0], caller_traceback, nargs="*" if isinstance(val, list) else len(val), default=val, is_optional=is_optional) # get argument type from value (this can be int, but also 42 for instance) if isinstance(val, Enum): @@ -823,7 +835,7 @@ def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, defau elif not isinstance(val, type) and not isinstance(val, EnumMeta): kwarg["default"] = default if default is not None else val else: - self.settings_parser_required_arguments.append([varname]) + self.cli_required_arguments.append([varname]) # for bool: enable --varname as alternative for --varname true if argtype == Enum: @@ -847,6 +859,8 @@ def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, defau # remember the varname also for fuzzy searching self._varid_list.append(varname) + return kwarg + # ======= # # runtime # # ======= # @@ -954,63 +968,53 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme self.register_defaults(overwrite_globals, scope="", overwrite=True, caller_traceback=caller_traceback) # check fuzzy matching of overwrites - for varname, val, cliargs, parsefn, caller_traceback, _mf in self._settings_parse_later_overwrites_list: - if varname not in self._settings_parse_later: - found_varids = get_varid_from_fuzzy(varname, self._settings_parse_later.keys()) + for varname, val, node in self._state_overwrites_list: + if varname not in self.state_registrations: + found_varids = get_varid_from_fuzzy(varname, self.state_registrations.keys()) if len(found_varids) == 0: - raise RegisterError("Variable '%s' is not registered yet, however it seems like you wold like to overwrite it (see exception below)." % (fg('red') + varname + attr('reset')), traceback=caller_traceback) + raise RegisterError("Variable '%s' is not registered yet, however it seems like you wold like to overwrite it (see exception below)." % (fg('red') + varname + attr('reset')), traceback=node.caller_traceback) if len(found_varids) > 1: - raise RegisterError("Variable-Identifier '%s' is not unique. Found %i variables:\n\t%s\n\n Call:\n %s" % (highlight_module(found_varids), len(found_varids), "\n\t".join(found_varids), " ".join(found_varids)), traceback=caller_traceback) - self._settings_parse_later_overwrites[found_varids[0]] = (val, cliargs, parsefn, caller_traceback, _mf) - else: - self._settings_parse_later_overwrites[varname] = (val, cliargs, parsefn, caller_traceback, _mf) - - # add variables to argparse and remember defaults - settings_recheck = {} - for settings in [self._settings_parse_later, self._settings_parse_later_overwrites, settings_recheck]: - overwrite = settings == self._settings_parse_later_overwrites - recheck = settings == settings_recheck + raise RegisterError("Variable-Identifier '%s' is not unique. Found %i variables:\n\t%s\n\n Call:\n %s" % (highlight_module(found_varids), len(found_varids), "\n\t".join(found_varids), " ".join(found_varids)), traceback=node.caller_traceback) - for varname, (val, cliargs, parsefn, caller_traceback, _mf) in settings.items(): - is_fn = callable(val) and not isinstance(val, type) and not isinstance(val, EnumMeta) and parsefn + varname = found_varids[0] - # eval dependencies/like expressions - if is_fn: - visited_callables = set() - try: - the_val = val - while callable(the_val) and not isinstance(the_val, type): - if the_val in visited_callables: - raise RecursionError("Circular dependency found in state variable definitions.") - visited_callables.add(the_val) - the_val = the_val(_mf.state, self.event) - except RecursionError as e: - raise RecursionError("In parsing of value '%s'." % varname) from e - else: - the_val = val + # this is important: the deferred node could not not know to this point what variable it manages + node.varid = varname - # remember caculated values for other lambda expressions - self.state[varname] = the_val - - # register user-changeable variables - if cliargs: - - # remember default state for 'settings' module - if is_fn: - val.default = the_val - self.state_default[varname] = val - else: - self.state_default[varname] = the_val - - # repeat function parsing later - # (in case we have overwritten a dependency during second pass, overwrite == True) - if is_fn and not recheck: - settings_recheck[varname] = (val, cliargs, parsefn, caller_traceback, _mf) - - # add to argument parser - # Note: the condition ensures that the last value (an overwrite-variable) should be the one that generates the argparser) - if recheck or overwrite and varname not in settings_recheck or varname not in self._settings_parse_later_overwrites and varname not in settings_recheck: - self._settings_parser_add(varname, the_val, caller_traceback, is_optional=isinstance(val, optional_default)) + # apply the deferred initialization + self.state[varname] = val + self.state_registrations[varname].append(node) + + # first we build the reversed graph of dependency, i.e. the graph of what nodes are affected by each node + last_state_registrations = {varid: nodes[-1] for varid, nodes in self.state_registrations.items()} + + # sort nodes topologically, check for circular_dependencies & dependency errors for variables + topolically_sorted_state_nodes, circular_dependencies, unresolved_dependencies = state_node.topological_sort(last_state_registrations) + registration_errors = [] + if len(circular_dependencies) > 0: + registration_errors.append("Circular dependencies found! (A → B means \"A depends on B\")\n\n" + "\n".join([ + "\n → ".join(highlight_loading_module(str(c)) for c in cycle) for cycle in circular_dependencies + ])) + if len(unresolved_dependencies) > 0: + registration_errors.append("Dependency not found! (A → B means \"A depends on B\")\n\n" + "\n".join([ + "\n → ".join(highlight_loading_module(str(c)) for c in cycle) + highlight_error(" ← not found") for cycle in unresolved_dependencies + ])) + + if len(registration_errors) > 0: + registration_errors_str = "\n\n\n".join([highlight_error() + r for r in registration_errors]) + raise RegisterError(f"The registration of state variables has led to the following errors:\n\n{registration_errors_str}") + + # evaluate the dependency-graph into state + state_node.evaluate(topolically_sorted_state_nodes, self.state) + + # register user-changeable variables + for varid, node in last_state_registrations.items(): + if node.cliargs: + node.pre_cli_value = self.state[varid] + val = self.state[varid] if not isinstance(self.state[varid], optional_default) else self.state[varid].type + argparse_kwargs = self._settings_parser_add(varid, val, node.caller_traceback, is_optional=isinstance(self.state[varid], optional_default)) + if "default" in argparse_kwargs: + self.state[varid] = argparse_kwargs["default"] # add help message print_help = False @@ -1114,48 +1118,37 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme user_varids[varid] = True # parse user overwrites (first time, s.t. lambdas change adaptively) - settings_args = self.settings_parser.parse_args(argv) - for varname, val in vars(settings_args).items(): - self.state[varname] = val + settings_args = vars(self.settings_parser.parse_args(argv)) + for varid in user_varids: + val = settings_args[varid] + self.state[varid] = val + self.state_registrations[varid][-1].cli_overwritten = True # check if required arguments are given by now missing_arguments = [] - for variables in self.settings_parser_required_arguments: - if self.state[variables[0]] is None: + for variables in self.cli_required_arguments: + if not self.state_registrations[variables[0]][-1].cli_overwritten: missing_arguments.append(variables) if len(missing_arguments) > 0: args_err_strs = [] error_message_str = "" for args in missing_arguments: arg_err_str = "\t" + " or ".join([highlight_module("--" + arg) for arg in reversed(args)]) - if args[0] in self._settings_parser_tracebacks: - for definition_type, caller_traceback, missing_argument_message in self._settings_parser_tracebacks[args[0]]: - summary = next(filter(lambda t: not t.filename.endswith("miniflask/miniflask.py"), reversed(caller_traceback))) - adj = (fg('blue') + "Defined" if definition_type == "definition" else fg('yellow') + "Overwritten") + attr('reset') + if args[0] in self.state_registrations: + for node in self.state_registrations[args[0]]: + summary = next(filter(lambda t: not t.filename.endswith("miniflask/miniflask.py"), reversed(node.caller_traceback))) + adj = (fg('blue') + "Defined" if not node.is_ovewriting else fg('yellow') + "Overwritten") + attr('reset') arg_err_str += linesep + "\t " + adj + " in line %s in file '%s'." % (highlight_event(str(summary.lineno)), attr('dim') + path.relpath(summary.filename) + attr('reset')) - if isinstance(missing_argument_message, str): - error_message_str = linesep * 2 + attr("bold") + missing_argument_message + attr("reset") + if isinstance(node.missing_argument_message, str): + error_message_str = linesep * 2 + attr("bold") + node.missing_argument_message + attr("reset") args_err_strs.append(arg_err_str) raise ValueError("Missing CLI-arguments or unspecified variables during miniflask call." + linesep + linesep.join(args_err_strs) + error_message_str) - # finally parse lambda-dependencies - for varname, (val, cliargs, parsefn, caller_traceback, _mf) in self._settings_parse_later.items(): - - # optional_default assumes that argparse has set the variable - if isinstance(val, optional_default): - continue - - # Note: if has not been overwritten by user lambda can be evaluated again - # Three cases exist in wich lambda expression shall be recalculated: - # The value is a function AND one of - # 1. was helper variable, thus no cli-argument can overwrite it anyway - # 2. the value has not been overwritten by user - while callable(val) and not isinstance(val, type) and not isinstance(val, EnumMeta) and parsefn and (not cliargs or varname not in user_varids): - val = val(_mf.state, _mf.event) - self.state[varname] = val + # re-evaluate the dependency-graph with the user-cli arguments + state_node.evaluate(topolically_sorted_state_nodes, self.state) # print help message when everything is parsed - self.settings_parser.print_help = lambda: (print("usage: modulelist [optional arguments]"), print(), print("optional arguments (and their defaults):"), print(listsettings(state("", self.state, self.state_default), self.event))) + self.settings_parser.print_help = lambda: (print("usage: modulelist [optional arguments]"), print(), print("optional arguments (and their defaults):"), print(listsettings(state("", self.state, self.state_registrations), self.event))) if print_help: self.settings_parser.parse_args(['--help']) @@ -1278,7 +1271,7 @@ def __init__(self, module_name, mf): # pylint: disable=super-init-not-called self.module_name = module_name.split(".")[-1] self.module_base = module_name.split(".")[0] self.wrapped_class = mf.wrapped_class if hasattr(mf, 'wrapped_class') else mf - self.state = state(module_name, self.wrapped_class.state, self.wrapped_class.state_default) + self.state = state(module_name, self.wrapped_class.state, self.wrapped_class.state_registrations) self._recently_loaded = [] self._defined_events = {} @@ -1352,6 +1345,7 @@ def optional(self, variable_type): return optional_default(variable_type) def as_is_callable(self, variable): + # TODO r""" Wrap variables for register_-calls to ensure they are not parsed during initialization. diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 95103706..cd8f7cfa 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -1,11 +1,12 @@ import sys import re from collections.abc import MutableMapping +from inspect import getsource from colored import fg, attr from .util import get_varid_from_fuzzy, highlight_module, get_relative_id -from .exceptions import StateKeyError +from .exceptions import StateKeyError, RegisterError class temporary_state: @@ -37,7 +38,7 @@ def __exit__(self, _type, _value, _traceback): class state(MutableMapping): - def __init__(self, module_name, internal_state_dict, state_default): # pylint: disable=super-init-not-called + def __init__(self, module_name, internal_state_dict, state_dependencies): # pylint: disable=super-init-not-called r"""!... is a local dict Global Variables, but Fancy. ;) @@ -93,7 +94,7 @@ def register(mf): """ # noqa: W291 self.all = internal_state_dict - self.default = state_default + self.dependencies = state_dependencies self.module_id = module_name self.fuzzy_names = {} # self.temporary = temporary_state(self) @@ -136,7 +137,7 @@ def register(mf): ``` """ # noqa: W291 - return state(module_name, self.all, self.default) + return state(module_name, self.all, self.dependencies) def temporary(self, variables): r""" @@ -373,7 +374,111 @@ def __call__(self, state, event): # pylint: disable=redefined-outer-name def str(self, asciicodes=True, color_attr=attr): if not asciicodes: color_attr = lambda x: '' # noqa: E731 no-lambda - return color_attr('dim') + "'" + str(self.type) + "' or '" + "None" + "' ⟶ " + color_attr('reset') + str(self.default) + return color_attr('dim') + "'" + str(self.type) + "' or '" + "None" + "' ⟶ " + color_attr('reset') + str(self.dependencies) def __str__(self): return self.str() + + +state_regex = re.compile(r"state\[\"((?:\.*\w+)+)\"\]") + + +class state_node: + def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is_ovewriting=False, missing_argument_message=None, fn=None): + self.varid = varid + self.mf = mf + self.caller_traceback = caller_traceback + self.cliargs = cliargs + self.parsefn = parsefn + self.is_ovewriting = is_ovewriting + self.cli_overwritten = False + self.missing_argument_message = missing_argument_message + + self.fn = fn + self.fn_src = getsource(self.fn) if self.fn is not None else None + if self.fn_src is not None: + if "lambda" not in self.fn_src: + raise RegisterError(f"Expected lambda expression, but found {self.fn_src}") + self.fn_src = self.fn_src.split("lambda")[1].strip().rstrip(',') + + + self.depends_on = state_regex.findall(self.fn_src) if self.fn_src else [] + self.affects = [] + + def str(self): + return str(self.varid) + + def __str__(self): + return self.str() + + def __repr__(self): + content = [self.str()] + if self.fn_src is not None: + content.append(f"fn=λ {self.fn_src}") + if len(self.depends_on) > 0: + content.append(f"depends_on={self.depends_on}") + return f"variable({', '.join(content)})" + + # ------------------- # + # dependency routines # + # ------------------- # + @staticmethod + def calculate_affects_lists(node_dict): + for node in node_dict.values(): + for depends_on_varid in node.depends_on: + node_dict[depends_on_varid].affects(node.varid) + + @staticmethod + def topological_sort(node_dict): # noqa: C901 + nodes = list(node_dict.values()) + varid2index = {node.varid: i for i, node in enumerate(nodes)} + visited = [False for i in range(len(nodes))] + sorted_nodes, cycles, unresolved = [], [], [] + + # note: rewrite this recursive DFS to while-loop if RecursionError occurs + def DFS(node_i, parentnodes=None): + if parentnodes is None: + parentnodes = [] + + node = nodes[node_i] + + if node in parentnodes: + cycles.append(parentnodes + [node]) + return + + parentnodes = parentnodes + [node] + + if visited[node_i]: + return + + visited[node_i] = True + + for dependency in nodes[node_i].depends_on: + if dependency not in node.mf.state: + unresolved.append((node, dependency)) + continue + if hasattr(node.mf.state, "fuzzy_names"): + dependency_varid = node.mf.state.fuzzy_names[dependency] + else: + dependency_varid = dependency + dependency_i = varid2index[dependency_varid] + DFS(dependency_i, parentnodes=parentnodes) + + sorted_nodes.append(node) + + for i in range(len(nodes)): + DFS(i) + + return sorted_nodes, cycles, unresolved + + @staticmethod + def evaluate(nodes, global_state): + for node in nodes: + varid = node.varid + if node.cli_overwritten or node.fn_src is None: + continue + if node.fn: + if node.fn_src.split(":")[0].strip() == "state": + global_state[varid] = node.fn(node.mf.state) + else: + global_state[varid] = node.fn() diff --git a/src/miniflask/util.py b/src/miniflask/util.py index 0955507a..35019d1c 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -47,7 +47,7 @@ def highlight_loading_module(x): return fg('light_gray') + ".".join(x[:-1]) + ("" if len(x) == 1 else ".") + attr('reset') + fg('green') + attr('bold') + x[-1] + attr('reset') -highlight_error = lambda: fg('red') + attr('bold') + "Error:" + attr('reset') + " " +highlight_error = lambda x="Error:": fg('red') + attr('bold') + x + attr('reset') + " " highlight_name = lambda x: fg('blue') + attr('bold') + x + attr('reset') highlight_module = lambda x: fg('green') + attr('bold') + str(x) + attr('reset') highlight_loading = highlight_loading_module From dd5d48e2f46adda03a462e7775ca760b45ddd913 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 18 Oct 2022 14:35:31 +0200 Subject: [PATCH 02/26] tests: added tests for lambda expressions with alternative dependencies Split from "fixup! API-change: refactor of state-dependency handling" --- .../dependencychain_module3/__init__.py | 3 +- .../lambdaarguments_module1/__init__.py | 16 ++- .../dependencies/test_state_dependencies.py | 115 +++++++++++------- 3 files changed, 80 insertions(+), 54 deletions(-) diff --git a/tests/state/dependencies/modules/dependencychain_module3/__init__.py b/tests/state/dependencies/modules/dependencychain_module3/__init__.py index 1b1d3368..c214c192 100644 --- a/tests/state/dependencies/modules/dependencychain_module3/__init__.py +++ b/tests/state/dependencies/modules/dependencychain_module3/__init__.py @@ -6,6 +6,5 @@ def register(mf): "foo2": lambda: 200, "foo3": lambda: 300, "foo4": lambda: 400, - "foo5": lambda state: 12 + state["..dependencychain_module4.foo5"], - # "foo5": lambda state: state["..dependencychain_module4.foo5"] if "..dependencychain_module4.foo5" in state else state["..dependencychain_module1.foo5"], + "foo5": lambda state: 12 * state["..dependencychain_module4.foo5"] if "..dependencychain_module4.foo5" in state else (state["..dependencychain_module1.foo3"] // 100), }) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index a4d7b522..186b9343 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -3,11 +3,17 @@ def getvalue(state): return state["42"] + +def somefunction(val): + return val * 300 + 2 + + def register(mf): mf.register_defaults({ - "foo": lambda: 42, - "bar": lambda state: state["foo"] + 1, - "foobar": lambda event: event.getvalue() + 2, - "foobar2": lambda mf: mf.event.getvalue() + 3, - "foofoobar": lambda state, event: (event.getvalue() + 2) * (state["foo"] + 1), + "var1": lambda: 42, + "var2": lambda state: state["var1"], + "var3": lambda state: state['var2'] + state['var1'], + "var4": lambda state: somefunction(state["var1"] + 1) * 5, + "var5": lambda state: somefunction(state["var1"] + 1) * 5 if "var1" in state else state["var3"], + "var6": lambda state: somefunction(state['var1'] + state["var2"]) * 5 if 'var1' in state and "var2" in state else state['var3'] + state["var4"], }) diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py index d201584f..0fefb820 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -6,10 +6,18 @@ def test_lambda_arguments(): mf = setup() mf.load("lambdaarguments_module1") - mf.parse_args([]) - with pytest.raises(RecursionError): - mf.parse_args([]) -# TODO: test + assert mf.state_registrations["modules.lambdaarguments_module1.var1"][-1].depends_on == [] + assert mf.state_registrations["modules.lambdaarguments_module1.var1"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.lambdaarguments_module1.var2"][-1].depends_on == ["var1"] + assert mf.state_registrations["modules.lambdaarguments_module1.var2"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.lambdaarguments_module1.var3"][-1].depends_on == ["var2", "var1"] + assert mf.state_registrations["modules.lambdaarguments_module1.var3"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.lambdaarguments_module1.var4"][-1].depends_on == ["var1"] + assert mf.state_registrations["modules.lambdaarguments_module1.var4"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.lambdaarguments_module1.var5"][-1].depends_on == ["var1", "var3"] + assert mf.state_registrations["modules.lambdaarguments_module1.var5"][-1].depends_alternatives == {"var1": ["var3"]} + assert mf.state_registrations["modules.lambdaarguments_module1.var6"][-1].depends_on == ["var1", "var2", "var3", "var4"] + assert mf.state_registrations["modules.lambdaarguments_module1.var6"][-1].depends_alternatives == {"var1": ["var3", "var4"], "var2": ["var3", "var4"]} def test_circular_dependency_errors(): @@ -59,11 +67,10 @@ def test_dependency_unresolved(): Error: Dependency not found! (A → B means "A depends on B") modules.unresolveddep_error_dependencynotfound.foo - → varnotfound + → varnotfound ← not found """.strip() == str(excinfo.value).strip() - def test_working_dependencies(capsys): mf = setup() mf.load("settings") @@ -80,65 +87,79 @@ def test_working_dependencies(capsys): modules.dependencychain_module1.foo2: 200 modules.dependencychain_module1.foo3: 600 modules.dependencychain_module1.foo4: 31105924806303 -modules.dependencychain_module1.foo5: 2048 +modules.dependencychain_module1.foo5: 24000 modules.dependencychain_module2.foo1: 100 modules.dependencychain_module2.foo2: 200 modules.dependencychain_module2.foo3: 300 modules.dependencychain_module2.foo4: 10368641602101 -modules.dependencychain_module2.foo5: 512 +modules.dependencychain_module2.foo5: 6000 modules.dependencychain_module3.foo1: 101 modules.dependencychain_module3.foo2: 200 modules.dependencychain_module3.foo3: 300 modules.dependencychain_module3.foo4: 400 -modules.dependencychain_module3.foo5: 512 +modules.dependencychain_module3.foo5: 6000 modules.dependencychain_module4.foo5: 500 """.lstrip() -def test_alternative_dependencies():#capsys): +def test_alternative_dependencies(capsys): mf = setup() mf.load("settings") mf.load("dependencychain_module1") mf.load("dependencychain_module2") mf.load("dependencychain_module3") mf.parse_args([]) - # captured = capsys.readouterr() + captured = capsys.readouterr() mf.event.print_all() - # captured = capsys.readouterr() -# assert captured.out == """ -# modules.dependencychain_module1.foo1: 100 -# modules.dependencychain_module1.foo2: 200 -# modules.dependencychain_module1.foo3: 600 -# modules.dependencychain_module1.foo4: 31105924806303 -# modules.dependencychain_module1.foo5: 2048 -# modules.dependencychain_module2.foo1: 100 -# modules.dependencychain_module2.foo2: 200 -# modules.dependencychain_module2.foo3: 300 -# modules.dependencychain_module2.foo4: 10368641602101 -# modules.dependencychain_module2.foo5: 512 -# modules.dependencychain_module3.foo1: 101 -# modules.dependencychain_module3.foo2: 200 -# modules.dependencychain_module3.foo3: 300 -# modules.dependencychain_module3.foo4: 400 -# modules.dependencychain_module3.foo5: 512 -# modules.dependencychain_module4.foo5: 500 -# """.lstrip() - - - - - - -# def test_state_argument_error(): -# mf = setup() -# mf.load("selfdependency_module1") -# mf.parse_args([]) -# # with pytest.raises(RecursionError): -# # mf.parse_args([]) - + captured = capsys.readouterr() + assert captured.out == """ +modules.dependencychain_module1.foo1: 100 +modules.dependencychain_module1.foo2: 200 +modules.dependencychain_module1.foo3: 600 +modules.dependencychain_module1.foo4: 31105924806303 +modules.dependencychain_module1.foo5: 24 +modules.dependencychain_module2.foo1: 100 +modules.dependencychain_module2.foo2: 200 +modules.dependencychain_module2.foo3: 300 +modules.dependencychain_module2.foo4: 10368641602101 +modules.dependencychain_module2.foo5: 6 +modules.dependencychain_module3.foo1: 101 +modules.dependencychain_module3.foo2: 200 +modules.dependencychain_module3.foo3: 300 +modules.dependencychain_module3.foo4: 400 +modules.dependencychain_module3.foo5: 6 +""".lstrip() -if __name__ == "__main__": - # test_lambda_arguments() - # test_working_dependencies() - test_alternative_dependencies() +def test_overwritten_dependencies(capsys): + mf = setup() + mf.load("settings") + mf.load("dependencychain_module1") + mf.load("dependencychain_module2") + mf.load("dependencychain_module3") + mf.parse_args([ + "--dependencychain_module1.foo1", "101", + "--dependencychain_module2.foo2", "201", + "--dependencychain_module2.foo3", "301", + "--dependencychain_module3.foo4", "401", + ]) + captured = capsys.readouterr() + mf.event.print_all() + captured = capsys.readouterr() + assert captured.out == """ +modules.dependencychain_module1.foo1: 101 +modules.dependencychain_module1.foo2: 201 +modules.dependencychain_module1.foo3: 602 +modules.dependencychain_module1.foo4: 31495718496396 +modules.dependencychain_module1.foo5: 24 +modules.dependencychain_module2.foo1: 100 +modules.dependencychain_module2.foo2: 201 +modules.dependencychain_module2.foo3: 301 +modules.dependencychain_module2.foo4: 10498572832132 +modules.dependencychain_module2.foo5: 6 +modules.dependencychain_module3.foo1: 101 +modules.dependencychain_module3.foo2: 200 +modules.dependencychain_module3.foo3: 300 +modules.dependencychain_module3.foo4: 401 +modules.dependencychain_module3.foo5: 6 +""".lstrip() From b3f5195b4f1d0dd74d68c46ee3776bdfc3bc17c5 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 18 Oct 2022 14:35:11 +0200 Subject: [PATCH 03/26] feature: alternative dependencies / lambda expressions of form expr(x) if x in state and [...] else expr --- src/miniflask/miniflask.py | 14 ++++++++- src/miniflask/state.py | 61 +++++++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 771a026f..e4d0745a 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -699,7 +699,19 @@ def register_defaults(self, defaults, scope="", overwrite=False, cliargs=True, p - One-dimensional lists of basic types (e.g. `[int]`) - One-dimensional tuples of basic types (e.g. `(int,int)`) - Lambda Expressions of the form `lambda state, event: ...`. - (As with events, lambdas can take `state`, `event`, `mf` or a combination of these arguments. Miniflask will outomatically find out what variables are required when parsing the expressions.) + - As with events, lambdas can take a `state`-argument. Miniflask will automatically find out what variables are required when parsing the expressions. + - Miniflask will also automatically detect circular dependencies and missing arguments in the variable dependency graph. + - Note that only "simple" if-statements of the following form are implemented for Lambda-Expressions. See below for examples of what is supported. + - Examples: + ``` + lambda: somefunction("arguments") + lambda state: state["myvariable"] + lambda state: (state["myvariable"] * 5) + state["othervariable"] + lambda state: somefunction(state["myvariable"]) + state["othervariable"] + lambda state: state["myvariable"] * 5 if "myvariable" in state else state["othervariable"] + lambda state: state["myvariable"] * state["othervariable"] if "myvariable" in state and "othervariable" in state else state["yetanothervariable"] + ``` + Note: This method is the base method for variable registrations. diff --git a/src/miniflask/state.py b/src/miniflask/state.py index cd8f7cfa..b989ceb4 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -380,7 +380,10 @@ def __str__(self): return self.str() -state_regex = re.compile(r"state\[\"((?:\.*\w+)+)\"\]") +state_regex = re.compile(r"state\[(\"|\')((?:\.*\w+)+)\1\]") +string_regex_g2 = r"([\"'])((?:\\\1|(?:(?!\1)).)*)(?:\1)" +if_else_regex = re.compile(r"(.*)if\s+(.*)\s+else(.*)") +state_in_regex = re.compile(string_regex_g2 + r"\s+in\s+state") class state_node: @@ -394,16 +397,45 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is self.cli_overwritten = False self.missing_argument_message = missing_argument_message + self.depends_on = [] + self.depends_alternatives = {} + self.fn = fn self.fn_src = getsource(self.fn) if self.fn is not None else None + if self.fn_src is not None: if "lambda" not in self.fn_src: raise RegisterError(f"Expected lambda expression, but found {self.fn_src}") - self.fn_src = self.fn_src.split("lambda")[1].strip().rstrip(',') - - - self.depends_on = state_regex.findall(self.fn_src) if self.fn_src else [] - self.affects = [] + fn_lambda_split = self.fn_src.split("lambda") + if len(fn_lambda_split) > 2: + raise RegisterError(f"Lambda expression is required to consist of a single lambda-keyword in that line of source, but found: {self.fn_src}") + self.fn_src = fn_lambda_split[1].strip().rstrip(',') + + # find all state-dependencies in the source code + self.depends_on = [m[1] for m in state_regex.findall(self.fn_src)] + + # we allow one simple alternative: state[x] if x in state else y + if_matches = if_else_regex.findall(self.fn_src.split(":")[1]) if self.fn_src else [] + if len(if_matches) > 1: + raise RegisterError(f"Lambda expression with only one if-else-statement of the form `EXPR(state[x]) if x in state else OTHEREXPR` allowed, but found multiple in: {self.fn_src}") + + # we know parse for lambda expressions of the form: + # expr1(x,y,...) if x in state and y in state ... else expr2 + if len(if_matches) == 1: + true_expr_src = if_matches[0][0] + false_expr_src = if_matches[0][2] + state_cond_src = if_matches[0][1] + false_expr_dependencies = [m[1] for m in state_regex.findall(false_expr_src)] + for bad_keyword in ["or", "not"]: + if f" {bad_keyword} " in state_cond_src: + raise RegisterError(f"Lambda expression allows only if-else-statements of the form `EXPR(state[x]) if x in state and ... else OTHEREXPR` allowed, but found `{bad_keyword}` in condition of: {self.fn_src}") + state_cond_vars = [m[1] for cond_src in state_cond_src.split(" and ") for m in state_in_regex.findall(cond_src)] + + # if the condition is also used in the true_expr_src we can ignore it later to check for its alternatives + for state_cond_var in state_cond_vars: + true_expr_regex = re.compile(r"state\s*\[\s*([\"'])" + state_cond_var + r"\1\s*\]") + if true_expr_regex.search(true_expr_src): + self.depends_alternatives[state_cond_var] = false_expr_dependencies def str(self): return str(self.varid) @@ -424,9 +456,11 @@ def __repr__(self): # ------------------- # @staticmethod def calculate_affects_lists(node_dict): + for node in node_dict.values(): + node.affects = [] for node in node_dict.values(): for depends_on_varid in node.depends_on: - node_dict[depends_on_varid].affects(node.varid) + node_dict[depends_on_varid].affects.append(node.varid) @staticmethod def topological_sort(node_dict): # noqa: C901 @@ -453,8 +487,19 @@ def DFS(node_i, parentnodes=None): visited[node_i] = True - for dependency in nodes[node_i].depends_on: + for dependency in node.depends_on: if dependency not in node.mf.state: + + # in case dependency has an alternative, we can ignore this if all alternatives exist + if dependency in node.depends_alternatives: + alternatives_exist = True + for alternative_dependency in node.depends_alternatives[dependency]: + if alternative_dependency not in node.mf.state: + alternatives_exist = False + break + if alternatives_exist: + continue + unresolved.append((node, dependency)) continue if hasattr(node.mf.state, "fuzzy_names"): From f659cf6c352daf06bfad215ca7cfa1545f07e742 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 17 Oct 2022 18:52:09 +0200 Subject: [PATCH 04/26] [settings]: adapted to match new dependency API --- src/miniflask/settings/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/miniflask/settings/__init__.py b/src/miniflask/settings/__init__.py index 297e98cb..1dc67e0e 100644 --- a/src/miniflask/settings/__init__.py +++ b/src/miniflask/settings/__init__.py @@ -1,6 +1,5 @@ import os import copy -from enum import EnumMeta from itertools import zip_longest from collections import deque @@ -59,12 +58,13 @@ def listsettings(mf, state, asciicodes=True): for k, v in sorted_by_state_key_modules(state.all.items()): # ignore state variables that are not registered for argument parsing - if k not in state.default: + if k not in mf.state_registrations or not mf.state_registrations[k][-1].cliargs: continue k_len = len(k) k_orig = k - overwritten = v != (state.default[k].default if hasattr(state.default[k], 'default') else state.default[k]) + v_precli = mf.state_registrations[k][-1].pre_cli_value + overwritten = v != v_precli k = k.split(".") if len(k) > 1: k_hidden = [" " * len(ki) if ki == ki2 and asciicodes else ki for ki, ki2 in zip_longest(k, last_k) if ki is not None] @@ -75,17 +75,17 @@ def listsettings(mf, state, asciicodes=True): k_hidden = k k_hidden[-1] = color_module(k_hidden[-1]) - is_lambda = callable(state.default[k_orig]) and not isinstance(state.default[k_orig], type) and not isinstance(state.default[k_orig], EnumMeta) - value_str = attr_fn('dim') + "λ ⟶ " + attr_fn('reset') + str(state.default[k_orig].default) if is_lambda else state.default[k_orig].str(asciicodes=False) if hasattr(state.default[k_orig], 'str') else str(state.default[k_orig]) + is_lambda = mf.state_registrations[k_orig][-1].fn is not None + value_str = attr_fn('dim') + "λ ⟶ " + attr_fn('reset') + str(v_precli) if is_lambda else v_precli.str(asciicodes=False) if hasattr(v_precli, 'str') else str(v_precli) append = "" if not overwritten else " ⟶ " + color_val_overwrite(str(v)) text_list.append("│".join(k_hidden) + (" " * (max_k_len - k_len)) + " = " + color_val(value_str) + append) # add definition paths if state["show_registration_definitions"]: prefix = "│".join([" " * len(k) for k in k_orig.split(".")]) + (" " * (max_k_len - k_len)) + " " - for definition_type, caller_traceback in mf._settings_parser_tracebacks[k_orig]: - summary = next(filter(lambda t: not t.filename.endswith("miniflask/miniflask.py"), reversed(caller_traceback))) - arg_err_str = (fg('blue') + "definition" if definition_type == "definition" else fg('yellow') + "overwritten") + attr('reset') + " in line %s in file '%s'." % (highlight_event(str(summary.lineno)), attr('dim') + os.path.relpath(summary.filename) + attr('reset')) + for node in mf.state_registrations[k_orig]: + summary = next(filter(lambda t: not t.filename.endswith("miniflask/miniflask.py"), reversed(node.caller_traceback))) + arg_err_str = (fg('blue') + "definition" if not node.is_ovewriting else fg('yellow') + "overwritten") + attr('reset') + " in line %s in file '%s'." % (highlight_event(str(summary.lineno)), attr('dim') + os.path.relpath(summary.filename) + attr('reset')) text_list.append(prefix + arg_err_str) return preamble_text + linesep.join(text_list) From 6a8c67f39d4cd64a82fbc937d02afff9ebcbd373 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 18 Oct 2022 15:29:10 +0200 Subject: [PATCH 05/26] tests: fixed wrong test case --- .../argparse/nested_arguments/test_nested_arguments.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/argparse/nested_arguments/test_nested_arguments.py b/tests/argparse/nested_arguments/test_nested_arguments.py index ddb26a86..cf3b7989 100644 --- a/tests/argparse/nested_arguments/test_nested_arguments.py +++ b/tests/argparse/nested_arguments/test_nested_arguments.py @@ -31,7 +31,7 @@ def test_nested_arguments_init(capsys): ├── modules.parentdir.module1 ├── modules.parentdir.module2 ╰── modules.parentdir.module3.submodule.subsubmodule -number 0 +number 9 modules.otherdir.otherdir_var 0 modules.otherdir.module2.module2_var 1 modules.otherdir.module2.submodule.submodule_var 2 @@ -66,7 +66,7 @@ def test_nested_arguments_level1_nesting(capsys): ├── modules.parentdir.module1 ├── modules.parentdir.module2 ╰── modules.parentdir.module3.submodule.subsubmodule -number 0 +number 9 modules.otherdir.otherdir_var 42 modules.otherdir.module2.module2_var 43 modules.otherdir.module2.submodule.submodule_var 44 @@ -100,7 +100,7 @@ def test_nested_arguments_level2_nesting(capsys): ├── modules.parentdir.module1 ├── modules.parentdir.module2 ╰── modules.parentdir.module3.submodule.subsubmodule -number 0 +number 9 modules.otherdir.otherdir_var 42 modules.otherdir.module2.module2_var 43 modules.otherdir.module2.submodule.submodule_var 2 @@ -145,7 +145,7 @@ def test_nested_arguments_level3_nesting(capsys): ├── modules.parentdir.module1 ├── modules.parentdir.module2 ╰── modules.parentdir.module3.submodule.subsubmodule -number 0 +number 9 modules.otherdir.otherdir_var 42 modules.otherdir.module2.module2_var 43 modules.otherdir.module2.submodule.submodule_var 2 @@ -194,7 +194,7 @@ def test_nested_arguments_level5_nesting(capsys): ├── modules.parentdir.module1 ├── modules.parentdir.module2 ╰── modules.parentdir.module3.submodule.subsubmodule -number 0 +number 9 modules.otherdir.otherdir_var 42 modules.otherdir.module2.module2_var 43 modules.otherdir.module2.submodule.submodule_var 2 From 2f5e7931415819746219f543b3142832f5ad6324 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Fri, 4 Nov 2022 14:02:29 +0100 Subject: [PATCH 06/26] Add test for state dependencies in defs. --- .../modules/defarguments_module1/.module | 0 .../modules/defarguments_module1/__init__.py | 43 +++++++++++++++++++ .../dependencies/test_state_dependencies.py | 17 ++++++++ 3 files changed, 60 insertions(+) create mode 100644 tests/state/dependencies/modules/defarguments_module1/.module create mode 100644 tests/state/dependencies/modules/defarguments_module1/__init__.py diff --git a/tests/state/dependencies/modules/defarguments_module1/.module b/tests/state/dependencies/modules/defarguments_module1/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/defarguments_module1/__init__.py b/tests/state/dependencies/modules/defarguments_module1/__init__.py new file mode 100644 index 00000000..8e801a5a --- /dev/null +++ b/tests/state/dependencies/modules/defarguments_module1/__init__.py @@ -0,0 +1,43 @@ + + +def getvalue(state): + return state["42"] + + +def somefunction(val): + return val * 300 + 2 + + +def var1_fn(state): + return 42 + + +def var2_fn(state): + return state["var1"] + + +def var3_fn(state): + return state['var2'] + state['var1'] + + +def var4_fn(state): + return somefunction(state["var1"] + 1) * 5 + + +def var5_fn(state): + return somefunction(state["var1"] + 1) * 5 if "var1" in state else state["var3"] + + +def var6_fn(state): + return somefunction(state['var1'] + state["var2"]) * 5 if 'var1' in state and "var2" in state else state['var3'] + state["var4"] + + +def register(mf): + mf.register_defaults({ + "var1": var1_fn, + "var2": var2_fn, + "var3": var3_fn, + "var4": var4_fn, + "var5": var5_fn, + "var6": var6_fn, + }) diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py index 0fefb820..92efc414 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -20,6 +20,23 @@ def test_lambda_arguments(): assert mf.state_registrations["modules.lambdaarguments_module1.var6"][-1].depends_alternatives == {"var1": ["var3", "var4"], "var2": ["var3", "var4"]} +def test_def_arguments(): + mf = setup() + mf.load("defarguments_module1") + assert mf.state_registrations["modules.defarguments_module1.var1"][-1].depends_on == [] + assert mf.state_registrations["modules.defarguments_module1.var1"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.defarguments_module1.var2"][-1].depends_on == ["var1"] + assert mf.state_registrations["modules.defarguments_module1.var2"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.defarguments_module1.var3"][-1].depends_on == ["var2", "var1"] + assert mf.state_registrations["modules.defarguments_module1.var3"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.defarguments_module1.var4"][-1].depends_on == ["var1"] + assert mf.state_registrations["modules.defarguments_module1.var4"][-1].depends_alternatives == {} + assert mf.state_registrations["modules.defarguments_module1.var5"][-1].depends_on == ["var1", "var3"] + assert mf.state_registrations["modules.defarguments_module1.var5"][-1].depends_alternatives == {"var1": ["var3"]} + assert mf.state_registrations["modules.defarguments_module1.var6"][-1].depends_on == ["var1", "var2", "var3", "var4"] + assert mf.state_registrations["modules.defarguments_module1.var6"][-1].depends_alternatives == {"var1": ["var3", "var4"], "var2": ["var3", "var4"]} + + def test_circular_dependency_errors(): mf = setup() mf.load("circulardep_error_selfdependency") From 25345929e3bc966dcca9c20294898cb416810163 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Mon, 7 Nov 2022 16:08:22 +0100 Subject: [PATCH 07/26] Fix order of deps, now always sorted. --- tests/state/dependencies/test_state_dependencies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py index 92efc414..542d8ee7 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -10,7 +10,7 @@ def test_lambda_arguments(): assert mf.state_registrations["modules.lambdaarguments_module1.var1"][-1].depends_alternatives == {} assert mf.state_registrations["modules.lambdaarguments_module1.var2"][-1].depends_on == ["var1"] assert mf.state_registrations["modules.lambdaarguments_module1.var2"][-1].depends_alternatives == {} - assert mf.state_registrations["modules.lambdaarguments_module1.var3"][-1].depends_on == ["var2", "var1"] + assert mf.state_registrations["modules.lambdaarguments_module1.var3"][-1].depends_on == ["var1", "var2"] assert mf.state_registrations["modules.lambdaarguments_module1.var3"][-1].depends_alternatives == {} assert mf.state_registrations["modules.lambdaarguments_module1.var4"][-1].depends_on == ["var1"] assert mf.state_registrations["modules.lambdaarguments_module1.var4"][-1].depends_alternatives == {} @@ -27,7 +27,7 @@ def test_def_arguments(): assert mf.state_registrations["modules.defarguments_module1.var1"][-1].depends_alternatives == {} assert mf.state_registrations["modules.defarguments_module1.var2"][-1].depends_on == ["var1"] assert mf.state_registrations["modules.defarguments_module1.var2"][-1].depends_alternatives == {} - assert mf.state_registrations["modules.defarguments_module1.var3"][-1].depends_on == ["var2", "var1"] + assert mf.state_registrations["modules.defarguments_module1.var3"][-1].depends_on == ["var1", "var2"] assert mf.state_registrations["modules.defarguments_module1.var3"][-1].depends_alternatives == {} assert mf.state_registrations["modules.defarguments_module1.var4"][-1].depends_on == ["var1"] assert mf.state_registrations["modules.defarguments_module1.var4"][-1].depends_alternatives == {} From 2c25e762d7b98dff70ed81f31ba20ddb085ba8be Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Mon, 7 Nov 2022 16:09:21 +0100 Subject: [PATCH 08/26] Implement dependency checks using AST. --- src/miniflask/state.py | 95 +++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index b989ceb4..836b3372 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -1,3 +1,4 @@ +import ast import sys import re from collections.abc import MutableMapping @@ -6,7 +7,7 @@ from colored import fg, attr from .util import get_varid_from_fuzzy, highlight_module, get_relative_id -from .exceptions import StateKeyError, RegisterError +from .exceptions import StateKeyError class temporary_state: @@ -380,13 +381,10 @@ def __str__(self): return self.str() -state_regex = re.compile(r"state\[(\"|\')((?:\.*\w+)+)\1\]") -string_regex_g2 = r"([\"'])((?:\\\1|(?:(?!\1)).)*)(?:\1)" -if_else_regex = re.compile(r"(.*)if\s+(.*)\s+else(.*)") -state_in_regex = re.compile(string_regex_g2 + r"\s+in\s+state") +class state_node: + _lambda_str_regex = re.compile(r"^{?\s*\"\w*\"\s*:\s*lambda\s*\w*:.*}?") -class state_node: def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is_ovewriting=False, missing_argument_message=None, fn=None): self.varid = varid self.mf = mf @@ -404,38 +402,30 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is self.fn_src = getsource(self.fn) if self.fn is not None else None if self.fn_src is not None: - if "lambda" not in self.fn_src: - raise RegisterError(f"Expected lambda expression, but found {self.fn_src}") - fn_lambda_split = self.fn_src.split("lambda") - if len(fn_lambda_split) > 2: - raise RegisterError(f"Lambda expression is required to consist of a single lambda-keyword in that line of source, but found: {self.fn_src}") - self.fn_src = fn_lambda_split[1].strip().rstrip(',') - - # find all state-dependencies in the source code - self.depends_on = [m[1] for m in state_regex.findall(self.fn_src)] - - # we allow one simple alternative: state[x] if x in state else y - if_matches = if_else_regex.findall(self.fn_src.split(":")[1]) if self.fn_src else [] - if len(if_matches) > 1: - raise RegisterError(f"Lambda expression with only one if-else-statement of the form `EXPR(state[x]) if x in state else OTHEREXPR` allowed, but found multiple in: {self.fn_src}") - - # we know parse for lambda expressions of the form: - # expr1(x,y,...) if x in state and y in state ... else expr2 - if len(if_matches) == 1: - true_expr_src = if_matches[0][0] - false_expr_src = if_matches[0][2] - state_cond_src = if_matches[0][1] - false_expr_dependencies = [m[1] for m in state_regex.findall(false_expr_src)] - for bad_keyword in ["or", "not"]: - if f" {bad_keyword} " in state_cond_src: - raise RegisterError(f"Lambda expression allows only if-else-statements of the form `EXPR(state[x]) if x in state and ... else OTHEREXPR` allowed, but found `{bad_keyword}` in condition of: {self.fn_src}") - state_cond_vars = [m[1] for cond_src in state_cond_src.split(" and ") for m in state_in_regex.findall(cond_src)] - - # if the condition is also used in the true_expr_src we can ignore it later to check for its alternatives - for state_cond_var in state_cond_vars: - true_expr_regex = re.compile(r"state\s*\[\s*([\"'])" + state_cond_var + r"\1\s*\]") - if true_expr_regex.search(true_expr_src): - self.depends_alternatives[state_cond_var] = false_expr_dependencies + _fn_src = self.fn_src.strip() + _ast_mode = "exec" + if self._lambda_str_regex.match(_fn_src): + # function source of a lambda looks a little different, because a whole line is returned by 'getsource' + # we assume a k, v pair is given, only missing the brackets for a valid syntax + # Note: if the lambda is not written in a standalone line, it will break the following tweak + _fn_src = "{" + _fn_src + "}" + _ast_mode = "eval" + # find first lambda of def expression + fn_lbd_node = next(iter( + node + for node in ast.walk(ast.parse(_fn_src, mode=_ast_mode)) + if isinstance(node, (ast.FunctionDef, ast.Lambda)) + )) + # get argument names for def/lambda expression + lcl_vars = [n.arg for n in fn_lbd_node.args.args] + # find all dependencies in the source code, which use local arguments + self.depends_on = self._find_var_names(fn_lbd_node, lcl_variables=lcl_vars) + # find all alternative-dependencies + for node in ast.walk(fn_lbd_node): + if isinstance(node, ast.IfExp): + rvs = self._find_var_names(node.orelse, lcl_variables=lcl_vars) + for lv in self._find_comp_names(node.test, lcl_vars): + self.depends_alternatives[lv] = rvs def str(self): return str(self.varid) @@ -451,6 +441,35 @@ def __repr__(self): content.append(f"depends_on={self.depends_on}") return f"variable({', '.join(content)})" + # ------------------- # + # AST routines # + # ------------------- # + + @staticmethod + def _find_var_names(tree: ast.AST, lcl_variables=None): + lcl_variables = set([]) if lcl_variables is None else set(lcl_variables) + return sorted([ + node.slice.value + for node in ast.walk(tree) + if hasattr(node, "value") and isinstance(node.value, ast.Name) and node.value.id in lcl_variables + ]) + + @staticmethod + def _find_comp_names(tree: ast.AST, lcl_variables): + ret = [] + for node in ast.walk(tree): + if isinstance(node, ast.Compare): + _args = [nc.id for c in node.comparators for nc in ast.walk(c) if + isinstance(nc, ast.Name) and nc.id in lcl_variables] + _names = sorted([ + node.value + for node in ast.walk(tree) + if hasattr(node, "value") and isinstance(node, ast.Constant) + ]) + if len(_args): + ret += _names + return ret + # ------------------- # # dependency routines # # ------------------- # From 42fc7800897f6e4935f88cd74f244fb1ad49d444 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Mon, 7 Nov 2022 16:59:05 +0100 Subject: [PATCH 09/26] Remove unused argument 'state'. --- .../state/dependencies/modules/defarguments_module1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/state/dependencies/modules/defarguments_module1/__init__.py b/tests/state/dependencies/modules/defarguments_module1/__init__.py index 8e801a5a..a816a0fa 100644 --- a/tests/state/dependencies/modules/defarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/defarguments_module1/__init__.py @@ -8,7 +8,7 @@ def somefunction(val): return val * 300 + 2 -def var1_fn(state): +def var1_fn(): return 42 From 4cf054e9e8e47cc25d0759934609d5019ebeac66 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Fri, 18 Nov 2022 13:14:19 +0100 Subject: [PATCH 10/26] Inspect register() call to maybe get more insights. --- src/miniflask/state.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 836b3372..5ffa9f5f 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -2,6 +2,7 @@ import sys import re from collections.abc import MutableMapping +import inspect from inspect import getsource from colored import fg, attr @@ -401,6 +402,12 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is self.fn = fn self.fn_src = getsource(self.fn) if self.fn is not None else None + # TODO: inspect register() call to maybe get more insights. + frame = inspect.currentframe() + frame = frame.f_back.f_back.f_back # go up one in the stack + register_defaults_src = inspect.getsource(frame) + register_defaults_ast = ast.parse(register_defaults_src, mode="exec") + if self.fn_src is not None: _fn_src = self.fn_src.strip() _ast_mode = "exec" From 78352046e84ca53a45b8839901fd6946632ecc4d Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Fri, 18 Nov 2022 13:14:24 +0100 Subject: [PATCH 11/26] Add more test cases. --- .../lambdaarguments_module1/__init__.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 186b9343..80e8b774 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -4,16 +4,36 @@ def getvalue(state): return state["42"] -def somefunction(val): +def some_function(val): return val * 300 + 2 +class TestClass: + + def __init__(self, state): + self.state = state + + +lambda_definition = lambda: 42 # noqa + + +lambda_tuple_definition = lambda: 42, lambda: 43 # noqa + + def register(mf): mf.register_defaults({ + "test_multiple_inline_I": lambda: 1 * 42, "test_multiple_inline_II": lambda: 2 * 42, + "test_lambda_def": lambda_definition, + "test_class": TestClass, + "test_lambda_tuple": lambda_tuple_definition[1], + "test_line_breaks": lambda state: state[ + "var1" + ], + "test_not_cmp": lambda state: some_function(state["var1"] + 1) * 5 if "var3" not in state and "var3" not in state else state["var3"], "var1": lambda: 42, "var2": lambda state: state["var1"], "var3": lambda state: state['var2'] + state['var1'], - "var4": lambda state: somefunction(state["var1"] + 1) * 5, - "var5": lambda state: somefunction(state["var1"] + 1) * 5 if "var1" in state else state["var3"], - "var6": lambda state: somefunction(state['var1'] + state["var2"]) * 5 if 'var1' in state and "var2" in state else state['var3'] + state["var4"], + "var4": lambda state: some_function(state["var1"] + 1) * 5, + "var5": lambda state: some_function(state["var1"] + 1) * 5 if "var1" in state else state["var3"], + "var6": lambda state: some_function(state['var1'] + state["var2"]) * 5 if 'var1' in state and "var2" in state else state['var3'] + state["var4"], }) From be63d5125bac1d0db0fa563b00f1862982062e1c Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:34:00 +0100 Subject: [PATCH 12/26] Remove 'meta' inspection. --- src/miniflask/state.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 5ffa9f5f..de6ee2e9 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -402,12 +402,6 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is self.fn = fn self.fn_src = getsource(self.fn) if self.fn is not None else None - # TODO: inspect register() call to maybe get more insights. - frame = inspect.currentframe() - frame = frame.f_back.f_back.f_back # go up one in the stack - register_defaults_src = inspect.getsource(frame) - register_defaults_ast = ast.parse(register_defaults_src, mode="exec") - if self.fn_src is not None: _fn_src = self.fn_src.strip() _ast_mode = "exec" From fadd479ca65a3f7098ccc879bff7d2e758c1db47 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:34:09 +0100 Subject: [PATCH 13/26] Ensure single expression is found in source. --- src/miniflask/state.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index de6ee2e9..f1264c00 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -411,12 +411,18 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is # Note: if the lambda is not written in a standalone line, it will break the following tweak _fn_src = "{" + _fn_src + "}" _ast_mode = "eval" - # find first lambda of def expression - fn_lbd_node = next(iter( + # find lambda or def expression + fn_lbd_iter = list(iter( node for node in ast.walk(ast.parse(_fn_src, mode=_ast_mode)) if isinstance(node, (ast.FunctionDef, ast.Lambda)) )) + # check if exactly one expression is found + if len(fn_lbd_iter) == 0: + raise RuntimeError(f"Exactly one expression needs to be defined, found {len(fn_lbd_iter)}.") + if len(fn_lbd_iter) > 1: + raise RuntimeError(f"Only one expression is allowed per line, found {len(fn_lbd_iter)}.") + fn_lbd_node = fn_lbd_iter[0] # get argument names for def/lambda expression lcl_vars = [n.arg for n in fn_lbd_node.args.args] # find all dependencies in the source code, which use local arguments From 8d05d23e5c2dd86a3bc3bd43c818030dc6ee12f7 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:41:29 +0100 Subject: [PATCH 14/26] Define and use private members. --- src/miniflask/state.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index f1264c00..f611d722 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -385,6 +385,9 @@ def __str__(self): class state_node: _lambda_str_regex = re.compile(r"^{?\s*\"\w*\"\s*:\s*lambda\s*\w*:.*}?") + local_arguments = [] + depends_on = [] + depends_alternatives = [] def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is_ovewriting=False, missing_argument_message=None, fn=None): self.varid = varid @@ -424,14 +427,14 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is raise RuntimeError(f"Only one expression is allowed per line, found {len(fn_lbd_iter)}.") fn_lbd_node = fn_lbd_iter[0] # get argument names for def/lambda expression - lcl_vars = [n.arg for n in fn_lbd_node.args.args] + self.local_arguments = [n.arg for n in fn_lbd_node.args.args] # find all dependencies in the source code, which use local arguments - self.depends_on = self._find_var_names(fn_lbd_node, lcl_variables=lcl_vars) + self.depends_on = self._find_var_names(fn_lbd_node, lcl_variables=self.local_arguments) # find all alternative-dependencies for node in ast.walk(fn_lbd_node): if isinstance(node, ast.IfExp): - rvs = self._find_var_names(node.orelse, lcl_variables=lcl_vars) - for lv in self._find_comp_names(node.test, lcl_vars): + rvs = self._find_var_names(node.orelse, lcl_variables=self.local_arguments) + for lv in self._find_comp_names(node.test, self.local_arguments): self.depends_alternatives[lv] = rvs def str(self): From be25a90490963c219f92862beb9dbf3d8273db40 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:42:03 +0100 Subject: [PATCH 15/26] Fix string search for 'state' argument. --- src/miniflask/state.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index f611d722..4af0c04d 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -548,11 +548,12 @@ def DFS(node_i, parentnodes=None): @staticmethod def evaluate(nodes, global_state): for node in nodes: - varid = node.varid if node.cli_overwritten or node.fn_src is None: continue if node.fn: - if node.fn_src.split(":")[0].strip() == "state": - global_state[varid] = node.fn(node.mf.state) + # Note: This is very precise, we could also assume the first + # arguments needs to be 'state', regardless of its name + if "state" in node.local_arguments: + global_state[node.varid] = node.fn(node.mf.state) else: - global_state[varid] = node.fn() + global_state[node.varid] = node.fn() From dd2acd34d0628db5d74ae6a8628e79e8200c263b Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:42:21 +0100 Subject: [PATCH 16/26] Remove not allowed statements. --- .../modules/lambdaarguments_module1/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 80e8b774..c465d3c5 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -17,15 +17,12 @@ def __init__(self, state): lambda_definition = lambda: 42 # noqa -lambda_tuple_definition = lambda: 42, lambda: 43 # noqa - - def register(mf): mf.register_defaults({ - "test_multiple_inline_I": lambda: 1 * 42, "test_multiple_inline_II": lambda: 2 * 42, + "test_multiple_inline_I": lambda: 1 * 42, + "test_multiple_inline_II": lambda: 2 * 42, "test_lambda_def": lambda_definition, "test_class": TestClass, - "test_lambda_tuple": lambda_tuple_definition[1], "test_line_breaks": lambda state: state[ "var1" ], From cb8108482e5b4b94f48b85db193e3f74ed793aa4 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:42:36 +0100 Subject: [PATCH 17/26] Add test for failing stuff. --- .../modules/lambdaarguments_module2/.module | 0 .../modules/lambdaarguments_module2/__init__.py | 4 ++++ .../modules/lambdaarguments_module3/.module | 0 .../modules/lambdaarguments_module3/__init__.py | 7 +++++++ .../state/dependencies/test_state_dependencies.py | 14 +++++++++++++- 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/state/dependencies/modules/lambdaarguments_module2/.module create mode 100644 tests/state/dependencies/modules/lambdaarguments_module2/__init__.py create mode 100644 tests/state/dependencies/modules/lambdaarguments_module3/.module create mode 100644 tests/state/dependencies/modules/lambdaarguments_module3/__init__.py diff --git a/tests/state/dependencies/modules/lambdaarguments_module2/.module b/tests/state/dependencies/modules/lambdaarguments_module2/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/lambdaarguments_module2/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module2/__init__.py new file mode 100644 index 00000000..14d7e943 --- /dev/null +++ b/tests/state/dependencies/modules/lambdaarguments_module2/__init__.py @@ -0,0 +1,4 @@ +def register(mf): + mf.register_defaults({ + "test_multiple_inline_I": lambda: 1 * 42, "test_multiple_inline_II": lambda: 2 * 42, + }) diff --git a/tests/state/dependencies/modules/lambdaarguments_module3/.module b/tests/state/dependencies/modules/lambdaarguments_module3/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/lambdaarguments_module3/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module3/__init__.py new file mode 100644 index 00000000..30079d69 --- /dev/null +++ b/tests/state/dependencies/modules/lambdaarguments_module3/__init__.py @@ -0,0 +1,7 @@ +lambda_tuple_definition = lambda: 42, lambda: 43 # noqa + + +def register(mf): + mf.register_defaults({ + "test_lambda_tuple": lambda_tuple_definition[1], + }) diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py index 542d8ee7..ef05f5aa 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -3,7 +3,7 @@ from miniflask.exceptions import RegisterError -def test_lambda_arguments(): +def test_lambda_arguments_1(): mf = setup() mf.load("lambdaarguments_module1") assert mf.state_registrations["modules.lambdaarguments_module1.var1"][-1].depends_on == [] @@ -20,6 +20,18 @@ def test_lambda_arguments(): assert mf.state_registrations["modules.lambdaarguments_module1.var6"][-1].depends_alternatives == {"var1": ["var3", "var4"], "var2": ["var3", "var4"]} +def test_lambda_arguments_2(): + mf = setup() + with pytest.raises(Exception): + mf.load("lambdaarguments_module2") + + +def test_lambda_arguments_3(): + mf = setup() + with pytest.raises(Exception): + mf.load("lambdaarguments_module3") + + def test_def_arguments(): mf = setup() mf.load("defarguments_module1") From 72d6326ae9197afd8a15c20d4d021790eda9bc8c Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:43:44 +0100 Subject: [PATCH 18/26] Remove unused import. --- src/miniflask/state.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 4af0c04d..a6508697 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -2,7 +2,6 @@ import sys import re from collections.abc import MutableMapping -import inspect from inspect import getsource from colored import fg, attr From 39904e7e861a67ac635fa3b0121609f81847df16 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:44:01 +0100 Subject: [PATCH 19/26] Remove empty lines. --- .../dependencies/modules/lambdaarguments_module1/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index c465d3c5..88e8c683 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -1,5 +1,3 @@ - - def getvalue(state): return state["42"] From cefcf742c10d97927c856f80665d5f897f0ec55f Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:46:37 +0100 Subject: [PATCH 20/26] Add noqa to class. --- .../dependencies/modules/lambdaarguments_module1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 88e8c683..6fe8c593 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -6,7 +6,7 @@ def some_function(val): return val * 300 + 2 -class TestClass: +class TestClass: # noqa def __init__(self, state): self.state = state From 32797a7727c377c590bef6a2a1fcf03034fd3193 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:48:24 +0100 Subject: [PATCH 21/26] Add concurrency and cancel in progress workflows, if updated commit is pushed. --- .github/workflows/linting-python.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/linting-python.yml b/.github/workflows/linting-python.yml index 05fbab46..e59e5805 100644 --- a/.github/workflows/linting-python.yml +++ b/.github/workflows/linting-python.yml @@ -1,5 +1,9 @@ name: Linting Python +concurrency: + group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.type }} + cancel-in-progress: true + on: [push] jobs: From 2269ba514cebda34877e97defeccc8df30827de4 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:48:54 +0100 Subject: [PATCH 22/26] Spam noqa. --- .../dependencies/modules/lambdaarguments_module1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 6fe8c593..3e1ffabf 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -8,7 +8,7 @@ def some_function(val): class TestClass: # noqa - def __init__(self, state): + def __init__(self, state): # noqa self.state = state From cd6263d71c5019785c300ba8fd145c4af5c7737e Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:51:50 +0100 Subject: [PATCH 23/26] fjgruiewhjfiogbhpa --- .flake8 | 1 + .../dependencies/modules/lambdaarguments_module1/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 54b2c646..e01d744c 100644 --- a/.flake8 +++ b/.flake8 @@ -17,3 +17,4 @@ per-file-ignores = # C901 loop to complex # R1702 too many nested blocks src/miniflask/__init__.py: E221,F401,F403 + tests/state/dependencies/modules/lambdaarguments_module1/__init__.py: R0903 diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 3e1ffabf..88e8c683 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -6,9 +6,9 @@ def some_function(val): return val * 300 + 2 -class TestClass: # noqa +class TestClass: - def __init__(self, state): # noqa + def __init__(self, state): self.state = state From 81baeaa9664addec449590862e470dc389f98790 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:55:15 +0100 Subject: [PATCH 24/26] fjgruiewhjfiogbhpa --- .flake8 | 1 - .../dependencies/modules/lambdaarguments_module1/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index e01d744c..54b2c646 100644 --- a/.flake8 +++ b/.flake8 @@ -17,4 +17,3 @@ per-file-ignores = # C901 loop to complex # R1702 too many nested blocks src/miniflask/__init__.py: E221,F401,F403 - tests/state/dependencies/modules/lambdaarguments_module1/__init__.py: R0903 diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 88e8c683..7af07bd6 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -6,7 +6,7 @@ def some_function(val): return val * 300 + 2 -class TestClass: +class TestClass: # pylint: disable=too-few-public-methods def __init__(self, state): self.state = state From abaf47f902b261a33c8f6218e6960c3f7a37fc1b Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:57:08 +0100 Subject: [PATCH 25/26] Disable more warnings. --- .../dependencies/modules/lambdaarguments_module1/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 7af07bd6..25bbd7f5 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -12,7 +12,7 @@ def __init__(self, state): self.state = state -lambda_definition = lambda: 42 # noqa +lambda_definition = lambda: 42 # noqa # pylint: disable=unnecessary-lambda-assignment def register(mf): From 21e90c83565588e0247df2c19a33e33d97684454 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Thu, 15 Dec 2022 09:44:40 +0100 Subject: [PATCH 26/26] Fix typo. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e593a94e..2b6ca446 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def read(*parts): """ - Build an absolute path from *parts* and and return the contents of the + Build an absolute path from *parts* and return the contents of the resulting file. Assume UTF-8 encoding. """ with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: