Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/06-Predefined-Modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ $$

---


### Module `definitions`
This module extends `settings` by showing also the code-locations of the setting-definitions.

**Example Output:**
{.customsyntax}$$
Folder│\title{module}│\literal{variable} = value
—————————————————————————————
data│\title{augment}│\literal{fn} = transform
│ │ definition in line 10 in file 'data/augment/__init__.py'
│\title{batches}│\literal{size} = 4
│ │ definition in line 10 in file 'data/augment/__init__.py'
loops│\title{main_loop}│\literal{epoch} = 2
│ │ definition in line 42 in file 'loops/main_loop/__init__.py'
--------------------------------------------------
$$

---


### Module `info`
Calls `modules` and then `events`.
(Finishes the program afterwards.)
Expand Down
17 changes: 16 additions & 1 deletion src/miniflask/miniflask.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def __init__(self, module_dirs, debug=False):
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.default_modules = []
self.bind_events = True
Expand Down Expand Up @@ -745,6 +746,11 @@ class TESTENUM(Enum):

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))

def _settings_parser_add(self, varname, val, caller_traceback, nargs=None, default=None): # noqa: C901 too-complex

# lists are just multiple arguments
Expand Down Expand Up @@ -1026,7 +1032,16 @@ def parse_args(self, # noqa: C901 too-complex pylint: disable=too-many-stateme
if self.state[variables[0]] is None:
missing_arguments.append(variables)
if len(missing_arguments) > 0:
self.settings_parser.error(linesep + linesep.join(["The following argument is required: " + " or ".join(["--" + arg for arg in reversed(args)]) for args in missing_arguments]))
args_err_strs = []
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 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')
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'))
args_err_strs.append(arg_err_str)
raise ValueError(("Missing CLI-arguments or unspecified variables during miniflask call." + linesep + linesep.join(args_err_strs)))

# finally parse lambda-dependencies
for varname, (val, cliargs, parsefn, caller_traceback, _mf) in self._settings_parse_later.items():
Expand Down
2 changes: 1 addition & 1 deletion src/miniflask/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

def registerPredefined(modules_avail):
for m in ["modules", "events", "info", "settings"]:
for m in ["modules", "events", "info", "settings", "definitions"]:
module_name_id = 'miniflask.' + m
importname = 'miniflask.modules.' + m
modules_avail[module_name_id] = {
Expand Down
6 changes: 6 additions & 0 deletions src/miniflask/modules/definitions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

def register(mf):
mf.load("settings")
mf.overwrite_globals({
"settings.show_registration_definitions": True
})
25 changes: 18 additions & 7 deletions src/miniflask/modules/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from enum import EnumMeta
from itertools import zip_longest

from colored import attr
from colored import attr, fg

from ...util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite
from ...util import highlight_module, highlight_val, highlight_name, highlight_val_overwrite, highlight_event
from ...state import like


Expand All @@ -14,7 +14,7 @@
html_val_overwrite = lambda x: x # noqa: E731 no-lambda


def listsettings(state, asciicodes=True):
def listsettings(mf, state, asciicodes=True):

# colors
color_module = highlight_module if asciicodes else html_module
Expand Down Expand Up @@ -54,15 +54,23 @@ def listsettings(state, asciicodes=True):
append = "" if not overwritten else " ⟶ " + color_val_overwrite(str(v))
text += "│".join(k_hidden) + (" " * (maxklen - klen)) + " = " + color_val(value_str) + append + linesep

# add definition paths
if state["show_registration_definitions"]:
prefix = "│".join([" " * len(k) for k in korig.split(".")]) + (" " * (maxklen - klen)) + " "
for definition_type, caller_traceback in mf._settings_parser_tracebacks[korig]:
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'))
text += prefix + arg_err_str + linesep

return text


def init(state):
print(listsettings(state))
def init(mf, state):
print(listsettings(mf, state))


def settings_html(state):
html = listsettings(state, asciicodes=False)
def settings_html(mf, state):
html = listsettings(mf, state, asciicodes=False)
html = html.split("\n")
html = "\n".join([h.replace('=', '</td><td style="padding: 0 0.5em;">=</td><td><code>', 1) for h in html])
html = html.replace('\n', '</code></td></tr>\n<tr><td style="text-align:left;">')
Expand All @@ -72,5 +80,8 @@ def settings_html(state):


def register(mf):
mf.register_helpers({
"show_registration_definitions": False
})
mf.register_event("init", init, unique=False)
mf.register_event("settings_html", settings_html, unique=False)
72 changes: 51 additions & 21 deletions tests/argparse/required/test_argparse_required_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import re
from pathlib import Path
import pytest

import miniflask # noqa: E402

mf = miniflask.init(
Expand All @@ -9,26 +8,57 @@
)


def test_types(capsys):
def escape_ansi(line):
ansi_escape = re.compile(r'(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]')
return ansi_escape.sub('', line)


def test_types():
mf.load("module1")
error_message = """
The following argument is required: --modules.module1.int1
The following argument is required: --modules.module1.int2
The following argument is required: --modules.module1.float1
The following argument is required: --modules.module1.float2
The following argument is required: --modules.module1.float3
The following argument is required: --modules.module1.float4
The following argument is required: --modules.module1.float5
The following argument is required: --modules.module1.float6
The following argument is required: --modules.module1.enum1
The following argument is required: --modules.module1.str1
The following argument is required: --modules.module1.str2
The following argument is required: --modules.module1.str3
""".strip()
with pytest.raises(SystemExit) as excinfo:
Missing CLI-arguments or unspecified variables during miniflask call.
--modules.module1.int1
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.int2
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.float1
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.float2
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.float3
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.float4
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.float5
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.float6
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.bool1
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.bool2
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.enum1
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.str1
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.str2
Defined in line 38 in file 'modules/module1/__init__.py'.
--modules.module1.str3
Defined in line 38 in file 'modules/module1/__init__.py'.
""".strip().split("\n")
try:
mf.parse_args(argv=[])
captured = capsys.readouterr()
assert excinfo.value.args[0] == 2
except ValueError as excinfo:
errtext = escape_ansi(str(excinfo)).split("\n")

assert len(error_message) == len(errtext)
for expected, error in zip(error_message, errtext):

# we allow any order of the messages "The following argument is required ..."
assert all(message in captured.err for message in error_message.split("\n"))
# we ignore the folder from which pytest is run
# (this would change the relative file path of the error message)
if "'" in expected:
start, end = expected.strip().split("'", 1)
assert error.strip().startswith(start)
assert error.strip().endswith(end)
else:
assert error.strip() == expected.strip()