Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints #27

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:

strategy:
matrix:
click-version: ["4.1", "5.1", "6.7", "7.1.2", "8.1.6"]
click-version: ["8.1.3", "8.1.7", "7.1.2", "6.7", "5.1"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]

steps:
Expand All @@ -20,10 +20,15 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install '.[test']
pip install '.[test]'
pip install click==${{ matrix.click-version }}
- name: Test with pytest
run: pytest
- name: Check with flake8
run: flake8 click_default_group.py test.py -v --show-source
- name: Check type hints
if: ${{ startsWith( matrix.click-version, '8.' ) }}
run: python3 -m mypy --follow-imports=silent --python-version "${{ matrix.python-version }}" --strict --implicit-reexport -- click_default_group.py

build:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
Expand Down
97 changes: 65 additions & 32 deletions click_default_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ def bar():
bar

"""

import typing as t
import warnings

import click


__all__ = ['DefaultGroup']
__version__ = '1.2.4'

Expand All @@ -61,77 +62,109 @@ class DefaultGroup(click.Group):

"""

def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
# To resolve as the default command.
if not kwargs.get('ignore_unknown_options', True):
raise ValueError('Default group accepts unknown options')
self.ignore_unknown_options = True
self.default_cmd_name = kwargs.pop('default', None)
self.default_if_no_args = kwargs.pop('default_if_no_args', False)
super(DefaultGroup, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def set_default_command(self, command):
def set_default_command(self, command: t.Any) -> None:
"""Sets a command function as the default command."""
cmd_name = command.name
self.add_command(command)
self.default_cmd_name = cmd_name

def parse_args(self, ctx, args):
def parse_args(self, ctx: click.core.Context, args: t.List[str]) -> t.List[str]:
if not args and self.default_if_no_args:
args.insert(0, self.default_cmd_name)
return super(DefaultGroup, self).parse_args(ctx, args)
return super().parse_args(ctx, args)

def get_command(self, ctx, cmd_name):
def get_command(
self, ctx: click.core.Context, cmd_name: str
) -> t.Optional[click.core.Command]:
if cmd_name not in self.commands:
# No command name matched.
ctx.arg0 = cmd_name
ctx.arg0 = cmd_name # type: ignore
cmd_name = self.default_cmd_name
return super(DefaultGroup, self).get_command(ctx, cmd_name)
return super().get_command(ctx, cmd_name)

def resolve_command(self, ctx, args):
base = super(DefaultGroup, self)
cmd_name, cmd, args = base.resolve_command(ctx, args)
if hasattr(ctx, 'arg0'):
def resolve_command(
self, ctx: click.core.Context, args: t.List[str]
) -> t.Tuple[t.Optional[str], t.Optional[click.core.Command], t.List[str]]:
cmd_name, cmd, args = super().resolve_command(ctx, args)
if cmd and hasattr(ctx, 'arg0'):
args.insert(0, ctx.arg0)
cmd_name = cmd.name
return cmd_name, cmd, args

def format_commands(self, ctx, formatter):
formatter = DefaultCommandFormatter(self, formatter, mark='*')
return super(DefaultGroup, self).format_commands(ctx, formatter)

def command(self, *args, **kwargs):
default = kwargs.pop('default', False)
decorator = super(DefaultGroup, self).command(*args, **kwargs)
def format_commands(
self, ctx: click.core.Context, formatter: click.formatting.HelpFormatter
) -> None:
new_formatter = DefaultCommandFormatter(self, formatter, mark="*")
return super().format_commands(ctx, new_formatter)

@t.overload
def command(self, __func: t.Callable[..., t.Any]) -> click.core.Command:
...

@t.overload
def command(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], click.core.Command]:
...

def command(
self, *args: t.Any, **kwargs: t.Any
) -> t.Union[
t.Callable[[t.Callable[..., t.Any]], click.core.Command], click.core.Command
]:
default = kwargs.pop("default", False)
decorator: t.Callable[[t.Callable[..., t.Any]], click.core.Command] = (
super().command(*args, **kwargs)
)
if not default:
return decorator
warnings.warn('Use default param of DefaultGroup or '
'set_default_command() instead', DeprecationWarning)
warnings.warn(
"Use default param of DefaultGroup or set_default_command() instead",
DeprecationWarning,
)

def _decorator(f):
cmd = decorator(f)
def _decorator(f: t.Callable[..., t.Any]) -> click.core.Command:
cmd: click.core.Command = decorator(f)
self.set_default_command(cmd)
return cmd

return _decorator


class DefaultCommandFormatter(object):
class DefaultCommandFormatter(click.formatting.HelpFormatter):
"""Wraps a formatter to mark a default command."""

def __init__(self, group, formatter, mark='*'):
def __init__(
self,
group: DefaultGroup,
formatter: click.formatting.HelpFormatter,
mark: str = "*",
):
self.group = group
self.formatter = formatter
self.mark = mark

def __getattr__(self, attr):
super().__init__()

def __getattr__(self, attr: str) -> t.Any:
return getattr(self.formatter, attr)

def write_dl(self, rows, *args, **kwargs):
rows_ = []
for cmd_name, help in rows:
def write_dl(
self, rows: t.Sequence[t.Tuple[str, str]], *args: t.Any, **kwargs: t.Any
) -> None:
rows_: t.List[t.Tuple[str, str]] = []
for cmd_name, text in rows:
if cmd_name == self.group.default_cmd_name:
rows_.insert(0, (cmd_name + self.mark, help))
rows_.insert(0, (cmd_name + self.mark, text))
else:
rows_.append((cmd_name, help))
rows_.append((cmd_name, text))
return self.formatter.write_dl(rows_, *args, **kwargs)
Empty file added py.typed
Empty file.
8 changes: 2 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: Public Domain",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
Expand All @@ -25,12 +21,12 @@ classifiers = [
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
requires-python = ">=2.7"
requires-python = ">=3.7"
dependencies = ["click"]
dynamic = ["version", "description"]

[project.urls]
Source = "https://github.com/click-contrib/click-default-group"

[project.optional-dependencies]
test = ["pytest"]
test = ["pytest", "mypy", "flake8", "flake8-import-order", "pytest-cov", "coveralls"]
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ignore = E301
import-order-style = google
application-import-names = click_default_group
max-line-length = 88

[tool:pytest]
python_files = test.py