Skip to content

[RFC] structured NamedTuples for code locations#8056

Draft
RonnyPfannschmidt wants to merge 4 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:location-as-tuple
Draft

[RFC] structured NamedTuples for code locations#8056
RonnyPfannschmidt wants to merge 4 commits into
pytest-dev:mainfrom
RonnyPfannschmidt:location-as-tuple

Conversation

@RonnyPfannschmidt
Copy link
Copy Markdown
Member

@RonnyPfannschmidt RonnyPfannschmidt commented Nov 21, 2020

Introduces CodeLocation and ItemLocation NamedTuples to replace the unstructured strings and bare tuples currently used for code locations. Both types store a 0-based lineindex field and expose a 1-based .lineno property, making the convention explicit rather than something each call site has to remember.

CodeLocation (replaces str from getlocation())

Two fields: path: Path, lineindex: int. Used for fixture locations in error messages and --fixtures / --fixtures-per-test output. __str__ produces path:lineno (1-based).

getlocation() previously stored co_firstlineno + 1 — an off-by-one since co_firstlineno is already 1-based. Now stores co_firstlineno - 1 (0-based) and lets the .lineno property add +1 back correctly.

ItemLocation (replaces tuple[str, int | None, str] from Item.location)

Three fields: path: str, lineindex: int | None, testname: str. Returned by Item.location, stored on TestReport. Tuple subclass — all existing indexing, unpacking, and equality comparisons continue to work unchanged.

Other changes

  • write_item in fixtures.py now uses item.location instead of getlocation(item.function, ...), removing the item.function assumption that fails for DoctestItem and custom Item subclasses.
  • BaseReport.location / TestReport.location typed as ItemLocation. TestReport.__init__ coerces plain tuples for backwards compatibility.
  • _report_kwargs_from_json reconstructs ItemLocation after JSON round-trip.
  • Hook signatures, reportinfo() contract, and JSON wire format are all unchanged.

@RonnyPfannschmidt RonnyPfannschmidt added type: proposal proposal for a new feature, often to gather opinions or design the API around the new feature type: backward compatibility might present some backward compatibility issues which should be carefully noted in the changelog topic: typing type-annotation issue labels Nov 21, 2020
Copy link
Copy Markdown
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

Looks good so far!

So the idea is to not change Item.location's return value or any other exposed value, just use CodeLocation internally?

Comment thread src/_pytest/compat.py Outdated
lineno = function.__code__.co_firstlineno

# TODO: this cycle indicates a larger issue
from .pathlib import bestrelpath
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.

Hmm indeed, perhaps getlocation can be moved to pathlib then? Makes sense there, is all about obtaining the "path" location to a function.

Comment thread src/_pytest/python.py Outdated
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
if verbose > 0:
tw.write(" -- %s" % bestrel, yellow=True)
tw.write(" -- %s" % str(bestrel), yellow=True)
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.

Is the str redudant?

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.

since namedtuple isinstance tuple, %-formatting will try and unpack it. if this converts to f-string you won't need str(...)

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.

Good execuse to switch to an fstring :)

Copy link
Copy Markdown
Member

@asottile asottile left a comment

Choose a reason for hiding this comment

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

looks good!

Comment thread src/_pytest/compat.py Outdated
Comment thread src/_pytest/python.py Outdated
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
if verbose > 0:
tw.write(" -- %s" % bestrel, yellow=True)
tw.write(" -- %s" % str(bestrel), yellow=True)
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.

since namedtuple isinstance tuple, %-formatting will try and unpack it. if this converts to f-string you won't need str(...)

Copy link
Copy Markdown
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

Nice cleanup!

Comment thread src/_pytest/pathlib.py

import py

from _pytest.compat import assert_never
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.

What is the reason for this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

import cycle maangement

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.

I thought

from _pytest.compat import assert_never

and

import _pytest.compat

are equivalent from an import-cycle perspective. Maybe not...

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.

the latter delays the access of the module, allowing ~some cycles to occur (as long as there's no attribute access the cycle will import a "partially initialized module" which gets finalized later)

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.

Interesting, didn't know about this partially initialized state.

Comment thread src/_pytest/compat.py Outdated
Comment thread src/_pytest/python.py Outdated
Comment thread src/_pytest/python.py Outdated
Comment thread src/_pytest/python.py Outdated
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")

def get_best_relpath(func):
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.

IMO we can just inline this function now.

Comment thread src/_pytest/python.py Outdated
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
if verbose > 0:
tw.write(" -- %s" % bestrel, yellow=True)
tw.write(" -- %s" % str(bestrel), yellow=True)
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.

Good execuse to switch to an fstring :)

@RonnyPfannschmidt
Copy link
Copy Markdown
Member Author

@nicoddemus for practical reasons we have to deprecate node.location and reportinfo() - its a hard to fix api

adding a qualified name to the code location is no longer needed/sensible on modern python, where the qualified name is available

pytest itself computes it by a listchain from hell

i pushed my current state as im currently error blind on location determination wrt allowing/disallowing escape, the testsuite should be made to pass

Comment thread src/_pytest/compat.py Outdated
Comment thread src/_pytest/compat.py Outdated
Comment thread src/_pytest/pathlib.py

import py

from _pytest.compat import assert_never
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.

Interesting, didn't know about this partially initialized state.

Comment thread src/_pytest/python.py Outdated
Comment thread src/_pytest/python.py Outdated
Base automatically changed from master to main March 9, 2021 20:40
@RonnyPfannschmidt RonnyPfannschmidt marked this pull request as draft November 16, 2021 12:51
Co-authored-by: Cursor <cursoragent@cursor.com>
…0-based

- CodeLocation: rename field `lineno` to `lineindex` (0-based stored),
  add `.lineno` property (1-based) and `__str__` using 1-based display.
  `getlocation()` now stores `co_firstlineno - 1` as lineindex.

- ItemLocation: new NamedTuple (path, lineindex, testname) that replaces
  the bare `tuple[str, int | None, str]` returned by `Item.location`.
  Has `.lineno` property (1-based) and `__str__` for display.

- Item.location returns ItemLocation instead of a plain tuple.
  Runtime-compatible: ItemLocation is a tuple subclass.

- TestReport/BaseReport: location typed as ItemLocation, with coercion
  in __init__ for backwards compat. JSON deserialization in
  _report_kwargs_from_json reconstructs ItemLocation.

- write_item in fixtures.py now uses item.location instead of
  getlocation(item.function, ...), eliminating the item.function
  assumption that broke for DoctestItem and custom Item subclasses.

- Fixes off-by-one in getlocation display (was co_firstlineno + 1,
  now correctly co_firstlineno via 0-based storage + 1-based property).

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
@RonnyPfannschmidt RonnyPfannschmidt changed the title [RFC] code location as a namedtuple feat: structured CodeLocation and ItemLocation NamedTuples for code locations May 24, 2026
@RonnyPfannschmidt RonnyPfannschmidt changed the title feat: structured CodeLocation and ItemLocation NamedTuples for code locations [RFC] structured NamedTuples for code locations May 24, 2026
pytest-xdist (<= 3.x) sends the location parameter from
pytest_runtest_logstart/logfinish directly over execnet, which
cannot serialize NamedTuple subclasses. Convert to plain tuple
at the hook call site and in _report_to_json serialization.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label May 24, 2026
@RonnyPfannschmidt RonnyPfannschmidt added this to the 10.0 milestone May 24, 2026
- Remove unused allow_escape parameter from getlocation()
  (added in this branch, never called with True)
- Add ItemLocation.__str__ coverage tests for None lineindex
- Add changelog entry for the feature

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
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 topic: typing type-annotation issue type: backward compatibility might present some backward compatibility issues which should be carefully noted in the changelog type: proposal proposal for a new feature, often to gather opinions or design the API around the new feature

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

4 participants