From 786246e82652ca32fc6a6107ba47fee06fe90a77 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:11:20 +0200 Subject: [PATCH 01/25] ruff rules --- pyproject.toml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f63224..88d5978 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,4 +61,57 @@ all = [ homepage = "https://github.com/pylhc/generic_parser" repository = "https://github.com/pylhc/generic_parser" documentation = "https://pylhc.github.io/generic_parser/" -changelog = "https://github.com/pylhc/generic_parser/blob/master/CHANGELOG.md" \ No newline at end of file +changelog = "https://github.com/pylhc/generic_parser/blob/master/CHANGELOG.md" + +# ----- Dev Tools Configuration ----- # + +[tool.ruff] +exclude = [ + ".eggs", + ".git", + ".mypy_cache", + ".venv", + "_build", + "build", + "dist", +] + +# Assume Python 3.10+ +target-version = "py310" + +line-length = 100 +indent-width = 4 + +[tool.ruff.lint] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +ignore = [ + "E501", # line too long + "FBT001", # boolean-type-hint-positional-argument + "FBT002", # boolean-default-value-positional-argument + "PT019", # pytest-fixture-param-without-value (but suggested solution fails) +] +extend-select = [ + "F", # Pyflakes rules + "W", # PyCodeStyle warnings + "E", # PyCodeStyle errors + "I", # Sort imports properly + "A", # Detect shadowed builtins + "N", # enforce naming conventions, e.g. ClassName vs function_name + "UP", # Warn if certain things can changed due to newer Python versions + "C4", # Catch incorrect use of comprehensions, dict, list, etc + "FA", # Enforce from __future__ import annotations + "FBT", # detect boolean traps + "ISC", # Good use of string concatenation + "BLE", # disallow catch-all exceptions + "ICN", # Use common import conventions + "RET", # Good return practices + "SIM", # Common simplification rules + "TID", # Some good import practices + "TC", # Enforce importing certain types in a TYPE_CHECKING block + "PTH", # Use pathlib instead of os.path + "NPY", # Some numpy-specific things +] +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] From 8ea0377f9c432f906f5d5a409af8af7d2834429d Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:15:49 +0200 Subject: [PATCH 02/25] formatting --- doc/conf.py | 18 +- generic_parser/dict_parser.py | 207 ++++++++++++--------- generic_parser/entry_datatypes.py | 13 +- generic_parser/entrypoint_parser.py | 113 ++++++++---- generic_parser/tools.py | 29 +-- tests/conftest.py | 5 +- tests/test_dict_parser.py | 28 +-- tests/test_entrypoint.py | 276 ++++++++++++++++------------ tests/test_tools.py | 6 +- 9 files changed, 417 insertions(+), 278 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 05e8126..171ec60 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -126,14 +126,14 @@ def about_package(init_posixpath: pathlib.Path) -> dict: html_theme = "sphinx_rtd_theme" html_theme_options = { - 'collapse_navigation': False, - 'display_version': True, - 'logo_only': True, - 'navigation_depth': 2, + "collapse_navigation": False, + "display_version": True, + "logo_only": True, + "navigation_depth": 2, } -html_logo = '_static/img/omc_logo.svg' -html_static_path = ['_static'] +html_logo = "_static/img/omc_logo.svg" +html_static_path = ["_static"] html_css_files = ["css/custom.css"] smartquotes_action = "qe" # renders only quotes and ellipses (...) but not dashes (option: D) @@ -189,7 +189,7 @@ def about_package(init_posixpath: pathlib.Path) -> dict: # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, "generic-parser.tex", u"generic-parser Documentation", u"OMC-TEAM", "manual"), + (master_doc, "generic-parser.tex", "generic-parser Documentation", "OMC-TEAM", "manual"), ] @@ -197,7 +197,7 @@ def about_package(init_posixpath: pathlib.Path) -> dict: # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "generic-parser", u"Generic-Parser Documentation", [author], 1)] +man_pages = [(master_doc, "generic-parser", "Generic-Parser Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -209,7 +209,7 @@ def about_package(init_posixpath: pathlib.Path) -> dict: ( master_doc, "generic-parser", - u"generic-parser Documentation", + "generic-parser Documentation", author, "OMC-TEAM", "One line description of project.", diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index e542836..878dcdc 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -4,6 +4,7 @@ This module holds classes to handle different `dictionaries` as argument containers. """ + import argparse import copy import logging @@ -54,9 +55,10 @@ def _validate_parameters(dictionary): Args: dictionary: Dictionary to validate. """ + # Helper ------------------------------------------ def _check_key(key, param): - """ Checks if key coincides with param.name. """ + """Checks if key coincides with param.name.""" if key != param.name: raise ParameterError(f"'{key:s}': Key and name need to be the same.") @@ -96,49 +98,58 @@ def _check_value(key, arg_dict, param_dict): param = param_dict[key] if not arg_dict or key not in arg_dict: if param.required: - raise ArgumentError(f"'{key:s}' required in options.\n" - f"Help: {param.help:s}") + raise ArgumentError(f"'{key:s}' required in options.\nHelp: {param.help:s}") return param.default opt = arg_dict[key] if opt is None: if param.required: - raise ArgumentError(f"'{key:s}' required in options.\n" - f"Help: {param.help:s}") + raise ArgumentError(f"'{key:s}' required in options.\nHelp: {param.help:s}") return opt if param.type and not isinstance(opt, param.type): - raise ArgumentError(f"'{key:s}' is not of type {param.type.__name__:s}.\n" - f"Help: {param.help:s}") + raise ArgumentError( + f"'{key:s}' is not of type {param.type.__name__:s}.\nHelp: {param.help:s}" + ) if param.type == list: if param.nargs: if isinstance(param.nargs, int) and not param.nargs == len(opt): - raise ArgumentError(f"'{key:s}' should be list of length {param.nargs:d}," - f" instead it was of length {len(opt):d}.\n" - f"Help: {param.help:s}") + raise ArgumentError( + f"'{key:s}' should be list of length {param.nargs:d}," + f" instead it was of length {len(opt):d}.\n" + f"Help: {param.help:s}" + ) if param.nargs == argparse.ONE_OR_MORE and not len(opt): - raise ArgumentError(f"'{key:s}' should be list of length >= 1," - f" instead it was of length {len(opt):d}.\n" - f"Help: {param.help:s}") + raise ArgumentError( + f"'{key:s}' should be list of length >= 1," + f" instead it was of length {len(opt):d}.\n" + f"Help: {param.help:s}" + ) if param.subtype: for idx, item in enumerate(opt): if not isinstance(item, param.subtype): - raise ArgumentError(f"Item {idx:d} of '{key:s}'" - f" is not of type '{param.subtype.__name__:s}'.\n" - f"Help: {param.help:s}") + raise ArgumentError( + f"Item {idx:d} of '{key:s}'" + f" is not of type '{param.subtype.__name__:s}'.\n" + f"Help: {param.help:s}" + ) if param.choices and any([o for o in opt if o not in param.choices]): - raise ArgumentError(f"All elements of '{key:s}' need to be one of " - f"'{param.choices}', instead the list was {opt}.\n" - f"Help: {param.help:s}") + raise ArgumentError( + f"All elements of '{key:s}' need to be one of " + f"'{param.choices}', instead the list was {opt}.\n" + f"Help: {param.help:s}" + ) elif param.choices and opt not in param.choices: - raise ArgumentError(f"'{key:s}' needs to be one of '{param.choices}', " - f"instead it was {opt}.\n" - f"Help: {param.help:s}") + raise ArgumentError( + f"'{key:s}' needs to be one of '{param.choices}', " + f"instead it was {opt}.\n" + f"Help: {param.help:s}" + ) return opt def _parse_arguments(self, arg_dict, param_dict): @@ -229,7 +240,7 @@ def add_parameter(self, param, **kwargs): Returns: This object. """ - loc = kwargs.pop('loc', None) + loc = kwargs.pop("loc", None) if not isinstance(param, Parameter): param = Parameter(param, **kwargs) self._add_param_to_dict(param, loc) @@ -246,9 +257,9 @@ def add_parameter_dict(self, dictionary, loc): Returns: This object. """ - fields = loc.split('.') + fields = loc.split(".") name = fields[-1] - sub_dict = self._traverse_dict('.'.join(fields[:-1])) + sub_dict = self._traverse_dict(".".join(fields[:-1])) if name in sub_dict: raise ParameterError(f"'{name}' already exists in parser!") @@ -263,35 +274,35 @@ def help(self): def tree(self): """Prints the current Parameter-Tree.""" + def print_tree(tree, level_char): for i, key in enumerate(sorted(tree.keys())): if i == len(tree) - 1: - node_char = _TC['L'] + _TC['-'] - level_char_pp = level_char + ' ' + node_char = _TC["L"] + _TC["-"] + level_char_pp = level_char + " " else: - node_char = _TC['S'] + _TC['-'] - level_char_pp = level_char + _TC['|'] + ' ' + node_char = _TC["S"] + _TC["-"] + level_char_pp = level_char + _TC["|"] + " " LOG.info(f"{level_char:s}{node_char:s} {key:s}") if isinstance(tree[key], dict): print_tree(tree[key], level_char_pp) else: leaf = tree[key] - LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s}" - f" Required: {leaf.required}") + LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s} Required: {leaf.required}") - LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s}" - f" Default: {leaf.default}") + LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s} Default: {leaf.default}") - LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s}" - f" Type: {leaf.type.__name__ if leaf.type else 'None'}") + LOG.info( + f"{level_char_pp + _TC['S'] + _TC['-']:s}" + f" Type: {leaf.type.__name__ if leaf.type else 'None'}" + ) - LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s}" - f" Choices: {leaf.choices}") + LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s} Choices: {leaf.choices}") - LOG.info(f"{level_char_pp + _TC['L'] + _TC['-']:s}" - f" Help: {leaf.help:s}") - LOG.info('Parameter Dictionary') - print_tree(self.dictionary, '') + LOG.info(f"{level_char_pp + _TC['L'] + _TC['-']:s} Help: {leaf.help:s}") + + LOG.info("Parameter Dictionary") + print_tree(self.dictionary, "") ######################### # Private Methods @@ -328,7 +339,7 @@ def _traverse_dict(self, loc=None): """ d = self.dictionary if loc: - traverse = loc.split('.') + traverse = loc.split(".") for i, t in enumerate(traverse): try: d = d[t] @@ -338,11 +349,14 @@ def _traverse_dict(self, loc=None): if isinstance(d, Parameter): raise ParameterError( "'{:s}' is already an argument and hence cannot be a subdict.".format( - '.'.join(traverse[:i] + [t]))) + ".".join(traverse[:i] + [t]) + ) + ) return d def _convert_config_items(self, items): """Converts items list to a dictionary with types already in place.""" + def evaluate(name, item): try: return eval(item) # sorry for using that @@ -351,7 +365,7 @@ def evaluate(name, item): def eval_type(my_type, item): if issubclass(my_type, (str, Path)): - return my_type(item.strip("\'\"")) + return my_type(item.strip("'\"")) if issubclass(my_type, bool): return bool(eval(item)) @@ -360,7 +374,7 @@ def eval_type(my_type, item): out = {} for name, value in items: - if value == '': # only needed if save_dict allows `key=` + if value == "": # only needed if save_dict allows `key=` out[name] = None # type doesn't matter elif name in self.dictionary: @@ -395,15 +409,16 @@ class ArgumentError(Exception): class Parameter: """Helper Class for DictParser.""" + def __init__(self, name, **kwargs): self.name = name - self.required = kwargs.pop('required', False) - self.default = kwargs.pop('default', None) - self.help = kwargs.pop('help', '') - self.type = kwargs.pop('type', None) - self.nargs = kwargs.pop('nargs', None) - self.subtype = kwargs.pop('subtype', None) - self.choices = kwargs.pop('choices', None) + self.required = kwargs.pop("required", False) + self.default = kwargs.pop("default", None) + self.help = kwargs.pop("help", "") + self.type = kwargs.pop("type", None) + self.nargs = kwargs.pop("nargs", None) + self.subtype = kwargs.pop("subtype", None) + self.choices = kwargs.pop("choices", None) if len(kwargs) > 0: ParameterError(f"'{kwargs.keys()}' are not valid parameters for Argument.") @@ -412,58 +427,76 @@ def __init__(self, name, **kwargs): def _validate(self): if not isinstance(self.name, str): - raise ParameterError(f"Parameter '{self.name}': " + - "Name is not a valid string.") + raise ParameterError(f"Parameter '{self.name}': " + "Name is not a valid string.") if self.default and self.type and not isinstance(self.default, self.type): - raise ParameterError(f"Parameter '{self.name:s}': " + - "Default value not of specified type.") + raise ParameterError( + f"Parameter '{self.name:s}': " + "Default value not of specified type." + ) if self.subtype and not (self.type or self.type == list): - raise ParameterError(f"Parameter '{self.name:s}': " + - "field 'subtype' is only accepted if 'type' is list.") + raise ParameterError( + f"Parameter '{self.name:s}': " + + "field 'subtype' is only accepted if 'type' is list." + ) if self.nargs: - if (not isinstance(self.nargs, int) and - self.nargs not in [argparse.ONE_OR_MORE, argparse.ZERO_OR_MORE]): - raise ParameterError(f"Parameter '{self.name:s}': " - "nargs needs to be an integer or either " - f"'{argparse.ONE_OR_MORE}' or '{argparse.ZERO_OR_MORE}'. " - f"Instead it was '{self.nargs}'") + if not isinstance(self.nargs, int) and self.nargs not in [ + argparse.ONE_OR_MORE, + argparse.ZERO_OR_MORE, + ]: + raise ParameterError( + f"Parameter '{self.name:s}': " + "nargs needs to be an integer or either " + f"'{argparse.ONE_OR_MORE}' or '{argparse.ZERO_OR_MORE}'. " + f"Instead it was '{self.nargs}'" + ) if not (self.type or self.type == list): - raise ParameterError(f"Parameter '{self.name:s}': " + - "'type' needs to be 'list' if 'nargs' is given.") - - if self.default is not None: # default-type is checked above as self.type needs to be present + raise ParameterError( + f"Parameter '{self.name:s}': " + + "'type' needs to be 'list' if 'nargs' is given." + ) + + if ( + self.default is not None + ): # default-type is checked above as self.type needs to be present if (self.nargs == argparse.ONE_OR_MORE) and not len(self.default): - raise ParameterError(f"Parameter '{self.name:s}': " + - f"Empty list as default not allowed for nargs='{self.nargs}'.") + raise ParameterError( + f"Parameter '{self.name:s}': " + + f"Empty list as default not allowed for nargs='{self.nargs}'." + ) if isinstance(self.nargs, int) and not (self.nargs == len(self.default)): - raise ParameterError(f"Parameter '{self.name:s}': " + - f"Default value has wrong length (={len(self.default):d}) " - f"for given nargs={self.nargs:d}.") + raise ParameterError( + f"Parameter '{self.name:s}': " + + f"Default value has wrong length (={len(self.default):d}) " + f"for given nargs={self.nargs:d}." + ) if self.choices: try: [choice for choice in self.choices] except TypeError: - raise ParameterError(f"Parameter '{self.name:s}': " + - "'Choices' need to be iterable.") + raise ParameterError( + f"Parameter '{self.name:s}': " + "'Choices' need to be iterable." + ) if self.default: if self.type == list: not_a_choice = [d for d in self.default if d not in self.choices] if len(not_a_choice) > 0: - raise ParameterError(f"Parameter '{self.name:s}': " + - f"Default value(s) '{str(not_a_choice)}'" - " not found in choices.") + raise ParameterError( + f"Parameter '{self.name:s}': " + + f"Default value(s) '{str(not_a_choice)}'" + " not found in choices." + ) else: if self.default not in self.choices: - raise ParameterError(f"Parameter '{self.name:s}': " + - "Default value not found in choices.") + raise ParameterError( + f"Parameter '{self.name:s}': " + "Default value not found in choices." + ) if self.type or self.subtype: if self.nargs is None: @@ -474,10 +507,14 @@ def _validate(self): if check is not None: for choice in self.choices: if not isinstance(choice, check): - raise ParameterError(f"Choice '{choice}' " + - f"of parameter '{self.name:s}': " + - f"is not of type '{check.__name__:s}'.") + raise ParameterError( + f"Choice '{choice}' " + + f"of parameter '{self.name:s}': " + + f"is not of type '{check.__name__:s}'." + ) if self.required and self.default is not None: - LOG.warning(f"Parameter '{self.name:s}': " + - "Value is required but default value is given. The latter will be ignored.") + LOG.warning( + f"Parameter '{self.name:s}': " + + "Value is required but default value is given. The latter will be ignored." + ) diff --git a/generic_parser/entry_datatypes.py b/generic_parser/entry_datatypes.py index bff4afc..88c6ffb 100644 --- a/generic_parser/entry_datatypes.py +++ b/generic_parser/entry_datatypes.py @@ -4,6 +4,7 @@ This module contains advanced datatypes to add as type to any entrypoint or parser. """ + import abc from array import array @@ -16,6 +17,7 @@ def get_instance_faker_meta(*classes): """Returns the metaclass that fakes the ``isinstance()`` and ``issubclass()`` checks.""" + class FakeMeta(abc.ABCMeta): def __instancecheck__(cls, inst): return isinstance(inst, classes) @@ -37,8 +39,8 @@ def get_multi_class(*classes): input to the classes in the given order (i.e. string-classes need to go to the end, as they 'always' succeed). """ - class MultiClass(metaclass=get_instance_faker_meta(*classes)): + class MultiClass(metaclass=get_instance_faker_meta(*classes)): @classmethod def _convert_to_a_type(cls, value): for c in classes: @@ -47,7 +49,7 @@ def _convert_to_a_type(cls, value): except (ValueError, TypeError): pass else: - cls_string = ','.join([c.__name__ for c in classes]) + cls_string = ",".join([c.__name__ for c in classes]) raise ValueError( f"The value '{value}' cant be converted to any of the classes '{cls_string:s}'" ) @@ -65,11 +67,12 @@ def __new__(cls, value): class DictAsString(metaclass=get_instance_faker_meta(str, dict)): """Use dicts in command line like {"key":value}.""" + def __new__(cls, s): if isinstance(s, dict): return s - d = eval(s, {'nan': float('nan'), 'array':array}) + d = eval(s, {"nan": float("nan"), "array": array}) if not isinstance(d, dict): raise ValueError(f"'{s}' can't be converted to a dictionary.") return d @@ -77,9 +80,10 @@ def __new__(cls, s): class BoolOrString(metaclass=get_instance_faker_meta(bool, str)): """A class that behaves like a boolean when possible, otherwise like a string.""" + def __new__(cls, value): if isinstance(value, str): - value = value.strip("\'\"") # behavior like dict-parser + value = value.strip("'\"") # behavior like dict-parser if value in TRUE_ITEMS: return True @@ -96,6 +100,7 @@ class BoolOrList(metaclass=get_instance_faker_meta(bool, list)): A class that behaves like a boolean when possible, otherwise like a list. Hint: ``list.__new__(list, value)`` returns an empty list. """ + def __new__(cls, value): if value in TRUE_ITEMS: return True diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index ea5ba71..55ab77b 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -160,6 +160,7 @@ def entry_function(opt): python script.py --entry_cfg config.ini python script.py --entry_cfg config.ini --section section """ + import copy import json import argparse @@ -176,6 +177,7 @@ def entry_function(opt): from generic_parser.dict_parser import ParameterError, ArgumentError, DictParser import logging + LOG = logging.getLogger(__name__) @@ -189,7 +191,13 @@ def entry_function(opt): class EntryPoint: - def __init__(self, parameter, strict: bool = False, argument_parser_args: Mapping = None, help_printer: Callable = None): + def __init__( + self, + parameter, + strict: bool = False, + argument_parser_args: Mapping = None, + help_printer: Callable = None, + ): """Initialize decoration: Handle the desired input parameter.""" self.strict = strict @@ -225,7 +233,7 @@ def parse(self, *args, **kwargs): section: Section to use in config file, or subdirectory to use in json file. Only works with the key-value version of config file. If not given only one-section config files are allowed. - """ + """ if len(args) > 0 and len(kwargs) > 0: raise ArgumentError("Cannot combine positional parameter with keyword parameter.") @@ -251,8 +259,17 @@ def parse(self, *args, **kwargs): def _create_config_argument(self): """Creates the config-file argument parser.""" parser = ArgumentParser() - parser.add_argument('--{}'.format(ID_CONFIG), type=str, dest=ID_CONFIG, required=True,) - parser.add_argument('--{}'.format(ID_SECTION), type=str, dest=ID_SECTION,) + parser.add_argument( + "--{}".format(ID_CONFIG), + type=str, + dest=ID_CONFIG, + required=True, + ) + parser.add_argument( + "--{}".format(ID_SECTION), + type=str, + dest=ID_SECTION, + ) return parser def _create_argument_parser(self, args_dict: Mapping): @@ -334,8 +351,9 @@ def _handle_arg(self, arg): # list of commandline parameter options = self._handle_commandline(arg) else: - raise ArgumentError("Only dictionary or configfiles " - "are allowed as positional arguments") + raise ArgumentError( + "Only dictionary or configfiles are allowed as positional arguments" + ) return options # options might include known and unknown options def _handle_kwargs(self, kwargs): @@ -343,10 +361,10 @@ def _handle_kwargs(self, kwargs): if ID_CONFIG in kwargs: if len(kwargs) > 2 or (len(kwargs) == 2 and ID_SECTION not in kwargs): raise ArgumentError( - f"Only '{ID_CONFIG:s}' and '{ID_SECTION:s}'" + - " arguments are allowed, when using a config file.") - options = self._read_config(kwargs[ID_CONFIG], - kwargs.get(ID_SECTION, None)) + f"Only '{ID_CONFIG:s}' and '{ID_SECTION:s}'" + + " arguments are allowed, when using a config file." + ) + options = self._read_config(kwargs[ID_CONFIG], kwargs.get(ID_SECTION, None)) options = self.dictparse.parse_config_items(options) elif ID_DICT in kwargs: @@ -357,9 +375,10 @@ def _handle_kwargs(self, kwargs): elif ID_JSON in kwargs: if len(kwargs) > 2 or (len(kwargs) == 2 and ID_SECTION not in kwargs): raise ArgumentError( - f"Only '{ID_JSON:s}' and '{ID_SECTION:s}'" + - " arguments are allowed, when using a json file.") - with open(kwargs[ID_JSON], 'r') as json_file: + f"Only '{ID_JSON:s}' and '{ID_SECTION:s}'" + + " arguments are allowed, when using a json file." + ) + with open(kwargs[ID_JSON], "r") as json_file: json_dict = json.load(json_file) if ID_SECTION in kwargs: @@ -370,7 +389,7 @@ def _handle_kwargs(self, kwargs): else: options = self.dictparse.parse_arguments(kwargs) - return options # options might include known and unknown options + return options # options might include known and unknown options ######################### # Helpers @@ -384,17 +403,21 @@ def _check_parameter(self): raise ParameterError("A Parameter needs a Name!") if param.get("nargs", None) == argparse.REMAINDER: - raise ParameterError(f"Parameter '{arg_name:s}' is set as remainder." + - "This method is really buggy, hence it is forbidden.") + raise ParameterError( + f"Parameter '{arg_name:s}' is set as remainder." + + "This method is really buggy, hence it is forbidden." + ) if param.get("nargs", None) == argparse.OPTIONAL: - raise ParameterError(f"Parameter '{arg_name:s}' is set as optional." + - "As entrypoint does not use 'const', the use is prohibited.") + raise ParameterError( + f"Parameter '{arg_name:s}' is set as optional." + + "As entrypoint does not use 'const', the use is prohibited." + ) if param.get("flags", None) is None: # if flags aren't supplied, it defaults to the name - LOG.debug(f'Missing flags parameter. Defaulting to --{arg_name}') - param['flags'] = [f'--{arg_name}'] + LOG.debug(f"Missing flags parameter. Defaulting to --{arg_name}") + param["flags"] = [f"--{arg_name}"] def _read_config(self, cfgfile_path, section=None): """Get content from config file.""" @@ -411,8 +434,9 @@ def _read_config(self, cfgfile_path, section=None): elif len(sections) == 1: section = sections[0] # our convention elif len(sections) > 1: - raise ArgumentError(f"'{cfgfile_path:s}' contains multiple sections. " + - " Please specify one!") + raise ArgumentError( + f"'{cfgfile_path:s}' contains multiple sections. " + " Please specify one!" + ) items = cfgparse.items(section) return items @@ -442,38 +466,46 @@ def __call__(self, func): """ func_args = getfullargspec(func).args nargs = len(func_args) - is_bound = func_args[0] in ['self', 'cls'] # naming assumption...sorry (jdilly) + is_bound = func_args[0] in ["self", "cls"] # naming assumption...sorry (jdilly) if self.strict: if not is_bound and nargs == 1: + @wraps(func) def wrapper(*args, **kwargs): return func(self.parse(*args, **kwargs)) elif is_bound and nargs == 2: + @wraps(func) def wrapper(other, *args, **kwargs): return func(other, self.parse(*args, **kwargs)) else: - raise OptionsError("In strict mode, only one option-structure will be passed." - " The entrypoint needs to have the following structure: " - " ([self/cls,] options)." - f" Found: '{func.__name__:s}({', '.join(func_args):s})'") + raise OptionsError( + "In strict mode, only one option-structure will be passed." + " The entrypoint needs to have the following structure: " + " ([self/cls,] options)." + f" Found: '{func.__name__:s}({', '.join(func_args):s})'" + ) else: if not is_bound and nargs == 2: + @wraps(func) def wrapper(*args, **kwargs): options, unknown_options = self.parse(*args, **kwargs) return func(options, unknown_options) elif is_bound and nargs == 3: + @wraps(func) def wrapper(other, *args, **kwargs): options, unknown_options = self.parse(*args, **kwargs) return func(other, options, unknown_options) else: - raise OptionsError("Two option-structures will be passed." - " The entrypoint needs to have the following structure: " - " ([self/cls,] options, unknown_options)." - f" Found: '{func.__name__:s}({', '.join(func_args):s})'") + raise OptionsError( + "Two option-structures will be passed." + " The entrypoint needs to have the following structure: " + " ([self/cls,] options, unknown_options)." + f" Found: '{func.__name__:s}({', '.join(func_args):s})'" + ) return wrapper @@ -485,6 +517,7 @@ class EntryPointParameters(DotDict): Helps to build a simple dictionary structure via add_argument functions. You really don't need that, but old habits die hard. """ + def add_parameter(self, **kwargs): """Add parameter.""" name = kwargs.pop("name") @@ -517,22 +550,22 @@ def help(self): try: flags = f"{space}flags: **{item['flags']}**\n\n" except KeyError: - flags = '' + flags = "" try: choices = f"{space}choices: ``{item['choices']}``\n\n" except KeyError: - choices = '' + choices = "" try: default = f"{space}default: ``{item['default']}``\n\n" except KeyError: - default = '' + default = "" try: action = f"{space}action: ``{item['action']}``\n\n" except KeyError: - action = '' + action = "" item_str = f"{name_and_type}{help_str}{flags}{choices}{default}{action}" @@ -611,6 +644,7 @@ def add_to_arguments(args, entry_params=None, **kwargs): # parameter adders --- + def add_params_to_generic(parser, params): """ Adds entry-point style parameter to either `ArgumentParser`, `DictParser` or @@ -672,6 +706,7 @@ def _add_params_to_argument_parser(arg_parser, params): arg_parser.add_argument(*flags, **param) return arg_parser + # --- @@ -707,8 +742,9 @@ def split_arguments(args, *param_list): # should be a dictionary of params, so do it the manual way for params in param_list: params = param_names(params) - split_args.append(DotDict([(key, args.pop(key)) - for key in list(args.keys()) if key in params])) + split_args.append( + DotDict([(key, args.pop(key)) for key in list(args.keys()) if key in params]) + ) split_args.append(DotDict(args)) return split_args @@ -750,9 +786,10 @@ def save_options_to_config(filepath, opt, unknown=None): opt: parsed known options. unknown: unknown options (only safe for non-commandline parameters). """ + def _to_key_value_str(key, value): if value is None: - value = '' # defined as empty (see dict_parser._convert_config_items) + value = "" # defined as empty (see dict_parser._convert_config_items) elif isinstance(value, (str, Path)): value = f'"{value}"' if "\n" in value: diff --git a/generic_parser/tools.py b/generic_parser/tools.py index 12d13df..5f6a843 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -4,6 +4,7 @@ Provides utilities to use in other modules. """ + import logging import os import sys @@ -13,10 +14,10 @@ LOG = logging.getLogger(__name__) _TC = { # Tree Characters - '|': u'\u2502', # Horizontal - '-': u'\u2500', # Vertical - 'L': u'\u2514', # L-Shape - 'S': u'\u251C', # Split + "|": "\u2502", # Horizontal + "-": "\u2500", # Vertical + "L": "\u2514", # L-Shape + "S": "\u251c", # Split } @@ -25,6 +26,7 @@ class DotDict(dict): """Make dict fields accessible by dot notation.""" + def __init__(self, *args, **kwargs): super(DotDict, self).__init__(*args, **kwargs) for key in self: @@ -47,16 +49,17 @@ def get_subdict(self, keys, strict=True): return DotDict(get_subdict(self, keys, strict)) -def print_dict_tree(dictionary, name='Dictionary', print_fun=LOG.info): +def print_dict_tree(dictionary, name="Dictionary", print_fun=LOG.info): """Prints a dictionary as a tree.""" + def print_tree(tree, level_char): for i, key in enumerate(sorted(tree.keys())): if i == len(tree) - 1: - node_char = _TC['L'] + _TC['-'] - level_char_pp = level_char + ' ' + node_char = _TC["L"] + _TC["-"] + level_char_pp = level_char + " " else: - node_char = _TC['S'] + _TC['-'] - level_char_pp = level_char + _TC['|'] + ' ' + node_char = _TC["S"] + _TC["-"] + level_char_pp = level_char + _TC["|"] + " " if isinstance(tree[key], dict): print_fun(f"{level_char:s}{node_char:s} {str(key):s}") @@ -64,8 +67,8 @@ def print_tree(tree, level_char): else: print_fun(f"{level_char:s}{node_char:s} {str(key):s}: {str(tree[key]):s}") - print_fun('{:s}:'.format(name)) - print_tree(dictionary, '') + print_fun("{:s}:".format(name)) + print_tree(dictionary, "") def get_subdict(full_dict, keys, strict=True): @@ -88,6 +91,7 @@ def get_subdict(full_dict, keys, strict=True): # Contexts ##################################################################### + @contextmanager def log_out(stdout=sys.stdout, stderr=sys.stderr): """Temporarily changes sys.stdout and sys.stderr.""" @@ -142,6 +146,7 @@ class TempStringLogger: module: module to log, defaults to the caller file. level: logging level, defaults to ``INFO``. """ + def __init__(self, module="", level=logging.INFO): self.stream = StringIO() self.handler = logging.StreamHandler(stream=self.stream) @@ -162,5 +167,5 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.log.setLevel(self._level) def get_log(self): - """ Get the log as string. """ + """Get the log as string.""" return self.stream.getvalue() diff --git a/tests/conftest.py b/tests/conftest.py index 5906e6c..407120d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ See also https://stackoverflow.com/a/34520971 . """ + import shutil import sys from contextlib import contextmanager @@ -15,7 +16,7 @@ @contextmanager def cli_args(*args, **kwargs): - """ Provides context to run an entrypoint like with commandline args. + """Provides context to run an entrypoint like with commandline args. Arguments are restored after context. Args: @@ -25,7 +26,7 @@ def cli_args(*args, **kwargs): script: script-name. Used as first commandline-arg. Otherwise it's 'somescript.py' """ - script = kwargs.get('script', 'somescript.py') + script = kwargs.get("script", "somescript.py") args_save = sys.argv.copy() sys.argv = [script] + list(args) yield diff --git a/tests/test_dict_parser.py b/tests/test_dict_parser.py index 5028ff1..3dfc4c4 100644 --- a/tests/test_dict_parser.py +++ b/tests/test_dict_parser.py @@ -9,16 +9,16 @@ def test_deep_dict(): - parser = DictParser({ - 'sub': {'param': Parameter('param', type=int)}, - 'sub2': {'suub': {'param': Parameter("param", type=str)}} - }, strict=True) + parser = DictParser( + { + "sub": {"param": Parameter("param", type=int)}, + "sub2": {"suub": {"param": Parameter("param", type=str)}}, + }, + strict=True, + ) # parser.tree() - opt = { - 'sub': {'param': 4}, - 'sub2': {'suub': {'param': "myString"}} - } + opt = {"sub": {"param": 4}, "sub2": {"suub": {"param": "myString"}}} opt = parser.parse_arguments(opt) assert opt.sub.param == 4 @@ -31,12 +31,16 @@ def test_add_param_loc(): assert parser.dictionary["sub"]["suub"]["suuub"]["test"].name == "test" assert parser.dictionary["sub"]["suub"]["suuub"]["test"].default == "def" + def test_non_string_outside_choices(): parser = DictParser() parser.add_parameter(Parameter("test", choices=[1, 2])) with pytest.raises(ArgumentError): - parser.parse_arguments({'test': {'param': 4},}) - + parser.parse_arguments( + { + "test": {"param": 4}, + } + ) def test_add_param_loc2(): @@ -64,8 +68,8 @@ def test_add_parameter_dict(): def test_most_basic_init(): - p = Parameter(name='test') - assert p.name == 'test' + p = Parameter(name="test") + assert p.name == "test" def test_missing_name(): diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index bd18c92..efb166e 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -7,11 +7,15 @@ from generic_parser.dict_parser import ParameterError, ArgumentError from generic_parser.entry_datatypes import get_multi_class, DictAsString, BoolOrString, BoolOrList -from generic_parser.entrypoint_parser import (EntryPointParameters, - entrypoint, EntryPoint, - OptionsError, split_arguments, - create_parameter_help, save_options_to_config - ) +from generic_parser.entrypoint_parser import ( + EntryPointParameters, + entrypoint, + EntryPoint, + OptionsError, + split_arguments, + create_parameter_help, + save_options_to_config, +) from generic_parser.tools import silence, print_dict_tree, TempStringLogger @@ -27,6 +31,7 @@ def test_strict_wrapper_fail(): with pytest.raises(OptionsError): + @entrypoint(get_simple_params(), strict=True) def strict(opt, unknown): # too many option-structures pass @@ -34,6 +39,7 @@ def strict(opt, unknown): # too many option-structures def test_class_wrapper_fail(): with pytest.raises(OptionsError): + class MyClass(object): @entrypoint(get_simple_params()) def fun(self, opt): # too few option-structures @@ -42,6 +48,7 @@ def fun(self, opt): # too few option-structures def test_normal_wrapper_fail(): with pytest.raises(OptionsError): + @entrypoint(get_simple_params()) def fun(opt, unknown, more): # too many option-structures pass @@ -78,9 +85,15 @@ def test_wrong_param_creation_other(): def test_choices_not_iterable(): with pytest.raises((ParameterError, ValueError)): # Value error comes from argparse (would be caught in dict_parser as well) - EntryPoint([{"name": "test", "flags": "--flag", - "choices": 3, - }]) + EntryPoint( + [ + { + "name": "test", + "flags": "--flag", + "choices": 3, + } + ] + ) def test_empty_list_default_for_nargs_plus(): @@ -97,26 +110,30 @@ def test_missing_flag_replaced_by_name(): ep = EntryPoint([{"name": "test"}]) assert len(ep.parameter[0]) == 2 - assert ep.parameter[0]['flags'] == ['--test'] - assert ep.parameter[0]['name'] == 'test' + assert ep.parameter[0]["flags"] == ["--test"] + assert ep.parameter[0]["name"] == "test" def test_missing_flag_replaced_by_name_in_multiple_lists(): - ep = EntryPoint([{"name": "test"}, - {"name": "thermos_coffee"}, - {"name": "tee_kessel", "flags": ['--tee_kessel']}]) + ep = EntryPoint( + [ + {"name": "test"}, + {"name": "thermos_coffee"}, + {"name": "tee_kessel", "flags": ["--tee_kessel"]}, + ] + ) assert len(ep.parameter[0]) == 2 - assert ep.parameter[0]['flags'] == ['--test'] - assert ep.parameter[0]['name'] == 'test' + assert ep.parameter[0]["flags"] == ["--test"] + assert ep.parameter[0]["name"] == "test" assert len(ep.parameter[1]) == 2 - assert ep.parameter[1]['flags'] == ['--thermos_coffee'] - assert ep.parameter[1]['name'] == 'thermos_coffee' + assert ep.parameter[1]["flags"] == ["--thermos_coffee"] + assert ep.parameter[1]["name"] == "thermos_coffee" assert len(ep.parameter[2]) == 2 - assert ep.parameter[2]['flags'] == ['--tee_kessel'] - assert ep.parameter[2]['name'] == 'tee_kessel' + assert ep.parameter[2]["flags"] == ["--tee_kessel"] + assert ep.parameter[2]["name"] == "tee_kessel" # Argument Tests @@ -133,10 +150,7 @@ def test_strict_fail(): def test_as_kwargs(): opt, unknown = paramtest_function( - name="myname", - int=3, - list=[4, 5, 6], - unknown="myfinalargument" + name="myname", int=3, list=[4, 5, 6], unknown="myfinalargument" ) assert opt.name == "myname" assert opt.int == 3 @@ -147,10 +161,7 @@ def test_as_kwargs(): def test_as_string(): opt, unknown = paramtest_function( - ["--name", "myname", - "--int", "3", - "--list", "4", "5", "6", - "--other"] + ["--name", "myname", "--int", "3", "--list", "4", "5", "6", "--other"] ) assert opt.name == "myname" assert opt.int == 3 @@ -160,8 +171,7 @@ def test_as_string(): def test_as_argv(): # almost identical to above - with cli_args("--name", "myname", "--int", "3", "--list", "4", "5", "6", - "--other"): + with cli_args("--name", "myname", "--int", "3", "--list", "4", "5", "6", "--other"): opt, unknown = paramtest_function() assert opt.name == "myname" assert opt.int == 3 @@ -173,23 +183,23 @@ def test_as_argv(): # almost identical to above def test_as_config(tmp_path): cfg_file = tmp_path / "config.ini" with open(cfg_file, "w") as f: - f.write("\n".join([ - "[Section]", - "name = 'myname'", - "int = 3", - "list = [4, 5, 6]", - "unknown = 'other'", - ])) + f.write( + "\n".join( + [ + "[Section]", + "name = 'myname'", + "int = 3", + "list = [4, 5, 6]", + "unknown = 'other'", + ] + ) + ) # test config as kwarg - opt1, unknown1 = paramtest_function( - entry_cfg=cfg_file, section="Section" - ) + opt1, unknown1 = paramtest_function(entry_cfg=cfg_file, section="Section") # test config as commandline args - opt2, unknown2 = paramtest_function( - ["--entry_cfg", str(cfg_file), "--section", "Section"] - ) + opt2, unknown2 = paramtest_function(["--entry_cfg", str(cfg_file), "--section", "Section"]) assert opt1.name == "myname" assert opt1.int == 3 @@ -222,7 +232,7 @@ def test_wrong_choice(): def test_wrong_type(): with pytest.raises(ArgumentError): - some_function(accel="LHCB1", anint=3.) + some_function(accel="LHCB1", anint=3.0) def test_wrong_type_in_list(): @@ -254,12 +264,12 @@ def fun(opt): def test_optional_parameter_default_accepts_none(): - @entrypoint(dict(foo=dict(required=False, default='test')), strict=True) + @entrypoint(dict(foo=dict(required=False, default="test")), strict=True) def fun(opt): return opt opt = fun({}) - assert opt.foo == 'test' + assert opt.foo == "test" opt = fun(foo=None) assert opt.foo is None @@ -276,6 +286,7 @@ def fun(opt): # Test Config read-write ------------------------------------------------------- + def test_save_options(tmp_path): opt, unknown = paramtest_function( name="myname", @@ -294,19 +305,16 @@ def test_save_options(tmp_path): def test_save_cli_options_cfg(tmp_path): opt, unknown = paramtest_function( - ["--name", "myname", - "--int", "3", - "--list", "4", "5", "6", - "--other"] + ["--name", "myname", "--int", "3", "--list", "4", "5", "6", "--other"] ) cfg_file = tmp_path / "config.ini" save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) - with open(cfg_file, 'r') as f: + with open(cfg_file, "r") as f: content = f.read() - assert 'Unknown' in content - assert '--other' in content + assert "Unknown" in content + assert "--other" in content _assert_dicts_equal(opt, opt_load) assert len(unknown_load) == 0 @@ -341,7 +349,7 @@ def test_save_and_load_cfg_with_none_explicit(tmp_path): def test_string_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(name={'type': str})), strict=True) + @entrypoint(EntryPointParameters(dict(name={"type": str})), strict=True) def fun(opt): return opt @@ -355,7 +363,7 @@ def fun(opt): cfg_noquotes = tmp_path / "config_noquotes.ini" with open(cfg_noquotes, "w") as f: - f.write('[Section]\nname = My String with Spaces') + f.write("[Section]\nname = My String with Spaces") opt_quotes = fun(entry_cfg=cfg_quotes) opt_doublequotes = fun(entry_cfg=cfg_doublequotes) @@ -366,7 +374,7 @@ def fun(opt): def test_string_with_break_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(name={'type': str})), strict=True) + @entrypoint(EntryPointParameters(dict(name={"type": str})), strict=True) def fun(opt): return opt @@ -380,7 +388,7 @@ def fun(opt): def test_path_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(path={'type': Path})), strict=True) + @entrypoint(EntryPointParameters(dict(path={"type": Path})), strict=True) def fun(opt): return opt @@ -394,7 +402,7 @@ def fun(opt): def test_list_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(lst={'type': int, 'nargs': "*"})), strict=True) + @entrypoint(EntryPointParameters(dict(lst={"type": int, "nargs": "*"})), strict=True) def fun(opt): return opt @@ -409,11 +417,12 @@ def fun(opt): # Special Classes -------------------------------------------------------------- + def test_multiclass_class(): float_str = get_multi_class(float, str) - assert isinstance(1., float_str) + assert isinstance(1.0, float_str) assert isinstance("", float_str) - assert isinstance(float_str(1.), float) + assert isinstance(float_str(1.0), float) assert isinstance(float_str(1), float) assert not isinstance(float_str(1), int) assert float_str("myString") == "myString" @@ -468,8 +477,8 @@ def fun(opt): opt = fun(ios=3) assert opt.ios == 3 - opt = fun(ios='3') - assert opt.ios == '3' + opt = fun(ios="3") + assert opt.ios == "3" opt = fun(["--ios", "3"]) assert opt.ios == 3 @@ -483,13 +492,13 @@ def test_dict_as_string(): def fun(opt): return opt - opt = fun(dict={'int': 5, 'str': 'hello'}) - assert opt.dict['int'] == 5 - assert opt.dict['str'] == 'hello' + opt = fun(dict={"int": 5, "str": "hello"}) + assert opt.dict["int"] == 5 + assert opt.dict["str"] == "hello" opt = fun(["--dict", "{'int': 5, 'str': 'hello'}"]) - assert opt.dict['int'] == 5 - assert opt.dict['str'] == 'hello' + assert opt.dict["int"] == 5 + assert opt.dict["str"] == "hello" def test_bool_or_str(tmp_path): @@ -500,8 +509,8 @@ def fun(opt): opt = fun(bos=True) assert opt.bos is True - opt = fun(bos='myString') - assert opt.bos == 'myString' + opt = fun(bos="myString") + assert opt.bos == "myString" opt = fun(["--bos", "False"]) assert opt.bos is False @@ -520,8 +529,13 @@ def fun(opt): def test_bool_or_str_cfg(tmp_path): - @entrypoint([dict(flags="--bos1", name="bos1", type=BoolOrString), - dict(flags="--bos2", name="bos2", type=BoolOrString)], strict=True) + @entrypoint( + [ + dict(flags="--bos1", name="bos1", type=BoolOrString), + dict(flags="--bos2", name="bos2", type=BoolOrString), + ], + strict=True, + ) def fun(opt): return opt @@ -529,7 +543,7 @@ def fun(opt): with open(cfg_file, "w") as f: f.write("[Section]\nbos1 = 'myString'\nbos2 = True") opt = fun(entry_cfg=cfg_file) - assert opt.bos1 == 'myString' + assert opt.bos1 == "myString" assert opt.bos2 is True @@ -555,8 +569,13 @@ def fun(opt): def test_bool_or_list_cfg(tmp_path): - @entrypoint([dict(flags="--bol1", name="bol1", type=BoolOrList), - dict(flags="--bol2", name="bol2", type=BoolOrList)], strict=True) + @entrypoint( + [ + dict(flags="--bol1", name="bol1", type=BoolOrList), + dict(flags="--bol2", name="bol2", type=BoolOrList), + ], + strict=True, + ) def fun(opt): return opt @@ -613,61 +632,92 @@ def test_create_param_help_other(): def get_simple_params(): - """ Parameters as a list of dicts, to test this behaviour as well.""" - return [{"name": "arg1", "flags": "--a1", }, - {"name": "arg2", "flags": "--a2", } - ] + """Parameters as a list of dicts, to test this behaviour as well.""" + return [ + { + "name": "arg1", + "flags": "--a1", + }, + { + "name": "arg2", + "flags": "--a2", + }, + ] def get_testing_params(): - """ Parameters as a dict of dicts, to test this behaviour as well.""" + """Parameters as a dict of dicts, to test this behaviour as well.""" return { "name": dict(flags="--name", type=str), "int": dict(flags="--int", type=int), - "list": dict(flags="--list", type=int, nargs="+") + "list": dict(flags="--list", type=int, nargs="+"), } def get_params(): - """ Parameters defined with EntryPointArguments (which is a dict *cough*) """ + """Parameters defined with EntryPointArguments (which is a dict *cough*)""" args = EntryPointParameters() - args.add_parameter(name="accel", - flags=["-a", "--accel"], - help="Which accelerator: LHCB1 LHCB2 LHCB4? SPS RHIC TEVATRON", - choices=["LHCB1", "LHCB2", "LHCB5"], - required=True, - ) - args.add_parameter(name="anint", - flags=["-i", "--int"], - help="Just a number.", - type=int, - required=True, - ) - args.add_parameter(name="alist", - flags=["-l", "--lint"], - help="Just a number.", - type=int, - nargs="+", - ) - args.add_parameter(name="anotherlist", - flags=["-k", "--alint"], - help="list.", - type=str, - nargs=3, - default=["a", "c", "f"], - choices=["a", "b", "c", "d", "e", "f"], - ), + args.add_parameter( + name="accel", + flags=["-a", "--accel"], + help="Which accelerator: LHCB1 LHCB2 LHCB4? SPS RHIC TEVATRON", + choices=["LHCB1", "LHCB2", "LHCB5"], + required=True, + ) + args.add_parameter( + name="anint", + flags=["-i", "--int"], + help="Just a number.", + type=int, + required=True, + ) + args.add_parameter( + name="alist", + flags=["-l", "--lint"], + help="Just a number.", + type=int, + nargs="+", + ) + ( + args.add_parameter( + name="anotherlist", + flags=["-k", "--alint"], + help="list.", + type=str, + nargs=3, + default=["a", "c", "f"], + choices=["a", "b", "c", "d", "e", "f"], + ), + ) return args def get_other_params(): - """ For testing the create_param_help()""" - args = EntryPointParameters({ - "arg1": dict(flags="--arg1", help="A help.", default=1,), - "arg2": dict(flags="--arg2", help="More help.", default=2,), - "arg3": dict(flags="--arg3", help="Even more...", default=3,), - "arg4": dict(flags="--arg4", help="...heeeeeeeeelp.", default=4,), - }) + """For testing the create_param_help()""" + args = EntryPointParameters( + { + "arg1": dict( + flags="--arg1", + help="A help.", + default=1, + ), + "arg2": dict( + flags="--arg2", + help="More help.", + default=2, + ), + "arg3": dict( + flags="--arg3", + help="Even more...", + default=3, + ), + "arg4": dict( + flags="--arg4", + help="...heeeeeeeeelp.", + default=4, + ), + } + ) return args diff --git a/tests/test_tools.py b/tests/test_tools.py index a3d3650..fd9cff8 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -7,7 +7,7 @@ def test_dot_dict(simple_dict): dd = DotDict(simple_dict) assert dd.a == 1 - assert dd.b == 'str' + assert dd.b == "str" assert dd.c.e == [1, 2, 3] @@ -15,7 +15,7 @@ def test_get_subdict(simple_dict): dd = DotDict(simple_dict) sub = dd.get_subdict(["a", "b"]) assert sub.a == 1 - assert sub.b == 'str' + assert sub.b == "str" assert "c" not in sub @@ -42,4 +42,4 @@ def print_with_blank_line(s): @pytest.fixture() def simple_dict(): - return dict(a=1, b='str', c=dict(e=[1, 2, 3])) + return dict(a=1, b="str", c=dict(e=[1, 2, 3])) From 2079a303eaa8bb37088c23d8dd0a7e3d6c49bc26 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:16:39 +0200 Subject: [PATCH 03/25] simple automated fixes --- doc/conf.py | 1 - generic_parser/dict_parser.py | 18 +++++++------- generic_parser/entry_datatypes.py | 14 +++++------ generic_parser/entrypoint_parser.py | 37 +++++++++++++---------------- generic_parser/tools.py | 2 +- tests/conftest.py | 4 ---- tests/test_dict_parser.py | 7 +++--- tests/test_entrypoint.py | 25 ++++++++++--------- tests/test_tools.py | 1 + 9 files changed, 49 insertions(+), 60 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 171ec60..2d6de2a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Generic-Parser documentation build configuration file, created by # sphinx-quickstart on Tue Feb 6 12:10:18 2018. diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index 878dcdc..207632b 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -10,7 +10,7 @@ import logging from pathlib import Path -from generic_parser.tools import DotDict, _TC +from generic_parser.tools import _TC, DotDict LOG = logging.getLogger(__name__) @@ -437,7 +437,7 @@ def _validate(self): if self.subtype and not (self.type or self.type == list): raise ParameterError( f"Parameter '{self.name:s}': " - + "field 'subtype' is only accepted if 'type' is list." + "field 'subtype' is only accepted if 'type' is list." ) if self.nargs: @@ -455,7 +455,7 @@ def _validate(self): if not (self.type or self.type == list): raise ParameterError( f"Parameter '{self.name:s}': " - + "'type' needs to be 'list' if 'nargs' is given." + "'type' needs to be 'list' if 'nargs' is given." ) if ( @@ -464,13 +464,13 @@ def _validate(self): if (self.nargs == argparse.ONE_OR_MORE) and not len(self.default): raise ParameterError( f"Parameter '{self.name:s}': " - + f"Empty list as default not allowed for nargs='{self.nargs}'." + f"Empty list as default not allowed for nargs='{self.nargs}'." ) if isinstance(self.nargs, int) and not (self.nargs == len(self.default)): raise ParameterError( f"Parameter '{self.name:s}': " - + f"Default value has wrong length (={len(self.default):d}) " + f"Default value has wrong length (={len(self.default):d}) " f"for given nargs={self.nargs:d}." ) @@ -489,7 +489,7 @@ def _validate(self): if len(not_a_choice) > 0: raise ParameterError( f"Parameter '{self.name:s}': " - + f"Default value(s) '{str(not_a_choice)}'" + f"Default value(s) '{str(not_a_choice)}'" " not found in choices." ) else: @@ -509,12 +509,12 @@ def _validate(self): if not isinstance(choice, check): raise ParameterError( f"Choice '{choice}' " - + f"of parameter '{self.name:s}': " - + f"is not of type '{check.__name__:s}'." + f"of parameter '{self.name:s}': " + f"is not of type '{check.__name__:s}'." ) if self.required and self.default is not None: LOG.warning( f"Parameter '{self.name:s}': " - + "Value is required but default value is given. The latter will be ignored." + "Value is required but default value is given. The latter will be ignored." ) diff --git a/generic_parser/entry_datatypes.py b/generic_parser/entry_datatypes.py index 88c6ffb..99e30f8 100644 --- a/generic_parser/entry_datatypes.py +++ b/generic_parser/entry_datatypes.py @@ -88,11 +88,10 @@ def __new__(cls, value): if value in TRUE_ITEMS: return True - elif value in FALSE_ITEMS: + if value in FALSE_ITEMS: return False - else: - return str(value) + return str(value) class BoolOrList(metaclass=get_instance_faker_meta(bool, list)): @@ -105,9 +104,8 @@ def __new__(cls, value): if value in TRUE_ITEMS: return True - elif value in FALSE_ITEMS: + if value in FALSE_ITEMS: return False - else: - if isinstance(value, str): - value = eval(value) - return list(value) + if isinstance(value, str): + value = eval(value) + return list(value) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index 55ab77b..d54e0a8 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -161,22 +161,21 @@ def entry_function(opt): python script.py --entry_cfg config.ini --section section """ +import argparse import copy import json -import argparse +import logging import sys from argparse import ArgumentParser +from collections.abc import Callable, Mapping from configparser import ConfigParser -from inspect import getfullargspec from functools import wraps +from inspect import getfullargspec from pathlib import Path from textwrap import wrap -from typing import Callable, Mapping -from generic_parser.tools import DotDict, silence, unformatted_console_logging, StringIO, log_out -from generic_parser.dict_parser import ParameterError, ArgumentError, DictParser - -import logging +from generic_parser.dict_parser import ArgumentError, DictParser, ParameterError +from generic_parser.tools import DotDict, StringIO, log_out, silence, unformatted_console_logging LOG = logging.getLogger(__name__) @@ -260,13 +259,13 @@ def _create_config_argument(self): """Creates the config-file argument parser.""" parser = ArgumentParser() parser.add_argument( - "--{}".format(ID_CONFIG), + f"--{ID_CONFIG}", type=str, dest=ID_CONFIG, required=True, ) parser.add_argument( - "--{}".format(ID_SECTION), + f"--{ID_SECTION}", type=str, dest=ID_SECTION, ) @@ -331,10 +330,9 @@ def _handle_commandline(self, args=None): if unknown_opts: raise ArgumentError(f"Unknown options: {unknown_opts}") return options - else: - if unknown_opts: - LOG.debug(f"Unknown options: {unknown_opts}") - return options, unknown_opts + if unknown_opts: + LOG.debug(f"Unknown options: {unknown_opts}") + return options, unknown_opts else: # parse config file return self.dictparse.parse_config_items(self._read_config(vars(options)[ID_CONFIG])) @@ -362,7 +360,7 @@ def _handle_kwargs(self, kwargs): if len(kwargs) > 2 or (len(kwargs) == 2 and ID_SECTION not in kwargs): raise ArgumentError( f"Only '{ID_CONFIG:s}' and '{ID_SECTION:s}'" - + " arguments are allowed, when using a config file." + " arguments are allowed, when using a config file." ) options = self._read_config(kwargs[ID_CONFIG], kwargs.get(ID_SECTION, None)) options = self.dictparse.parse_config_items(options) @@ -376,9 +374,9 @@ def _handle_kwargs(self, kwargs): if len(kwargs) > 2 or (len(kwargs) == 2 and ID_SECTION not in kwargs): raise ArgumentError( f"Only '{ID_JSON:s}' and '{ID_SECTION:s}'" - + " arguments are allowed, when using a json file." + " arguments are allowed, when using a json file." ) - with open(kwargs[ID_JSON], "r") as json_file: + with open(kwargs[ID_JSON]) as json_file: json_dict = json.load(json_file) if ID_SECTION in kwargs: @@ -405,13 +403,13 @@ def _check_parameter(self): if param.get("nargs", None) == argparse.REMAINDER: raise ParameterError( f"Parameter '{arg_name:s}' is set as remainder." - + "This method is really buggy, hence it is forbidden." + "This method is really buggy, hence it is forbidden." ) if param.get("nargs", None) == argparse.OPTIONAL: raise ParameterError( f"Parameter '{arg_name:s}' is set as optional." - + "As entrypoint does not use 'const', the use is prohibited." + "As entrypoint does not use 'const', the use is prohibited." ) if param.get("flags", None) is None: @@ -523,8 +521,7 @@ def add_parameter(self, **kwargs): name = kwargs.pop("name") if name in self: raise ParameterError(f"'{name:s}' is already a parameter.") - else: - self[name] = kwargs + self[name] = kwargs def help(self): """Prints current help. Usable to paste into docstrings.""" diff --git a/generic_parser/tools.py b/generic_parser/tools.py index 5f6a843..da65ffb 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -67,7 +67,7 @@ def print_tree(tree, level_char): else: print_fun(f"{level_char:s}{node_char:s} {str(key):s}: {str(tree[key]):s}") - print_fun("{:s}:".format(name)) + print_fun(f"{name:s}:") print_tree(dictionary, "") diff --git a/tests/conftest.py b/tests/conftest.py index 407120d..ac3ad9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,12 +6,8 @@ See also https://stackoverflow.com/a/34520971 . """ -import shutil import sys from contextlib import contextmanager -from pathlib import Path - -import pytest @contextmanager diff --git a/tests/test_dict_parser.py b/tests/test_dict_parser.py index 3dfc4c4..3067278 100644 --- a/tests/test_dict_parser.py +++ b/tests/test_dict_parser.py @@ -1,10 +1,9 @@ -from io import StringIO -import pytest import sys -import logging -from generic_parser.dict_parser import ParameterError, Parameter, DictParser, ArgumentError +import pytest + +from generic_parser.dict_parser import ArgumentError, DictParser, Parameter, ParameterError from generic_parser.tools import TempStringLogger diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index efb166e..ea024f4 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -3,21 +3,20 @@ from pathlib import Path import pytest -from tests.conftest import cli_args -from generic_parser.dict_parser import ParameterError, ArgumentError -from generic_parser.entry_datatypes import get_multi_class, DictAsString, BoolOrString, BoolOrList +from generic_parser.dict_parser import ArgumentError, ParameterError +from generic_parser.entry_datatypes import BoolOrList, BoolOrString, DictAsString, get_multi_class from generic_parser.entrypoint_parser import ( - EntryPointParameters, - entrypoint, EntryPoint, + EntryPointParameters, OptionsError, - split_arguments, create_parameter_help, + entrypoint, save_options_to_config, + split_arguments, ) -from generic_parser.tools import silence, print_dict_tree, TempStringLogger - +from generic_parser.tools import TempStringLogger, print_dict_tree, silence +from tests.conftest import cli_args LOG = logging.getLogger(__name__) DEBUG = False @@ -40,7 +39,7 @@ def strict(opt, unknown): # too many option-structures def test_class_wrapper_fail(): with pytest.raises(OptionsError): - class MyClass(object): + class MyClass: @entrypoint(get_simple_params()) def fun(self, opt): # too few option-structures pass @@ -55,7 +54,7 @@ def fun(opt, unknown, more): # too many option-structures def test_class_functions(): - class MyClass(object): + class MyClass: @classmethod @entrypoint(get_simple_params()) def fun(cls, opt, unknown): @@ -63,7 +62,7 @@ def fun(cls, opt, unknown): def test_instance_functions(): - class MyClass(object): + class MyClass: @entrypoint(get_simple_params()) def fun(self, opt, unknown): pass @@ -311,7 +310,7 @@ def test_save_cli_options_cfg(tmp_path): save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) - with open(cfg_file, "r") as f: + with open(cfg_file) as f: content = f.read() assert "Unknown" in content assert "--other" in content @@ -728,7 +727,7 @@ def get_other_params(): def some_function(options, unknown_options): LOG.debug("Some Function") print_dict_tree(options, print_fun=LOG.debug) - LOG.debug("Unknown Options: \n {:s}".format(str(unknown_options))) + LOG.debug(f"Unknown Options: \n {str(unknown_options):s}") LOG.debug("\n") diff --git a/tests/test_tools.py b/tests/test_tools.py index fd9cff8..1bcf1ca 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,4 +1,5 @@ from io import StringIO + import pytest from generic_parser.tools import DotDict, print_dict_tree From 5e6c63620671ab614341ddd77df28ad3f43569fe Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:17:27 +0200 Subject: [PATCH 04/25] use dict literals --- generic_parser/dict_parser.py | 18 +++---- generic_parser/entrypoint_parser.py | 8 +-- tests/test_dict_parser.py | 3 +- tests/test_entrypoint.py | 78 ++++++++++++++--------------- tests/test_tools.py | 2 +- 5 files changed, 53 insertions(+), 56 deletions(-) diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index 207632b..87e88d2 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -436,8 +436,7 @@ def _validate(self): if self.subtype and not (self.type or self.type == list): raise ParameterError( - f"Parameter '{self.name:s}': " - "field 'subtype' is only accepted if 'type' is list." + f"Parameter '{self.name:s}': field 'subtype' is only accepted if 'type' is list." ) if self.nargs: @@ -454,8 +453,7 @@ def _validate(self): if not (self.type or self.type == list): raise ParameterError( - f"Parameter '{self.name:s}': " - "'type' needs to be 'list' if 'nargs' is given." + f"Parameter '{self.name:s}': 'type' needs to be 'list' if 'nargs' is given." ) if ( @@ -464,13 +462,13 @@ def _validate(self): if (self.nargs == argparse.ONE_OR_MORE) and not len(self.default): raise ParameterError( f"Parameter '{self.name:s}': " - f"Empty list as default not allowed for nargs='{self.nargs}'." + f"Empty list as default not allowed for nargs='{self.nargs}'." ) if isinstance(self.nargs, int) and not (self.nargs == len(self.default)): raise ParameterError( f"Parameter '{self.name:s}': " - f"Default value has wrong length (={len(self.default):d}) " + f"Default value has wrong length (={len(self.default):d}) " f"for given nargs={self.nargs:d}." ) @@ -489,7 +487,7 @@ def _validate(self): if len(not_a_choice) > 0: raise ParameterError( f"Parameter '{self.name:s}': " - f"Default value(s) '{str(not_a_choice)}'" + f"Default value(s) '{str(not_a_choice)}'" " not found in choices." ) else: @@ -509,12 +507,12 @@ def _validate(self): if not isinstance(choice, check): raise ParameterError( f"Choice '{choice}' " - f"of parameter '{self.name:s}': " - f"is not of type '{check.__name__:s}'." + f"of parameter '{self.name:s}': " + f"is not of type '{check.__name__:s}'." ) if self.required and self.default is not None: LOG.warning( f"Parameter '{self.name:s}': " - "Value is required but default value is given. The latter will be ignored." + "Value is required but default value is given. The latter will be ignored." ) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index d54e0a8..cc85202 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -360,7 +360,7 @@ def _handle_kwargs(self, kwargs): if len(kwargs) > 2 or (len(kwargs) == 2 and ID_SECTION not in kwargs): raise ArgumentError( f"Only '{ID_CONFIG:s}' and '{ID_SECTION:s}'" - " arguments are allowed, when using a config file." + " arguments are allowed, when using a config file." ) options = self._read_config(kwargs[ID_CONFIG], kwargs.get(ID_SECTION, None)) options = self.dictparse.parse_config_items(options) @@ -374,7 +374,7 @@ def _handle_kwargs(self, kwargs): if len(kwargs) > 2 or (len(kwargs) == 2 and ID_SECTION not in kwargs): raise ArgumentError( f"Only '{ID_JSON:s}' and '{ID_SECTION:s}'" - " arguments are allowed, when using a json file." + " arguments are allowed, when using a json file." ) with open(kwargs[ID_JSON]) as json_file: json_dict = json.load(json_file) @@ -403,13 +403,13 @@ def _check_parameter(self): if param.get("nargs", None) == argparse.REMAINDER: raise ParameterError( f"Parameter '{arg_name:s}' is set as remainder." - "This method is really buggy, hence it is forbidden." + "This method is really buggy, hence it is forbidden." ) if param.get("nargs", None) == argparse.OPTIONAL: raise ParameterError( f"Parameter '{arg_name:s}' is set as optional." - "As entrypoint does not use 'const', the use is prohibited." + "As entrypoint does not use 'const', the use is prohibited." ) if param.get("flags", None) is None: diff --git a/tests/test_dict_parser.py b/tests/test_dict_parser.py index 3067278..d3753d2 100644 --- a/tests/test_dict_parser.py +++ b/tests/test_dict_parser.py @@ -1,4 +1,3 @@ - import sys import pytest @@ -111,7 +110,7 @@ def test_name_not_string(): Parameter(name=5) with pytest.raises(ParameterError): - DictParser({5: dict()}) + DictParser({5: {}}) def test_name_not_key(): diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index ea024f4..2789e42 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -245,7 +245,7 @@ def test_not_enough_length(): def test_optional_parameter_no_default_accepts_none(): - @entrypoint(dict(foo=dict(required=False)), strict=True) + @entrypoint({"foo": {"required": False}}, strict=True) def fun(opt): return opt @@ -254,7 +254,7 @@ def fun(opt): def test_optional_list_parameter_no_default_accepts_none(): - @entrypoint(dict(foo=dict(required=False, nargs=3)), strict=True) + @entrypoint({"foo": {"required": False, "nargs": 3}}, strict=True) def fun(opt): return opt @@ -263,7 +263,7 @@ def fun(opt): def test_optional_parameter_default_accepts_none(): - @entrypoint(dict(foo=dict(required=False, default="test")), strict=True) + @entrypoint({"foo": {"required": False, "default": "test"}}, strict=True) def fun(opt): return opt @@ -275,7 +275,7 @@ def fun(opt): def test_required_parameter_does_not_accept_none(): - @entrypoint(dict(foo=dict(required=True)), strict=True) + @entrypoint({"foo": {"required": True}}, strict=True) def fun(opt): return opt @@ -348,7 +348,7 @@ def test_save_and_load_cfg_with_none_explicit(tmp_path): def test_string_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(name={"type": str})), strict=True) + @entrypoint(EntryPointParameters({"name": {"type": str}}), strict=True) def fun(opt): return opt @@ -373,7 +373,7 @@ def fun(opt): def test_string_with_break_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(name={"type": str})), strict=True) + @entrypoint(EntryPointParameters({"name": {"type": str}}), strict=True) def fun(opt): return opt @@ -387,7 +387,7 @@ def fun(opt): def test_path_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(path={"type": Path})), strict=True) + @entrypoint(EntryPointParameters({"path": {"type": Path}}), strict=True) def fun(opt): return opt @@ -401,7 +401,7 @@ def fun(opt): def test_list_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(lst={"type": int, "nargs": "*"})), strict=True) + @entrypoint(EntryPointParameters({"lst": {"type": int, "nargs": "*"}}), strict=True) def fun(opt): return opt @@ -469,7 +469,7 @@ def test_bool_or_list_class(): def test_multiclass(): IntOrStr = get_multi_class(int, str) - @entrypoint([dict(flags="--ios", name="ios", type=IntOrStr)], strict=True) + @entrypoint([{"flags": "--ios", "name": "ios", "type": IntOrStr}], strict=True) def fun(opt): return opt @@ -487,7 +487,7 @@ def fun(opt): def test_dict_as_string(): - @entrypoint([dict(flags="--dict", name="dict", type=DictAsString)], strict=True) + @entrypoint([{"flags": "--dict", "name": "dict", "type": DictAsString}], strict=True) def fun(opt): return opt @@ -501,7 +501,7 @@ def fun(opt): def test_bool_or_str(tmp_path): - @entrypoint([dict(flags="--bos", name="bos", type=BoolOrString)], strict=True) + @entrypoint([{"flags": "--bos", "name": "bos", "type": BoolOrString}], strict=True) def fun(opt): return opt @@ -530,8 +530,8 @@ def fun(opt): def test_bool_or_str_cfg(tmp_path): @entrypoint( [ - dict(flags="--bos1", name="bos1", type=BoolOrString), - dict(flags="--bos2", name="bos2", type=BoolOrString), + {"flags": "--bos1", "name": "bos1", "type": BoolOrString}, + {"flags": "--bos2", "name": "bos2", "type": BoolOrString}, ], strict=True, ) @@ -547,7 +547,7 @@ def fun(opt): def test_bool_or_list(): - @entrypoint([dict(flags="--bol", name="bol", type=BoolOrList)], strict=True) + @entrypoint([{"flags": "--bol", "name": "bol", "type": BoolOrList}], strict=True) def fun(opt): return opt @@ -570,8 +570,8 @@ def fun(opt): def test_bool_or_list_cfg(tmp_path): @entrypoint( [ - dict(flags="--bol1", name="bol1", type=BoolOrList), - dict(flags="--bol2", name="bol2", type=BoolOrList), + {"flags": "--bol1", "name": "bol1", "type": BoolOrList}, + {"flags": "--bol2", "name": "bol2", "type": BoolOrList}, ], strict=True, ) @@ -647,9 +647,9 @@ def get_simple_params(): def get_testing_params(): """Parameters as a dict of dicts, to test this behaviour as well.""" return { - "name": dict(flags="--name", type=str), - "int": dict(flags="--int", type=int), - "list": dict(flags="--list", type=int, nargs="+"), + "name": {"flags": "--name", "type": str}, + "int": {"flags": "--int", "type": int}, + "list": {"flags": "--list", "type": int, "nargs": "+"}, } @@ -695,26 +695,26 @@ def get_other_params(): """For testing the create_param_help()""" args = EntryPointParameters( { - "arg1": dict( - flags="--arg1", - help="A help.", - default=1, - ), - "arg2": dict( - flags="--arg2", - help="More help.", - default=2, - ), - "arg3": dict( - flags="--arg3", - help="Even more...", - default=3, - ), - "arg4": dict( - flags="--arg4", - help="...heeeeeeeeelp.", - default=4, - ), + "arg1": { + "flags": "--arg1", + "help": "A help.", + "default": 1, + }, + "arg2": { + "flags": "--arg2", + "help": "More help.", + "default": 2, + }, + "arg3": { + "flags": "--arg3", + "help": "Even more...", + "default": 3, + }, + "arg4": { + "flags": "--arg4", + "help": "...heeeeeeeeelp.", + "default": 4, + }, } ) return args diff --git a/tests/test_tools.py b/tests/test_tools.py index 1bcf1ca..b72a5e6 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -43,4 +43,4 @@ def print_with_blank_line(s): @pytest.fixture() def simple_dict(): - return dict(a=1, b="str", c=dict(e=[1, 2, 3])) + return {"a": 1, "b": "str", "c": {"e": [1, 2, 3]}} From 4a1980e492cd210aaf3feb6f9de71232247d95f6 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:18:28 +0200 Subject: [PATCH 05/25] no need for .key to iterate dict --- tests/test_entrypoint.py | 4 ++-- tests/test_tools.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 2789e42..f7b7b7f 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -613,7 +613,7 @@ def test_create_param_help(): with TempStringLogger(entrypoint_module) as log: create_parameter_help(this_module) text = log.get_log() - for name in get_params().keys(): + for name in get_params(): assert name in text @@ -623,7 +623,7 @@ def test_create_param_help_other(): with TempStringLogger(entrypoint_module) as log: create_parameter_help(this_module, "get_other_params") text = log.get_log() - for name in get_other_params().keys(): + for name in get_other_params(): assert name in text diff --git a/tests/test_tools.py b/tests/test_tools.py index b72a5e6..54d1acd 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -34,8 +34,8 @@ def print_with_blank_line(s): # print(text) # I'll leave it here, in case you want to see the dict assert name in text - for l in simple_dict.keys(): - assert l in text + for line in simple_dict: + assert line in text # Fixtures ##################################################################### From d7bc113f00a3930dc368bbe9569c765c7c498761 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:19:35 +0200 Subject: [PATCH 06/25] do not assign to variables that will be unused --- generic_parser/entrypoint_parser.py | 12 ++++-------- tests/test_entrypoint.py | 3 +-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index cc85202..0037f3f 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -279,19 +279,16 @@ def _create_argument_parser(self, args_dict: Mapping): else: parser = ArgumentParser() - parser = _add_params_to_argument_parser(parser, self.parameter) - return parser + return _add_params_to_argument_parser(parser, self.parameter) def _create_dict_parser(self): """Creates the DictParser from parameter.""" parser = DictParser(strict=self.strict) - parser = _add_params_to_dict_parser(parser, self.parameter) - return parser + return _add_params_to_dict_parser(parser, self.parameter) def _create_config_parser(self): """Creates the config parser. Maybe more to do here later with parameter.""" - parser = ConfigParser({}) - return parser + return ConfigParser({}) ######################### # Handlers @@ -436,8 +433,7 @@ def _read_config(self, cfgfile_path, section=None): f"'{cfgfile_path:s}' contains multiple sections. " + " Please specify one!" ) - items = cfgparse.items(section) - return items + return cfgparse.items(section) # entrypoint Decorator ######################################################### diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index f7b7b7f..76eab5f 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -693,7 +693,7 @@ def get_params(): def get_other_params(): """For testing the create_param_help()""" - args = EntryPointParameters( + return EntryPointParameters( { "arg1": { "flags": "--arg1", @@ -717,7 +717,6 @@ def get_other_params(): }, } ) - return args # Example Wrapped Functions ---------------------------------------------------- From 37fb869417dc3999f62dc77b6ee7a8ca8ca0915a Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:21:38 +0200 Subject: [PATCH 07/25] no positional-only boolean arguments --- tests/test_entrypoint.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 76eab5f..112a3d2 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -441,11 +441,11 @@ def test_dict_as_string_class(): def test_bool_or_str_class(): - assert isinstance(True, BoolOrString) + assert isinstance(True, BoolOrString) # noqa FBT003 (isinstance takes no kwargs) assert isinstance("myString", BoolOrString) assert BoolOrString("True") is True assert BoolOrString("1") is True - assert BoolOrString(True) is True + assert BoolOrString(value=True) is True assert BoolOrString(1) is True assert BoolOrString("myString") == "myString" assert issubclass(bool, BoolOrString) @@ -454,11 +454,11 @@ def test_bool_or_str_class(): def test_bool_or_list_class(): - assert isinstance(True, BoolOrList) + assert isinstance(True, BoolOrList) # noqa FBT003 (isinstance takes no kwargs) assert isinstance([], BoolOrList) assert BoolOrList("False") is False assert BoolOrList("0") is False - assert BoolOrList(False) is False + assert BoolOrList(value=False) is False assert BoolOrList(0) is False assert BoolOrList("[1, 2]") == [1, 2] assert issubclass(bool, BoolOrList) From 5be677a5a3dec138c075ed0a0e04f404d4ab85d5 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:22:29 +0200 Subject: [PATCH 08/25] unecessary comprehension --- generic_parser/dict_parser.py | 2 +- generic_parser/entry_datatypes.py | 2 +- tests/test_entrypoint.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index 87e88d2..2f13bc6 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -137,7 +137,7 @@ def _check_value(key, arg_dict, param_dict): f"Help: {param.help:s}" ) - if param.choices and any([o for o in opt if o not in param.choices]): + if param.choices and any(o for o in opt if o not in param.choices): raise ArgumentError( f"All elements of '{key:s}' need to be one of " f"'{param.choices}', instead the list was {opt}.\n" diff --git a/generic_parser/entry_datatypes.py b/generic_parser/entry_datatypes.py index 99e30f8..d11684c 100644 --- a/generic_parser/entry_datatypes.py +++ b/generic_parser/entry_datatypes.py @@ -23,7 +23,7 @@ def __instancecheck__(cls, inst): return isinstance(inst, classes) def __subclasscheck__(self, subclass): - return any([issubclass(c, subclass) for c in classes]) + return any(issubclass(c, subclass) for c in classes) return FakeMeta diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 112a3d2..b2c310d 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -331,7 +331,7 @@ def test_save_and_cfg_load_with_none(tmp_path): _assert_dicts_equal(opt, opt_load) assert len(unknown) == 0 assert len(unknown_load) == 0 - assert all([val is None for val in opt.values()]) + assert all(val is None for val in opt.values()) def test_save_and_load_cfg_with_none_explicit(tmp_path): @@ -344,7 +344,7 @@ def test_save_and_load_cfg_with_none_explicit(tmp_path): _assert_dicts_equal(opt, opt_load) assert len(unknown) == 0 assert len(unknown_load) == 0 - assert all([val is None for val in opt.values()]) + assert all(val is None for val in opt.values()) def test_string_cfg(tmp_path): From bd2dc20f006ea359a33c79a36a5aefcd9d268605 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:25:07 +0200 Subject: [PATCH 09/25] do not do exact comparison for types, use is --- generic_parser/dict_parser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index 2f13bc6..69e5003 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -112,7 +112,7 @@ def _check_value(key, arg_dict, param_dict): f"'{key:s}' is not of type {param.type.__name__:s}.\nHelp: {param.help:s}" ) - if param.type == list: + if param.type is list: if param.nargs: if isinstance(param.nargs, int) and not param.nargs == len(opt): raise ArgumentError( @@ -379,7 +379,7 @@ def eval_type(my_type, item): elif name in self.dictionary: arg = self.dictionary[name] - if arg.type == list: + if arg.type is list: value = evaluate(name, value) if arg.subtype: for idx, entry in enumerate(value): @@ -434,7 +434,7 @@ def _validate(self): f"Parameter '{self.name:s}': " + "Default value not of specified type." ) - if self.subtype and not (self.type or self.type == list): + if self.subtype and not (self.type or self.type is list): raise ParameterError( f"Parameter '{self.name:s}': field 'subtype' is only accepted if 'type' is list." ) @@ -451,7 +451,7 @@ def _validate(self): f"Instead it was '{self.nargs}'" ) - if not (self.type or self.type == list): + if not (self.type or self.type is list): raise ParameterError( f"Parameter '{self.name:s}': 'type' needs to be 'list' if 'nargs' is given." ) @@ -481,7 +481,7 @@ def _validate(self): ) if self.default: - if self.type == list: + if self.type is list: not_a_choice = [d for d in self.default if d not in self.choices] if len(not_a_choice) > 0: From 6663f47de6c408ce77aef33fe5a006115fb10273 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:26:02 +0200 Subject: [PATCH 10/25] combine with statements --- tests/test_entrypoint.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index b2c310d..21842fb 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -214,9 +214,8 @@ def test_as_config(tmp_path): def test_all_missing(): - with pytest.raises(SystemExit): - with silence(): - some_function() + with pytest.raises(SystemExit), silence(): + some_function() def test_required_missing(): From a74dd8fd8aa7940790ea19c52017ea055e08fc88 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:26:41 +0200 Subject: [PATCH 11/25] noqa this one --- tests/test_entrypoint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 21842fb..a46fd4a 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -466,7 +466,7 @@ def test_bool_or_list_class(): def test_multiclass(): - IntOrStr = get_multi_class(int, str) + IntOrStr = get_multi_class(int, str) # noqa N806 (this is a class) @entrypoint([{"flags": "--ios", "name": "ios", "type": IntOrStr}], strict=True) def fun(opt): From 990e836101d56bac68cf2cc15553af0697b16c1d Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:28:19 +0200 Subject: [PATCH 12/25] open devnull with context manager, do not close finally --- generic_parser/tools.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/generic_parser/tools.py b/generic_parser/tools.py index da65ffb..6373a58 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -111,12 +111,10 @@ def silence(): """ Suppress all console output. ``sys.stdout`` and ``sys.stderr`` are rerouted to ``devnull``. """ - devnull = open(os.devnull, "w") - with log_out(stdout=devnull, stderr=devnull): - try: + with open(os.devnull, "w") as devnull: + with log_out(stdout=devnull, stderr=devnull): yield - finally: - devnull.close() + @contextmanager From 354c43ed956afc76b9a9cdc31e62c9117219b108 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:29:15 +0200 Subject: [PATCH 13/25] combine with statements --- generic_parser/tools.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/generic_parser/tools.py b/generic_parser/tools.py index 6373a58..bd9165c 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -111,10 +111,8 @@ def silence(): """ Suppress all console output. ``sys.stdout`` and ``sys.stderr`` are rerouted to ``devnull``. """ - with open(os.devnull, "w") as devnull: - with log_out(stdout=devnull, stderr=devnull): - yield - + with open(os.devnull, "w") as devnull, log_out(stdout=devnull, stderr=devnull): + yield @contextmanager From 39499d9ce6d5f8fdf138f7a39ff758323d4ee41b Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:29:47 +0200 Subject: [PATCH 14/25] no ambiguous variable name --- tests/test_dict_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_dict_parser.py b/tests/test_dict_parser.py index d3753d2..a8dddcc 100644 --- a/tests/test_dict_parser.py +++ b/tests/test_dict_parser.py @@ -129,7 +129,7 @@ def test_print_tree(): parser.tree() text = log.get_log() - for l in loc.split("."): - assert l in text + for line in loc.split("."): + assert line in text for attr in ["name", "default", "required", "choices", "help"]: assert str(getattr(param, attr)) in text From b82e2346b7a596ad78b2ab3c5b34df45fee95cc5 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:30:26 +0200 Subject: [PATCH 15/25] no need to call class and self in super() call --- generic_parser/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generic_parser/tools.py b/generic_parser/tools.py index bd9165c..308d29b 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -28,7 +28,7 @@ class DotDict(dict): """Make dict fields accessible by dot notation.""" def __init__(self, *args, **kwargs): - super(DotDict, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) for key in self: if isinstance(self[key], dict): self[key] = DotDict(self[key]) @@ -40,7 +40,7 @@ def __init__(self, *args, **kwargs): def __getattr__(self, key): """Needed to raise the correct exceptions.""" try: - return super(DotDict, self).__getitem__(key) + return super().__getitem__(key) except KeyError as e: raise AttributeError(e).with_traceback(e.__traceback__) from e From d8d230324a7d673f92deb4fa52b1c4e7c8a504b6 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:32:54 +0200 Subject: [PATCH 16/25] noqa this one --- generic_parser/entrypoint_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index 0037f3f..0943940 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -439,7 +439,7 @@ def _read_config(self, cfgfile_path, section=None): # entrypoint Decorator ######################################################### -class entrypoint(EntryPoint): +class entrypoint(EntryPoint): # noqa N801 """ Decorator extension of `EntryPoint`. Implements the ``__call__`` method needed for decorating. Lowercase looks nicer if used as decorator. From cb3917cc23e885ce489a56446130eb71350996fa Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:33:34 +0200 Subject: [PATCH 17/25] unecessary comprehension and use of | --- generic_parser/dict_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index 69e5003..522f146 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -364,7 +364,7 @@ def evaluate(name, item): raise ArgumentError(f"Could not evaluate argument '{name:s}', unknown '{item:s}'") def eval_type(my_type, item): - if issubclass(my_type, (str, Path)): + if issubclass(my_type, (str | Path)): return my_type(item.strip("'\"")) if issubclass(my_type, bool): @@ -474,7 +474,7 @@ def _validate(self): if self.choices: try: - [choice for choice in self.choices] + list(choice for choice in self.choices) except TypeError: raise ParameterError( f"Parameter '{self.name:s}': " + "'Choices' need to be iterable." From a6e88c1fec267965cb4bec288deb65af89280ef3 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:34:10 +0200 Subject: [PATCH 18/25] dont use not, unequal comparison directly --- generic_parser/entrypoint_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index 0943940..660c8bd 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -674,7 +674,7 @@ def _add_params_to_dict_parser(dict_parser, params): if "action" in param: if param["action"] in ("store_true", "store_false"): param["type"] = bool - param["default"] = not param["action"][6] == "t" + param["default"] = param["action"][6] != "t" else: raise ParameterError(f"Action '{param['action']:s}' not allowed in EntryPoint") param.pop("action") From e8527fbd0910190bfaa0f3d245f3c5b36aedc574 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:34:31 +0200 Subject: [PATCH 19/25] prefer ternary operator to if-else checks --- generic_parser/entrypoint_parser.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index 660c8bd..3f19317 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -274,11 +274,7 @@ def _create_config_argument(self): def _create_argument_parser(self, args_dict: Mapping): """Creates the ArgumentParser from parameter.""" - if args_dict: - parser = ArgumentParser(**args_dict) - else: - parser = ArgumentParser() - + parser = ArgumentParser(**args_dict) if args_dict else ArgumentParser() return _add_params_to_argument_parser(parser, self.parameter) def _create_dict_parser(self): From c9078cff2976315c62efc4fef1d6d63a2011e789 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:35:01 +0200 Subject: [PATCH 20/25] first arg of classmethod is cls --- generic_parser/entry_datatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_parser/entry_datatypes.py b/generic_parser/entry_datatypes.py index d11684c..4c09a63 100644 --- a/generic_parser/entry_datatypes.py +++ b/generic_parser/entry_datatypes.py @@ -22,7 +22,7 @@ class FakeMeta(abc.ABCMeta): def __instancecheck__(cls, inst): return isinstance(inst, classes) - def __subclasscheck__(self, subclass): + def __subclasscheck__(cls, subclass): return any(issubclass(c, subclass) for c in classes) return FakeMeta From e51ce3681045ccf1bdd6b182bbd96691ed15d0cc Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:35:27 +0200 Subject: [PATCH 21/25] unecessary generator --- generic_parser/dict_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_parser/dict_parser.py b/generic_parser/dict_parser.py index 522f146..3b4b8f5 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -474,7 +474,7 @@ def _validate(self): if self.choices: try: - list(choice for choice in self.choices) + list(self.choices) except TypeError: raise ParameterError( f"Parameter '{self.name:s}': " + "'Choices' need to be iterable." From 49fad6c729e14974cc5582880fbf119f0c2b34a1 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:35:56 +0200 Subject: [PATCH 22/25] use | --- generic_parser/entrypoint_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index 3f19317..453da3e 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -779,7 +779,7 @@ def save_options_to_config(filepath, opt, unknown=None): def _to_key_value_str(key, value): if value is None: value = "" # defined as empty (see dict_parser._convert_config_items) - elif isinstance(value, (str, Path)): + elif isinstance(value, (str | Path)): value = f'"{value}"' if "\n" in value: value = value.replace("\n", "\n ") # spaces in new line indicate continuation From dceab52c264caaa9afd6acb679a2b790ecc1dd14 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:38:52 +0200 Subject: [PATCH 23/25] give language, replace deprecated option, remove unnecessary line --- doc/conf.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 2d6de2a..e9de913 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -15,7 +15,6 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os import pathlib import sys @@ -27,9 +26,6 @@ warnings.filterwarnings("ignore", message="numpy.ufunc size changed") -sys.path.insert(0, os.path.abspath("../")) - - TOPLEVEL_DIR = pathlib.Path(__file__).parent.parent.absolute() ABOUT_FILE = TOPLEVEL_DIR / "generic_parser" / "__init__.py" @@ -103,7 +99,7 @@ def about_package(init_posixpath: pathlib.Path) -> dict: # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -126,7 +122,7 @@ def about_package(init_posixpath: pathlib.Path) -> dict: html_theme_options = { "collapse_navigation": False, - "display_version": True, + "version_selector": True, # sphinx-rtd-theme>=3.0, formerly 'display_version' "logo_only": True, "navigation_depth": 2, } From f41437a798270392ec00f750f64fe4e3dbe3ca9c Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Fri, 11 Jul 2025 10:45:57 +0200 Subject: [PATCH 24/25] use pathlib operations where possible --- generic_parser/entrypoint_parser.py | 8 +++--- generic_parser/tools.py | 3 ++- tests/test_entrypoint.py | 42 ++++++++++++----------------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/generic_parser/entrypoint_parser.py b/generic_parser/entrypoint_parser.py index 453da3e..b69614e 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -369,9 +369,8 @@ def _handle_kwargs(self, kwargs): f"Only '{ID_JSON:s}' and '{ID_SECTION:s}'" " arguments are allowed, when using a json file." ) - with open(kwargs[ID_JSON]) as json_file: - json_dict = json.load(json_file) + json_dict = json.loads(Path(kwargs[ID_JSON]).read_text()) if ID_SECTION in kwargs: json_dict = json_dict[kwargs[ID_SECTION]] @@ -415,7 +414,7 @@ def _read_config(self, cfgfile_path, section=None): # create new config parser, as it keeps defaults between files cfgparse = self._create_config_parser() - with open(cfgfile_path) as config_file: + with Path(cfgfile_path).open() as config_file: cfgparse.read_file(config_file) sections = cfgparse.sections() @@ -797,5 +796,4 @@ def _to_key_value_str(key, value): else: lines += f"; {' '.join(unknown)}\n" - with open(filepath, "w") as f: - f.write(lines) + Path(filepath).write_text(lines) diff --git a/generic_parser/tools.py b/generic_parser/tools.py index 308d29b..0e28281 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -10,6 +10,7 @@ import sys from contextlib import contextmanager from io import StringIO +from pathlib import Path LOG = logging.getLogger(__name__) @@ -111,7 +112,7 @@ def silence(): """ Suppress all console output. ``sys.stdout`` and ``sys.stderr`` are rerouted to ``devnull``. """ - with open(os.devnull, "w") as devnull, log_out(stdout=devnull, stderr=devnull): + with Path(os.devnull).open("w") as devnull, log_out(stdout=devnull, stderr=devnull): yield diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index a46fd4a..820876a 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -181,18 +181,17 @@ def test_as_argv(): # almost identical to above def test_as_config(tmp_path): cfg_file = tmp_path / "config.ini" - with open(cfg_file, "w") as f: - f.write( - "\n".join( - [ - "[Section]", - "name = 'myname'", - "int = 3", - "list = [4, 5, 6]", - "unknown = 'other'", - ] - ) + Path(cfg_file).write_text( + "\n".join( + [ + "[Section]", + "name = 'myname'", + "int = 3", + "list = [4, 5, 6]", + "unknown = 'other'", + ] ) + ) # test config as kwarg opt1, unknown1 = paramtest_function(entry_cfg=cfg_file, section="Section") @@ -309,8 +308,7 @@ def test_save_cli_options_cfg(tmp_path): save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) - with open(cfg_file) as f: - content = f.read() + content = Path(cfg_file).read_text() assert "Unknown" in content assert "--other" in content @@ -352,16 +350,13 @@ def fun(opt): return opt cfg_quotes = tmp_path / "config_quotes.ini" - with open(cfg_quotes, "w") as f: - f.write("[Section]\nname = 'My String with Spaces'") + Path(cfg_quotes).write_text("[Section]\nname = 'My String with Spaces'") cfg_doublequotes = tmp_path / "config_doublequotes.ini" - with open(cfg_doublequotes, "w") as f: - f.write('[Section]\nname = "My String with Spaces"') + Path(cfg_doublequotes).write_text('[Section]\nname = "My String with Spaces"') cfg_noquotes = tmp_path / "config_noquotes.ini" - with open(cfg_noquotes, "w") as f: - f.write("[Section]\nname = My String with Spaces") + Path(cfg_noquotes).write_text("[Section]\nname = My String with Spaces") opt_quotes = fun(entry_cfg=cfg_quotes) opt_doublequotes = fun(entry_cfg=cfg_doublequotes) @@ -520,8 +515,7 @@ def fun(opt): assert opt.bos == "myString" cfg_file = tmp_path / "bos.ini" - with open(cfg_file, "w") as f: - f.write("[Section]\nbos = 'myString'") + Path(cfg_file).write_text("[Section]\nbos = 'myString'") opt = fun(entry_cfg=cfg_file) assert opt.bos == "myString" @@ -538,8 +532,7 @@ def fun(opt): return opt cfg_file = tmp_path / "bos.ini" - with open(cfg_file, "w") as f: - f.write("[Section]\nbos1 = 'myString'\nbos2 = True") + Path(cfg_file).write_text("[Section]\nbos1 = 'myString'\nbos2 = True") opt = fun(entry_cfg=cfg_file) assert opt.bos1 == "myString" assert opt.bos2 is True @@ -578,8 +571,7 @@ def fun(opt): return opt cfg_file = tmp_path / "bol.ini" - with open(cfg_file, "w") as f: - f.write("[Section]\nbol1 = 1,2\nbol2 = True") + Path(cfg_file).write_text("[Section]\nbol1 = 1,2\nbol2 = True") opt = fun(entry_cfg=cfg_file) assert opt.bol1 == [1, 2] assert opt.bol2 is True From 16e13a2b2c5577df4c1d07a5a662542b9dfd1466 Mon Sep 17 00:00:00 2001 From: Felix Soubelet Date: Thu, 24 Jul 2025 09:35:54 +0200 Subject: [PATCH 25/25] review comments --- tests/test_entrypoint.py | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 820876a..3b944ee 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -179,9 +179,9 @@ def test_as_argv(): # almost identical to above assert len(unknown) > 0 -def test_as_config(tmp_path): - cfg_file = tmp_path / "config.ini" - Path(cfg_file).write_text( +def test_as_config(tmp_path: Path): + cfg_file: Path = tmp_path / "config.ini" + cfg_file.write_text( "\n".join( [ "[Section]", @@ -284,7 +284,7 @@ def fun(opt): # Test Config read-write ------------------------------------------------------- -def test_save_options(tmp_path): +def test_save_options(tmp_path: Path): opt, unknown = paramtest_function( name="myname", int=3, @@ -292,7 +292,7 @@ def test_save_options(tmp_path): unknown="myfinalargument", unknoown=10, ) - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) @@ -300,15 +300,15 @@ def test_save_options(tmp_path): _assert_dicts_equal(unknown, unknown_load) -def test_save_cli_options_cfg(tmp_path): +def test_save_cli_options_cfg(tmp_path: Path): opt, unknown = paramtest_function( ["--name", "myname", "--int", "3", "--list", "4", "5", "6", "--other"] ) - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) - content = Path(cfg_file).read_text() + content = cfg_file.read_text() assert "Unknown" in content assert "--other" in content @@ -316,12 +316,12 @@ def test_save_cli_options_cfg(tmp_path): assert len(unknown_load) == 0 -def test_save_and_cfg_load_with_none(tmp_path): +def test_save_and_cfg_load_with_none(tmp_path: Path): # use cli_args in case we run in test-wrapper (e.g. pycharm) with cli_args(): # name, int, list = None opt, unknown = paramtest_function() - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) @@ -331,10 +331,10 @@ def test_save_and_cfg_load_with_none(tmp_path): assert all(val is None for val in opt.values()) -def test_save_and_load_cfg_with_none_explicit(tmp_path): +def test_save_and_load_cfg_with_none_explicit(tmp_path: Path): opt, unknown = paramtest_function(name=None, int=None, list=None) - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" save_options_to_config(cfg_file, opt, unknown) opt_load, unknown_load = paramtest_function(entry_cfg=cfg_file) @@ -344,19 +344,19 @@ def test_save_and_load_cfg_with_none_explicit(tmp_path): assert all(val is None for val in opt.values()) -def test_string_cfg(tmp_path): +def test_string_cfg(tmp_path: Path): @entrypoint(EntryPointParameters({"name": {"type": str}}), strict=True) def fun(opt): return opt - cfg_quotes = tmp_path / "config_quotes.ini" - Path(cfg_quotes).write_text("[Section]\nname = 'My String with Spaces'") + cfg_quotes: Path = tmp_path / "config_quotes.ini" + cfg_quotes.write_text("[Section]\nname = 'My String with Spaces'") - cfg_doublequotes = tmp_path / "config_doublequotes.ini" - Path(cfg_doublequotes).write_text('[Section]\nname = "My String with Spaces"') + cfg_doublequotes: Path = tmp_path / "config_doublequotes.ini" + cfg_doublequotes.write_text('[Section]\nname = "My String with Spaces"') - cfg_noquotes = tmp_path / "config_noquotes.ini" - Path(cfg_noquotes).write_text("[Section]\nname = My String with Spaces") + cfg_noquotes: Path = tmp_path / "config_noquotes.ini" + cfg_noquotes.write_text("[Section]\nname = My String with Spaces") opt_quotes = fun(entry_cfg=cfg_quotes) opt_doublequotes = fun(entry_cfg=cfg_doublequotes) @@ -366,26 +366,26 @@ def fun(opt): assert opt_quotes.name == opt_noquotes.name -def test_string_with_break_cfg(tmp_path): +def test_string_with_break_cfg(tmp_path: Path): @entrypoint(EntryPointParameters({"name": {"type": str}}), strict=True) def fun(opt): return opt opt = fun(name="this is\nmystring") - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" save_options_to_config(cfg_file, opt) opt_load = fun(entry_cfg=cfg_file) assert opt_load.name == opt.name -def test_path_cfg(tmp_path): +def test_path_cfg(tmp_path: Path): @entrypoint(EntryPointParameters({"path": {"type": Path}}), strict=True) def fun(opt): return opt - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" opt = fun(path=tmp_path) save_options_to_config(cfg_file, opt) @@ -394,12 +394,12 @@ def fun(opt): _assert_dicts_equal(opt, opt_load) -def test_list_cfg(tmp_path): +def test_list_cfg(tmp_path: Path): @entrypoint(EntryPointParameters({"lst": {"type": int, "nargs": "*"}}), strict=True) def fun(opt): return opt - cfg_file = tmp_path / "config.ini" + cfg_file: Path = tmp_path / "config.ini" opt = fun(lst=[1, 2, 3, 4]) save_options_to_config(cfg_file, opt)