Skip to content

Commit 37e1cac

Browse files
committed
retreat on __get__ typing
1 parent 179b0b1 commit 37e1cac

File tree

2 files changed

+83
-43
lines changed

2 files changed

+83
-43
lines changed

django_typer/__init__.py

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,14 @@
6464
# quirk imposed by the base class for users to be aware of.
6565
# ruff: noqa: E402
6666

67-
# Most of the bookeeping complexity is induced by the necessity of figuring out if and
68-
# where django's base command line parameters should be attached. To do this commands
69-
# and Typer instances (groups in django-typer parlance) need to be able to access the
70-
# django command class they are attached to. The length of this file is largely a result
71-
# of wrapping Typer.command, Typer.callback and Typer.add_typer in many different contexts
67+
# Most of the book keeping complexity is induced by the necessity of figuring out if and
68+
# where django's base command line parameters should be attached and how to bind a method
69+
# function to a class that may not been created yet. To do this commands and Typer
70+
# instances (groups in click/django-typer parlance) need to be able to access the django
71+
# command class they are attached to. The length of this file is largely a result of
72+
# wrapping Typer.command, Typer.callback and Typer.add_typer in many different contexts
7273
# to achieve the nice interface we would like and also because we list out each parameter
73-
# for developer experience reasons
74+
# for developer experience reasons, its only ~600 actual statements
7475

7576
import inspect
7677
import sys
@@ -142,6 +143,12 @@
142143

143144
__all__ = [
144145
"TyperCommand",
146+
"CommandNode",
147+
"CommandGroup",
148+
"Typer",
149+
"DjangoTyperMixin",
150+
"DTCommand",
151+
"DTGroup",
145152
"Context",
146153
"initialize",
147154
"command",
@@ -454,7 +461,7 @@ def __init__(
454461
parent.children.append(self)
455462

456463

457-
class DjangoTyperCommand(with_typehint(CoreTyperGroup)): # type: ignore[misc]
464+
class DjangoTyperMixin(with_typehint(CoreTyperGroup)): # type: ignore[misc]
458465
"""
459466
A mixin we use to add additional needed contextual awareness to click Commands
460467
and Groups.
@@ -604,19 +611,47 @@ def call_with_self(*args, **kwargs):
604611
)
605612

606613

607-
class TyperCommandWrapper(DjangoTyperCommand, CoreTyperCommand):
614+
class DTCommand(DjangoTyperMixin, CoreTyperCommand):
608615
"""
609-
This class extends the TyperCommand class to work with the django-typer
610-
interfaces. If you need to add functionality to the command class - which
611-
you should not - you should inherit from this class.
616+
This class extends the TyperCommand class to work with the django-typer interfaces.
617+
If you need to add functionality to the command class - you should inherit from
618+
this class. You can then pass your custom class to the command() decorators
619+
using the cls parameter.
620+
621+
.. code-block:: python
622+
623+
from django_typer import TyperCommand, DTCommand, command
624+
625+
class CustomCommand(DTCommand):
626+
...
627+
628+
class Command(TyperCommand):
629+
630+
@command(cls=CustomCommand)
631+
def handle(self):
632+
...
612633
"""
613634

614635

615-
class TyperGroupWrapper(DjangoTyperCommand, CoreTyperGroup):
636+
class DTGroup(DjangoTyperMixin, CoreTyperGroup):
616637
"""
617-
This class extends the TyperGroup class to work with the django-typer
618-
interfaces. If you need to add functionality to the group class - which
619-
you should not - you should inherit from this class.
638+
This class extends the TyperGroup class to work with the django-typer interfaces.
639+
If you need to add functionality to the group class you should inherit from this
640+
class. You can then pass your custom class to the command() decorators using the
641+
cls parameter.
642+
643+
.. code-block:: python
644+
645+
from django_typer import TyperCommand, DTGroup, group
646+
647+
class CustomGroup(DTGroup):
648+
...
649+
650+
class Command(TyperCommand):
651+
652+
@group(cls=CustomGroup)
653+
def grp(self):
654+
...
620655
"""
621656

622657

@@ -633,7 +668,7 @@ def _cache_initializer(
633668
common_init: bool,
634669
name: t.Optional[str] = Default(None),
635670
help: t.Optional[str] = Default(None),
636-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
671+
cls: t.Type[DTGroup] = DTGroup,
637672
**kwargs,
638673
):
639674
if not hasattr(callback, _CACHE_KEY):
@@ -663,7 +698,7 @@ def _cache_command(
663698
callback: t.Callable[..., t.Any],
664699
name: t.Optional[str] = None,
665700
help: t.Optional[str] = None,
666-
cls: t.Type[TyperCommandWrapper] = TyperCommandWrapper,
701+
cls: t.Type[DTCommand] = DTCommand,
667702
**kwargs,
668703
):
669704
if not hasattr(callback, _CACHE_KEY):
@@ -690,7 +725,7 @@ def __call__(
690725
app_cls, # pyright: ignore[reportSelfClsParameterName]
691726
*args,
692727
name: t.Optional[str] = Default(None),
693-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
728+
cls: t.Type[DTGroup] = DTGroup,
694729
invoke_without_command: bool = Default(False),
695730
no_args_is_help: bool = Default(False),
696731
subcommand_metavar: t.Optional[str] = Default(None),
@@ -817,7 +852,7 @@ def __init__(
817852
self,
818853
*,
819854
name: t.Optional[str] = Default(None),
820-
cls: t.Optional[t.Type[TyperGroupWrapper]] = TyperGroupWrapper,
855+
cls: t.Optional[t.Type[DTGroup]] = DTGroup,
821856
invoke_without_command: bool = Default(False),
822857
no_args_is_help: bool = Default(False),
823858
subcommand_metavar: t.Optional[str] = Default(None),
@@ -876,7 +911,7 @@ def callback( # type: ignore
876911
self,
877912
name: t.Optional[str] = Default(None),
878913
*,
879-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
914+
cls: t.Type[DTGroup] = DTGroup,
880915
invoke_without_command: bool = Default(False),
881916
no_args_is_help: bool = Default(False),
882917
subcommand_metavar: t.Optional[str] = Default(None),
@@ -967,7 +1002,7 @@ def command( # type: ignore
9671002
self,
9681003
name: t.Optional[str] = None,
9691004
*,
970-
cls: t.Type[TyperCommandWrapper] = TyperCommandWrapper,
1005+
cls: t.Type[DTCommand] = DTCommand,
9711006
context_settings: t.Optional[t.Dict[t.Any, t.Any]] = None,
9721007
help: t.Optional[str] = None,
9731008
epilog: t.Optional[str] = None,
@@ -1036,7 +1071,7 @@ def add_typer( # type: ignore
10361071
typer_instance: "CommandGroup",
10371072
*,
10381073
name: t.Optional[str] = Default(None),
1039-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
1074+
cls: t.Type[DTGroup] = DTGroup,
10401075
invoke_without_command: bool = Default(False),
10411076
no_args_is_help: bool = Default(False),
10421077
subcommand_metavar: t.Optional[str] = Default(None),
@@ -1181,11 +1216,14 @@ def __get__(
11811216
@t.overload
11821217
def __get__(
11831218
self, obj: "TyperCommand", owner: t.Any = None
1184-
) -> t.Union[MethodType, t.Callable[P, R]]: ...
1219+
) -> MethodType: # t.Union[MethodType, t.Callable[P, R]]
1220+
# todo - we could return the generic callable type here but the problem
1221+
# is self is included in the ParamSpec and it seems tricky to remove?
1222+
# MethodType loses the parameters but is preferable to type checking errors
1223+
# https://github.com/bckohan/django-typer/issues/73
1224+
...
11851225

1186-
def __get__(
1187-
self, obj: t.Any, owner: t.Any = None
1188-
) -> t.Union["CommandGroup[P, R]", MethodType, t.Callable[P, R]]:
1226+
def __get__(self, obj, owner=None):
11891227
"""
11901228
Our Typer app wrapper also doubles as a descriptor, so when
11911229
it is accessed on the instance, we return the wrapped function
@@ -1204,7 +1242,7 @@ def __init__(
12041242
self,
12051243
*,
12061244
name: t.Optional[str] = Default(None),
1207-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
1245+
cls: t.Type[DTGroup] = DTGroup,
12081246
invoke_without_command: bool = Default(False),
12091247
no_args_is_help: bool = Default(False),
12101248
subcommand_metavar: t.Optional[str] = Default(None),
@@ -1293,7 +1331,7 @@ def callback( # type: ignore
12931331
self,
12941332
name: t.Optional[str] = Default(None),
12951333
*,
1296-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
1334+
cls: t.Type[DTGroup] = DTGroup,
12971335
invoke_without_command: bool = Default(False),
12981336
no_args_is_help: bool = Default(False),
12991337
subcommand_metavar: t.Optional[str] = Default(None),
@@ -1346,7 +1384,7 @@ def command( # type: ignore
13461384
self,
13471385
name: t.Optional[str] = None,
13481386
*,
1349-
cls: t.Type[TyperCommandWrapper] = TyperCommandWrapper,
1387+
cls: t.Type[DTCommand] = DTCommand,
13501388
context_settings: t.Optional[t.Dict[t.Any, t.Any]] = None,
13511389
help: t.Optional[str] = None, # pylint: disable=redefined-builtin
13521390
epilog: t.Optional[str] = None,
@@ -1458,7 +1496,7 @@ def make_command(f: t.Callable[P2, R2]) -> t.Callable[P2, R2]:
14581496
def group(
14591497
self,
14601498
name: t.Optional[str] = Default(None),
1461-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
1499+
cls: t.Type[DTGroup] = DTGroup,
14621500
invoke_without_command: bool = Default(False),
14631501
no_args_is_help: bool = Default(False),
14641502
subcommand_metavar: t.Optional[str] = Default(None),
@@ -1591,7 +1629,7 @@ def create_app(func: t.Callable[P2, R2]) -> CommandGroup[P2, R2]:
15911629
def initialize(
15921630
name: t.Optional[str] = Default(None),
15931631
*,
1594-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
1632+
cls: t.Type[DTGroup] = DTGroup,
15951633
invoke_without_command: bool = Default(False),
15961634
no_args_is_help: bool = Default(False),
15971635
subcommand_metavar: t.Optional[str] = Default(None),
@@ -1735,7 +1773,7 @@ def make_initializer(func: t.Callable[P2, R2]) -> t.Callable[P2, R2]:
17351773
def command( # pylint: disable=keyword-arg-before-vararg
17361774
name: t.Optional[str] = None,
17371775
*,
1738-
cls: t.Type[TyperCommandWrapper] = TyperCommandWrapper,
1776+
cls: t.Type[DTCommand] = DTCommand,
17391777
context_settings: t.Optional[t.Dict[t.Any, t.Any]] = None,
17401778
help: t.Optional[str] = None, # pylint: disable=redefined-builtin
17411779
epilog: t.Optional[str] = None,
@@ -1830,7 +1868,7 @@ def make_command(func: t.Callable[P, R]) -> t.Callable[P, R]:
18301868

18311869
def group(
18321870
name: t.Optional[str] = Default(None),
1833-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
1871+
cls: t.Type[DTGroup] = DTGroup,
18341872
invoke_without_command: bool = Default(False),
18351873
no_args_is_help: bool = Default(False),
18361874
subcommand_metavar: t.Optional[str] = Default(None),
@@ -1955,7 +1993,7 @@ def _add_common_initializer(
19551993
cmd.typer_app.callback(
19561994
cls=type(
19571995
"_Initializer",
1958-
(TyperGroupWrapper,),
1996+
(DTGroup,),
19591997
{
19601998
"django_command": cmd,
19611999
"_callback_is_method": False,
@@ -2065,7 +2103,7 @@ def __new__(
20652103
bases,
20662104
attrs,
20672105
name: t.Optional[str] = Default(None),
2068-
cls: t.Optional[t.Type[TyperGroupWrapper]] = TyperGroupWrapper,
2106+
cls: t.Optional[t.Type[DTGroup]] = DTGroup,
20692107
invoke_without_command: bool = Default(False),
20702108
no_args_is_help: bool = Default(False),
20712109
subcommand_metavar: t.Optional[str] = Default(None),
@@ -2224,7 +2262,7 @@ class CommandNode:
22242262
The name of the group or command that this node represents.
22252263
"""
22262264

2227-
click_command: DjangoTyperCommand
2265+
click_command: DjangoTyperMixin
22282266
"""
22292267
The click command object that this node represents.
22302268
"""
@@ -2260,7 +2298,7 @@ def callback(self) -> t.Callable[..., t.Any]:
22602298
def __init__(
22612299
self,
22622300
name: str,
2263-
click_command: DjangoTyperCommand,
2301+
click_command: DjangoTyperMixin,
22642302
context: TyperContext,
22652303
django_command: "TyperCommand",
22662304
parent: t.Optional["CommandNode"] = None,
@@ -2589,7 +2627,7 @@ def initialize(
25892627
cmd, # pyright: ignore[reportSelfClsParameterName]
25902628
name: t.Optional[str] = Default(None),
25912629
*,
2592-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
2630+
cls: t.Type[DTGroup] = DTGroup,
25932631
invoke_without_command: bool = Default(False),
25942632
no_args_is_help: bool = Default(False),
25952633
subcommand_metavar: t.Optional[str] = Default(None),
@@ -2705,7 +2743,7 @@ def command(
27052743
cmd, # pyright: ignore[reportSelfClsParameterName]
27062744
name: t.Optional[str] = None,
27072745
*,
2708-
cls: t.Type[TyperCommandWrapper] = TyperCommandWrapper,
2746+
cls: t.Type[DTCommand] = DTCommand,
27092747
context_settings: t.Optional[t.Dict[t.Any, t.Any]] = None,
27102748
help: t.Optional[str] = None, # pylint: disable=redefined-builtin
27112749
epilog: t.Optional[str] = None,
@@ -2799,7 +2837,7 @@ def make_command(func: t.Callable[P, R]) -> t.Callable[P, R]:
27992837
def group(
28002838
cmd, # pyright: ignore[reportSelfClsParameterName]
28012839
name: t.Optional[str] = Default(None),
2802-
cls: t.Type[TyperGroupWrapper] = TyperGroupWrapper,
2840+
cls: t.Type[DTGroup] = DTGroup,
28032841
invoke_without_command: bool = Default(False),
28042842
no_args_is_help: bool = Default(False),
28052843
subcommand_metavar: t.Optional[str] = Default(None),
@@ -3061,7 +3099,7 @@ def _build_cmd_tree(
30613099
:param node: the parent node or None if this is a root node
30623100
"""
30633101
assert cmd.name
3064-
assert isinstance(cmd, DjangoTyperCommand)
3102+
assert isinstance(cmd, DjangoTyperMixin)
30653103
ctx = Context(cmd, info_name=info_name, parent=parent, django_command=self)
30663104
current = CommandNode(cmd.name, cmd, ctx, self, parent=node)
30673105
if node:

django_typer/tests/test_groups.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,9 @@ def test_command_line(self, settings=None):
280280
"-0.03",
281281
)
282282

283-
self.assertEqual(get_command("groups").divide(1.2, 3.5, [-12.3]), "-0.03")
283+
self.assertEqual(
284+
get_command("groups", Groups).divide(1.2, 3.5, [-12.3]), "-0.03"
285+
)
284286
self.assertEqual(
285287
get_command("groups", "math", "divide")(1.2, 3.5, [-12.3]), "-0.03"
286288
)
@@ -297,7 +299,7 @@ def test_command_line(self, settings=None):
297299
"annamontes",
298300
)
299301

300-
grp_cmd = get_command("groups")
302+
grp_cmd = get_command("groups", Groups)
301303
grp_cmd.string("ANNAmontes")
302304
self.assertEqual(grp_cmd.lower(), "annamontes")
303305

0 commit comments

Comments
 (0)