Skip to content

Validate enum-valued configuration options with Literal types - #14791

Draft
Pierre-Sassoulas wants to merge 8 commits into
pytest-dev:mainfrom
Pierre-Sassoulas:ini-literal-option-types
Draft

Validate enum-valued configuration options with Literal types#14791
Pierre-Sassoulas wants to merge 8 commits into
pytest-dev:mainfrom
Pierre-Sassoulas:ini-literal-option-types

Conversation

@Pierre-Sassoulas

Copy link
Copy Markdown
Member

Follow up to #14751, which added type expressions to Parser.addini, and to the review of #14692 which asked for an audit of the other configuration options. Better reviewed commit by commit.

Register the seven options that have a fixed set of valid values with a Literal type, so the accepted values live in the registration, are validated by config.getini, and show up in pytest --help and the API reference

Invalid values now fail with ERROR: <inipath>: config option '...' expects one of ..., got '...' (exit code 4). Configure-time reads re-raise as UsageError because a raw ValueError from a pytest_configure hook surfaces as an INTERNALERROR traceback.

Minor behavior changes:

  • empty_parameter_set_mark now defaults to "skip" explicitly; an empty value is rejected instead of normalized to skip
  • junit_duration_report no longer accepts the undocumented setup/teardown values that worked by accident

Pierre-Sassoulas and others added 8 commits July 28, 2026 07:49
The option was registered as a plain string and validated by hand in
TempPathFactory.from_config with a bare ValueError, which surfaced as
an INTERNALERROR traceback during pytest_configure. Register it with
the existing RetentionType Literal so config.getini owns the choices,
re-raise as UsageError for a clean 'ERROR: ...' report, and drop the
manual check. pytest --help and the API reference now show the accepted
values, so the '(all/failed/none)' suffix in the help string goes away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option was registered as a plain string with an implicit '' default
that every consumer normalized to 'skip', validated by a hand-written
check in pytest_configure, and get_empty_parameterset_mark ended in a
LookupError fallback. Register it with an _EmptyParameterSetMark
Literal and an explicit 'skip' default: config.getini owns the choices
(re-raised as UsageError at configure time for a clean report), the
''/None tolerance and the LookupError branch become unreachable, and
the consumer is an exhaustive match with assert_never like
compare_text.py.

test_parameterset_for_parametrize_bad_markname smuggled 'bad' through
the now Literal-typed valid-values test; it is superseded by
test_parameterset_for_parametrize_marks_invalid, which asserts the
clean error end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option was registered as a plain string; an invalid value silently
fell back to the classic style in _determine_show_progress_info, and
the help string did not mention the 'times' value at all. Register it
with a _ConsoleOutputStyle Literal: pytest_configure validates eagerly
(re-raised as UsageError for a clean report, since the value is only
read lazily during reporting) and the consumer becomes an exhaustive
match with an explicit 'classic' case and assert_never instead of the
silent else fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option already had clean UsageError validation, but hand-rolled:
a get_assertion_text_diff_style wrapper matching the value against
ASSERTION_TEXT_DIFF_STYLE_* constants duplicating the
_AssertionTextDiffStyle Literal. Register it with the alias so
config.getini owns the choices, inline the wrapper at its two call
sites (pytest_configure validates eagerly like the other plugins; the
per-comparison read is a plain getini of the cached, already-validated
value), and drop the constants — mypy checks bare 'ndiff'/'block'
literals against the alias, now also in the test helpers that
previously carried the value as str.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option was registered as a plain string with no validation at all:
an invalid family passed through LogXML untouched and crashed at report
time with a KeyError on the families table, but only once a testcase
was recorded. Register it with a _JunitFamily Literal so config.getini
rejects invalid values up front (re-raised as UsageError at configure
time for a clean report), type the value end to end through
LogXML.__init__ and the test helpers, and drop the choice list from the
help string now that pytest --help renders it from the type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option was registered as a plain string with no validation: an
invalid value matched none of the branches in write_captured_output and
silently behaved like 'no', with no warning that the requested capture
never made it into the report. Register it with a _JunitLogging Literal
so config.getini rejects invalid values (re-raised as UsageError at
configure time), type the value through LogXML.__init__ and the test
helpers, and drop the choice list from the help string now that
pytest --help renders it from the type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The option was registered as a plain string with no validation: an
invalid value matched neither 'total' nor any report phase in
update_testcase_duration, so every testcase silently reported
time="0.000" ('setup' and 'teardown' also worked by accident through
the report.when comparison, undocumented). Register it with a
_JunitDurationReport Literal restricted to the documented values so
config.getini rejects anything else (re-raised as UsageError at
configure time), type the value through LogXML.__init__ and the test
helper, drop the choice list from the help string and the vestigial
'# choices=' comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The changelog file is named XXXXX pending the PR number.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Pierre-Sassoulas Pierre-Sassoulas added the type: enhancement new feature or API change, should be merged into features branch label Jul 28, 2026
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jul 28, 2026
# Eagerly validate the value; it is only read lazily when an assertion fails.
try:
config.getini("assertion_text_diff_style")
except (TypeError, ValueError) as e:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should getini create this usage-error in general?

Comment thread src/_pytest/terminal.py
else:
return False
cfg: _ConsoleOutputStyle = self.config.getini("console_output_style")
match cfg:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im wondering if we may rely on type checkers exthausiveness checks here in some way
but this may need some addition we cant have in this pr

Comment thread src/_pytest/tmpdir.py
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
)
try:
policy: RetentionType = config.getini("tmp_path_retention_policy")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im of the impression that practically all cases of this need to transform to usage-error - so i believe its a good idea to just make it that way by default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR type: enhancement new feature or API change, should be merged into features branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants