Skip to content

Commit

Permalink
Merge pull request #62 from dskrypa/dev
Browse files Browse the repository at this point in the history
Switched from Blue to Ruff and updated some basic type annotations
  • Loading branch information
dskrypa authored May 11, 2024
2 parents a7b53c7 + 4bac88d commit da30f92
Show file tree
Hide file tree
Showing 37 changed files with 416 additions and 327 deletions.
15 changes: 0 additions & 15 deletions .flake8

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dist/
.coverage
coverage.xml
tmp_*/
.ruff_cache/

# Ignore all directories/files under docs/ unless excluded here
docs/*/
Expand Down
20 changes: 13 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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]
6 changes: 3 additions & 3 deletions docs/_src/index.rst
Original file line number Diff line number Diff line change
@@ -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/
Expand All @@ -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
Expand Down
46 changes: 24 additions & 22 deletions lib/cli_command_parser/command_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -83,15 +85,15 @@ 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
except AttributeError:
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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}:
Expand Down
18 changes: 8 additions & 10 deletions lib/cli_command_parser/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 3 additions & 2 deletions lib/cli_command_parser/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
characters.
"""

from __future__ import annotations

from textwrap import TextWrapper
from typing import List

from .utils import wcswidth

Expand All @@ -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]
Expand Down
22 changes: 15 additions & 7 deletions lib/cli_command_parser/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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',
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -104,13 +106,15 @@ 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
# & 4 -> display options set
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:
Expand Down Expand Up @@ -153,9 +157,11 @@ class that is registered as a subcommand / subclass of another Command) should b
``Alias of: <first choice/alias value>``.
"""

# 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]
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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}
Expand Down
Loading

0 comments on commit da30f92

Please sign in to comment.