152
152
153
153
P = ParamSpec ("P" )
154
154
R = t .TypeVar ("R" )
155
+ C = t .TypeVar ("C" , bound = BaseCommand )
155
156
156
157
_CACHE_KEY = "_register_typer"
157
158
@@ -220,15 +221,52 @@ def handle(
220
221
}
221
222
222
223
224
+ @t .overload
225
+ def get_command ( # type: ignore[overload-overlap]
226
+ command_name : str ,
227
+ stdout : t .Optional [t .IO [str ]] = None ,
228
+ stderr : t .Optional [t .IO [str ]] = None ,
229
+ no_color : bool = False ,
230
+ force_color : bool = False ,
231
+ ** kwargs ,
232
+ ) -> BaseCommand : ...
233
+
234
+
235
+ @t .overload
236
+ # mypy seems to break on this one, but this is correct
223
237
def get_command (
224
238
command_name : str ,
225
- * subcommand : str ,
239
+ cmd_type : t . Type [ C ] ,
226
240
stdout : t .Optional [t .IO [str ]] = None ,
227
241
stderr : t .Optional [t .IO [str ]] = None ,
228
242
no_color : bool = False ,
229
243
force_color : bool = False ,
230
244
** kwargs ,
231
- ) -> t .Union [BaseCommand , MethodType ]:
245
+ ) -> C : ...
246
+
247
+
248
+ @t .overload
249
+ def get_command (
250
+ command_name : str ,
251
+ path0 : str ,
252
+ * path : str ,
253
+ stdout : t .Optional [t .IO [str ]] = None ,
254
+ stderr : t .Optional [t .IO [str ]] = None ,
255
+ no_color : bool = False ,
256
+ force_color : bool = False ,
257
+ ** kwargs ,
258
+ ) -> MethodType : ...
259
+
260
+
261
+ def get_command (
262
+ command_name ,
263
+ * path ,
264
+ stdout = None ,
265
+ stderr = None ,
266
+ no_color : bool = False ,
267
+ force_color : bool = False ,
268
+ ** kwargs ,
269
+ ):
232
270
"""
233
271
Get a Django_ command by its name and instantiate it with the provided options. This
234
272
will work for subclasses of BaseCommand_ as well as for :class:`~django_typer.TyperCommand`
@@ -261,8 +299,17 @@ def get_command(
261
299
divide = get_command('hierarchy', 'math', 'divide')
262
300
result = divide(10, 2)
263
301
302
+ When fetching an entire TyperCommand (i.e. no group or subcommand path), you may supply
303
+ the type of the expected TyperCommand as the second argument. This will allow the type
304
+ system to infer the correct return type:
305
+
306
+ .. code-block:: python
307
+
308
+ from myapp.management.commands import Command as Hierarchy
309
+ hierarchy: Hierarchy = get_command('hierarchy', Hierarchy)
310
+
264
311
:param command_name: the name of the command to get
265
- :param subcommand : the subcommand to get if any
312
+ :param path : the path walking down the group/command tree
266
313
:param stdout: the stdout stream to use
267
314
:param stderr: the stderr stream to use
268
315
:param no_color: whether to disable color
@@ -274,16 +321,15 @@ def get_command(
274
321
module = import_module (
275
322
f"{ get_commands ()[command_name ]} .management.commands.{ command_name } "
276
323
)
277
- cmd = module .Command (
324
+ cmd : BaseCommand = module .Command (
278
325
stdout = stdout ,
279
326
stderr = stderr ,
280
327
no_color = no_color ,
281
328
force_color = force_color ,
282
329
** kwargs ,
283
330
)
284
- if subcommand :
285
- method = cmd .get_subcommand (* subcommand ).click_command ._callback .__wrapped__
286
- return MethodType (method , cmd ) # return the bound method
331
+ if path and (isinstance (path [0 ], str ) or len (path ) > 1 ):
332
+ return t .cast (TyperCommand , cmd ).get_subcommand (* path ).callback
287
333
288
334
return cmd
289
335
@@ -406,7 +452,7 @@ def __init__(
406
452
parent .children .append (self )
407
453
408
454
409
- class _DjangoAdapterMixin (with_typehint (CoreTyperGroup )): # type: ignore[misc]
455
+ class DjangoTyperCommand (with_typehint (CoreTyperGroup )): # type: ignore[misc]
410
456
"""
411
457
A mixin we use to add additional needed contextual awareness to click Commands
412
458
and Groups.
@@ -556,15 +602,15 @@ def call_with_self(*args, **kwargs):
556
602
)
557
603
558
604
559
- class TyperCommandWrapper (_DjangoAdapterMixin , CoreTyperCommand ):
605
+ class TyperCommandWrapper (DjangoTyperCommand , CoreTyperCommand ):
560
606
"""
561
607
This class extends the TyperCommand class to work with the django-typer
562
608
interfaces. If you need to add functionality to the command class - which
563
609
you should not - you should inherit from this class.
564
610
"""
565
611
566
612
567
- class TyperGroupWrapper (_DjangoAdapterMixin , CoreTyperGroup ):
613
+ class TyperGroupWrapper (DjangoTyperCommand , CoreTyperGroup ):
568
614
"""
569
615
This class extends the TyperGroup class to work with the django-typer
570
616
interfaces. If you need to add functionality to the group class - which
@@ -2162,16 +2208,47 @@ class CommandNode:
2162
2208
"""
2163
2209
2164
2210
name : str
2165
- click_command : click .Command
2211
+ """
2212
+ The name of the group or command that this node represents.
2213
+ """
2214
+
2215
+ click_command : DjangoTyperCommand
2216
+ """
2217
+ The click command object that this node represents.
2218
+ """
2219
+
2166
2220
context : TyperContext
2221
+ """
2222
+ The Typer context object used to run this command.
2223
+ """
2224
+
2167
2225
django_command : "TyperCommand"
2226
+ """
2227
+ Back reference to the django command instance that this command belongs to.
2228
+ """
2229
+
2168
2230
parent : t .Optional ["CommandNode" ] = None
2231
+ """
2232
+ The parent node of this command node or None if this is a root node.
2233
+ """
2234
+
2169
2235
children : t .Dict [str , "CommandNode" ]
2236
+ """
2237
+ The child group and command nodes of this command node.
2238
+ """
2239
+
2240
+ @property
2241
+ def callback (self ) -> t .Callable [..., t .Any ]:
2242
+ """Get the function for this command or group"""
2243
+ cb = getattr (self .click_command ._callback , "__wrapped__" )
2244
+ return (
2245
+ MethodType (cb , self .django_command ) if self .click_command .is_method else cb
2246
+ )
2170
2247
2171
2248
def __init__ (
2172
2249
self ,
2173
2250
name : str ,
2174
- click_command : click . Command ,
2251
+ click_command : DjangoTyperCommand ,
2175
2252
context : TyperContext ,
2176
2253
django_command : "TyperCommand" ,
2177
2254
parent : t .Optional ["CommandNode" ] = None ,
@@ -2197,8 +2274,9 @@ def get_command(self, *command_path: str) -> "CommandNode":
2197
2274
Return the command node for the given command path at or below
2198
2275
this node.
2199
2276
2200
- :param command_path: the path(s) to the command to retrieve
2201
- :return: the command node at the given path
2277
+ :param command_path: the parent group names followed by the name of the command
2278
+ or group to retrieve
2279
+ :return: the command node at the given group/subcommand path
2202
2280
:raises LookupError: if the command path does not exist
2203
2281
"""
2204
2282
if not command_path :
@@ -2208,6 +2286,15 @@ def get_command(self, *command_path: str) -> "CommandNode":
2208
2286
except KeyError as err :
2209
2287
raise LookupError (f'No such command "{ command_path [0 ]} "' ) from err
2210
2288
2289
+ def __call__ (self , * args , ** kwargs ) -> t .Any :
2290
+ """
2291
+ Call this command or group directly.
2292
+
2293
+ :param args: the arguments to pass to the command or group callback
2294
+ :param kwargs: the named parameters to pass to the command or group callback
2295
+ """
2296
+ return self .callback (* args , ** kwargs )
2297
+
2211
2298
2212
2299
class TyperParser :
2213
2300
"""
@@ -2902,7 +2989,14 @@ def __init__(
2902
2989
) from rerr
2903
2990
2904
2991
def get_subcommand (self , * command_path : str ) -> CommandNode :
2905
- """Get the CommandNode"""
2992
+ """
2993
+ Retrieve a :class:`~django_typer.CommandNode` at the given command path.
2994
+
2995
+ :param command_path: the path to the command to retrieve, where each argument
2996
+ is the string name in order of a group or command in the hierarchy.
2997
+ :return: the command node at the given path
2998
+ :raises LookupError: if no group or command exists at the given path
2999
+ """
2906
3000
return self .command_tree .get_command (* command_path )
2907
3001
2908
3002
def _filter_commands (
@@ -2955,6 +3049,7 @@ def _build_cmd_tree(
2955
3049
:param node: the parent node or None if this is a root node
2956
3050
"""
2957
3051
assert cmd .name
3052
+ assert isinstance (cmd , DjangoTyperCommand )
2958
3053
ctx = Context (cmd , info_name = info_name , parent = parent , django_command = self )
2959
3054
current = CommandNode (cmd .name , cmd , ctx , self , parent = node )
2960
3055
if node :
0 commit comments