71
71
# command class they are attached to. The length of this file is largely a result of
72
72
# wrapping Typer.command, Typer.callback and Typer.add_typer in many different contexts
73
73
# to achieve the nice interface we would like and also because we list out each parameter
74
- # for developer experience reasons, its only ~600 actual statements
74
+ # for developer experience
75
+ # The other complexity comes from the fact that we enter pull in methods into the typer
76
+ # infrastructure before classes are created so we have to do some booking to make sure
77
+ # methods are bound to the right objects when called
75
78
76
79
import inspect
77
80
import sys
80
83
from importlib import import_module
81
84
from pathlib import Path
82
85
from types import MethodType , SimpleNamespace
86
+ import threading
83
87
84
88
import click
85
89
from click .shell_completion import CompletionItem
@@ -655,8 +659,10 @@ def grp(self):
655
659
"""
656
660
657
661
658
- def _staticmethod (func : t .Callable [..., t .Any ]) -> staticmethod :
659
- static_wrapper = staticmethod (func )
662
+ def _staticmethod (func : t .Callable [..., t .Any ]) -> t .Callable [..., t .Any ]:
663
+ static_wrapper = func
664
+ if not type (func ).__name__ == "staticmethod" :
665
+ static_wrapper = staticmethod (func )
660
666
cached = getattr (func , _CACHE_KEY , None )
661
667
if cached :
662
668
setattr (static_wrapper , _CACHE_KEY , cached )
@@ -848,6 +854,23 @@ def django_command(self) -> t.Optional[t.Type["TyperCommand"]]:
848
854
def django_command (self , cmd : t .Optional [t .Type ["TyperCommand" ]]):
849
855
self ._django_command = cmd
850
856
857
+ def __deepcopy__ (self , memo ):
858
+ """
859
+ A one level deep shallow-ish deepcopy that makes sure we have our own new lists
860
+ of commands and groups, which is all we care about.
861
+ """
862
+ if id (self ) in memo :
863
+ return memo [id (self )]
864
+ new_obj = self .__class__ .__new__ (self .__class__ )
865
+ memo [id (self )] = new_obj
866
+ for k , v in self .__dict__ .items ():
867
+ if callable (v ) and type (v ).__name__ == "staticmethod" :
868
+ if hasattr (self .__class__ , k ):
869
+ setattr (new_obj , k , getattr (self .__class__ , k ))
870
+ else :
871
+ setattr (new_obj , k , copy (v ))
872
+ return new_obj
873
+
851
874
def __init__ (
852
875
self ,
853
876
* ,
@@ -881,6 +904,8 @@ def __init__(
881
904
):
882
905
self .parent = parent
883
906
self ._django_command = django_command
907
+ if callback and not is_method (callback ):
908
+ callback = staticmethod (callback )
884
909
super ().__init__ (
885
910
name = name ,
886
911
cls = cls ,
@@ -962,11 +987,15 @@ def callback( # type: ignore
962
987
def make_initializer (
963
988
func : typer .models .CommandFunctionType ,
964
989
) -> typer .models .CommandFunctionType :
990
+ fn = t .cast (
991
+ typer .models .CommandFunctionType ,
992
+ func if is_method (func ) else _staticmethod (func ),
993
+ )
965
994
if self .__class__ is Typer :
966
995
# only cache at the top level - this enables subclassing of
967
996
# commands defined through the typer style interface.
968
997
_cache_initializer (
969
- func ,
998
+ fn ,
970
999
common_init = self .parent is None ,
971
1000
name = name ,
972
1001
help = help ,
@@ -987,12 +1016,8 @@ def make_initializer(
987
1016
** kwargs ,
988
1017
)
989
1018
if self .django_command and not hasattr (self .django_command , func .__name__ ):
990
- setattr (
991
- self .django_command ,
992
- func .__name__ ,
993
- func if is_method (func ) else _staticmethod (func ),
994
- )
995
- return register_initializer (func )
1019
+ setattr (self .django_command , func .__name__ , fn )
1020
+ return register_initializer (fn )
996
1021
997
1022
return make_initializer
998
1023
@@ -1037,11 +1062,15 @@ def command( # type: ignore
1037
1062
def make_command (
1038
1063
func : typer .models .CommandFunctionType ,
1039
1064
) -> typer .models .CommandFunctionType :
1065
+ fn = t .cast (
1066
+ typer .models .CommandFunctionType ,
1067
+ func if is_method (func ) else _staticmethod (func ),
1068
+ )
1040
1069
if self .__class__ is Typer :
1041
1070
# only cache at the top level - this enables subclassing of
1042
1071
# commands defined through the typer style interface.
1043
1072
_cache_command (
1044
- func ,
1073
+ fn ,
1045
1074
name = name ,
1046
1075
help = help ,
1047
1076
cls = cls ,
@@ -1057,12 +1086,8 @@ def make_command(
1057
1086
** kwargs ,
1058
1087
)
1059
1088
if self .django_command and not hasattr (self .django_command , func .__name__ ):
1060
- setattr (
1061
- self .django_command ,
1062
- func .__name__ ,
1063
- func if is_method (func ) else _staticmethod (func ),
1064
- )
1065
- return register_command (func )
1089
+ setattr (self .django_command , func .__name__ , fn )
1090
+ return register_command (fn )
1066
1091
1067
1092
return make_command
1068
1093
@@ -1093,6 +1118,8 @@ def add_typer( # type: ignore
1093
1118
) -> None :
1094
1119
typer_instance .parent = self
1095
1120
typer_instance .django_command = self .django_command
1121
+ if callback and not is_method (callback ):
1122
+ callback = _staticmethod (callback )
1096
1123
return super ().add_typer (
1097
1124
typer_instance = typer_instance ,
1098
1125
name = name ,
@@ -1116,6 +1143,48 @@ def add_typer( # type: ignore
1116
1143
)
1117
1144
1118
1145
1146
+ class _ChainBoundMethod :
1147
+ """
1148
+ A descriptor utility class that allows us to call sub groups and commands
1149
+ like this:
1150
+
1151
+ .. code-block:: python
1152
+ Command().grp1.grp2()
1153
+ Command().grp1.grp2.cmd()
1154
+
1155
+ If grp1 and grp2 return themselves you can chain calls like this:
1156
+
1157
+ .. code-block:: python
1158
+ Command().grp1().grp2().cmd()
1159
+ """
1160
+ parent : t .Optional ["Typer" ] = None
1161
+
1162
+ is_method : t .Optional [bool ] = None
1163
+
1164
+ _callback : t .Callable [..., t .Any ]
1165
+ _local = threading .local ()
1166
+
1167
+ def __init__ (self , callback : t .Callable [..., t .Any ]):
1168
+ self ._callback = callback
1169
+
1170
+ @property
1171
+ def cmd_obj (self ) -> t .Optional ["TyperCommand" ]:
1172
+ """
1173
+ If this command group was ultimately accessed from a TyperCommand instance,
1174
+ get that instance. For instance Command().lvl1.lvl2.cmd() will pass self
1175
+ to cmd()
1176
+ """
1177
+ assert self .parent is None or isinstance (self .parent , Typer )
1178
+ obj = self ._local .object or (
1179
+ self .parent .cmd_obj if isinstance (self .parent , _ChainBoundMethod ) else None
1180
+ )
1181
+ return (
1182
+ obj
1183
+ if isinstance (obj , TyperCommand )
1184
+ else None
1185
+ )
1186
+
1187
+
1119
1188
class CommandGroup (t .Generic [P , R ], Typer , metaclass = type ):
1120
1189
"""
1121
1190
Typer_ adds additional groups of commands by adding Typer_ apps to parent
@@ -1128,14 +1197,19 @@ class CommandGroup(t.Generic[P, R], Typer, metaclass=type):
1128
1197
_initializer : t .Optional [t .Callable [P , R ]] = None
1129
1198
_bindings : t .Dict [t .Type ["TyperCommand" ], "CommandGroup[P, R]" ]
1130
1199
_owner : t .Any = None
1131
-
1132
- is_method : t .Optional [bool ] = None
1200
+ _local = threading .local ()
1133
1201
1134
1202
@Typer .django_command .setter # type: ignore[attr-defined]
1135
1203
def django_command (self , cmd : t .Optional [t .Type ["TyperCommand" ]]):
1136
1204
self ._django_command = cmd
1137
1205
self .initializer = self .initializer # trigger class binding
1138
1206
1207
+ @property
1208
+ def name (self ) -> t .Optional [str ]:
1209
+ if self .initializer :
1210
+ return self .initializer .__name__
1211
+ return None
1212
+
1139
1213
@property
1140
1214
def initializer (self ) -> t .Optional [t .Callable [P , R ]]:
1141
1215
return self ._initializer
@@ -1161,7 +1235,6 @@ def initializer(self, initializer: t.Optional[t.Callable[P, R]]):
1161
1235
setattr (
1162
1236
cmd_cls ,
1163
1237
initializer .__name__ ,
1164
- # initializer if self.is_method else staticmethod(initializer),
1165
1238
self ,
1166
1239
)
1167
1240
for cmd in getattr (self , "registered_commands" , []):
@@ -1185,7 +1258,7 @@ def bound(self) -> bool:
1185
1258
return bool (len (self ._bindings ))
1186
1259
1187
1260
@property
1188
- def owner (self ):
1261
+ def owner (self ) -> t . Optional [ t . Type [ "TyperCommand" ]] :
1189
1262
"""
1190
1263
If this function or its root ancestor was accessed as a member of a TyperCommand class,
1191
1264
return that class, if it was accessed in any other way - return None.
@@ -1206,6 +1279,8 @@ def owner(self):
1206
1279
1207
1280
def __call__ (self , * args : P .args , ** kwargs : P .kwargs ) -> R :
1208
1281
assert self .initializer
1282
+ if self .is_method :
1283
+ return MethodType (self .initializer , self .owner )(* args , ** kwargs )
1209
1284
return self .initializer (* args , ** kwargs )
1210
1285
1211
1286
@t .overload # pragma: no cover
@@ -1232,12 +1307,18 @@ def __get__(self, obj, owner=None):
1232
1307
on the class and subclasses.
1233
1308
"""
1234
1309
self ._owner = owner or None
1235
- if obj is None or not isinstance (obj , TyperCommand ) or not self .initializer :
1310
+ self ._local .object = obj
1311
+ if obj is None or not isinstance (obj , (TyperCommand , Typer )) or not self .initializer :
1236
1312
return self
1237
1313
if self .is_method :
1238
1314
return MethodType (self .initializer , obj )
1239
1315
return self .initializer
1240
1316
1317
+ def __deepcopy__ (self , memo ):
1318
+ new_obj = super ().__deepcopy__ (memo )
1319
+ new_obj .initializer = self .initializer
1320
+ return new_obj
1321
+
1241
1322
def __init__ (
1242
1323
self ,
1243
1324
* ,
@@ -1316,6 +1397,9 @@ def attach(
1316
1397
# it. This is done to avoid polluting the base command which should still be expected
1317
1398
# to be instantiable and behave normally regardless of the app stack. This is not an
1318
1399
# issue when using the typer-style interface because no inheritance is involved.
1400
+ # we also take this opportunity to add direct functions of groups and commands to parent
1401
+ # command groups as attributes. This allows fully namespaced references to be used when
1402
+ # calling command trees. Should be rarely used, but a nice basically zero cost feature.
1319
1403
self .django_command = self .django_command or django_command
1320
1404
if not parent :
1321
1405
parent = django_command .typer_app
@@ -1325,6 +1409,18 @@ def attach(
1325
1409
self ._bindings [django_command ] = cpy
1326
1410
for grp in self .groups :
1327
1411
grp .attach (django_command = django_command , parent = cpy )
1412
+ initializer = getattr (cpy , "initializer" , None )
1413
+ if grp .name and initializer and not hasattr (initializer , grp .name ):
1414
+ setattr (initializer , grp .name , grp )
1415
+ for cmd in getattr (self , "registered_commands" , []):
1416
+ if not hasattr (cpy , cmd .callback .__name__ ):
1417
+ setattr (
1418
+ cpy ,
1419
+ cmd .callback .__name__ ,
1420
+ cmd .callback
1421
+ if is_method (cmd .callback )
1422
+ else _staticmethod (cmd .callback ),
1423
+ )
1328
1424
return self
1329
1425
1330
1426
def callback ( # type: ignore
@@ -1470,9 +1566,12 @@ def command1(self):
1470
1566
1471
1567
def make_command (f : t .Callable [P2 , R2 ]) -> t .Callable [P2 , R2 ]:
1472
1568
owner = kwargs .pop ("_owner" , None )
1569
+ cmd = f if is_method (f ) else _staticmethod (f )
1473
1570
if owner :
1474
1571
# attach this function to the adapted Command class
1475
- setattr (owner , f .__name__ , f if is_method (f ) else _staticmethod (f ))
1572
+ setattr (owner , f .__name__ , cmd )
1573
+ if not hasattr (self , f .__name__ ):
1574
+ setattr (self , f .__name__ , cmd )
1476
1575
return super ( # pylint: disable=super-with-arguments
1477
1576
CommandGroup , self
1478
1577
).command (
@@ -1615,12 +1714,15 @@ def create_app(func: t.Callable[P2, R2]) -> CommandGroup[P2, R2]:
1615
1714
** kwargs ,
1616
1715
)
1617
1716
)
1717
+ new_grp = self .groups [- 1 ]
1618
1718
if owner :
1619
- new_grp = self .groups [- 1 ]
1620
1719
cpy = deepcopy (new_grp )
1621
1720
new_grp ._bindings [owner ] = cpy
1622
1721
self .add_typer (cpy )
1623
1722
setattr (owner , func .__name__ , new_grp )
1723
+ assert new_grp .name
1724
+ if not hasattr (self , new_grp .name ):
1725
+ setattr (self , new_grp .name , new_grp )
1624
1726
return self .groups [- 1 ]
1625
1727
1626
1728
return create_app
@@ -1741,6 +1843,8 @@ def divide(
1741
1843
"""
1742
1844
1743
1845
def make_initializer (func : t .Callable [P2 , R2 ]) -> t .Callable [P2 , R2 ]:
1846
+ if not is_method (func ):
1847
+ func = staticmethod (func )
1744
1848
_cache_initializer (
1745
1849
func ,
1746
1850
common_init = True ,
@@ -1844,6 +1948,8 @@ def other_command(self):
1844
1948
"""
1845
1949
1846
1950
def make_command (func : t .Callable [P , R ]) -> t .Callable [P , R ]:
1951
+ if not is_method (func ):
1952
+ func = staticmethod (func )
1847
1953
_cache_command (
1848
1954
func ,
1849
1955
name = name ,
@@ -1952,6 +2058,8 @@ def subcommand(self):
1952
2058
"""
1953
2059
1954
2060
def create_app (func : t .Callable [P , R ]) -> CommandGroup [P , R ]:
2061
+ if not is_method (func ):
2062
+ func = staticmethod (func )
1955
2063
grp : CommandGroup = CommandGroup ( # pyright: ignore[reportAssignmentType]
1956
2064
name = name ,
1957
2065
cls = cls ,
@@ -2744,6 +2852,9 @@ def init(self, ...):
2744
2852
)
2745
2853
2746
2854
def make_initializer (func : t .Callable [P , R ]) -> t .Callable [P , R ]:
2855
+ func = t .cast (
2856
+ t .Callable [P , R ], func if is_method (func ) else _staticmethod (func )
2857
+ )
2747
2858
cmd .typer_app .callback (
2748
2859
name = name ,
2749
2860
cls = type ("_Initializer" , (cls ,), {"django_command" : cmd }),
@@ -2764,9 +2875,7 @@ def make_initializer(func: t.Callable[P, R]) -> t.Callable[P, R]:
2764
2875
rich_help_panel = rich_help_panel ,
2765
2876
** kwargs ,
2766
2877
)(func )
2767
- setattr (
2768
- cmd , func .__name__ , func if is_method (func ) else _staticmethod (func )
2769
- )
2878
+ setattr (cmd , func .__name__ , func )
2770
2879
return func
2771
2880
2772
2881
return make_initializer
@@ -2845,6 +2954,9 @@ def new_command(self, ...):
2845
2954
)
2846
2955
2847
2956
def make_command (func : t .Callable [P , R ]) -> t .Callable [P , R ]:
2957
+ func = t .cast (
2958
+ t .Callable [P , R ], func if is_method (func ) else _staticmethod (func )
2959
+ )
2848
2960
cmd .typer_app .command (
2849
2961
name = name ,
2850
2962
cls = type ("_Command" , (cls ,), {"django_command" : cmd }),
@@ -2861,9 +2973,7 @@ def make_command(func: t.Callable[P, R]) -> t.Callable[P, R]:
2861
2973
rich_help_panel = rich_help_panel ,
2862
2974
** kwargs ,
2863
2975
)(func )
2864
- setattr (
2865
- cmd , func .__name__ , func if is_method (func ) else _staticmethod (func )
2866
- )
2976
+ setattr (cmd , func .__name__ , func )
2867
2977
return func
2868
2978
2869
2979
return make_command
0 commit comments