Skip to content

Commit 7e9a476

Browse files
authored
Merge pull request #211 from pyapp-org/development
Release 4.12
2 parents bfbf8d8 + 3cd2ef6 commit 7e9a476

26 files changed

+1083
-711
lines changed

HISTORY

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
4.12
2+
====
3+
4+
Changes
5+
-------
6+
7+
- Introduction of typed_settings, providing a cleaner way to access default settings
8+
that supports auto-completion and type inference. And preparing the way for more
9+
in-depth checks that ensure settings values match the expected types.
10+
11+
- Support ``Literal`` types in CLI. Maps str and int literals to choices.
12+
13+
- Support sequences of Enum values in the CLI. This is implemented via the
14+
``AppendEnumValue`` and ``AppendEnumName`` actions.
15+
16+
- Migrate from pkg_resources to the standardised builtin importlib.metadata.
17+
18+
- Fix for handling events/callbacks on classes where the parent has __slots__ defined.
19+
20+
121
4.11.0
222
======
323

docs/reference/conf/base_settings.rst

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.. automodule:: pyapp.typed_settings

docs/reference/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Contents:
1010
app
1111
checks
1212
conf
13-
conf/base_settings
13+
conf/typed-settings
1414
conf/helpers
1515
events
1616
injection

poetry.lock

Lines changed: 513 additions & 526 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pyapp"
7-
version = "4.11.0"
7+
version = "4.12.0"
88
description = "A Python application framework - Let us handle the boring stuff!"
99
authors = ["Tim Savage <[email protected]>"]
1010
license = "BSD-3-Clause"
@@ -37,6 +37,7 @@ python = "^3.8"
3737
argcomplete = "*"
3838
colorama = "*"
3939
yarl = "*"
40+
importlib_metadata = {version = "*", python = "<=3.9"}
4041

4142
pyyaml = {version = "*", optional = true }
4243
toml = {version = "*", optional = true }

src/pyapp/app/argument_actions.py

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
1515
.. autoclass:: EnumName
1616
17+
.. autoclass:: AppendEnumValue
18+
19+
.. autoclass:: AppendEnumName
20+
1721
1822
Date and Time types
1923
~~~~~~~~~~~~~~~~~~~
@@ -46,6 +50,8 @@
4650
"EnumValue",
4751
"EnumName",
4852
"EnumNameList",
53+
"AppendEnumValue",
54+
"AppendEnumName",
4955
"DateAction",
5056
"TimeAction",
5157
"DateTimeAction",
@@ -59,15 +65,15 @@ class KeyValueAction(Action):
5965
6066
Example of use::
6167
68+
@app.command
69+
def my_command(options: Mapping[str, str]):
70+
print(options)
71+
6272
@app.command
6373
@argument("--option", action=KeyValueAction)
6474
def my_command(args: Namespace):
6575
print(args.option)
6676
67-
@app.command
68-
def my_command(options: Mapping[str, str]):
69-
print(options)
70-
7177
From CLI::
7278
7379
> my_app m_command --option a=foo --option b=bar
@@ -159,7 +165,7 @@ def to_enum(self, value):
159165
class EnumValue(_EnumAction):
160166
"""
161167
Action to use an Enum as the type of an argument. In this mode the Enum is
162-
reference by value.
168+
referenced by value.
163169
164170
The choices are automatically generated for help.
165171
@@ -194,7 +200,7 @@ def to_enum(self, value):
194200
class EnumName(_EnumAction):
195201
"""
196202
Action to use an Enum as the type of an argument. In this mode the Enum is
197-
reference by name.
203+
referenced by name.
198204
199205
The choices are automatically generated for help.
200206
@@ -226,10 +232,53 @@ def to_enum(self, value):
226232
return self._enum[value]
227233

228234

229-
class EnumNameList(EnumName):
235+
def _copy_items(items):
230236
"""
231-
Action to use an Enum as the type of an argument. In this mode the Enum is
232-
reference by name and appended to a list.
237+
Extracted from argparse
238+
"""
239+
if items is None:
240+
return []
241+
242+
# The copy module is used only in the 'append' and 'append_const'
243+
# actions, and it is needed only when the default value isn't a list.
244+
# Delay its import for speeding up the common case.
245+
if isinstance(items, list):
246+
return items[:]
247+
248+
import copy # pylint: disable=import-outside-toplevel
249+
250+
return copy.copy(items)
251+
252+
253+
class _AppendEnumActionMixin(_EnumAction):
254+
"""
255+
Mixin to support appending enum items
256+
"""
257+
258+
def __call__(self, parser, namespace, values, option_string=None):
259+
items = getattr(namespace, self.dest, None)
260+
items = _copy_items(items)
261+
enum = self.to_enum(values)
262+
items.append(enum)
263+
setattr(namespace, self.dest, items)
264+
265+
def get_choices(self, choices: Union[Enum, Sequence[Enum]]):
266+
"""
267+
Get choices from the enum
268+
"""
269+
raise NotImplementedError # pragma: no cover
270+
271+
def to_enum(self, value):
272+
"""
273+
Get enum from the supplied value.
274+
"""
275+
raise NotImplementedError # pragma: no cover
276+
277+
278+
class AppendEnumValue(EnumValue, _AppendEnumActionMixin):
279+
"""
280+
Action to use an Enum as the type of an argument and to accept multiple
281+
enum values. In this mode the Enum is referenced by value.
233282
234283
The choices are automatically generated for help.
235284
@@ -241,28 +290,56 @@ class Colour(Enum):
241290
Blue = "blue"
242291
243292
@app.command
244-
@argument("--colour", type=Colour, action=EnumNameList)
293+
@argument("--colours", type=Colour, action=AppendEnumValue)
245294
def my_command(args: Namespace):
246295
print(args.colour)
247296
297+
# Or using typing definition
298+
248299
@app.command
249-
def my_command(*, colour: Sequence[Colour]):
300+
def my_command(*, colours: Sequence[Colour]):
301+
print(colours)
302+
303+
From CLI::
304+
305+
> my_app m_command --colour red --colour blue
306+
[Colour.Red, Colour.Blue]
307+
308+
.. versionadded:: 4.9
309+
310+
"""
311+
312+
313+
class AppendEnumName(EnumName, _AppendEnumActionMixin):
314+
"""
315+
Action to use an Enum as the type of an argument and to accept multiple
316+
enum values. In this mode the Enum is referenced by name.
317+
318+
The choices are automatically generated for help.
319+
320+
Example of use::
321+
322+
class Colour(Enum):
323+
Red = "red"
324+
Green = "green"
325+
Blue = "blue"
326+
327+
@app.command
328+
@argument("--colours", type=Colour, action=AppendEnumName)
329+
def my_command(args: Namespace):
250330
print(args.colour)
251331
252332
From CLI::
253333
254-
> my_app m_command --colour Red --colour Green
255-
[Colour.Red, Colour.Green]
334+
> my_app m_command --colour Red --colour Blue
335+
[Colour.Red, Colour.Blue]
256336
257-
.. versionadded:: 4.8.2
337+
.. versionadded:: 4.9
258338
259339
"""
260340

261-
def __call__(self, parser, namespace, values, option_string=None):
262-
enum = self.to_enum(values)
263-
items = getattr(namespace, self.dest, None) or []
264-
items.append(enum)
265-
setattr(namespace, self.dest, items)
341+
342+
EnumNameList = AppendEnumName
266343

267344

268345
class _DateTimeAction(Action):

src/pyapp/app/arguments.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
from pyapp.compatability import async_run
2626
from pyapp.utils import cached_property
2727

28+
from .argument_actions import AppendEnumName
2829
from .argument_actions import EnumName
29-
from .argument_actions import EnumNameList
3030
from .argument_actions import KeyValueAction
3131
from .argument_actions import TYPE_ACTIONS
3232

@@ -233,13 +233,25 @@ def _handle_generics( # pylint: disable=too-many-branches
233233
"Only Optional[TYPE] or Union[TYPE, None] are supported"
234234
)
235235

236+
elif name == "typing.Literal":
237+
choices = type_.__args__
238+
choice_type = type(choices[0])
239+
if choice_type not in (str, int):
240+
raise TypeError("Only str and int Literal types are supported")
241+
# Ensure only a single type is supplied
242+
if not all(isinstance(choice, choice_type) for choice in choices):
243+
raise TypeError("All literal values must be the same type")
244+
245+
kwargs["choices"] = type_.__args__
246+
return choice_type
247+
236248
elif issubclass(origin, Tuple):
237249
kwargs["nargs"] = len(type_.__args__)
238250

239251
elif issubclass(origin, Sequence):
240252
args = type_.__args__
241253
if len(args) == 1 and issubclass(args[0], Enum):
242-
kwargs["action"] = EnumNameList
254+
kwargs["action"] = AppendEnumName
243255
elif positional:
244256
kwargs["nargs"] = "+"
245257
else:
@@ -339,7 +351,7 @@ def from_parameter(cls, name: str, parameter: inspect.Parameter) -> "Argument":
339351
def __init__(
340352
self,
341353
*name_or_flags,
342-
action: str = None,
354+
action: Union[str, Type[argparse.Action]] = None,
343355
nargs: Union[int, str] = None,
344356
const: Any = None,
345357
default: Any = EMPTY,

src/pyapp/checks/registry.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import List
1212
from typing import NamedTuple
1313
from typing import Sequence
14+
from typing import TypeVar
1415
from typing import Union
1516

1617
from pyapp import extensions
@@ -29,6 +30,7 @@ class Tags:
2930

3031

3132
Check = Callable[[Settings], Union[CheckMessage, Sequence[CheckMessage]]]
33+
_C = TypeVar("_C", bound=Callable[[Check], Check])
3234

3335

3436
class CheckResult(NamedTuple):
@@ -47,9 +49,11 @@ class CheckRegistry(List[Check]):
4749
Registry list for checks.
4850
"""
4951

50-
def register(
51-
self, check: Check = None, *tags
52-
): # pylint: disable=keyword-arg-before-vararg
52+
def register( # pylint: disable=keyword-arg-before-vararg
53+
self,
54+
check: Union[Check, str] = None,
55+
*tags: str,
56+
) -> Union[_C, Callable[[_C], _C]]:
5357
"""
5458
Can be used as a function or a decorator. Register given function
5559
`func` labeled with given `tags`. The function should receive **kwargs

src/pyapp/conf/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@
8989
9090
.. autoclass:: HttpLoader
9191
92+
Default settings
93+
================
94+
95+
.. automodule:: pyapp.conf.base_settings
96+
:members:
97+
9298
"""
9399
import logging
94100
import os

0 commit comments

Comments
 (0)