Skip to content

Opt-in FunctionDefinition collection tree node (#3926) - #14769

Draft
RonnyPfannschmidt wants to merge 3 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:feature/function-definition-collection-node
Draft

Opt-in FunctionDefinition collection tree node (#3926)#14769
RonnyPfannschmidt wants to merge 3 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:feature/function-definition-collection-node

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Jul 23, 2026

Copy link
Copy Markdown
Member

Note

Stack: #14855 -> #14856 -> #14769 -> #14857 -> #14858. Review only the last commit.

Adds the collect_function_definition ini option, promoting the internal
FunctionDefinition to a real node in the collection tree.

  • hidden (default): unchanged flat layout, no node in the tree
  • pedantic: node inserted, function-level markers scoped to it
  • messy: node inserted, markers transferred down to each invocation (warns)

Node ids of the individual invocations are unchanged in all modes.

Also introduces nodes.ItemDefinition, the collector-agnostic base for "a
collector standing for one test definition". Concept only here — it carries the
name/selection semantics and nothing else. The generic parametrization protocol
on top of it is #14858.

Node-id selection is fixed as part of this: the definition node carries the bare
name while the parametrization lives on the items below it, so the argument
matcher has to look through the definition. Without this, pytest test_x.py::test_a[1] reports "no match" under pedantic/messy. Covered by a
6 selector x 3 mode regression matrix.

Part of #3926.

@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

Heads-up on a defect in this PR as it currently stands: selecting a test by node id is broken in the tree modes.

$ cat pytest.ini
[pytest]
collect_function_definition = pedantic

$ pytest -q "test_x.py::test_a[1]"
ERROR: not found: test_x.py::test_a
(no match in any of [<Module test_x.py>])

Works under hidden, fails under both pedantic and messy. So the PR description's

Invocation node ids stay flat and identical across all modes, so selection, caching and reporting are unaffected.

holds for the node id strings, but not for selection: Session._collect_and_match_nodes walks the tree comparing one name part per level, and the last part is matched as node.name == part + parametrization. With the definition interposed, alpha[1] is compared against the definition's bare name alpha and never matches, so it never descends to the items. -k and full-path invocation are fine — only ::-addressing of a specific test is affected.

The minimal fix is to let the matcher look through an ItemDefinition for the last part (match the bare name, then re-match the same part one level down against the items). I have that plus a regression matrix (6 selector shapes × 3 modes) in #14805, which is stacked on this branch — happy to move it down into this PR instead if you'd rather this one be self-contained.

Worth noting the existing comment right at that spot already anticipates this:

# A non-parameterized arg matches all parametrizations (if any).
# TODO: Remove the hacky split once the collection structure
# contains parametrization.
is_match = node.name.split("[")[0] == matchparts[0]

This PR is exactly what makes the collection structure contain the parametrization, so that split("[") hack is now removable in principle — though doing it properly probably wants the structured NodeId/ParamId from #14758 rather than more string surgery. Will follow up on that.

nicoddemus added a commit to nicoddemus/pytest that referenced this pull request Aug 5, 2026
leaf(name, params) conflated two operations that NodeId already separates
elsewhere: child() appends a name segment, with_params() attaches params
(terminalizing the id). Separating them makes the API self-documenting:

    parent.child("test_a").with_params("1-x")  # → test_x.py::test_a[1-x]

More importantly, this enables the FunctionDefinition collection node work
(pytest-dev#14769 / pytest-dev#14805) to be merged without conflict: a FunctionDefinition
collector already carries the function name in its names tuple, so
Function.__init__ can simply call definition.id.with_params(params) — no
name added, correct id produced:

    definition node           test_x.py::test_a
    definition.with_params()  test_x.py::test_a[1]   ← correct
    old leaf(name, p)         test_x.py::test_a::test_a[1]  ← name doubled

The flat layout (no FunctionDefinition) is identical in outcome because
child(name).with_params(params) produces the same string as the old
leaf(name, params).

Production callers updated: nodes.py (generic Item) and python.py (Function).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from d011e35 to 708a8e8 Compare August 10, 2026 12:58
@RonnyPfannschmidt
RonnyPfannschmidt requested review from The-Compiler and nicoddemus and removed request for nicoddemus August 10, 2026 13:31
@RonnyPfannschmidt
RonnyPfannschmidt marked this pull request as ready for review August 10, 2026 13:32
@RonnyPfannschmidt
RonnyPfannschmidt marked this pull request as draft August 11, 2026 10:07
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from 708a8e8 to c7fa6ea Compare August 11, 2026 10:54
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from c7fa6ea to 7cd892a Compare August 11, 2026 12:46
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from 7cd892a to 478b721 Compare August 11, 2026 18:34
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from 478b721 to 36095a1 Compare August 11, 2026 18:39
TestReport.from_item_and_call asserted reportinfo() yields a line
number. Items which cannot point at a line -- non-Python items
legitimately report None -- crashed with an INTERNALERROR when skipped
via a marker. Report the location without a line instead.
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from 36095a1 to 382218b Compare August 11, 2026 18:54
RonnyPfannschmidt and others added 2 commits August 11, 2026 21:25
Move IdMaker, CallSpec and _ascii_escaped_by_config out of python.py
into a new module, together with the long-str id strategy literals they
own. python.py imports them back, so _pytest.python.CallSpec and the
CallSpec2 alias keep resolving as before.

Pure move, no behaviour change. The two CollectError call sites go
through a small _collect_error() helper which imports nodes lazily, so
nodes stays free to import this module later.
Introduce the ``collect_function_definition`` ini option to promote the
internal ``FunctionDefinition`` from a transient parametrization helper to
a real collector node in the collection tree.

The option is tri-state:

- ``hidden`` (default): unchanged flat layout; the definition drives
  parametrization and is kept out of the tree.
- ``pedantic``: insert the definition node between the Module/Class and its
  test functions; function-level markers are scoped to the definition and
  each invocation owns only its callspec markers.
- ``messy``: migration stopgap -- insert the node but transfer the
  function-level markers back onto each invocation to preserve the legacy
  marker layout for code not yet prepared for the new scope; emits a header
  warning.

FunctionDefinition becomes a PyCollector (no longer a Function/Item), so it
is collected rather than run. Invocation node ids stay flat and identical
across all modes, so selection, caching and reporting are unaffected. obj
and instance resolution and getmodpath skip an interposed definition node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the feature/function-definition-collection-node branch from 382218b to 38e054a Compare August 11, 2026 19:25
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant