Skip to content

feat(python): read .python-version when resolving the version - #288

Merged
CalvinAllen merged 1 commit into
mainfrom
feat/python/version-file
Oct 22, 2026
Merged

CalvinAllen merged 1 commit into
mainfrom
feat/python/version-file

Conversation

@CalvinAllen

@CalvinAllen CalvinAllen commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

dtvem now reads .python-version when resolving the Python version. It's the file pyenv, uv and actions/setup-python use to pin a project's Python. A repository that pins Python for CI, or already carries the file for pyenv or uv, gets the same version on a dtvem machine without a second, dtvem-specific file.

It reuses the version-file mechanism from #286 (#283).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Test coverage improvement
  • CI/CD or build process change

Related Issues

Resolves #284
Relates to #283, #285, #287

Changes Made

  • Where and precedence: the same upward walk as .dtvem/runtimes.json, so subdirectories inherit the file. The nearest directory wins. Within one directory .dtvem/runtimes.json comes first, then .python-version, and the global version stays the fallback.
  • Syntax:
    • An exact (3.12.4) or partial (3.12) version, with pyenv's optional python- prefix (pyenv-version-name strips it). A partial version resolves to the highest installed match.
    • Blank lines and # comments are skipped.
    • pyenv allows several versions, one per line. dtvem runs one version, so it uses the first, and the first word of that line, as pyenv does.
  • Unsupported forms are reported, not replaced by the global version:
    • system
    • pyenv-virtualenv names and pointers (my-venv, 3.10/envs/venv)
    • uv requests that aren't a plain version number (pypy, cpython@3.12)
  • Not installed / visibility: these come from feat(node): read .node-version and .nvmrc when resolving the version #286 unchanged. The shim names the file and suggests dtvem install python <version>, and dtvem current shows the file in its Source column. pip, python3 and the other Python shims follow the file too.
  • Shared rules: versionFiles now maps each runtime to a versionFileRules (its file names plus an optional version prefix: v for Node.js, python- for Python), so feat(ruby): read .ruby-version when resolving the version #285 and feat(config): read .tool-versions when resolving versions #287 can extend it.
  • Two small changes to Node.js parsing, from the shared parser:
    • Only the first word of the version line is used, so 22.12.0 # note and 22.12.0 extra both read as 22.12.0. Trailing text used to make the file an error.
    • The unsupported-version message drops "not aliases" and now reads … asks for "lts/*": dtvem supports only version numbers (x, x.y or x.y.z), which also fits system and virtualenv names.
  • Docs: README feature line, dtvem current --help, and the CLAUDE.md "Version Resolution" section.

Testing

Test Environment

  • OS: Windows 11
  • Go Version: 1.25.4 (module targets 1.23)
  • dtvem Version: built from this branch; resolved against a real 0.6.5 install with Python 3.8.9, 3.9.9 and 3.12.12 installed, 3.12.12 global

Test Steps

  1. ./rnr check: gofmt, golangci-lint (0 issues), and all tests pass.
  2. New unit tests in src/internal/config/versionfile_test.go:
    • Python parsing: exact, partial, python- prefix, comments, multi-line, CRLF.
    • Unsupported forms: system, virtualenv names and pointers, uv implementation requests, v prefix, empty file.
    • Resolution: partial matching from a subdirectory, precedence against runtimes.json in the same directory and in a parent.
    • Each runtime reads only its own files: Ruby sees neither file, Node reads .node-version, and Python reports its unsupported .python-version.
    • Existing Node tests cover the first-word change.
  3. End-to-end: built the CLI and shim into a temp directory, put the shim there as python.exe and pip.exe, and ran them against the real install from temp project directories.

Test Results

In the directory python --version
no file Python 3.12.12 (global)
.python-version = 3.9.9 Python 3.9.9
# pinned + 3.8, run from sub/deep Python 3.8.9
python-3.9.9 Python 3.9.9
3.9.9 + 3.8.9 (pyenv multi-line) Python 3.9.9
.dtvem/runtimes.json = 3.8.9 + .python-version = 3.9.9 Python 3.8.9
system error naming the file, exit 1
3.10/envs/venv error naming the file, exit 1
3.11 (not installed) "configured but not installed", Set by: the file, dtvem install python 3.11, exit 1

With .python-version = 3.9, pip --version ran pip from the 3.9.9 install, and dtvem current python showed 3.9.9 with the file as its Source.

uv projects

I also ran real uv projects (uv 0.9.22). uv's cache and install folders were in a temp folder, downloads were disabled, and only the shim being tested was on PATH:

uv project pinned to… Old 0.6.5 shim This branch
a version dtvem has (3.9) uv sync finds no 3.9: the shim only ever offers the global 3.12.12 uv sync builds .venv on dtvem's 3.9.9 (its pyvenv.cfg home is dtvem's 3.9.9 install)
a version only uv manages (3.14) uses uv's 3.14.4 uses uv's 3.14.4 (same)
a version neither has (3.11) "No interpreter found for Python 3.11" uv skips the failing shim. With downloads disabled, the error names the shim; with uv's default settings it downloads 3.11 exactly as before

The last row was checked against uv's source. A shim that exits non-zero is a non-critical "bad interpreter": uv skips it and keeps searching, and a non-critical error still lets uv download a Python.

An activated venv and uv run bypass dtvem's shims entirely; unchanged. In a folder with only a .venv (no pin), python still runs the global version. Matching the venv's version is #289.

Checklist

  • My code follows the Google Go Style Guide
  • I have run golangci-lint run and fixed all issues (includes gofmt, govet, and 40+ linters)
  • I have run go mod tidy to clean up dependencies (no dependency changes in this PR)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes (go test ./...)
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation (README.md, CONTRIBUTING.md, etc.) if needed
  • My changes generate no new warnings or errors
  • I have checked my code works on multiple platforms (Windows, macOS, Linux) if applicable (tested on Windows; CI covers macOS and Linux)
  • I have added appropriate error handling
  • I have followed conventional commit format in my commit messages

Breaking Changes

None intended. As with .nvmrc in #286, a .python-version in any parent directory now affects the Python shims. One holding system or a virtualenv name, such as a pyenv user's ~/.python-version, makes them stop with an error where they used to run the global version.

Additional Notes

Out of scope, as the issue lists: pyproject.toml requires-python, uv's .python-versions, and .tool-versions (#287).

Version resolution now reads .python-version, the file pyenv, uv and
actions/setup-python use to pin a project's Python version, through the
same upward walk and precedence as Node.js's .node-version and .nvmrc.
Within one directory .dtvem/runtimes.json still comes first.

- An exact (3.12.4) or partial (3.12) version is accepted, with pyenv's
  optional python- prefix. A partial version resolves to the highest
  installed match.
- pyenv allows several versions, one per line; dtvem runs one, so the
  first is used.
- system, pyenv-virtualenv names and pointers, and uv requests that are
  not a plain version number (pypy, cpython@3.12) are reported as
  unsupported instead of falling back to the global version.

Each runtime's version files now come with their own rules
(versionFileRules), starting with the optional prefix a version may
carry: v for Node.js, python- for Python. As in pyenv and rbenv, the
first word of the first non-comment line is used, so trailing text on
that line no longer makes a Node.js file unreadable. The unsupported-
version message no longer says "aliases", since Python's unsupported
forms are not aliases.
@CalvinAllen
CalvinAllen merged commit 2d493a3 into main Oct 22, 2026
13 checks passed
@CalvinAllen
CalvinAllen deleted the feat/python/version-file branch October 22, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

feat(python): read .python-version when resolving the version

1 participant