diff --git a/doc/conf.py b/doc/conf.py index 05e8126..e9de913 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. @@ -16,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 @@ -28,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" @@ -104,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,14 +121,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, + "version_selector": True, # sphinx-rtd-theme>=3.0, formerly 'display_version' + "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 +184,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 +192,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 +204,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..3b4b8f5 100644 --- a/generic_parser/dict_parser.py +++ b/generic_parser/dict_parser.py @@ -4,12 +4,13 @@ This module holds classes to handle different `dictionaries` as argument containers. """ + import argparse import copy import logging from pathlib import Path -from generic_parser.tools import DotDict, _TC +from generic_parser.tools import _TC, DotDict LOG = logging.getLogger(__name__) @@ -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.type is 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}") - - 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"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}" + ) 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} Default: {leaf.default}") - LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s}" - f" 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} Choices: {leaf.choices}") - LOG.info(f"{level_char_pp + _TC['S'] + _TC['-']:s}" - f" Choices: {leaf.choices}") + LOG.info(f"{level_char_pp + _TC['L'] + _TC['-']:s} Help: {leaf.help:s}") - 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("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 @@ -350,8 +364,8 @@ 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)): - return my_type(item.strip("\'\"")) + if issubclass(my_type, (str | Path)): + return my_type(item.strip("'\"")) if issubclass(my_type, bool): return bool(eval(item)) @@ -360,12 +374,12 @@ 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: 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): @@ -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,74 @@ 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.") + 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." + ) 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 (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 + 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 is 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 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] + list(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: + 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: - 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 +505,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..4c09a63 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,12 +17,13 @@ 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) - def __subclasscheck__(self, subclass): - return any([issubclass(c, subclass) for c in classes]) + def __subclasscheck__(cls, subclass): + return any(issubclass(c, subclass) for c in classes) return FakeMeta @@ -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,18 +80,18 @@ 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 - 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)): @@ -96,13 +99,13 @@ 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 - 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 ea5ba71..b69614e 100644 --- a/generic_parser/entrypoint_parser.py +++ b/generic_parser/entrypoint_parser.py @@ -160,22 +160,23 @@ def entry_function(opt): python script.py --entry_cfg config.ini 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 +from generic_parser.dict_parser import ArgumentError, DictParser, ParameterError +from generic_parser.tools import DotDict, StringIO, log_out, silence, unformatted_console_logging -import logging LOG = logging.getLogger(__name__) @@ -189,7 +190,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 +232,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,31 +258,33 @@ 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( + f"--{ID_CONFIG}", + type=str, + dest=ID_CONFIG, + required=True, + ) + parser.add_argument( + f"--{ID_SECTION}", + type=str, + dest=ID_SECTION, + ) return parser def _create_argument_parser(self, args_dict: Mapping): """Creates the ArgumentParser from parameter.""" - if args_dict: - parser = ArgumentParser(**args_dict) - else: - parser = ArgumentParser() - - parser = _add_params_to_argument_parser(parser, self.parameter) - return parser + parser = ArgumentParser(**args_dict) if args_dict else ArgumentParser() + 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 @@ -314,10 +323,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])) @@ -334,8 +342,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 +352,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,11 +366,11 @@ 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: - json_dict = json.load(json_file) + f"Only '{ID_JSON:s}' and '{ID_SECTION:s}'" + " arguments are allowed, when using a json file." + ) + json_dict = json.loads(Path(kwargs[ID_JSON]).read_text()) if ID_SECTION in kwargs: json_dict = json_dict[kwargs[ID_SECTION]] @@ -370,7 +379,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,24 +393,28 @@ 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.""" # 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() @@ -411,17 +424,17 @@ 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 + return cfgparse.items(section) # 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. @@ -442,38 +455,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,13 +506,13 @@ 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") 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.""" @@ -517,22 +538,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 +632,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 @@ -647,7 +669,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") @@ -672,6 +694,7 @@ def _add_params_to_argument_parser(arg_parser, params): arg_parser.add_argument(*flags, **param) return arg_parser + # --- @@ -707,8 +730,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,10 +774,11 @@ 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) - elif isinstance(value, (str, Path)): + value = "" # defined as empty (see dict_parser._convert_config_items) + elif isinstance(value, (str | Path)): value = f'"{value}"' if "\n" in value: value = value.replace("\n", "\n ") # spaces in new line indicate continuation @@ -771,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 12d13df..0e28281 100644 --- a/generic_parser/tools.py +++ b/generic_parser/tools.py @@ -4,19 +4,21 @@ Provides utilities to use in other modules. """ + import logging import os import sys from contextlib import contextmanager from io import StringIO +from pathlib import Path 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,8 +27,9 @@ 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]) @@ -38,7 +41,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 @@ -47,16 +50,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 +68,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(f"{name:s}:") + print_tree(dictionary, "") def get_subdict(full_dict, keys, strict=True): @@ -88,6 +92,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.""" @@ -107,12 +112,8 @@ 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: - yield - finally: - devnull.close() + with Path(os.devnull).open("w") as devnull, log_out(stdout=devnull, stderr=devnull): + yield @contextmanager @@ -142,6 +143,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 +164,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/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 = [] diff --git a/tests/conftest.py b/tests/conftest.py index 5906e6c..ac3ad9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,17 +5,14 @@ See also https://stackoverflow.com/a/34520971 . """ -import shutil + import sys from contextlib import contextmanager -from pathlib import Path - -import pytest @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 +22,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..a8dddcc 100644 --- a/tests/test_dict_parser.py +++ b/tests/test_dict_parser.py @@ -1,24 +1,22 @@ -from io import StringIO +import sys import pytest -import sys -import logging -from generic_parser.dict_parser import ParameterError, Parameter, DictParser, ArgumentError +from generic_parser.dict_parser import ArgumentError, DictParser, Parameter, ParameterError from generic_parser.tools import TempStringLogger 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 +29,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 +66,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(): @@ -108,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(): @@ -127,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 diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index bd18c92..3b944ee 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -3,17 +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.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 +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 ( + EntryPoint, + EntryPointParameters, + OptionsError, + create_parameter_help, + entrypoint, + save_options_to_config, + split_arguments, +) +from generic_parser.tools import TempStringLogger, print_dict_tree, silence +from tests.conftest import cli_args LOG = logging.getLogger(__name__) DEBUG = False @@ -27,6 +30,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,7 +38,8 @@ 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 @@ -42,13 +47,14 @@ 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 def test_class_functions(): - class MyClass(object): + class MyClass: @classmethod @entrypoint(get_simple_params()) def fun(cls, opt, unknown): @@ -56,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 @@ -78,9 +84,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 +109,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 +149,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 +160,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 +170,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 @@ -170,26 +179,25 @@ def test_as_argv(): # almost identical to above assert len(unknown) > 0 -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'", - ])) +def test_as_config(tmp_path: Path): + cfg_file: Path = tmp_path / "config.ini" + 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" - ) + 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 @@ -205,9 +213,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(): @@ -222,7 +229,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(): @@ -236,7 +243,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 @@ -245,7 +252,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 @@ -254,19 +261,19 @@ 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 opt = fun({}) - assert opt.foo == 'test' + assert opt.foo == "test" opt = fun(foo=None) assert opt.foo is None 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 @@ -276,7 +283,8 @@ 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, @@ -284,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) @@ -292,70 +300,63 @@ 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"] + ["--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) - with open(cfg_file, 'r') as f: - content = f.read() - assert 'Unknown' in content - assert '--other' in content + content = cfg_file.read_text() + assert "Unknown" in content + assert "--other" in content _assert_dicts_equal(opt, opt_load) 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) _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): +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) _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): - @entrypoint(EntryPointParameters(dict(name={'type': str})), strict=True) +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" - with open(cfg_quotes, "w") as f: - f.write("[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" - with open(cfg_doublequotes, "w") as f: - f.write('[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" - with open(cfg_noquotes, "w") as f: - f.write('[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) @@ -365,26 +366,26 @@ def fun(opt): assert opt_quotes.name == opt_noquotes.name -def test_string_with_break_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(name={'type': str})), strict=True) +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): - @entrypoint(EntryPointParameters(dict(path={'type': Path})), strict=True) +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) @@ -393,12 +394,12 @@ def fun(opt): _assert_dicts_equal(opt, opt_load) -def test_list_cfg(tmp_path): - @entrypoint(EntryPointParameters(dict(lst={'type': int, 'nargs': "*"})), strict=True) +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) @@ -409,11 +410,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" @@ -433,11 +435,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) @@ -446,11 +448,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) @@ -459,17 +461,17 @@ 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([dict(flags="--ios", name="ios", type=IntOrStr)], strict=True) + @entrypoint([{"flags": "--ios", "name": "ios", "type": IntOrStr}], strict=True) def fun(opt): return 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 @@ -479,29 +481,29 @@ 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 - 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): - @entrypoint([dict(flags="--bos", name="bos", type=BoolOrString)], strict=True) + @entrypoint([{"flags": "--bos", "name": "bos", "type": BoolOrString}], strict=True) def fun(opt): return 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 @@ -513,28 +515,31 @@ 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" 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( + [ + {"flags": "--bos1", "name": "bos1", "type": BoolOrString}, + {"flags": "--bos2", "name": "bos2", "type": BoolOrString}, + ], + strict=True, + ) 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.bos1 == "myString" assert opt.bos2 is True 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 @@ -555,14 +560,18 @@ 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( + [ + {"flags": "--bol1", "name": "bol1", "type": BoolOrList}, + {"flags": "--bol2", "name": "bol2", "type": BoolOrList}, + ], + strict=True, + ) 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 @@ -595,7 +604,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 @@ -605,7 +614,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 @@ -613,62 +622,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="+") + "name": {"flags": "--name", "type": str}, + "int": {"flags": "--int", "type": int}, + "list": {"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,), - }) - return args + """For testing the create_param_help()""" + return EntryPointParameters( + { + "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, + }, + } + ) # Example Wrapped Functions ---------------------------------------------------- @@ -678,7 +717,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 a3d3650..54d1acd 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 @@ -7,7 +8,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 +16,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 @@ -33,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 ##################################################################### @@ -42,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]}}