fix(runfiles): update Path method signatures for Python 3.14 compatibility and fix match() (#4100)
Align runfiles.Path method signatures with Python 3.14 typeshed stubs to
avoid type checker errors on newer Python releases. This also fixes a
bug where pattern matching failed on Python 3.12+.
This change splits the runfiles compatibility updates from PR #4023.
diff --git a/news/runfiles_py314_compat.fixed.md b/news/runfiles_py314_compat.fixed.md
new file mode 100644
index 0000000..accf48c
--- /dev/null
+++ b/news/runfiles_py314_compat.fixed.md
@@ -0,0 +1,3 @@
+(runfiles) Updated {obj}`runfiles.Path` method signatures for Python 3.14
+typeshed compatibility and fixed {obj}`runfiles.Path.match` on Python 3.12+.
+([#4023](https://github.com/bazel-contrib/rules_python/issues/4023))
diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py
index 1c6dca6..0c56702 100644
--- a/python/runfiles/runfiles.py
+++ b/python/runfiles/runfiles.py
@@ -31,9 +31,20 @@
import posixpath
import sys
from collections import defaultdict
-from collections.abc import Generator
+from collections.abc import Generator, Iterator
from typing import cast
+if sys.version_info >= (3, 12):
+ from typing import override
+else:
+ from typing import TypeVar
+
+ _FuncT = TypeVar("_FuncT")
+
+ def override(func: _FuncT) -> _FuncT:
+ return func
+
+
if sys.version_info >= (3, 11):
from typing import Self
elif sys.version_info >= (3, 10):
@@ -202,7 +213,7 @@
# __new__ or with_segments(), the runfiles state is preserved. We delegate
# to self._as_path() because super().resolve() creates intermediate objects
# that would otherwise crash during internal stat() calls.
- # override
+ @override
def resolve(self, strict: bool = False) -> Self:
return type(self)(
self._as_path().resolve(strict=strict),
@@ -210,7 +221,7 @@
source_repo=self._source_repo,
)
- # override
+ @override
def absolute(self) -> Self:
return type(self)(
self._as_path().absolute(),
@@ -218,7 +229,7 @@
source_repo=self._source_repo,
)
- # override
+ @override
def with_segments(self, *pathsegments: str | os.PathLike) -> Self:
"""Used by Python 3.12+ pathlib to create new path objects."""
return type(self)(
@@ -228,7 +239,6 @@
)
# For Python < 3.12
- # override
def _make_child(self, args: tuple[str, ...]) -> Self:
# _make_child is an internal CPython method in Python < 3.12 omitted from
# typeshed stubs. We ignore [missing-attribute] for pyrefly.
@@ -237,8 +247,8 @@
obj._source_repo = self._source_repo
return cast(Self, obj)
- # override
@property
+ @override
def parents(self) -> tuple[Self, ...]:
return tuple(
type(self)(
@@ -249,8 +259,8 @@
for p in super().parents
)
- # override
@property
+ @override
def parent(self) -> Self:
return type(self)(
super().parent,
@@ -266,7 +276,7 @@
return ""
return path_posix
- # override
+ @override
def with_name(self, name: str) -> Self:
return type(self)(
super().with_name(name),
@@ -274,7 +284,7 @@
source_repo=self._source_repo,
)
- # override
+ @override
def with_suffix(self, suffix: str) -> Self:
return type(self)(
super().with_suffix(suffix),
@@ -285,49 +295,55 @@
def _as_path(self) -> pathlib.Path:
return pathlib.Path(str(self))
- # override
+ @override
def stat(self, *, follow_symlinks: bool = True) -> os.stat_result:
return self._as_path().stat(follow_symlinks=follow_symlinks)
- # override
+ @override
def lstat(self) -> os.stat_result:
return self._as_path().lstat()
- # override
- def exists(self) -> bool:
+ @override
+ def exists(self, *, follow_symlinks: bool = True) -> bool:
+ if not follow_symlinks and sys.version_info >= (3, 12):
+ return self._as_path().exists(follow_symlinks=follow_symlinks)
return self._as_path().exists()
- # override
- def is_dir(self) -> bool:
+ @override
+ def is_dir(self, *, follow_symlinks: bool = True) -> bool:
+ if not follow_symlinks and sys.version_info >= (3, 13):
+ return self._as_path().is_dir(follow_symlinks=follow_symlinks)
return self._as_path().is_dir()
- # override
- def is_file(self) -> bool:
+ @override
+ def is_file(self, *, follow_symlinks: bool = True) -> bool:
+ if not follow_symlinks and sys.version_info >= (3, 13):
+ return self._as_path().is_file(follow_symlinks=follow_symlinks)
return self._as_path().is_file()
- # override
+ @override
def is_symlink(self) -> bool:
return self._as_path().is_symlink()
- # override
+ @override
def is_block_device(self) -> bool:
return self._as_path().is_block_device()
- # override
+ @override
def is_char_device(self) -> bool:
return self._as_path().is_char_device()
- # override
+ @override
def is_fifo(self) -> bool:
return self._as_path().is_fifo()
- # override
+ @override
def is_socket(self) -> bool:
return self._as_path().is_socket()
# Path.open in pathlib has multiple overloads in typeshed. We use a
# simplified delegation signature here.
- # override
+ @override
def open( # pyrefly: ignore[bad-override]
self,
mode: str = "r",
@@ -344,32 +360,86 @@
newline=newline,
)
- # override
+ @override
def read_bytes(self) -> bytes:
return self._as_path().read_bytes()
- # override
- def read_text(self, encoding: str | None = None, errors: str | None = None) -> str:
+ @override
+ def read_text(
+ self,
+ encoding: str | None = None,
+ errors: str | None = None,
+ newline: str | None = None,
+ ) -> str:
+ if sys.version_info >= (3, 13) and newline is not None:
+ return self._as_path().read_text(
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ )
return self._as_path().read_text(encoding=encoding, errors=errors)
- # override
+ @override
def iterdir(self) -> Generator[Self, None, None]:
resolved = self._as_path()
for p in resolved.iterdir():
yield self / p.name
- # override
- def glob(self, pattern: str) -> Generator[Self, None, None]:
+ @override
+ def glob( # pyrefly: ignore[bad-override]
+ self,
+ pattern: str,
+ *,
+ case_sensitive: bool | None = None,
+ recurse_symlinks: bool = False,
+ ) -> Iterator[Self]:
resolved = self._as_path()
- for p in resolved.glob(pattern):
+ if sys.version_info >= (3, 13):
+ it = resolved.glob(
+ pattern,
+ case_sensitive=case_sensitive,
+ recurse_symlinks=recurse_symlinks,
+ )
+ elif sys.version_info >= (3, 12):
+ it = resolved.glob(pattern, case_sensitive=case_sensitive)
+ else:
+ it = resolved.glob(pattern)
+ for p in it:
yield self / p.relative_to(resolved)
- # override
- def rglob(self, pattern: str) -> Generator[Self, None, None]:
+ @override
+ def rglob( # pyrefly: ignore[bad-override]
+ self,
+ pattern: str,
+ *,
+ case_sensitive: bool | None = None,
+ recurse_symlinks: bool = False,
+ ) -> Iterator[Self]:
resolved = self._as_path()
- for p in resolved.rglob(pattern):
+ if sys.version_info >= (3, 13):
+ it = resolved.rglob(
+ pattern,
+ case_sensitive=case_sensitive,
+ recurse_symlinks=recurse_symlinks,
+ )
+ elif sys.version_info >= (3, 12):
+ it = resolved.rglob(pattern, case_sensitive=case_sensitive)
+ else:
+ it = resolved.rglob(pattern)
+ for p in it:
yield self / p.relative_to(resolved)
+ @override
+ def match(
+ self,
+ path_pattern: str,
+ *,
+ case_sensitive: bool | None = None,
+ ) -> bool:
+ if sys.version_info >= (3, 12):
+ return self._as_path().match(path_pattern, case_sensitive=case_sensitive)
+ return self._as_path().match(path_pattern)
+
def __repr__(self) -> str:
return "runfiles.Path({!r})".format(self.runfile_path)
diff --git a/tests/runfiles/BUILD.bazel b/tests/runfiles/BUILD.bazel
index 04d7f1a..08944ef 100644
--- a/tests/runfiles/BUILD.bazel
+++ b/tests/runfiles/BUILD.bazel
@@ -1,6 +1,6 @@
load("@bazel_skylib//rules:build_test.bzl", "build_test")
-load("@rules_python//python:py_test.bzl", "py_test")
load("@rules_python//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility
+load("//tests/support:support.bzl", "SUPPORTS_BZLMOD")
load("//tests/support/pytest_test:pytest_test.bzl", "pytest_test")
pytest_test(
@@ -12,34 +12,28 @@
env = {
"BZLMOD_ENABLED": "1" if BZLMOD_ENABLED else "0",
},
- deps = ["//python/runfiles"],
-)
-
-py_test(
- name = "runfiles_min_python_test",
- srcs = ["runfiles_test.py"],
- data = [
- "//tests/support:current_build_settings",
+ python_versions = [
+ "3.10",
+ "3.11",
+ "3.12",
+ "3.13",
+ "3.14",
],
- env = {
- "BZLMOD_ENABLED": "1" if BZLMOD_ENABLED else "0",
- },
- main = "runfiles_test.py",
- python_version = "3.10",
+ target_compatible_with = SUPPORTS_BZLMOD,
deps = ["//python/runfiles"],
)
pytest_test(
name = "pathlib_test",
srcs = ["pathlib_test.py"],
- deps = ["//python/runfiles"],
-)
-
-py_test(
- name = "pathlib_min_python_test",
- srcs = ["pathlib_test.py"],
- main = "pathlib_test.py",
- python_version = "3.10",
+ python_versions = [
+ "3.10",
+ "3.11",
+ "3.12",
+ "3.13",
+ "3.14",
+ ],
+ target_compatible_with = SUPPORTS_BZLMOD,
deps = ["//python/runfiles"],
)