Skip to content

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

Merged
Pierre-Sassoulas merged 9 commits into
pytest-dev:mainfrom
Pierre-Sassoulas:ini-literal-option-types
Aug 1, 2026
Merged

Validate enum-valued configuration options with Literal types#14791
Pierre-Sassoulas merged 9 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 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
Comment thread src/_pytest/assertion/__init__.py Outdated
Comment thread src/_pytest/terminal.py
Comment thread src/_pytest/tmpdir.py Outdated
@Pierre-Sassoulas
Pierre-Sassoulas marked this pull request as ready for review July 31, 2026 03:01
Pierre-Sassoulas and others added 9 commits August 1, 2026 07:22
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>
Review feedback from Ronny: instead of each call site wrapping the
TypeError/ValueError from getini into a UsageError, make getini raise
UsageError itself when the value read from the configuration file does
not match the registered type. Asking for an unregistered option name
still raises ValueError, as documented, since that is a programming
error rather than a user error.

This removes the try/except wrapping at the five configure-time call
sites, and makes lazy mid-run reads degrade into a clean usage error
as well instead of an internal error traceback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Pierre-Sassoulas
Pierre-Sassoulas force-pushed the ini-literal-option-types branch from 8f6eaa5 to 13dd9d2 Compare August 1, 2026 05:22

@bluetech bluetech left a comment

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.

All of the changes look like nice all-around improvements.
Thanks for splitting the commits :)

@Pierre-Sassoulas
Pierre-Sassoulas merged commit 1d5c403 into pytest-dev:main Aug 1, 2026
36 checks passed
@Pierre-Sassoulas
Pierre-Sassoulas deleted the ini-literal-option-types branch August 1, 2026 20:24
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.

3 participants