Skip to content

Commit

Permalink
Add 'int' and 'float' ini option types
Browse files Browse the repository at this point in the history
Fixes #11381

---------

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
Co-authored-by: Florian Bruhin <me@the-compiler.org>
  • Loading branch information
3 people authored Feb 15, 2025
1 parent 62aa427 commit d126389
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 8 deletions.
17 changes: 17 additions & 0 deletions changelog/11381.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
The ``type`` parameter of the ``parser.addini`` method now accepts `"int"` and ``"float"`` parameters, facilitating the parsing of configuration values in the configuration file.

Example:

.. code-block:: python
def pytest_addoption(parser):
parser.addini("int_value", type="int", default=2, help="my int value")
parser.addini("float_value", type="float", default=4.2, help="my float value")
The `pytest.ini` file:

.. code-block:: ini
[pytest]
int_value = 3
float_value = 5.4
22 changes: 19 additions & 3 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,8 @@ def getini(self, name: str):
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
``bool`` : ``False``
``string`` : empty string ``""``
``int`` : ``0``
``float`` : ``0.0``
If neither the ``default`` nor the ``type`` parameter is passed
while registering the configuration through
Expand All @@ -1605,9 +1607,11 @@ def getini(self, name: str):

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

def _getini(self, name: str):
try:
Expand Down Expand Up @@ -1656,6 +1660,18 @@ def _getini(self, name: str):
return _strtobool(str(value).strip())
elif type == "string":
return value
elif type == "int":
if not isinstance(value, str):
raise TypeError(
f"Expected an int string for option {name} of type integer, but got: {value!r}"
) from None
return int(value)
elif type == "float":
if not isinstance(value, str):
raise TypeError(
f"Expected a float string for option {name} of type float, but got: {value!r}"
) from None
return float(value)
elif type is None:
return value
else:
Expand Down
27 changes: 25 additions & 2 deletions src/_pytest/config/argparsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ def addini(
* ``linelist``: a list of strings, separated by line breaks
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell
* ``int``: an integer
* ``float``: a floating-point number
.. versionadded:: 8.4
The ``float`` and ``int`` types.
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
In case the execution is happening without an ini-file defined,
Expand All @@ -209,7 +215,17 @@ def addini(
The value of ini-variables 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")
assert type in (
None,
"string",
"paths",
"pathlist",
"args",
"linelist",
"bool",
"int",
"float",
)
if default is NOT_SET:
default = get_ini_default_for_type(type)

Expand All @@ -218,7 +234,10 @@ def addini(


def get_ini_default_for_type(
type: Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None,
type: Literal[
"string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
]
| None,
) -> Any:
"""
Used by addini to get the default value for a given ini-option type, when
Expand All @@ -230,6 +249,10 @@ def get_ini_default_for_type(
return []
elif type == "bool":
return False
elif type == "int":
return 0
elif type == "float":
return 0.0
else:
return ""

Expand Down
17 changes: 14 additions & 3 deletions src/_pytest/config/findpaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
from pathlib import Path
import sys
from typing import TYPE_CHECKING

import iniconfig

Expand All @@ -15,6 +16,16 @@
from _pytest.pathlib import safe_exists


if TYPE_CHECKING:
from typing import Union

from typing_extensions import TypeAlias

# Even though TOML supports richer data types, all values are converted to str/list[str] during
# parsing to maintain compatibility with the rest of the configuration system.
ConfigDict: TypeAlias = dict[str, Union[str, list[str]]]


def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
the parsed object.
Expand All @@ -29,7 +40,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig:

def load_config_dict_from_file(
filepath: Path,
) -> dict[str, str | list[str]] | None:
) -> ConfigDict | None:
"""Load pytest configuration from the given file path, if supported.
Return None if the file does not contain valid pytest configuration.
Expand Down Expand Up @@ -85,7 +96,7 @@ def make_scalar(v: object) -> str | list[str]:
def locate_config(
invocation_dir: Path,
args: Iterable[Path],
) -> tuple[Path | None, Path | None, dict[str, str | list[str]]]:
) -> tuple[Path | None, Path | None, ConfigDict]:
"""Search in the list of arguments for a valid ini-file for pytest,
and return a tuple of (rootdir, inifile, cfg-dict)."""
config_names = [
Expand Down Expand Up @@ -172,7 +183,7 @@ def determine_setup(
args: Sequence[str],
rootdir_cmd_arg: str | None,
invocation_dir: Path,
) -> tuple[Path, Path | None, dict[str, str | list[str]]]:
) -> tuple[Path, Path | None, ConfigDict]:
"""Determine the rootdir, inifile and ini configuration values from the
command line arguments.
Expand Down
76 changes: 76 additions & 0 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,82 @@ def pytest_addoption(parser):
config = pytester.parseconfig()
assert config.getini("strip") is bool_val

@pytest.mark.parametrize("str_val, int_val", [("10", 10), ("no-ini", 2)])
def test_addini_int(self, pytester: Pytester, str_val: str, int_val: bool) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("ini_param", "", type="int", default=2)
"""
)
if str_val != "no-ini":
pytester.makeini(
f"""
[pytest]
ini_param={str_val}
"""
)
config = pytester.parseconfig()
assert config.getini("ini_param") == int_val

def test_addini_int_invalid(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("ini_param", "", type="int", default=2)
"""
)
pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
ini_param=["foo"]
"""
)
config = pytester.parseconfig()
with pytest.raises(
TypeError, match="Expected an int string for option ini_param"
):
_ = config.getini("ini_param")

@pytest.mark.parametrize("str_val, float_val", [("10.5", 10.5), ("no-ini", 2.2)])
def test_addini_float(
self, pytester: Pytester, str_val: str, float_val: bool
) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("ini_param", "", type="float", default=2.2)
"""
)
if str_val != "no-ini":
pytester.makeini(
f"""
[pytest]
ini_param={str_val}
"""
)
config = pytester.parseconfig()
assert config.getini("ini_param") == float_val

def test_addini_float_invalid(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
def pytest_addoption(parser):
parser.addini("ini_param", "", type="float", default=2.2)
"""
)
pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
ini_param=["foo"]
"""
)
config = pytester.parseconfig()
with pytest.raises(
TypeError, match="Expected a float string for option ini_param"
):
_ = config.getini("ini_param")

def test_addinivalue_line_existing(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
Expand Down

0 comments on commit d126389

Please sign in to comment.