Skip to content

Commit 41c7743

Browse files
committed
fix #74 fix #69
1 parent 681ba79 commit 41c7743

File tree

20 files changed

+222
-36
lines changed

20 files changed

+222
-36
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,6 @@ jobs:
6666
- name: Run Static Analysis
6767
run: |
6868
source .venv/bin/activate
69-
ruff format --check django_typer
70-
ruff check --no-fix --select I django_typer
71-
ruff check django_typer
72-
mypy django_typer
73-
pyright
74-
poetry check
75-
pip check
69+
./check.sh --no-fix
7670
python -m readme_renderer ./README.md -o /tmp/README.html
77-
cd ./doc
78-
doc8 --ignore-path build --max-line-length 100
7971
echo "$(poetry env info --path)/bin" >> $GITHUB_PATH

check.sh

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
1-
poetry run ruff format
2-
poetry run ruff format --line-length 80 django_typer/tests/apps/examples
3-
poetry run ruff check --fix --select I
4-
poetry run ruff check --fix
1+
set -e # Exit immediately if a command exits with a non-zero status.
2+
3+
if [ "$1" == "--no-fix" ]; then
4+
poetry run ruff format --check
5+
poetry run ruff format --line-length 80 --check examples
6+
poetry run ruff check --select I
7+
poetry run ruff check
8+
else
9+
poetry run ruff format
10+
poetry run ruff format --line-length 80 examples
11+
poetry run ruff check --fix --select I
12+
poetry run ruff check --fix
13+
fi
14+
515
poetry run mypy django_typer
616
pyright
717
poetry check
818
poetry run pip check
19+
cd ./doc
20+
poetry run doc8 --ignore-path build --max-line-length 100 -q
21+
cd ..

django_typer/__init__.py

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,7 +2011,18 @@ def _resolve_help(dj_cmd: "TyperCommand"):
20112011
20122012
:param dj_cmd: The TyperCommand to resolve the help string for.
20132013
"""
2014-
hlp = dj_cmd.__class__.__doc__
2014+
hlp = None
2015+
for cmd_cls in [
2016+
dj_cmd.__class__,
2017+
*[
2018+
c
2019+
for c in dj_cmd.__class__.__mro__
2020+
if issubclass(c, TyperCommand) and c is not TyperCommand
2021+
],
2022+
]:
2023+
hlp = cmd_cls.__doc__
2024+
if hlp:
2025+
break
20152026
if hlp:
20162027
if dj_cmd.typer_app.registered_callback:
20172028
cb = dj_cmd.typer_app.registered_callback
@@ -2154,10 +2165,18 @@ def __new__(
21542165
"short", pretty_exceptions_short
21552166
)
21562167

2168+
attr_help = attrs.get("help", Default(None))
2169+
if not help:
2170+
for base in [base for base in bases if issubclass(base, TyperCommand)]:
2171+
if isinstance(help, DefaultPlaceholder):
2172+
help = base._help_kwarg # type: ignore[unreachable]
2173+
if isinstance(attr_help, DefaultPlaceholder):
2174+
attr_help = base.help
2175+
21572176
typer_app = Typer(
21582177
name=name or attrs["__module__"].rsplit(".", maxsplit=1)[-1],
21592178
# cls=cls,
2160-
help=help or attrs.get("help", Default(None)),
2179+
help=help or attr_help, # pyright: ignore[reportArgumentType]
21612180
invoke_without_command=invoke_without_command,
21622181
no_args_is_help=no_args_is_help,
21632182
subcommand_metavar=subcommand_metavar,
@@ -2187,6 +2206,12 @@ def __new__(
21872206
**attrs,
21882207
"typer_app": typer_app,
21892208
}
2209+
else:
2210+
# we do this to avoid typing complaints on handle overrides
2211+
attrs["handle"] = attrs.pop("_run")
2212+
2213+
if help:
2214+
attrs["_help_kwarg"] = help
21902215

21912216
return super().__new__(mcs, cls_name, bases, attrs)
21922217

@@ -2206,6 +2231,7 @@ def get_ctor(attr: t.Any) -> t.Optional[t.Callable[..., t.Any]]:
22062231

22072232
# because we're mapping a non-class based interface onto a class based
22082233
# interface, we have to handle this class mro stuff manually here
2234+
handle = None # there can be only one or none
22092235
for cmd_cls, cls_attrs in [
22102236
*[(base, vars(base)) for base in reversed(bases)],
22112237
(cls, attrs),
@@ -2222,23 +2248,25 @@ def get_ctor(attr: t.Any) -> t.Optional[t.Callable[..., t.Any]]:
22222248
elif register := get_ctor(attr):
22232249
to_register.append(register)
22242250

2225-
if cmd_cls._handle:
2226-
ctor = get_ctor(cmd_cls._handle)
2227-
if ctor:
2228-
ctor(
2229-
cls,
2230-
_name=cls.typer_app.info.name,
2231-
_help=getattr(cls, "help", None),
2232-
)
2233-
else:
2234-
cls.typer_app.command(
2235-
cls.typer_app.info.name,
2236-
help=cls.typer_app.info.help or None,
2237-
)(cmd_cls._handle)
2251+
handle = cmd_cls._handle or handle
22382252

22392253
for cmd in to_register:
22402254
cmd(cls)
22412255

2256+
if handle:
2257+
ctor = get_ctor(handle)
2258+
if ctor:
2259+
ctor(
2260+
cls,
2261+
_name=cls.typer_app.info.name,
2262+
_help=getattr(cls, "help", None),
2263+
)
2264+
else:
2265+
cls.typer_app.command(
2266+
cls.typer_app.info.name,
2267+
help=cls.typer_app.info.help or None,
2268+
)(handle)
2269+
22422270
_add_common_initializer(cls)
22432271

22442272
super().__init__(cls_name, bases, attrs, **kwargs)
@@ -2619,9 +2647,16 @@ def command2(self, option: t.Optional[str] = None):
26192647
force_color: bool = False
26202648
_handle: t.Callable[..., t.Any]
26212649
_traceback: bool = False
2650+
_help_kwarg: t.Optional[str] = Default(None)
2651+
2652+
help: t.Optional[t.Union[DefaultPlaceholder, str]] = Default(None) # type: ignore
26222653

26232654
command_tree: CommandNode
26242655

2656+
# allow deriving commands to override handle() from BaseCommand
2657+
# without triggering static type checking complaints
2658+
handle = None # type: ignore
2659+
26252660
@classmethod
26262661
def initialize(
26272662
cmd, # pyright: ignore[reportSelfClsParameterName]
@@ -3189,7 +3224,8 @@ def handle(self, option1: bool, option2: bool):
31893224
).format(cls=self.__class__)
31903225
)
31913226

3192-
def handle(self, *args, **options):
3227+
@t.no_type_check
3228+
def _run(self, *args, **options):
31933229
"""
31943230
Invoke the underlying Typer app with the given arguments and parameters.
31953231

django_typer/tests/apps/test_app/management/commands/handle.py

Whitespace-only changes.

django_typer/tests/apps/test_app/management/commands/handle_as_init.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from click import get_current_context
2-
2+
import typing as t
33
from django_typer import TyperCommand, command, initialize
44

55

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import json
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from .help_precedence9 import Command as BaseCommand
6+
7+
8+
# should inherit docstring up from base
9+
class Command(BaseCommand):
10+
def handle(self, arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
11+
assert self.__class__ == Command
12+
opts = {"arg1": arg1, "arg2": arg2, "arg3": arg3, "arg4": arg4}
13+
return json.dumps(opts)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import json
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from .help_precedence9 import Command as BaseCommand
6+
7+
8+
class Command(BaseCommand):
9+
"""
10+
This docstring overrides base docstring.
11+
"""
12+
13+
def handle(self, arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
14+
assert self.__class__ == Command
15+
opts = {"arg1": arg1, "arg2": arg2, "arg3": arg3, "arg4": arg4}
16+
return json.dumps(opts)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import json
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from .help_precedence10 import Command as BaseCommand
6+
7+
8+
# should inherit docstring up from multiple levels up
9+
class Command(BaseCommand):
10+
def handle(self, arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
11+
assert self.__class__ == Command
12+
opts = {"arg1": arg1, "arg2": arg2, "arg3": arg3, "arg4": arg4}
13+
return json.dumps(opts)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import json
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from .help_precedence8 import Command as BaseCommand
6+
7+
8+
# helps defined upstream at higher precedence override those at lower precedence
9+
# locally
10+
class Command(BaseCommand):
11+
"""
12+
Override higher precedence inherit.
13+
"""
14+
15+
def handle(self, arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
16+
assert self.__class__ == Command
17+
opts = {"arg1": arg1, "arg2": arg2, "arg3": arg3, "arg4": arg4}
18+
return json.dumps(opts)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import json
2+
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from .help_precedence8 import Command as BaseCommand
6+
7+
8+
# inherits docstring from base
9+
class Command(BaseCommand):
10+
def handle(self, arg1: str, arg2: str, arg3: float = 0.5, arg4: int = 1):
11+
assert self.__class__ == Command
12+
opts = {"arg1": arg1, "arg2": arg2, "arg3": arg3, "arg4": arg4}
13+
return json.dumps(opts)

0 commit comments

Comments
 (0)