Repository navigation
Fix Windows path handling on Python 3.13+ - #2196
Conversation
bed2df4 to
8c0966f
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates GitPython’s Windows path handling to account for Python 3.13+ changes (notably around drive-rooted paths), expands coverage across config/includes, index/submodule path confinement, and adjusts CI/tox + tests to validate newer Python versions and avoid assumptions about a local master branch.
Changes:
- Introduces shared path-kind and confinement helpers and uses them in index/submodule path normalization.
- Updates conditional include matching to normalize
gitdirpatterns to Git-style separators and improves repository confinement checks. - Expands/adjusts test suite for Windows path kinds and removes reliance on a
masterbranch; enables Python 3.13–3.15 in tox and CI.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tox.ini |
Adds py313–py315 tox environments. |
git/util.py |
Adds _is_path_rooted and _to_relative_path helper for Git-style, confined relative paths. |
git/index/base.py |
Switches index-relative path normalization to the new confinement helper (with special bare-repo handling). |
git/objects/submodule/base.py |
Uses the new confinement helper for submodule checkout paths and rejects repository-root paths. |
git/refs/symbolic.py |
Adjusts reference path confinement check based on commonpath. |
git/config.py |
Normalizes includeIf gitdir patterns and repository gitdir comparison to forward slashes; tweaks drive-prefix behavior. |
git/repo/base.py |
Reads/writes alternates from common_dir (important for linked worktrees). |
.github/workflows/pythonpackage.yml |
Re-enables Windows testing for Python 3.13–3.15 by removing exclusions. |
test/test_index.py |
Updates expected separators to Git-style and adds Windows path-kind tests for index confinement. |
test/test_submodule.py |
Adds confinement/path-kind tests and a drive-rooted gitfile test on Windows. |
test/test_config.py |
Adds Windows drive-rooted include test and expands conditional include coverage for forward slashes. |
test/test_repo.py |
Removes reliance on master, adds Windows drive-rooted clone/worktree tests, and adds alternates/common_dir test. |
test/test_refs.py |
Removes reliance on master for remote refs and reflog tests. |
test/test_commit.py |
Replaces master with HEAD in commit iteration/trailer tests. |
test/test_docs.py |
Makes docs tests branch-agnostic and updates examples to use active/default branches. |
test/test_util.py |
Adds a Windows drive-rooted cygpath test. |
.basedpyright/baseline.json |
Drops baseline entries corresponding to fixed type-check findings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
git/refs/symbolic.py:123
- On Windows, comparing
common_pathandbase_pathwith raw string equality can incorrectly reject valid paths when only casing differs (case-insensitive filesystem) or whenrealpath()normalizes the drive letter. Previously this check usednormcase()to avoid false positives; removing it can make_get_validated_pathraiseValueErrorfor refs that are actually inside the repo.
base_path = os.path.realpath(os.fspath(base))
abs_path = os.path.realpath(os.path.join(base_path, path))
try:
common_path = os.path.commonpath([base_path, abs_path])
except ValueError as e:
raise ValueError("Reference path %r escapes the repository" % path) from e
if common_path != base_path:
raise ValueError("Reference path %r escapes the repository" % path)
git/util.py:363
_to_relative_path()claims to resolve drive-rooted paths like\\diragainst the drive ofroot, butabspath(join(root_abs, path_str))drops the drive and then resolves against the process current drive. Ifrootis on a different drive than the current working directory,\\dirwill be resolved to the wrong drive, and confinement checks will be incorrect.
root_abs = osp.abspath(os.fspath(root))
path_abs = osp.abspath(osp.join(root_abs, path_str))
try:
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
git/index/base.py:660
- This method can now raise :exc:
InvalidGitRepositoryError(bare repo + rooted/drive path), but the docstring only mentions :exc:ValueError. Please document the additional exception so error handling is discoverable from the API contract.
:raise ValueError:
"""
git/objects/submodule/base.py:381
- The docstring says ValueError is only raised when the path is outside the working tree, but the implementation also rejects paths that refer to the working tree root (".") after normalization. Documenting that behavior will make this constraint clearer.
:raise ValueError:
If path is not contained in the parent repository's working tree.
git/index/base.py:666
- The docstring above still describes this as making paths relative to the repo's "git directory", but the new implementation delegates to git.util._to_relative_path(..., working_tree_dir) and returns a Git-style (forward-slash) path. Please update the return description so it matches the current behavior.
return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path)
ecdce93 to
464da19
Compare
|
Alright, to me it seems good to go, and I will wait a week or so for feedback, assuming there are no major concerns after that and that everything can be forward fixed, with a new release following. A week could easily be more though as I will leave it to chance that I rediscover the issue (which is expected, at least if other GitPython work happens). |
464da19 to
2185732
Compare
|
Merging, also because I desperately want the |
This commit also makes the test-suite independent of the presence of a local `master` branch. <!-- agent --> - make index and submodule path normalization distinguish drive-rooted, drive-relative, UNC, and device paths on Windows - confine normalized paths with `commonpath` instead of string-prefix checks - normalize `includeIf gitdir` patterns to Git-style separators - cover the other `isabs` consumers whose drive-rooted behavior correctly relies on Windows join semantics - remove the Windows 3.13-3.15 CI exclusions and add 3.13-3.15 to tox - make the pre-existing tests independent of a local `master` branch and linked-worktree-specific alternates For context, the classifiers mentioned in the issue #2072 were introduced by 904aa71 (`Declare support for Python 3.13-3.14`) and 7d5370d (`Add support for Python 3.15`), both included in 3.1.56. This PR supplies the Windows path work that those metadata changes did not include. - paths absolute on the current drive - drive-rooted paths such as `\directory` and `/directory` - drive-relative paths such as `C:directory` (rejected where repository confinement is required) - other-drive, UNC, and device paths - traversal and adjacent-prefix escape attempts - native and Git-style separators in conditional includes - config includes, submodule gitfiles, linked worktree metadata, clone destinations, and Cygwin-path conversion - Python 3.12.13, 3.13.14, 3.14.6, 3.14.6t, 3.15.0b4, and 3.15.0b4t installed locally with uv - full test set passed for every interpreter in an independent clone without a local `master` branch; daemon-free suites ran in parallel and `test_remote.py` ran serially because its static local daemon endpoint cannot be shared across processes - Ruff check and format - mypy - basedpyright - all pre-commit hooks Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
- Preserve POSIX backslashes in include patterns - Accept UNC share roots as rooted paths - treat includeIf patterns correctly on Windows, expecting forward slashes - Recognize UNC roots on older Python versions Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
2185732 to
308d371
Compare
The `ruff`, `format`, `mypy`, `html`, and `misc` environments are each
meant to run once, on the first available interpreter in a preference
order: Python 3.9 first, then 3.10, 3.11, 3.12, and 3.8. They wrote
that as `base_python = py{39,310,311,312,38}`, a form introduced in
62c024e (gitpython-developers#1693).
tox expands braces in generative environment lists and section names,
such as `env_list = py{38,...}` and `[testenv:py{38,39,310}]` in this
file, and in factor conditions, but not in the values of settings
like `base_python`. There, it just splits on commas, giving the five
specs `py{39`, `310`, `311`, `312`, and `38}`. The first and last are
not valid interpreter specs, so 3.9 and 3.8 were never candidates.
The bare `310`, `311`, and `312` are accepted as version specs, so the
environments still ran if 3.10, 3.11, or 3.12 was found. If none was,
the environment did not run. Older tox skipped it: with tox 4.11.3
running on 3.13 and only 3.9 on `PATH`, it reported "skipped because
could not find python interpreter with spec(s):
py{39, 310, 311, 312, 38}". tox 4.56.0 changed the default of
`skip_missing_interpreters` to false, so current tox fails it
instead: with tox 4.67.1 and only 3.9 available, it reported "failed
with could not find python interpreter matching any of the specs
py{39, 310, 311, 312, 38}".
Write each list as `py39, py310, py311, py312, py313, py314, py315`,
the documented form for several `base_python` values, of which the
first one found is used. This keeps the intended order, except that
3.8 is left out, and adds the newer versions at the end.
62c024e listed as fallbacks all the Python versions the project
supported, but the lists were not extended when da58f39 (gitpython-developers#2196)
added 3.13, 3.14, and 3.15 to `env_list`. So on a system with only
3.13 or later, these environments could not run at all. With only
3.13, 3.14, and 3.15 available, a probe with
`py39, py310, py311, py312` was skipped with tox 4.23.2 and
virtualenv 20.28.1 and failed with tox 4.67.1 and virtualenv
21.14.13, while the full list ran on 3.13 with both.
The brace form never selected 3.8, so leaving it out takes away
nothing that worked, and listing it would now cause failures. Since
virtualenv 21.5.0, virtualenv cannot create Python 3.8 environments.
tox 4.56.0 and later work around that by bootstrapping an older
virtualenv, but only when the installed virtualenv can create none of
the `base_python` candidates, which is not the case for a list that
also names newer versions. Current virtualenv also tries the first
listed interpreter last, as described below, so 3.8 would be tried
before 3.9. With tox 4.67.1 and virtualenv 21.14.13 and only 3.8 and
3.9 available, a probe with `py39, py310, py311, py312, py38`
accepted 3.8 and then failed with "could not find python interpreter
matching any of the specs". With `py39, py310, py311, py312`, it ran
on 3.9.
Even where 3.8 environments can be created, `mypy` and `html` would
not work as intended on 3.8. The `test` extra installs mypy only on
3.9 and later, so the `mypy` environment would not have the `mypy`
command it runs, and `ignore_outcome` would turn that failure into a
warning. The `doc` group's Sphinx requirement also applies only on
3.9 and later, so `html` would build with whatever Sphinx version the
other documentation dependencies happened to pull in.
Checked with a minimal tox project whose only environment prints its
interpreter's version, with Python 3.9 and 3.12 on `PATH` and tox
itself running on 3.13. (The brace form got 3.12 by matching `312`.)
| Tooling | Brace form | Comma form |
|----------------------------------|------------|------------|
| tox 4.11.3, virtualenv 20.24.5 | 3.12 | 3.9 |
| tox 4.23.2, virtualenv 20.28.1 | 3.12 | 3.9 |
`tox config` shows the intended entries for each environment.
With tox 4.23.2 and virtualenv 20.28.1, `tox -e ruff` and
`tox -e format` with this `tox.ini` ran on Python 3.9.6 and passed.
With tox 4.67.1, each of the five environments also ran and passed on
each of Python 3.13.17, 3.14.9, and 3.15.1, selected with
`-x testenv:<name>.base_python=py313` and so on. On 3.14 and 3.15,
`html` reports 13 more Sphinx warnings than on 3.13, all about
`__annotate_func__`, which Python 3.14 adds to the `__dict__` of
classes with annotations. The build used Sphinx 7.4.7, as the `doc`
group requires Sphinx below 8. CI's documentation build on 3.14
reports the same 14 warnings.
Current virtualenv does not keep this order. tox passes the
`base_python` list to virtualenv in `VIRTUALENV_PYTHON`, and since
virtualenv 20.29, when that variable is set, virtualenv moves the
first spec to the end of the list. That change,
pypa/virtualenv@bc7a91a5 (pypa/virtualenv#2821), was meant to let a
`--python` option take precedence over a single interpreter set in
the variable. So with tox 4.67.1 and virtualenv 21.14.13, the first
listed interpreter is tried last: a probe with `py39, py312, py311`
ran on 3.12, and one with `py312, py39` ran on 3.9. This is a problem
in how tox and virtualenv interact, not in this configuration. With
current versions, these environments are tried on 3.9 only if none of
3.10 through 3.15 is found.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The `ruff`, `format`, `mypy`, `html`, and `misc` environments are each
meant to run once, on the first available interpreter in a preference
order: Python 3.9 first, then 3.10, 3.11, 3.12, and 3.8. They wrote
that as `base_python = py{39,310,311,312,38}`, a form introduced in
62c024e (gitpython-developers#1693).
tox expands braces in generative environment lists and section names,
such as `env_list = py{38,...}` and `[testenv:py{38,39,310}]` in this
file, and in factor conditions, but not in the values of settings
like `base_python`. There, it just splits on commas, giving the five
specs `py{39`, `310`, `311`, `312`, and `38}`. The first and last are
not valid interpreter specs, so 3.9 and 3.8 were never candidates.
The bare `310`, `311`, and `312` are accepted as version specs, so the
environments still ran if 3.10, 3.11, or 3.12 was found. If none was,
the environment did not run. Older tox skipped it: with tox 4.11.3
running on 3.13 and only 3.9 on `PATH`, it reported "skipped because
could not find python interpreter with spec(s):
py{39, 310, 311, 312, 38}". tox 4.56.0 changed the default of
`skip_missing_interpreters` to false, so current tox fails it
instead: with tox 4.67.1 and only 3.9 available, it reported "failed
with could not find python interpreter matching any of the specs
py{39, 310, 311, 312, 38}".
Write each list as `py39, py310, py311, py312, py313, py314, py315`,
the documented form for several `base_python` values, of which the
first one found is used. This keeps the intended order, except that
3.8 is left out, and adds the newer versions at the end.
62c024e listed as fallbacks all the Python versions the project
supported, but the lists were not extended when da58f39 (gitpython-developers#2196)
added 3.13, 3.14, and 3.15 to `env_list`. So on a system with only
3.13 or later, these environments could not run at all. With only
3.13, 3.14, and 3.15 available, a probe with
`py39, py310, py311, py312` was skipped with tox 4.23.2 and
virtualenv 20.28.1 and failed with tox 4.67.1 and virtualenv
21.14.13, while the full list ran on 3.13 with both.
The brace form never selected 3.8, so leaving it out takes away
nothing that worked, and listing it would now cause failures. Since
virtualenv 21.5.0, virtualenv cannot create Python 3.8 environments.
tox 4.56.0 and later work around that by bootstrapping an older
virtualenv, but only when the installed virtualenv can create none of
the `base_python` candidates, which is not the case for a list that
also names newer versions. Current virtualenv also tries the first
listed interpreter last, as described below, so 3.8 would be tried
before 3.9. With tox 4.67.1 and virtualenv 21.14.13 and only 3.8 and
3.9 available, a probe with `py39, py310, py311, py312, py38`
accepted 3.8 and then failed with "could not find python interpreter
matching any of the specs". With `py39, py310, py311, py312`, it ran
on 3.9.
Even where 3.8 environments can be created, `mypy` and `html` would
not work as intended on 3.8. The `test` extra installs mypy only on
3.9 and later, so the `mypy` environment would not have the `mypy`
command it runs, and `ignore_outcome` would turn that failure into a
warning. The `doc` group's Sphinx requirement also applies only on
3.9 and later, so `html` would build with whatever Sphinx version the
other documentation dependencies happened to pull in.
Checked with a minimal tox project whose only environment prints its
interpreter's version, with Python 3.9 and 3.12 on `PATH` and tox
itself running on 3.13. (The brace form got 3.12 by matching `312`.)
| Tooling | Brace form | Comma form |
|----------------------------------|------------|------------|
| tox 4.11.3, virtualenv 20.24.5 | 3.12 | 3.9 |
| tox 4.23.2, virtualenv 20.28.1 | 3.12 | 3.9 |
`tox config` shows the intended entries for each environment.
With tox 4.23.2 and virtualenv 20.28.1, `tox -e ruff` and
`tox -e format` with this `tox.ini` ran on Python 3.9.6 and passed.
With tox 4.67.1, each of the five environments also ran and passed on
each of Python 3.13.17, 3.14.9, and 3.15.1, selected with
`-x testenv:<name>.base_python=py313` and so on. `html` reports no
Sphinx warnings on 3.13, but 13 on each of 3.14 and 3.15, all about
`__annotate_func__`, which Python 3.14 adds to the `__dict__` of
classes with annotations. The build used Sphinx 7.4.7, as the `doc`
group requires Sphinx below 8. CI's documentation build on 3.14
reports the same 13 warnings.
Current virtualenv does not keep this order. tox passes the
`base_python` list to virtualenv in `VIRTUALENV_PYTHON`, and since
virtualenv 20.29, when that variable is set, virtualenv moves the
first spec to the end of the list. That change,
pypa/virtualenv@bc7a91a5 (pypa/virtualenv#2821), was meant to let a
`--python` option take precedence over a single interpreter set in
the variable. So with tox 4.67.1 and virtualenv 21.14.13, the first
listed interpreter is tried last: a probe with `py39, py312, py311`
ran on 3.12, and one with `py312, py39` ran on 3.9. This is a problem
in how tox and virtualenv interact, not in this configuration. With
current versions, these environments are tried on 3.9 only if none of
3.10 through 3.15 is found.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Tasks
Summary
commonpathinstead of string-prefix checksincludeIf gitdirpatterns to Git-style separatorsisabsconsumers whose drive-rooted behavior correctly relies on Windows join semanticsmasterbranch and linked-worktree-specific alternatesThis addresses the remaining Windows path checklist from #1955 and closes #2072.
For context, the classifiers mentioned in the issue were introduced by 904aa71 (
Declare support for Python 3.13-3.14) and 7d5370d (Add support for Python 3.15), both included in 3.1.56. This PR supplies the Windows path work that those metadata changes did not include.Windows path coverage
\directoryand/directoryC:directory(rejected where repository confinement is required)Validation
masterbranch; daemon-free suites ran in parallel andtest_remote.pyran serially because its fixed local daemon endpoint cannot be shared across processes