From 7a63c8e72db2d9c4380aa6284c1d89c3284a5c58 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sat, 20 Dec 2025 21:36:20 +0100 Subject: [PATCH 1/6] upath: small fixes for hardlink_to backports --- upath/extensions.py | 5 +++++ upath/implementations/local.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/upath/extensions.py b/upath/extensions.py index 84d56798..d75dd43b 100644 --- a/upath/extensions.py +++ b/upath/extensions.py @@ -16,6 +16,7 @@ from urllib.parse import SplitResult from fsspec import AbstractFileSystem +from pathlib_abc import vfspath from upath._chain import Chain from upath._chain import ChainSegment @@ -160,6 +161,8 @@ def symlink_to( target: ReadablePathLike, target_is_directory: bool = False, ) -> None: + if not isinstance(target, str): + target = vfspath(target) self.__wrapped__.symlink_to(target, target_is_directory=target_is_directory) def mkdir( @@ -431,6 +434,8 @@ def is_relative_to(self, other, /, *_deprecated) -> bool: # type: ignore[overri return self.__wrapped__.is_relative_to(other, *_deprecated) def hardlink_to(self, target: ReadablePathLike) -> None: + if not isinstance(target, str): + target = vfspath(target) return self.__wrapped__.hardlink_to(target) def match(self, pattern: str, *, case_sensitive: bool | None = None) -> bool: diff --git a/upath/implementations/local.py b/upath/implementations/local.py index 7506b435..739c78aa 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -642,7 +642,7 @@ def relative_to( # type: ignore[override] def hardlink_to(self, target: ReadablePathLike) -> None: try: - os.link(target, self) # type: ignore[arg-type] + os.link(os.fspath(target), os.fspath(self)) # type: ignore[arg-type] except AttributeError: raise UnsupportedOperation("hardlink operation not supported") From b9cc8660c7059bc0c12a6d00ac668b5e3a74f1a4 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sat, 20 Dec 2025 21:35:13 +0100 Subject: [PATCH 2/6] tests: update symlink and hardlink tests --- upath/tests/test_extensions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index c3976957..e1e3a7ee 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -130,6 +130,18 @@ def test_cwd(self): with pytest.raises(UnsupportedOperation): type(self.path).cwd() + def test_lchmod(self): + self.path.lchmod(mode=0o777) + + def test_symlink_to(self): + self.path.joinpath("link").symlink_to(self.path) + + def test_hardlink_to(self): + try: + self.path.joinpath("link").hardlink_to(self.path) + except PermissionError: + pass # hardlink may require elevated permissions + def test_custom_subclass(): From fd154de912e2ac5a9d0f286365afa17162897125 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sat, 20 Dec 2025 23:09:18 +0100 Subject: [PATCH 3/6] tests: lchmod might not be available --- upath/tests/test_extensions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index e1e3a7ee..8f2d256f 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -1,5 +1,6 @@ import os import sys +from contextlib import nullcontext import pytest @@ -131,7 +132,12 @@ def test_cwd(self): type(self.path).cwd() def test_lchmod(self): - self.path.lchmod(mode=0o777) + if hasattr(os, "lchmod") and os.lchmod in os.supports_follow_symlinks: + cm = nullcontext() + else: + cm = pytest.raises(UnsupportedOperation) + with cm: + self.path.lchmod(mode=0o777) def test_symlink_to(self): self.path.joinpath("link").symlink_to(self.path) From 719a0377bd828af74609d3c249e6e60b04efca75 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sat, 20 Dec 2025 23:47:20 +0100 Subject: [PATCH 4/6] tests: better check for lchmod test --- upath/tests/test_extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 8f2d256f..55cebddd 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -132,7 +132,8 @@ def test_cwd(self): type(self.path).cwd() def test_lchmod(self): - if hasattr(os, "lchmod") and os.lchmod in os.supports_follow_symlinks: + # see: https://github.com/python/cpython/issues/108660#issuecomment-1854645898 + if hasattr(os, "lchmod") or os.chmod in os.supports_follow_symlinks: cm = nullcontext() else: cm = pytest.raises(UnsupportedOperation) From 18e4c006c094c7f615f2f6f4f1bfc028dc8aaf0f Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 00:49:55 +0100 Subject: [PATCH 5/6] tests: adjust lchmod --- upath/tests/test_extensions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 55cebddd..5ca38d91 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -132,13 +132,19 @@ def test_cwd(self): type(self.path).cwd() def test_lchmod(self): + # setup + a = self.path.joinpath("a") + b = self.path.joinpath("b") + a.touch() + b.symlink_to(a) + # see: https://github.com/python/cpython/issues/108660#issuecomment-1854645898 if hasattr(os, "lchmod") or os.chmod in os.supports_follow_symlinks: cm = nullcontext() else: cm = pytest.raises(UnsupportedOperation) with cm: - self.path.lchmod(mode=0o777) + b.lchmod(mode=0o777) def test_symlink_to(self): self.path.joinpath("link").symlink_to(self.path) From 501c2cffb83ef8c3ee418242490109e6e4804e22 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 01:01:04 +0100 Subject: [PATCH 6/6] tests: lchmod raises notimplemented on linux --- upath/tests/test_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upath/tests/test_extensions.py b/upath/tests/test_extensions.py index 5ca38d91..fdd802a5 100644 --- a/upath/tests/test_extensions.py +++ b/upath/tests/test_extensions.py @@ -142,7 +142,7 @@ def test_lchmod(self): if hasattr(os, "lchmod") or os.chmod in os.supports_follow_symlinks: cm = nullcontext() else: - cm = pytest.raises(UnsupportedOperation) + cm = pytest.raises((UnsupportedOperation, NotImplementedError)) with cm: b.lchmod(mode=0o777)