diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e0cb9bb8..00000000 --- a/.flake8 +++ /dev/null @@ -1,15 +0,0 @@ -[flake8] -ignore = - # module level import not at top of file - E402, - # imported but unused - F401, - # invalid escape sequence - W605, - # local variable is assigned to but never used - F841, - # line break before binary operator (conflicts with Black) - W503, -max-line-length = 9000 -exclude = .git,__pycache__,*.yaml,*.yml,Makefile,.flake8,*.toml,*.md,*.rst,*.sh,venv*,temp -max-complexity = 100 diff --git a/.gitignore b/.gitignore index 4dc653bc..04127049 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ dist/ .coverage coverage.xml tmp_*/ +.ruff_cache/ # Ignore all directories/files under docs/ unless excluded here docs/*/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b81b2070..40f35e69 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,18 @@ fail_fast: true repos: - - repo: https://github.com/grantjenks/blue - rev: v0.9.1 + - repo: local hooks: - - id: blue + - id: ruff-check + name: ruff-check + entry: ruff + args: ['check', '--fix', '--no-cache'] + language: system types: [python] - - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 - hooks: - - id: flake8 + stages: [commit] + - id: ruff-format + name: ruff-format + entry: ruff + args: ['format', '--no-cache'] + language: system types: [python] + stages: [commit] diff --git a/docs/_src/index.rst b/docs/_src/index.rst index b8f21c26..678ebbda 100644 --- a/docs/_src/index.rst +++ b/docs/_src/index.rst @@ -1,7 +1,7 @@ CLI Command Parser ################## -|downloads| |py_version| |coverage_badge| |build_status| |Blue| +|downloads| |py_version| |coverage_badge| |build_status| |Ruff| .. |py_version| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20-blue :target: https://pypi.org/project/cli-command-parser/ @@ -12,8 +12,8 @@ CLI Command Parser .. |build_status| image:: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml/badge.svg :target: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml -.. |Blue| image:: https://img.shields.io/badge/code%20style-blue-blue.svg - :target: https://blue.readthedocs.io/ +.. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://docs.astral.sh/ruff/ .. |downloads| image:: https://img.shields.io/pypi/dm/cli-command-parser :target: https://pypistats.org/packages/cli-command-parser diff --git a/lib/cli_command_parser/command_parameters.py b/lib/cli_command_parser/command_parameters.py index a9392f33..73b0c510 100644 --- a/lib/cli_command_parser/command_parameters.py +++ b/lib/cli_command_parser/command_parameters.py @@ -12,25 +12,26 @@ from collections import defaultdict from functools import cached_property -from typing import TYPE_CHECKING, Optional, Collection, Iterator, List, Dict, Set, Tuple +from typing import TYPE_CHECKING, Collection, Iterator, Optional -from .config import CommandConfig, AmbiguousComboMode -from .exceptions import CommandDefinitionError, ParameterDefinitionError, AmbiguousShortForm, AmbiguousCombo -from .parameters.base import ParamBase, Parameter, BaseOption, BasePositional -from .parameters import SubCommand, PassThru, ActionFlag, ParamGroup, Action, help_action +from .config import AmbiguousComboMode, CommandConfig +from .exceptions import AmbiguousCombo, AmbiguousShortForm, CommandDefinitionError, ParameterDefinitionError +from .parameters import Action, ActionFlag, ParamGroup, PassThru, SubCommand, help_action +from .parameters.base import BaseOption, BasePositional, ParamBase, Parameter if TYPE_CHECKING: from .context import Context from .formatting.commands import CommandHelpFormatter from .typing import CommandCls, Strings -__all__ = ['CommandParameters'] + OptionMap = dict[str, BaseOption] + ActionFlags = list[ActionFlag] -OptionMap = Dict[str, BaseOption] -ActionFlags = List[ActionFlag] +__all__ = ['CommandParameters'] class CommandParameters: + # fmt: off command: CommandCls #: The Command associated with this CommandParameters object formatter: CommandHelpFormatter #: The formatter used for this Command's help text command_parent: Optional[CommandCls] #: The parent Command, if any @@ -39,13 +40,14 @@ class CommandParameters: _pass_thru: Optional[PassThru] = None #: A PassThru Parameter, if specified sub_command: Optional[SubCommand] = None #: A SubCommand Parameter, if specified action_flags: ActionFlags #: List of action flags - split_action_flags: Tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main - options: List[BaseOption] #: List of optional Parameters + split_action_flags: tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main + options: list[BaseOption] #: List of optional Parameters combo_option_map: OptionMap #: Mapping of {short opt: Parameter} (no dash characters) - groups: List[ParamGroup] #: List of ParamGroup objects - positionals: List[BasePositional] #: List of positional Parameters - _deferred_positionals: List[BasePositional] = () #: Positional Parameters that are deferred to sub commands + groups: list[ParamGroup] #: List of ParamGroup objects + positionals: list[BasePositional] #: List of positional Parameters + _deferred_positionals: list[BasePositional] = () #: Positional Parameters that are deferred to sub commands option_map: OptionMap #: Mapping of {--opt / -opt: Parameter} + # fmt: on def __init__( self, @@ -83,7 +85,7 @@ def has_nested_pass_thru(self) -> bool: # endregion @cached_property - def all_positionals(self) -> List[BasePositional]: + def all_positionals(self) -> list[BasePositional]: try: if not self.parent.sub_command: return self.parent.all_positionals + self.positionals @@ -91,7 +93,7 @@ def all_positionals(self) -> List[BasePositional]: pass return self.positionals - def get_positionals_to_parse(self, ctx: Context) -> List[BasePositional]: + def get_positionals_to_parse(self, ctx: Context) -> list[BasePositional]: if self.all_positionals: for i, param in enumerate(self.all_positionals): if not ctx.num_provided(param): @@ -170,13 +172,13 @@ def _process_parameters(self): self._process_options(options) self._process_groups(groups) - def _process_groups(self, groups: Set[ParamGroup]): + def _process_groups(self, groups: set[ParamGroup]): if self.parent: self.groups = sorted((*self.parent.groups, *groups)) if groups else self.parent.groups.copy() else: self.groups = sorted(groups) if groups else [] - def _process_positionals(self, params: List[BasePositional]): + def _process_positionals(self, params: list[BasePositional]): unfollowable = action_or_sub_cmd = split_index = None if self.parent and (deferred := self.parent._deferred_positionals): params = deferred + params @@ -216,7 +218,7 @@ def _process_positionals(self, params: List[BasePositional]): self.positionals = params - def _process_options(self, params: List[BaseOption]): + def _process_options(self, params: list[BaseOption]): if parent := self.parent: option_map = parent.option_map.copy() combo_option_map = parent.combo_option_map.copy() @@ -298,7 +300,7 @@ def _process_action_flags(self): # region Ambiguous Short Combo Handling @cached_property - def _classified_combo_options(self) -> Tuple[OptionMap, OptionMap]: + def _classified_combo_options(self) -> tuple[OptionMap, OptionMap]: multi_char_combos = {} items = self.combo_option_map.items() for combo, param in items: @@ -308,7 +310,7 @@ def _classified_combo_options(self) -> Tuple[OptionMap, OptionMap]: return {}, multi_char_combos @cached_property - def _potentially_ambiguous_combo_opts(self) -> Dict[str, Tuple[BaseOption, OptionMap]]: + def _potentially_ambiguous_combo_opts(self) -> dict[str, tuple[BaseOption, OptionMap]]: return _find_ambiguous_combos(*self._classified_combo_options) @cached_property @@ -357,7 +359,7 @@ def _iter_nested_params(self) -> Iterator[CommandParameters]: def short_option_to_param_value_pairs( self, option: str - ) -> Tuple[List[Tuple[str, BaseOption, Optional[str]]], bool]: + ) -> tuple[list[tuple[str, BaseOption, Optional[str]]], bool]: option, eq, value = option.partition('=') if eq: # An `=` was present in the string # Note: if the option is not in this Command's option_map, the KeyError is handled by CommandParser @@ -404,7 +406,7 @@ def required_check_params(self) -> Iterator[Parameter]: def _find_ambiguous_combos( single_char_combos: OptionMap, multi_char_combos: OptionMap -) -> Dict[str, Tuple[BaseOption, OptionMap]]: +) -> dict[str, tuple[BaseOption, OptionMap]]: ambiguous_combo_options = {} for combo, param in multi_char_combos.items(): if singles := {c: single_char_combos[c] for c in combo if c in single_char_combos}: diff --git a/lib/cli_command_parser/commands.py b/lib/cli_command_parser/commands.py index a080cefb..6614c827 100644 --- a/lib/cli_command_parser/commands.py +++ b/lib/cli_command_parser/commands.py @@ -9,10 +9,10 @@ import logging from abc import ABC from contextlib import ExitStack -from typing import TYPE_CHECKING, Type, Sequence, Optional, overload +from typing import TYPE_CHECKING, Optional, Sequence, Type, overload -from .core import CommandMeta, get_top_level_commands, get_params -from .context import Context, ActionPhase, get_or_create_context +from .context import ActionPhase, Context, get_or_create_context +from .core import CommandMeta, get_params, get_top_level_commands from .exceptions import ParamConflict from .parser import parse_args_and_get_next_cmd from .utils import maybe_await @@ -54,12 +54,12 @@ def __repr__(self) -> str: @classmethod @overload def parse_and_run(cls: Type[CommandObj], argv: Argv = None, **kwargs) -> Optional[CommandObj]: - ... # These overloads indicate that an instance of the same type or another may be returned + # These overloads indicate that an instance of the same type or another may be returned + ... @classmethod @overload - def parse_and_run(cls, argv: Argv = None, **kwargs) -> Optional[CommandObj]: - ... + def parse_and_run(cls, argv: Argv = None, **kwargs) -> Optional[CommandObj]: ... @classmethod def parse_and_run(cls, argv=None, **kwargs): @@ -94,13 +94,11 @@ def parse_and_run(cls, argv=None, **kwargs): @classmethod @overload - def parse(cls: Type[CommandObj], argv: Argv = None) -> CommandObj: - ... + def parse(cls: Type[CommandObj], argv: Argv = None) -> CommandObj: ... @classmethod @overload - def parse(cls, argv: Argv = None) -> CommandObj: - ... + def parse(cls, argv: Argv = None) -> CommandObj: ... @classmethod def parse(cls, argv=None): diff --git a/lib/cli_command_parser/compat.py b/lib/cli_command_parser/compat.py index 6f974166..da666089 100644 --- a/lib/cli_command_parser/compat.py +++ b/lib/cli_command_parser/compat.py @@ -8,8 +8,9 @@ characters. """ +from __future__ import annotations + from textwrap import TextWrapper -from typing import List from .utils import wcswidth @@ -24,7 +25,7 @@ class WCTextWrapper(TextWrapper): optional ``wcwidth`` dependency is available). Minimal formatting changes are applied. No logic has been changed. """ - def _wrap_chunks(self, chunks: List[str]) -> List[str]: + def _wrap_chunks(self, chunks: list[str]) -> list[str]: """ _wrap_chunks(chunks : [string]) -> [string] diff --git a/lib/cli_command_parser/config.py b/lib/cli_command_parser/config.py index 9ced1f13..60948ab2 100644 --- a/lib/cli_command_parser/config.py +++ b/lib/cli_command_parser/config.py @@ -8,7 +8,7 @@ from collections import ChainMap from enum import Enum -from typing import TYPE_CHECKING, Optional, Any, Union, Callable, Type, TypeVar, Generic, overload, Dict +from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Type, TypeVar, Union, overload from .utils import FixedFlag, MissingMixin, _NotSet, positive_int @@ -17,7 +17,7 @@ from .error_handling import ErrorHandler from .formatting.commands import CommandHelpFormatter from .formatting.params import ParamHelpFormatter - from .typing import Bool, ParamOrGroup, CommandType + from .typing import Bool, CommandType, ParamOrGroup __all__ = [ 'CommandConfig', @@ -49,11 +49,13 @@ class ShowDefaults(FixedFlag): is equivalent to ``ShowDefaults.MISSING | ShowDefaults.NEVER``, which will result in no default values being shown. """ + # fmt: off NEVER = 1 #: Never include the default value in help text MISSING = 2 #: Only include the default value if ``default:`` is not already present TRUTHY = 4 #: Only include the default value if it is treated as True in a boolean context NON_EMPTY = 8 #: Only include the default value if it is not ``None`` or an empty container ANY = 16 #: Any default value, regardless of truthiness, will be included + # fmt: on @classmethod def _missing_(cls, value: Union[str, int]) -> ShowDefaults: @@ -104,6 +106,7 @@ class OptionNameMode(FixedFlag): - ``'underscore'`` or ``'dash'`` or ``'both'`` or ``'both_underscore'`` or ``'both_dash'`` or ``'none'`` """ + # fmt: off UNDERSCORE = 1 DASH = 2 BOTH = 3 # = 1|2 @@ -111,6 +114,7 @@ class OptionNameMode(FixedFlag): BOTH_UNDERSCORE = 15 # & 8 -> show only underscore version BOTH_DASH = 23 # & 16 -> show only dash version NONE = 32 + # fmt: on @classmethod def _missing_(cls, value: Union[str, int, None]) -> OptionNameMode: @@ -153,9 +157,11 @@ class that is registered as a subcommand / subclass of another Command) should b ``Alias of: ``. """ + # fmt: off REPEAT = 'repeat' # Repeat the description as if it was a separate subcommand COMBINE = 'combine' # Combine aliases onto a single line ALIAS = 'alias' # Indicate the subcommand that it is an alias for; do not repeat the description + # fmt: on CmdAliasMode = Union[SubcommandAliasHelpMode, str] @@ -176,9 +182,11 @@ class AmbiguousComboMode(MissingMixin, Enum): input. """ + # fmt: off IGNORE = 'ignore' # Ignore potentially ambiguous combinations of short options entirely PERMISSIVE = 'permissive' # Allow multi-char short options that overlap with a single char one for exact matches STRICT = 'strict' # Reject multi-char short options that overlap with a single char one before parsing + # fmt: on class AllowLeadingDash(Enum): @@ -193,9 +201,11 @@ class AllowLeadingDash(Enum): :NEVER: Never allow values with a leading dash. """ + # fmt: off NUMERIC = 'numeric' # Allow a leading dash when the value is numeric ALWAYS = 'always' # Always allow a leading dash NEVER = 'never' # Never allow a leading dash + # fmt: on @classmethod def _missing_(cls, value): @@ -229,12 +239,10 @@ def __set_name__(self, owner: Type[CommandConfig], name: str): owner.FIELDS.add(name) @overload - def __get__(self, instance: None, owner: Type[CommandConfig]) -> ConfigItem[CV, DV]: - ... + def __get__(self, instance: None, owner: Type[CommandConfig]) -> ConfigItem[CV, DV]: ... @overload - def __get__(self, instance: CommandConfig, owner: Type[CommandConfig]) -> ConfigValue: - ... + def __get__(self, instance: CommandConfig, owner: Type[CommandConfig]) -> ConfigValue: ... def __get__(self, instance, owner): if instance is None: @@ -432,7 +440,7 @@ def __repr__(self) -> str: settings = ', '.join(f'{k}={v!r}' for k, v in self.as_dict(False).items()) return f'<{self.__class__.__name__}[depth={len(self._data.maps)}]({settings})>' - def as_dict(self, full: Bool = True) -> Dict[str, Any]: + def as_dict(self, full: Bool = True) -> dict[str, Any]: """Return a dict representing the configured options.""" if full: return {key: getattr(self, key) for key in self.FIELDS} diff --git a/lib/cli_command_parser/context.py b/lib/cli_command_parser/context.py index e4f4fe3a..4a7b5818 100644 --- a/lib/cli_command_parser/context.py +++ b/lib/cli_command_parser/context.py @@ -13,20 +13,19 @@ from contextvars import ContextVar from enum import Enum from functools import cached_property -from inspect import Signature, Parameter as _Parameter -from typing import TYPE_CHECKING, Any, Callable, Union, Sequence, Optional, Iterator, Collection, cast -from typing import Dict, Tuple, List +from inspect import Parameter as _Parameter, Signature +from typing import TYPE_CHECKING, Any, Callable, Collection, Iterator, Optional, Sequence, Union, cast -from .config import CommandConfig, DEFAULT_CONFIG +from .config import DEFAULT_CONFIG, CommandConfig from .error_handling import ErrorHandler, NullErrorHandler, extended_error_handler from .exceptions import NoActiveContext -from .utils import _NotSet, Terminal +from .utils import Terminal, _NotSet if TYPE_CHECKING: from .command_parameters import CommandParameters from .commands import Command - from .parameters import Parameter, Option, ActionFlag - from .typing import Bool, ParamOrGroup, CommandType, CommandObj, AnyConfig, OptStr, StrSeq, PathLike # noqa + from .parameters import ActionFlag, Option, Parameter + from .typing import AnyConfig, Bool, CommandObj, CommandType, OptStr, ParamOrGroup, PathLike, StrSeq # noqa __all__ = ['Context', 'ctx', 'get_current_context', 'get_or_create_context', 'get_context', 'get_parsed', 'get_raw_arg'] @@ -49,7 +48,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma allow_argv_prog: Bool = True _command_obj: CommandObj = None _terminal_width: Optional[int] - _provided: Dict[ParamOrGroup, int] + _provided: dict[ParamOrGroup, int] def __init__( self, @@ -158,7 +157,7 @@ def get_parsed( recursive: Bool = True, default: Any = None, include_defaults: Bool = True, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """ Returns all of the parsed arguments as a dictionary. @@ -247,7 +246,7 @@ def num_provided(self, param: ParamOrGroup) -> int: """Not intended to be called by users. Used by Parameters during parsing to handle nargs.""" return self._provided[param] - def get_missing(self) -> List[Parameter]: + def get_missing(self) -> list[Parameter]: """Not intended to be called by users. Used during parsing to determine if any Parameters are missing.""" return [p for p in self.params.required_check_params() if not self._provided[p]] @@ -260,7 +259,7 @@ def missing_options_with_env_var(self) -> Iterator[Option]: # region Actions @cached_property - def _parsed_action_flags(self) -> Tuple[int, List[ActionFlag], List[ActionFlag]]: + def _parsed_action_flags(self) -> tuple[int, list[ActionFlag], list[ActionFlag]]: """ Not intended to be accessed by users. Returns a tuple containing the total number of action flags provided, the action flags to run before main, and the action flags to run after main. @@ -281,13 +280,13 @@ def action_flag_count(self) -> int: return self._parsed_action_flags[0] @cached_property - def all_action_flags(self) -> List[ActionFlag]: + def all_action_flags(self) -> list[ActionFlag]: """Not intended to be accessed by users. Returns all parsed action flags.""" _, before_main, after_main = self._parsed_action_flags return before_main + after_main @cached_property - def categorized_action_flags(self) -> Dict[ActionPhase, Sequence[ActionFlag]]: + def categorized_action_flags(self) -> dict[ActionPhase, Sequence[ActionFlag]]: """ Not intended to be accessed by users. Returns a dict of parsed action flags, categorized by the :class:`ActionPhase` during which they will run. @@ -321,7 +320,7 @@ def iter_action_flags(self, phase: ActionPhase) -> Iterator[ActionFlag]: def _normalize_config( - config: AnyConfig, kwargs: Dict[str, Any], parent: Context | None, command: CommandType | None + config: AnyConfig, kwargs: dict[str, Any], parent: Context | None, command: CommandType | None ) -> CommandConfig: if config is not None: if kwargs: @@ -469,7 +468,7 @@ def get_context(command: Command) -> Context: def get_parsed( command: Command, to_call: Callable = None, default: Any = None, include_defaults: Bool = True -) -> Dict[str, Any]: +) -> dict[str, Any]: """ Provides a way to obtain all of the arguments that were parsed for the given Command as a dictionary. diff --git a/lib/cli_command_parser/conversion/argparse_ast.py b/lib/cli_command_parser/conversion/argparse_ast.py index 2a9e1a25..b15bfa3d 100644 --- a/lib/cli_command_parser/conversion/argparse_ast.py +++ b/lib/cli_command_parser/conversion/argparse_ast.py @@ -5,24 +5,24 @@ import sys from argparse import ArgumentParser from ast import AST, Assign, Call, withitem -from functools import partial, cached_property -from inspect import Signature, BoundArguments +from functools import cached_property, partial +from inspect import BoundArguments, Signature from pathlib import Path -from typing import TYPE_CHECKING, Union, Optional, Callable, Collection, TypeVar, Generic, Type, Iterator -from typing import List, Tuple, Dict, Set +from typing import TYPE_CHECKING, Callable, Collection, Generic, Iterator, Type, TypeVar, Union from .argparse_utils import ArgumentParser as _ArgumentParser, SubParsersAction as _SubParsersAction from .utils import get_name_repr, iter_module_parents, unparse if TYPE_CHECKING: from cli_command_parser.typing import PathLike - from .visitor import TrackedRefMap, TrackedRef + + from .visitor import TrackedRef, TrackedRefMap __all__ = ['ParserArg', 'ArgGroup', 'MutuallyExclusiveGroup', 'AstArgumentParser', 'SubParser', 'Script'] log = logging.getLogger(__name__) InitNode = Union[Call, Assign, withitem] -OptCall = Optional[Call] +OptCall = Union[Call, None] ParserCls = Type['AstArgumentParser'] ParserObj = TypeVar('ParserObj', bound='AstArgumentParser') RepresentedCallable = TypeVar('RepresentedCallable', bound=Callable) @@ -33,7 +33,7 @@ class Script: _parser_classes = {} - path: Optional[Path] + path: Union[Path, None] def __init__(self, src_text: str, smart_loop_handling: bool = True, path: PathLike = None): self.smart_loop_handling = smart_loop_handling @@ -49,7 +49,7 @@ def __repr__(self) -> str: return f'<{self.__class__.__name__}[{parsers=}{location}]>' @property - def mod_cls_to_ast_cls_map(self) -> Dict[str, Dict[str, ParserCls]]: + def mod_cls_to_ast_cls_map(self) -> dict[str, dict[str, ParserCls]]: return self._parser_classes @classmethod @@ -72,7 +72,7 @@ def add_parser(self, ast_cls: ParserCls, node: InitNode, call: OptCall, tracked_ return parser @cached_property - def parsers(self) -> List[ParserObj]: + def parsers(self) -> list[ParserObj]: from .visitor import ScriptVisitor, TrackedRef # noqa: F811 track_refs = (TrackedRef('argparse.REMAINDER'), TrackedRef('argparse.SUPPRESS')) @@ -97,7 +97,7 @@ def __init__(self, func): self.func = func def __set_name__(self, owner: Type[AstCallable], name: str): - if owner._add_visit_func(name): # This check is only to enable a low-value unit test... + if owner._add_visit_func(name): # This check is only to enable a low-value unit test... setattr(owner, name, self.func) # There's no need to keep the descriptor - replace self with func def __get__(self, instance, owner): @@ -162,7 +162,7 @@ def __init__(self, node: InitNode, parent: AstCallable | Script, tracked_refs: T def __repr__(self) -> str: return f'<{self.__class__.__name__}[{self.init_call_repr()}]>' - def get_tracked_refs(self, module: str, name: str, default: D = _NotSet) -> Union[Set[str], D]: + def get_tracked_refs(self, module: str, name: str, default: D = _NotSet) -> Union[set[str], D]: for tracked_ref, refs in self._tracked_refs.items(): if tracked_ref.module == module and tracked_ref.name == name: return refs @@ -193,7 +193,7 @@ def _init_func_bound(self) -> BoundArguments: return self.signature.bind(*args, **{kw.arg: kw.value for kw in self.call_kwargs}) @cached_property - def init_func_args(self) -> List[str]: + def init_func_args(self) -> list[str]: try: args = self._init_func_bound.args[1:] except (TypeError, AttributeError): # No represents func @@ -201,7 +201,7 @@ def init_func_args(self) -> List[str]: return [unparse(arg) for arg in args] @cached_property - def init_func_raw_kwargs(self) -> Dict[str, AST]: + def init_func_raw_kwargs(self) -> dict[str, AST]: try: kwargs = self._init_func_bound.arguments except (TypeError, AttributeError): # No represents func @@ -215,11 +215,11 @@ def init_func_raw_kwargs(self) -> Dict[str, AST]: kwargs.update(kwargs.pop('kwargs')) return kwargs - def _init_func_kwargs(self) -> Dict[str, str]: + def _init_func_kwargs(self) -> dict[str, str]: return {key: unparse(val) for key, val in self.init_func_raw_kwargs.items()} @cached_property - def init_func_kwargs(self) -> Dict[str, str]: + def init_func_kwargs(self) -> dict[str, str]: return self._init_func_kwargs() def init_call_repr(self) -> str: @@ -244,8 +244,8 @@ class ParserArg(AstCallable, represents=ArgumentParser.add_argument): class ArgCollection(AstCallable): parent: ArgCollection | Script _children = ('args', 'groups') - args: List[ParserArg] - groups: List[ArgGroup] + args: list[ParserArg] + groups: list[ArgGroup] add_argument = AddVisitedChild(ParserArg, 'args') def __init_subclass__(cls, children: Collection[str] = (), **kwargs): @@ -261,7 +261,7 @@ def __init__(self, node: InitNode, parent: AstCallable | Script, tracked_refs: T def __repr__(self) -> str: return f'<{self.__class__.__name__}: ``{self.init_call_repr()}``>' - def _add_child(self, cls: Type[AC], container: List[AC], node: InitNode, call: Call, refs: TrackedRefMap) -> AC: + def _add_child(self, cls: Type[AC], container: list[AC], node: InitNode, call: Call, refs: TrackedRefMap) -> AC: child = cls(node, self, refs, call) container.append(child) return child @@ -274,7 +274,7 @@ def add_mutually_exclusive_group(self, node: InitNode, call: Call, tracked_refs: def add_argument_group(self, node: InitNode, call: Call, tracked_refs: TrackedRefMap): return self._add_child(ArgGroup, self.groups, node, call, tracked_refs) - def grouped_children(self) -> Iterator[Tuple[Type[AC], List[AC]]]: + def grouped_children(self) -> Iterator[tuple[Type[AC], list[AC]]]: yield ParserArg, self.args yield ArgGroup, self.groups @@ -311,7 +311,7 @@ def add_parser(self, node: InitNode, call: Call, tracked_refs: TrackedRefMap): @Script.register_parser class AstArgumentParser(ArgCollection, represents=ArgumentParser, children=('sub_parsers',)): - sub_parsers: List[SubParser] + sub_parsers: list[SubParser] add_subparsers = AddVisitedChild(SubparsersAction, '_subparsers_actions') def __init__(self, node: InitNode, parent: AstCallable | Script, tracked_refs: TrackedRefMap, call: Call = None): @@ -333,7 +333,7 @@ class SubParser(AstArgumentParser, represents=_SubParsersAction.add_parser): sp_parent: SubparsersAction @cached_property - def init_func_kwargs(self) -> Dict[str, str]: + def init_func_kwargs(self) -> dict[str, str]: kwargs = self.sp_parent.init_func_kwargs.copy() kwargs.update(self._init_func_kwargs()) return kwargs diff --git a/lib/cli_command_parser/conversion/command_builder.py b/lib/cli_command_parser/conversion/command_builder.py index 42b4ff1b..ec6e069e 100644 --- a/lib/cli_command_parser/conversion/command_builder.py +++ b/lib/cli_command_parser/conversion/command_builder.py @@ -3,18 +3,20 @@ import keyword import logging from abc import ABC, abstractmethod -from ast import literal_eval, Attribute, Name, GeneratorExp, Subscript, DictComp, ListComp, SetComp, Constant, Str +from ast import Attribute, Constant, DictComp, GeneratorExp, ListComp, Name, SetComp, Str, Subscript, literal_eval from dataclasses import dataclass, fields from functools import cached_property from itertools import count -from typing import TYPE_CHECKING, Union, Optional, Iterator, Iterable, Type, TypeVar, Generic, List, Tuple +from typing import TYPE_CHECKING, Generic, Iterable, Iterator, Optional, Type, TypeVar, Union from cli_command_parser.nargs import Nargs -from .argparse_ast import AC, ParserArg, ArgGroup, MutuallyExclusiveGroup, AstArgumentParser, Script + +from .argparse_ast import AC, ArgGroup, AstArgumentParser, MutuallyExclusiveGroup, ParserArg, Script from .utils import collection_contents, unparse if TYPE_CHECKING: from cli_command_parser.typing import OptStr + from .argparse_ast import ArgCollection __all__ = ['convert_script'] @@ -65,7 +67,7 @@ def init_for_ast_callable(cls, ast_obj: AC, *args, **kwargs) -> Converter[AC]: return cls.for_ast_callable(ast_obj)(ast_obj, *args, **kwargs) @classmethod - def init_group(cls: Type[C], parent: CollectionConverter, ast_objs: List[AC]) -> ConverterGroup[C]: + def init_group(cls: Type[C], parent: CollectionConverter, ast_objs: list[AC]) -> ConverterGroup[C]: return ConverterGroup(parent, cls, [cls(ast_obj, parent) for ast_obj in ast_objs]) def convert(self, indent: int = 0) -> str: @@ -79,7 +81,7 @@ def format_lines(self, indent: int = 0) -> Iterator[str]: class ConverterGroup(Generic[C]): __slots__ = ('parent', 'member_type', 'members') - def __init__(self, parent: CollectionConverter, member_type: Type[C], members: List[C]): + def __init__(self, parent: CollectionConverter, member_type: Type[C], members: list[C]): self.parent = parent self.member_type = member_type self.members = members @@ -127,7 +129,7 @@ def name_mode(self) -> str | None: return self._name_mode or (self.parent.name_mode if self.parent else None) @cached_property - def grouped_children(self) -> List[ConverterGroup[ParamConverter | GroupConverter | Converter]]: + def grouped_children(self) -> list[ConverterGroup[ParamConverter | GroupConverter | Converter]]: return [self.for_ast_callable(cg_cls).init_group(self, cg) for cg_cls, cg in self.ast_obj.grouped_children()] def descendant_args(self) -> Iterator[ParamConverter]: @@ -173,7 +175,7 @@ def __init__( self.add_methods = add_methods @cached_property - def sub_parser_converters(self) -> List[ParserConverter]: + def sub_parser_converters(self) -> list[ParserConverter]: cls, add_methods = self.__class__, self.add_methods return [cls(sub_parser, self, self.counter, add_methods=add_methods) for sub_parser in self.ast_obj.sub_parsers] @@ -250,7 +252,7 @@ def _custom_name(self) -> OptStr: return name.title().replace(' ', '').replace('_', '').replace('-', '') @cached_property - def _choices(self) -> Tuple[OptStr, OptStr]: + def _choices(self) -> tuple[OptStr, OptStr]: if not self.is_sub_parser: return None, None name = self.ast_obj.init_func_kwargs.get('name') @@ -339,7 +341,7 @@ def __lt__(self, other: ParamConverter) -> bool: return self.num < other.num @classmethod - def init_group(cls, parent: CollectionConverter, args: List[ParserArg]) -> ParamConverterGroup: + def init_group(cls, parent: CollectionConverter, args: list[ParserArg]) -> ParamConverterGroup: return ParamConverterGroup(parent, cls, [cls(arg, parent, i) for i, arg in enumerate(args)]) def format_lines(self, indent: int = 4) -> Iterator[str]: @@ -391,7 +393,7 @@ def _attr_name_candidates(self) -> Iterator[str]: # region Arg Processing @cached_property - def cmd_option_strs(self) -> List[str]: + def cmd_option_strs(self) -> list[str]: if not self.is_option: return [] long, short, plain = self._grouped_opt_strs @@ -407,7 +409,7 @@ def use_auto_long_opt_str(self) -> bool: def get_pos_args(self) -> Iterable[str]: return (repr(arg) for arg in self.cmd_option_strs) - def get_cls_and_kwargs(self) -> Tuple[str, BaseArgs]: + def get_cls_and_kwargs(self) -> tuple[str, BaseArgs]: kwargs = self.ast_obj.init_func_kwargs.copy() if (help_arg := kwargs.get('help')) and help_arg in self.ast_obj.get_tracked_refs('argparse', 'SUPPRESS', ()): kwargs.update({'hide': 'True', 'help': None}) @@ -462,7 +464,7 @@ def is_option(self) -> bool: # endregion @cached_property - def _grouped_opt_strs(self) -> Tuple[List[str], List[str], List[str]]: + def _grouped_opt_strs(self) -> tuple[list[str], list[str], list[str]]: option_strs = (literal_eval(opt) for opt in self.ast_obj.init_func_args) long, short, plain = [], [], [] for opt in option_strs: @@ -516,7 +518,7 @@ def format_all(self, indent: int = 4) -> Iterator[str]: class BaseArgs: help: OptStr = None - def _to_str(self, args: Tuple[str, ...], end_fields: List[str]) -> str: + def _to_str(self, args: tuple[str, ...], end_fields: list[str]) -> str: skip = set(end_fields) keys = [f.name for f in fields(self) if f.name not in skip] + end_fields kv_iter = ((key, getattr(self, key)) for key in keys) diff --git a/lib/cli_command_parser/conversion/visitor.py b/lib/cli_command_parser/conversion/visitor.py index 304b8f21..44ba3e68 100644 --- a/lib/cli_command_parser/conversion/visitor.py +++ b/lib/cli_command_parser/conversion/visitor.py @@ -2,18 +2,20 @@ import logging import re -from ast import NodeVisitor, AST, Assign, Call, For, Attribute, Name, Import, ImportFrom, expr +from ast import AST, Assign, Attribute, Call, For, Import, ImportFrom, Name, NodeVisitor, expr from collections import ChainMap, defaultdict from functools import partial, wraps -from typing import Iterator, Union, Callable, Collection, Tuple, List, Dict, Set +from typing import TYPE_CHECKING, Callable, Collection, Iterator, Union from .argparse_ast import AstArgumentParser from .utils import get_name_repr +if TYPE_CHECKING: + TrackedRefMap = dict['TrackedRef', set[str]] + __all__ = ['ScriptVisitor', 'TrackedRef'] log = logging.getLogger(__name__) -TrackedRefMap = Dict['TrackedRef', Set[str]] _NoCall = object() @@ -123,7 +125,7 @@ def visit_For(self, node: For): visit_AsyncFor = visit_For - def _visit_for_smart(self, node: For, loop_var: str, ele_names: List[str]): + def _visit_for_smart(self, node: For, loop_var: str, ele_names: list[str]): log.debug(f'Attempting smart for loop visit for {loop_var=} in {ele_names=}') refs = [ref for ref in (self.scopes.get(name) for name in ele_names) if ref] # log.debug(f' > Found {len(refs)=}, {len(ele_names)=}') @@ -141,7 +143,7 @@ def _visit_for_smart(self, node: For, loop_var: str, ele_names: List[str]): log.debug('Falling back to generic loop handling') self._visit_for_elements(node, loop_var, ele_names) - def _visit_for_elements(self, node: For, loop_var: str, ele_names: List[str]): + def _visit_for_elements(self, node: For, loop_var: str, ele_names: list[str]): visited_any = False for name in ele_names: if ref := self.scopes.get(name): @@ -218,7 +220,7 @@ def __eq__(self, other: TrackedRef) -> bool: return self.name == other.name and self.module == other.module -def imp_names(imp: Import | ImportFrom) -> Iterator[Tuple[str, str]]: +def imp_names(imp: Import | ImportFrom) -> Iterator[tuple[str, str]]: for alias in imp.names: name = alias.name as_name = alias.asname or name diff --git a/lib/cli_command_parser/core.py b/lib/cli_command_parser/core.py index beffa7f0..16c550a7 100644 --- a/lib/cli_command_parser/core.py +++ b/lib/cli_command_parser/core.py @@ -8,25 +8,24 @@ from __future__ import annotations from abc import ABC, ABCMeta -from typing import TYPE_CHECKING, Optional, Union, TypeVar, Callable, Iterable, Collection, Any, Mapping, Iterator -from typing import Dict, Tuple, List +from typing import TYPE_CHECKING, Any, Callable, Collection, Iterable, Iterator, Mapping, Optional, TypeVar, Union from warnings import warn from weakref import WeakSet from .command_parameters import CommandParameters -from .config import CommandConfig, DEFAULT_CONFIG +from .config import DEFAULT_CONFIG, CommandConfig from .exceptions import CommandDefinitionError from .metadata import ProgramMetadata if TYPE_CHECKING: - from .typing import Config, AnyConfig, CommandCls, CommandAny, OptStr + from .typing import AnyConfig, CommandAny, CommandCls, Config, OptStr -__all__ = ['CommandMeta', 'get_parent', 'get_config', 'get_params', 'get_metadata', 'get_top_level_commands'] + Bases = Union[tuple[type, ...], Iterable[type]] + Choices = Union[Mapping[str, Optional[str]], Collection[str]] + OptChoices = Optional[Choices] + T = TypeVar('T') -Bases = Union[Tuple[type, ...], Iterable[type]] -Choices = Union[Mapping[str, Optional[str]], Collection[str]] -OptChoices = Optional[Choices] -T = TypeVar('T') +__all__ = ['CommandMeta', 'get_parent', 'get_config', 'get_params', 'get_metadata', 'get_top_level_commands'] _NotSet = object() META_KEYS = {'prog', 'usage', 'description', 'epilog', 'doc_name'} @@ -69,7 +68,7 @@ class CommandMeta(ABCMeta, type): _commands = WeakSet() @classmethod - def __prepare__(mcs, name: str, bases: Bases, **kwargs) -> Dict[str, Any]: + def __prepare__(mcs, name: str, bases: Bases, **kwargs) -> dict[str, Any]: """Called before ``__new__`` and before evaluating the contents of a class.""" return { '_CommandMeta__params': None, # Prevent commands from inheriting parent params @@ -82,7 +81,7 @@ def __new__( mcs, name: str, bases: Bases, - namespace: Dict[str, Any], + namespace: dict[str, Any], *, choice: str = _NotSet, choices: Choices = None, @@ -141,7 +140,7 @@ def _from_parent(mcs, meth: Callable[[CommandCls], T], bases: Bases) -> Optional # region Config Methods @classmethod - def _prepare_config(mcs, bases: Bases, config: AnyConfig, kwargs: Dict[str, Any]) -> Config: + def _prepare_config(mcs, bases: Bases, config: AnyConfig, kwargs: dict[str, Any]) -> Config: if config is not None: if kwargs: raise CommandDefinitionError(f'Cannot combine {config=} with keyword config arguments={kwargs}') @@ -159,8 +158,8 @@ def _prepare_config(mcs, bases: Bases, config: AnyConfig, kwargs: Dict[str, Any] @classmethod def config(mcs, cls: CommandAny, default: T = None) -> Union[CommandConfig, T]: try: - return cls.__config # This attr is not overwritten for every subclass - except AttributeError: # This means that the Command and all of its parents have no custom config + return cls.__config # This attr is not overwritten for every subclass + except AttributeError: # This means that the Command and all of its parents have no custom config return default # endregion @@ -230,7 +229,7 @@ def _mro(cmd_cls): return cmd_cls, type.mro(cmd_cls)[1:-1] -def _choice_items(choice: OptStr, choices: OptChoices) -> Iterator[Tuple[OptStr, OptStr]]: +def _choice_items(choice: OptStr, choices: OptChoices) -> Iterator[tuple[OptStr, OptStr]]: if not choices: # Automatic use of the subcommand class name is handled by SubCommand.register_command when choice is None if choice is not None: # Allow an explicit None to be used to prevent registration @@ -260,7 +259,7 @@ def _no_choices_registered_warning(choice: OptStr, choices: OptChoices, cls, rea get_params = CommandMeta.params -def get_top_level_commands() -> List[CommandCls]: +def get_top_level_commands() -> list[CommandCls]: """ Returns a list of Command subclasses that are inferred to be direct subclasses of :class:`~commands.Command`. diff --git a/lib/cli_command_parser/documentation.py b/lib/cli_command_parser/documentation.py index e5d7e520..97cf59c2 100644 --- a/lib/cli_command_parser/documentation.py +++ b/lib/cli_command_parser/documentation.py @@ -12,18 +12,18 @@ from collections import defaultdict from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -from typing import TYPE_CHECKING, Iterable, Mapping, Any, List, Dict +from typing import TYPE_CHECKING, Any, Iterable, Mapping from .commands import Command from .context import Context -from .core import CommandMeta, get_params, get_parent, get_metadata -from .formatting.commands import get_formatter, NameFunc +from .core import CommandMeta, get_metadata, get_params, get_parent +from .formatting.commands import NameFunc, get_formatter from .formatting.restructured_text import MODULE_TEMPLATE, rst_header, rst_toc_tree if TYPE_CHECKING: - from .typing import Bool, PathLike, CommandCls, Strings + from .typing import Bool, CommandCls, PathLike, Strings - Commands = Dict[str, CommandCls] + Commands = dict[str, CommandCls] __all__ = ['render_script_rst', 'render_command_rst', 'load_commands', 'RstWriter'] log = logging.getLogger(__name__) @@ -98,7 +98,7 @@ def load_commands(path: PathLike, top_only: Bool = False, include_abc: Bool = Fa return commands -def filtered_commands(obj_map: Dict[str, Any], top_only: Bool = False, include_abc: Bool = False) -> Commands: +def filtered_commands(obj_map: dict[str, Any], top_only: Bool = False, include_abc: Bool = False) -> Commands: commands = {key: val for key, val in obj_map.items() if not key.startswith('__') and _is_command(val, include_abc)} if top_only: commands = top_level_commands(commands) @@ -276,7 +276,7 @@ def document_package( empty: Bool = False, caption: str = None, max_depth: int = 4, - ) -> List[str]: + ) -> list[str]: """ :param pkg_name: The name of the package to document :param pkg_path: The path to the package @@ -314,7 +314,7 @@ def document_package( ) return contents - def _generate_code_rsts(self, pkg_name: str, pkg_path: Path, subdir: str = None, max_depth: int = 4) -> List[str]: + def _generate_code_rsts(self, pkg_name: str, pkg_path: Path, subdir: str = None, max_depth: int = 4) -> list[str]: contents = [] for path in pkg_path.iterdir(): if path.is_dir(): diff --git a/lib/cli_command_parser/error_handling.py b/lib/cli_command_parser/error_handling.py index 65993ee1..038ce43f 100644 --- a/lib/cli_command_parser/error_handling.py +++ b/lib/cli_command_parser/error_handling.py @@ -21,7 +21,7 @@ import platform import sys from collections import ChainMap -from typing import Type, Callable, Union, Optional, Iterator, Dict +from typing import Callable, Iterator, Optional, Type, Union from .exceptions import CommandParserException @@ -33,8 +33,8 @@ class ErrorHandler: __slots__ = ('exc_handler_map',) - _exc_handler_map: Dict[Type[BaseException], Handler] = {} - exc_handler_map: Dict[Type[BaseException], Handler] + _exc_handler_map: dict[Type[BaseException], Handler] = {} + exc_handler_map: dict[Type[BaseException], Handler] def __init__(self): self.exc_handler_map = {} diff --git a/lib/cli_command_parser/formatting/params.py b/lib/cli_command_parser/formatting/params.py index 1e5410ca..02c704ee 100644 --- a/lib/cli_command_parser/formatting/params.py +++ b/lib/cli_command_parser/formatting/params.py @@ -8,23 +8,23 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Type, Callable, Iterator, Iterable, Tuple, Dict +from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Type -from ..config import SubcommandAliasHelpMode, CmdAliasMode +from ..config import CmdAliasMode, SubcommandAliasHelpMode from ..context import ctx from ..core import get_config -from ..parameters.base import BasePositional, BaseOption -from ..parameters.choice_map import ChoiceMap, Choice from ..parameters import ParamGroup, PassThru, TriFlag +from ..parameters.base import BaseOption, BasePositional +from ..parameters.choice_map import Choice, ChoiceMap from .restructured_text import RstTable -from .utils import format_help_entry, _should_add_default +from .utils import _should_add_default, format_help_entry if TYPE_CHECKING: from ..nargs import Nargs from ..parameters.option_strings import TriFlagOptionStrings - from ..typing import Bool, ParamOrGroup, OptStr + from ..typing import Bool, OptStr, ParamOrGroup -BoolFormatterMap = Dict[bool, Callable[[str], str]] + BoolFormatterMap = dict[bool, Callable[[str], str]] class ParamHelpFormatter: @@ -137,11 +137,11 @@ def rst_usage(self) -> str: usage = self.format_usage(include_meta=True, full=True) return f'``{usage}``' - def rst_row(self) -> Tuple[str, str]: + def rst_row(self) -> tuple[str, str]: """Returns a tuple of (usage, description)""" return self.rst_usage(), self.format_description(rst=True) - def rst_rows(self) -> Iterator[Tuple[str, str]]: + def rst_rows(self) -> Iterator[tuple[str, str]]: yield self.rst_row() # endregion @@ -215,7 +215,7 @@ def format_help(self, prefix: str = '', tw_offset: int = 0) -> str: alt_entry = format_help_entry(opts.alt_option_strs(), alt_desc, prefix, tw_offset, lpad=2 if alt_desc else 4) return f'{primary}\n{alt_entry}' - def rst_rows(self) -> Iterator[Tuple[str, str]]: + def rst_rows(self) -> Iterator[tuple[str, str]]: opts: TriFlagOptionStrings = self.param.option_strs for alt in (False, True): usage = ', '.join(f'``{part}``' for part in opts.option_strs(alt)) @@ -270,7 +270,7 @@ def rst_table(self) -> RstTable: table.add_rows(rows) return table - def _format_rst_rows(self) -> Iterator[Tuple[str, OptStr]]: + def _format_rst_rows(self) -> Iterator[tuple[str, OptStr]]: mode = ctx.config.cmd_alias_mode or SubcommandAliasHelpMode.ALIAS for choice_group in self.choice_groups: for choice, usage, description in choice_group.prepare(mode): @@ -327,7 +327,7 @@ def format(self, default_mode: CmdAliasMode, tw_offset: int = 0, prefix: str = ' for choice, usage, description in self.prepare(default_mode): yield format_help_entry((usage,), description, lpad=4, tw_offset=tw_offset, prefix=prefix) - def prepare(self, default_mode: CmdAliasMode) -> Iterator[Tuple[Choice, OptStr, OptStr]]: + def prepare(self, default_mode: CmdAliasMode) -> Iterator[tuple[Choice, OptStr, OptStr]]: """ Prepares the choice values and descriptions to use for each Choice in this group based on the configured alias mode. @@ -354,7 +354,7 @@ def prepare(self, default_mode: CmdAliasMode) -> Iterator[Tuple[Choice, OptStr, else: # Treat as a format string yield from self.prepare_aliases(mode) - def prepare_combined(self) -> Tuple[Choice, OptStr, OptStr]: + def prepare_combined(self) -> tuple[Choice, OptStr, OptStr]: """ Prepare this group's Choices for inclusion in help text / documentation by combining all aliases into a single entry. @@ -370,7 +370,7 @@ def prepare_combined(self) -> Tuple[Choice, OptStr, OptStr]: return first, usage, first.help - def prepare_aliases(self, format_str: str = 'Alias of: {choice}') -> Iterator[Tuple[Choice, OptStr, OptStr]]: + def prepare_aliases(self, format_str: str = 'Alias of: {choice}') -> Iterator[tuple[Choice, OptStr, OptStr]]: """ Prepare this group's Choices for inclusion in help text / documentation using an alternate description for aliases. @@ -402,7 +402,7 @@ def prepare_aliases(self, format_str: str = 'Alias of: {choice}') -> Iterator[Tu for choice_str in choice_strs: yield first, choice_str, format_str.format(choice=first_str, alias=choice_str, help=help_str) - def prepare_repeated(self) -> Iterator[Tuple[Choice, OptStr, OptStr]]: + def prepare_repeated(self) -> Iterator[tuple[Choice, OptStr, OptStr]]: """ Prepare this group's Choices for inclusion in help text / documentation with no modifications. Choices that are considered aliases are simply repeated as if they were not aliases. @@ -449,7 +449,7 @@ def format_description(self, rst: Bool = False, description: str = None) -> str: def _get_spacer(self) -> str: # TODO: Config option to set these chars OR to have them just be spaces if self.param.mutually_exclusive: - return '\u00A6 ' # BROKEN BAR + return '\u00a6 ' # BROKEN BAR elif self.param.mutually_dependent: return '\u2551 ' # BOX DRAWINGS DOUBLE VERTICAL else: @@ -509,6 +509,6 @@ def rst_table(self) -> RstTable: return table -def _pad_and_quote(description: str, rst: bool) -> Tuple[str, str]: +def _pad_and_quote(description: str, rst: bool) -> tuple[str, str]: """Returns a 2-tuple of ``(pad char, quote string)``""" return ' ' if description else '', '``' if rst else '' diff --git a/lib/cli_command_parser/formatting/restructured_text.py b/lib/cli_command_parser/formatting/restructured_text.py index 39d2ba26..41703386 100644 --- a/lib/cli_command_parser/formatting/restructured_text.py +++ b/lib/cli_command_parser/formatting/restructured_text.py @@ -7,12 +7,12 @@ from __future__ import annotations from itertools import starmap -from typing import TYPE_CHECKING, Union, Iterator, Iterable, Any, TypeVar, Sequence, Mapping, Dict, Tuple, List +from typing import TYPE_CHECKING, Any, Iterable, Iterator, Mapping, Sequence, TypeVar, Union from .utils import line_iter if TYPE_CHECKING: - from ..typing import OptStr, Bool, Strings + from ..typing import Bool, OptStr, Strings __all__ = ['rst_bar', 'rst_list_table', 'RstTable'] @@ -62,7 +62,7 @@ def spaced_rst_header(text: str, level: int = 1, before: bool = True) -> Iterato def _rst_directive( - directive: str, args: str = None, options: Dict[str, Any] = None, indent: int = 4, check: Bool = False + directive: str, args: str = None, options: dict[str, Any] = None, indent: int = 4, check: Bool = False ) -> Iterator[str]: yield f'.. {directive}:: {args}' if args else f'.. {directive}::' if options: @@ -73,7 +73,7 @@ def _rst_directive( def rst_directive( - directive: str, args: str = None, options: Dict[str, Any] = None, indent: int = 4, check: Bool = False + directive: str, args: str = None, options: dict[str, Any] = None, indent: int = 4, check: Bool = False ) -> str: return '\n'.join(_rst_directive(directive, args, options, indent, check)) @@ -100,7 +100,7 @@ def rst_toc_tree(name: str, content_fmt: str, contents: Strings, max_depth: int return '\n'.join(_rst_toc_tree(name, content_fmt, contents, max_depth, **kwargs)) -def rst_list_table(data: Dict[str, str], value_pad: int = 20) -> str: +def rst_list_table(data: dict[str, str], value_pad: int = 20) -> str: max_key = max(map(len, data)) max_val = max(map(len, data.values())) widths = f'{max_key} {max_val + value_pad}' @@ -252,7 +252,7 @@ def __str__(self) -> str: return '\n'.join(self.iter_build()) -def _widths(columns: Iterable[OptStr]) -> Tuple[bool, List[int]]: +def _widths(columns: Iterable[OptStr]) -> tuple[bool, list[int]]: widths = [] any_new_line = False for column in columns: diff --git a/lib/cli_command_parser/formatting/utils.py b/lib/cli_command_parser/formatting/utils.py index 2f573f73..0d996d88 100644 --- a/lib/cli_command_parser/formatting/utils.py +++ b/lib/cli_command_parser/formatting/utils.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Any, Collection, Sequence, Iterator, Iterable, List +from typing import TYPE_CHECKING, Any, Collection, Iterable, Iterator, Optional, Sequence from ..compat import WCTextWrapper from ..config import ShowDefaults @@ -14,7 +14,7 @@ from ..utils import _NotSet, wcswidth if TYPE_CHECKING: - from ..typing import Bool, Strs, OptStrs + from ..typing import Bool, OptStrs, Strs __all__ = ['format_help_entry', 'line_iter'] @@ -104,7 +104,7 @@ def _description_start_line(usage: Iterable[str], max_usage_width: int) -> int: return line -def _single_line_strs(lines: Strs) -> List[str]: +def _single_line_strs(lines: Strs) -> list[str]: if isinstance(lines, str): lines = (lines,) return [line for full_line in lines for line in full_line.splitlines()] @@ -138,7 +138,7 @@ def _should_add_default(default: Any, help_text: Optional[str], param_show_defau return bool(default) -def line_iter(*columns: Strs) -> Iterator[List[str, ...]]: +def line_iter(*columns: Strs) -> Iterator[list[str, ...]]: """More complicated than what would be necessary for just 2 columns, but this will scale to handle 3+""" exhausted = 0 column_count = len(columns) diff --git a/lib/cli_command_parser/inputs/numeric.py b/lib/cli_command_parser/inputs/numeric.py index 7ddcdff7..1b2ff6ad 100644 --- a/lib/cli_command_parser/inputs/numeric.py +++ b/lib/cli_command_parser/inputs/numeric.py @@ -8,9 +8,9 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Union, Optional +from typing import Optional, Union -from ..typing import Bool, NT, Number, NumType, RngType +from ..typing import NT, Bool, Number, NumType, RngType from .base import InputType from .exceptions import InputValidationError @@ -161,8 +161,8 @@ def __init__( ) self.snap = snap - self.min = self.type(min) if min is not None else min # for floats especially, such as a range like 0~1, this - self.max = self.type(max) if max is not None else max # helps to highlight the type in reprs + self.min = self.type(min) if min is not None else min # for floats especially, such as a range like 0~1, this + self.max = self.type(max) if max is not None else max # helps to highlight the type in reprs self.include_min = include_min self.include_max = include_max diff --git a/lib/cli_command_parser/inputs/patterns.py b/lib/cli_command_parser/inputs/patterns.py index 674c1ae3..b5e567d0 100644 --- a/lib/cli_command_parser/inputs/patterns.py +++ b/lib/cli_command_parser/inputs/patterns.py @@ -11,7 +11,7 @@ from abc import ABC from enum import Enum from fnmatch import translate -from typing import TypeVar, Union, Collection, Sequence, Pattern, Match, Tuple, Dict +from typing import Collection, Dict, Match, Pattern, Sequence, Tuple, TypeVar, Union from ..utils import MissingMixin from .base import InputType, T @@ -24,7 +24,7 @@ class PatternInput(InputType[T], ABC): __slots__ = ('patterns',) - patterns: Tuple[Pattern, ...] + patterns: tuple[Pattern, ...] def _pattern_strings(self, sort: bool = False) -> Sequence[str]: patterns = [p.pattern for p in self.patterns] @@ -45,11 +45,13 @@ def _describe_patterns(self) -> str: class RegexMode(MissingMixin, Enum): """The RegexMode for a given Regex input governs the type of value it returns during parsing.""" + # fmt: off STRING = 'string' #: Return the full original string if it matches a given pattern MATCH = 'match' #: Return a :ref:`Match ` object GROUP = 'group' #: Return the string from the specified capturing group GROUPS = 'groups' #: Return a tuple containing all captured groups, or specific captured groups DICT = 'dict' #: Return a dictionary containing all named capturing groups and their captured values + # fmt: on @classmethod def normalize(cls, value: Union[str, RegexMode, None], group, groups) -> RegexMode: diff --git a/lib/cli_command_parser/inputs/time.py b/lib/cli_command_parser/inputs/time.py index aa22fc84..23d781ba 100644 --- a/lib/cli_command_parser/inputs/time.py +++ b/lib/cli_command_parser/inputs/time.py @@ -17,15 +17,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from calendar import day_name, day_abbr, month_name, month_abbr -from datetime import datetime, date, time, timedelta +from calendar import day_abbr, day_name, month_abbr, month_name +from datetime import date, datetime, time, timedelta from enum import Enum from locale import LC_ALL, setlocale from threading import RLock -from typing import Union, Iterator, Collection, Sequence, Optional, TypeVar, Type, Literal, overload -from typing import Tuple, Dict +from typing import Collection, Iterator, Literal, Optional, Sequence, Type, TypeVar, Union, overload -from ..typing import T, Bool, Locale, TimeBound +from ..typing import Bool, Locale, T, TimeBound from ..utils import MissingMixin from .base import InputType from .exceptions import InputValidationError, InvalidChoiceError @@ -103,15 +102,17 @@ def fix_default(self, value: Union[str, T, None]) -> Optional[T]: class DTFormatMode(MissingMixin, Enum): + # fmt: off FULL = 'full' #: The full name of a given date/time unit ABBREVIATION = 'abbreviation' #: The abbreviation of a given date/time unit NUMERIC = 'numeric' #: The numeric representation of a given date/time unit NUMERIC_ISO = 'numeric_iso' #: The ISO numeric representation of a given date/time unit + # fmt: on class CalendarUnitInput(DTInput[Union[str, int]], ABC): __slots__ = ('full', 'abbreviation', 'numeric', 'out_format', 'out_locale') - _formats: Dict[DTFormatMode, Sequence[Union[str, int]]] + _formats: dict[DTFormatMode, Sequence[Union[str, int]]] _min_index: int = 0 def __init_subclass__(cls, min_index: int = 0, **kwargs): @@ -151,7 +152,7 @@ def __init__( if self.out_format not in self._formats: raise ValueError(f'Unsupported out_format={self.out_format} for {self.__class__.__name__} inputs') - def _values(self) -> Iterator[Tuple[int, str]]: + def _values(self) -> Iterator[tuple[int, str]]: if not self.full and not self.abbreviation: return min_index = self._min_index diff --git a/lib/cli_command_parser/metadata.py b/lib/cli_command_parser/metadata.py index c09efaf8..b1a0050d 100644 --- a/lib/cli_command_parser/metadata.py +++ b/lib/cli_command_parser/metadata.py @@ -9,15 +9,15 @@ from collections import defaultdict from functools import cached_property -from importlib.metadata import entry_points, EntryPoint, Distribution +from importlib.metadata import Distribution, EntryPoint, entry_points from inspect import getmodule from pathlib import Path from sys import modules from textwrap import dedent -from typing import TYPE_CHECKING, Any, Type, Callable, Optional, Union, Iterator, Tuple, Dict, Set +from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Type, Union from urllib.parse import urlparse -from .context import ctx, NoActiveContext +from .context import NoActiveContext, ctx if TYPE_CHECKING: from .typing import Bool, CommandType, OptStr @@ -58,7 +58,7 @@ def __set__(self, instance: ProgramMetadata, value: Union[str, Path, None]): def __repr__(self) -> str: return f'{self.__class__.__name__}({", ".join(f"{a}={v}" for a, v in self._attrs())})' - def _attrs(self) -> Iterator[Tuple[str, Any]]: + def _attrs(self) -> Iterator[tuple[str, Any]]: for base in self.__class__.mro()[:-1]: for attr in base.__slots__: # noqa if attr != 'name': @@ -186,7 +186,7 @@ def __repr__(self) -> str: # region Program Name Properties @cached_property - def _prog_and_src(self) -> Tuple[str, str]: + def _prog_and_src(self) -> tuple[str, str]: if prog := self.__dict__.get('prog'): return prog, 'class kwargs' return _prog_finder.normalize(self.path, self.parent, None, self.cmd_module, self.command) @@ -196,7 +196,7 @@ def prog(self) -> str: return self._prog_and_src[0] @cached_property - def _doc_prog_and_src(self) -> Tuple[str, str]: + def _doc_prog_and_src(self) -> tuple[str, str]: if prog := self.__dict__.get('prog'): return prog, 'class kwargs' return _prog_finder.normalize(self.path, self.parent, False, self.cmd_module, self.command) @@ -204,7 +204,7 @@ def _doc_prog_and_src(self) -> Tuple[str, str]: def get_prog(self, allow_sys_argv: Bool = None) -> str: return self._get_prog(allow_sys_argv)[0] - def _get_prog(self, allow_sys_argv: Bool = None) -> Tuple[str, str]: + def _get_prog(self, allow_sys_argv: Bool = None) -> tuple[str, str]: return self._prog_and_src if allow_sys_argv or allow_sys_argv is None else self._doc_prog_and_src @dynamic_metadata(inheritable=False) @@ -287,7 +287,7 @@ def _repr(obj, indent=0) -> str: class ProgFinder: @cached_property - def mod_obj_prog_map(self) -> Dict[str, Dict[str, str]]: + def mod_obj_prog_map(self) -> dict[str, dict[str, str]]: mod_obj_prog_map = defaultdict(dict) for entry_point in self._get_console_scripts(): module, obj = map(str.strip, entry_point.value.split(':', 1)) @@ -298,7 +298,7 @@ def mod_obj_prog_map(self) -> Dict[str, Dict[str, str]]: return mod_obj_prog_map @classmethod - def _get_console_scripts(cls) -> Tuple[EntryPoint, ...]: + def _get_console_scripts(cls) -> tuple[EntryPoint, ...]: try: return entry_points(group='console_scripts') # noqa except TypeError: # Python 3.8 or 3.9 @@ -311,7 +311,7 @@ def normalize( allow_sys_argv: Bool, cmd_module: str, cmd_name: str, - ) -> Tuple[OptStr, str]: + ) -> tuple[OptStr, str]: if ep_name := self._from_entry_point(cmd_module, cmd_name): return ep_name, 'entry_points' @@ -387,7 +387,7 @@ def _distributions(self) -> dict[str, Distribution]: # Note: Distribution.name was not added until 3.10, and it returns `self.metadata['Name']` return {dist.metadata['Name']: dist for dist in Distribution.discover()} - def _get_top_levels(self, dist_name: str, dist: Distribution) -> Set[str]: + def _get_top_levels(self, dist_name: str, dist: Distribution) -> set[str]: # dist_name = dist.metadata['Name'] # Distribution.name was not added until 3.10, and it returns this if (top_levels := self._dist_top_levels.get(dist_name)) is not None: return top_levels @@ -428,7 +428,7 @@ def _dist_for_obj_main(self, obj) -> Distribution | None: return dist return None - def get_urls(self, dist: Distribution) -> Dict[str, str]: + def get_urls(self, dist: Distribution) -> dict[str, str]: metadata = dist.metadata dist_name = metadata['Name'] if (urls := self._dist_urls.get(dist_name)) is not None: @@ -448,7 +448,7 @@ def get_urls(self, dist: Distribution) -> Dict[str, str]: _dist_finder = DistributionFinder() -def _path_and_globals(command: CommandType, path: Path = None) -> Tuple[Path, Dict[str, Any]]: +def _path_and_globals(command: CommandType, path: Path = None) -> tuple[Path, dict[str, Any]]: module = getmodule(command) # Returns the module object (obj.__module__ just provides the name of the module) if path is None: try: diff --git a/lib/cli_command_parser/nargs.py b/lib/cli_command_parser/nargs.py index f8c0ff1e..4e1fae0c 100644 --- a/lib/cli_command_parser/nargs.py +++ b/lib/cli_command_parser/nargs.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import Any, Union, Optional, Sequence, Collection, Iterable, Tuple, Set, FrozenSet +from typing import Any, Collection, FrozenSet, Iterable, Optional, Sequence, Set, Tuple, Union __all__ = ['Nargs', 'NargsValue', 'REMAINDER', 'nargs_min_and_max_sums'] @@ -179,7 +179,7 @@ def upper_bound(self) -> Union[int, float]: return self.max if self._has_upper_bound else float('inf') -def nargs_min_and_max_sums(nargs_objects: Iterable[Nargs]) -> Tuple[int, Union[int, float]]: +def nargs_min_and_max_sums(nargs_objects: Iterable[Nargs]) -> tuple[int, Union[int, float]]: min_sum, max_sum = 0, 0 iter_nargs = iter(nargs_objects) for obj in iter_nargs: diff --git a/lib/cli_command_parser/parameters/actions.py b/lib/cli_command_parser/parameters/actions.py index 603ecc63..ed2bd9cf 100644 --- a/lib/cli_command_parser/parameters/actions.py +++ b/lib/cli_command_parser/parameters/actions.py @@ -6,16 +6,16 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Iterator, Iterable, Callable, Union, Sequence, TypeVar, NoReturn, List +from typing import TYPE_CHECKING, Callable, Iterable, Iterator, NoReturn, Sequence, TypeVar, Union from ..context import ctx -from ..exceptions import BadArgument, MissingArgument, InvalidChoice, TooManyArguments, ParamUsageError, ParamConflict +from ..exceptions import BadArgument, InvalidChoice, MissingArgument, ParamConflict, ParamUsageError, TooManyArguments from ..inputs import InputType from ..nargs import Nargs from ..utils import _NotSet, camel_to_snake_case if TYPE_CHECKING: - from ..typing import CommandObj, Param, Bool, T_co + from ..typing import Bool, CommandObj, Param, T_co __all__ = [ 'ParamAction', @@ -123,7 +123,7 @@ def would_accept(self, value: str, combo: bool = False) -> bool: # region Backtracking - def get_maybe_poppable_counts(self) -> List[int]: + def get_maybe_poppable_counts(self) -> list[int]: """ :return: The indexes on which the parsed values may be split such that the remaining number of values will still be acceptable for the Parameter's nargs. @@ -318,7 +318,7 @@ def would_accept(self, value: str, combo: bool = False) -> bool: # region Backtracking - def get_maybe_poppable_counts(self) -> List[int]: + def get_maybe_poppable_counts(self) -> list[int]: """ :return: The indexes on which the parsed values may be split such that the remaining number of values will still be acceptable for the Parameter's nargs. @@ -525,7 +525,7 @@ class StoreAll(Store): # region Add Parsed Value / Constant Methods - def add_values(self, values: List[str], *, opt: str = None, combo: bool = False) -> Found: + def add_values(self, values: list[str], *, opt: str = None, combo: bool = False) -> Found: param = self.param ctx.record_action(param) diff --git a/lib/cli_command_parser/parameters/base.py b/lib/cli_command_parser/parameters/base.py index 10d4723c..ad904666 100644 --- a/lib/cli_command_parser/parameters/base.py +++ b/lib/cli_command_parser/parameters/base.py @@ -3,7 +3,6 @@ :author: Doug Skrypa """ -# pylint: disable=R0801 from __future__ import annotations @@ -12,24 +11,23 @@ from contextvars import ContextVar from functools import cached_property from itertools import chain -from typing import TYPE_CHECKING, Any, Type, Generic, Optional, Callable, Collection, Union, Iterator, TypeVar, overload -from typing import NoReturn, Dict, Tuple +from typing import TYPE_CHECKING, Any, Callable, Collection, Generic, Iterator, NoReturn, Type, TypeVar, Union, overload from ..annotations import get_descriptor_value_type -from ..config import CommandConfig, OptionNameMode, AllowLeadingDash, DEFAULT_CONFIG +from ..config import DEFAULT_CONFIG, AllowLeadingDash, CommandConfig, OptionNameMode from ..context import Context, ctx, get_current_context -from ..exceptions import ParameterDefinitionError, BadArgument, MissingArgument, InvalidChoice +from ..exceptions import BadArgument, InvalidChoice, MissingArgument, ParameterDefinitionError from ..inputs import InputType, normalize_input_type from ..inputs.choices import _ChoicesBase from ..inputs.exceptions import InputValidationError, InvalidChoiceError -from ..nargs import Nargs, REMAINDER -from ..typing import T_co, DefaultFunc, CommandMethod +from ..nargs import REMAINDER, Nargs +from ..typing import CommandMethod, DefaultFunc, T_co from ..utils import _NotSet from .option_strings import OptionStrings if TYPE_CHECKING: from ..formatting.params import ParamHelpFormatter - from ..typing import OptStr, OptStrs, Strings, Bool, CommandCls, CommandObj, CommandAny, Param, LeadingDash + from ..typing import Bool, CommandAny, CommandCls, CommandObj, LeadingDash, OptStr, OptStrs, Param, Strings from .actions import ParamAction from .groups import ParamGroup @@ -51,6 +49,7 @@ class ParamBase(ABC): :param hide: If ``True``, this parameter will not be included in usage / help messages. Defaults to ``False``. """ + # fmt: off # Class Attributes missing_hint: str = None #: Hint to provide if this param/group is missing # Instance Attributes @@ -61,6 +60,7 @@ class ParamBase(ABC): required: Bool #: Whether this param/group is required help: str #: The description for this param/group that will appear in ``--help`` text hide: Bool #: Whether this param/group should be hidden in ``--help`` text + # fmt: on def __init__(self, name: str = None, required: Bool = False, help: str = None, hide: Bool = False): # noqa self.__doc__ = help # Prevent this class's docstring from showing up for params in generated documentation @@ -81,7 +81,7 @@ def name(self) -> str: return self._default_name() @name.setter - def name(self, value: Optional[str]): + def name(self, value: Union[str, None]): if value is not None: self._name = value @@ -99,7 +99,7 @@ def __set_name__(self, command: CommandCls, name: str): def __hash__(self) -> int: return hash(self.__class__) ^ hash(self._attr_name) ^ hash(self._name) ^ hash(self.command) - def _ctx(self, command: CommandAny = None) -> Optional[Context]: + def _ctx(self, command: CommandAny = None) -> Union[Context, None]: if context := get_current_context(True): return context if command is None: @@ -176,18 +176,20 @@ class Parameter(ParamBase, Generic[T_co], ABC): # region Attributes & Initialization + # fmt: off # Class attributes - _action_map: Dict[str, Type[ParamAction]] = {} - _repr_attrs: Optional[Strings] = None #: Attributes to include in ``repr()`` output + _action_map: dict[str, Type[ParamAction]] = {} + _repr_attrs: Union[Strings, None] = None #: Attributes to include in ``repr()`` output # Instance attributes with class defaults metavar: str = None - nargs: Nargs # Expected to be set in subclasses - type: Optional[Callable[[str], T_co]] = None # Expected to be set in subclasses - allow_leading_dash: AllowLeadingDash = AllowLeadingDash.NUMERIC # Set in some subclasses + nargs: Nargs # Expected to be set in subclasses + type: Union[Callable[[str], T_co], None] = None # Expected to be set in subclasses + allow_leading_dash: AllowLeadingDash = AllowLeadingDash.NUMERIC # Set in some subclasses default = _NotSet default_cb: DefaultCallback | None = None show_default: Bool = None strict_default: Bool = False + # fmt: on def __init_subclass__(cls, repr_attrs: Strings = None, actions: Collection[Type[ParamAction]] = None, **kwargs): """ @@ -320,7 +322,7 @@ def __repr__(self) -> str: def get_const(self, opt_str: OptStr = None): return _NotSet - def get_env_const(self, value: str, env_var: str) -> Tuple[T_co, bool]: + def get_env_const(self, value: str, env_var: str) -> tuple[T_co, bool]: return _NotSet, False def prepare_value(self, value: str, short_combo: Bool = False, pre_action: Bool = False) -> T_co: @@ -338,7 +340,7 @@ def prepare_value(self, value: str, short_combo: Bool = False, pre_action: Bool except Exception as e: raise BadArgument(self, f'unable to cast {value=} to type={type_func!r}') from e - def validate(self, value: Optional[T_co], joined: Bool = False): + def validate(self, value: Union[T_co, None], joined: Bool = False): if not isinstance(value, str) or not value or not value[0] == '-': return elif self.allow_leading_dash == AllowLeadingDash.NUMERIC: @@ -360,12 +362,10 @@ def is_valid_arg(self, value: Any) -> bool: # region Parse Results / Argument Value Handling @overload - def __get__(self: Param, command: None, owner: CommandCls) -> Param: - ... + def __get__(self: Param, command: None, owner: CommandCls) -> Param: ... @overload - def __get__(self, command: CommandObj, owner: CommandCls) -> Optional[T_co]: - ... + def __get__(self, command: CommandObj, owner: CommandCls) -> Union[T_co, None]: ... def __get__(self, command, owner): if command is None: @@ -545,7 +545,7 @@ def __init__(self, default: AllowLeadingDash = AllowLeadingDash.NUMERIC): def __set_name__(self, owner, name: str): self.name = name - def __get__(self, instance: Optional[Parameter], owner) -> Union[AllowLeadingDash, AllowLeadingDashProperty]: + def __get__(self, instance: Union[Parameter, None], owner) -> Union[AllowLeadingDash, AllowLeadingDashProperty]: if instance is None: return self return instance.__dict__.get(self.name, self.default) diff --git a/lib/cli_command_parser/parameters/choice_map.py b/lib/cli_command_parser/parameters/choice_map.py index 6b09fd2f..7938719d 100644 --- a/lib/cli_command_parser/parameters/choice_map.py +++ b/lib/cli_command_parser/parameters/choice_map.py @@ -7,12 +7,12 @@ from __future__ import annotations from functools import partial -from string import whitespace, printable -from typing import Type, TypeVar, Generic, Optional, Callable, Union, Collection, Mapping, NoReturn, Dict +from string import printable, whitespace from types import MethodType +from typing import Callable, Collection, Generic, Mapping, NoReturn, Optional, Type, TypeVar, Union from ..context import ctx -from ..exceptions import ParameterDefinitionError, BadArgument, InvalidChoice, CommandDefinitionError +from ..exceptions import BadArgument, CommandDefinitionError, InvalidChoice, ParameterDefinitionError from ..formatting.utils import format_help_entry from ..nargs import Nargs from ..typing import Bool, CommandCls, CommandObj @@ -78,7 +78,7 @@ class ChoiceMap(BasePositional[str], Generic[T], actions=(Concatenate,)): _choice_validation_exc = ParameterDefinitionError _default_title: str = 'Choices' nargs = Nargs('+') - choices: Dict[str, Choice[T]] + choices: dict[str, Choice[T]] title: OptStr description: OptStr @@ -135,7 +135,11 @@ def register_choice(self, choice: str, target: T = _NotSet, help: str = None): self._register_choice(choice, target, help) def _register_choice( - self, choice: OptStr, target: Optional[T] = _NotSet, help: str = None, local: bool = False # noqa + self, + choice: OptStr, + target: Optional[T] = _NotSet, + help: str = None, # noqa + local: bool = False, ): try: existing = self.choices[choice] @@ -256,7 +260,11 @@ def register_command(self, choice: OptStr, command: CommandCls, help: OptStr) -> return command def register( - self, command_or_choice: Union[str, CommandCls] = None, *, choice: str = None, help: str = None # noqa + self, + command_or_choice: Union[str, CommandCls] = None, + *, + choice: str = None, + help: str = None, # noqa ) -> Callable[[CommandCls], CommandCls]: """ Class decorator version of :meth:`.register_command`. Registers the wrapped :class:`.Command` as the @@ -297,7 +305,11 @@ class Action(ChoiceMap[MethodType], title='Actions'): """ def register_action( - self, choice: OptStr, method: MethodType, help: str = None, default: Bool = False # noqa + self, + choice: OptStr, + method: MethodType, + help: str = None, # noqa + default: Bool = False, ) -> MethodType: if help is None: try: diff --git a/lib/cli_command_parser/parameters/groups.py b/lib/cli_command_parser/parameters/groups.py index bb342c9b..86d528ba 100644 --- a/lib/cli_command_parser/parameters/groups.py +++ b/lib/cli_command_parser/parameters/groups.py @@ -7,11 +7,11 @@ from __future__ import annotations from itertools import count -from typing import TYPE_CHECKING, Optional, Iterable, Iterator, Tuple, List +from typing import TYPE_CHECKING, Iterable, Iterator, Optional from ..context import ctx -from ..exceptions import ParameterDefinitionError, CommandDefinitionError, ParamConflict, ParamsMissing -from .base import ParamBase, BasePositional, BaseOption, _group_stack +from ..exceptions import CommandDefinitionError, ParamConflict, ParameterDefinitionError, ParamsMissing +from .base import BaseOption, BasePositional, ParamBase, _group_stack from .pass_thru import PassThru if TYPE_CHECKING: @@ -45,7 +45,7 @@ class ParamGroup(ParamBase): _num: int description: Optional[str] - members: List[ParamOrGroup] + members: list[ParamOrGroup] mutually_exclusive: Bool = False mutually_dependent: Bool = False @@ -177,7 +177,7 @@ def register_all(self, params: Iterable[ParamOrGroup]): # region Argument Handling - def _categorize_params(self) -> Tuple[ParamList, ParamList]: + def _categorize_params(self) -> tuple[ParamList, ParamList]: """Called after parsing to group this group's members by whether they were provided or not.""" provided = [] missing = [] @@ -238,7 +238,7 @@ def validate(self): if req_missing := [p for p in missing if p.required]: raise ParamsMissing(req_missing) - def _classify_required(self) -> Tuple[bool, bool]: + def _classify_required(self) -> tuple[bool, bool]: """Returns a tuple of (req_any, req_all)""" if self.mutually_dependent and (self.required or any(p.required for p in self.members)): return True, True diff --git a/lib/cli_command_parser/parameters/option_strings.py b/lib/cli_command_parser/parameters/option_strings.py index 2073f695..6b27da68 100644 --- a/lib/cli_command_parser/parameters/option_strings.py +++ b/lib/cli_command_parser/parameters/option_strings.py @@ -6,9 +6,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Collection, Union, Iterator, List, Set, Tuple +from typing import TYPE_CHECKING, Collection, Iterator, Optional, Union -from ..config import OptionNameMode, DEFAULT_CONFIG +from ..config import DEFAULT_CONFIG, OptionNameMode from ..exceptions import ParameterDefinitionError from ..utils import _NotSet @@ -23,10 +23,10 @@ class OptionStrings: __slots__ = ('name_mode', '_long', '_short', 'combinable', '_display_long') name_mode: Optional[OptionNameMode] - combinable: Set[str] - _display_long: Set[str] - _long: Set[str] - _short: Set[str] + combinable: set[str] + _display_long: set[str] + _long: set[str] + _short: set[str] def __init__(self, option_strs: Collection[str], name_mode: Union[OptionNameMode, str, None] = _NotSet): self.name_mode = OptionNameMode(name_mode) if name_mode is not _NotSet else None @@ -69,19 +69,19 @@ def update(self, name: str): if mode_val & 8: # OptionNameMode.BOTH_UNDERSCORE = OptionNameMode.DASH | 4 | 8 self._display_long.add(option) - def get_sets(self) -> Tuple[Set[str], Set[str]]: + def get_sets(self) -> tuple[set[str], set[str]]: return self._long, self._short @property - def long(self) -> List[str]: + def long(self) -> list[str]: return sorted(self._long, key=_options_sort_key) @property - def short(self) -> List[str]: + def short(self) -> list[str]: return sorted(self._short, key=_options_sort_key) @property - def display_long(self) -> List[str]: + def display_long(self) -> list[str]: return sorted(self._display_long, key=_options_sort_key) def get_usage_opt(self) -> str: @@ -100,7 +100,7 @@ class TriFlagOptionStrings(OptionStrings): __slots__ = ('_alt_prefix', '_alt_long', '_alt_short') _alt_prefix: Optional[str] _alt_short: Optional[str] - _alt_long: Union[Set[str], Tuple[str]] + _alt_long: Union[set[str], tuple[str]] def has_long(self) -> Bool: """Whether any primary / non-alternate long option strings were defined""" @@ -137,7 +137,7 @@ def update_alts(self, name: str): self._display_long.add(option) @property - def alt_allowed(self) -> Set[str]: + def alt_allowed(self) -> set[str]: allowed = set(self._alt_long) if self._alt_short: allowed.add(self._alt_short) @@ -149,11 +149,11 @@ def alt_allowed(self) -> Set[str]: # return [opt for opt in self.long if opt not in self._alt_long] @property - def display_long_primary(self) -> List[str]: + def display_long_primary(self) -> list[str]: return [opt for opt in self.display_long if opt not in self._alt_long] @property - def short_primary(self) -> List[str]: + def short_primary(self) -> list[str]: return [opt for opt in self.short if opt != self._alt_short] # @property @@ -161,11 +161,11 @@ def short_primary(self) -> List[str]: # return sorted(self._alt_long, key=_options_sort_key) @property - def display_long_alt(self) -> List[str]: + def display_long_alt(self) -> list[str]: return [opt for opt in self.display_long if opt in self._alt_long] @property - def short_alt(self) -> List[str]: + def short_alt(self) -> list[str]: return [self._alt_short] if self._alt_short else [] def primary_option_strs(self) -> Iterator[str]: @@ -195,12 +195,12 @@ def _options_sort_key(opt: str): return -len(opt), opt -def _split_options(opt_strs: Collection[str]) -> Tuple[Set[str], Set[str]]: +def _split_options(opt_strs: Collection[str]) -> tuple[set[str], set[str]]: """Split long and short option strings and ensure that all of the provided option strings are valid.""" long_opts, short_opts, bad_opts, bad_short = set(), set(), [], [] for opt in opt_strs: - if not opt: # Ignore None / empty strings / etc - continue # Only raise an exception if invalid values that were intended to be used were provided + if not opt: # Ignore None / empty strings / etc + continue # Only raise an exception if invalid values that were intended to be used were provided elif not 0 < opt.count('-', 0, 3) < 3 or opt.endswith('-') or '=' in opt: bad_opts.append(opt) elif opt.startswith('--'): diff --git a/lib/cli_command_parser/parameters/options.py b/lib/cli_command_parser/parameters/options.py index 87640168..03f423a8 100644 --- a/lib/cli_command_parser/parameters/options.py +++ b/lib/cli_command_parser/parameters/options.py @@ -9,19 +9,19 @@ import logging from abc import ABC from functools import partial, update_wrapper -from typing import TYPE_CHECKING, Any, Optional, Callable, Union, TypeVar, NoReturn, Literal, Tuple +from typing import TYPE_CHECKING, Any, Callable, Literal, NoReturn, Optional, TypeVar, Union -from ..exceptions import ParameterDefinitionError, BadArgument, CommandDefinitionError, ParamUsageError, ParserExit +from ..exceptions import BadArgument, CommandDefinitionError, ParameterDefinitionError, ParamUsageError, ParserExit from ..inputs import normalize_input_type from ..nargs import Nargs, NargsValue from ..typing import T_co, TypeFunc from ..utils import _NotSet, str_to_bool -from .actions import Store, StoreConst, Append, AppendConst, Count -from .base import BaseOption, AllowLeadingDashProperty +from .actions import Append, AppendConst, Count, Store, StoreConst +from .base import AllowLeadingDashProperty, BaseOption from .option_strings import TriFlagOptionStrings if TYPE_CHECKING: - from ..typing import Bool, ChoicesType, InputTypeFunc, CommandCls, CommandObj, OptStr, LeadingDash, CommandMethod + from ..typing import Bool, ChoicesType, CommandCls, CommandMethod, CommandObj, InputTypeFunc, LeadingDash, OptStr __all__ = [ 'Option', @@ -188,7 +188,7 @@ def __init__( def register_default_cb(self, method): raise ParameterDefinitionError(f'{self.__class__.__name__}s do not support default callback methods') - def get_env_const(self, value: str, env_var: str) -> Tuple[Union[TC, TD], bool]: + def get_env_const(self, value: str, env_var: str) -> tuple[Union[TC, TD], bool]: try: parsed = self.type(value) except Exception as e: @@ -241,12 +241,12 @@ class TriFlag(BaseOption[Union[TD, TC, TA]], ABC, actions=(StoreConst, AppendCon option_strs: TriFlagOptionStrings alt_help: OptStr = None default: TD - consts: Tuple[TC, TA] + consts: tuple[TC, TA] def __init__( self, *option_strs: str, - consts: Tuple[TC, TA] = (True, False), + consts: tuple[TC, TA] = (True, False), alt_prefix: str = None, alt_long: str = None, alt_short: str = None, @@ -304,7 +304,7 @@ def get_const(self, opt_str: OptStr = None) -> Union[TC, TA]: else: return self.consts[0] - def get_env_const(self, value: str, env_var: str) -> Tuple[Union[TC, TA, TD], bool]: + def get_env_const(self, value: str, env_var: str) -> tuple[Union[TC, TA, TD], bool]: try: parsed = self.type(value) except Exception as e: diff --git a/lib/cli_command_parser/parse_tree.py b/lib/cli_command_parser/parse_tree.py index 7d4f9bc8..d4718169 100644 --- a/lib/cli_command_parser/parse_tree.py +++ b/lib/cli_command_parser/parse_tree.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union, Optional, Collection, Iterable, Iterator, MutableMapping, Dict, Set, Tuple +from typing import TYPE_CHECKING, Collection, Iterable, Iterator, MutableMapping, Optional, Union from .exceptions import AmbiguousParseTree from .nargs import nargs_min_and_max_sums @@ -14,7 +14,7 @@ from .nargs import Nargs from .parameters.base import BasePositional from .parameters.choice_map import Choice - from .typing import OptStr, CommandCls + from .typing import CommandCls, OptStr __all__ = ['PosNode'] @@ -60,7 +60,7 @@ def __hash__(self) -> int: class PosNode(MutableMapping[Word, 'PosNode']): __slots__ = ('links', 'param', 'parent', 'target', 'word', '_any_word', '_any_node') - links: Dict[Word, PosNode] + links: dict[Word, PosNode] param: Optional[BasePositional] parent: Optional[PosNode] target: Target @@ -79,7 +79,7 @@ def __init__( except TypeError: # parent was None pass - def link_params(self, recursive: bool = False) -> Set[BasePositional]: + def link_params(self, recursive: bool = False) -> set[BasePositional]: return set(self._link_params(recursive)) def _link_params(self, recursive: bool = False) -> Iterator[BasePositional]: @@ -89,7 +89,7 @@ def _link_params(self, recursive: bool = False) -> Iterator[BasePositional]: for node in self.values(): yield from node._link_params(_has_upper_bound(node)) - def nargs_min_and_max(self) -> Tuple[int, Union[int, float]]: + def nargs_min_and_max(self) -> tuple[int, Union[int, float]]: return nargs_min_and_max_sums(p.nargs for p in self.link_params(True)) # region AnyWord Methods @@ -134,7 +134,7 @@ def _create_child(self) -> PosNode: # region Introspection @property - def raw_path(self) -> Tuple[Word, ...]: + def raw_path(self) -> tuple[Word, ...]: word = self.word if not word: return () @@ -325,17 +325,17 @@ def _has_upper_bound(node) -> bool: return True -def process_params(command: CommandCls, nodes: Iterable[PosNode], params: Iterable[BasePositional]) -> Set[PosNode]: +def process_params(command: CommandCls, nodes: Iterable[PosNode], params: Iterable[BasePositional]) -> set[PosNode]: for param in params: nodes = process_param(command, nodes, param) return nodes -def process_param(command: CommandCls, nodes: Iterable[PosNode], param: BasePositional) -> Set[PosNode]: +def process_param(command: CommandCls, nodes: Iterable[PosNode], param: BasePositional) -> set[PosNode]: # At each step, the number of branches grows try: - choices: Dict[OptStr, Choice] = param.choices # noqa + choices: dict[OptStr, Choice] = param.choices # noqa except AttributeError: # It was not a ChoiceMap param pass else: diff --git a/lib/cli_command_parser/parser.py b/lib/cli_command_parser/parser.py index 0d40e1ab..c4c8b37d 100644 --- a/lib/cli_command_parser/parser.py +++ b/lib/cli_command_parser/parser.py @@ -9,20 +9,27 @@ import logging from collections import deque from os import environ -from typing import TYPE_CHECKING, Optional, Iterable, Deque, List +from typing import TYPE_CHECKING, Deque, Iterable, Optional -from .core import get_parent from .context import ActionPhase, Context -from .exceptions import UsageError, ParamUsageError, NoSuchOption, MissingArgument, ParamsMissing -from .exceptions import Backtrack, NextCommand +from .core import get_parent +from .exceptions import ( + Backtrack, + MissingArgument, + NextCommand, + NoSuchOption, + ParamsMissing, + ParamUsageError, + UsageError, +) from .nargs import REMAINDER, nargs_min_and_max_sums +from .parameters.base import BaseOption, BasePositional, Parameter from .parse_tree import PosNode -from .parameters.base import Parameter, BasePositional, BaseOption if TYPE_CHECKING: from .command_parameters import CommandParameters from .config import CommandConfig - from .typing import CommandType, OptStr, Bool + from .typing import Bool, CommandType, OptStr __all__ = ['CommandParser', 'parse_args_and_get_next_cmd'] log = logging.getLogger(__name__) @@ -37,9 +44,9 @@ class CommandParser: arg_deque: Optional[Deque[str]] config: CommandConfig - deferred: Optional[List[str]] + deferred: Optional[list[str]] params: CommandParameters - positionals: List[BasePositional] + positionals: list[BasePositional] _last: Optional[Parameter] def __init__(self, ctx: Context, params: CommandParameters, config: CommandConfig): @@ -374,7 +381,7 @@ def _finalize_consume(self, param: Parameter, value: OptStr, found: int, exc: Op parse_args_and_get_next_cmd = CommandParser.parse_args_and_get_next_cmd -def _to_pop(positionals: Iterable[BasePositional], can_pop: List[int], available: int, req_mod: int = 0) -> int: +def _to_pop(positionals: Iterable[BasePositional], can_pop: list[int], available: int, req_mod: int = 0) -> int: if not can_pop: return 0 diff --git a/lib/cli_command_parser/testing.py b/lib/cli_command_parser/testing.py index b3157191..553a2f13 100644 --- a/lib/cli_command_parser/testing.py +++ b/lib/cli_command_parser/testing.py @@ -10,12 +10,12 @@ import sys from contextlib import AbstractContextManager, contextmanager from difflib import unified_diff -from io import StringIO, BytesIO +from io import BytesIO, StringIO from pathlib import Path from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Any, Iterable, Type, Union, Callable, IO, ContextManager, Dict, List, Tuple +from typing import IO, TYPE_CHECKING, Any, Callable, ContextManager, Dict, Iterable, List, Tuple, Type, Union from unittest import TestCase -from unittest.mock import Mock, seal, patch +from unittest.mock import Mock, patch, seal from .commands import Command from .context import Context @@ -235,7 +235,7 @@ def format_diff(a: str, b: str, name_a: str = 'expected', name_b: str = ' actua return sio.getvalue() -def format_dict_diff(a: Dict[str, Any], b: Dict[str, Any]) -> str: +def format_dict_diff(a: dict[str, Any], b: dict[str, Any]) -> str: formatted_a = [] formatted_b = [] for key in sorted(set(a) | set(b)): diff --git a/lib/cli_command_parser/utils.py b/lib/cli_command_parser/utils.py index a980de87..5332d1d9 100644 --- a/lib/cli_command_parser/utils.py +++ b/lib/cli_command_parser/utils.py @@ -6,11 +6,11 @@ from __future__ import annotations -from enum import Flag, EnumMeta +from enum import EnumMeta, Flag from inspect import isawaitable from shutil import get_terminal_size from time import monotonic -from typing import Any, Callable, TypeVar, Awaitable, List +from typing import Any, Awaitable, Callable, TypeVar try: from enum import CONFORM @@ -139,7 +139,7 @@ def _missing_str(cls, value: str) -> FlagEnum: raise KeyError - def _decompose(self) -> List[FlagEnum]: + def _decompose(self) -> list[FlagEnum]: if self._name_ is None or '|' in self._name_: # | check is for 3.11 where pseudo-members are assigned names val = self._value_ return sorted(mem for mem in self.__class__ if mem._value_ & val == mem._value_) # noqa diff --git a/pyproject.toml b/pyproject.toml index 2421b6b4..16cdff84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,73 @@ -[tool.blue] +[tool.ruff] +# https://docs.astral.sh/ruff/configuration/ +exclude = [ + '.bzr', + '.direnv', + '.eggs', + '.git', + '.git-rewrite', + '.hg', + '.ipynb_checkpoints', + '.mypy_cache', + '.nox', + '.pants.d', + '.pyenv', + '.pytest_cache', + '.pytype', + '.ruff_cache', + '.svn', + '.tox', + '.venv', + '.vscode', + '__pypackages__', + '_build', + 'buck-out', + 'build', + 'dist', + 'node_modules', + 'site-packages', + 'venv', +] line-length = 120 -include = '\.pyi?$' -target-version = ['py38', 'py39', 'py310'] -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - | __pycache__ - | .*\.ya?ml$ - | .*\.toml$ -)/ -''' -force-exclude = '''.*?(?:\.ya?ml|Makefile|\.toml)$''' +indent-width = 4 +target-version = 'py38' +show-fixes = true + + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ['E4', 'E7', 'E9', 'F', 'W', 'I001'] +ignore = [ + 'E402', # module level import not at top of file + 'F401', # imported but unused + 'F841', # local variable is assigned to but never used +# 'W503', # line break before binary operator (conflicts with Black) + 'W605', # invalid escape sequence +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ['ALL'] +unfixable = [] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = '^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$' + + +[tool.ruff.format] +quote-style = 'single' +indent-style = 'space' +skip-magic-trailing-comma = false # Like Black, respect magic trailing commas. +line-ending = 'lf' + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# This is currently disabled by default, but it is planned for this to be opt-out in the future. +docstring-code-format = false +# Set the line length limit used when formatting code snippets in docstrings. +# This only has an effect when the `docstring-code-format` setting is enabled. +docstring-code-line-length = 'dynamic' + + +[tool.ruff.lint.isort] +combine-as-imports = true diff --git a/readme.rst b/readme.rst index 590db933..dd15fd19 100644 --- a/readme.rst +++ b/readme.rst @@ -1,7 +1,7 @@ CLI Command Parser ################## -|downloads| |py_version| |coverage_badge| |build_status| |Blue| +|downloads| |py_version| |coverage_badge| |build_status| |Ruff| .. |py_version| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20-blue :target: https://pypi.org/project/cli-command-parser/ @@ -12,8 +12,8 @@ CLI Command Parser .. |build_status| image:: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml/badge.svg :target: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml -.. |Blue| image:: https://img.shields.io/badge/code%20style-blue-blue.svg - :target: https://blue.readthedocs.io/ +.. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://docs.astral.sh/ruff/ .. |downloads| image:: https://img.shields.io/pypi/dm/cli-command-parser :target: https://pypistats.org/packages/cli-command-parser diff --git a/requirements-dev.txt b/requirements-dev.txt index 1109d6d2..f9c23e0a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ -e .[wcwidth,conversion] -r docs/requirements.txt pre-commit +ruff # QoL ipython diff --git a/tests/test_conversion/test_convert_argparse.py b/tests/test_conversion/test_convert_argparse.py index 1295e43a..6d20b614 100644 --- a/tests/test_conversion/test_convert_argparse.py +++ b/tests/test_conversion/test_convert_argparse.py @@ -9,17 +9,28 @@ from unittest import main from unittest.mock import Mock, patch -from cli_command_parser.conversion.argparse_ast import Script, AstArgumentParser, AstCallable -from cli_command_parser.conversion.argparse_ast import AddVisitedChild, visit_func +from cli_command_parser.conversion.argparse_ast import ( + AddVisitedChild, + AstArgumentParser, + AstCallable, + Script, + visit_func, +) from cli_command_parser.conversion.argparse_utils import ArgumentParser, SubParsersAction -from cli_command_parser.conversion.command_builder import Converter, ParserConverter, ParamConverter, convert_script -from cli_command_parser.conversion.command_builder import ConversionError -from cli_command_parser.conversion.utils import get_name_repr, collection_contents -from cli_command_parser.conversion.visitor import ScriptVisitor, TrackedRefMap, TrackedRef +from cli_command_parser.conversion.command_builder import ( + ConversionError, + Converter, + ParamConverter, + ParserConverter, + convert_script, +) +from cli_command_parser.conversion.utils import collection_contents, get_name_repr +from cli_command_parser.conversion.visitor import ScriptVisitor, TrackedRef from cli_command_parser.testing import ParserTest, RedirectStreams if TYPE_CHECKING: from cli_command_parser.conversion.argparse_ast import InitNode + from cli_command_parser.conversion.visitor import TrackedRefMap PACKAGE = 'cli_command_parser.conversion' @@ -397,7 +408,7 @@ def test_utils_bad_types(self): def test_get_name_repr_call(self): node = ast.parse('foo()').body[0] self.assertEqual('foo', get_name_repr(node.value)) # noqa # Tests the isinstance(node, Call) line - self.assertEqual('foo()', get_name_repr(node)) # noqa # Tests the isinstance(node, AST) line + self.assertEqual('foo()', get_name_repr(node)) # noqa # Tests the isinstance(node, AST) line class AstCallableTest(ParserTest):