From aabc39c402a3e42d22810499326fadd380e6a0c5 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Wed, 5 Oct 2022 17:56:58 +0200 Subject: [PATCH 01/45] tests: renamed like_expressions -> dependencies --- .../state/{like_expressions => dependencies}/modules/__init__.py | 0 .../modules/actual_data/.module | 0 .../modules/actual_data/__init__.py | 0 .../modules/actual_data_lambda/.module | 0 .../modules/actual_data_lambda/__init__.py | 0 .../modules/circularlike/.module | 0 .../modules/circularlike/__init__.py | 0 .../modules/dependency_one/.module | 0 .../modules/dependency_one/__init__.py | 0 .../modules/dependency_two/.module | 0 .../modules/dependency_two/__init__.py | 0 .../modules/dependency_two_lambda/.module | 0 .../modules/dependency_two_lambda/__init__.py | 0 .../test_circular_definition.py | 0 .../{like_expressions => dependencies}/test_dependency_lambda.py | 0 .../{like_expressions => dependencies}/test_dependency_like.py | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename tests/state/{like_expressions => dependencies}/modules/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/modules/actual_data/.module (100%) rename tests/state/{like_expressions => dependencies}/modules/actual_data/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/modules/actual_data_lambda/.module (100%) rename tests/state/{like_expressions => dependencies}/modules/actual_data_lambda/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/modules/circularlike/.module (100%) rename tests/state/{like_expressions => dependencies}/modules/circularlike/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/modules/dependency_one/.module (100%) rename tests/state/{like_expressions => dependencies}/modules/dependency_one/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/modules/dependency_two/.module (100%) rename tests/state/{like_expressions => dependencies}/modules/dependency_two/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/modules/dependency_two_lambda/.module (100%) rename tests/state/{like_expressions => dependencies}/modules/dependency_two_lambda/__init__.py (100%) rename tests/state/{like_expressions => dependencies}/test_circular_definition.py (100%) rename tests/state/{like_expressions => dependencies}/test_dependency_lambda.py (100%) rename tests/state/{like_expressions => dependencies}/test_dependency_like.py (100%) diff --git a/tests/state/like_expressions/modules/__init__.py b/tests/state/dependencies/modules/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/__init__.py rename to tests/state/dependencies/modules/__init__.py diff --git a/tests/state/like_expressions/modules/actual_data/.module b/tests/state/dependencies/modules/actual_data/.module similarity index 100% rename from tests/state/like_expressions/modules/actual_data/.module rename to tests/state/dependencies/modules/actual_data/.module diff --git a/tests/state/like_expressions/modules/actual_data/__init__.py b/tests/state/dependencies/modules/actual_data/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/actual_data/__init__.py rename to tests/state/dependencies/modules/actual_data/__init__.py diff --git a/tests/state/like_expressions/modules/actual_data_lambda/.module b/tests/state/dependencies/modules/actual_data_lambda/.module similarity index 100% rename from tests/state/like_expressions/modules/actual_data_lambda/.module rename to tests/state/dependencies/modules/actual_data_lambda/.module diff --git a/tests/state/like_expressions/modules/actual_data_lambda/__init__.py b/tests/state/dependencies/modules/actual_data_lambda/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/actual_data_lambda/__init__.py rename to tests/state/dependencies/modules/actual_data_lambda/__init__.py diff --git a/tests/state/like_expressions/modules/circularlike/.module b/tests/state/dependencies/modules/circularlike/.module similarity index 100% rename from tests/state/like_expressions/modules/circularlike/.module rename to tests/state/dependencies/modules/circularlike/.module diff --git a/tests/state/like_expressions/modules/circularlike/__init__.py b/tests/state/dependencies/modules/circularlike/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/circularlike/__init__.py rename to tests/state/dependencies/modules/circularlike/__init__.py diff --git a/tests/state/like_expressions/modules/dependency_one/.module b/tests/state/dependencies/modules/dependency_one/.module similarity index 100% rename from tests/state/like_expressions/modules/dependency_one/.module rename to tests/state/dependencies/modules/dependency_one/.module diff --git a/tests/state/like_expressions/modules/dependency_one/__init__.py b/tests/state/dependencies/modules/dependency_one/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/dependency_one/__init__.py rename to tests/state/dependencies/modules/dependency_one/__init__.py diff --git a/tests/state/like_expressions/modules/dependency_two/.module b/tests/state/dependencies/modules/dependency_two/.module similarity index 100% rename from tests/state/like_expressions/modules/dependency_two/.module rename to tests/state/dependencies/modules/dependency_two/.module diff --git a/tests/state/like_expressions/modules/dependency_two/__init__.py b/tests/state/dependencies/modules/dependency_two/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/dependency_two/__init__.py rename to tests/state/dependencies/modules/dependency_two/__init__.py diff --git a/tests/state/like_expressions/modules/dependency_two_lambda/.module b/tests/state/dependencies/modules/dependency_two_lambda/.module similarity index 100% rename from tests/state/like_expressions/modules/dependency_two_lambda/.module rename to tests/state/dependencies/modules/dependency_two_lambda/.module diff --git a/tests/state/like_expressions/modules/dependency_two_lambda/__init__.py b/tests/state/dependencies/modules/dependency_two_lambda/__init__.py similarity index 100% rename from tests/state/like_expressions/modules/dependency_two_lambda/__init__.py rename to tests/state/dependencies/modules/dependency_two_lambda/__init__.py diff --git a/tests/state/like_expressions/test_circular_definition.py b/tests/state/dependencies/test_circular_definition.py similarity index 100% rename from tests/state/like_expressions/test_circular_definition.py rename to tests/state/dependencies/test_circular_definition.py diff --git a/tests/state/like_expressions/test_dependency_lambda.py b/tests/state/dependencies/test_dependency_lambda.py similarity index 100% rename from tests/state/like_expressions/test_dependency_lambda.py rename to tests/state/dependencies/test_dependency_lambda.py diff --git a/tests/state/like_expressions/test_dependency_like.py b/tests/state/dependencies/test_dependency_like.py similarity index 100% rename from tests/state/like_expressions/test_dependency_like.py rename to tests/state/dependencies/test_dependency_like.py From 1987517342b8bf250530abb47e22a36540d17a01 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 10 Oct 2022 16:28:46 +0200 Subject: [PATCH 02/45] remove mf.like from api & tests --- src/miniflask/__init__.py | 2 +- src/miniflask/miniflask.py | 42 +------------------ src/miniflask/settings/__init__.py | 3 +- src/miniflask/state.py | 21 ---------- .../dependencies/modules/actual_data/.module | 0 .../modules/actual_data/__init__.py | 18 -------- .../modules/circularlike/__init__.py | 3 +- .../modules/dependency_two/.module | 0 .../modules/dependency_two/__init__.py | 9 ---- .../dependencies/test_dependency_like.py | 22 ---------- 10 files changed, 5 insertions(+), 115 deletions(-) delete mode 100644 tests/state/dependencies/modules/actual_data/.module delete mode 100644 tests/state/dependencies/modules/actual_data/__init__.py delete mode 100644 tests/state/dependencies/modules/dependency_two/.module delete mode 100644 tests/state/dependencies/modules/dependency_two/__init__.py delete mode 100644 tests/state/dependencies/test_dependency_like.py diff --git a/src/miniflask/__init__.py b/src/miniflask/__init__.py index b6b797b1..5fc3551b 100644 --- a/src/miniflask/__init__.py +++ b/src/miniflask/__init__.py @@ -1,7 +1,7 @@ from .miniflask import miniflask as init, print_info, get_default_args from .event import outervar from .modules import * -from .state import like, optional +from .state import optional # meta __version__ = "4.3.2" diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index ea32c1ac..ea6da6c3 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, like, as_is_callable, optional as optional_default +from .state import state, as_is_callable, optional as optional_default 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 @@ -701,9 +701,8 @@ def register_defaults(self, defaults, scope="", overwrite=False, cliargs=True, p - Enums (`Enum`) - One-dimensional lists of basic types (e.g. `[int]`) - One-dimensional tuples of basic types (e.g. `(int,int)`) - - [Like Expressions](../../08-API/03-register(mf\)-Object/03-like.md) - Lambda Expressions of the form `lambda state, event: ...`. - (Functionally, this is just like a `like`-Expression, but cannot take circular dependencies into account. The pro-side, however, is that any expression can be used inside the lambda-function.) + (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.) Note: This method is the base method for variable registrations. @@ -1352,43 +1351,6 @@ def optional(self, variable_type): """ # noqa: W291 return optional_default(variable_type) - # like with relative imports - def like(self, varname, alt, scope="."): - r""" - Define state dependencies. - - # Note {.alert} - Like variables are parsed *after* the CLI-arguments have been parsed: - - In case any CLI-argument changes a dependency the like-Dependency should change accordingly. - - If any CLI-argument changes the variable defined as a dependency, the dependency is canceled. - - Args: - - `varname`: The variable identifier to use for the dependency. - - `alt`: The default value if no variable found. - - `scope`: The variable scope to search for the variable. - - Examples: - - **Global variables with Fallback** - ```python - mf.register_defaults({ - "myvar": mf.like("globalvar", alt=42) - }) - ``` - - **Local dependencies** - ```python - mf.register_defaults({ - "othervar": mf.like(".var"), - "likeparent": mf.like("..parentvar") - }) - ``` - """ # noqa: W291 - scope_name = scope - if scope is not None: - scope, _ = self._get_relative_module_id(scope) - return like(varname, alt, scope=scope, scope_name=scope_name) - def as_is_callable(self, variable): r""" Wrap variables for register_-calls to ensure they are not parsed during initialization. diff --git a/src/miniflask/settings/__init__.py b/src/miniflask/settings/__init__.py index eb01c0f1..297e98cb 100644 --- a/src/miniflask/settings/__init__.py +++ b/src/miniflask/settings/__init__.py @@ -7,7 +7,6 @@ from colored import attr, fg from ..util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite, highlight_event -from ..state import like html_module = lambda x: x # noqa: E731 no-lambda @@ -76,7 +75,7 @@ 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) and not isinstance(state.default[k_orig], like) + 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]) 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) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 242855c7..95103706 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -355,27 +355,6 @@ def _create_excpetion_notunique(found_varids, name): return StateKeyError("Variable-Identifier '%s' is not unique. Found %i variables:\n\t%s\n\n Call:\n %s" % (highlight_module(name), len(found_varids), "\n\t".join(found_varids), name)) -class like: - def __init__(self, varname, alt, scope=None, scope_name=None): - if scope_name is None: - scope_name = scope - global_varname = varname if scope is None else scope + "." + varname - self.varname = scope_name + "." + varname if scope_name is not None else varname - self.alt = alt - self.fn = lambda state, event: state[global_varname] if global_varname in state else alt # noqa: E731 no-lambda - - def __call__(self, state, event): # pylint: disable=redefined-outer-name - return self.fn(state, event) - - 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.varname) + "' or '" + str(self.alt) + "' ⟶ " + color_attr('reset') + str(self.default) - - def __str__(self): - return self.str() - - class as_is_callable(): # pylint: disable=too-few-public-methods def __init__(self, obj): self.obj = obj diff --git a/tests/state/dependencies/modules/actual_data/.module b/tests/state/dependencies/modules/actual_data/.module deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/state/dependencies/modules/actual_data/__init__.py b/tests/state/dependencies/modules/actual_data/__init__.py deleted file mode 100644 index aa6d138a..00000000 --- a/tests/state/dependencies/modules/actual_data/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from miniflask import like - - -def main(state): - print("foo:", state["foo"]) - print("foo2:", state["foo2"]) - print("foo3:", state["foo3"]) - print("foo4:", state["foo4"]) - - -def register(mf): - mf.register_defaults({ - "foo": like("bar", 0), - "foo2": like("bar2", 1), - "foo3": like("bar3", 1), - "foo4": like("bar4", 4) - }) - mf.register_event('main', main, unique=False) diff --git a/tests/state/dependencies/modules/circularlike/__init__.py b/tests/state/dependencies/modules/circularlike/__init__.py index bb05b302..e8200517 100644 --- a/tests/state/dependencies/modules/circularlike/__init__.py +++ b/tests/state/dependencies/modules/circularlike/__init__.py @@ -1,4 +1,3 @@ -from miniflask import like def main(): @@ -7,6 +6,6 @@ def main(): def register(mf): mf.register_defaults({ - "foobar": like("foobar", 0) + "foobar": lambda state, event: state["foobar"] }) mf.register_event('main', main, unique=False) diff --git a/tests/state/dependencies/modules/dependency_two/.module b/tests/state/dependencies/modules/dependency_two/.module deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/state/dependencies/modules/dependency_two/__init__.py b/tests/state/dependencies/modules/dependency_two/__init__.py deleted file mode 100644 index 69ef41a0..00000000 --- a/tests/state/dependencies/modules/dependency_two/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from miniflask import like - - -def register(mf): - mf.register_defaults({ - "bar": 11, - "bar2": like("foobar2", 12), - "bar3": like("foobar3", 13) - }) diff --git a/tests/state/dependencies/test_dependency_like.py b/tests/state/dependencies/test_dependency_like.py deleted file mode 100644 index 411cba0d..00000000 --- a/tests/state/dependencies/test_dependency_like.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path -import miniflask # noqa: E402 - - -mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), - debug=True -) - - -def test_like_depedencies(capsys): - mf.load(["actual_data", "dependency_one", "dependency_two"]) - mf.parse_args([]) - captured = capsys.readouterr() - mf.event.main() - captured = capsys.readouterr() - assert captured.out == """ -foo: 11 -foo2: 112 -foo3: 13 -foo4: 4 -""".lstrip() From 3c6ee3ac50149a550197c61b4db7887c6b14aaa9 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Wed, 12 Oct 2022 14:09:31 +0200 Subject: [PATCH 03/45] typo fix in comments --- src/miniflask/miniflask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index ea6da6c3..8f79ae1c 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -773,7 +773,7 @@ class TESTENUM(Enum): if isinstance(val, as_is_callable): parsefn = False - # actual initialization is done when all modules has been parsed + # actual initialization is done when all modules have been parsed if overwrite: self._settings_parse_later_overwrites_list.append((varname, val, cliargs, parsefn, caller_traceback, self)) else: From c870cd2eecda08d4df1692596b86ad1decf6ce51 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Wed, 12 Oct 2022 14:10:17 +0200 Subject: [PATCH 04/45] tests: rewrite of state_dependencies test cases --- .../modules/actual_data_lambda/__init__.py | 16 -- .../.module | 0 .../circulardep_error_module1/__init__.py | 7 + .../.module | 0 .../circulardep_error_module2/__init__.py | 8 + .../.module | 0 .../circulardep_error_module3/__init__.py | 8 + .../.module | 0 .../__init__.py | 10 ++ .../modules/circularlike/__init__.py | 11 -- .../modules/dependency_two_lambda/__init__.py | 7 - .../modules/dependencychain_module1/.module | 0 .../dependencychain_module1/__init__.py | 18 +++ .../modules/dependencychain_module2/.module | 0 .../dependencychain_module2/__init__.py | 14 ++ .../modules/dependencychain_module3/.module | 0 .../dependencychain_module3/__init__.py | 11 ++ .../modules/dependencychain_module4/.module | 0 .../__init__.py | 4 +- .../modules/lambdaarguments_module1/.module | 0 .../lambdaarguments_module1/__init__.py | 13 ++ .../.module | 0 .../__init__.py | 5 + tests/state/dependencies/setup.py | 16 ++ .../dependencies/test_circular_definition.py | 15 -- .../dependencies/test_dependency_lambda.py | 22 --- .../dependencies/test_state_dependencies.py | 144 ++++++++++++++++++ 27 files changed, 256 insertions(+), 73 deletions(-) delete mode 100644 tests/state/dependencies/modules/actual_data_lambda/__init__.py rename tests/state/dependencies/modules/{actual_data_lambda => circulardep_error_module1}/.module (100%) create mode 100644 tests/state/dependencies/modules/circulardep_error_module1/__init__.py rename tests/state/dependencies/modules/{circularlike => circulardep_error_module2}/.module (100%) create mode 100644 tests/state/dependencies/modules/circulardep_error_module2/__init__.py rename tests/state/dependencies/modules/{dependency_one => circulardep_error_module3}/.module (100%) create mode 100644 tests/state/dependencies/modules/circulardep_error_module3/__init__.py rename tests/state/dependencies/modules/{dependency_two_lambda => circulardep_error_selfdependency}/.module (100%) create mode 100644 tests/state/dependencies/modules/circulardep_error_selfdependency/__init__.py delete mode 100644 tests/state/dependencies/modules/circularlike/__init__.py delete mode 100644 tests/state/dependencies/modules/dependency_two_lambda/__init__.py create mode 100644 tests/state/dependencies/modules/dependencychain_module1/.module create mode 100644 tests/state/dependencies/modules/dependencychain_module1/__init__.py create mode 100644 tests/state/dependencies/modules/dependencychain_module2/.module create mode 100644 tests/state/dependencies/modules/dependencychain_module2/__init__.py create mode 100644 tests/state/dependencies/modules/dependencychain_module3/.module create mode 100644 tests/state/dependencies/modules/dependencychain_module3/__init__.py create mode 100644 tests/state/dependencies/modules/dependencychain_module4/.module rename tests/state/dependencies/modules/{dependency_one => dependencychain_module4}/__init__.py (53%) create mode 100644 tests/state/dependencies/modules/lambdaarguments_module1/.module create mode 100644 tests/state/dependencies/modules/lambdaarguments_module1/__init__.py create mode 100644 tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/.module create mode 100644 tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/__init__.py create mode 100644 tests/state/dependencies/setup.py delete mode 100644 tests/state/dependencies/test_circular_definition.py delete mode 100644 tests/state/dependencies/test_dependency_lambda.py create mode 100644 tests/state/dependencies/test_state_dependencies.py diff --git a/tests/state/dependencies/modules/actual_data_lambda/__init__.py b/tests/state/dependencies/modules/actual_data_lambda/__init__.py deleted file mode 100644 index 65aea5b5..00000000 --- a/tests/state/dependencies/modules/actual_data_lambda/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ - -def main(state): - print("foo:", state["foo"]) - print("foo2:", state["foo2"]) - print("foo3:", state["foo3"]) - print("foo4:", state["foo4"]) - - -def register(mf): - mf.register_defaults({ - "foo": lambda state, event: state["bar"] if "bar" in state else 0, - "foo2": lambda state, event: state["bar2"] if "bar2" in state else 1, - "foo3": lambda state, event: state["bar3"] if "bar3" in state else 1, - "foo4": lambda state, event: state["bar4"] if "bar4" in state else 4, - }) - mf.register_event('main', main, unique=False) diff --git a/tests/state/dependencies/modules/actual_data_lambda/.module b/tests/state/dependencies/modules/circulardep_error_module1/.module similarity index 100% rename from tests/state/dependencies/modules/actual_data_lambda/.module rename to tests/state/dependencies/modules/circulardep_error_module1/.module diff --git a/tests/state/dependencies/modules/circulardep_error_module1/__init__.py b/tests/state/dependencies/modules/circulardep_error_module1/__init__.py new file mode 100644 index 00000000..f3bf001b --- /dev/null +++ b/tests/state/dependencies/modules/circulardep_error_module1/__init__.py @@ -0,0 +1,7 @@ + +def register(mf): + mf.register_defaults({ + "foo": lambda state: state["..circulardep_error_module2.foo"], + "bar": lambda state: state["..circulardep_error_module2.bar"], + "bar2": lambda state: state["..circulardep_error_module3.bar2"] + }) diff --git a/tests/state/dependencies/modules/circularlike/.module b/tests/state/dependencies/modules/circulardep_error_module2/.module similarity index 100% rename from tests/state/dependencies/modules/circularlike/.module rename to tests/state/dependencies/modules/circulardep_error_module2/.module diff --git a/tests/state/dependencies/modules/circulardep_error_module2/__init__.py b/tests/state/dependencies/modules/circulardep_error_module2/__init__.py new file mode 100644 index 00000000..bb7acf91 --- /dev/null +++ b/tests/state/dependencies/modules/circulardep_error_module2/__init__.py @@ -0,0 +1,8 @@ + + +def register(mf): + mf.register_defaults({ + "foo": lambda state: state["..circulardep_error_module3.foo"], + "bar": lambda state: state["..circulardep_error_module3.bar"], + "bar2": lambda state: state["..circulardep_error_module1.bar2"] + }) diff --git a/tests/state/dependencies/modules/dependency_one/.module b/tests/state/dependencies/modules/circulardep_error_module3/.module similarity index 100% rename from tests/state/dependencies/modules/dependency_one/.module rename to tests/state/dependencies/modules/circulardep_error_module3/.module diff --git a/tests/state/dependencies/modules/circulardep_error_module3/__init__.py b/tests/state/dependencies/modules/circulardep_error_module3/__init__.py new file mode 100644 index 00000000..c709e532 --- /dev/null +++ b/tests/state/dependencies/modules/circulardep_error_module3/__init__.py @@ -0,0 +1,8 @@ + + +def register(mf): + mf.register_defaults({ + "foo": lambda state: state["..circulardep_error_module1.foo"], + "bar": lambda state: state["bar2"], + "bar2": lambda state: state["..circulardep_error_module2.bar2"] + }) diff --git a/tests/state/dependencies/modules/dependency_two_lambda/.module b/tests/state/dependencies/modules/circulardep_error_selfdependency/.module similarity index 100% rename from tests/state/dependencies/modules/dependency_two_lambda/.module rename to tests/state/dependencies/modules/circulardep_error_selfdependency/.module diff --git a/tests/state/dependencies/modules/circulardep_error_selfdependency/__init__.py b/tests/state/dependencies/modules/circulardep_error_selfdependency/__init__.py new file mode 100644 index 00000000..61141a0b --- /dev/null +++ b/tests/state/dependencies/modules/circulardep_error_selfdependency/__init__.py @@ -0,0 +1,10 @@ + +def register(mf): + mf.register_defaults({ + "foo": lambda state: state["foo"], + "foo2": lambda state: state["foo2bar"], + "foo2bar": lambda state: state["foo2"], + "foo3": lambda state: state["foo3barbar"], + "foo3bar": lambda state: state["foo3"], + "foo3barbar": lambda state: state["foo3bar"] + }) diff --git a/tests/state/dependencies/modules/circularlike/__init__.py b/tests/state/dependencies/modules/circularlike/__init__.py deleted file mode 100644 index e8200517..00000000 --- a/tests/state/dependencies/modules/circularlike/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ - - -def main(): - print("main event") - - -def register(mf): - mf.register_defaults({ - "foobar": lambda state, event: state["foobar"] - }) - mf.register_event('main', main, unique=False) diff --git a/tests/state/dependencies/modules/dependency_two_lambda/__init__.py b/tests/state/dependencies/modules/dependency_two_lambda/__init__.py deleted file mode 100644 index fdd7201f..00000000 --- a/tests/state/dependencies/modules/dependency_two_lambda/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ - -def register(mf): - mf.register_defaults({ - "bar": 11, - "bar2": lambda state, event: state["foobar2"] if "foobar2" in state else 12, - "bar3": lambda state, event: state["foobar3"] if "foobar3" in state else 13, - }) diff --git a/tests/state/dependencies/modules/dependencychain_module1/.module b/tests/state/dependencies/modules/dependencychain_module1/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/dependencychain_module1/__init__.py b/tests/state/dependencies/modules/dependencychain_module1/__init__.py new file mode 100644 index 00000000..d9b615f5 --- /dev/null +++ b/tests/state/dependencies/modules/dependencychain_module1/__init__.py @@ -0,0 +1,18 @@ + + +def print_all(state): + for k, v in state.all.items(): + if not k.startswith("modules."): + continue + print(k + ":", v) + + +def register(mf): + mf.register_event("print_all", print_all) + mf.register_defaults({ + "foo1": lambda state: state["..dependencychain_module2.foo1"], + "foo2": lambda state: state["..dependencychain_module2.foo2"], + "foo3": lambda state: 2 * state["..dependencychain_module2.foo3"], + "foo4": lambda state: 3 * state["..dependencychain_module2.foo4"], + "foo5": lambda state: 4 * state["..dependencychain_module2.foo5"], + }) diff --git a/tests/state/dependencies/modules/dependencychain_module2/.module b/tests/state/dependencies/modules/dependencychain_module2/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/dependencychain_module2/__init__.py b/tests/state/dependencies/modules/dependencychain_module2/__init__.py new file mode 100644 index 00000000..f273811c --- /dev/null +++ b/tests/state/dependencies/modules/dependencychain_module2/__init__.py @@ -0,0 +1,14 @@ + + +def somefunction(value): + return value + 1 + + +def register(mf): + mf.register_defaults({ + "foo1": lambda: 100, + "foo2": lambda state: state["..dependencychain_module3.foo2"], + "foo3": lambda state: state["..dependencychain_module3.foo3"], + "foo4": lambda state: somefunction(state["..dependencychain_module3.foo4"])**5 + state["foo1"], + "foo5": lambda state: state["..dependencychain_module3.foo5"], + }) diff --git a/tests/state/dependencies/modules/dependencychain_module3/.module b/tests/state/dependencies/modules/dependencychain_module3/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/dependencychain_module3/__init__.py b/tests/state/dependencies/modules/dependencychain_module3/__init__.py new file mode 100644 index 00000000..1b1d3368 --- /dev/null +++ b/tests/state/dependencies/modules/dependencychain_module3/__init__.py @@ -0,0 +1,11 @@ + + +def register(mf): + mf.register_defaults({ + "foo1": lambda: 101, + "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"], + }) diff --git a/tests/state/dependencies/modules/dependencychain_module4/.module b/tests/state/dependencies/modules/dependencychain_module4/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/dependency_one/__init__.py b/tests/state/dependencies/modules/dependencychain_module4/__init__.py similarity index 53% rename from tests/state/dependencies/modules/dependency_one/__init__.py rename to tests/state/dependencies/modules/dependencychain_module4/__init__.py index d26c8c47..8644b041 100644 --- a/tests/state/dependencies/modules/dependency_one/__init__.py +++ b/tests/state/dependencies/modules/dependencychain_module4/__init__.py @@ -1,6 +1,6 @@ + def register(mf): mf.register_defaults({ - "foobar1": 111, - "foobar2": 112 + "foo5": lambda: 500, }) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/.module b/tests/state/dependencies/modules/lambdaarguments_module1/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py new file mode 100644 index 00000000..a4d7b522 --- /dev/null +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -0,0 +1,13 @@ + + +def getvalue(state): + return state["42"] + +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), + }) diff --git a/tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/.module b/tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/__init__.py b/tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/__init__.py new file mode 100644 index 00000000..7cce7d8a --- /dev/null +++ b/tests/state/dependencies/modules/unresolveddep_error_dependencynotfound/__init__.py @@ -0,0 +1,5 @@ + +def register(mf): + mf.register_defaults({ + "foo": lambda state: state["varnotfound"], + }) diff --git a/tests/state/dependencies/setup.py b/tests/state/dependencies/setup.py new file mode 100644 index 00000000..4d98c0b8 --- /dev/null +++ b/tests/state/dependencies/setup.py @@ -0,0 +1,16 @@ +from pathlib import Path +import miniflask # noqa: E402 + + +def printAll(state): + for k in state.all: + print(f"{k}:", state[k]) + + +def setup(): + mf = miniflask.init( + module_dirs=str(Path(__file__).parent / "modules"), + debug=True + ) + mf.register_event('main', printAll, unique=False) + return mf diff --git a/tests/state/dependencies/test_circular_definition.py b/tests/state/dependencies/test_circular_definition.py deleted file mode 100644 index fd64f6da..00000000 --- a/tests/state/dependencies/test_circular_definition.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path -import pytest -import miniflask # noqa: E402 - - -mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), - debug=True -) - - -def test_directcall_beforeafter(): - mf.load("circularlike") - with pytest.raises(RecursionError): - mf.parse_args([]) diff --git a/tests/state/dependencies/test_dependency_lambda.py b/tests/state/dependencies/test_dependency_lambda.py deleted file mode 100644 index b8b73a54..00000000 --- a/tests/state/dependencies/test_dependency_lambda.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path -import miniflask # noqa: E402 - - -mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), - debug=True -) - - -def test_like_depedencies(capsys): - mf.load(["actual_data_lambda", "dependency_one", "dependency_two_lambda"]) - mf.parse_args([]) - captured = capsys.readouterr() - mf.event.main() - captured = capsys.readouterr() - assert captured.out == """ -foo: 11 -foo2: 112 -foo3: 13 -foo4: 4 -""".lstrip() diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py new file mode 100644 index 00000000..d201584f --- /dev/null +++ b/tests/state/dependencies/test_state_dependencies.py @@ -0,0 +1,144 @@ +import pytest +from setup import setup +from miniflask.exceptions import RegisterError + + +def test_lambda_arguments(): + mf = setup() + mf.load("lambdaarguments_module1") + mf.parse_args([]) + with pytest.raises(RecursionError): + mf.parse_args([]) +# TODO: test + + +def test_circular_dependency_errors(): + mf = setup() + mf.load("circulardep_error_selfdependency") + mf.load("circulardep_error_module1") + mf.load("circulardep_error_module2") + mf.load("circulardep_error_module3") + with pytest.raises(RegisterError) as excinfo: + mf.parse_args([]) + assert """ +The registration of state variables has led to the following errors: + +Error: Circular dependencies found! (A → B means "A depends on B") + +modules.circulardep_error_selfdependency.foo + → modules.circulardep_error_selfdependency.foo +modules.circulardep_error_selfdependency.foo2 + → modules.circulardep_error_selfdependency.foo2bar + → modules.circulardep_error_selfdependency.foo2 +modules.circulardep_error_selfdependency.foo3 + → modules.circulardep_error_selfdependency.foo3barbar + → modules.circulardep_error_selfdependency.foo3bar + → modules.circulardep_error_selfdependency.foo3 +modules.circulardep_error_module1.foo + → modules.circulardep_error_module2.foo + → modules.circulardep_error_module3.foo + → modules.circulardep_error_module1.foo +modules.circulardep_error_module1.bar + → modules.circulardep_error_module2.bar + → modules.circulardep_error_module3.bar + → modules.circulardep_error_module3.bar2 + → modules.circulardep_error_module2.bar2 + → modules.circulardep_error_module1.bar2 + → modules.circulardep_error_module3.bar2 +""".strip() == str(excinfo.value).strip() + + +def test_dependency_unresolved(): + mf = setup() + mf.load("unresolveddep_error_dependencynotfound") + with pytest.raises(RegisterError) as excinfo: + mf.parse_args([]) + assert """ +The registration of state variables has led to the following errors: + +Error: Dependency not found! (A → B means "A depends on B") + +modules.unresolveddep_error_dependencynotfound.foo + → varnotfound +""".strip() == str(excinfo.value).strip() + + + +def test_working_dependencies(capsys): + mf = setup() + mf.load("settings") + mf.load("dependencychain_module1") + mf.load("dependencychain_module2") + mf.load("dependencychain_module3") + mf.load("dependencychain_module4") + mf.parse_args([]) + 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_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() + 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([]) + + + +if __name__ == "__main__": + # test_lambda_arguments() + # test_working_dependencies() + test_alternative_dependencies() From b12218b74c005ef800e9d4030ca09d5c50d43658 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 17 Oct 2022 18:51:51 +0200 Subject: [PATCH 05/45] API-change: refactor of state-dependency handling --- src/miniflask/miniflask.py | 205 ++++++++++++++++++------------------- src/miniflask/state.py | 115 ++++++++++++++++++++- src/miniflask/util.py | 2 +- 3 files changed, 210 insertions(+), 112 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 8f79ae1c..267aead0 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 = {} 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 7c4a2e081d56f4347d145f1154226d6dd2445d4c Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 18 Oct 2022 14:35:31 +0200 Subject: [PATCH 06/45] 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 a9198643f114fba12da815028d868595cc6e36a0 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 18 Oct 2022 14:35:11 +0200 Subject: [PATCH 07/45] 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 267aead0..aa9f10c1 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 5e3523ed03680719a6384abbf9b08c859c43c075 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 17 Oct 2022 18:52:09 +0200 Subject: [PATCH 08/45] [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 4bc38a14fae34f2637d482635bce3344d4094072 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 18 Oct 2022 15:29:10 +0200 Subject: [PATCH 09/45] 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 fe4d23dd593d700fbfc22686b90a5b18870a03a3 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 21 Oct 2022 13:32:57 +0200 Subject: [PATCH 10/45] refactored dependency-solver DFS to use stack+loop --- src/miniflask/state.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index b989ceb4..59a56966 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -469,24 +469,30 @@ def topological_sort(node_dict): # noqa: C901 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 = [] + # the following code implements DFS using a stack of nodes-to-traverse + stack = [(node_i, []) for node_i in range(len(nodes) - 1, -1, -1)] + while len(stack) > 0: + node_i, parentnodes = stack.pop() + + if node_i == "add_to_sorted_nodes": + sorted_nodes.append(parentnodes) + continue node = nodes[node_i] if node in parentnodes: cycles.append(parentnodes + [node]) - return + continue parentnodes = parentnodes + [node] if visited[node_i]: - return + continue visited[node_i] = True + stack.append(("add_to_sorted_nodes", node)) + dependency_stack = [] for dependency in node.depends_on: if dependency not in node.mf.state: @@ -507,12 +513,10 @@ def DFS(node_i, parentnodes=None): else: dependency_varid = dependency dependency_i = varid2index[dependency_varid] - DFS(dependency_i, parentnodes=parentnodes) - - sorted_nodes.append(node) + dependency_stack.append((dependency_i, parentnodes)) - for i in range(len(nodes)): - DFS(i) + dependency_stack.reverse() + stack = stack + dependency_stack return sorted_nodes, cycles, unresolved From 2ca3861c866697a92d687707d1865435d003d4cf Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Fri, 4 Nov 2022 14:02:29 +0100 Subject: [PATCH 11/45] tests: Add test for state dependencies in defs. Fix order of deps, now always sorted. Add more test cases. Remove unused argument 'state'. tests: Add test for failing stuff. Split from "Remove 'meta' inspection." --- .../modules/defarguments_module1/.module | 0 .../modules/defarguments_module1/__init__.py | 43 +++++++++++++++++++ .../lambdaarguments_module1/__init__.py | 27 +++++++++--- .../modules/lambdaarguments_module2/.module | 0 .../lambdaarguments_module2/__init__.py | 4 ++ .../modules/lambdaarguments_module3/.module | 0 .../lambdaarguments_module3/__init__.py | 7 +++ .../dependencies/test_state_dependencies.py | 33 +++++++++++++- 8 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 tests/state/dependencies/modules/defarguments_module1/.module create mode 100644 tests/state/dependencies/modules/defarguments_module1/__init__.py 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/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..a816a0fa --- /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(): + 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/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 186b9343..25bbd7f5 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -1,19 +1,34 @@ - - def getvalue(state): return state["42"] -def somefunction(val): +def some_function(val): return val * 300 + 2 +class TestClass: # pylint: disable=too-few-public-methods + + def __init__(self, state): + self.state = state + + +lambda_definition = lambda: 42 # noqa # pylint: disable=unnecessary-lambda-assignment + + 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_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"], }) 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 0fefb820..ef05f5aa 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -3,14 +3,14 @@ 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 == [] 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 == {} @@ -20,6 +20,35 @@ 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") + 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 == ["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 == {} + 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 993dacac282cb107dcf99a2749969e8732613ddc Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 14 Dec 2022 15:48:24 +0100 Subject: [PATCH 12/45] github: 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 a054722ddbe93932cbe031503df1deae762939af Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Thu, 15 Dec 2022 09:44:40 +0100 Subject: [PATCH 13/45] setup.py: 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: From d22ecac86465df9074fb447b535f8cdaa57c8d2e Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Mon, 7 Nov 2022 16:09:21 +0100 Subject: [PATCH 14/45] Implement dependency checks using AST. Inspect register() call to maybe get more insights. Remove 'meta' inspection. Ensure single expression is found in source. Define and use private members. Fix string search for 'state' argument. Remove not allowed statements. Remove unused import. Remove empty lines. Add noqa to class. Spam noqa. fjgruiewhjfiogbhpa fjgruiewhjfiogbhpa Disable more warnings. Fix typo. --- src/miniflask/state.py | 113 ++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 59a56966..8020287e 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,13 @@ 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*:.*}?") + local_arguments = [] + depends_on = [] + depends_alternatives = [] -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 +405,36 @@ 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 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 + 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=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=self.local_arguments) + for lv in self._find_comp_names(node.test, self.local_arguments): + self.depends_alternatives[lv] = rvs def str(self): return str(self.varid) @@ -451,6 +450,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 # # ------------------- # @@ -523,11 +551,12 @@ def topological_sort(node_dict): # noqa: C901 @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 61753633738eeb4284c5bc205ded80e94d8c7ad2 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 13:08:46 +0200 Subject: [PATCH 15/45] tests: restructured to have 2-level directory structure --- tests/{features => basic/module_loading}/modules/__init__.py | 0 tests/{ => basic}/module_loading/modules/modules_b | 0 .../modules => basic/module_loading/modules/otherdir}/__init__.py | 0 .../a => basic/module_loading/modules/otherdir/module2}/.module | 0 .../module_loading/modules/otherdir/module2/__init__.py | 0 .../module_loading/modules/parentdir}/__init__.py | 0 .../b => basic/module_loading/modules/parentdir/module1}/.module | 0 .../module_loading/modules/parentdir/module1/__init__.py | 0 .../module_loading/modules/parentdir}/module2/.module | 0 .../module_loading/modules/parentdir/module2/__init__.py | 0 .../module_loading/modules/parentdir/module3}/.module | 0 .../module_loading/modules/parentdir/module3/__init__.py | 0 .../module_loading/modules/parentdir/module3/submodule}/.module | 0 .../modules/parentdir/module3/submodule/__init__.py | 0 .../modules/parentdir/module3/submodule/subsubmodule}/.module | 0 .../modules/parentdir/module3/submodule/subsubmodule/__init__.py | 0 .../module_loading/modules/parentdir/module3/submodule2}/.module | 0 .../modules/parentdir/module3/submodule2/__init__.py | 0 .../submodule_dir/submodule_with_folder_in_between}/.module | 0 .../submodule_dir/submodule_with_folder_in_between/__init__.py | 0 .../modules/parentdir/module3/submodule_without_autoload}/.module | 0 .../parentdir/module3/submodule_without_autoload/__init__.py | 0 .../module_loading/modules/parentdir/module4}/.module | 0 .../module_loading/modules/parentdir/module4/__init__.py | 0 .../module_loading/modules/parentdir/module5}/.module | 0 .../module_loading/modules/parentdir/module5/__init__.py | 0 .../module_loading/modules/parentdir/module6}/.module | 0 .../module_loading/modules/parentdir/module6/__init__.py | 0 .../module_loading/modules/parentdir/module7}/.module | 0 .../module_loading/modules/parentdir/module7/__init__.py | 0 tests/{ => basic}/module_loading/modules_b | 0 tests/{ => basic}/module_loading/test_basic_import.py | 0 tests/{ => basic}/module_loading/test_basic_import_dict.py | 0 tests/{ => basic}/module_loading/test_default_module.py | 0 tests/{ => basic}/module_loading/test_parent_autoloading.py | 0 tests/{ => basic}/module_loading/test_relative_import.py | 0 .../modules/parentdir => features/features/modules}/__init__.py | 0 .../parentdir/module6 => features/features/modules/a}/.module | 0 tests/features/{ => features}/modules/a/__init__.py | 0 .../parentdir/module7 => features/features/modules/b}/.module | 0 tests/features/{ => features}/modules/b/__init__.py | 0 tests/features/{ => features}/test_features.py | 0 42 files changed, 0 insertions(+), 0 deletions(-) rename tests/{features => basic/module_loading}/modules/__init__.py (100%) rename tests/{ => basic}/module_loading/modules/modules_b (100%) rename tests/{module_loading/modules => basic/module_loading/modules/otherdir}/__init__.py (100%) rename tests/{features/modules/a => basic/module_loading/modules/otherdir/module2}/.module (100%) rename tests/{ => basic}/module_loading/modules/otherdir/module2/__init__.py (100%) rename tests/{module_loading/modules/otherdir => basic/module_loading/modules/parentdir}/__init__.py (100%) rename tests/{features/modules/b => basic/module_loading/modules/parentdir/module1}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module1/__init__.py (100%) rename tests/{module_loading/modules/otherdir => basic/module_loading/modules/parentdir}/module2/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module2/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module1 => basic/module_loading/modules/parentdir/module3}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module3/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module2 => basic/module_loading/modules/parentdir/module3/submodule}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module3/submodule/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module3 => basic/module_loading/modules/parentdir/module3/submodule/subsubmodule}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module3/submodule/subsubmodule/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module3/submodule => basic/module_loading/modules/parentdir/module3/submodule2}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module3/submodule2/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module3/submodule/subsubmodule => basic/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module3/submodule2 => basic/module_loading/modules/parentdir/module3/submodule_without_autoload}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module3/submodule_without_autoload/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between => basic/module_loading/modules/parentdir/module4}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module4/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module3/submodule_without_autoload => basic/module_loading/modules/parentdir/module5}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module5/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module4 => basic/module_loading/modules/parentdir/module6}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module6/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module5 => basic/module_loading/modules/parentdir/module7}/.module (100%) rename tests/{ => basic}/module_loading/modules/parentdir/module7/__init__.py (100%) rename tests/{ => basic}/module_loading/modules_b (100%) rename tests/{ => basic}/module_loading/test_basic_import.py (100%) rename tests/{ => basic}/module_loading/test_basic_import_dict.py (100%) rename tests/{ => basic}/module_loading/test_default_module.py (100%) rename tests/{ => basic}/module_loading/test_parent_autoloading.py (100%) rename tests/{ => basic}/module_loading/test_relative_import.py (100%) rename tests/{module_loading/modules/parentdir => features/features/modules}/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module6 => features/features/modules/a}/.module (100%) rename tests/features/{ => features}/modules/a/__init__.py (100%) rename tests/{module_loading/modules/parentdir/module7 => features/features/modules/b}/.module (100%) rename tests/features/{ => features}/modules/b/__init__.py (100%) rename tests/features/{ => features}/test_features.py (100%) diff --git a/tests/features/modules/__init__.py b/tests/basic/module_loading/modules/__init__.py similarity index 100% rename from tests/features/modules/__init__.py rename to tests/basic/module_loading/modules/__init__.py diff --git a/tests/module_loading/modules/modules_b b/tests/basic/module_loading/modules/modules_b similarity index 100% rename from tests/module_loading/modules/modules_b rename to tests/basic/module_loading/modules/modules_b diff --git a/tests/module_loading/modules/__init__.py b/tests/basic/module_loading/modules/otherdir/__init__.py similarity index 100% rename from tests/module_loading/modules/__init__.py rename to tests/basic/module_loading/modules/otherdir/__init__.py diff --git a/tests/features/modules/a/.module b/tests/basic/module_loading/modules/otherdir/module2/.module similarity index 100% rename from tests/features/modules/a/.module rename to tests/basic/module_loading/modules/otherdir/module2/.module diff --git a/tests/module_loading/modules/otherdir/module2/__init__.py b/tests/basic/module_loading/modules/otherdir/module2/__init__.py similarity index 100% rename from tests/module_loading/modules/otherdir/module2/__init__.py rename to tests/basic/module_loading/modules/otherdir/module2/__init__.py diff --git a/tests/module_loading/modules/otherdir/__init__.py b/tests/basic/module_loading/modules/parentdir/__init__.py similarity index 100% rename from tests/module_loading/modules/otherdir/__init__.py rename to tests/basic/module_loading/modules/parentdir/__init__.py diff --git a/tests/features/modules/b/.module b/tests/basic/module_loading/modules/parentdir/module1/.module similarity index 100% rename from tests/features/modules/b/.module rename to tests/basic/module_loading/modules/parentdir/module1/.module diff --git a/tests/module_loading/modules/parentdir/module1/__init__.py b/tests/basic/module_loading/modules/parentdir/module1/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module1/__init__.py rename to tests/basic/module_loading/modules/parentdir/module1/__init__.py diff --git a/tests/module_loading/modules/otherdir/module2/.module b/tests/basic/module_loading/modules/parentdir/module2/.module similarity index 100% rename from tests/module_loading/modules/otherdir/module2/.module rename to tests/basic/module_loading/modules/parentdir/module2/.module diff --git a/tests/module_loading/modules/parentdir/module2/__init__.py b/tests/basic/module_loading/modules/parentdir/module2/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module2/__init__.py rename to tests/basic/module_loading/modules/parentdir/module2/__init__.py diff --git a/tests/module_loading/modules/parentdir/module1/.module b/tests/basic/module_loading/modules/parentdir/module3/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module1/.module rename to tests/basic/module_loading/modules/parentdir/module3/.module diff --git a/tests/module_loading/modules/parentdir/module3/__init__.py b/tests/basic/module_loading/modules/parentdir/module3/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module3/__init__.py rename to tests/basic/module_loading/modules/parentdir/module3/__init__.py diff --git a/tests/module_loading/modules/parentdir/module2/.module b/tests/basic/module_loading/modules/parentdir/module3/submodule/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module2/.module rename to tests/basic/module_loading/modules/parentdir/module3/submodule/.module diff --git a/tests/module_loading/modules/parentdir/module3/submodule/__init__.py b/tests/basic/module_loading/modules/parentdir/module3/submodule/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule/__init__.py rename to tests/basic/module_loading/modules/parentdir/module3/submodule/__init__.py diff --git a/tests/module_loading/modules/parentdir/module3/.module b/tests/basic/module_loading/modules/parentdir/module3/submodule/subsubmodule/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module3/.module rename to tests/basic/module_loading/modules/parentdir/module3/submodule/subsubmodule/.module diff --git a/tests/module_loading/modules/parentdir/module3/submodule/subsubmodule/__init__.py b/tests/basic/module_loading/modules/parentdir/module3/submodule/subsubmodule/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule/subsubmodule/__init__.py rename to tests/basic/module_loading/modules/parentdir/module3/submodule/subsubmodule/__init__.py diff --git a/tests/module_loading/modules/parentdir/module3/submodule/.module b/tests/basic/module_loading/modules/parentdir/module3/submodule2/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule/.module rename to tests/basic/module_loading/modules/parentdir/module3/submodule2/.module diff --git a/tests/module_loading/modules/parentdir/module3/submodule2/__init__.py b/tests/basic/module_loading/modules/parentdir/module3/submodule2/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule2/__init__.py rename to tests/basic/module_loading/modules/parentdir/module3/submodule2/__init__.py diff --git a/tests/module_loading/modules/parentdir/module3/submodule/subsubmodule/.module b/tests/basic/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule/subsubmodule/.module rename to tests/basic/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/.module diff --git a/tests/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/__init__.py b/tests/basic/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/__init__.py rename to tests/basic/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/__init__.py diff --git a/tests/module_loading/modules/parentdir/module3/submodule2/.module b/tests/basic/module_loading/modules/parentdir/module3/submodule_without_autoload/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule2/.module rename to tests/basic/module_loading/modules/parentdir/module3/submodule_without_autoload/.module diff --git a/tests/module_loading/modules/parentdir/module3/submodule_without_autoload/__init__.py b/tests/basic/module_loading/modules/parentdir/module3/submodule_without_autoload/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule_without_autoload/__init__.py rename to tests/basic/module_loading/modules/parentdir/module3/submodule_without_autoload/__init__.py diff --git a/tests/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/.module b/tests/basic/module_loading/modules/parentdir/module4/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule_dir/submodule_with_folder_in_between/.module rename to tests/basic/module_loading/modules/parentdir/module4/.module diff --git a/tests/module_loading/modules/parentdir/module4/__init__.py b/tests/basic/module_loading/modules/parentdir/module4/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module4/__init__.py rename to tests/basic/module_loading/modules/parentdir/module4/__init__.py diff --git a/tests/module_loading/modules/parentdir/module3/submodule_without_autoload/.module b/tests/basic/module_loading/modules/parentdir/module5/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module3/submodule_without_autoload/.module rename to tests/basic/module_loading/modules/parentdir/module5/.module diff --git a/tests/module_loading/modules/parentdir/module5/__init__.py b/tests/basic/module_loading/modules/parentdir/module5/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module5/__init__.py rename to tests/basic/module_loading/modules/parentdir/module5/__init__.py diff --git a/tests/module_loading/modules/parentdir/module4/.module b/tests/basic/module_loading/modules/parentdir/module6/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module4/.module rename to tests/basic/module_loading/modules/parentdir/module6/.module diff --git a/tests/module_loading/modules/parentdir/module6/__init__.py b/tests/basic/module_loading/modules/parentdir/module6/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module6/__init__.py rename to tests/basic/module_loading/modules/parentdir/module6/__init__.py diff --git a/tests/module_loading/modules/parentdir/module5/.module b/tests/basic/module_loading/modules/parentdir/module7/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module5/.module rename to tests/basic/module_loading/modules/parentdir/module7/.module diff --git a/tests/module_loading/modules/parentdir/module7/__init__.py b/tests/basic/module_loading/modules/parentdir/module7/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/module7/__init__.py rename to tests/basic/module_loading/modules/parentdir/module7/__init__.py diff --git a/tests/module_loading/modules_b b/tests/basic/module_loading/modules_b similarity index 100% rename from tests/module_loading/modules_b rename to tests/basic/module_loading/modules_b diff --git a/tests/module_loading/test_basic_import.py b/tests/basic/module_loading/test_basic_import.py similarity index 100% rename from tests/module_loading/test_basic_import.py rename to tests/basic/module_loading/test_basic_import.py diff --git a/tests/module_loading/test_basic_import_dict.py b/tests/basic/module_loading/test_basic_import_dict.py similarity index 100% rename from tests/module_loading/test_basic_import_dict.py rename to tests/basic/module_loading/test_basic_import_dict.py diff --git a/tests/module_loading/test_default_module.py b/tests/basic/module_loading/test_default_module.py similarity index 100% rename from tests/module_loading/test_default_module.py rename to tests/basic/module_loading/test_default_module.py diff --git a/tests/module_loading/test_parent_autoloading.py b/tests/basic/module_loading/test_parent_autoloading.py similarity index 100% rename from tests/module_loading/test_parent_autoloading.py rename to tests/basic/module_loading/test_parent_autoloading.py diff --git a/tests/module_loading/test_relative_import.py b/tests/basic/module_loading/test_relative_import.py similarity index 100% rename from tests/module_loading/test_relative_import.py rename to tests/basic/module_loading/test_relative_import.py diff --git a/tests/module_loading/modules/parentdir/__init__.py b/tests/features/features/modules/__init__.py similarity index 100% rename from tests/module_loading/modules/parentdir/__init__.py rename to tests/features/features/modules/__init__.py diff --git a/tests/module_loading/modules/parentdir/module6/.module b/tests/features/features/modules/a/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module6/.module rename to tests/features/features/modules/a/.module diff --git a/tests/features/modules/a/__init__.py b/tests/features/features/modules/a/__init__.py similarity index 100% rename from tests/features/modules/a/__init__.py rename to tests/features/features/modules/a/__init__.py diff --git a/tests/module_loading/modules/parentdir/module7/.module b/tests/features/features/modules/b/.module similarity index 100% rename from tests/module_loading/modules/parentdir/module7/.module rename to tests/features/features/modules/b/.module diff --git a/tests/features/modules/b/__init__.py b/tests/features/features/modules/b/__init__.py similarity index 100% rename from tests/features/modules/b/__init__.py rename to tests/features/features/modules/b/__init__.py diff --git a/tests/features/test_features.py b/tests/features/features/test_features.py similarity index 100% rename from tests/features/test_features.py rename to tests/features/features/test_features.py From edfe443507b529520b3ccacb523e5ad3a11d19d0 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 13:09:51 +0200 Subject: [PATCH 16/45] tests: convert test-folder to nested python module --- tests/argparse/list/__init__.py | 0 tests/argparse/nested_arguments/__init__.py | 0 tests/argparse/optional/__init__.py | 0 tests/argparse/required/__init__.py | 0 tests/argparse/special/__init__.py | 0 tests/argparse/tuples/__init__.py | 0 tests/argparse/values/__init__.py | 0 tests/basic/__init__.py | 0 tests/basic/module_loading/__init__.py | 0 tests/event/beforeafter/__init__.py | 0 tests/event/exception_event/__init__.py | 0 tests/event/named_call/__init__.py | 0 tests/event/overwrite_event/__init__.py | 0 tests/event/register_event/__init__.py | 0 tests/event/unregister_event/__init__.py | 0 tests/features/features/__init__.py | 0 tests/state/dataclass_as_state/__init__.py | 0 tests/state/dynamic_state/__init__.py | 0 tests/state/outervar/__init__.py | 0 tests/state/register_wrong_scope/__init__.py | 0 tests/state/temporary/__init__.py | 0 21 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/argparse/list/__init__.py create mode 100644 tests/argparse/nested_arguments/__init__.py create mode 100644 tests/argparse/optional/__init__.py create mode 100644 tests/argparse/required/__init__.py create mode 100644 tests/argparse/special/__init__.py create mode 100644 tests/argparse/tuples/__init__.py create mode 100644 tests/argparse/values/__init__.py create mode 100644 tests/basic/__init__.py create mode 100644 tests/basic/module_loading/__init__.py create mode 100644 tests/event/beforeafter/__init__.py create mode 100644 tests/event/exception_event/__init__.py create mode 100644 tests/event/named_call/__init__.py create mode 100644 tests/event/overwrite_event/__init__.py create mode 100644 tests/event/register_event/__init__.py create mode 100644 tests/event/unregister_event/__init__.py create mode 100644 tests/features/features/__init__.py create mode 100644 tests/state/dataclass_as_state/__init__.py create mode 100644 tests/state/dynamic_state/__init__.py create mode 100644 tests/state/outervar/__init__.py create mode 100644 tests/state/register_wrong_scope/__init__.py create mode 100644 tests/state/temporary/__init__.py diff --git a/tests/argparse/list/__init__.py b/tests/argparse/list/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/argparse/nested_arguments/__init__.py b/tests/argparse/nested_arguments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/argparse/optional/__init__.py b/tests/argparse/optional/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/argparse/required/__init__.py b/tests/argparse/required/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/argparse/special/__init__.py b/tests/argparse/special/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/argparse/tuples/__init__.py b/tests/argparse/tuples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/argparse/values/__init__.py b/tests/argparse/values/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/basic/__init__.py b/tests/basic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/basic/module_loading/__init__.py b/tests/basic/module_loading/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/event/beforeafter/__init__.py b/tests/event/beforeafter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/event/exception_event/__init__.py b/tests/event/exception_event/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/event/named_call/__init__.py b/tests/event/named_call/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/event/overwrite_event/__init__.py b/tests/event/overwrite_event/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/event/register_event/__init__.py b/tests/event/register_event/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/event/unregister_event/__init__.py b/tests/event/unregister_event/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/features/features/__init__.py b/tests/features/features/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dataclass_as_state/__init__.py b/tests/state/dataclass_as_state/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dynamic_state/__init__.py b/tests/state/dynamic_state/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/outervar/__init__.py b/tests/state/outervar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/register_wrong_scope/__init__.py b/tests/state/register_wrong_scope/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/temporary/__init__.py b/tests/state/temporary/__init__.py new file mode 100644 index 00000000..e69de29b From d93f1ee4d5a72a51fdc4e49455711ab8f355d284 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 14:04:36 +0200 Subject: [PATCH 17/45] adapted importname of miniflask-modules, remove unneeded .modules import --- src/miniflask/__init__.py | 1 - src/miniflask/miniflask.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/miniflask/__init__.py b/src/miniflask/__init__.py index 5fc3551b..d2af2f84 100644 --- a/src/miniflask/__init__.py +++ b/src/miniflask/__init__.py @@ -1,6 +1,5 @@ from .miniflask import miniflask as init, print_info, get_default_args from .event import outervar -from .modules import * from .state import optional # meta diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index aa9f10c1..cada74a5 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -41,7 +41,7 @@ def get_default_args(func): def registerPredefined(modules_avail): for m in ["modules", "events", "info", "settings", "definitions"]: module_name_id = 'miniflask.' + m - importname = 'miniflask.modules.' + m + importname = 'miniflask.' + m modules_avail[module_name_id] = { 'id': module_name_id, 'importpath': "system", From 10cddd9a354cee08c0f2fd336b88bf91adaffdbb Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Thu, 6 Oct 2022 15:35:09 +0200 Subject: [PATCH 18/45] remove filesystem import --- src/miniflask/miniflask.py | 36 +----------------------------------- src/miniflask/util.py | 1 - 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index cada74a5..47f498b1 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -6,7 +6,6 @@ from functools import partial from os import path, listdir, linesep, get_terminal_size from importlib import import_module -from importlib.machinery import PathFinder as ImportPathFinder from enum import Enum, EnumMeta from argparse import ArgumentParser, REMAINDER as ARGPARSE_REMAINDER from typing import List @@ -44,7 +43,6 @@ def registerPredefined(modules_avail): importname = 'miniflask.' + m modules_avail[module_name_id] = { 'id': module_name_id, - 'importpath': "system", 'importname': importname, 'lowpriority': False } @@ -181,40 +179,8 @@ def print_recently_loaded(self, prepend="", loading_text=highlight_loading): def import_module(self, module_name): module_spec = self.modules_avail[module_name] - importpath = module_spec["importpath"] + return import_module(module_spec["id"]) - # system import (e.g. miniflask internal modules or pip-module repositories) - if importpath == "system": - return import_module(module_spec["id"]) - - try: - direct_import = import_module(module_spec["id"]) - if direct_import: - return direct_import - except ModuleNotFoundError: - pass - - # imports across filesystem - # the random id (_instance_id) ensures sys.modules does not cache different miniflask instances - # (first we need to load the parent module, if available) - parent_module_name, rest = "miniflask." + self._instance_id + "." + path.basename(path.abspath(module_spec["importpath"])), "" - spec = ImportPathFinder().find_spec(parent_module_name, [path.dirname(importpath)]) - if spec is None: - if rest: - raise ValueError("Could not import parent Module named '%s'. This is needed for module named '%s' (defined in '%s')." % (parent_module_name, module_spec["id"], module_spec["importpath"])) - raise ValueError("Module named '%s' (defined in '%s') could not be imported." % (module_spec["id"], module_spec["importpath"])) - if spec.loader is None: - raise ValueError("Could not import parent Module named '%s'. This is needed for module named '%s' (defined in '%s'). Did you maybe miss to define a `__init__.py` file in any subfolder?" % (parent_module_name, module_spec["id"], module_spec["importpath"])) - - # set parent module name to identifier given by dict (base_id) - parent_module_name = "miniflask." + self._instance_id + "." + module_spec["base_id"] - spec.loader.name = parent_module_name - - # actually load the parent module - spec.loader.load_module() - - # now load the requested module - return import_module(parent_module_name + "." + module_spec["importname"]) # module event def getModuleEvents(self, module_id, mf=None): diff --git a/src/miniflask/util.py b/src/miniflask/util.py index 35019d1c..7c4bcd25 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -36,7 +36,6 @@ def getModulesAvail(module_dirs, f=None): 'id': module_name_id, 'lowpriority': path.exists(path.join(dirpath, ".lowpriority")), 'importname': local_import_name, - 'importpath': directory, } return f From 2b1bc682e742629a5d0df6e98dcaf11d0bf4f0e4 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 14:13:40 +0200 Subject: [PATCH 19/45] import modules using true python import paths --- src/miniflask/miniflask.py | 3 +-- src/miniflask/util.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 47f498b1..ab43e824 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -179,8 +179,7 @@ def print_recently_loaded(self, prepend="", loading_text=highlight_loading): def import_module(self, module_name): module_spec = self.modules_avail[module_name] - return import_module(module_spec["id"]) - + return import_module(module_spec["importname"]) # module event def getModuleEvents(self, module_id, mf=None): diff --git a/src/miniflask/util.py b/src/miniflask/util.py index 7c4bcd25..ff4e3c2c 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -2,10 +2,24 @@ import argparse from argparse import Action from os import walk, path +from pathlib import Path from colored import attr, fg +# get total importable module name of directory, as seen from python +# - i.e. $result can be loaded using `import $result` +def get_full_base_module_name(directory): + directory = Path(directory).absolute() + base_import = directory.name + + while (directory.parent / "__init__.py").exists(): + directory = directory.parent + base_import = directory.name + "." + base_import + + return base_import + + # get modules in a directory def getModulesAvail(module_dirs, f=None): if f is None: @@ -13,6 +27,8 @@ def getModulesAvail(module_dirs, f=None): for base_module_name, directory in module_dirs.items(): base_module_name = base_module_name.replace(".", "_") directory = str(directory) # in case directory is given as PosixPath etc. + full_base_module_name = get_full_base_module_name(directory) + for (dirpath, dirnames, filenames) in walk(directory): local_import_name = dirpath[len(directory) + 1:].replace(path.sep, ".") module_name_id = base_module_name + "." + local_import_name @@ -35,8 +51,9 @@ def getModulesAvail(module_dirs, f=None): 'base_id': base_module_name, 'id': module_name_id, 'lowpriority': path.exists(path.join(dirpath, ".lowpriority")), - 'importname': local_import_name, + 'importname': full_base_module_name + "." + local_import_name } + return f From ef2545f7c39a7c8dac6a30b1ea4fc4818d01ac65 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 14:16:48 +0200 Subject: [PATCH 20/45] remove normalizing of module names with dots (would not be importable by python anyway) --- src/miniflask/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/miniflask/util.py b/src/miniflask/util.py index ff4e3c2c..d24bb7e4 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -25,7 +25,6 @@ def getModulesAvail(module_dirs, f=None): if f is None: f = {} for base_module_name, directory in module_dirs.items(): - base_module_name = base_module_name.replace(".", "_") directory = str(directory) # in case directory is given as PosixPath etc. full_base_module_name = get_full_base_module_name(directory) From 34782c86fa85b02974ba89b08432866235bbac7d Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 14:20:37 +0200 Subject: [PATCH 21/45] disable dict-based module-repositories in mf.init completely --- src/miniflask/miniflask.py | 14 +------------- src/miniflask/util.py | 1 - 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index ab43e824..3d8546e5 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -65,7 +65,6 @@ def __init__(self, module_dirs, debug=False): (The directory name will also be repository name / module prefix for all modules inside that folder.) - **List**: specifies multiple paths to the module repository to use. (The directory names will also be repository names / module prefixes for all modules inside that folders.) - - **Dict**: specifies repository names / module prefixes together with their respective repository paths. - `debug`: Debug Mode Debug Mode disables catching/printing + beautifying Exceptions. Also, it disables truncating the traceback messages of internal miniflask functions. @@ -77,13 +76,6 @@ def __init__(self, module_dirs, debug=False): # Multiple Module Repositories (named "publicmodules" and "privatemodules") mf = miniflask.init(["privatemodules", "publicmodules"]) - - # Multiple Module Repositories (with custom names "pub" and "priv") - # -> modules inside of privatemodules will get the prefix "priv." - mf = miniflask.init({ - "priv": "privatemodules", - "pub": "publicmodules" - }) ``` """ # noqa: W291 @@ -97,10 +89,8 @@ def __init__(self, module_dirs, debug=False): self.module_dirs = {path.basename(m): m for m in module_dirs} elif isinstance(module_dirs, str): self.module_dirs = {path.basename(module_dirs): module_dirs} - elif isinstance(module_dirs, dict): - self.module_dirs = module_dirs else: - raise ValueError("Only dict or list allowed for `module_dirs'. Found type '%s'." % type(module_dirs)) + raise ValueError("Only string or list allowed for `module_dirs'. Found type '%s'." % type(module_dirs)) # arguments from cli-stdin self.settings_parser = ArgumentParser(usage=sys.argv[0] + " modulelist [optional arguments]") @@ -1237,7 +1227,6 @@ def __init__(self, module_name, mf): # pylint: disable=super-init-not-called The object has the following public variables: - `module_id`: The internal unique id used for this module. - `module_name`: The actual name of the module (the part after the last dot). - - `module_base`: The repository name of the module - `state`: The local state object. Also as a `miniflask` object itself it inherits all methods described in [miniflask object](../02-miniflask-instance/) with the exceptions listed in this chapter. @@ -1246,7 +1235,6 @@ def __init__(self, module_name, mf): # pylint: disable=super-init-not-called self.module_id = module_name self.module_id_initial = module_name 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_registrations) self._recently_loaded = [] diff --git a/src/miniflask/util.py b/src/miniflask/util.py index d24bb7e4..5f806396 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -47,7 +47,6 @@ def getModulesAvail(module_dirs, f=None): # module found f[module_name_id] = { - 'base_id': base_module_name, 'id': module_name_id, 'lowpriority': path.exists(path.join(dirpath, ".lowpriority")), 'importname': full_base_module_name + "." + local_import_name From 2194c6ccbaf568dd48c666c09d994b31283f5234 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 14:56:48 +0200 Subject: [PATCH 22/45] tests: replaced test_basic_import_dict by test_basic_import_{relativebasedir,selectivebasedir} --- ...y => test_basic_import_relativebasedir.py} | 27 +++++++++++++++++-- .../test_basic_import_selectivebasedir.py | 26 ++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) rename tests/basic/module_loading/{test_basic_import_dict.py => test_basic_import_relativebasedir.py} (66%) create mode 100644 tests/basic/module_loading/test_basic_import_selectivebasedir.py diff --git a/tests/basic/module_loading/test_basic_import_dict.py b/tests/basic/module_loading/test_basic_import_relativebasedir.py similarity index 66% rename from tests/basic/module_loading/test_basic_import_dict.py rename to tests/basic/module_loading/test_basic_import_relativebasedir.py index ad8eed36..17ef4729 100644 --- a/tests/basic/module_loading/test_basic_import_dict.py +++ b/tests/basic/module_loading/test_basic_import_relativebasedir.py @@ -1,10 +1,33 @@ -from pathlib import Path +import os import pytest import miniflask # noqa: E402 +# to test relative imports, we have to assume that the base-directory can be anywhere, for pytests sake +relpath = os.path.relpath(os.path.dirname(__file__), os.getcwd()) + def init_mf(): - return miniflask.init(module_dirs={"modules": str(Path(__file__).parent / "modules_b")}, debug=True) + return miniflask.init(module_dirs=[relpath + "/" + "./modules"], debug=True) + + +def test_setup(): + mf = init_mf() + mf.run(argv=[], modules=["miniflask.modules"]) + assert sorted([m for m in mf.modules_avail.keys() if not m.startswith("miniflask")]) == [ + 'modules.otherdir.module2', + 'modules.parentdir.module1', + 'modules.parentdir.module2', + 'modules.parentdir.module3', + 'modules.parentdir.module3.submodule', + 'modules.parentdir.module3.submodule.subsubmodule', + 'modules.parentdir.module3.submodule2', + 'modules.parentdir.module3.submodule_dir.submodule_with_folder_in_between', + 'modules.parentdir.module3.submodule_without_autoload', + 'modules.parentdir.module4', + 'modules.parentdir.module5', + 'modules.parentdir.module6', + 'modules.parentdir.module7', + ] def test_shortid(): diff --git a/tests/basic/module_loading/test_basic_import_selectivebasedir.py b/tests/basic/module_loading/test_basic_import_selectivebasedir.py new file mode 100644 index 00000000..33d63e3f --- /dev/null +++ b/tests/basic/module_loading/test_basic_import_selectivebasedir.py @@ -0,0 +1,26 @@ +import os +import miniflask # noqa: E402 + +# to test relative imports, we have to assume that the base-directory can be anywhere, for pytests sake +relpath = os.path.relpath(os.path.dirname(__file__), os.getcwd()) + + +def init_mf(): + return miniflask.init(module_dirs=[ + relpath + "/" + "./modules/parentdir/module3", + relpath + "/" + "./modules/otherdir/module2" + ], debug=True) + + +def test_setup(): + mf = init_mf() + mf.run(argv=[], modules=["miniflask.modules"]) + assert sorted([m for m in mf.modules_avail.keys() if not m.startswith("miniflask")]) == [ + 'module2', + 'module3', + 'module3.submodule', + 'module3.submodule.subsubmodule', + 'module3.submodule2', + 'module3.submodule_dir.submodule_with_folder_in_between', + 'module3.submodule_without_autoload' + ] From 8aa3450206bd8dcc4b3f8afa78388a0542d62e6f Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 7 Oct 2022 14:57:01 +0200 Subject: [PATCH 23/45] bugfix: fix module id for base directories --- src/miniflask/util.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/miniflask/util.py b/src/miniflask/util.py index 5f806396..47aa1c24 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -30,7 +30,10 @@ def getModulesAvail(module_dirs, f=None): for (dirpath, dirnames, filenames) in walk(directory): local_import_name = dirpath[len(directory) + 1:].replace(path.sep, ".") - module_name_id = base_module_name + "." + local_import_name + if local_import_name: + module_name_id = base_module_name + "." + local_import_name + else: + module_name_id = base_module_name # empty module id is not allowed if len(module_name_id) == 0: From 534ff5a3dc025c2a694f248609982d27cb6c3631 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 14 Oct 2022 12:34:27 +0200 Subject: [PATCH 24/45] specify module-directories by specifying sys.path package names --- src/miniflask/miniflask.py | 781 ++++++++++++++++++++++++++++--------- src/miniflask/util.py | 30 +- 2 files changed, 608 insertions(+), 203 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 3d8546e5..43dc032f 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -14,18 +14,40 @@ from colored import fg, attr # package modules -from .exceptions import save_traceback, format_traceback_list, RegisterError, StateKeyError +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, 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 +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, +) from .settings import listsettings -def print_info(*args, color=fg('green'), msg="INFO"): - print(color + attr('bold') + msg + attr('reset') + color + ": " + attr('reset'), *args, attr('reset')) +def print_info(*args, color=fg("green"), msg="INFO"): + print( + color + attr("bold") + msg + attr("reset") + color + ": " + attr("reset"), + *args, + attr("reset"), + ) def get_default_args(func): @@ -39,20 +61,20 @@ def get_default_args(func): def registerPredefined(modules_avail): for m in ["modules", "events", "info", "settings", "definitions"]: - module_name_id = 'miniflask.' + m - importname = 'miniflask.' + m + module_name_id = "miniflask." + m + importname = "miniflask." + m modules_avail[module_name_id] = { - 'id': module_name_id, - 'importname': importname, - 'lowpriority': False + "id": module_name_id, + "importname": importname, + "lowpriority": False, } # ================ # # MiniFlask Kernel # # ================ # -class miniflask(): - def __init__(self, module_dirs, debug=False): +class miniflask: + def __init__(self, *module_repositories, debug=False): r"""miniflask.init Initializes miniflask with a module repository. @@ -60,12 +82,12 @@ def __init__(self, module_dirs, debug=False): By initializing miniflask, we have to define the folders miniflask will look in. Args: - - `module_dirs`: a string, a list or a dict of paths to module repositories. - - **String**: specifies a single path to the module repository to use. + - `module_repositories`: a string, a list python packages to search module repositories in. + - **String**: specifies a single import path to the module repository to use. (The directory name will also be repository name / module prefix for all modules inside that folder.) - - **List**: specifies multiple paths to the module repository to use. + - **List**: specifies multiple import path to the module repository to use. (The directory names will also be repository names / module prefixes for all modules inside that folders.) - - `debug`: Debug Mode + - `debug`: Debug Mode Debug Mode disables catching/printing + beautifying Exceptions. Also, it disables truncating the traceback messages of internal miniflask functions. Examples: @@ -81,19 +103,16 @@ def __init__(self, module_dirs, debug=False): """ # noqa: W291 self._instance_id = str(random.getrandbits(128)) self.debug = debug - if not module_dirs: + if not module_repositories: return # module dir to be read from - if isinstance(module_dirs, list): - self.module_dirs = {path.basename(m): m for m in module_dirs} - elif isinstance(module_dirs, str): - self.module_dirs = {path.basename(module_dirs): module_dirs} - else: - raise ValueError("Only string or list allowed for `module_dirs'. Found type '%s'." % type(module_dirs)) + self.module_repositories = {m.split(".")[-1]: m for m in module_repositories} # arguments from cli-stdin - self.settings_parser = ArgumentParser(usage=sys.argv[0] + " modulelist [optional arguments]") + self.settings_parser = ArgumentParser( + usage=sys.argv[0] + " modulelist [optional arguments]" + ) self.cli_required_arguments = [] self.default_modules = [] self.default_modules_overwrites = [] @@ -107,11 +126,13 @@ def __init__(self, module_dirs, debug=False): self.event = event(self, optional=False) self.event.optional = event(self, optional=True) self.state = {} - self.state_registrations = {} # saves the information of the respective variables (initial value, registration, etc.) + 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) + self.modules_avail = getModulesAvail(self.module_repositories) registerPredefined(self.modules_avail) self._varid_list = [] self._recently_loaded = [] @@ -123,45 +144,55 @@ def __init__(self, module_dirs, debug=False): # ------- # # helpers # # ------- # - def print_heading(self, *args, color=fg('blue'), margin=8): + def print_heading(self, *args, color=fg("blue"), margin=8): line = "—" * self._consolecolumns if len(args) > 0: s = " ".join(args) - line = line[:margin] + " " + s + " " + line[margin + len(s) + 2:] + line = line[:margin] + " " + s + " " + line[margin + len(s) + 2 :] print() - print(color + attr('bold') + line + attr('reset')) + print(color + attr("bold") + line + attr("reset")) def print_recently_loaded(self, prepend="", loading_text=highlight_loading): last = "" last_formatted = "" for i, mod in enumerate(self._recently_loaded): - module_id = module_id_formatted = module_id = mod.miniflask_obj.module_id_initial + module_id = ( + module_id_formatted + ) = module_id = mod.miniflask_obj.module_id_initial # if previously printed parent, make the module_id shorter if module_id.startswith(last): - module_id_formatted = last_formatted + module_id[len(last):] + module_id_formatted = last_formatted + module_id[len(last) :] is_last = i == len(self._recently_loaded) - 1 has_children = len(mod.miniflask_obj._recently_loaded) > 0 - part_of_next = i < len(self._recently_loaded) - 1 and self._recently_loaded[i + 1].miniflask_obj.module_id_initial.startswith(module_id) + part_of_next = i < len(self._recently_loaded) - 1 and self._recently_loaded[ + i + 1 + ].miniflask_obj.module_id_initial.startswith(module_id) if is_last: - tree_symb = " " # noqa: E221 + tree_symb = " " # noqa: E221 tree_symb_current = "╰── " # "└── " elif has_children: - tree_symb = "│ " # noqa: E221 + tree_symb = "│ " # noqa: E221 tree_symb_current = "│ " else: - tree_symb = "│ " # noqa: E221 + tree_symb = "│ " # noqa: E221 tree_symb_current = "├── " # "├── " if not part_of_next: if prepend == "": print(loading_text(module_id_formatted)) else: - print(prepend + tree_symb_current + loading_text(module_id_formatted)) + print( + prepend + tree_symb_current + loading_text(module_id_formatted) + ) if len(mod.miniflask_obj._recently_loaded) > 0: - mod.miniflask_obj.print_recently_loaded(prepend + tree_symb, loading_text) + mod.miniflask_obj.print_recently_loaded( + prepend + tree_symb, loading_text + ) last = module_id - last_formatted = loading_text(module_id_formatted) if part_of_next else module_id + last_formatted = ( + loading_text(module_id_formatted) if part_of_next else module_id + ) # ==================== # # module introspection # @@ -177,9 +208,9 @@ def getModuleEvents(self, module_id, mf=None): Retrieve List of Events defined by specified module. Args: - - `module_id`: (required) + - `module_id`: (required) unique module id - - `mf`: + - `mf`: Internal variable to specify the miniflask object to register the events into. If set to `None` no events will be registered. """ # noqa: W291 if not mf: @@ -194,7 +225,16 @@ def getModuleEvents(self, module_id, mf=None): return mf.getEvents() # pretty print of all available modules - def showModules(self, directory=None, prepend="", id_pre=None, with_event=True, direct_print=True, visited=None): # noqa: C901 too-complex pylint: disable=inconsistent-return-statements + def showModules( + self, + directory=None, + prepend="", + id_pre=None, + with_event=True, + direct_print=True, + visited=None, + ): # noqa: C901 too-complex pylint: disable=inconsistent-return-statements + out = "" any_module_found = False @@ -202,8 +242,13 @@ def showModules(self, directory=None, prepend="", id_pre=None, with_event=True, visited = set() if not directory: - for basename, loop_directory in self.module_dirs.items(): - self.showModules(loop_directory, prepend=prepend, id_pre=basename if id_pre is None else id_pre + "." + basename, with_event=with_event) + for basename, base_import_path in self.module_repositories.items(): + self.showModules( + loop_directory, + prepend=prepend, + id_pre=basename if id_pre is None else id_pre + "." + basename, + with_event=with_event, + ) return if id_pre is None: @@ -211,8 +256,14 @@ def showModules(self, directory=None, prepend="", id_pre=None, with_event=True, if len(prepend) == 0: out += "\n" out += highlight_name(path.basename(id_pre)) + "\n" - dirs = [d for d in listdir(directory) if path.isdir(path.join(directory, d)) and not d.startswith("_")] - visited_next = visited.union(set(path.realpath(path.join(directory, d)) for d in dirs)) + dirs = [ + d + for d in listdir(directory) + if path.isdir(path.join(directory, d)) and not d.startswith("_") + ] + visited_next = visited.union( + set(path.realpath(path.join(directory, d)) for d in dirs) + ) for i, d in enumerate(dirs): if d.startswith("."): continue @@ -235,9 +286,23 @@ def showModules(self, directory=None, prepend="", id_pre=None, with_event=True, else: tree_symb = "├── " is_last = False - append = " " + fg('blue') + "(" + shortestid + ")" + attr('reset') if is_module and not has_shortid else "" - append += attr('dim') + " (low-priority module)" + attr('reset') if is_lowpriority_module else "" - out += prepend + tree_symb + (highlight_name(d) if is_module else d) + append + "\n" + append = ( + " " + fg("blue") + "(" + shortestid + ")" + attr("reset") + if is_module and not has_shortid + else "" + ) + append += ( + attr("dim") + " (low-priority module)" + attr("reset") + if is_lowpriority_module + else "" + ) + out += ( + prepend + + tree_symb + + (highlight_name(d) if is_module else d) + + append + + "\n" + ) tree_symb_next = " " if is_last else "│ " if is_module: @@ -246,9 +311,22 @@ def showModules(self, directory=None, prepend="", id_pre=None, with_event=True, if len(events) > 0: for e in events: unique_flag = "!" if e[1] else ">" - print(prepend + tree_symb_next + unique_flag + " " + highlight_event(e[0])) - - out_sub, module_found_sub = self.showModules(path.join(directory, d), prepend=prepend + tree_symb_next, id_pre=module_id, with_event=with_event, direct_print=False, visited=visited_next) + print( + prepend + + tree_symb_next + + unique_flag + + " " + + highlight_event(e[0]) + ) + + out_sub, module_found_sub = self.showModules( + path.join(directory, d), + prepend=prepend + tree_symb_next, + id_pre=module_id, + with_event=with_event, + direct_print=False, + visited=visited_next, + ) if module_found_sub: out += out_sub if direct_print: @@ -289,15 +367,31 @@ def getModuleId(self, module_id): # if more than one module found, exclude all low-priority modules if len(found_modules) > 1: - found_modules = list(filter(lambda mid: not self.modules_avail[mid]["lowpriority"], found_modules)) + found_modules = list( + filter( + lambda mid: not self.modules_avail[mid]["lowpriority"], + found_modules, + ) + ) # if more than one module found let the user know if len(found_modules) > 1: - raise ValueError(highlight_error() + "Module-Identifier '%s' is not unique. Found %i modules:\n\t%s" % (highlight_module(module_id), len(found_modules), "\n\t".join(found_modules))) + raise ValueError( + highlight_error() + + "Module-Identifier '%s' is not unique. Found %i modules:\n\t%s" + % ( + highlight_module(module_id), + len(found_modules), + "\n\t".join(found_modules), + ) + ) # no module found with both variants if len(found_modules) == 0: - raise ValueError(highlight_error() + "Module '%s' not known." % highlight_module(module_id)) + raise ValueError( + highlight_error() + + "Module '%s' not known." % highlight_module(module_id) + ) # module_id is a unique identifier module = found_modules[0] @@ -309,7 +403,10 @@ def getModuleShortId(self, module_id): Given the full unique module id this method returns the shortest module id that is still uniquely assignable by miniflask. """ if module_id not in self.modules_avail: - raise ValueError(highlight_error() + "Module '%s' not known." % highlight_module(module_id)) + raise ValueError( + highlight_error() + + "Module '%s' not known." % highlight_module(module_id) + ) uniqueId = self.modules_avail[module_id]["id"].split(".") # find the shortest substring to match a module uniquely @@ -323,14 +420,16 @@ def getModuleShortId(self, module_id): return module_id # maps 'folder.subfolder.module.list.of.vars' to 'folder.subfoldder.module' - def _getModuleIdFromVarId(self, varid, varid_list=None, scope=None): # noqa: C901 too-complex + def _getModuleIdFromVarId( + self, varid, varid_list=None, scope=None + ): # noqa: C901 too-complex # try to use scope as module id if scope is not None: try: module_id = self.getModuleId(scope) if varid.startswith(scope): - varid = varid[len(scope) + 1:] + varid = varid[len(scope) + 1 :] return module_id, varid except ValueError: pass @@ -349,7 +448,15 @@ def _getModuleIdFromVarId(self, varid, varid_list=None, scope=None): # noqa: C9 return None, ".".join(varid_list) # loads module (once) - def load(self, module_name, verbose=True, auto_query=True, loading_text=highlight_loading, as_id=None, bind_events=True): # noqa: C901 too-complex pylint: disable=too-many-statements + def load( + self, + module_name, + verbose=True, + auto_query=True, + loading_text=highlight_loading, + as_id=None, + bind_events=True, + ): # noqa: C901 too-complex pylint: disable=too-many-statements r""" Directly load a module by name @@ -358,18 +465,18 @@ def load(self, module_name, verbose=True, auto_query=True, loading_text=highligh - To prevent this, add the global variable `register_parents = False` to the modules `__init__.py`. Args: - - `module_name`: (required) + - `module_name`: (required) Module name to be loaded directly. - Prepending the module name with a `-` sign lets miniflask ignore the module. - Can be a fuzzy or complete module identifier - Can be a `str`, a python `list` or string containing a list of modules seperated by a comma. - - `verbose`: (default: `True`) + - `verbose`: (default: `True`) Visualizes the module tree that has been loaded during this call. - - `auto_query`: (default: `True`) + - `auto_query`: (default: `True`) Enables/Disables fuzzy search. - - `as_id`: (default: `None`) + - `as_id`: (default: `None`) The module id to be used to register the module upon loading. - - `bind_events`: (default: `None`) + - `bind_events`: (default: `None`) Registers all events of the module to be loaded. Examples: @@ -395,7 +502,14 @@ def load(self, module_name, verbose=True, auto_query=True, loading_text=highligh module_name = module_name.split(",") if isinstance(module_name, list): for m in module_name: - self.load(m, verbose=verbose, auto_query=auto_query, loading_text=loading_text, as_id=as_id, bind_events=bind_events) + self.load( + m, + verbose=verbose, + auto_query=auto_query, + loading_text=loading_text, + as_id=as_id, + bind_events=bind_events, + ) return if module_name.startswith("-"): @@ -408,7 +522,10 @@ def load(self, module_name, verbose=True, auto_query=True, loading_text=highligh if auto_query: module_name = self.getModuleId(module_name) elif module_name not in self.modules_avail: - raise ValueError(highlight_error() + "Module '%s' not known." % highlight_module(module_name)) + raise ValueError( + highlight_error() + + "Module '%s' not known." % highlight_module(module_name) + ) # check if already loaded if module_name in self.modules_loaded and as_id is None: @@ -417,7 +534,10 @@ def load(self, module_name, verbose=True, auto_query=True, loading_text=highligh # load module mod = self.import_module(module_name) if not hasattr(mod, "register"): - raise ValueError(highlight_error() + "Module '%s' does not register itself." % module_name) + raise ValueError( + highlight_error() + + "Module '%s' does not register itself." % module_name + ) module_name = module_name if as_id is None else as_id mod.miniflask_obj = miniflask_wrapper(module_name, self) mod.miniflask_obj.bind_events = bind_events @@ -425,13 +545,25 @@ def load(self, module_name, verbose=True, auto_query=True, loading_text=highligh # first load all parents # (starting with root parent, specializing with every step) - if not hasattr(mod, 'register_parents') or mod.register_parents: + if not hasattr(mod, "register_parents") or mod.register_parents: module_path = module_name.split(".") for depth in range(1, len(module_path)): parent_module = ".".join(module_path[:depth]) - if parent_module in self.modules_avail and parent_module not in self.modules_loaded: - parent_as_id = None if as_id is None else ".".join(as_id.split(".")[:-1]) - self.load(parent_module, verbose=False, auto_query=False, loading_text=loading_text, as_id=parent_as_id, bind_events=bind_events) + if ( + parent_module in self.modules_avail + and parent_module not in self.modules_loaded + ): + parent_as_id = ( + None if as_id is None else ".".join(as_id.split(".")[:-1]) + ) + self.load( + parent_module, + verbose=False, + auto_query=False, + loading_text=loading_text, + as_id=parent_as_id, + bind_events=bind_events, + ) # remember loaded modules self._recently_loaded.append(mod) @@ -447,11 +579,13 @@ def load(self, module_name, verbose=True, auto_query=True, loading_text=highligh self._recently_loaded = [] # register default module that is loaded if none of glob is matched - def register_default_module(self, module, required_event=None, required_id=None, overwrite_globals=None): + def register_default_module( + self, module, required_event=None, required_id=None, overwrite_globals=None + ): r""" Specify modules to load if specific behaviour is not yet matched by already loaded modules. - In more detail, this allows modules to be loaded depending on the choice of loaded modules upon start of the whole script. + In more detail, this allows modules to be loaded depending on the choice of loaded modules upon start of the whole script. Typically, the requirement will be tested after parsing the modules given using cli-arguments. # Note {.alert} @@ -465,16 +599,16 @@ def register_default_module(self, module, required_event=None, required_id=None, - the latest settings overwrite the older settings Args: - - `module`: (required) + - `module`: (required) Module name to be loaded if the specified requirement is not met. - Can be fuzzy or complete - Can be a python list of module names. - - `required_event`: + - `required_event`: Specifies the event name to be used as a condition to be met, otherwise the specified modules will be loaded. - - `required_id`: + - `required_id`: Specifies a regular expression that shall be used as a test condition. If there was no match against all loaded module ids, the specified modules will be loaded. - - `overwrite_globals`: - This argument takes a `dict` and binds a `register_globals` call to be called after the specified have been called. + - `overwrite_globals`: + This argument takes a `dict` and binds a `register_globals` call to be called after the specified have been called. (If no modules are loaded due to already fulfilled conditions, the dict will be discarded.) Examples: @@ -498,11 +632,19 @@ def register_default_module(self, module, required_event=None, required_id=None, if overwrite_globals is None: overwrite_globals = {} if required_event and required_id: - raise RegisterError("Default Modules should depend either on a event interface OR a regular expression. However, both are given") + raise RegisterError( + "Default Modules should depend either on a event interface OR a regular expression. However, both are given" + ) if not required_event and not required_id: - raise RegisterError("Default Modules should depend either on a event interface OR a regular expression. However, none are given") - self.default_modules.append((module, required_event, required_id, overwrite_globals, save_traceback())) - self.default_modules_overwrites.append((module, required_event, required_id, overwrite_globals, save_traceback())) + raise RegisterError( + "Default Modules should depend either on a event interface OR a regular expression. However, none are given" + ) + self.default_modules.append( + (module, required_event, required_id, overwrite_globals, save_traceback()) + ) + self.default_modules_overwrites.append( + (module, required_event, required_id, overwrite_globals, save_traceback()) + ) # saves function to a given (event-)name def register_event(self, name, fn, unique=True, call_before_after=True): @@ -513,14 +655,14 @@ def register_event(self, name, fn, unique=True, call_before_after=True): Note, that `init`, `main` and `final` are predefined event names that are called automatically on every [`mf.run()`](../../08-API/02-miniflask-Instance/10-run.md) call. Args: - - `name`: (required) + - `name`: (required) Event name to bind the function with. - - `fn`: (required) - The function to be bound to the name. + - `fn`: (required) + The function to be bound to the name. **Function Signatures**: - There are no requirements to the function signatures for plain events. - However, it is possible to prepend the argument list using the keywords: `state`, `event` and/or `mf` in any order. + However, it is possible to prepend the argument list using the keywords: `state`, `event` and/or `mf` in any order. Miniflask will look for these keywords in any event signature and pass the module specific objects, - `state`, (see also the API-Reference for [state](../05-state)) - `event` (see also the API-Reference for [event](../04-event)) and @@ -556,16 +698,16 @@ def after_fn(): - Any `before_`/`after_` event gets called with a unique event object, that behaves just like the *normal* event object. - This new event-object has a dict-attribute `.hook` - **`before_`-events can read and manipulate** the function call arguments *before* the actual event will be called using `event.hook["args"]` and `event.hook["kwargs"]` - - **`after_`-events can read** the function call arguments *after* the actual event has been called with (including the changes of potential `before_`-hooks using `event.hook["args"]` and `event.hook["kwargs"]` + - **`after_`-events can read** the function call arguments *after* the actual event has been called with (including the changes of potential `before_`-hooks using `event.hook["args"]` and `event.hook["kwargs"]` (**Note**: Any modification will only change the arguments for future `after_`-events but not for the function call itself.) - `after_`-events can additionally alternate the return value of the event by modifying `event.hook["result"]`. - both event types can access the name of the actual event-name using `event.hook["name"]` - - `unique`: (Default: `True`) - - Unique functions can only be registered by exactly one module. + - `unique`: (Default: `True`) + - Unique functions can only be registered by exactly one module. **Note**: Miniflask will throw an error if multiple modules register the same event. - Non-Unique events will be called in sequence of registration. The result of such an event is a list of all return values. - **Note**: Before/After events will be called only **once for non-unique event calls**. - - `call_before_after`: (Default: `True`) + - `call_before_after`: (Default: `True`) Turning this flag off will disable the possibility to hook to this function using before/after events. This is especially useful, if the before/after event shall be directly defined. @@ -611,15 +753,46 @@ def register(mf): # check if is unique event. if yes, check if event already registered if name in self.event_objs and (unique or self.event_objs[name].unique): - eobj = self.event_objs[name].modules if not self.event_objs[name].unique else [self.event_objs[name].modules] + eobj = ( + self.event_objs[name].modules + if not self.event_objs[name].unique + else [self.event_objs[name].modules] + ) # catch some user errors if not unique and self.event_objs[name].unique: - raise RegisterError(highlight_error() + "Event '%s' has been registered as `unique` before, but as `non-unique` now. Please check the registrations.\n\t(Imported by %s)" % (highlight_event(name), ", ".join(["'" + highlight_module(e.module_name) + "'" for e in eobj]))) + raise RegisterError( + highlight_error() + + "Event '%s' has been registered as `unique` before, but as `non-unique` now. Please check the registrations.\n\t(Imported by %s)" + % ( + highlight_event(name), + ", ".join( + ["'" + highlight_module(e.module_name) + "'" for e in eobj] + ), + ) + ) if unique and not self.event_objs[name].unique: - raise RegisterError(highlight_error() + "Event '%s' has been registered as `non-unique` before, but as `unique` now. Please check the registrations.\n\t(Imported by %s)" % (highlight_event(name), ", ".join(["'" + highlight_module(e.module_name) + "'" for e in eobj]))) - - raise RegisterError(highlight_error() + "Event '%s' is unique, and thus, cannot be imported twice.\n\t(Imported by %s)" % (highlight_event(name), ", ".join(["'" + highlight_module(e.module_name) + "'" for e in eobj]))) + raise RegisterError( + highlight_error() + + "Event '%s' has been registered as `non-unique` before, but as `unique` now. Please check the registrations.\n\t(Imported by %s)" + % ( + highlight_event(name), + ", ".join( + ["'" + highlight_module(e.module_name) + "'" for e in eobj] + ), + ) + ) + + raise RegisterError( + highlight_error() + + "Event '%s' is unique, and thus, cannot be imported twice.\n\t(Imported by %s)" + % ( + highlight_event(name), + ", ".join( + ["'" + highlight_module(e.module_name) + "'" for e in eobj] + ), + ) + ) # register event if name in self.event_objs: @@ -632,14 +805,23 @@ def register(mf): # Note: the problem lies in the fact that the true id of a variable is defined as scope.key, # however scope can be empty if key is meant as a reference in the global scope=="". # Otherwise, this function would be a lot simpler. - def register_defaults(self, defaults, scope="", overwrite=False, cliargs=True, parsefn=True, caller_traceback=None, missing_argument_message=None): + def register_defaults( + self, + defaults, + scope="", + overwrite=False, + cliargs=True, + parsefn=True, + caller_traceback=None, + missing_argument_message=None, + ): r""" Register variables bound to a module. The variable registration process is the miniflask feature to - allow users to overwrite *default parameters* based on the value types defined during registration using the cli, (See the section [Modules/Register Settings](../../03-Modules/02-Register-Settings.md) in the documentation for details how to overwrite registered variables using the CLI.) - - allow variables to form *dependency chains* in between modules + - allow variables to form *dependency chains* in between modules (”if one variable is like this then the other variable should be like that“) - but also to allow *other modules* to be predefined sets of default parameters themselves. @@ -650,10 +832,10 @@ def register_defaults(self, defaults, scope="", overwrite=False, cliargs=True, p - Floats (`float`) - Strings (`string`) - Boolean (`bool`) - - Enums (`Enum`) + - Enums (`Enum`) - 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: ...`. + - Lambda Expressions of the form `lambda state, event: ...`. - 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. @@ -678,20 +860,20 @@ def register_defaults(self, defaults, scope="", overwrite=False, cliargs=True, p - [`overwrite_globals`](../../08-API/03-register(mf\)-Object/08-overwrite_globals.md) Args: - - `defaults`: (required) + - `defaults`: (required) Dict of variables to define under the defined scope (variable name -> value). - - `scope`: - Scope to define variables in. + - `scope`: + Scope to define variables in. (Defaults to global scope. This is the main difference to the local mf-object variants.) - - `overwrite`: + - `overwrite`: Setting to `True` enables redefinition of predefined variables. Raises error if the variables to overwrite are not known. - - `cliargs`: + - `cliargs`: Setting to `False` disables to change that variable using CLI. - - `parsefn`: + - `parsefn`: Setting to `False` disables function parsing if the value is a method itself. Doing so may be required if the value to be saved is a function itself. By default miniflask will call function values to set the value dynamically as part of the variable dependency chain. - - `caller_traceback`: + - `caller_traceback`: The traceback to use when an error occurs during registration of any of the listed variables. (Defaults to current traceback). - - `missing_argument_message`: + - `missing_argument_message`: String to show the user whenever one of the given (and required) arguments are not present after CLI-parsing. Examples: @@ -752,7 +934,12 @@ class TESTENUM(Enum): # - 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 + is_dependency = ( + callable(val) + and not isinstance(val, type) + and not isinstance(val, EnumMeta) + and parsefn + ) node = state_node( varid=varname, mf=self, @@ -761,7 +948,7 @@ class TESTENUM(Enum): parsefn=parsefn, is_ovewriting=overwrite, missing_argument_message=missing_argument_message, - fn=val if is_dependency else None + fn=val if is_dependency else None, ) if overwrite: self._state_overwrites_list.append((varname, val, node)) @@ -772,7 +959,15 @@ class TESTENUM(Enum): 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 + def _settings_parser_add( + self, + varname, + val, + caller_traceback, + nargs=None, + default=None, + is_optional=False, + ): # noqa: C901 too-complex # lists are just multiple arguments if isinstance(val, (list, tuple)): @@ -781,8 +976,19 @@ def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, defau val_type, start_del, end_del = "list", "[", "]" 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) - return self._settings_parser_add(varname, val[0], caller_traceback, nargs="*" if isinstance(val, list) else len(val), default=val, is_optional=is_optional) + 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, + ) + 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): @@ -793,7 +999,7 @@ def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, defau argtype = val if val != bool else str2bool else: argtype = type(val) if not isinstance(val, bool) else str2bool - kwarg = {'dest': varname, 'type': argtype, 'nargs': nargs} + kwarg = {"dest": varname, "type": argtype, "nargs": nargs} # we know the default argument, if the value is given # otherwise the value is a required argument (to be tested later) @@ -808,20 +1014,29 @@ def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, defau if argtype == Enum: kwarg["action"] = EnumAction kwarg["type"] = val if isinstance(val, EnumMeta) else type(val) - elif argtype == str2bool and nargs != '*': # pylint: disable=comparison-with-callable - kwarg["nargs"] = '?' + elif ( + argtype == str2bool and nargs != "*" + ): # pylint: disable=comparison-with-callable + kwarg["nargs"] = "?" kwarg["const"] = True # define the actual arguments if argtype in [int, str, float, str2bool, Enum]: self.settings_parser.add_argument("--" + varname, **kwarg) else: - raise ValueError("Type '%s' not supported. (Used for setting '%s')" % (type(val), varname)) + raise ValueError( + "Type '%s' not supported. (Used for setting '%s')" + % (type(val), varname) + ) # for bool: enable --no-varname as alternative for --varname false # Note: this has to be defined AFTER --varname - if argtype == str2bool and nargs != '*': # pylint: disable=comparison-with-callable - self.settings_parser.add_argument('--no-' + varname, dest=varname, action='store_false') + if ( + argtype == str2bool and nargs != "*" + ): # pylint: disable=comparison-with-callable + self.settings_parser.add_argument( + "--no-" + varname, dest=varname, action="store_false" + ) # remember the varname also for fuzzy searching self._varid_list.append(varname) @@ -837,10 +1052,12 @@ def stop_parse(self): """ # noqa: W291 self.halt_parse = True - def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-statements - argv: List[str] or str = None, - optional: bool = True, - fuzzy_args: bool = True): + def parse_args( + self, # noqa: C901 too-complex pylint: disable=too-many-statements + argv: List[str] or str = None, + optional: bool = True, + fuzzy_args: bool = True, + ): r""" Parse CLI-Arguments. @@ -848,22 +1065,26 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme Typically you would call the run()-method instead. You will need this method only, if [run](./10-run.md) does not fit your needs. Args: - - `argv`: (can also be `str`) + - `argv`: (can also be `str`) Arguments to parse. If is `None` will use CLI arguments (`sys.argv[1:]`). - - `optional`: + - `optional`: If set to `False` disables the requirement to any modules to load. This option is interesting if your launch script is intended to use a predefined set of modules only. - - `fuzzy_args`: + - `fuzzy_args`: If set to `False` disables fuzzy variable name search for the module arguments. """ # noqa: W291 if self.argparse_called: - raise SystemError("The function `parse_args` has been called already. Did you maybe called `mf.parse_args()` and `mf.run()` in the same script? Solutions are:\n\t- Please use only one of those functions.\n\t- If you actually need both functions, please do not hesitate to write an issue on\n\t\thttps://github/da-h/miniflask/issues\n\t to explain yout used case.\n\t (It's not hard to implement, but I need to know, if and when this functionality is needed. ;) )") + raise SystemError( + "The function `parse_args` has been called already. Did you maybe called `mf.parse_args()` and `mf.run()` in the same script? Solutions are:\n\t- Please use only one of those functions.\n\t- If you actually need both functions, please do not hesitate to write an issue on\n\t\thttps://github/da-h/miniflask/issues\n\t to explain yout used case.\n\t (It's not hard to implement, but I need to know, if and when this functionality is needed. ;) )" + ) has_module_args = argv is None or argv == sys.argv if argv is None: # check if 'argv' is passed argv = sys.argv[1:] elif isinstance(argv, list): # check if passed 'argv' is a list pass - elif isinstance(argv, str): # check if passed 'argv' is a str (split by whitespace) + elif isinstance( + argv, str + ): # check if passed 'argv' is a str (split by whitespace) argv = argv.split(" ") else: # unknown passed variable (don't pass on any arguments) argv = [] @@ -871,8 +1092,8 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme # actually parse the input if has_module_args: parser = ArgumentParser() - parser.add_argument('cmds', type=str, nargs=1 if not optional else "?") - parser.add_argument('module_arguments', nargs=ARGPARSE_REMAINDER) + parser.add_argument("cmds", type=str, nargs=1 if not optional else "?") + parser.add_argument("module_arguments", nargs=ARGPARSE_REMAINDER) args = parser.parse_args(argv) # save remainder for module setting parser @@ -881,9 +1102,9 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme # load modules if args.cmds: if optional: - cmds = args.cmds.split(',') + cmds = args.cmds.split(",") else: - cmds = args.cmds[0].split(',') + cmds = args.cmds[0].split(",") for cmd in cmds: if self.halt_parse: break @@ -901,15 +1122,25 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme # the default_module list gives us the order (last-in, first-out) of the default-modules to call # we assume that newer default modules are meant to overwrite older ones while len(self.default_modules) > 0: - module, evt, glob, overwrite_globals, caller_traceback = self.default_modules.pop() + ( + module, + evt, + glob, + overwrite_globals, + caller_traceback, + ) = self.default_modules.pop() del overwrite_globals, caller_traceback if evt: if not isinstance(module, list): module = [module] - modules_already_loaded = all(self.getModuleId(m) in self.modules_loaded for m in module) + modules_already_loaded = all( + self.getModuleId(m) in self.modules_loaded for m in module + ) if not modules_already_loaded and evt not in self.event_objs: - self.load(module, loading_text=partial(highlight_loading_default, evt)) + self.load( + module, loading_text=partial(highlight_loading_default, evt) + ) else: found = self.event_objs[evt].modules if not isinstance(found, list): @@ -918,9 +1149,13 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme print(highlight_loaded_default(found, evt)) elif glob: - found = [highlight_loading_module(x) for x in keys if re.search(glob, x)] + found = [ + highlight_loading_module(x) for x in keys if re.search(glob, x) + ] if len(found) == 0: - self.load(module, loading_text=partial(highlight_loading_default, glob)) + self.load( + module, loading_text=partial(highlight_loading_default, glob) + ) elif len(found) > 1: print(highlight_loaded_default(found, glob)) else: @@ -928,20 +1163,46 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme # in case a default module / overwrite_global-combination is used in two places in the loading-tree, # we assume that we want to overwrite the older values with the newer values - for module, _, _, overwrite_globals, caller_traceback in self.default_modules_overwrites: + for ( + module, + _, + _, + overwrite_globals, + caller_traceback, + ) in self.default_modules_overwrites: if not isinstance(module, list): module = [module] if all(self.getModuleId(m) in self.modules_loaded for m in module): - self.register_defaults(overwrite_globals, scope="", overwrite=True, caller_traceback=caller_traceback) + self.register_defaults( + overwrite_globals, + scope="", + overwrite=True, + caller_traceback=caller_traceback, + ) # check fuzzy matching of overwrites 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()) + 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=node.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=node.caller_traceback) + 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, + ) varname = found_varids[0] @@ -953,23 +1214,46 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme 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()} + 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) + ( + 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 - ])) + 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 - ])) + 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}") + 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) @@ -978,8 +1262,17 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme 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)) + 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"] @@ -994,7 +1287,11 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme # split `--varname=value` expressions to `--varname value` # (argparse does only allow the `key=value`-syntax for single-dash definitions) - argv = [v for val in argv for v in (val.split("=", 1) if val.startswith("--") else [val])] # pylint: disable=superfluous-parens + argv = [ + v + for val in argv + for v in (val.split("=", 1) if val.startswith("--") else [val]) + ] # pylint: disable=superfluous-parens # parse nested expressions argv_flat = [] @@ -1009,7 +1306,9 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme if arg == "]": namespaces.pop() if len(namespaces) == 0: - raise ValueError("Nesting-Error during parse of CLI-Arguments. Did you forget to include an '[' ?") + raise ValueError( + "Nesting-Error during parse of CLI-Arguments. Did you forget to include an '[' ?" + ) continue # all non-arguments are values and thus should be retained @@ -1041,7 +1340,11 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme if not varid.startswith("--"): # special case: negative scientific notation currently does not work for argparse - if varid.startswith("-") and "e" in varid and varid[1:].replace("e", "").isnumeric(): + if ( + varid.startswith("-") + and "e" in varid + and varid[1:].replace("e", "").isnumeric() + ): try: argv[i] = str(float(varid)) except ValueError: @@ -1069,12 +1372,25 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme # if no matching varid found, check for fuzzy identifier if len(found_varids) > 1: argv[i] = highlight_module(argv[i]) - raise ValueError(highlight_error() + "Variable-Identifier '--%s' is not unique. Found %i variables:\n\t%s\n\n Call:\n %s" % (highlight_module(varid), len(found_varids), "\n\t".join(found_varids), " ".join(argv))) + raise ValueError( + highlight_error() + + "Variable-Identifier '--%s' is not unique. Found %i variables:\n\t%s\n\n Call:\n %s" + % ( + highlight_module(varid), + len(found_varids), + "\n\t".join(found_varids), + " ".join(argv), + ) + ) # no module found with both variants if len(found_varids) == 0: argv[i] = highlight_module(argv[i]) - raise ValueError(highlight_error() + "Variable '--%s' not known.\n\n Call:\n %s" % (highlight_module(varid), " ".join(argv))) + raise ValueError( + highlight_error() + + "Variable '--%s' not known.\n\n Call:\n %s" + % (highlight_module(varid), " ".join(argv)) + ) varid = found_varids[0] @@ -1100,29 +1416,77 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme 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)]) + arg_err_str = "\t" + " or ".join( + [highlight_module("--" + arg) for arg in reversed(args)] + ) 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')) + 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(node.missing_argument_message, str): - error_message_str = linesep * 2 + attr("bold") + node.missing_argument_message + attr("reset") + 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) + raise ValueError( + "Missing CLI-arguments or unspecified variables during miniflask call." + + linesep + + linesep.join(args_err_strs) + + error_message_str + ) # 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_registrations), 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']) + self.settings_parser.parse_args(["--help"]) # mark this instance as run self.argparse_called = True - def run(self, modules: List[str] or str = None, call: str = "main", argv: List[str] or str = None): # noqa: C901 too-complex pylint: disable=too-many-statements + def run( + self, + modules: List[str] or str = None, + call: str = "main", + argv: List[str] or str = None, + ): # noqa: C901 too-complex pylint: disable=too-many-statements r""" Entrypoint of most miniflask programs. @@ -1138,15 +1502,15 @@ def run(self, modules: List[str] or str = None, call: str = "main", argv: List[s This method also pretty prints - the distinct phases described above - - uncatched exceptions that occur during any event. + - uncatched exceptions that occur during any event. (This method strips any miniflask-related exceptinos from the traceback. In case a debugger is used or the `debug` flag is set for the miniflask instance, the full exception traceback is shown.) Args: - - `modules`: + - `modules`: List or String of modules - - `call`: + - `call`: The name of the default “main” event. - - `argv`: + - `argv`: Arguments to parse. If is `None` will use CLI arguments (`sys.argv[1:]`). """ # noqa: W291 if modules is None or (isinstance(modules, list) and len(modules) == 0): @@ -1165,7 +1529,7 @@ def run(self, modules: List[str] or str = None, call: str = "main", argv: List[s if not self.halt_parse: # optional init event - if hasattr(self.event, 'init'): + if hasattr(self.event, "init"): self.print_heading("init Event") self.event.optional.init() @@ -1180,35 +1544,47 @@ def run(self, modules: List[str] or str = None, call: str = "main", argv: List[s else: getattr(self.event, call)() else: - print("No event '{0}' registered. " - "Please make sure to register the event '{0}', " - "or provide a suitable event to call with mf.run(call=\"myevent\").".format(call)) + print( + "No event '{0}' registered. " + "Please make sure to register the event '{0}', " + 'or provide a suitable event to call with mf.run(call="myevent").'.format( + call + ) + ) # optional final event - if hasattr(self.event, 'final'): + if hasattr(self.event, "final"): self.print_heading("final Event") self.event.optional.final() except (RegisterError, StateKeyError) as e: - gettrace = getattr(sys, 'gettrace', None) + gettrace = getattr(sys, "gettrace", None) # check if debugger will catch this if not self.debug and (not gettrace or not gettrace()): tb = traceback.extract_tb(e.__traceback__) print() - print(fg("red") + "Uncatched Exception occured. Traceback:" + attr("reset")) + print( + fg("red") + + "Uncatched Exception occured. Traceback:" + + attr("reset") + ) print(format_traceback_list(tb, exc=e, ignore_miniflask=False)) sys.exit(1) else: raise except Exception as e: # pylint: disable=broad-except - gettrace = getattr(sys, 'gettrace', None) + gettrace = getattr(sys, "gettrace", None) # check if debugger will catch this if not self.debug and (not gettrace or not gettrace()): tb = traceback.extract_tb(e.__traceback__) print() - print(fg("red") + "Uncatched Exception occured. Traceback:" + attr("reset")) + print( + fg("red") + + "Uncatched Exception occured. Traceback:" + + attr("reset") + ) # nicer version of: print(traceback.format_exc()) print(format_traceback_list(tb, exc=e, ignore_miniflask=not self.debug)) @@ -1235,24 +1611,32 @@ def __init__(self, module_name, mf): # pylint: disable=super-init-not-called self.module_id = module_name self.module_id_initial = module_name self.module_name = module_name.split(".")[-1] - 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_registrations) + 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_registrations, + ) self._recently_loaded = [] self._defined_events = {} def _get_relative_module_id(self, possibly_relative_path, offset=1): - module_name, was_relative = get_relative_id(possibly_relative_path, self.module_id, offset=offset) + module_name, was_relative = get_relative_id( + possibly_relative_path, self.module_id, offset=offset + ) return module_name, was_relative def __getattr__(self, name): - orig_attr = super().__getattribute__('wrapped_class').__getattribute__(name) + orig_attr = super().__getattribute__("wrapped_class").__getattribute__(name) if callable(orig_attr): + def hooked(*args, **kwargs): result = orig_attr(*args, **kwargs) # prevent wrapped_class from becoming unwrapped if result == self.wrapped_class: return self return result + return hooked return orig_attr @@ -1269,7 +1653,10 @@ def redefine_scope(self, new_module_name): old_module_name = self.module_id new_module_name = self.set_scope(new_module_name) if new_module_name in self.modules_avail: - raise ValueError("Scope `%s` already used. Cannot define multiple modules using `redefine_scope`. Did you maybe mean to use `set_scope`?" % new_module_name) + raise ValueError( + "Scope `%s` already used. Cannot define multiple modules using `redefine_scope`. Did you maybe mean to use `set_scope`?" + % new_module_name + ) m = self.modules_avail[old_module_name] del self.modules_avail[old_module_name] m["id"] = new_module_name @@ -1321,7 +1708,7 @@ def as_is_callable(self, variable): Examples: - **Background / When to use this feature?** + **Background / When to use this feature?** Consider the following variable definitions of `var1` and `var2`, whene `MyClass` is callable, i.e. we can run `state["var1"](*args, **kwargs)`. ```python mf.register_helpers({ @@ -1346,7 +1733,7 @@ def load_as_child(self, module_name, bind_events=False, **kwargs): These kinds of child modules are a preliminary feature of miniflask. Thus, at the moment the function is rather limited: No events will be registered to be accessible globally by default. However, it is possible to access the state variables of that module. """ # noqa: W291 - self.load(module_name, as_id='.', bind_events=bind_events, **kwargs) + self.load(module_name, as_id=".", bind_events=bind_events, **kwargs) # enables relative imports def load(self, module_name, as_id=None, auto_query=True, **kwargs): @@ -1396,7 +1783,9 @@ def load(self, module_name, as_id=None, auto_query=True, **kwargs): # call load (but ensure no querying is made if relative imports were given) if "verbose" in kwargs: del kwargs["verbose"] - super().load(module_name, verbose=False, auto_query=auto_query, as_id=as_id, **kwargs) + super().load( + module_name, verbose=False, auto_query=auto_query, as_id=as_id, **kwargs + ) # checks if child modules are already loaded def any_child_loaded(self): @@ -1412,7 +1801,9 @@ def any_child_loaded(self): mf.load(".childmoduleA") ``` """ # noqa: W291 - return any(x for x in self.modules_loaded if re.search(self.module_id + r"\..*", x)) + return any( + x for x in self.modules_loaded if re.search(self.module_id + r"\..*", x) + ) # register default module that is loaded if none of glob is matched # (enables relative imports) @@ -1431,13 +1822,15 @@ def register_default_module(self, module, **kwargs): super().register_default_module(module, **kwargs) - def unregister_event(self, name: str, only_cache: bool = False, keep_attached_events: bool = True): + def unregister_event( + self, name: str, only_cache: bool = False, keep_attached_events: bool = True + ): r""" Clears an event by name. Args: - `name`: The event to clear from the event object. - - `only_cache`: + - `only_cache`: - Setting to `True` means that the event cache will be cleared. Upon the next call of `event.name` the cache will be rebuild. - Setting to `False` means that the event cache will be cleared *and* the internal event objects will be removed as well. Upon the next call of `event.name` miniflask will not recognize the event anymore. - `keep_attached_events`: By default (`True`), all events that are called together with this event (`before_`/`after_`-events) are kept. Setting to `False` clears those as well. @@ -1466,7 +1859,9 @@ def register_event(self, name: str, fn, **kwargs): super().register_event(name, fn, **kwargs) # overwrite event definition - def overwrite_event(self, name: str, fn, keep_attached_events: bool = True, **kwargs): + def overwrite_event( + self, name: str, fn, keep_attached_events: bool = True, **kwargs + ): r""" Unregister an existing event & redefine it using another function. @@ -1475,12 +1870,14 @@ def overwrite_event(self, name: str, fn, keep_attached_events: bool = True, **kw Args: - `name`: The event to clear from the event object. - - `only_cache`: + - `only_cache`: - Setting to `True` means that the event cache will be cleared. Upon the next call of `event.name` the cache will be rebuild. - Setting to `False` means that the event cache will be cleared *and* the internal event objects will be removed as well. Upon the next call of `event.name` miniflask will not recognize the event anymore. - `keep_attached_events`: By default (`True`), all events that are called together with this event (`before_`/`after_`-events) are kept. Setting to `False` clears those as well. """ # noqa: W291 - self.unregister_event(name, only_cache=False, keep_attached_events=keep_attached_events) + self.unregister_event( + name, only_cache=False, keep_attached_events=keep_attached_events + ) self._defined_events[name] = fn super().register_event(name, fn, **kwargs) diff --git a/src/miniflask/util.py b/src/miniflask/util.py index 47aa1c24..791f412a 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -3,11 +3,13 @@ from argparse import Action from os import walk, path from pathlib import Path +from pkgutil import resolve_name from colored import attr, fg +import inspect -# get total importable module name of directory, as seen from python +# get the absolute package name of the current file to cwd() # - i.e. $result can be loaded using `import $result` def get_full_base_module_name(directory): directory = Path(directory).absolute() @@ -21,22 +23,28 @@ def get_full_base_module_name(directory): # get modules in a directory -def getModulesAvail(module_dirs, f=None): +def getModulesAvail(python_import_paths, f=None): if f is None: f = {} - for base_module_name, directory in module_dirs.items(): - directory = str(directory) # in case directory is given as PosixPath etc. - full_base_module_name = get_full_base_module_name(directory) + for base_module_id, base_module_path in python_import_paths.items(): + if base_module_path.startswith("."): + stack_frame = inspect.stack()[2] # the frame in which miniflask.init has been called + callee_module_path = Path(stack_frame.filename).parent + base_module_path = get_full_base_module_name(callee_module_path) + base_module_path + module = resolve_name(base_module_path) + directory = module.__path__[0] for (dirpath, dirnames, filenames) in walk(directory): local_import_name = dirpath[len(directory) + 1:].replace(path.sep, ".") if local_import_name: - module_name_id = base_module_name + "." + local_import_name + module_id = base_module_id + "." + local_import_name + module_path = base_module_path + "." + local_import_name else: - module_name_id = base_module_name + module_id = base_module_id + module_path = base_module_path # empty module id is not allowed - if len(module_name_id) == 0: + if len(module_id) == 0: continue # ignore sub directories @@ -49,10 +57,10 @@ def getModulesAvail(module_dirs, f=None): continue # module found - f[module_name_id] = { - 'id': module_name_id, + f[module_id] = { + 'id': module_id, 'lowpriority': path.exists(path.join(dirpath, ".lowpriority")), - 'importname': full_base_module_name + "." + local_import_name + 'importname': module_path } return f From f6522a804773f84fd8b5fd91aeea9c967bc91088 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 14 Oct 2022 15:24:43 +0200 Subject: [PATCH 25/45] README.md: remove notes from README (not required anymore) --- tests/README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/README.md b/tests/README.md index c1624d13..98680bb3 100644 --- a/tests/README.md +++ b/tests/README.md @@ -28,14 +28,6 @@ or simply run all tests pytest ``` -#### Some notes - -- pytest executes all tests with the initial working directory (e.g. `tests`). - Therefore, relative module imports in miniflask down work and the absolute path is used (e.g. `str(Path(__file__).parent / "modules")` to add the `modules` directory as module). -- pytest adds additional arguments to the execution call. - Therefore, pass the `argv` to miniflask manually (e.g. `mf.run(argv=[])`). - - ### More information - [Full pytest documentation](https://docs.pytest.org/) From 5cc4949c7628c79efcf306b9217a1f224a5435b0 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 14 Oct 2022 15:25:10 +0200 Subject: [PATCH 26/45] doc: adapt to new api --- README.md | 2 +- docs/01-Getting-Started/index.md | 2 +- docs/03-Modules/01-Module-Declaration.md | 10 ++++------ docs/03-Modules/03-Register-Events.md | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8c891248..90528989 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Our main.py looks like this: import miniflask # initialize miniflask -mf = miniflask.init(module_dirs="./modules") +mf = miniflask.init(".modules") mf.run() ``` diff --git a/docs/01-Getting-Started/index.md b/docs/01-Getting-Started/index.md index 9e47a0c2..4acee2a9 100644 --- a/docs/01-Getting-Started/index.md +++ b/docs/01-Getting-Started/index.md @@ -62,7 +62,7 @@ Our main.py looks like this: import miniflask # initialize miniflask -mf = miniflask.init(module_dirs="./modules") +mf = miniflask.init(".modules") mf.parse_args() # check if all requested modules are loaded diff --git a/docs/03-Modules/01-Module-Declaration.md b/docs/03-Modules/01-Module-Declaration.md index cc901c68..3a53ae46 100644 --- a/docs/03-Modules/01-Module-Declaration.md +++ b/docs/03-Modules/01-Module-Declaration.md @@ -16,7 +16,7 @@ Telling MiniFlask what exists. import miniflask # tell miniflask where to look for modules -mf = miniflask.init(module_dirs="./modules") +mf = miniflask.init(".modules") # parse CLI-args & run the main event mf.run() @@ -48,7 +48,7 @@ For instance, we could organize the folder `./modules` like this: ```shell > ls main.py -modules/ +mods/ module1/__init__.py ... /.module module2/__init__.py @@ -63,11 +63,9 @@ modules/ Further, we initialized miniflask using the line ```py -mf = miniflask.init({ - "mods": "./modules", -}) +mf = miniflask.init(".mods") ``` -by specifying the name `mods` for our main repository contained in the folder `./modules/`. +by specifying the name `mods` for our main repository contained in the folder `./mods/`. Every module in this repository can now be referenced in two ways: \block[ diff --git a/docs/03-Modules/03-Register-Events.md b/docs/03-Modules/03-Register-Events.md index 62a0fc8c..916e2b18 100644 --- a/docs/03-Modules/03-Register-Events.md +++ b/docs/03-Modules/03-Register-Events.md @@ -36,7 +36,7 @@ def register(mf): import miniflask # initialize miniflask -mf = miniflask.init(module_dirs="./modules") +mf = miniflask.init(".modules") state, event = mf.state, mf.event mf.parse_args() mf.event.main() @@ -96,7 +96,7 @@ def register(mf): import miniflask # initialize miniflask -mf = miniflask.init(module_dirs="./modules") +mf = miniflask.init(".modules") state, event = mf.state, mf.event mf.parse_args() varA=42 From 4cbacc86701967d656af2d7a8a966378da06d3f5 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Fri, 14 Oct 2022 15:55:07 +0200 Subject: [PATCH 27/45] tests: adapt tests to new api --- .../list/test_argparse_list_overwrites.py | 14 ++++++-------- .../test_argparse_list_overwrites_multiple.py | 14 ++++++-------- tests/argparse/list/test_argparse_list_types.py | 4 +--- .../nested_arguments/test_nested_arguments.py | 3 +-- .../optional/test_argparse_optional_types.py | 8 +++----- .../test_argparse_required_overwrites.py | 6 ++---- .../required/test_argparse_required_types.py | 3 +-- .../test_argparse_required_with_error_message.py | 3 +-- tests/argparse/special/test_global.py | 10 ++++------ tests/argparse/special/test_helpers.py | 9 ++++----- tests/argparse/special/test_overwrite.py | 13 ++++++------- .../argparse/tuples/test_argparse_tuple_error.py | 3 +-- .../argparse/tuples/test_argparse_tuple_types.py | 4 +--- .../argparse/values/test_argparse_overwrites.py | 16 +++++++--------- tests/argparse/values/test_argparse_types.py | 4 +--- tests/basic/module_loading/test_basic_import.py | 3 +-- .../test_basic_import_relativebasedir.py | 2 +- .../test_basic_import_selectivebasedir.py | 5 +---- .../basic/module_loading/test_default_module.py | 3 +-- .../module_loading/test_parent_autoloading.py | 3 +-- .../basic/module_loading/test_relative_import.py | 3 +-- tests/event/beforeafter/test_beforeafter.py | 13 ++++++------- .../beforeafter/test_beforeafter_none_return.py | 13 ++++++------- .../beforeafter/test_beforeafter_nonunique.py | 9 ++++----- .../beforeafter/test_nonevent_beforeafter.py | 3 +-- .../exception_event/test_exception_event.py | 9 ++++----- tests/event/named_call/test_named_call.py | 3 +-- .../overwrite_event/test_overwrite_event.py | 9 ++++----- tests/event/register_event/_test_basic_event.py | 3 +-- .../register_event/_test_exception_in_event.py | 3 +-- .../_test_register_event_exception.py | 3 +-- tests/event/register_event/test_event_exists.py | 3 +-- .../test_register_event_during_event.py | 3 +-- .../unregister_event/test_unregister_event.py | 3 +-- tests/features/features/test_features.py | 3 +-- tests/state/dataclass_as_state/test_dataclass.py | 3 +-- tests/state/dependencies/setup.py | 8 ++------ tests/state/dynamic_state/test_dynamic_state.py | 3 +-- tests/state/enums/test_enums.py | 5 ++--- tests/state/outervar/test_outervar.py | 6 ++---- .../test_exception_register.py | 3 +-- tests/state/temporary/test_temporary.py | 4 +--- 42 files changed, 94 insertions(+), 151 deletions(-) diff --git a/tests/argparse/list/test_argparse_list_overwrites.py b/tests/argparse/list/test_argparse_list_overwrites.py index 3df74b7f..3e3c53ba 100644 --- a/tests/argparse/list/test_argparse_list_overwrites.py +++ b/tests/argparse/list/test_argparse_list_overwrites.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: [E402] def test_space(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -50,7 +48,7 @@ def test_space(capsys): def test_equal(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -95,7 +93,7 @@ def test_equal(capsys): def test_bool_int(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -116,7 +114,7 @@ def test_bool_int(capsys): def test_bool_yesno(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -137,7 +135,7 @@ def test_bool_yesno(capsys): def test_bool_tf(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -158,7 +156,7 @@ def test_bool_tf(capsys): def test_bool_truefalse(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/list/test_argparse_list_overwrites_multiple.py b/tests/argparse/list/test_argparse_list_overwrites_multiple.py index 1b790788..115d8712 100644 --- a/tests/argparse/list/test_argparse_list_overwrites_multiple.py +++ b/tests/argparse/list/test_argparse_list_overwrites_multiple.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: [E402] def test_space(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -50,7 +48,7 @@ def test_space(capsys): def test_equal(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -95,7 +93,7 @@ def test_equal(capsys): def test_bool_int(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -116,7 +114,7 @@ def test_bool_int(capsys): def test_bool_yesno(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -137,7 +135,7 @@ def test_bool_yesno(capsys): def test_bool_tf(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -158,7 +156,7 @@ def test_bool_tf(capsys): def test_bool_truefalse(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/list/test_argparse_list_types.py b/tests/argparse/list/test_argparse_list_types.py index b6bad46b..28e563f6 100644 --- a/tests/argparse/list/test_argparse_list_types.py +++ b/tests/argparse/list/test_argparse_list_types.py @@ -1,9 +1,7 @@ -from pathlib import Path - import miniflask # noqa: [E402] mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/nested_arguments/test_nested_arguments.py b/tests/argparse/nested_arguments/test_nested_arguments.py index cf3b7989..06b0a934 100644 --- a/tests/argparse/nested_arguments/test_nested_arguments.py +++ b/tests/argparse/nested_arguments/test_nested_arguments.py @@ -1,5 +1,4 @@ import re -from pathlib import Path import miniflask # noqa: E402 @@ -12,7 +11,7 @@ def ansi_escape(s): def init_mf(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load("all") diff --git a/tests/argparse/optional/test_argparse_optional_types.py b/tests/argparse/optional/test_argparse_optional_types.py index e9ff2847..7d73cf5c 100644 --- a/tests/argparse/optional/test_argparse_optional_types.py +++ b/tests/argparse/optional/test_argparse_optional_types.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: E402 def test_none(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -35,7 +33,7 @@ def test_none(capsys): def test_space(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -81,7 +79,7 @@ def test_space(capsys): def test_equal(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/required/test_argparse_required_overwrites.py b/tests/argparse/required/test_argparse_required_overwrites.py index 9e63924b..a573e1c0 100644 --- a/tests/argparse/required/test_argparse_required_overwrites.py +++ b/tests/argparse/required/test_argparse_required_overwrites.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: E402 def test_space(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -49,7 +47,7 @@ def test_space(capsys): def test_equal(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/required/test_argparse_required_types.py b/tests/argparse/required/test_argparse_required_types.py index 12e5e781..b8948817 100644 --- a/tests/argparse/required/test_argparse_required_types.py +++ b/tests/argparse/required/test_argparse_required_types.py @@ -1,9 +1,8 @@ import re -from pathlib import Path import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/required/test_argparse_required_with_error_message.py b/tests/argparse/required/test_argparse_required_with_error_message.py index e7522020..8c12edae 100644 --- a/tests/argparse/required/test_argparse_required_with_error_message.py +++ b/tests/argparse/required/test_argparse_required_with_error_message.py @@ -1,9 +1,8 @@ import re -from pathlib import Path import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/special/test_global.py b/tests/argparse/special/test_global.py index 8c3044d1..5b9788f0 100644 --- a/tests/argparse/special/test_global.py +++ b/tests/argparse/special/test_global.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: E402 def test_global_setup(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -22,7 +20,7 @@ def test_global_setup(capsys): def test_global_cli_override(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -40,7 +38,7 @@ def test_global_cli_override(capsys): def test_override(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -56,7 +54,7 @@ def test_override(capsys): def test_override_cli(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/special/test_helpers.py b/tests/argparse/special/test_helpers.py index 432e39bd..76926710 100644 --- a/tests/argparse/special/test_helpers.py +++ b/tests/argparse/special/test_helpers.py @@ -1,11 +1,10 @@ -from pathlib import Path import miniflask # noqa: E402 import pytest def test_helper_setup(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -21,7 +20,7 @@ def test_helper_setup(capsys): def test_helper_overwrite(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -37,7 +36,7 @@ def test_helper_overwrite(capsys): def test_helper_cli(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -51,7 +50,7 @@ def test_helper_cli(capsys): def test_helper_cli_after_overwrite(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/special/test_overwrite.py b/tests/argparse/special/test_overwrite.py index a4721283..dc8bf904 100644 --- a/tests/argparse/special/test_overwrite.py +++ b/tests/argparse/special/test_overwrite.py @@ -1,11 +1,10 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 def test_setup(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -26,7 +25,7 @@ def test_setup(capsys): def test_override(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -47,7 +46,7 @@ def test_override(capsys): def test_override_twice(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -68,7 +67,7 @@ def test_override_twice(capsys): def test_override_conflict(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -80,7 +79,7 @@ def test_override_conflict(): def test_override_scoped_absolute(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -91,7 +90,7 @@ def test_override_scoped_absolute(): def test_override_scoped_relative(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/tuples/test_argparse_tuple_error.py b/tests/argparse/tuples/test_argparse_tuple_error.py index 03c65ce3..cf3295f4 100644 --- a/tests/argparse/tuples/test_argparse_tuple_error.py +++ b/tests/argparse/tuples/test_argparse_tuple_error.py @@ -1,4 +1,3 @@ -from pathlib import Path import pytest import miniflask # noqa: [E402] @@ -6,7 +5,7 @@ def test_tuple_len_error(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/tuples/test_argparse_tuple_types.py b/tests/argparse/tuples/test_argparse_tuple_types.py index 3f66b222..49842304 100644 --- a/tests/argparse/tuples/test_argparse_tuple_types.py +++ b/tests/argparse/tuples/test_argparse_tuple_types.py @@ -1,9 +1,7 @@ -from pathlib import Path - import miniflask # noqa: [E402] mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/values/test_argparse_overwrites.py b/tests/argparse/values/test_argparse_overwrites.py index b5d6add3..3b1ce7f4 100644 --- a/tests/argparse/values/test_argparse_overwrites.py +++ b/tests/argparse/values/test_argparse_overwrites.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: E402 def test_space(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -49,7 +47,7 @@ def test_space(capsys): def test_equal(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -93,7 +91,7 @@ def test_equal(capsys): def test_bool_int(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -114,7 +112,7 @@ def test_bool_int(capsys): def test_bool_yesno(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -135,7 +133,7 @@ def test_bool_yesno(capsys): def test_bool_tf(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -156,7 +154,7 @@ def test_bool_tf(capsys): def test_bool_truefalse(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -177,7 +175,7 @@ def test_bool_truefalse(capsys): def test_bool_novalue(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/argparse/values/test_argparse_types.py b/tests/argparse/values/test_argparse_types.py index ac20c882..53f3fd01 100644 --- a/tests/argparse/values/test_argparse_types.py +++ b/tests/argparse/values/test_argparse_types.py @@ -1,9 +1,7 @@ -from pathlib import Path - import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/basic/module_loading/test_basic_import.py b/tests/basic/module_loading/test_basic_import.py index 77474660..216ded74 100644 --- a/tests/basic/module_loading/test_basic_import.py +++ b/tests/basic/module_loading/test_basic_import.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_shortid(): diff --git a/tests/basic/module_loading/test_basic_import_relativebasedir.py b/tests/basic/module_loading/test_basic_import_relativebasedir.py index 17ef4729..ea4b20c4 100644 --- a/tests/basic/module_loading/test_basic_import_relativebasedir.py +++ b/tests/basic/module_loading/test_basic_import_relativebasedir.py @@ -7,7 +7,7 @@ def init_mf(): - return miniflask.init(module_dirs=[relpath + "/" + "./modules"], debug=True) + return miniflask.init(".modules", debug=True) def test_setup(): diff --git a/tests/basic/module_loading/test_basic_import_selectivebasedir.py b/tests/basic/module_loading/test_basic_import_selectivebasedir.py index 33d63e3f..ed107172 100644 --- a/tests/basic/module_loading/test_basic_import_selectivebasedir.py +++ b/tests/basic/module_loading/test_basic_import_selectivebasedir.py @@ -6,10 +6,7 @@ def init_mf(): - return miniflask.init(module_dirs=[ - relpath + "/" + "./modules/parentdir/module3", - relpath + "/" + "./modules/otherdir/module2" - ], debug=True) + return miniflask.init(".modules.parentdir.module3", ".modules.otherdir.module2", debug=True) def test_setup(): diff --git a/tests/basic/module_loading/test_default_module.py b/tests/basic/module_loading/test_default_module.py index 07cb4676..cb4f5c8e 100644 --- a/tests/basic/module_loading/test_default_module.py +++ b/tests/basic/module_loading/test_default_module.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_default_module_byid_noload(): diff --git a/tests/basic/module_loading/test_parent_autoloading.py b/tests/basic/module_loading/test_parent_autoloading.py index 73acea4d..cf675deb 100644 --- a/tests/basic/module_loading/test_parent_autoloading.py +++ b/tests/basic/module_loading/test_parent_autoloading.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_parent_autoload_level1(): diff --git a/tests/basic/module_loading/test_relative_import.py b/tests/basic/module_loading/test_relative_import.py index b607d61b..ac3c725f 100644 --- a/tests/basic/module_loading/test_relative_import.py +++ b/tests/basic/module_loading/test_relative_import.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_passive_submodule_import(): diff --git a/tests/event/beforeafter/test_beforeafter.py b/tests/event/beforeafter/test_beforeafter.py index f105a4fd..0017a431 100644 --- a/tests/event/beforeafter/test_beforeafter.py +++ b/tests/event/beforeafter/test_beforeafter.py @@ -1,10 +1,9 @@ -from pathlib import Path import miniflask # noqa: E402 def test_beforeafter_setup(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load("setup") @@ -20,7 +19,7 @@ def test_beforeafter_setup(capsys): def test_beforeafter_before(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup", "beforeevent", "beforeevent2"]) @@ -38,7 +37,7 @@ def test_beforeafter_before(capsys): def test_beforeafter_before_otherorder(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup", "beforeevent2", "beforeevent"]) @@ -56,7 +55,7 @@ def test_beforeafter_before_otherorder(capsys): def test_beforeafter_after(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup", "afterevent", "afterevent2"]) @@ -74,7 +73,7 @@ def test_beforeafter_after(capsys): def test_beforeafter_after_otherorder(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup", "afterevent2", "afterevent"]) @@ -92,7 +91,7 @@ def test_beforeafter_after_otherorder(capsys): def test_beforeafter_all(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup", "beforeevent", "beforeevent2", "afterevent", "afterevent2"]) diff --git a/tests/event/beforeafter/test_beforeafter_none_return.py b/tests/event/beforeafter/test_beforeafter_none_return.py index bc2239d9..8e4fa19d 100644 --- a/tests/event/beforeafter/test_beforeafter_none_return.py +++ b/tests/event/beforeafter/test_beforeafter_none_return.py @@ -1,10 +1,9 @@ -from pathlib import Path import miniflask # noqa: E402 def test_beforeafter_setup_none_return(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load("setup_none_return") @@ -20,7 +19,7 @@ def test_beforeafter_setup_none_return(capsys): def test_beforeafter_before(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_none_return", "beforeevent", "beforeevent2"]) @@ -38,7 +37,7 @@ def test_beforeafter_before(capsys): def test_beforeafter_before_otherorder(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_none_return", "beforeevent2", "beforeevent"]) @@ -56,7 +55,7 @@ def test_beforeafter_before_otherorder(capsys): def test_beforeafter_after(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_none_return", "afterevent", "afterevent2"]) @@ -74,7 +73,7 @@ def test_beforeafter_after(capsys): def test_beforeafter_after_otherorder(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_none_return", "afterevent2", "afterevent"]) @@ -92,7 +91,7 @@ def test_beforeafter_after_otherorder(capsys): def test_beforeafter_all(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_none_return", "beforeevent", "beforeevent2", "afterevent", "afterevent2"]) diff --git a/tests/event/beforeafter/test_beforeafter_nonunique.py b/tests/event/beforeafter/test_beforeafter_nonunique.py index 909de4b4..17f97dce 100644 --- a/tests/event/beforeafter/test_beforeafter_nonunique.py +++ b/tests/event/beforeafter/test_beforeafter_nonunique.py @@ -1,10 +1,9 @@ -from pathlib import Path import miniflask # noqa: E402 def test_beforeafter_setup(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load("setup_multiple") @@ -23,7 +22,7 @@ def test_beforeafter_setup(capsys): def test_beforeafter_before(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_multiple", "beforeevent", "beforeevent2"]) @@ -44,7 +43,7 @@ def test_beforeafter_before(capsys): def test_beforeafter_after(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_multiple", "afterevent_multiple", "afterevent2_multiple"]) @@ -65,7 +64,7 @@ def test_beforeafter_after(capsys): def test_beforeafter_before_and_after(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.load(["setup_multiple", "beforeevent", "beforeevent2", "afterevent_multiple", "afterevent2_multiple"]) diff --git a/tests/event/beforeafter/test_nonevent_beforeafter.py b/tests/event/beforeafter/test_nonevent_beforeafter.py index a2d72a29..787fec8e 100644 --- a/tests/event/beforeafter/test_nonevent_beforeafter.py +++ b/tests/event/beforeafter/test_nonevent_beforeafter.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/event/exception_event/test_exception_event.py b/tests/event/exception_event/test_exception_event.py index 81ba336e..a77d3464 100644 --- a/tests/event/exception_event/test_exception_event.py +++ b/tests/event/exception_event/test_exception_event.py @@ -1,11 +1,10 @@ import pytest -from pathlib import Path import miniflask # noqa: E402 def test_exception_event(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) @@ -17,7 +16,7 @@ def test_exception_event(): def test_exception_event_debugmode(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -29,7 +28,7 @@ def test_exception_event_debugmode(): def test_exception_event_unique_register(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) @@ -39,7 +38,7 @@ def test_exception_event_unique_register(): def test_exception_event_unique_register_debugmode(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/event/named_call/test_named_call.py b/tests/event/named_call/test_named_call.py index b534f84b..1c021eb6 100644 --- a/tests/event/named_call/test_named_call.py +++ b/tests/event/named_call/test_named_call.py @@ -1,10 +1,9 @@ -from pathlib import Path import miniflask # noqa: E402 def test_named_call(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) diff --git a/tests/event/overwrite_event/test_overwrite_event.py b/tests/event/overwrite_event/test_overwrite_event.py index 00f925c2..5feb7e5c 100644 --- a/tests/event/overwrite_event/test_overwrite_event.py +++ b/tests/event/overwrite_event/test_overwrite_event.py @@ -1,10 +1,9 @@ -from pathlib import Path import miniflask # noqa: E402 def test_overwrite_setup(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) @@ -22,7 +21,7 @@ def test_overwrite_setup(capsys): def test_overwrite(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) @@ -40,7 +39,7 @@ def test_overwrite(capsys): def test_overwrite_during_event(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) @@ -64,7 +63,7 @@ def test_overwrite_during_event(capsys): def test_overwrite_with_attached(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) diff --git a/tests/event/register_event/_test_basic_event.py b/tests/event/register_event/_test_basic_event.py index 77474660..216ded74 100644 --- a/tests/event/register_event/_test_basic_event.py +++ b/tests/event/register_event/_test_basic_event.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_shortid(): diff --git a/tests/event/register_event/_test_exception_in_event.py b/tests/event/register_event/_test_exception_in_event.py index 77474660..216ded74 100644 --- a/tests/event/register_event/_test_exception_in_event.py +++ b/tests/event/register_event/_test_exception_in_event.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_shortid(): diff --git a/tests/event/register_event/_test_register_event_exception.py b/tests/event/register_event/_test_register_event_exception.py index 77474660..216ded74 100644 --- a/tests/event/register_event/_test_register_event_exception.py +++ b/tests/event/register_event/_test_register_event_exception.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_shortid(): diff --git a/tests/event/register_event/test_event_exists.py b/tests/event/register_event/test_event_exists.py index e8311b39..b34a654c 100644 --- a/tests/event/register_event/test_event_exists.py +++ b/tests/event/register_event/test_event_exists.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_event_exists_false(): diff --git a/tests/event/register_event/test_register_event_during_event.py b/tests/event/register_event/test_register_event_during_event.py index f0eb0d13..b0655dc3 100644 --- a/tests/event/register_event/test_register_event_during_event.py +++ b/tests/event/register_event/test_register_event_during_event.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) def test_register_event_during_event(capsys): diff --git a/tests/event/unregister_event/test_unregister_event.py b/tests/event/unregister_event/test_unregister_event.py index 0264717f..51aea8bd 100644 --- a/tests/event/unregister_event/test_unregister_event.py +++ b/tests/event/unregister_event/test_unregister_event.py @@ -1,11 +1,10 @@ import pytest -from pathlib import Path import miniflask # noqa: E402 def test_unregister(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=False ) diff --git a/tests/features/features/test_features.py b/tests/features/features/test_features.py index cbc9446d..a11aaeb7 100644 --- a/tests/features/features/test_features.py +++ b/tests/features/features/test_features.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/state/dataclass_as_state/test_dataclass.py b/tests/state/dataclass_as_state/test_dataclass.py index d533c087..870bd143 100644 --- a/tests/state/dataclass_as_state/test_dataclass.py +++ b/tests/state/dataclass_as_state/test_dataclass.py @@ -1,9 +1,8 @@ -from pathlib import Path import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/state/dependencies/setup.py b/tests/state/dependencies/setup.py index 4d98c0b8..e5d5bda3 100644 --- a/tests/state/dependencies/setup.py +++ b/tests/state/dependencies/setup.py @@ -1,4 +1,3 @@ -from pathlib import Path import miniflask # noqa: E402 @@ -8,9 +7,6 @@ def printAll(state): def setup(): - mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), - debug=True - ) - mf.register_event('main', printAll, unique=False) + mf = miniflask.init(".modules") + mf.register_event("main", printAll, unique=False) return mf diff --git a/tests/state/dynamic_state/test_dynamic_state.py b/tests/state/dynamic_state/test_dynamic_state.py index 09f47609..9f555973 100644 --- a/tests/state/dynamic_state/test_dynamic_state.py +++ b/tests/state/dynamic_state/test_dynamic_state.py @@ -1,11 +1,10 @@ -from pathlib import Path from os.path import dirname import pytest import miniflask # noqa: E402 def init_mf(): - return miniflask.init(module_dirs=str(Path(__file__).parent / "modules"), debug=True) + return miniflask.init(".modules", debug=True) all_modules = ["otherdir", "otherdir.module2", "parentdir", "parentdir.module1", "parentdir.module2", "parentdir.module3", "parentdir.module3.submodule", "parentdir.module3.submodule.subsubmodule"] diff --git a/tests/state/enums/test_enums.py b/tests/state/enums/test_enums.py index 9c5937f9..eb01368f 100644 --- a/tests/state/enums/test_enums.py +++ b/tests/state/enums/test_enums.py @@ -1,10 +1,9 @@ -from pathlib import Path import miniflask # noqa: E402 def test_enum(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.run( @@ -16,7 +15,7 @@ def test_enum(): def test_enum_relative_import(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) mf.run( diff --git a/tests/state/outervar/test_outervar.py b/tests/state/outervar/test_outervar.py index 85febf8d..c1ac393d 100755 --- a/tests/state/outervar/test_outervar.py +++ b/tests/state/outervar/test_outervar.py @@ -1,11 +1,9 @@ -from pathlib import Path - import miniflask # noqa: E402 def test_outervar(): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) @@ -18,7 +16,7 @@ def test_outervar(): def test_outervar_with_before_event(capsys): mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/state/register_wrong_scope/test_exception_register.py b/tests/state/register_wrong_scope/test_exception_register.py index d26b26fc..8b6b1781 100644 --- a/tests/state/register_wrong_scope/test_exception_register.py +++ b/tests/state/register_wrong_scope/test_exception_register.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) diff --git a/tests/state/temporary/test_temporary.py b/tests/state/temporary/test_temporary.py index afa10645..45217b7c 100644 --- a/tests/state/temporary/test_temporary.py +++ b/tests/state/temporary/test_temporary.py @@ -1,9 +1,7 @@ -from pathlib import Path - import miniflask # noqa: E402 mf = miniflask.init( - module_dirs=str(Path(__file__).parent / "modules"), + ".modules", debug=True ) From 09ea11a09eb6df929ad9b09e0a993096e14cc2c2 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 19 Dec 2022 17:07:16 +0100 Subject: [PATCH 28/45] fix mf.showModules by hiding miniflask internal modules --- src/miniflask/miniflask.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 43dc032f..ac355031 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -6,9 +6,11 @@ from functools import partial from os import path, listdir, linesep, get_terminal_size from importlib import import_module +from pathlib import Path from enum import Enum, EnumMeta from argparse import ArgumentParser, REMAINDER as ARGPARSE_REMAINDER from typing import List +from pkgutil import resolve_name import random from colored import fg, attr @@ -37,6 +39,7 @@ highlight_event, str2bool, get_varid_from_fuzzy, + get_full_base_module_name, ) from .settings import listsettings @@ -242,9 +245,23 @@ def showModules( visited = set() if not directory: - for basename, base_import_path in self.module_repositories.items(): + for basename, base_module_path in self.module_repositories.items(): + if base_module_path.startswith("."): + stack_frame = inspect.stack()[ + 2 + ] # the frame in which miniflask.init has been called + callee_module_path = Path(stack_frame.filename).parent + base_module_path = ( + get_full_base_module_name(callee_module_path) + base_module_path + ) + if base_module_path.startswith("miniflask"): + continue + + module = resolve_name(base_module_path) + directory = module.__path__[0] + self.showModules( - loop_directory, + directory, prepend=prepend, id_pre=basename if id_pre is None else id_pre + "." + basename, with_event=with_event, From 0ae1033130dede67c85c94b80206d2b5423c863b Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Mon, 19 Dec 2022 18:24:52 +0100 Subject: [PATCH 29/45] lint: adapt config and code to new version --- .github/workflows/linting-python.yml | 4 ++-- .pre-commit-config.yaml | 18 +++++++++--------- src/miniflask/miniflask.py | 6 +++--- src/miniflask/util.py | 7 +++---- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/.github/workflows/linting-python.yml b/.github/workflows/linting-python.yml index e59e5805..60901389 100644 --- a/.github/workflows/linting-python.yml +++ b/.github/workflows/linting-python.yml @@ -31,8 +31,8 @@ jobs: flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics - name: Static Analysis run: | - flake8 src --count --ignore E501 --statistics - pylint -d W0511 -d C0114 -d C0116 -d C0115 -d C0301 -d C0302 -d C0303 -d C0103 -d C0209 -d C3001 -d R0913 -d R0914 -d R0902 -d R0912 -d R0801 -d R1702 -d W0212 -d W0223 -d E1101 -d W0221 -d E1102 src/miniflask + flake8 src --count --ignore 'E501,W503,C901' --statistics + pylint -d W0511 -d C0114 -d C0116 -d C0115 -d C0301 -d C0302 -d C0303 -d C0103 -d C0209 -d C3001 -d R0913 -d R0914 -d R0902 -d R0912 -d R0801 -d R1702 -d W0212 -d W0223 -d E1101 -d W0221 -d E1102 -d R0915 -d R1710 src/miniflask pylint --exit-zero src/miniflask - name: Syntax Check of Tests run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acab54ef..625c4547 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,18 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/pre-commit/pre-commit-hooks + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - - id: check-yaml - - id: check-added-large-files - - id: flake8 - args: [--count,--ignore,E501,--statistics,--exclude,.git] -- repo: local + - id: check-yaml + - id: check-added-large-files + - id: flake8 + args: [--count, --ignore, 'E501,W503,C901', --statistics, --exclude, .git] + - repo: local hooks: - - id: pylint + - id: pylint name: pylint - entry: bash -c "PYTHONPATH=$(pwd)/src find tests -type d -exec test -e '{}'/.module \; -exec pylint -d W0511 -d C0114 -d C0116 -d C0115 -d C0301 -d C0103 -d C0209 -d C901 -d R0913 -d R0914 -d R0902 -d R0912 -d R0801 -d W0212 -d W0223 -d E1101 -d W0221 -d E1102 '{}' +" + entry: bash -c "PYTHONPATH=$(pwd)/src find tests -type d -exec test -e '{}'/.module \; -exec pylint -d W0511 -d C0114 -d C0116 -d C0115 -d C0301 -d C0103 -d C0209 -d C901 -d R0913 -d R0914 -d R0902 -d R0912 -d R0801 -d W0212 -d W0223 -d E1101 -d W0221 -d R0915 -d R1710 -d E1102 '{}' +" language: system types: [python] - args: [ ] + args: [] diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index ac355031..6501335c 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -151,7 +151,7 @@ def print_heading(self, *args, color=fg("blue"), margin=8): line = "—" * self._consolecolumns if len(args) > 0: s = " ".join(args) - line = line[:margin] + " " + s + " " + line[margin + len(s) + 2 :] + line = line[:margin] + " " + s + " " + line[margin + len(s) + 2:] print() print(color + attr("bold") + line + attr("reset")) @@ -165,7 +165,7 @@ def print_recently_loaded(self, prepend="", loading_text=highlight_loading): # if previously printed parent, make the module_id shorter if module_id.startswith(last): - module_id_formatted = last_formatted + module_id[len(last) :] + module_id_formatted = last_formatted + module_id[len(last):] is_last = i == len(self._recently_loaded) - 1 has_children = len(mod.miniflask_obj._recently_loaded) > 0 @@ -446,7 +446,7 @@ def _getModuleIdFromVarId( try: module_id = self.getModuleId(scope) if varid.startswith(scope): - varid = varid[len(scope) + 1 :] + varid = varid[len(scope) + 1:] return module_id, varid except ValueError: pass diff --git a/src/miniflask/util.py b/src/miniflask/util.py index 791f412a..b1630d6f 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -1,15 +1,14 @@ import re import argparse +import inspect from argparse import Action from os import walk, path from pathlib import Path from pkgutil import resolve_name - from colored import attr, fg -import inspect -# get the absolute package name of the current file to cwd() +# get the absolute package name of the current file to cwd() # - i.e. $result can be loaded using `import $result` def get_full_base_module_name(directory): directory = Path(directory).absolute() @@ -28,7 +27,7 @@ def getModulesAvail(python_import_paths, f=None): f = {} for base_module_id, base_module_path in python_import_paths.items(): if base_module_path.startswith("."): - stack_frame = inspect.stack()[2] # the frame in which miniflask.init has been called + stack_frame = inspect.stack()[2] # the frame in which miniflask.init has been called callee_module_path = Path(stack_frame.filename).parent base_module_path = get_full_base_module_name(callee_module_path) + base_module_path module = resolve_name(base_module_path) From 2d3a405a633407e2b31b433757dd4318bb7d047d Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 20 Dec 2022 12:28:22 +0100 Subject: [PATCH 30/45] tests: add test against illegal-annotation bug --- .../state/dependencies/modules/defarguments_module1/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/state/dependencies/modules/defarguments_module1/__init__.py b/tests/state/dependencies/modules/defarguments_module1/__init__.py index a816a0fa..0f061377 100644 --- a/tests/state/dependencies/modules/defarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/defarguments_module1/__init__.py @@ -40,4 +40,6 @@ def register(mf): "var4": var4_fn, "var5": var5_fn, "var6": var6_fn, + "annotation": lambda: 10, + "annotation.bug": lambda: 10 }) From a2b3d30d0bdfdd0dd80ed0ead33fb87e634aeded Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 20 Dec 2022 13:15:51 +0100 Subject: [PATCH 31/45] state: improve lambda regex, allowing any key name --- src/miniflask/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 8020287e..379958a8 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -383,7 +383,7 @@ def __str__(self): class state_node: - _lambda_str_regex = re.compile(r"^{?\s*\"\w*\"\s*:\s*lambda\s*\w*:.*}?") + _lambda_str_regex = re.compile(r"^{?\s*(\"[^\"]*\")|('[^']*')\s*:\s*lambda\s*\w*:.*}?") local_arguments = [] depends_on = [] depends_alternatives = [] From f6b53b707bf613a23d192d8694be43e2cdb5f08a Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 20 Dec 2022 14:02:58 +0100 Subject: [PATCH 32/45] tests: add dependency test for lambda with comment --- .../dependencies/modules/defarguments_module1/__init__.py | 4 +--- .../modules/potential_ast_bugs_module/.module | 0 .../modules/potential_ast_bugs_module/__init__.py | 8 ++++++++ tests/state/dependencies/test_state_dependencies.py | 5 +++++ 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 tests/state/dependencies/modules/potential_ast_bugs_module/.module create mode 100644 tests/state/dependencies/modules/potential_ast_bugs_module/__init__.py diff --git a/tests/state/dependencies/modules/defarguments_module1/__init__.py b/tests/state/dependencies/modules/defarguments_module1/__init__.py index 0f061377..5298236a 100644 --- a/tests/state/dependencies/modules/defarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/defarguments_module1/__init__.py @@ -39,7 +39,5 @@ def register(mf): "var3": var3_fn, "var4": var4_fn, "var5": var5_fn, - "var6": var6_fn, - "annotation": lambda: 10, - "annotation.bug": lambda: 10 + "var6": var6_fn }) diff --git a/tests/state/dependencies/modules/potential_ast_bugs_module/.module b/tests/state/dependencies/modules/potential_ast_bugs_module/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/dependencies/modules/potential_ast_bugs_module/__init__.py b/tests/state/dependencies/modules/potential_ast_bugs_module/__init__.py new file mode 100644 index 00000000..fadac2e0 --- /dev/null +++ b/tests/state/dependencies/modules/potential_ast_bugs_module/__init__.py @@ -0,0 +1,8 @@ + +def register(mf): + mf.register_defaults({ + "annotation": lambda: 10, + "annotation.bug": lambda: 10, + "lambda_with_comment": lambda: 10, # just some comment + "var_with_comment": 10, # just some comment + }) diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py index ef05f5aa..717cb697 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -3,6 +3,11 @@ from miniflask.exceptions import RegisterError +def test_potential_ast_bugs(): + mf = setup() + mf.load("potential_ast_bugs_module") + + def test_lambda_arguments_1(): mf = setup() mf.load("lambdaarguments_module1") From a75af9c65366ad0682a92af40f894483df55eac1 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 20 Dec 2022 14:05:43 +0100 Subject: [PATCH 33/45] fix lambda state definition with comment --- src/miniflask/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 379958a8..df7cee88 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -411,7 +411,7 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is # 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 + "}" + _fn_src = "{\n" + _fn_src + "\n}" _ast_mode = "eval" # find lambda or def expression fn_lbd_iter = list(iter( From 0fa66aa236fd044ebdcab81bdbd095e409e9567b Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:37:27 +0100 Subject: [PATCH 34/45] Add parsing test for multilevel variable slices. --- .../dependencies/modules/lambdaarguments_module1/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index 25bbd7f5..b876be12 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -17,6 +17,8 @@ def __init__(self, state): def register(mf): mf.register_defaults({ + "multilevelindex": lambda state: "cuda:" + str(state["gpu"][0]) if state["gpu"][0] >= 0 else "cpu", + "multileveltag": lambda state: state["var1"]["other"], "test_multiple_inline_I": lambda: 1 * 42, "test_multiple_inline_II": lambda: 2 * 42, "test_lambda_def": lambda_definition, From 119d7846e6383a8b22596c5a76e4969dcb2fe1f9 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:38:11 +0100 Subject: [PATCH 35/45] Check if alternative exists / is necessary. --- src/miniflask/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index df7cee88..02dfc61e 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -434,7 +434,8 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is if isinstance(node, ast.IfExp): 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 + if len(rvs): + self.depends_alternatives[lv] = rvs def str(self): return str(self.varid) From 443afba70204818de6ae11b50f9dbb62bb85cc46 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:38:46 +0100 Subject: [PATCH 36/45] Return unique and sorted variable names. --- src/miniflask/state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 02dfc61e..fa7f25e0 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -458,11 +458,11 @@ def __repr__(self): @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([ + return sorted(set([ 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): From 2405f7d7001fe9749daaab881bd5bcdd9d1ed587 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:39:28 +0100 Subject: [PATCH 37/45] Add search for all slices variables and compared variables with 'in' statement. --- src/miniflask/state.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index fa7f25e0..9a6cf501 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -466,19 +466,23 @@ def _find_var_names(tree: ast.AST, lcl_variables=None): @staticmethod def _find_comp_names(tree: ast.AST, lcl_variables): - ret = [] + # find all regular loaded local variables with slices + ret = [ + n.slice.value for n in ast.walk(tree) if ( + hasattr(n, "value") and isinstance(n.value, ast.Name) and n.value.id in lcl_variables + and hasattr(n, "ctx") and isinstance(n.ctx, ast.Load) + and hasattr(n, "slice") + ) + ] + # add all 'x in var' cases for node in ast.walk(tree): - if isinstance(node, ast.Compare): + if isinstance(node, ast.Compare) and hasattr(node, "left") and isinstance(node.left, ast.Constant): _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) - ]) + _names = [node.left.value] if len(_args): ret += _names - return ret + return list(sorted(ret)) # ------------------- # # dependency routines # From c69ffedf786ba76fe011747cd3663c41f079fd02 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:42:56 +0100 Subject: [PATCH 38/45] Fix over-indented lines. --- src/miniflask/state.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 9a6cf501..7c47eda4 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -469,9 +469,9 @@ def _find_comp_names(tree: ast.AST, lcl_variables): # find all regular loaded local variables with slices ret = [ n.slice.value for n in ast.walk(tree) if ( - hasattr(n, "value") and isinstance(n.value, ast.Name) and n.value.id in lcl_variables - and hasattr(n, "ctx") and isinstance(n.ctx, ast.Load) - and hasattr(n, "slice") + hasattr(n, "value") and isinstance(n.value, ast.Name) and n.value.id in lcl_variables + and hasattr(n, "ctx") and isinstance(n.ctx, ast.Load) + and hasattr(n, "slice") ) ] # add all 'x in var' cases From 6c42e65893fc49b8f38cb5436fc3ae76ebda2e6c Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:45:40 +0100 Subject: [PATCH 39/45] Use set comprehension. --- src/miniflask/state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 7c47eda4..f4b410f2 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -458,11 +458,11 @@ def __repr__(self): @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(set([ + 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): From be6b0393f72afe327a56859218946e2ec5d57070 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 21 Dec 2022 14:46:39 +0100 Subject: [PATCH 40/45] Use set comprehension. --- src/miniflask/state.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index f4b410f2..6c06230e 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -467,22 +467,21 @@ def _find_var_names(tree: ast.AST, lcl_variables=None): @staticmethod def _find_comp_names(tree: ast.AST, lcl_variables): # find all regular loaded local variables with slices - ret = [ + ret = { n.slice.value for n in ast.walk(tree) if ( hasattr(n, "value") and isinstance(n.value, ast.Name) and n.value.id in lcl_variables and hasattr(n, "ctx") and isinstance(n.ctx, ast.Load) and hasattr(n, "slice") ) - ] + } # add all 'x in var' cases for node in ast.walk(tree): if isinstance(node, ast.Compare) and hasattr(node, "left") and isinstance(node.left, ast.Constant): _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 = [node.left.value] if len(_args): - ret += _names - return list(sorted(ret)) + ret.add(node.left.value) + return sorted(ret) # ------------------- # # dependency routines # From 05c487550f1162d014adad69d7d18189d14960a1 Mon Sep 17 00:00:00 2001 From: Sebastian Brodehl Date: Wed, 11 Jan 2023 14:28:47 +0100 Subject: [PATCH 41/45] Fix issue with non-existent dependencies. --- src/miniflask/state.py | 3 +-- .../dependencies/modules/lambdaarguments_module1/__init__.py | 1 + tests/state/dependencies/test_state_dependencies.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/miniflask/state.py b/src/miniflask/state.py index 6c06230e..bd6a03df 100644 --- a/src/miniflask/state.py +++ b/src/miniflask/state.py @@ -434,8 +434,7 @@ def __init__(self, varid, mf, caller_traceback, cliargs=False, parsefn=False, is if isinstance(node, ast.IfExp): rvs = self._find_var_names(node.orelse, lcl_variables=self.local_arguments) for lv in self._find_comp_names(node.test, self.local_arguments): - if len(rvs): - self.depends_alternatives[lv] = rvs + self.depends_alternatives[lv] = rvs def str(self): return str(self.varid) diff --git a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py index b876be12..e8dc5193 100644 --- a/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py +++ b/tests/state/dependencies/modules/lambdaarguments_module1/__init__.py @@ -17,6 +17,7 @@ def __init__(self, state): def register(mf): mf.register_defaults({ + "emptydependency": lambda state: state["doesntexists"] if "doesntexists" in state else "cpu", "multilevelindex": lambda state: "cuda:" + str(state["gpu"][0]) if state["gpu"][0] >= 0 else "cpu", "multileveltag": lambda state: state["var1"]["other"], "test_multiple_inline_I": lambda: 1 * 42, diff --git a/tests/state/dependencies/test_state_dependencies.py b/tests/state/dependencies/test_state_dependencies.py index 717cb697..54ff6069 100644 --- a/tests/state/dependencies/test_state_dependencies.py +++ b/tests/state/dependencies/test_state_dependencies.py @@ -11,6 +11,8 @@ def test_potential_ast_bugs(): def test_lambda_arguments_1(): mf = setup() mf.load("lambdaarguments_module1") + assert mf.state_registrations["modules.lambdaarguments_module1.emptydependency"][-1].depends_on == ["doesntexists"] + assert mf.state_registrations["modules.lambdaarguments_module1.emptydependency"][-1].depends_alternatives == {"doesntexists": []} 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"] From e6813b4d9ea7f0ac0fd9af81820ca281d42ccd8d Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Thu, 11 Nov 2021 22:29:18 +0100 Subject: [PATCH 42/45] tests: added simple state/units test case tests: added conversion tests tests: adapted unit-feature tests --- tests/state/units/modules/__init__.py | 0 tests/state/units/modules/defineunits/.module | 0 .../units/modules/defineunits/__init__.py | 73 +++++++++++++++ .../units/modules/defineunits_cached/.module | 0 .../modules/defineunits_cached/__init__.py | 79 ++++++++++++++++ tests/state/units/modules/main/.module | 0 tests/state/units/modules/main/__init__.py | 60 +++++++++++++ tests/state/units/test_units.py | 90 +++++++++++++++++++ tests/state/units/test_units_cached.py | 90 +++++++++++++++++++ 9 files changed, 392 insertions(+) create mode 100644 tests/state/units/modules/__init__.py create mode 100644 tests/state/units/modules/defineunits/.module create mode 100644 tests/state/units/modules/defineunits/__init__.py create mode 100644 tests/state/units/modules/defineunits_cached/.module create mode 100644 tests/state/units/modules/defineunits_cached/__init__.py create mode 100644 tests/state/units/modules/main/.module create mode 100644 tests/state/units/modules/main/__init__.py create mode 100644 tests/state/units/test_units.py create mode 100644 tests/state/units/test_units_cached.py diff --git a/tests/state/units/modules/__init__.py b/tests/state/units/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/units/modules/defineunits/.module b/tests/state/units/modules/defineunits/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/units/modules/defineunits/__init__.py b/tests/state/units/modules/defineunits/__init__.py new file mode 100644 index 00000000..e362dfe5 --- /dev/null +++ b/tests/state/units/modules/defineunits/__init__.py @@ -0,0 +1,73 @@ + + +# define how to transform data when initializing a unit +def init_time_unit(data): + units = list(data.keys()) + assert len(units) == 1, f"Unitvalue should contain exactly one data point, but found {units}." + unit = units[0] + value = list(data.values())[0] + data = {"computation_list": []} + if unit == "m": + return {"s": 60 * value, **data} + if unit == "h": + return {"s": 60 * 60 * value, **data} + if unit == "d": + return {"s": 24 * 60 * 60 * value, **data} + raise ValueError(f"Could not determine initial values from {repr(data)}.") + + +def get_time_unit(unitvalue, targetunit): + + # we use seconds as base unit und convert any start to that value + if "s" not in unitvalue.data: + unitvalue.data = init_time_unit(unitvalue.data) + + unitvalue.data["computation_list"].append(targetunit) + + if targetunit == "s": + return unitvalue.data["s"] + if targetunit == "m": + return unitvalue.data["s"] / 60 + if targetunit == "h": + return unitvalue.data["s"] / 60 / 60 + if targetunit == "d": + return unitvalue.data["s"] / 60 / 60 / 24 + + raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}") + + +def set_time_unit(unitvalue, targetunit, newvalue): + + # we use seconds as base unit und convert any start to that value + if "s" not in unitvalue.data: + unitvalue.data = init_time_unit(unitvalue.data) + + unitvalue.data["computation_list"].append(targetunit) + + if targetunit == "s": + unitvalue.data["s"] += newvalue + return + if targetunit == "m": + unitvalue.data["s"] += 60 * newvalue + return + if targetunit == "h": + unitvalue.data["s"] += 60 * 60 * newvalue + return + if targetunit == "d": + unitvalue.data["s"] += 24 * 60 * 60 * newvalue + return + + raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}") + + +def register(mf): + time = mf.register_unit("time", get_time_unit, set_time_unit, [ + ["m", "minute", "minutes"], + ["h", "hour", "hours"], + ["s", "second", "seconds"], + ["d", "day", "days"] + ]) + mf.register_defaults({ + "time": time(6.0, "h"), + "time2": time(6.0, "d"), + }) diff --git a/tests/state/units/modules/defineunits_cached/.module b/tests/state/units/modules/defineunits_cached/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/units/modules/defineunits_cached/__init__.py b/tests/state/units/modules/defineunits_cached/__init__.py new file mode 100644 index 00000000..f6c62545 --- /dev/null +++ b/tests/state/units/modules/defineunits_cached/__init__.py @@ -0,0 +1,79 @@ + + +# define how to transform data when initializing a unit +def init_time_unit(data): + units = list(data.keys()) + assert len(units) == 1, f"Unitvalue should contain exactly one data point, but found {units}." + unit = units[0] + value = list(data.values())[0] + data = {"computation_list": []} + if unit == "m": + return {"s": 60 * value, **data} + if unit == "h": + return {"s": 60 * 60 * value, **data} + if unit == "d": + return {"s": 24 * 60 * 60 * value, **data} + raise ValueError(f"Could not determine initial values from {repr(data)}.") + + +def get_time_unit(unitvalue, targetunit): + + # we use seconds as base unit und convert any start to that value + if "s" not in unitvalue.data: + unitvalue.data = init_time_unit(unitvalue.data) + + unitvalue.data["computation_list"].append(targetunit) + + if targetunit == "s": + return unitvalue.data["s"] + if targetunit == "m": + unitvalue.data["m"] = unitvalue.data["s"] / 60 + return unitvalue.data["m"] + if targetunit == "h": + unitvalue.data["h"] = unitvalue.data["s"] / 60 / 60 + return unitvalue.data["h"] + if targetunit == "d": + unitvalue.data["d"] = unitvalue.data["s"] / 60 / 60 / 24 + return unitvalue.data["d"] + + raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}") + + +def set_time_unit(unitvalue, targetunit, newvalue): + + # we use seconds as base unit und convert any start to that value + if "s" not in unitvalue.data: + unitvalue.data = init_time_unit(unitvalue.data) + + # remove all but s and computation_list whenever we change something + unitvalue.data = {"s": unitvalue.data["s"], "computation_list": unitvalue.data["computation_list"]} + + unitvalue.data["computation_list"].append(targetunit) + + if targetunit == "s": + unitvalue.data["s"] += newvalue + return + if targetunit == "m": + unitvalue.data["s"] += 60 * newvalue + return + if targetunit == "h": + unitvalue.data["s"] += 60 * 60 * newvalue + return + if targetunit == "d": + unitvalue.data["s"] += 24 * 60 * 60 * newvalue + return + + raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}") + + +def register(mf): + time = mf.register_unit("time", get_time_unit, set_time_unit, [ + ["m", "minute", "minutes"], + ["h", "hour", "hours"], + ["s", "second", "seconds"], + ["d", "day", "days"] + ]) + mf.register_defaults({ + "time": time(6.0, "h"), + "time2": time(6.0, "d"), + }) diff --git a/tests/state/units/modules/main/.module b/tests/state/units/modules/main/.module new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/units/modules/main/__init__.py b/tests/state/units/modules/main/__init__.py new file mode 100644 index 00000000..7f493459 --- /dev/null +++ b/tests/state/units/modules/main/__init__.py @@ -0,0 +1,60 @@ + +def add50minutes(state): + print("-------------") + print("changing time") + print("-------------") + state["time"].minute += 50 + state["time2"].minute += 50 + + +def main(state, event): + print("time data", state["time"]) + print("--------------") + print("time %.2f [minute]" % state["time"].minute) + print("time %.2f [minute] (re-access)" % state["time"].minute) + print("time %.2f [minute] (re-access)" % state["time"].minute) + print("time %.2f [hour]" % state["time"].hour) + print("time %.2f [hour] (re-access)" % state["time"].hour) + print("time %.2f [second]" % state["time"].second) + print("time %.2f [day]" % state["time"].day) + print("time computation list %s" % ",".join(state["time"].computation_list)) + print("time data", state["time"]) + print() + print("time2 data", state["time2"]) + print("--------------") + print("time2 %.2f [minute]" % state["time2"].minute) + print("time2 %.2f [hour]" % state["time2"].hour) + print("time2 %.2f [second]" % state["time2"].second) + print("time2 %.2f [day]" % state["time2"].day) + print("time2 computation list %s" % ",".join(state["time2"].computation_list)) + print("time2 data", state["time2"]) + print() + event.add50minutes() + print() + print("time base", state["time"]) + print("--------------") + print("time %.2f [minute]" % state["time"].minute) + print("time %.2f [minute] (re-access)" % state["time"].minute) + print("time %.2f [minute] (re-access)" % state["time"].minute) + print("time %.2f [minute] (re-access)" % state["time"].minute) + print("time %.2f [hour]" % state["time"].hour) + print("time %.2f [hour] (re-access)" % state["time"].hour) + print("time %.2f [hour] (re-access)" % state["time"].hour) + print("time %.2f [second]" % state["time"].second) + print("time %.2f [day]" % state["time"].day) + print("time computation list %s" % ",".join(state["time"].computation_list)) + print("time data", state["time"]) + print() + print("time2 base", state["time2"]) + print("--------------") + print("time2 %.2f [mminute]" % state["time2"].minute) + print("time2 %.2f [hour]" % state["time2"].hour) + print("time2 %.2f [second]" % state["time2"].second) + print("time2 %.2f [day]" % state["time2"].day) + print("time2 computation list %s" % ",".join(state["time2"].computation_list)) + print("time2 data", state["time2"]) + + +def register(mf): + mf.register_event("main", main) + mf.register_event("add50minutes", add50minutes) diff --git a/tests/state/units/test_units.py b/tests/state/units/test_units.py new file mode 100644 index 00000000..86273c90 --- /dev/null +++ b/tests/state/units/test_units.py @@ -0,0 +1,90 @@ +import miniflask # noqa: E402 + + +assert_out = """ +modules.main +modules.defineunits +time data {time1} +-------------- +time 1440.00 [minute] +time 1440.00 [minute] (re-access) +time 1440.00 [minute] (re-access) +time 24.00 [hour] +time 24.00 [hour] (re-access) +time 86400.00 [second] +time 1.00 [day] +time computation list m,m,m,h,h,d +time data 86400s, ['m', 'm', 'm', 'h', 'h', 'd']computation_list + +time2 data {time2} +-------------- +time2 8640.00 [minute] +time2 144.00 [hour] +time2 518400.00 [second] +time2 6.00 [day] +time2 computation list m,h,d +time2 data 518400s, ['m', 'h', 'd']computation_list + +------------- +changing time +------------- + +time base 175800.0s, ['m', 'm', 'm', 'h', 'h', 'd', 'm', 'm']computation_list +-------------- +time 2930.00 [minute] +time 2930.00 [minute] (re-access) +time 2930.00 [minute] (re-access) +time 2930.00 [minute] (re-access) +time 48.83 [hour] +time 48.83 [hour] (re-access) +time 48.83 [hour] (re-access) +time 175800.00 [second] +time 2.03 [day] +time computation list m,m,m,h,h,d,m,m,m,m,m,m,h,h,h,d +time data 175800.0s, ['m', 'm', 'm', 'h', 'h', 'd', 'm', 'm', 'm', 'm', 'm', 'm', 'h', 'h', 'h', 'd']computation_list + +time2 base 1039800.0s, ['m', 'h', 'd', 'm', 'm']computation_list +-------------- +time2 17330.00 [mminute] +time2 288.83 [hour] +time2 1039800.00 [second] +time2 12.03 [day] +time2 computation list m,h,d,m,m,m,h,d +time2 data 1039800.0s, ['m', 'h', 'd', 'm', 'm', 'm', 'h', 'd']computation_list +""".lstrip() + + +def test_unit_argparse_1d(capsys): + mf = miniflask.init("modules") + mf.load(["main", "defineunits"]) + time1, time2 = "1d", "6d" + mf.parse_args([ + "--time", time1, + "--time2", time2 + ]) + mf.event.main() + assert assert_out.format(time1=time1, time2=time2) == capsys.readouterr().out + + +def test_unit_argparse_24h(capsys): + mf = miniflask.init("modules") + mf.load(["main", "defineunits"]) + time1, time2 = "24h", "144h" + mf.parse_args([ + "--time", time1, + "--time2", time2 + ]) + mf.event.main() + assert assert_out.format(time1=time1, time2=time2) == capsys.readouterr().out + + +def test_unit_argparse_1440m(capsys): + mf = miniflask.init("modules") + mf.load(["main", "defineunits"]) + time1, time2 = "24h", "144h" + mf.parse_args([ + "--time", time1, + "--time2", time2 + ]) + mf.event.main() + assert assert_out.format(time1=time1, time2=time2) == capsys.readouterr().out diff --git a/tests/state/units/test_units_cached.py b/tests/state/units/test_units_cached.py new file mode 100644 index 00000000..db96e4fe --- /dev/null +++ b/tests/state/units/test_units_cached.py @@ -0,0 +1,90 @@ +import miniflask # noqa: E402 + + +assert_out = """ +modules.main +modules.defineunits_cached +time data {time1} +-------------- +time 1440.00 [minute] +time 1440.00 [minute] (re-access) +time 1440.00 [minute] (re-access) +time 24.00 [hour] +time 24.00 [hour] (re-access) +time 86400.00 [second] +time 1.00 [day] +time computation list m,h,d +time data 86400s, ['m', 'h', 'd']computation_list, 1440.0m, 24.0h, 1.0d + +time2 data {time2} +-------------- +time2 8640.00 [minute] +time2 144.00 [hour] +time2 518400.00 [second] +time2 6.00 [day] +time2 computation list m,h,d +time2 data 518400s, ['m', 'h', 'd']computation_list, 8640.0m, 144.0h, 6.0d + +------------- +changing time +------------- + +time base 175800.0s, ['m', 'h', 'd', 'm']computation_list +-------------- +time 2930.00 [minute] +time 2930.00 [minute] (re-access) +time 2930.00 [minute] (re-access) +time 2930.00 [minute] (re-access) +time 48.83 [hour] +time 48.83 [hour] (re-access) +time 48.83 [hour] (re-access) +time 175800.00 [second] +time 2.03 [day] +time computation list m,h,d,m,m,h,d +time data 175800.0s, ['m', 'h', 'd', 'm', 'm', 'h', 'd']computation_list, 2930.0m, 48.833333333333336h, 2.0347222222222223d + +time2 base 1039800.0s, ['m', 'h', 'd', 'm']computation_list +-------------- +time2 17330.00 [mminute] +time2 288.83 [hour] +time2 1039800.00 [second] +time2 12.03 [day] +time2 computation list m,h,d,m,m,h,d +time2 data 1039800.0s, ['m', 'h', 'd', 'm', 'm', 'h', 'd']computation_list, 17330.0m, 288.8333333333333h, 12.034722222222221d +""".lstrip() + + +def test_unit_argparse_1d(capsys): + mf = miniflask.init("modules") + mf.load(["main", "defineunits_cached"]) + time1, time2 = "1d", "6d" + mf.parse_args([ + "--time", time1, + "--time2", time2 + ]) + mf.event.main() + assert assert_out.format(time1=time1, time2=time2) == capsys.readouterr().out + + +def test_unit_argparse_24h(capsys): + mf = miniflask.init("modules") + mf.load(["main", "defineunits_cached"]) + time1, time2 = "24h", "144h" + mf.parse_args([ + "--time", time1, + "--time2", time2 + ]) + mf.event.main() + assert assert_out.format(time1=time1, time2=time2) == capsys.readouterr().out + + +def test_unit_argparse_1440m(capsys): + mf = miniflask.init("modules") + mf.load(["main", "defineunits_cached"]) + time1, time2 = "24h", "144h" + mf.parse_args([ + "--time", time1, + "--time2", time2 + ]) + mf.event.main() + assert assert_out.format(time1=time1, time2=time2) == capsys.readouterr().out From b3e6c07a92a08218364621ec39ae11dcd8205025 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Thu, 11 Nov 2021 22:35:04 +0100 Subject: [PATCH 43/45] mf: added Unit- and UnitValue-Class and mf.register_unit --- src/miniflask/miniflask.py | 129 +++++++++++++++++++++++++++-- src/miniflask/settings/__init__.py | 4 +- src/miniflask/util.py | 90 ++++++++++++++++++++ 3 files changed, 216 insertions(+), 7 deletions(-) diff --git a/src/miniflask/miniflask.py b/src/miniflask/miniflask.py index 6501335c..7783fd55 100644 --- a/src/miniflask/miniflask.py +++ b/src/miniflask/miniflask.py @@ -25,7 +25,7 @@ from .event import event, event_obj 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 getModulesAvail, EnumAction, get_relative_id, Unit, UnitValue, make_unitvalue_argparse from .util import ( highlight_error, highlight_name, @@ -41,7 +41,6 @@ get_varid_from_fuzzy, get_full_base_module_name, ) - from .settings import listsettings @@ -133,6 +132,7 @@ def __init__(self, *module_repositories, debug=False): {} ) # saves the information of the respective variables (initial value, registration, etc.) self._state_overwrites_list = [] + self.units = {} self.modules_loaded = {} self.modules_ignored = [] self.modules_avail = getModulesAvail(self.module_repositories) @@ -864,8 +864,8 @@ def register_defaults( 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"] - ``` - + - [**Units**](../../08-API/02-miniflask-Instance/10-register_unit.md) (`Units`) + Units are custom numbers with multiple representations. Note: This method is the base method for variable registrations. @@ -1038,7 +1038,9 @@ def _settings_parser_add( kwarg["const"] = True # define the actual arguments - if argtype in [int, str, float, str2bool, Enum]: + if argtype == UnitValue: + kwarg["type"] = make_unitvalue_argparse(val) + if argtype in [int, str, float, str2bool, Enum, UnitValue]: self.settings_parser.add_argument("--" + varname, **kwarg) else: raise ValueError( @@ -1060,9 +1062,126 @@ def _settings_parser_add( return kwarg + def register_unit(self, unit_id, get_converter, set_converter, units): + r""" + Custom numbers with multiple representations. + + Args: + - `unit_id`: Unique id to distinguish different units. + - `converter`: A method with the signature `(src_value: Number, src_unit: str, dest_unit: str)` + - `units`: A list of lists. Each list specifies synonymes for units. + + Examples: + + **First, we define `time` to be a unit with four different representations with getter/setter functions to convert between these.**: + (Note that you can save arbitrary data to `unitvalue.data` and define yourself how units behave). + ```python + + + # define how to transform data when initializing a unit + def init_time_unit(data): + units = list(data.keys()) + assert len(units) == 1, f"Unitvalue should contain exactly one data point, but found {units}." + unit = units[0] + value = list(data.values())[0] + data = {"computation_list": []} + if unit == "m": + return {"s": 60 * value, **data} + elif unit == "h": + return {"s": 60 * 60 * value, **data} + elif unit == "d": + return {"s": 24 * 60 * 60 * value, **data} + raise ValueError(f"Could not determine initial values from {repr(data)}.") + + + def get_time_unit(unitvalue, targetunit): + + # we use seconds as base unit und convert any start to that value + if "s" not in unitvalue.data: + unitvalue.data = init_time_unit(unitvalue.data) + + unitvalue.data["computation_list"].append(targetunit) + + if targetunit == "s": + return unitvalue.data["s"] + if targetunit == "m": + return unitvalue.data["s"] / 60 + elif targetunit == "h": + return unitvalue.data["s"] / 60 / 60 + elif targetunit == "d": + return unitvalue.data["s"] / 60 / 60 / 24 + + raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}") + + + def set_time_unit(unitvalue, targetunit, newvalue): + + # we use seconds as base unit und convert any start to that value + if "s" not in unitvalue.data: + unitvalue.data = init_time_unit(unitvalue.data) + + unitvalue.data["computation_list"].append(targetunit) + + if targetunit == "s": + unitvalue.data["s"] += newvalue + return + elif targetunit == "m": + unitvalue.data["s"] += 60 * newvalue + return + elif targetunit == "h": + unitvalue.data["s"] += 60 * 60 * newvalue + return + elif targetunit == "d": + unitvalue.data["s"] += 24 * 60 * 60 * newvalue + return + + raise ValueError(f"Could not query {targetunit} from {repr(unitvalue)}") + + time = mf.register_unit("time", get_time_unit, set_time_unit, [ + ["m", "minute", "minutes"], + ["h", "hour", "hours"], + ["s", "second", "seconds"], + ["d", "day", "days"] + ]) + ``` + + **In CLI, we can overwrite this unit using**: + - `--time 1.5d` (1.5 days) + - `--time 24h` (24 hour) + - `--time 500m` (500 minutes) + + **Using `u in v` Notation, we can specify the base unit to convert all calculations to.**: + - `--time 12h in d` (0.5 days) + - `--time 1.5d in h` (18 hours) + + **To register a unit, pass it to `register_defaults` as follows**: + ```python + mf.register_defaults({ + "time": time(6.0, "h"), + "time2": time(6.0, "h in d"), # in notation works here as well + }) + ``` + + **Querying other representations can be done easily, once defined:** + ```python + state["time"].unit # the base unit name + state["time"].second + state["time"].minute + state["time"].hour + state["time"].day + ``` + """ # noqa: W291 + if unit_id in self.units: + raise ValueError(f"Unit with name {unit_id} is already defined.") + + u = Unit(unit_id, get_converter, set_converter, units) + self.units[unit_id] = u + return u + # ======= # # runtime # # ======= # + def stop_parse(self): r""" Stops loading of any new modules immediately and halts any further executions. diff --git a/src/miniflask/settings/__init__.py b/src/miniflask/settings/__init__.py index 1dc67e0e..b19806c6 100644 --- a/src/miniflask/settings/__init__.py +++ b/src/miniflask/settings/__init__.py @@ -5,7 +5,7 @@ from colored import attr, fg -from ..util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite, highlight_event +from ..util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite, highlight_event, UnitValue html_module = lambda x: x # noqa: E731 no-lambda @@ -76,7 +76,7 @@ def listsettings(mf, state, asciicodes=True): k_hidden[-1] = color_module(k_hidden[-1]) 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) + value_str = attr_fn('dim') + "λ ⟶ " + attr_fn('reset') + str(v_precli) if is_lambda or isinstance(v_precli, UnitValue) 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) diff --git a/src/miniflask/util.py b/src/miniflask/util.py index b1630d6f..938c8afa 100644 --- a/src/miniflask/util.py +++ b/src/miniflask/util.py @@ -5,6 +5,7 @@ from os import walk, path from pathlib import Path from pkgutil import resolve_name +from dataclasses import dataclass from colored import attr, fg @@ -168,3 +169,92 @@ def __call__(self, parser, namespace, values, option_string=None, return_enum=Fa if return_enum: return enum setattr(namespace, self.dest, enum) + + +# ===== # +# Units # +# ===== # + +# base unit class +# (used to save the unit endings and the converter functions) +class Unit: # pylint: disable=too-few-public-methods + + def __init__(self, name, get_converter, set_converter, units): + self.name = name + self.get_converter = get_converter + self.set_converter = set_converter + + # units is a list of lists + # - the first element of each list the canonical ending for each unit-form + # - using the unit-endings, we define a dict that always translates to the first/canonical unit-ending + # - in case a unit-ending has been used twice, throw an error + self.units = {} + for keys in units: + k_uid = keys[0] + for k in keys: + if k in self.units: + raise ValueError(f"All Unit-Synonymes must be unique, but {k} has been used already.") + self.units[k] = k_uid + + def __call__(self, value, unit, data=None): + if data is None: + data = {} + return UnitValue(self, {self.units[unit]: value, **data}) + + +# unit value class +# (used to actually save the value) +@dataclass +class UnitValue: + _unitclass: Unit + data: dict + + def __getattr__(self, name): + if name in self.data: + return self.data[name] + name = self._unitclass.units[name] + if name in self.data: + return self.data[name] + return self._unitclass.get_converter(self, self._unitclass.units[name]) + + def __setattr__(self, name, val): + if name in ["_unitclass", "data"]: + object.__setattr__(self, name, val) + elif name in self.data: + self.data[name] = val + else: + name = self._unitclass.units[name] + self._unitclass.set_converter(self, name, val) + + def __str__(self): + return f"{', '.join(str(v)+str(k) for k,v in self.data.items())}" + + def __repr__(self): + return "UnitValue(" + f"{', '.join(str(v)+str(k) for k,v in self.data.items())}" + ")" + + +# factory for objects +# (used by argparse to convert strings to unit values) +def make_unitvalue_argparse(unitvalue): + class UnitValueArgparse: # pylint: disable=too-few-public-methods + def __new__(cls, string): + + # iterate over units, to check which format has been used for the given string + units = sorted(unitvalue._unitclass.units.keys(), key=len, reverse=True) + for u in units: + if string.endswith(u): + + # convert string to number + number_str = string[:-len(u)] + try: + number = int(number_str) + except ValueError: + number = float(number_str) + + # construct unit from argument + return UnitValue(unitvalue._unitclass, {unitvalue._unitclass.units[u]: number}) + + units_str = ",".join(units) + raise ValueError(f"I do not know how to convert {string} to a Unit. Possible endings are {units_str}.") + + return UnitValueArgparse From 4e2f21651fcf50ba8e09aa0490563b679cdd59a2 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 14 Feb 2023 10:07:16 +0100 Subject: [PATCH 44/45] git: move actions/create-release to softprops/action-gh-release --- .github/workflows/pypi.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index b581d7db..6d6d6e9a 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -28,12 +28,11 @@ jobs: - name: Create Release if: steps.autotag.outputs.tagsha id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.autotag.outputs.tagname }} release_name: "Miniflask ${{ steps.autotag.outputs.tagname }}" + generate_release_notes: true - name: Show version if: steps.autotag.outputs.tagsha run: echo ${{ steps.autotg.outputs.tagsha }} From dc7b4b584d435c87058fe3cd9fab3d96a81dfb34 Mon Sep 17 00:00:00 2001 From: David Hartmann Date: Tue, 14 Feb 2023 11:47:40 +0100 Subject: [PATCH 45/45] release version 5.0.0 --- src/miniflask/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miniflask/__init__.py b/src/miniflask/__init__.py index d2af2f84..7f0817bc 100644 --- a/src/miniflask/__init__.py +++ b/src/miniflask/__init__.py @@ -3,7 +3,7 @@ from .state import optional # meta -__version__ = "4.3.2" +__version__ = "5.0.0" __title__ = "miniflask" __description__ = "Small research-oriented hook-based plugin engine." __url__ = "https://github.com/da-h/miniflask"