Skip to content

Commit 503b6cf

Browse files
committed
feat: add support for typing
1 parent 148564e commit 503b6cf

File tree

4 files changed

+64
-18
lines changed

4 files changed

+64
-18
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
66
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## Unreleased
10+
### Added
11+
- Added support for typing (tested with mypy).
12+
13+
914
## 0.1.0 - 2021-11-23
1015
### Added
1116
- First release!

argparse_subdec/subdec.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,44 @@
88
import typing as ty
99

1010

11+
F = ty.TypeVar('F', bound=ty.Callable[..., ty.Any])
12+
13+
14+
Decorator = ty.Callable[[F], F]
15+
16+
17+
DecoratorFactory = ty.Callable[..., Decorator[F]]
18+
19+
20+
AnyCallable = ty.Callable[..., ty.Any]
21+
22+
23+
AnyDecoratorFactory = DecoratorFactory[AnyCallable]
24+
25+
26+
class CommandDescriptor(ty.TypedDict):
27+
name: ty.Optional[str]
28+
fn: AnyCallable
29+
subparser_call_stack: ty.List[SubparserCallDescriptor]
30+
add_parser_args: ty.Optional[ty.Tuple[ty.Sequence[ty.Any],
31+
ty.Dict[str, ty.Any]]]
32+
33+
class SubparserCallDescriptor(ty.TypedDict):
34+
method_name: str
35+
args: ty.Tuple[ty.Any, ...]
36+
kwargs: ty.Dict[str, ty.Any]
37+
38+
39+
class SubparsersProtocol(ty.Protocol):
40+
"""
41+
A protocol class to describe the returned value of
42+
``argparse.ArgumentParser.add_subparsers()``, since the official
43+
documentation does not point to a public type for the returned value.
44+
"""
45+
def add_parser(self, name: str, **kw: ty.Any) -> argparse.ArgumentParser:
46+
... # pragma: no cover
47+
48+
1149
class SubDec:
1250
"""
1351
This class provides a way to decorate functions as subcommands for
@@ -75,37 +113,37 @@ def __init__(self,
75113
replace the underscore character ("_") when converting the name of the
76114
decorated function to a subcommand name.
77115
"""
78-
self.__decorators_cache = {}
79-
self.__commands = {}
116+
self.__decorators_cache: ty.Dict[str, AnyDecoratorFactory] = {}
117+
self.__commands: ty.Dict[AnyCallable, CommandDescriptor] = {}
80118
self.__name_prefix = name_prefix
81119
self.__fn_dest = fn_dest
82120
self.__sep = sep
83121

84-
def create_parsers(self, subparsers):
122+
def create_parsers(self, subparsers: SubparsersProtocol) -> None:
85123
"""
86124
Create subparsers by calling ``subparsers.add_parser()`` for each
87125
decorated function.
88126
"""
89127
for cmd in self.__commands.values():
90128
self.__create_parser(cmd, subparsers)
91129

92-
def cmd(self, *k, **kw):
130+
def cmd(self, *k: ty.Any, **kw: ty.Any) -> DecoratorFactory[F]:
93131
"""
94132
Special decorator to register arguments to be passed do
95133
``add_parser()``.
96134
"""
97-
def decorator(fn):
135+
def decorator(fn: F) -> F:
98136
cmd = self.__get_command(fn)
99137
cmd['add_parser_args'] = (k, kw)
100138
return fn
101139
return decorator
102140

103-
def __getattr__(self, name: str):
141+
def __getattr__(self, name: str) -> AnyDecoratorFactory:
104142
if name in self.__decorators_cache:
105143
return self.__decorators_cache[name]
106144

107-
def decorator_wrapper(*k, **kw):
108-
def decorator(fn):
145+
def decorator_wrapper(*k: ty.Any, **kw: ty.Any) -> Decorator[F]:
146+
def decorator(fn: F) -> F:
109147
cmd = self.__get_command(fn)
110148
cmd['subparser_call_stack'].append({
111149
'method_name': name,
@@ -118,7 +156,7 @@ def decorator(fn):
118156
self.__decorators_cache[name] = decorator_wrapper
119157
return decorator_wrapper
120158

121-
def __get_command(self, fn: ty.Callable):
159+
def __get_command(self, fn: AnyCallable) -> CommandDescriptor:
122160
if fn not in self.__commands:
123161
self.__commands[fn] = {
124162
'name': None,
@@ -128,7 +166,10 @@ def __get_command(self, fn: ty.Callable):
128166
}
129167
return self.__commands[fn]
130168

131-
def __create_parser(self, cmd: dict, subparsers: ty.Any):
169+
def __create_parser(self,
170+
cmd: CommandDescriptor,
171+
subparsers: SubparsersProtocol,
172+
) -> None:
132173
name = cmd['name']
133174
if not name:
134175
name = cmd['fn'].__name__
@@ -137,7 +178,7 @@ def __create_parser(self, cmd: dict, subparsers: ty.Any):
137178
if self.__sep is not None:
138179
name = name.replace('_', self.__sep)
139180

140-
if cmd['add_parser_args']:
181+
if cmd['add_parser_args'] is not None:
141182
add_parser_args, add_parser_kwargs = cmd['add_parser_args']
142183
if not add_parser_args:
143184
add_parser_args = (name,)

mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[mypy]
22
strict = True
3-
files = argparse_subdec/**.py
3+
files = argparse_subdec/**.py,tests/**.py

tests/test_subdec.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,31 @@
33
import argparse_subdec
44

55

6-
def test_command_decorator():
6+
def test_command_decorator() -> None:
77
sd = argparse_subdec.SubDec()
88

99
@sd.cmd()
10-
def foo():
10+
def foo() -> None:
1111
pass
1212

1313
@sd.cmd()
14-
def two_words():
14+
def two_words() -> None:
1515
pass
1616

1717
@sd.add_argument('--option-for-bar')
1818
@sd.add_argument('--another-option-for-bar')
19-
def bar():
19+
def bar() -> None:
2020
pass
2121

2222
@sd.cmd('changed-name')
2323
@sd.add_argument('--option')
24-
def original_name():
24+
def original_name() -> None:
2525
pass
2626

2727
# Test changing order of decorators
2828
@sd.add_argument('--option')
2929
@sd.cmd('changed-name-2')
30-
def another_original_name():
30+
def another_original_name() -> None:
3131
pass
3232

3333
parser = argparse.ArgumentParser()

0 commit comments

Comments
 (0)