-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Accept type expressions in Parser.addini(type=...) #14751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| :func:`parser.addini <pytest.Parser.addini>` now also accepts plain Python types (``str``, ``bool``, ``int``, ``float``) and unions of them (for example ``int | str``) for its ``type`` argument, in addition to the existing string tags. A union accepts a value of any of its member types. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,10 +7,15 @@ | |
| import os | ||
| import sys | ||
| import textwrap | ||
| import types | ||
| from typing import Any | ||
| from typing import cast | ||
| from typing import final | ||
| from typing import get_args | ||
| from typing import get_origin | ||
| from typing import Literal | ||
| from typing import NoReturn | ||
| from typing import Union | ||
|
|
||
| from .exceptions import UsageError | ||
| import _pytest._io | ||
|
|
@@ -20,6 +25,44 @@ | |
|
|
||
| FILE_OR_DIR = "file_or_dir" | ||
|
|
||
| #: The string tags accepted by :meth:`Parser.addini` for its ``type`` argument. | ||
| _IniTypeTag = Literal[ | ||
| "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" | ||
| ] | ||
|
|
||
| #: An ini option type, as stored internally after normalization: either a | ||
| #: single tag, or a tuple of tags meaning "accept a value of any of these | ||
| #: types" (e.g. ``("int", "string")``, normalized from ``int | str``). | ||
| IniType = _IniTypeTag | tuple[_IniTypeTag, ...] | ||
|
|
||
| _INI_TYPE_TAGS: tuple[str, ...] = get_args(_IniTypeTag) | ||
|
|
||
| #: Maps the plain Python types accepted by :meth:`Parser.addini` for its | ||
| #: ``type`` argument to the equivalent string tag. | ||
| _INI_TYPE_TO_TAG: dict[type, _IniTypeTag] = { | ||
| str: "string", | ||
| bool: "bool", | ||
| int: "int", | ||
| float: "float", | ||
| } | ||
|
|
||
|
|
||
| def _ini_type_to_tag(name: str, type_: object) -> _IniTypeTag: | ||
| """Normalize one member of an `addini(type=...)` argument to a string tag. | ||
|
|
||
| Raise ValueError for anything that is neither a known tag nor a supported | ||
| plain Python type. | ||
| """ | ||
| if isinstance(type_, str) and type_ in _INI_TYPE_TAGS: | ||
| return cast("_IniTypeTag", type_) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the string type needed here (i.e. does replacing with |
||
| if isinstance(type_, type) and type_ in _INI_TYPE_TO_TAG: | ||
| return _INI_TYPE_TO_TAG[type_] | ||
| raise ValueError( | ||
| f"invalid type for ini option {name!r}: {type_!r} (expected one of " | ||
| f"{', '.join(repr(tag) for tag in _INI_TYPE_TAGS)}, one of the types " | ||
| "str, bool, int, float, or a union of these types such as `int | str`)" | ||
| ) | ||
|
|
||
|
|
||
| @final | ||
| class Parser: | ||
|
|
@@ -54,7 +97,7 @@ def __init__( | |
| file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*") | ||
| file_or_dir_arg.completer = filescompleter # type: ignore | ||
|
|
||
| self._inidict: dict[str, tuple[str, str, Any]] = {} | ||
| self._inidict: dict[str, tuple[str, IniType, Any]] = {} | ||
| # Maps alias -> canonical name. | ||
| self._ini_aliases: dict[str, str] = {} | ||
|
|
||
|
|
@@ -182,9 +225,9 @@ def addini( | |
| self, | ||
| name: str, | ||
| help: str, | ||
| type: Literal[ | ||
| "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" | ||
| ] | ||
| type: _IniTypeTag | ||
| | type[bool | int | float | str] | ||
| | types.UnionType | ||
|
Comment on lines
+228
to
+230
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be written more type-safely as type: _IniTypeTag | TypeForm[bool | int | float | str] | None = None,where |
||
| | None = None, | ||
| default: Any = NOTSET, | ||
| *, | ||
|
|
@@ -210,6 +253,18 @@ def addini( | |
|
|
||
| The ``float`` and ``int`` types. | ||
|
|
||
| For the scalar types, the plain Python type may be passed instead | ||
| of the string tag: ``str``, ``bool``, ``int`` and ``float`` (for | ||
| example ``type=int``). A union of these types accepts a value of | ||
| any of its members, for example ``int | str``. In TOML | ||
| configuration files the value may then be any of the member types; | ||
| string-based formats (INI files, ``-o`` overrides) coerce it to the | ||
| first member that accepts it. | ||
|
|
||
| .. versionadded:: 9.1 | ||
|
|
||
| Passing a type expression such as ``int`` or ``int | str``. | ||
|
|
||
| For ``paths`` and ``pathlist`` types, they are considered relative to the config-file. | ||
| In case the execution is happening without a config-file defined, | ||
| they will be considered relative to the current working directory (for example with ``--override-ini``). | ||
|
|
@@ -233,23 +288,26 @@ def addini( | |
| The value of configuration keys can be retrieved via a call to | ||
| :py:func:`config.getini(name) <pytest.Config.getini>`. | ||
| """ | ||
| assert type in ( | ||
| None, | ||
| "string", | ||
| "paths", | ||
| "pathlist", | ||
| "args", | ||
| "linelist", | ||
| "bool", | ||
| "int", | ||
| "float", | ||
| ) | ||
| ini_type: IniType | ||
| if type is None: | ||
| type = "string" | ||
| ini_type = "string" | ||
| elif get_origin(type) in (Union, types.UnionType): | ||
| ini_type = tuple( | ||
| _ini_type_to_tag(name, member) for member in get_args(type) | ||
| ) | ||
| else: | ||
| ini_type = _ini_type_to_tag(name, type) | ||
| if default is NOTSET: | ||
| default = get_ini_default_for_type(type) | ||
| if isinstance(ini_type, tuple): | ||
| # A union has no unambiguous implicit default; require an | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the exception message is clear enough that the comment is redundant. |
||
| # explicit one. | ||
| raise ValueError( | ||
| f"ini option {name!r} has a union type, which has no " | ||
| "implicit default; pass an explicit `default` to `addini`" | ||
| ) | ||
| default = get_ini_default_for_type(ini_type) | ||
|
|
||
| self._inidict[name] = (help, type, default) | ||
| self._inidict[name] = (help, ini_type, default) | ||
|
|
||
| for alias in aliases: | ||
| if alias in self._inidict: | ||
|
|
@@ -261,11 +319,7 @@ def addini( | |
| self._ini_aliases[alias] = name | ||
|
|
||
|
|
||
| def get_ini_default_for_type( | ||
| type: Literal[ | ||
| "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float" | ||
| ], | ||
| ) -> Any: | ||
| def get_ini_default_for_type(type: _IniTypeTag) -> Any: | ||
| """ | ||
| Used by addini to get the default value for a given config option type, when | ||
| default is not supplied. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add
: TypeAliasto these, then it will be automatically upgraded totypestatement once that's available.