From 3321a12ef8a187d7939d1625df6941fd1c79dcef Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 01:43:23 +0100 Subject: [PATCH 01/17] tests: check proxyupath with posixupath and windowsupath --- upath/tests/test_extensions.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 5711a188..51eff174 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -3,6 +3,8 @@ from upath import UPath from upath.extensions import ProxyUPath from upath.implementations.local import FilePath +from upath.implementations.local import PosixUPath +from upath.implementations.local import WindowsUPath from upath.implementations.memory import MemoryPath from upath.tests.cases import BaseTests @@ -38,6 +40,22 @@ def test_chmod(self): self.path.joinpath("file1.txt").chmod(777) +class TestProxyPathlibPath(BaseTests): + @pytest.fixture(autouse=True) + def path(self, local_testdir): + self.path = ProxyUPath(f"{local_testdir}") + self.prepare_file_system() + + def test_is_ProxyUPath(self): + assert isinstance(self.path, ProxyUPath) + + def test_is_not_PosixUPath_WindowsUPath(self): + assert not isinstance(self.path, (PosixUPath, WindowsUPath)) + + def test_chmod(self): + self.path.joinpath("file1.txt").chmod(777) + + def test_custom_subclass(): class ReversePath(ProxyUPath): From 70739182e6c2f350995971aa47adcf6f461ed51f Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 03:21:21 +0100 Subject: [PATCH 02/17] tests: more extensions tests adjustmens --- upath/tests/cases.py | 3 ++- upath/tests/test_extensions.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 82bd136d..60a485d4 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -12,6 +12,7 @@ from upath import UPath from upath._stat import UPathStatResult +from upath.types import StatResultType class BaseTests: @@ -29,7 +30,7 @@ def test_home(self): def test_stat(self): stat = self.path.stat() - assert isinstance(stat, UPathStatResult) + assert isinstance(stat, StatResultType) assert len(tuple(stat)) == os.stat_result.n_sequence_fields with warnings.catch_warnings(): diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 51eff174..2e9fa8a9 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -1,5 +1,9 @@ +import os +import sys + import pytest +from upath import UnsupportedOperation from upath import UPath from upath.extensions import ProxyUPath from upath.implementations.local import FilePath @@ -55,6 +59,41 @@ def test_is_not_PosixUPath_WindowsUPath(self): def test_chmod(self): self.path.joinpath("file1.txt").chmod(777) + @pytest.mark.skipif( + sys.version_info < (3, 12), reason="storage options only handled in 3.12+" + ) + def test_eq(self): + super().test_eq() + + def test_group(self): + pytest.importorskip("grp") + self.path.group() + + def test_owner(self): + pytest.importorskip("pwd") + self.path.owner() + + def test_lchmod(self): + self.path.lchmod(0o777) + + def test_readlink(self): + try: + os.readlink + except AttributeError: + pytest.skip("os.readlink not available on this platform") + with pytest.raises((OSError, UnsupportedOperation)): + self.path.readlink() + + def test_protocol(self): + assert self.path.protocol == "" + + def test_as_uri(self): + assert self.path.as_uri().startswith("file://") + + @pytest.mark.xfail(reason="need to revisit relative path .rename") + def test_rename2(self): + super().test_rename2() + def test_custom_subclass(): From 05ea420432aa235558851229d2f3d4cd932f6884 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 03:22:17 +0100 Subject: [PATCH 03/17] upath: fixes for extensions, StatResultType --- upath/extensions.py | 18 ++++++++++++++---- upath/implementations/local.py | 7 +++++++ upath/types/__init__.py | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/upath/extensions.py b/upath/extensions.py index 434ac2a7..b548a97c 100644 --- a/upath/extensions.py +++ b/upath/extensions.py @@ -284,7 +284,9 @@ def is_absolute(self) -> bool: return self.__wrapped__.is_absolute() def __eq__(self, other: object) -> bool: - return self.__wrapped__.__eq__(other) + if not isinstance(other, type(self)): + return NotImplemented + return self.__wrapped__.__eq__(other.__wrapped__) def __hash__(self) -> int: return self.__wrapped__.__hash__() @@ -313,8 +315,11 @@ def lchmod(self, mode: int) -> None: def unlink(self, missing_ok: bool = False) -> None: self.__wrapped__.unlink(missing_ok=missing_ok) - def rmdir(self, recursive: bool = True) -> None: # fixme: non-standard - self.__wrapped__.rmdir(recursive=recursive) + def rmdir(self, recursive: bool = UNSET_DEFAULT) -> None: # fixme: non-standard + kwargs: dict[str, Any] = {} + if recursive is not UNSET_DEFAULT: + kwargs["recursive"] = recursive + self.__wrapped__.rmdir(**kwargs) def rename( self, @@ -324,9 +329,14 @@ def rename( maxdepth: int | None = UNSET_DEFAULT, **kwargs: Any, ) -> Self: + if recursive is not UNSET_DEFAULT: + kwargs["recursive"] = recursive + if maxdepth is not UNSET_DEFAULT: + kwargs["maxdepth"] = maxdepth return self._from_upath( self.__wrapped__.rename( - target, recursive=recursive, maxdepth=maxdepth, **kwargs + target.__wrapped__ if isinstance(target, ProxyUPath) else target, + **kwargs, ) ) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index 30b634dd..bb6c4bcf 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -2,6 +2,7 @@ import os import pathlib +import shutil import sys import warnings from collections.abc import Iterator @@ -316,6 +317,12 @@ def open( **fsspec_kwargs, ) + def rmdir(self, recursive: bool = UNSET_DEFAULT) -> None: + if recursive is UNSET_DEFAULT or not recursive: + return super().rmdir() + else: + shutil.rmtree(self) + if sys.version_info < (3, 14): # noqa: C901 @overload diff --git a/upath/types/__init__.py b/upath/types/__init__.py index 5b582dbf..d5398643 100644 --- a/upath/types/__init__.py +++ b/upath/types/__init__.py @@ -61,6 +61,7 @@ class _DefaultValue(enum.Enum): # WritablePath.register(pathlib.Path) +@runtime_checkable class StatResultType(Protocol): """duck-type for os.stat_result""" From c05e595c357564d2c3a06af9659dba9bad65d0fe Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 12:13:20 +0100 Subject: [PATCH 04/17] tests: fix permissions in lchmod test --- upath/tests/cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 60a485d4..dbec433f 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -177,7 +177,7 @@ def test_parents(self): def test_lchmod(self): with pytest.raises(NotImplementedError): - self.path.lchmod(mode=77) + self.path.lchmod(mode=0o777) def test_lstat(self): with pytest.warns(UserWarning, match=r"[A-Za-z]+.stat"): From 2b4d2e29bd4af290b49aa1b95035008b059aa870 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 12:13:55 +0100 Subject: [PATCH 05/17] upath: fix UPathStatResult attributes --- upath/_stat.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/upath/_stat.py b/upath/_stat.py index 4d67ee07..6ad9200c 100644 --- a/upath/_stat.py +++ b/upath/_stat.py @@ -307,6 +307,42 @@ def st_birthtime(self) -> int | float: pass raise AttributeError("birthtime") + @property + def st_atime_ns(self) -> int: + """time of last access in nanoseconds""" + try: + return int(self._info["atime_ns"]) + except KeyError: + pass + atime = self.st_atime + if isinstance(atime, float): + return int(atime * 1e9) + return atime * 1_000_000_000 + + @property + def st_mtime_ns(self) -> int: + """time of last modification in nanoseconds""" + try: + return int(self._info["mtime_ns"]) + except KeyError: + pass + mtime = self.st_mtime + if isinstance(mtime, float): + return int(mtime * 1e9) + return mtime * 1_000_000_000 + + @property + def st_ctime_ns(self) -> int: + """time of last change in nanoseconds""" + try: + return int(self._info["ctime_ns"]) + except KeyError: + pass + ctime = self.st_ctime + if isinstance(ctime, float): + return int(ctime * 1e9) + return ctime * 1_000_000_000 + # --- extra fields ------------------------------------------------ def __getattr__(self, item): From 56b260fc33d0d01f14ac35f169ff91e8ff5227b5 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 12:46:24 +0100 Subject: [PATCH 06/17] tests: fix test cases and correct assumptions --- upath/tests/cases.py | 12 +++++++----- upath/tests/test_extensions.py | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index dbec433f..a0fb11da 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -541,14 +541,16 @@ def test_hashable(self): assert hash(self.path) def test_storage_options_dont_affect_hash(self): - p0 = UPath(str(self.path), test_extra=1, **self.path.storage_options) - p1 = UPath(str(self.path), test_extra=2, **self.path.storage_options) + cls = type(self.path) + p0 = cls(str(self.path), test_extra=1, **self.path.storage_options) + p1 = cls(str(self.path), test_extra=2, **self.path.storage_options) assert hash(p0) == hash(p1) def test_eq(self): - p0 = UPath(str(self.path), test_extra=1, **self.path.storage_options) - p1 = UPath(str(self.path), test_extra=1, **self.path.storage_options) - p2 = UPath(str(self.path), test_extra=2, **self.path.storage_options) + cls = type(self.path) + p0 = cls(str(self.path), test_extra=1, **self.path.storage_options) + p1 = cls(str(self.path), test_extra=1, **self.path.storage_options) + p2 = cls(str(self.path), test_extra=2, **self.path.storage_options) assert p0 == p1 assert p0 != p2 assert p1 != p2 diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 2e9fa8a9..95d85811 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -94,6 +94,10 @@ def test_as_uri(self): def test_rename2(self): super().test_rename2() + def test_lstat(self): + st = self.path.lstat() + assert st is not None + def test_custom_subclass(): From 206b9aa0d7095677c184b044f3dbb533f8a81bb7 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 12:47:15 +0100 Subject: [PATCH 07/17] upath: fix equality checks for extensions and locals --- upath/extensions.py | 21 +++++++++++++++++---- upath/implementations/local.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/upath/extensions.py b/upath/extensions.py index b548a97c..3450e10c 100644 --- a/upath/extensions.py +++ b/upath/extensions.py @@ -291,17 +291,30 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: return self.__wrapped__.__hash__() + def __ne__(self, other: object) -> bool: + if not isinstance(other, type(self)): + return NotImplemented + return self.__wrapped__.__ne__(other.__wrapped__) + def __lt__(self, other: object) -> bool: - return self.__wrapped__.__lt__(other) + if not isinstance(other, type(self)): + return NotImplemented + return self.__wrapped__.__lt__(other.__wrapped__) def __le__(self, other: object) -> bool: - return self.__wrapped__.__le__(other) + if not isinstance(other, type(self)): + return NotImplemented + return self.__wrapped__.__le__(other.__wrapped__) def __gt__(self, other: object) -> bool: - return self.__wrapped__.__gt__(other) + if not isinstance(other, type(self)): + return NotImplemented + return self.__wrapped__.__gt__(other.__wrapped__) def __ge__(self, other: object) -> bool: - return self.__wrapped__.__ge__(other) + if not isinstance(other, type(self)): + return NotImplemented + return self.__wrapped__.__ge__(other.__wrapped__) def resolve(self, strict: bool = False) -> Self: return self._from_upath(self.__wrapped__.resolve(strict=strict)) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index bb6c4bcf..81ef1b36 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -197,6 +197,24 @@ def __vfspath__(self) -> str: def __open_reader__(self) -> BinaryIO: return self.open("rb") + def __eq__(self, other: object) -> bool: + if not isinstance(other, UPath): + return NotImplemented + return ( + super().__eq__(other) + and self.protocol == other.protocol + and self.storage_options == other.storage_options + ) + + def __ne__(self, other: object) -> bool: + if not isinstance(other, UPath): + return NotImplemented + return ( + super().__ne__(other) + or self.protocol != other.protocol + or self.storage_options != other.storage_options + ) + if sys.version_info >= (3, 14): def __open_rb__(self, buffering: int = UNSET_DEFAULT) -> BinaryIO: From 109355b2af9840fd64cf7be78d3aa4084777fcc7 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 13:01:29 +0100 Subject: [PATCH 08/17] upath: fix equality NotImplemented behavior --- upath/implementations/local.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index 81ef1b36..d1cf0b50 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -200,8 +200,11 @@ def __open_reader__(self) -> BinaryIO: def __eq__(self, other: object) -> bool: if not isinstance(other, UPath): return NotImplemented + eq_path = super().__eq__(other) + if eq_path is NotImplemented: + return NotImplemented return ( - super().__eq__(other) + eq_path and self.protocol == other.protocol and self.storage_options == other.storage_options ) @@ -209,8 +212,11 @@ def __eq__(self, other: object) -> bool: def __ne__(self, other: object) -> bool: if not isinstance(other, UPath): return NotImplemented + ne_path = super().__ne__(other) + if ne_path is NotImplemented: + return NotImplemented return ( - super().__ne__(other) + ne_path or self.protocol != other.protocol or self.storage_options != other.storage_options ) From 2ff287d3a7c66d0c9ce8a85a0185420fdcc55e79 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 13:10:33 +0100 Subject: [PATCH 09/17] upath: local add __hash__ --- upath/implementations/local.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index d1cf0b50..4edf417f 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -221,6 +221,9 @@ def __ne__(self, other: object) -> bool: or self.storage_options != other.storage_options ) + def __hash__(self) -> int: + return super().__hash__() + if sys.version_info >= (3, 14): def __open_rb__(self, buffering: int = UNSET_DEFAULT) -> BinaryIO: From 63f6dc68b4178b081cd4ae9ed0f32c02fa3bd682 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 14:49:48 +0100 Subject: [PATCH 10/17] upath.types: further adjust StatResultType --- upath/types/__init__.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/upath/types/__init__.py b/upath/types/__init__.py index d5398643..5bb01a90 100644 --- a/upath/types/__init__.py +++ b/upath/types/__init__.py @@ -92,15 +92,17 @@ def st_mtime_ns(self) -> int: ... @property def st_ctime_ns(self) -> int: ... - if sys.platform == "win32": - if sys.version_info >= (3, 12): + # st_birthtime is available on Windows (3.12+), FreeBSD, and macOS + # On Linux it's currently unavailable + # see: https://discuss.python.org/t/st-birthtime-not-available/104350/2 + if sys.platform == "win32" and sys.version_info >= (3, 12): - @property - def st_birthtime(self) -> float: ... - @property - def st_birthtime_ns(self) -> int: ... + @property + def st_birthtime(self) -> float: ... + @property + def st_birthtime_ns(self) -> int: ... - else: + elif sys.platform == "darwin" or sys.platform.startswith("freebsd"): @property def st_birthtime(self) -> float: ... From 3d3f6de8a1e097f5ef16d39576fc664b69d464ba Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 15:22:09 +0100 Subject: [PATCH 11/17] upath.core: raise TypeError when creating with incompatible protocols --- upath/core.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/upath/core.py b/upath/core.py index d587c74a..5167f928 100644 --- a/upath/core.py +++ b/upath/core.py @@ -419,11 +419,18 @@ def __new__( protocol = storage_options.pop("scheme") # determine the protocol - pth_protocol = get_upath_protocol( - args[0] if args else "", - protocol=protocol, - storage_options=storage_options, - ) + try: + pth_protocol = get_upath_protocol( + args[0] if args else "", + protocol=protocol, + storage_options=storage_options, + ) + except ValueError as e: + if "incompatible with" in str(e): + raise TypeError( + f"{cls.__name__}.__new__(...) incompatible protocol" + ) from e + raise # determine which UPath subclass to dispatch to upath_cls: type[UPath] | None if cls._protocol_dispatch or cls._protocol_dispatch is None: From a0619e475b873ebc406766df38887a2a5442e925 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 15:22:43 +0100 Subject: [PATCH 12/17] upath.implementations.local: fix _copy_from --- upath/core.py | 8 ++++++++ upath/implementations/local.py | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/upath/core.py b/upath/core.py index 5167f928..b8cbe5d3 100644 --- a/upath/core.py +++ b/upath/core.py @@ -1264,6 +1264,14 @@ def move_into( target = self.with_segments(target_dir, name) return self.move(target) + def _copy_from( + self, + source: ReadablePath, + follow_symlinks: bool = True, + **kwargs: Any, + ) -> None: + return super()._copy_from(source, follow_symlinks) + # --- WritablePath attributes ------------------------------------- def symlink_to( diff --git a/upath/implementations/local.py b/upath/implementations/local.py index 4edf417f..d1876ba6 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -690,7 +690,10 @@ def chmod( if not hasattr(pathlib.Path, "_copy_from"): def _copy_from( - self, source: ReadablePath | LocalPath, follow_symlinks: bool = True + self, + source: ReadablePath | LocalPath, + follow_symlinks: bool = True, + preserve_metadata: bool = False, ) -> None: _copy_from: Any = WritablePath._copy_from.__get__(self) _copy_from(source, follow_symlinks=follow_symlinks) From 535354c3d4b2ff1902e9632167088b792a7483f7 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 15:56:14 +0100 Subject: [PATCH 13/17] upath.UPath: raise a TypeError subclass for incompatible protocols in constructor --- upath/core.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/upath/core.py b/upath/core.py index b8cbe5d3..96f2ad52 100644 --- a/upath/core.py +++ b/upath/core.py @@ -116,6 +116,13 @@ def _raise_unsupported(cls_name: str, method: str) -> NoReturn: raise UnsupportedOperation(f"{cls_name}.{method}() is unsupported") +class _IncompatibleProtocolError(TypeError, ValueError): + """switch to TypeError for incompatible protocols in a backward compatible way. + + !!! Do not use this exception directly !!! + """ + + class _UPathMeta(ABCMeta): """metaclass for UPath to customize instance creation @@ -427,9 +434,7 @@ def __new__( ) except ValueError as e: if "incompatible with" in str(e): - raise TypeError( - f"{cls.__name__}.__new__(...) incompatible protocol" - ) from e + raise _IncompatibleProtocolError(str(e)) from e raise # determine which UPath subclass to dispatch to upath_cls: type[UPath] | None From 9a6183114019398c61b6ce4e519888fe248991a3 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 16:49:26 +0100 Subject: [PATCH 14/17] tests: compatibility fixes for mount lchmod and relative_to --- upath/tests/cases.py | 12 ++++++++++-- upath/tests/test_extensions.py | 9 ++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index a0fb11da..138e7281 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -10,6 +10,7 @@ from fsspec import filesystem from packaging.version import Version +from upath import UnsupportedOperation from upath import UPath from upath._stat import UPathStatResult from upath.types import StatResultType @@ -118,7 +119,12 @@ def test_is_absolute(self): assert self.path.is_absolute() is True def test_is_mount(self): - assert self.path.is_mount() is False + try: + self.path.is_mount() + except UnsupportedOperation: + pytest.skip(f"is_mount() not supported for {type(self.path).__name__}") + else: + assert self.path.is_mount() is False def test_is_symlink(self): assert self.path.is_symlink() is False @@ -176,8 +182,10 @@ def test_parents(self): assert p.parents[1].name == self.path.name def test_lchmod(self): - with pytest.raises(NotImplementedError): + try: self.path.lchmod(mode=0o777) + except UnsupportedOperation: + pass def test_lstat(self): with pytest.warns(UserWarning, match=r"[A-Za-z]+.stat"): diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 95d85811..a949cac9 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -73,9 +73,6 @@ def test_owner(self): pytest.importorskip("pwd") self.path.owner() - def test_lchmod(self): - self.path.lchmod(0o777) - def test_readlink(self): try: os.readlink @@ -98,6 +95,12 @@ def test_lstat(self): st = self.path.lstat() assert st is not None + def test_relative_to(self): + base = self.path + child = self.path / "folder1" / "file1.txt" + relative = child.relative_to(base) + assert str(relative) == f"folder1{os.sep}file1.txt" + def test_custom_subclass(): From aaaaa8ffd5ed4724a216b1b96834f740f9f61084 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 16:49:55 +0100 Subject: [PATCH 15/17] tests: add stat attribute test for debugging --- upath/tests/cases.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 138e7281..adf46571 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -54,6 +54,14 @@ def test_stat_st_size(self): file1 = self.path.joinpath("file1.txt").stat() assert file1.st_size == 11 + def test_stat_attrs(self): + """helps with debugging os.stat_result compatibility""" + s = self.path.stat() + attrs = {attr for attr in dir(s) if attr.startswith("st_")} + required_attrs = StatResultType.__protocol_attrs__ + print(attrs) + assert attrs.issuperset(required_attrs) + def test_chmod(self): with pytest.raises(NotImplementedError): self.path.joinpath("file1.txt").chmod(777) From 4460f0b8c313c86a198dcbe5a43229d64bc355e4 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 17:04:46 +0100 Subject: [PATCH 16/17] tests: adjust stat debug test --- upath/tests/cases.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index adf46571..b99f1bc4 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -31,6 +31,11 @@ def test_home(self): def test_stat(self): stat = self.path.stat() + + # for debugging os.stat_result compatibility + attrs = {attr for attr in dir(stat) if attr.startswith("st_")} + print(attrs) + assert isinstance(stat, StatResultType) assert len(tuple(stat)) == os.stat_result.n_sequence_fields @@ -54,14 +59,6 @@ def test_stat_st_size(self): file1 = self.path.joinpath("file1.txt").stat() assert file1.st_size == 11 - def test_stat_attrs(self): - """helps with debugging os.stat_result compatibility""" - s = self.path.stat() - attrs = {attr for attr in dir(s) if attr.startswith("st_")} - required_attrs = StatResultType.__protocol_attrs__ - print(attrs) - assert attrs.issuperset(required_attrs) - def test_chmod(self): with pytest.raises(NotImplementedError): self.path.joinpath("file1.txt").chmod(777) From ae44063fe686c18892ef4f5702117b5b62c14179 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Thu, 13 Nov 2025 17:05:50 +0100 Subject: [PATCH 17/17] upath.types: os.stat_result.st_birthtime_ns not on windows server... --- upath/types/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/upath/types/__init__.py b/upath/types/__init__.py index 5bb01a90..f0c6c965 100644 --- a/upath/types/__init__.py +++ b/upath/types/__init__.py @@ -95,14 +95,9 @@ def st_ctime_ns(self) -> int: ... # st_birthtime is available on Windows (3.12+), FreeBSD, and macOS # On Linux it's currently unavailable # see: https://discuss.python.org/t/st-birthtime-not-available/104350/2 - if sys.platform == "win32" and sys.version_info >= (3, 12): - - @property - def st_birthtime(self) -> float: ... - @property - def st_birthtime_ns(self) -> int: ... - - elif sys.platform == "darwin" or sys.platform.startswith("freebsd"): + if (sys.platform == "win32" and sys.version_info >= (3, 12)) or ( + sys.platform == "darwin" or sys.platform.startswith("freebsd") + ): @property def st_birthtime(self) -> float: ...