Skip to content

Commit 7c7bdf4

Browse files
authored
Sanitize ini-options default handling #11282 (#11594)
Fixes #11282
1 parent 6fe4391 commit 7c7bdf4

File tree

4 files changed

+126
-6
lines changed

4 files changed

+126
-6
lines changed

changelog/11282.breaking.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Sanitized the handling of the ``default`` parameter when defining configuration options.
2+
3+
Previously if ``default`` was not supplied for :meth:`parser.addini <pytest.Parser.addini>` and the configuration option value was not defined in a test session, then calls to :func:`config.getini <pytest.Config.getini>` returned an *empty list* or an *empty string* depending on whether ``type`` was supplied or not respectively, which is clearly incorrect. Also, ``None`` was not honored even if ``default=None`` was used explicitly while defining the option.
4+
5+
Now the behavior of :meth:`parser.addini <pytest.Parser.addini>` is as follows:
6+
7+
* If ``default`` is NOT passed but ``type`` is provided, then a type-specific default will be returned. For example ``type=bool`` will return ``False``, ``type=str`` will return ``""``, etc.
8+
* If ``default=None`` is passed and the option is not defined in a test session, then ``None`` will be returned, regardless of the ``type``.
9+
* If neither ``default`` nor ``type`` are provided, assume ``type=str`` and return ``""`` as default (this is as per previous behavior).
10+
11+
The team decided to not introduce a deprecation period for this change, as doing so would be complicated both in terms of communicating this to the community as well as implementing it, and also because the team believes this change should not break existing plugins except in rare cases.

src/_pytest/config/__init__.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,27 @@ def addinivalue_line(self, name: str, line: str) -> None:
14951495
def getini(self, name: str):
14961496
"""Return configuration value from an :ref:`ini file <configfiles>`.
14971497
1498+
If a configuration value is not defined in an
1499+
:ref:`ini file <configfiles>`, then the ``default`` value provided while
1500+
registering the configuration through
1501+
:func:`parser.addini <pytest.Parser.addini>` will be returned.
1502+
Please note that you can even provide ``None`` as a valid
1503+
default value.
1504+
1505+
If ``default`` is not provided while registering using
1506+
:func:`parser.addini <pytest.Parser.addini>`, then a default value
1507+
based on the ``type`` parameter passed to
1508+
:func:`parser.addini <pytest.Parser.addini>` will be returned.
1509+
The default values based on ``type`` are:
1510+
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
1511+
``bool`` : ``False``
1512+
``string`` : empty string ``""``
1513+
1514+
If neither the ``default`` nor the ``type`` parameter is passed
1515+
while registering the configuration through
1516+
:func:`parser.addini <pytest.Parser.addini>`, then the configuration
1517+
is treated as a string and a default empty string '' is returned.
1518+
14981519
If the specified name hasn't been registered through a prior
14991520
:func:`parser.addini <pytest.Parser.addini>` call (usually from a
15001521
plugin), a ValueError is raised.
@@ -1521,11 +1542,7 @@ def _getini(self, name: str):
15211542
try:
15221543
value = self.inicfg[name]
15231544
except KeyError:
1524-
if default is not None:
1525-
return default
1526-
if type is None:
1527-
return ""
1528-
return []
1545+
return default
15291546
else:
15301547
value = override_value
15311548
# Coerce the values based on types.

src/_pytest/config/argparsing.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
FILE_OR_DIR = "file_or_dir"
2828

2929

30+
class NotSet:
31+
def __repr__(self) -> str:
32+
return "<notset>"
33+
34+
35+
NOT_SET = NotSet()
36+
37+
3038
@final
3139
class Parser:
3240
"""Parser for command line arguments and ini-file values.
@@ -176,7 +184,7 @@ def addini(
176184
type: Optional[
177185
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
178186
] = None,
179-
default: Any = None,
187+
default: Any = NOT_SET,
180188
) -> None:
181189
"""Register an ini-file option.
182190
@@ -203,10 +211,30 @@ def addini(
203211
:py:func:`config.getini(name) <pytest.Config.getini>`.
204212
"""
205213
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
214+
if default is NOT_SET:
215+
default = get_ini_default_for_type(type)
216+
206217
self._inidict[name] = (help, type, default)
207218
self._ininames.append(name)
208219

209220

221+
def get_ini_default_for_type(
222+
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
223+
) -> Any:
224+
"""
225+
Used by addini to get the default value for a given ini-option type, when
226+
default is not supplied.
227+
"""
228+
if type is None:
229+
return ""
230+
elif type in ("paths", "pathlist", "args", "linelist"):
231+
return []
232+
elif type == "bool":
233+
return False
234+
else:
235+
return ""
236+
237+
210238
class ArgumentError(Exception):
211239
"""Raised if an Argument instance is created with invalid or
212240
inconsistent arguments."""

testing/test_config.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import textwrap
77
from pathlib import Path
8+
from typing import Any
89
from typing import Dict
910
from typing import List
1011
from typing import Sequence
@@ -21,6 +22,7 @@
2122
from _pytest.config import ConftestImportFailure
2223
from _pytest.config import ExitCode
2324
from _pytest.config import parse_warning_filter
25+
from _pytest.config.argparsing import get_ini_default_for_type
2426
from _pytest.config.exceptions import UsageError
2527
from _pytest.config.findpaths import determine_setup
2628
from _pytest.config.findpaths import get_common_ancestor
@@ -857,6 +859,68 @@ def pytest_addoption(parser):
857859
assert len(values) == 2
858860
assert values == ["456", "123"]
859861

862+
def test_addini_default_values(self, pytester: Pytester) -> None:
863+
"""Tests the default values for configuration based on
864+
config type
865+
"""
866+
867+
pytester.makeconftest(
868+
"""
869+
def pytest_addoption(parser):
870+
parser.addini("linelist1", "", type="linelist")
871+
parser.addini("paths1", "", type="paths")
872+
parser.addini("pathlist1", "", type="pathlist")
873+
parser.addini("args1", "", type="args")
874+
parser.addini("bool1", "", type="bool")
875+
parser.addini("string1", "", type="string")
876+
parser.addini("none_1", "", type="linelist", default=None)
877+
parser.addini("none_2", "", default=None)
878+
parser.addini("no_type", "")
879+
"""
880+
)
881+
882+
config = pytester.parseconfig()
883+
# default for linelist, paths, pathlist and args is []
884+
value = config.getini("linelist1")
885+
assert value == []
886+
value = config.getini("paths1")
887+
assert value == []
888+
value = config.getini("pathlist1")
889+
assert value == []
890+
value = config.getini("args1")
891+
assert value == []
892+
# default for bool is False
893+
value = config.getini("bool1")
894+
assert value is False
895+
# default for string is ""
896+
value = config.getini("string1")
897+
assert value == ""
898+
# should return None if None is explicity set as default value
899+
# irrespective of the type argument
900+
value = config.getini("none_1")
901+
assert value is None
902+
value = config.getini("none_2")
903+
assert value is None
904+
# in case no type is provided and no default set
905+
# treat it as string and default value will be ""
906+
value = config.getini("no_type")
907+
assert value == ""
908+
909+
@pytest.mark.parametrize(
910+
"type, expected",
911+
[
912+
pytest.param(None, "", id="None"),
913+
pytest.param("string", "", id="string"),
914+
pytest.param("paths", [], id="paths"),
915+
pytest.param("pathlist", [], id="pathlist"),
916+
pytest.param("args", [], id="args"),
917+
pytest.param("linelist", [], id="linelist"),
918+
pytest.param("bool", False, id="bool"),
919+
],
920+
)
921+
def test_get_ini_default_for_type(self, type: Any, expected: Any) -> None:
922+
assert get_ini_default_for_type(type) == expected
923+
860924
def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
861925
"""Give an error if --confcutdir is not a valid directory (#2078)"""
862926
exp_match = r"^--confcutdir must be a directory, given: "

0 commit comments

Comments
 (0)