How putnami observe reads a Python repository
The Python detector finds the projects of a repository from its
pyproject.toml files. It reads files only: it runs no uv, Poetry, pip
or pytest, and reads no virtual environment. The change analyses use what
it finds to name the changed and dependent projects, attribute tests and
coverage, and diff the uv.lock and poetry.lock versions.
Components
Each Python project is a component of ecosystem python:
- When the root
pyproject.tomlhas a[tool.uv.workspace]table, each directory itsmembersglobs match, less those itsexcludeglobs match, that holds apyproject.tomlis a component of sourceworkspace. A glob segment is*,?or[…], or**for any number of directories. Only the root's workspace is read: a[tool.uv.workspace]in a subdirectory makes its projects manifest components, without its sources. The root is one too when it has a[project]table; a virtual workspace root, with no[project], is none. - Any other
pyproject.tomlwith a[project]or a[tool.poetry]table is a component of sourcemanifest. Apyproject.tomlthat only configures tools, such as[tool.ruff], is none.
The search skips node_modules, vendor, __pycache__, site-packages and
the directories whose name starts with ., such as .git and .venv. A
component's id is its directory relative to the repository root, . for the
root, and its name the [project] name, else the [tool.poetry] name. A
file belongs to the innermost component whose directory holds it, so the
uv.lock of a virtual workspace root belongs to no component.
Dependency edges
A project depends on another project of the repository through a dependency
it declares, in [project] dependencies or optional-dependencies, in a
[dependency-groups] group, or in a Poetry dependency table, when:
- its own
[tool.uv.sources]entry for that dependency, else the workspace root's for a workspace member, hasworkspace = true: the edge goes to the member whose project name it is, both names normalized, soShared_Libmatchesshared-lib; - that source has a local
path, such as{ path = "../shared" }, resolved against the directory of the file that declares it; - a Poetry dependency, in
[tool.poetry.dependencies],[tool.poetry.dev-dependencies]or a[tool.poetry.group.<name>.dependencies]table, has a localpath.
A registry, git or url source is no edge, nor is a path outside the
repository or to a directory that is no component. A requirement such as
shared @ file:///… is not read.
For example, in this uv workspace api and worker depend on shared:
# pyproject.toml
[tool.uv.workspace]
members = ["api", "worker", "shared"]
[tool.uv.sources]
shared = { workspace = true }# api/pyproject.toml
[project]
name = "api"
dependencies = ["fastapi>=0.115", "shared"]A change to shared marks api and worker dependent.
Reports
Run pytest with a JUnit and a Cobertura report, at the repository root or in a project:
uv run --all-packages pytest --junitxml=junit.xml --cov --cov-report=xmlpytest writes each case's class name from its test file's path relative to
its rootdir, such as api.tests.test_api.TestQuote for
api/tests/test_api.py, and the change analyses read it as that file,
relative to the repository root, to the report's project or to the report's
directory. pytest picks its rootdir from its arguments and configuration
files: when it differs from those directories, as with pytest api run at
the root of a repository whose api holds the pytest configuration, pass
--rootdir=., or write the report in the rootdir. pytest-cov writes coverage.xml with the
directory pytest ran in as its source root. See
tests and coverage.
Diagnostics
| Code | Meaning |
|---|---|
observe.python_manifest_invalid |
A pyproject.toml cannot be parsed: that file is no component. Or a members or exclude pattern of the uv workspace is not a repository-relative glob: it is ignored. |
observe.python_walk_bounded |
The search for pyproject.toml, or for the uv workspace members, stopped after 200,000 entries or could not walk the repository. |