Skip to content

Commit d126389

Browse files
harmin-parranicoddemusThe-Compiler
authored
Add 'int' and 'float' ini option types
Fixes #11381 --------- Co-authored-by: Bruno Oliveira <[email protected]> Co-authored-by: Florian Bruhin <[email protected]>
1 parent 62aa427 commit d126389

File tree

5 files changed

+151
-8
lines changed

5 files changed

+151
-8
lines changed

changelog/11381.improvement.rst

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
The ``type`` parameter of the ``parser.addini`` method now accepts `"int"` and ``"float"`` parameters, facilitating the parsing of configuration values in the configuration file.
2+
3+
Example:
4+
5+
.. code-block:: python
6+
7+
def pytest_addoption(parser):
8+
parser.addini("int_value", type="int", default=2, help="my int value")
9+
parser.addini("float_value", type="float", default=4.2, help="my float value")
10+
11+
The `pytest.ini` file:
12+
13+
.. code-block:: ini
14+
15+
[pytest]
16+
int_value = 3
17+
float_value = 5.4

src/_pytest/config/__init__.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,8 @@ def getini(self, name: str):
15871587
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
15881588
``bool`` : ``False``
15891589
``string`` : empty string ``""``
1590+
``int`` : ``0``
1591+
``float`` : ``0.0``
15901592
15911593
If neither the ``default`` nor the ``type`` parameter is passed
15921594
while registering the configuration through
@@ -1605,9 +1607,11 @@ def getini(self, name: str):
16051607

16061608
# Meant for easy monkeypatching by legacypath plugin.
16071609
# Can be inlined back (with no cover removed) once legacypath is gone.
1608-
def _getini_unknown_type(self, name: str, type: str, value: str | list[str]):
1609-
msg = f"unknown configuration type: {type}"
1610-
raise ValueError(msg, value) # pragma: no cover
1610+
def _getini_unknown_type(self, name: str, type: str, value: object):
1611+
msg = (
1612+
f"Option {name} has unknown configuration type {type} with value {value!r}"
1613+
)
1614+
raise ValueError(msg) # pragma: no cover
16111615

16121616
def _getini(self, name: str):
16131617
try:
@@ -1656,6 +1660,18 @@ def _getini(self, name: str):
16561660
return _strtobool(str(value).strip())
16571661
elif type == "string":
16581662
return value
1663+
elif type == "int":
1664+
if not isinstance(value, str):
1665+
raise TypeError(
1666+
f"Expected an int string for option {name} of type integer, but got: {value!r}"
1667+
) from None
1668+
return int(value)
1669+
elif type == "float":
1670+
if not isinstance(value, str):
1671+
raise TypeError(
1672+
f"Expected a float string for option {name} of type float, but got: {value!r}"
1673+
) from None
1674+
return float(value)
16591675
elif type is None:
16601676
return value
16611677
else:

src/_pytest/config/argparsing.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ def addini(
191191
* ``linelist``: a list of strings, separated by line breaks
192192
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
193193
* ``pathlist``: a list of ``py.path``, separated as in a shell
194+
* ``int``: an integer
195+
* ``float``: a floating-point number
196+
197+
.. versionadded:: 8.4
198+
199+
The ``float`` and ``int`` types.
194200
195201
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
196202
In case the execution is happening without an ini-file defined,
@@ -209,7 +215,17 @@ def addini(
209215
The value of ini-variables can be retrieved via a call to
210216
:py:func:`config.getini(name) <pytest.Config.getini>`.
211217
"""
212-
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
218+
assert type in (
219+
None,
220+
"string",
221+
"paths",
222+
"pathlist",
223+
"args",
224+
"linelist",
225+
"bool",
226+
"int",
227+
"float",
228+
)
213229
if default is NOT_SET:
214230
default = get_ini_default_for_type(type)
215231

@@ -218,7 +234,10 @@ def addini(
218234

219235

220236
def get_ini_default_for_type(
221-
type: Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None,
237+
type: Literal[
238+
"string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
239+
]
240+
| None,
222241
) -> Any:
223242
"""
224243
Used by addini to get the default value for a given ini-option type, when
@@ -230,6 +249,10 @@ def get_ini_default_for_type(
230249
return []
231250
elif type == "bool":
232251
return False
252+
elif type == "int":
253+
return 0
254+
elif type == "float":
255+
return 0.0
233256
else:
234257
return ""
235258

src/_pytest/config/findpaths.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
from pathlib import Path
77
import sys
8+
from typing import TYPE_CHECKING
89

910
import iniconfig
1011

@@ -15,6 +16,16 @@
1516
from _pytest.pathlib import safe_exists
1617

1718

19+
if TYPE_CHECKING:
20+
from typing import Union
21+
22+
from typing_extensions import TypeAlias
23+
24+
# Even though TOML supports richer data types, all values are converted to str/list[str] during
25+
# parsing to maintain compatibility with the rest of the configuration system.
26+
ConfigDict: TypeAlias = dict[str, Union[str, list[str]]]
27+
28+
1829
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
1930
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
2031
the parsed object.
@@ -29,7 +40,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
2940

3041
def load_config_dict_from_file(
3142
filepath: Path,
32-
) -> dict[str, str | list[str]] | None:
43+
) -> ConfigDict | None:
3344
"""Load pytest configuration from the given file path, if supported.
3445
3546
Return None if the file does not contain valid pytest configuration.
@@ -85,7 +96,7 @@ def make_scalar(v: object) -> str | list[str]:
8596
def locate_config(
8697
invocation_dir: Path,
8798
args: Iterable[Path],
88-
) -> tuple[Path | None, Path | None, dict[str, str | list[str]]]:
99+
) -> tuple[Path | None, Path | None, ConfigDict]:
89100
"""Search in the list of arguments for a valid ini-file for pytest,
90101
and return a tuple of (rootdir, inifile, cfg-dict)."""
91102
config_names = [
@@ -172,7 +183,7 @@ def determine_setup(
172183
args: Sequence[str],
173184
rootdir_cmd_arg: str | None,
174185
invocation_dir: Path,
175-
) -> tuple[Path, Path | None, dict[str, str | list[str]]]:
186+
) -> tuple[Path, Path | None, ConfigDict]:
176187
"""Determine the rootdir, inifile and ini configuration values from the
177188
command line arguments.
178189

testing/test_config.py

+76
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,82 @@ def pytest_addoption(parser):
848848
config = pytester.parseconfig()
849849
assert config.getini("strip") is bool_val
850850

851+
@pytest.mark.parametrize("str_val, int_val", [("10", 10), ("no-ini", 2)])
852+
def test_addini_int(self, pytester: Pytester, str_val: str, int_val: bool) -> None:
853+
pytester.makeconftest(
854+
"""
855+
def pytest_addoption(parser):
856+
parser.addini("ini_param", "", type="int", default=2)
857+
"""
858+
)
859+
if str_val != "no-ini":
860+
pytester.makeini(
861+
f"""
862+
[pytest]
863+
ini_param={str_val}
864+
"""
865+
)
866+
config = pytester.parseconfig()
867+
assert config.getini("ini_param") == int_val
868+
869+
def test_addini_int_invalid(self, pytester: Pytester) -> None:
870+
pytester.makeconftest(
871+
"""
872+
def pytest_addoption(parser):
873+
parser.addini("ini_param", "", type="int", default=2)
874+
"""
875+
)
876+
pytester.makepyprojecttoml(
877+
"""
878+
[tool.pytest.ini_options]
879+
ini_param=["foo"]
880+
"""
881+
)
882+
config = pytester.parseconfig()
883+
with pytest.raises(
884+
TypeError, match="Expected an int string for option ini_param"
885+
):
886+
_ = config.getini("ini_param")
887+
888+
@pytest.mark.parametrize("str_val, float_val", [("10.5", 10.5), ("no-ini", 2.2)])
889+
def test_addini_float(
890+
self, pytester: Pytester, str_val: str, float_val: bool
891+
) -> None:
892+
pytester.makeconftest(
893+
"""
894+
def pytest_addoption(parser):
895+
parser.addini("ini_param", "", type="float", default=2.2)
896+
"""
897+
)
898+
if str_val != "no-ini":
899+
pytester.makeini(
900+
f"""
901+
[pytest]
902+
ini_param={str_val}
903+
"""
904+
)
905+
config = pytester.parseconfig()
906+
assert config.getini("ini_param") == float_val
907+
908+
def test_addini_float_invalid(self, pytester: Pytester) -> None:
909+
pytester.makeconftest(
910+
"""
911+
def pytest_addoption(parser):
912+
parser.addini("ini_param", "", type="float", default=2.2)
913+
"""
914+
)
915+
pytester.makepyprojecttoml(
916+
"""
917+
[tool.pytest.ini_options]
918+
ini_param=["foo"]
919+
"""
920+
)
921+
config = pytester.parseconfig()
922+
with pytest.raises(
923+
TypeError, match="Expected a float string for option ini_param"
924+
):
925+
_ = config.getini("ini_param")
926+
851927
def test_addinivalue_line_existing(self, pytester: Pytester) -> None:
852928
pytester.makeconftest(
853929
"""

0 commit comments

Comments
 (0)