From c2a821636d9e6796efdbf8255f2c7abe16ee0b10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:16:24 +0000 Subject: [PATCH 1/3] Bump universal-pathlib from 0.2.6 to 0.3.10 Bumps [universal-pathlib](https://github.com/fsspec/universal_pathlib) from 0.2.6 to 0.3.10. - [Release notes](https://github.com/fsspec/universal_pathlib/releases) - [Changelog](https://github.com/fsspec/universal_pathlib/blob/main/CHANGELOG.md) - [Commits](https://github.com/fsspec/universal_pathlib/compare/v0.2.6...v0.3.10) --- updated-dependencies: - dependency-name: universal-pathlib dependency-version: 0.3.10 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- uv.lock | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 1df51b41..b875a8da 100644 --- a/uv.lock +++ b/uv.lock @@ -1324,6 +1324,7 @@ dependencies = [ { name = "griffecli" }, { name = "griffelib" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/04/56/28a0accac339c164b52a92c6cfc45a903acc0c174caa5c1713803467b533/griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f", size = 293906, upload-time = "2026-03-23T21:06:53.402Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, ] @@ -1336,6 +1337,7 @@ dependencies = [ { name = "colorama" }, { name = "griffelib" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/a4/f8/2e129fd4a86e52e58eefe664de05e7d502decf766e7316cc9e70fdec3e18/griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef", size = 56213, upload-time = "2026-03-23T21:06:54.8Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, ] @@ -1344,6 +1346,7 @@ wheels = [ name = "griffelib" version = "2.0.0" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] @@ -2486,6 +2489,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] +[[package]] +name = "pathlib-abc" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/cb/448649d7f25d228bf0be3a04590ab7afa77f15e056f8fa976ed05ec9a78f/pathlib_abc-0.5.2.tar.gz", hash = "sha256:fcd56f147234645e2c59c7ae22808b34c364bb231f685ddd9f96885aed78a94c", size = 33342, upload-time = "2025-10-10T18:37:20.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/29/c028a0731e202035f0e2e0bfbf1a3e46ad6c628cbb17f6f1cc9eea5d9ff1/pathlib_abc-0.5.2-py3-none-any.whl", hash = "sha256:4c9d94cf1b23af417ce7c0417b43333b06a106c01000b286c99de230d95eefbb", size = 19070, upload-time = "2025-10-10T18:37:19.437Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -3854,14 +3866,15 @@ wheels = [ [[package]] name = "universal-pathlib" -version = "0.2.6" +version = "0.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fsspec" }, + { name = "pathlib-abc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/21/dd871495af3933e585261adce42678dcdf1168c9d6fa0a8f7b6565e54472/universal_pathlib-0.2.6.tar.gz", hash = "sha256:50817aaeaa9f4163cb1e76f5bdf84207fa05ce728b23fd779479b3462e5430ac", size = 175427, upload-time = "2024-12-13T00:58:27.514Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/6e/d997a70ee8f4c61f9a7e2f4f8af721cf072a3326848fc881b05187e52558/universal_pathlib-0.3.10.tar.gz", hash = "sha256:4487cbc90730a48cfb64f811d99e14b6faed6d738420cd5f93f59f48e6930bfb", size = 261110, upload-time = "2026-02-22T14:40:58.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/4d/2e577f6db7aa0f932d19f799c18f604b2b302c65f733419b900ec07dbade/universal_pathlib-0.2.6-py3-none-any.whl", hash = "sha256:700dec2b58ef34b87998513de6d2ae153b22f083197dfafb8544744edabd1b18", size = 50087, upload-time = "2024-12-13T00:58:24.582Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1a/5d9a402b39ec892d856bbdd9db502ff73ce28cdf4aff72eb1ce1d6843506/universal_pathlib-0.3.10-py3-none-any.whl", hash = "sha256:dfaf2fb35683d2eb1287a3ed7b215e4d6016aa6eaf339c607023d22f90821c66", size = 83528, upload-time = "2026-02-22T14:40:57.316Z" }, ] [[package]] From e2943f6c545a559fc7447f7e312b0b390ee5b35d Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Thu, 26 Mar 2026 22:43:41 +0100 Subject: [PATCH 2/3] Fix UPath typing for universal-pathlib 0.3 --- src/_pytask/collect.py | 2 +- src/_pytask/lockfile.py | 8 +++++--- src/_pytask/node_protocols.py | 3 ++- src/_pytask/nodes.py | 22 +++++++++++----------- src/_pytask/path.py | 35 +++++++++++++++++++++-------------- src/_pytask/typing.py | 11 +++++++++-- 6 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index 4a09c9b0..42138ab1 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -501,7 +501,7 @@ def pytask_collect_node( # noqa: C901, PLR0912 if isinstance(node, UPath): # pragma: no cover if not node.protocol: - node = Path(node) + node = Path(str(node)) else: return PathNode(name=node.name, path=node) diff --git a/src/_pytask/lockfile.py b/src/_pytask/lockfile.py index ba18ccbd..e7c16ab7 100644 --- a/src/_pytask/lockfile.py +++ b/src/_pytask/lockfile.py @@ -27,6 +27,7 @@ if TYPE_CHECKING: from _pytask.session import Session + from _pytask.typing import NodePath CURRENT_LOCKFILE_VERSION = "1" @@ -71,13 +72,14 @@ def _encode_node_path(path: tuple[str | int, ...]) -> str: return msgspec.json.encode(path).decode() -def _relative_path(path: Path, root: Path) -> str: +def _relative_path(path: NodePath, root: Path) -> str: if isinstance(path, UPath) and path.protocol: return str(path) + local_path = Path(str(path)) if isinstance(path, UPath) else path try: - rel = os.path.relpath(path, root) + rel = os.path.relpath(local_path, root) except ValueError: - return path.as_posix() + return local_path.as_posix() return Path(rel).as_posix() diff --git a/src/_pytask/node_protocols.py b/src/_pytask/node_protocols.py index a50ed8f7..7c22b879 100644 --- a/src/_pytask/node_protocols.py +++ b/src/_pytask/node_protocols.py @@ -11,6 +11,7 @@ from _pytask.mark import Mark from _pytask.tree_util import PyTree + from _pytask.typing import NodePath __all__ = ["PNode", "PPathNode", "PProvisionalNode", "PTask", "PTaskWithPath"] @@ -60,7 +61,7 @@ class PPathNode(PNode, Protocol): """ - path: Path + path: NodePath @runtime_checkable diff --git a/src/_pytask/nodes.py b/src/_pytask/nodes.py index 9c34e19c..af728ce8 100644 --- a/src/_pytask/nodes.py +++ b/src/_pytask/nodes.py @@ -24,13 +24,13 @@ from _pytask.node_protocols import PTaskWithPath from _pytask.path import hash_path from _pytask.typing import NoDefault +from _pytask.typing import NodePath from _pytask.typing import no_default if TYPE_CHECKING: from collections.abc import Callable - from io import BufferedReader - from io import BufferedWriter from pathlib import Path + from typing import BinaryIO from _pytask.mark import Mark from _pytask.models import NodeInfo @@ -176,7 +176,7 @@ class PathNode(PPathNode): """ - path: Path + path: NodePath name: str = "" attributes: dict[Any, Any] = field(default_factory=dict) @@ -187,7 +187,7 @@ def signature(self) -> str: return hashlib.sha256(raw_key.encode()).hexdigest() @classmethod - def from_path(cls, path: Path) -> PathNode: + def from_path(cls, path: NodePath) -> PathNode: """Instantiate class from path to file.""" return cls(name=path.as_posix(), path=path) @@ -199,7 +199,7 @@ def state(self) -> str | None: """ return get_state_of_path(self.path) - def load(self, is_product: bool = False) -> Path: # noqa: ARG002 + def load(self, is_product: bool = False) -> NodePath: # noqa: ARG002 """Load the value.""" return self.path @@ -330,11 +330,11 @@ class PickleNode(PPathNode): """ - path: Path + path: NodePath name: str = "" attributes: dict[Any, Any] = field(default_factory=dict) - serializer: Callable[[Any, BufferedWriter], None] = field(default=pickle.dump) - deserializer: Callable[[BufferedReader], Any] = field(default=pickle.load) + serializer: Callable[[Any, BinaryIO], None] = field(default=pickle.dump) + deserializer: Callable[[BinaryIO], Any] = field(default=pickle.load) @property def signature(self) -> str: @@ -343,7 +343,7 @@ def signature(self) -> str: return hashlib.sha256(raw_key.encode()).hexdigest() @classmethod - def from_path(cls, path: Path) -> PickleNode: + def from_path(cls, path: NodePath) -> PickleNode: """Instantiate class from path to file.""" if not path.is_absolute(): msg = "Node must be instantiated from absolute path." @@ -409,7 +409,7 @@ def collect(self) -> list[Path]: return list(self.root_dir.glob(self.pattern)) # type: ignore[union-attr] -def get_state_of_path(path: Path) -> str | None: +def get_state_of_path(path: NodePath) -> str | None: """Get state of a path. A simple function to handle local and remote files. @@ -436,7 +436,7 @@ def get_state_of_path(path: Path) -> str | None: @deprecated("Use 'pytask.get_state_of_path' instead.") -def _get_state(path: Path) -> str | None: +def _get_state(path: NodePath) -> str | None: """Get state of a path. A simple function to handle local and remote files. diff --git a/src/_pytask/path.py b/src/_pytask/path.py index 5619c04f..eb2f8c92 100644 --- a/src/_pytask/path.py +++ b/src/_pytask/path.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + from _pytask.typing import NodePath + __all__ = [ "find_case_sensitive_path", "find_closest_ancestor", @@ -64,24 +66,27 @@ def relative_to(path: Path, source: Path, *, include_source: bool = True) -> Pat return Path(source_name, path.relative_to(source)) -def is_non_local_path(path: Path) -> bool: +def is_non_local_path(path: NodePath) -> bool: """Return whether a path points to a non-local `UPath` resource.""" return isinstance(path, UPath) and path.protocol not in _LOCAL_UPATH_PROTOCOLS -def normalize_local_upath(path: Path) -> Path: +def normalize_local_upath(path: NodePath) -> NodePath: """Convert local `UPath` variants to a stdlib `Path`.""" - if isinstance(path, UPath) and path.protocol in {"file", "local"}: - local_path = path.path - if ( - sys.platform == "win32" - and local_path.startswith("/") - and len(local_path) >= _WINDOWS_DRIVE_PREFIX_LENGTH - and local_path[1].isalpha() - and local_path[2] == ":" - ): - local_path = local_path[1:] - return Path(local_path) + if isinstance(path, UPath): + if path.protocol in {"file", "local"}: + local_path = path.path + if ( + sys.platform == "win32" + and local_path.startswith("/") + and len(local_path) >= _WINDOWS_DRIVE_PREFIX_LENGTH + and local_path[1].isalpha() + and local_path[2] == ":" + ): + local_path = local_path[1:] + return Path(local_path) + if not path.protocol: + return Path(str(path)) return path @@ -451,7 +456,7 @@ def _insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> module_name = ".".join(module_parts) -def shorten_path(path: Path, paths: Sequence[Path]) -> str: +def shorten_path(path: NodePath, paths: Sequence[NodePath]) -> str: """Shorten a path. The whole path of a node - which includes the drive letter - can be very long @@ -466,6 +471,8 @@ def shorten_path(path: Path, paths: Sequence[Path]) -> str: path = normalize_local_upath(path) paths = [normalize_local_upath(p) for p in paths] + path = Path(str(path)) if isinstance(path, UPath) else path + paths = [Path(str(p)) if isinstance(p, UPath) else p for p in paths] ancestor = find_closest_ancestor(path, paths) if ancestor is None: diff --git a/src/_pytask/typing.py b/src/_pytask/typing.py index a3ee26f7..ef479c8b 100644 --- a/src/_pytask/typing.py +++ b/src/_pytask/typing.py @@ -3,22 +3,25 @@ import functools from dataclasses import dataclass from enum import Enum +from pathlib import Path from typing import TYPE_CHECKING from typing import Any from typing import Final from typing import Literal from typing import Protocol +from typing import TypeAlias from typing import runtime_checkable -if TYPE_CHECKING: - from typing import TypeAlias +from upath import UPath +if TYPE_CHECKING: from _pytask.models import CollectionMetadata from pytask import PTask __all__ = [ "NoDefault", + "NodePath", "Product", "ProductType", "TaskFunction", @@ -27,6 +30,10 @@ ] +NodePath: TypeAlias = Path | UPath +"""A local stdlib path or a universal-pathlib path.""" + + @runtime_checkable class TaskFunction(Protocol): """Protocol for callables decorated with @task that have pytask_meta attached. From 2f01d23d4f6172e1bffbc0f2a8ec26495be0e3c7 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Fri, 27 Mar 2026 08:17:10 +0100 Subject: [PATCH 3/3] Fix local UPath joinpath typing --- src/_pytask/collect.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index 42138ab1..a5eacdde 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -465,7 +465,10 @@ def pytask_collect_node( # noqa: C901, PLR0912 and not is_non_local_path(node.path) and not node.path.is_absolute() ): - node.path = path.joinpath(node.path) + local_node_path = ( + Path(str(node.path)) if isinstance(node.path, UPath) else node.path + ) + node.path = path.joinpath(local_node_path) # ``normpath`` removes ``../`` from the path which is necessary for the casing # check which will fail since ``.resolves()`` also normalizes a path.