From 18eaac675b3cb1e3bbb0e2bc254389c6c2179423 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 13:35:20 +0200 Subject: [PATCH 01/10] typesafety: add extensive type checks for UPath and implementations --- .github/workflows/tests.yml | 4 +- noxfile.py | 1 + typesafety/test_upath_signatures.yml | 1064 ++++++++++++++++++++++++++ typesafety/test_upath_types.yml | 15 + 4 files changed, 1082 insertions(+), 2 deletions(-) create mode 100644 typesafety/test_upath_signatures.yml create mode 100644 typesafety/test_upath_types.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aea00a6a..cbc1137b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -67,7 +67,7 @@ jobs: with: fetch-depth: 0 - - name: Set up Python ${{ matrix.pyv }} + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.10' @@ -90,7 +90,7 @@ jobs: with: fetch-depth: 0 - - name: Set up Python ${{ matrix.pyv }} + - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/noxfile.py b/noxfile.py index c4d739a3..d5018acc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -115,6 +115,7 @@ def typesafety(session): "--mypy-pyproject-toml-file", "pyproject.toml", "typesafety", + *session.posargs, ) diff --git a/typesafety/test_upath_signatures.yml b/typesafety/test_upath_signatures.yml new file mode 100644 index 00000000..781a801a --- /dev/null +++ b/typesafety/test_upath_signatures.yml @@ -0,0 +1,1064 @@ +- case: upath_constructor_no_args + disable_cache: false + main: | + from upath import UPath + + reveal_type(UPath()) # N: Revealed type is "upath.core.UPath" + +- case: upath_constructor_string_arg + disable_cache: false + main: | + from upath import UPath + + reveal_type(UPath(".")) # N: Revealed type is "upath.core.UPath" + +- case: upath_constructor_purepath_arg + disable_cache: false + main: | + from pathlib import PurePath + from upath import UPath + + reveal_type(UPath(PurePath())) # N: Revealed type is "upath.core.UPath" + +- case: upath_constructor_path_arg + disable_cache: false + main: | + from pathlib import Path + from upath import UPath + + reveal_type(UPath(Path())) # N: Revealed type is "upath.core.UPath" + +- case: upath_constructor_pathlike_arg + disable_cache: false + main: | + from upath import UPath + + class CustomPathLike: + def __fspath__(self) -> str: + return "" + + reveal_type(UPath(CustomPathLike())) # N: Revealed type is "upath.core.UPath" + +- case: upath_constructor_vfspathlike_arg + disable_cache: false + main: | + from upath import UPath + + class CustomVFSPathLike: + def __vfspath__(self) -> str: + return "" + + reveal_type(UPath(CustomVFSPathLike())) # N: Revealed type is "upath.core.UPath" + +- case: upath_constructor_upath_arg + disable_cache: false + main: | + from upath import UPath + + reveal_type(UPath(UPath())) # N: Revealed type is "upath.core.UPath" + +- case: upath_implementations_constructor + disable_cache: false + parametrized: + - module: upath.implementations.cached + cls: SimpleCachePath + - module: upath.implementations.cloud + cls: S3Path + - module: upath.implementations.cloud + cls: GCSPath + - module: upath.implementations.cloud + cls: AzurePath + - module: upath.implementations.data + cls: DataPath + - module: upath.implementations.github + cls: GitHubPath + - module: upath.implementations.hdfs + cls: HDFSPath + - module: upath.implementations.http + cls: HTTPPath + - module: upath.implementations.local + cls: PosixUPath + - module: upath.implementations.local + cls: WindowsUPath + - module: upath.implementations.local + cls: FilePath + - module: upath.implementations.memory + cls: MemoryPath + - module: upath.implementations.sftp + cls: SFTPPath + - module: upath.implementations.smb + cls: SMBPath + - module: upath.implementations.webdav + cls: WebdavPath + - module: upath.extensions + cls: ProxyUPath + main: | + from pathlib import PurePath + from {{ module }} import {{ cls }} + + reveal_type({{ cls }}()) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type({{ cls }}(".")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type({{ cls }}(PurePath())) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type({{ cls }}({{ cls }}())) # N: Revealed type is "{{ module }}.{{ cls }}" + +- case: upath_attribute_parts + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.parts) # N: Revealed type is "typing.Sequence[builtins.str]" + +- case: upath_attribute_anchor + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.anchor) # N: Revealed type is "builtins.str" + +- case: upath_attribute_name + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.name) # N: Revealed type is "builtins.str" + +- case: upath_attribute_stem + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.stem) # N: Revealed type is "builtins.str" + +- case: upath_attribute_suffix + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.suffix) # N: Revealed type is "builtins.str" + +- case: upath_attribute_suffixes + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.suffixes) # N: Revealed type is "builtins.list[builtins.str]" + +- case: upath_attribute_root + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.root) # N: Revealed type is "builtins.str" + +- case: upath_attribute_drive + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.drive) # N: Revealed type is "builtins.str" + +- case: upath_attribute_parent + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.parent) # N: Revealed type is "upath.core.UPath" + +- case: upath_attribute_parents + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.parents) # N: Revealed type is "typing.Sequence[upath.core.UPath]" + +- case: upath_attribute_path + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.path) # N: Revealed type is "builtins.str" + +- case: upath_attribute_protocol + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.protocol) # N: Revealed type is "builtins.str" + +- case: upath_attribute_storage_options + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.storage_options) # N: Revealed type is "typing.Mapping[builtins.str, Any]" + +- case: upath_attribute__url + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p._url) # N: Revealed type is "tuple[builtins.str, builtins.str, builtins.str, builtins.str, builtins.str, fallback=urllib.parse.SplitResult]" + +# FIXME fsspec types are not available +# - case: upath_attribute_fs +# disable_cache: false +# main: | +# from upath import UPath +# +# p = UPath("") +# reveal_type(p.fs) # N: Revealed type is "fsspec.spec.AbstractFileSystem" + +- case: upath_attribute_parser + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.parser) # N: Revealed type is "upath.types.UPathParser" + +- case: upath_attribute_info + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.info) # N: Revealed type is "upath.types._abc.PathInfo" + +- case: upath_subclass_attributes + disable_cache: false + parametrized: + - module: upath.implementations.cached + cls: SimpleCachePath + - module: upath.implementations.cloud + cls: S3Path + - module: upath.implementations.cloud + cls: GCSPath + - module: upath.implementations.cloud + cls: AzurePath + - module: upath.implementations.data + cls: DataPath + - module: upath.implementations.github + cls: GitHubPath + - module: upath.implementations.hdfs + cls: HDFSPath + - module: upath.implementations.http + cls: HTTPPath + - module: upath.implementations.local + cls: PosixUPath + - module: upath.implementations.local + cls: WindowsUPath + - module: upath.implementations.local + cls: FilePath + - module: upath.implementations.memory + cls: MemoryPath + - module: upath.implementations.sftp + cls: SFTPPath + - module: upath.implementations.smb + cls: SMBPath + - module: upath.implementations.webdav + cls: WebdavPath + - module: upath.extensions + cls: ProxyUPath + main: | + from {{ module }} import {{ cls }} + + p = {{ cls }}("") + reveal_type(p.parts) # NR: Revealed type is "(typing\.Sequence|builtins\.tuple)\[builtins\.str(, ...)?\]" + reveal_type(p.anchor) # N: Revealed type is "builtins.str" + reveal_type(p.name) # N: Revealed type is "builtins.str" + reveal_type(p.stem) # N: Revealed type is "builtins.str" + reveal_type(p.suffix) # N: Revealed type is "builtins.str" + reveal_type(p.suffixes) # N: Revealed type is "builtins.list[builtins.str]" + reveal_type(p.root) # N: Revealed type is "builtins.str" + reveal_type(p.drive) # N: Revealed type is "builtins.str" + reveal_type(p.parent) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.parents) # N: Revealed type is "typing.Sequence[{{ module }}.{{ cls }}]" + reveal_type(p.path) # N: Revealed type is "builtins.str" + reveal_type(p.protocol) # N: Revealed type is "builtins.str" + reveal_type(p.storage_options) # N: Revealed type is "typing.Mapping[builtins.str, Any]" + # reveal_type(p.fs) + # reveal_type(p.parser) + reveal_type(p.info) # NR: Revealed type is ".*PathInfo" + +- case: upath_method_with_segments + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.with_segments("foo", "bar")) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_vfspath + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.__vfspath__()) # N: Revealed type is "builtins.str" + +- case: upath_method_with_name + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.with_name("newname")) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_with_stem + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.with_stem("newstem")) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_with_suffix + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.with_suffix(".txt")) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_joinpath + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.joinpath("foo")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.joinpath(UPath("bar"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_truediv + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p / "foo") # N: Revealed type is "upath.core.UPath" + reveal_type(p / UPath("bar")) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_rtruediv + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type("foo" / p) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_full_match + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.full_match("*.txt")) # N: Revealed type is "builtins.bool" + +- case: upath_method_open_reader + disable_cache: false + main: | + from upath import UPath + from typing import BinaryIO + + p = UPath("") + reveal_type(p.__open_reader__()) # N: Revealed type is "typing.BinaryIO" + +- case: upath_method_open_writer + disable_cache: false + main: | + from upath import UPath + from typing import BinaryIO + + p = UPath("") + reveal_type(p.__open_writer__("a")) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.__open_writer__("w")) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.__open_writer__("x")) # N: Revealed type is "typing.BinaryIO" + +- case: upath_method_read_bytes + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.read_bytes()) # N: Revealed type is "builtins.bytes" + +- case: upath_method_read_text + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.read_text()) # N: Revealed type is "builtins.str" + +- case: upath_method_iterdir + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.iterdir()) # N: Revealed type is "typing.Iterator[upath.core.UPath]" + +- case: upath_method_glob + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.glob("*.txt")) # N: Revealed type is "typing.Iterator[upath.core.UPath]" + +- case: upath_method_walk + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.walk()) # N: Revealed type is "typing.Iterator[tuple[upath.core.UPath, builtins.list[builtins.str], builtins.list[builtins.str]]]" + +- case: upath_method_readlink + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.readlink()) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_copy + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.copy("target")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.copy(UPath("target"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_copy_into + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.copy_into("target_dir")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.copy_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_symlink_to + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.symlink_to("target")) # N: Revealed type is "None" + reveal_type(p.symlink_to(UPath("target"))) # N: Revealed type is "None" + +- case: upath_method_mkdir + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.mkdir()) # N: Revealed type is "None" + +- case: upath_method_write_bytes + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.write_bytes(b"data")) # N: Revealed type is "builtins.int" + +- case: upath_method_write_text + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.write_text("data")) # N: Revealed type is "builtins.int" + +- case: upath_method_copy_from + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p._copy_from(UPath("source"))) # N: Revealed type is "None" + +- case: upath_method_joinuri + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.joinuri("other")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.joinuri(UPath("other"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_str + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(str(p)) # N: Revealed type is "builtins.str" + +- case: upath_method_repr + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(repr(p)) # N: Revealed type is "builtins.str" + +- case: upath_method_open + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.open()) # N: Revealed type is "typing.TextIO" + reveal_type(p.open("w")) # N: Revealed type is "typing.TextIO" + reveal_type(p.open("rb")) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.open("wb")) # N: Revealed type is "typing.BinaryIO" + +- case: upath_method_open_fsspec_kwargs + disable_cache: false + parametrized: + - module: upath.implementations.cached + cls: SimpleCachePath + - module: upath.implementations.cloud + cls: S3Path + - module: upath.implementations.cloud + cls: GCSPath + - module: upath.implementations.cloud + cls: AzurePath + - module: upath.implementations.data + cls: DataPath + - module: upath.implementations.github + cls: GitHubPath + - module: upath.implementations.hdfs + cls: HDFSPath + - module: upath.implementations.http + cls: HTTPPath + - module: upath.implementations.local + cls: PosixUPath + - module: upath.implementations.local + cls: WindowsUPath + - module: upath.implementations.local + cls: FilePath + - module: upath.implementations.memory + cls: MemoryPath + - module: upath.implementations.sftp + cls: SFTPPath + - module: upath.implementations.smb + cls: SMBPath + - module: upath.implementations.webdav + cls: WebdavPath + - module: upath.extensions + cls: ProxyUPath + main: | + from {{ module }} import {{ cls }} + + p = {{ cls }}("") + reveal_type(p.open(mode="rb", compression="gz")) # N: Revealed type is "typing.BinaryIO" + +- case: upath_method_stat + disable_cache: false + main: | + from upath import UPath + import os + + p = UPath("") + reveal_type(p.stat()) # N: Revealed type is "upath._stat.UPathStatResult" + +- case: upath_method_lstat + disable_cache: false + main: | + from upath import UPath + import os + + p = UPath("") + reveal_type(p.lstat()) # N: Revealed type is "upath._stat.UPathStatResult" + +- case: upath_method_chmod + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.chmod(0o755)) # N: Revealed type is "None" + +- case: upath_method_exists + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.exists()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_dir + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_dir()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_file + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_file()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_mount + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_mount()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_symlink + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_symlink()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_junction + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_junction()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_block_device + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_block_device()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_char_device + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_char_device()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_fifo + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_fifo()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_socket + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_socket()) # N: Revealed type is "builtins.bool" + +- case: upath_method_is_reserved + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_reserved()) # N: Revealed type is "builtins.bool" + +- case: upath_method_expanduser + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.expanduser()) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_rglob + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.rglob("*.txt")) # N: Revealed type is "typing.Iterator[upath.core.UPath]" + +- case: upath_method_owner + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.owner()) # N: Revealed type is "builtins.str" + +- case: upath_method_group + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.group()) # N: Revealed type is "builtins.str" + +- case: upath_method_absolute + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.absolute()) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_is_absolute + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_absolute()) # N: Revealed type is "builtins.bool" + +- case: upath_method_eq + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p == UPath("other")) # N: Revealed type is "builtins.bool" + +- case: upath_method_hash + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(hash(p)) # N: Revealed type is "builtins.int" + +- case: upath_method_lt + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p < UPath("other")) # N: Revealed type is "builtins.bool" + +- case: upath_method_le + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p <= UPath("other")) # N: Revealed type is "builtins.bool" + +- case: upath_method_gt + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p > UPath("other")) # N: Revealed type is "builtins.bool" + +- case: upath_method_ge + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p >= UPath("other")) # N: Revealed type is "builtins.bool" + +- case: upath_method_resolve + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.resolve()) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_touch + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.touch()) # N: Revealed type is "None" + +- case: upath_method_lchmod + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.lchmod(0o755)) # N: Revealed type is "None" + +- case: upath_method_unlink + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.unlink()) # N: Revealed type is "None" + +- case: upath_method_rmdir + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.rmdir()) # N: Revealed type is "None" + +- case: upath_method_rename + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.rename("new_name")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.rename(UPath("new_name"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_replace + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.replace("target")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.replace(UPath("target"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_as_uri + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.as_uri()) # N: Revealed type is "builtins.str" + +- case: upath_method_as_posix + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.as_posix()) # N: Revealed type is "builtins.str" + +- case: upath_method_samefile + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.samefile("other")) # N: Revealed type is "builtins.bool" + reveal_type(p.samefile(UPath("other"))) # N: Revealed type is "builtins.bool" + +- case: upath_classmethod_cwd + disable_cache: false + main: | + from upath import UPath + + reveal_type(UPath.cwd()) # N: Revealed type is "upath.core.UPath" + +- case: upath_classmethod_home + disable_cache: false + main: | + from upath import UPath + + reveal_type(UPath.home()) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_relative_to + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.relative_to("other")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.relative_to(UPath("other"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_is_relative_to + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.is_relative_to("other")) # N: Revealed type is "builtins.bool" + reveal_type(p.is_relative_to(UPath("other"))) # N: Revealed type is "builtins.bool" + +- case: upath_method_hardlink_to + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.hardlink_to("target")) # N: Revealed type is "None" + reveal_type(p.hardlink_to(UPath("target"))) # N: Revealed type is "None" + +- case: upath_method_match + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.match("*.txt")) # N: Revealed type is "builtins.bool" + +- case: upath_subclass_methods + disable_cache: false + parametrized: + - module: upath.implementations.cached + cls: SimpleCachePath + - module: upath.implementations.cloud + cls: S3Path + - module: upath.implementations.cloud + cls: GCSPath + - module: upath.implementations.cloud + cls: AzurePath + - module: upath.implementations.data + cls: DataPath + - module: upath.implementations.github + cls: GitHubPath + - module: upath.implementations.hdfs + cls: HDFSPath + - module: upath.implementations.http + cls: HTTPPath + - module: upath.implementations.local + cls: PosixUPath + - module: upath.implementations.local + cls: WindowsUPath + - module: upath.implementations.local + cls: FilePath + - module: upath.implementations.memory + cls: MemoryPath + - module: upath.implementations.sftp + cls: SFTPPath + - module: upath.implementations.smb + cls: SMBPath + - module: upath.implementations.webdav + cls: WebdavPath + - module: upath.extensions + cls: ProxyUPath + main: | + from {{ module }} import {{ cls }} + + p = {{ cls }}("") + + # Path manipulation methods + reveal_type(p.with_segments("foo", "bar")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.__vfspath__()) # N: Revealed type is "builtins.str" + reveal_type(p.with_name("newname")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.with_stem("newstem")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.with_suffix(".txt")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.joinpath("foo")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.joinpath({{ cls }}("bar"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p / "foo") # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p / {{ cls }}("bar")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type("foo" / p) # N: Revealed type is "{{ module }}.{{ cls }}" + + # Boolean methods + reveal_type(p.full_match("*.txt")) # N: Revealed type is "builtins.bool" + reveal_type(p.exists()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_dir()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_file()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_mount()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_symlink()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_junction()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_block_device()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_char_device()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_fifo()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_socket()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_reserved()) # N: Revealed type is "builtins.bool" + reveal_type(p.is_absolute()) # N: Revealed type is "builtins.bool" + reveal_type(p.match("*.txt")) # N: Revealed type is "builtins.bool" + reveal_type(p.is_relative_to("other")) # N: Revealed type is "builtins.bool" + reveal_type(p.is_relative_to({{ cls }}("other"))) # N: Revealed type is "builtins.bool" + + # File I/O methods + reveal_type(p.__open_reader__()) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.__open_writer__("a")) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.__open_writer__("w")) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.__open_writer__("x")) # N: Revealed type is "typing.BinaryIO" + reveal_type(p.read_bytes()) # N: Revealed type is "builtins.bytes" + reveal_type(p.read_text()) # N: Revealed type is "builtins.str" + reveal_type(p.open()) # NR: Revealed type is ".*TextIO.*" + reveal_type(p.open("w")) # NR: Revealed type is ".*TextIO.*" + reveal_type(p.open("rb")) # NR: Revealed type is ".*(BinaryIO|BufferedReader).*" + reveal_type(p.open("wb")) # NR: Revealed type is ".*(BinaryIO|BufferedWriter).*" + reveal_type(p.write_bytes(b"data")) # N: Revealed type is "builtins.int" + reveal_type(p.write_text("data")) # N: Revealed type is "builtins.int" + + # Iterator methods + reveal_type(p.iterdir()) # NR: Revealed type is "typing\.(Iterator|Generator)\[{{ module }}\.{{ cls }}.*\]" + reveal_type(p.glob("*.txt")) # NR: Revealed type is "typing\.(Iterator|Generator)\[{{ module }}\.{{ cls }}.*\]" + reveal_type(p.rglob("*.txt")) # NR: Revealed type is "typing\.(Iterator|Generator)\[{{ module }}\.{{ cls }}.*\]" + reveal_type(p.walk()) # N: Revealed type is "typing.Iterator[tuple[{{ module }}.{{ cls }}, builtins.list[builtins.str], builtins.list[builtins.str]]]" + + # Path resolution methods + reveal_type(p.readlink()) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.expanduser()) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.absolute()) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.resolve()) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.relative_to("other")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.relative_to({{ cls }}("other"))) # N: Revealed type is "{{ module }}.{{ cls }}" + + # File system operations that return paths + reveal_type(p.copy("target")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.copy({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.copy_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.copy_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.rename("new_name")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.rename({{ cls }}("new_name"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.replace("target")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.replace({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.joinuri("other")) # NR: Revealed type is "(upath.core.UPath|upath.extensions.ProxyUPath)" + reveal_type(p.joinuri({{ cls }}("other"))) # NR: Revealed type is "(upath.core.UPath|upath.extensions.ProxyUPath)" + + # String returning methods + reveal_type(str(p)) # N: Revealed type is "builtins.str" + reveal_type(repr(p)) # N: Revealed type is "builtins.str" + reveal_type(p.as_uri()) # N: Revealed type is "builtins.str" + reveal_type(p.as_posix()) # N: Revealed type is "builtins.str" + reveal_type(p.owner()) # N: Revealed type is "builtins.str" + reveal_type(p.group()) # N: Revealed type is "builtins.str" + + # Stat methods + reveal_type(p.stat()) # NR: Revealed type is ".*(UPathStatResult|stat_result).*" + reveal_type(p.lstat()) # NR: Revealed type is ".*(UPathStatResult|stat_result).*" + + # None methods + reveal_type(p.symlink_to("target")) # N: Revealed type is "None" + reveal_type(p.symlink_to({{ cls }}("target"))) # N: Revealed type is "None" + reveal_type(p.mkdir()) # N: Revealed type is "None" + reveal_type(p._copy_from({{ cls }}("source"))) # N: Revealed type is "None" + reveal_type(p.chmod(0o755)) # N: Revealed type is "None" + reveal_type(p.touch()) # N: Revealed type is "None" + reveal_type(p.lchmod(0o755)) # N: Revealed type is "None" + reveal_type(p.unlink()) # N: Revealed type is "None" + reveal_type(p.rmdir()) # N: Revealed type is "None" + reveal_type(p.hardlink_to("target")) # N: Revealed type is "None" + reveal_type(p.hardlink_to({{ cls }}("target"))) # N: Revealed type is "None" + + # Comparison methods + reveal_type(p == {{ cls }}("other")) # N: Revealed type is "builtins.bool" + reveal_type(hash(p)) # N: Revealed type is "builtins.int" + reveal_type(p < {{ cls }}("other")) # N: Revealed type is "builtins.bool" + reveal_type(p <= {{ cls }}("other")) # N: Revealed type is "builtins.bool" + reveal_type(p > {{ cls }}("other")) # N: Revealed type is "builtins.bool" + reveal_type(p >= {{ cls }}("other")) # N: Revealed type is "builtins.bool" + reveal_type(p.samefile("other")) # N: Revealed type is "builtins.bool" + reveal_type(p.samefile({{ cls }}("other"))) # N: Revealed type is "builtins.bool" + + # Class methods + reveal_type({{ cls }}.cwd()) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type({{ cls }}.home()) # N: Revealed type is "{{ module }}.{{ cls }}" diff --git a/typesafety/test_upath_types.yml b/typesafety/test_upath_types.yml new file mode 100644 index 00000000..efbb13c2 --- /dev/null +++ b/typesafety/test_upath_types.yml @@ -0,0 +1,15 @@ +- case: upath_types_joinablepath + disable_cache: false + main: | + from pathlib import PurePath + from upath.types import CompatJoinablePath + + a: CompatJoinablePath = PurePath() + +- case: upath_types_joinablepath_upath + disable_cache: false + main: | + from upath import UPath + from upath.types import CompatJoinablePath + + a: CompatJoinablePath = UPath() From 4982c4924b5824fc30e542f31ed2d9bcfd6bf62e Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 13:37:27 +0200 Subject: [PATCH 02/10] upath: add basic PathInfo implementation for UPath --- upath/_info.py | 33 +++++++++++++++++++++++++++++++++ upath/core.py | 3 ++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 upath/_info.py diff --git a/upath/_info.py b/upath/_info.py new file mode 100644 index 00000000..03743285 --- /dev/null +++ b/upath/_info.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from upath.types import PathInfo + +if TYPE_CHECKING: + from upath import UPath + + +__all__ = [ + "UPathInfo", +] + + +class UPathInfo(PathInfo): + """Path info for UPath objects.""" + + def __init__(self, path: UPath) -> None: + self._path = path.path + self._fs = path.fs + + def exists(self, *, follow_symlinks=True) -> bool: + return self._fs.exists(self._path) + + def is_dir(self, *, follow_symlinks=True) -> bool: + return self._fs.isdir(self._path) + + def is_file(self, *, follow_symlinks=True) -> bool: + return self._fs.isfile(self._path) + + def is_symlink(self) -> bool: + return False diff --git a/upath/core.py b/upath/core.py index cc3fd465..1c86665c 100644 --- a/upath/core.py +++ b/upath/core.py @@ -31,6 +31,7 @@ from upath._flavour import WrappedFileSystemFlavour from upath._flavour import upath_get_kwargs_from_url from upath._flavour import upath_urijoin +from upath._info import UPathInfo from upath._protocol import compatible_protocol from upath._protocol import get_upath_protocol from upath._stat import UPathStatResult @@ -574,7 +575,7 @@ def parents(self) -> Sequence[Self]: @property def info(self) -> PathInfo: - _raise_unsupported(type(self).__name__, "info") + return UPathInfo(self) def iterdir(self) -> Iterator[Self]: sep = self.parser.sep From 7991edb12dc217e5169828295bf4c6963ec3a88a Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 13:38:43 +0200 Subject: [PATCH 03/10] upath.types: adjust PathLike types --- upath/types/__init__.py | 54 ++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/upath/types/__init__.py b/upath/types/__init__.py index b7e0bed8..2262f44e 100644 --- a/upath/types/__init__.py +++ b/upath/types/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations import enum -import os import pathlib import sys from collections.abc import Iterator @@ -13,7 +12,6 @@ from typing import Literal from typing import Protocol from typing import TextIO -from typing import Union from typing import overload from typing import runtime_checkable @@ -25,6 +23,8 @@ from upath.types._abc import vfsopen if TYPE_CHECKING: + from os import PathLike # noqa: F401 + if sys.version_info > (3, 11): from typing import Self else: @@ -43,6 +43,7 @@ "JoinablePathLike", "ReadablePathLike", "WritablePathLike", + "SupportsPathLike", "CompatJoinablePath", "CompatReadablePath", "CompatWritablePath", @@ -54,9 +55,18 @@ "UNSET_DEFAULT", ] -JoinablePathLike: TypeAlias = Union[str, JoinablePath] -ReadablePathLike: TypeAlias = Union[str, ReadablePath] -WritablePathLike: TypeAlias = Union[str, WritablePath] + +class VFSPathLike(Protocol): + def __vfspath__(self) -> str: ... + + +SupportsPathLike: TypeAlias = "VFSPathLike | PathLike[str]" +JoinablePathLike: TypeAlias = "JoinablePath | SupportsPathLike | str" +ReadablePathLike: TypeAlias = "ReadablePath | SupportsPathLike | str" +WritablePathLike: TypeAlias = "WritablePath | SupportsPathLike | str" +CompatJoinablePathLike: TypeAlias = "CompatJoinablePath | SupportsPathLike | str" +CompatReadablePathLike: TypeAlias = "CompatReadablePath | SupportsPathLike | str" +CompatWritablePathLike: TypeAlias = "CompatWritablePath | SupportsPathLike | str" class _DefaultValue(enum.Enum): @@ -139,16 +149,16 @@ def suffixes(self) -> Sequence[str]: ... @property def stem(self) -> str: ... - def with_name(self, name) -> Self: ... - def with_stem(self, stem) -> Self: ... - def with_suffix(self, suffix) -> Self: ... + def with_name(self, name: str) -> Self: ... + def with_stem(self, stem: str) -> Self: ... + def with_suffix(self, suffix: str) -> Self: ... @property def parts(self) -> Sequence[str]: ... - def joinpath(self, *pathsegments: str | Self) -> Self: ... - def __truediv__(self, key: str | Self) -> Self: ... - def __rtruediv__(self, key: str | Self) -> Self: ... + def joinpath(self, *pathsegments: str) -> Self: ... + def __truediv__(self, key: str) -> Self: ... + def __rtruediv__(self, key: str) -> Self: ... @property def parent(self) -> Self: ... @@ -190,7 +200,7 @@ class CompatWritablePath(CompatJoinablePath, Protocol): __slots__ = () def symlink_to( - self, target: WritablePath, target_is_directory: bool = ... + self, target: str | Self, target_is_directory: bool = ... ) -> None: ... def mkdir(self) -> None: ... @@ -280,20 +290,20 @@ def st_birthtime_ns(self) -> int: ... class UPathParser(PathParser, Protocol): """duck-type for upath.core.UPathParser""" - def strip_protocol(self, path: JoinablePath | str) -> str: ... + def split(self, path: JoinablePathLike) -> tuple[str, str]: ... + def splitext(self, path: JoinablePathLike) -> tuple[str, str]: ... + def normcase(self, path: JoinablePathLike) -> str: ... + + def strip_protocol(self, path: JoinablePathLike) -> str: ... def join( self, - path: JoinablePath | os.PathLike[str] | str, - *paths: JoinablePath | os.PathLike[str] | str, + path: JoinablePathLike, + *paths: JoinablePathLike, ) -> str: ... - def isabs(self, path: JoinablePath | os.PathLike[str] | str) -> bool: ... + def isabs(self, path: JoinablePathLike) -> bool: ... - def splitdrive( - self, path: JoinablePath | os.PathLike[str] | str - ) -> tuple[str, str]: ... + def splitdrive(self, path: JoinablePathLike) -> tuple[str, str]: ... - def splitroot( - self, path: JoinablePath | os.PathLike[str] | str - ) -> tuple[str, str, str]: ... + def splitroot(self, path: JoinablePathLike) -> tuple[str, str, str]: ... From e785bf811c1962c9f70be64bb31e98d271901fa9 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 13:39:23 +0200 Subject: [PATCH 04/10] upath.types._abc: stricter types for pathlib_abc base types --- upath/types/_abc.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/upath/types/_abc.pyi b/upath/types/_abc.pyi index 6964b2cc..ea0810e7 100644 --- a/upath/types/_abc.pyi +++ b/upath/types/_abc.pyi @@ -26,7 +26,7 @@ class JoinablePath(ABC): @abstractmethod def parser(self) -> PathParser: ... @abstractmethod - def with_segments(self, *pathsegments: str | Self) -> Self: ... + def with_segments(self, *pathsegments: str) -> Self: ... @abstractmethod def __vfspath__(self) -> str: ... @property @@ -45,8 +45,8 @@ class JoinablePath(ABC): @property def parts(self) -> Sequence[str]: ... def joinpath(self, *pathsegments: str) -> Self: ... - def __truediv__(self, key: str | Self) -> Self: ... - def __rtruediv__(self, key: str | Self) -> Self: ... + def __truediv__(self, key: str) -> Self: ... + def __rtruediv__(self, key: str) -> Self: ... @property def parent(self) -> Self: ... @property From 18300631a1004f80b6a8ba876454403457bf16c7 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 13:43:02 +0200 Subject: [PATCH 05/10] upath.core: fix type annotations --- upath/_flavour.py | 30 +++++++++++------------- upath/_protocol.py | 9 +++---- upath/core.py | 58 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/upath/_flavour.py b/upath/_flavour.py index ed107c60..cbd921f2 100644 --- a/upath/_flavour.py +++ b/upath/_flavour.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING from typing import Any from typing import TypedDict -from typing import Union from urllib.parse import SplitResult from urllib.parse import urlsplit @@ -22,7 +21,7 @@ from upath._flavour_sources import flavour_registry from upath._protocol import get_upath_protocol from upath._protocol import normalize_empty_netloc -from upath.types import JoinablePath +from upath.types import JoinablePathLike from upath.types import UPathParser if TYPE_CHECKING: @@ -42,7 +41,6 @@ ] class_registry: Mapping[str, type[AbstractFileSystem]] = _class_registry -PathOrStr: TypeAlias = Union[str, os.PathLike[str], JoinablePath] class AnyProtocolFileSystemFlavour(FileSystemFlavourBase): @@ -237,7 +235,7 @@ def local_file(self) -> bool: return bool(getattr(self._spec, "local_file", False)) @staticmethod - def stringify_path(pth: PathOrStr) -> str: + def stringify_path(pth: JoinablePathLike) -> str: if isinstance(pth, str): out = pth elif isinstance(pth, upath.UPath) and not pth.is_absolute(): @@ -253,18 +251,18 @@ def stringify_path(pth: PathOrStr) -> str: out = str(pth) return normalize_empty_netloc(out) - def strip_protocol(self, pth: PathOrStr) -> str: + def strip_protocol(self, pth: JoinablePathLike) -> str: pth = self.stringify_path(pth) return self._spec._strip_protocol(pth) - def get_kwargs_from_url(self, url: PathOrStr) -> dict[str, Any]: + def get_kwargs_from_url(self, url: JoinablePathLike) -> dict[str, Any]: # NOTE: the public variant is _from_url not _from_urls if hasattr(url, "storage_options"): return dict(url.storage_options) url = self.stringify_path(url) return self._spec._get_kwargs_from_urls(url) - def parent(self, path: PathOrStr) -> str: + def parent(self, path: JoinablePathLike) -> str: path = self.stringify_path(path) return self._spec._parent(path) @@ -278,14 +276,14 @@ def sep(self) -> str: # type: ignore[override] def altsep(self) -> str | None: # type: ignore[override] return None - def isabs(self, path: PathOrStr) -> bool: + def isabs(self, path: JoinablePathLike) -> bool: path = self.strip_protocol(path) if self.local_file: return os.path.isabs(path) else: return path.startswith(self.root_marker) - def join(self, path: PathOrStr, *paths: PathOrStr) -> str: + def join(self, path: JoinablePathLike, *paths: JoinablePathLike) -> str: if not paths: return self.strip_protocol(path) or self.root_marker if self.local_file: @@ -309,7 +307,7 @@ def join(self, path: PathOrStr, *paths: PathOrStr) -> str: else: return drv + posixpath.join(p0, *pN) - def split(self, path: PathOrStr): + def split(self, path: JoinablePathLike) -> tuple[str, str]: stripped_path = self.strip_protocol(path) if self.local_file: return os.path.split(stripped_path) @@ -328,7 +326,7 @@ def split(self, path: PathOrStr): return self.split(head) return head, tail - def splitdrive(self, path: PathOrStr) -> tuple[str, str]: + def splitdrive(self, path: JoinablePathLike) -> tuple[str, str]: path = self.strip_protocol(path) if self.netloc_is_anchor: u = urlsplit(path) @@ -353,13 +351,13 @@ def splitdrive(self, path: PathOrStr) -> tuple[str, str]: # all other cases don't have a drive return "", path - def normcase(self, path: PathOrStr) -> str: + def normcase(self, path: JoinablePathLike) -> str: if self.local_file: return os.path.normcase(self.stringify_path(path)) else: return self.stringify_path(path) - def splitext(self, path: PathOrStr) -> tuple[str, str]: + def splitext(self, path: JoinablePathLike) -> tuple[str, str]: path = self.stringify_path(path) if self.local_file: return os.path.splitext(path) @@ -375,7 +373,7 @@ def splitext(self, path: PathOrStr) -> tuple[str, str]: # === Python3.12 pathlib flavour ================================== - def splitroot(self, path: PathOrStr) -> tuple[str, str, str]: + def splitroot(self, path: JoinablePathLike) -> tuple[str, str, str]: drive, tail = self.splitdrive(path) if self.netloc_is_anchor: root_marker = self.root_marker or self.sep @@ -422,13 +420,13 @@ def __repr__(self): return f"<{cls_name} of {self._owner.__name__}>" -def upath_strip_protocol(pth: PathOrStr) -> str: +def upath_strip_protocol(pth: JoinablePathLike) -> str: if protocol := get_upath_protocol(pth): return WrappedFileSystemFlavour.from_protocol(protocol).strip_protocol(pth) return WrappedFileSystemFlavour.stringify_path(pth) -def upath_get_kwargs_from_url(url: PathOrStr) -> dict[str, Any]: +def upath_get_kwargs_from_url(url: JoinablePathLike) -> dict[str, Any]: if protocol := get_upath_protocol(url): return WrappedFileSystemFlavour.from_protocol(protocol).get_kwargs_from_url(url) return {} diff --git a/upath/_protocol.py b/upath/_protocol.py index 51588c1d..fd92024b 100644 --- a/upath/_protocol.py +++ b/upath/_protocol.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import re from collections import ChainMap from pathlib import PurePath @@ -11,7 +10,7 @@ from fsspec.registry import registry as _registry if TYPE_CHECKING: - from upath.types import JoinablePath + from upath.types import JoinablePathLike __all__ = [ "get_upath_protocol", @@ -60,7 +59,7 @@ def _fsspec_protocol_equals(p0: str, p1: str) -> bool: def get_upath_protocol( - pth: str | os.PathLike[str] | PurePath | JoinablePath, + pth: JoinablePathLike, *, protocol: str | None = None, storage_options: dict[str, Any] | None = None, @@ -74,6 +73,8 @@ def get_upath_protocol( pth_protocol = pth.protocol elif isinstance(pth, PurePath): pth_protocol = getattr(pth, "protocol", "") + elif hasattr(pth, "__vfspath__"): + pth_protocol = _match_protocol(pth.__vfspath__()) elif hasattr(pth, "__fspath__"): pth_protocol = _match_protocol(pth.__fspath__()) else: @@ -102,7 +103,7 @@ def normalize_empty_netloc(pth: str) -> str: def compatible_protocol( protocol: str, - *args: str | os.PathLike[str] | PurePath | JoinablePath, + *args: JoinablePathLike, ) -> bool: """check if UPath protocols are compatible""" from upath.core import UPath diff --git a/upath/core.py b/upath/core.py index 1c86665c..42bf4293 100644 --- a/upath/core.py +++ b/upath/core.py @@ -17,6 +17,7 @@ from typing import Literal from typing import NoReturn from typing import TextIO +from typing import TypeVar from typing import overload from urllib.parse import SplitResult from urllib.parse import urlsplit @@ -41,7 +42,9 @@ from upath.types import OpenablePath from upath.types import PathInfo from upath.types import ReadablePathLike +from upath.types import SupportsPathLike from upath.types import UPathParser +from upath.types import WritablePath from upath.types import WritablePathLike if TYPE_CHECKING: @@ -53,10 +56,10 @@ from pydantic import GetCoreSchemaHandler from pydantic_core.core_schema import CoreSchema + _WT = TypeVar("_WT", bound="WritablePath") __all__ = ["UPath"] - _FSSPEC_HAS_WORKING_GLOB = None @@ -525,7 +528,7 @@ def parts(self) -> Sequence[str]: parts = [*names, drive + sep] return tuple(reversed(parts)) - def with_name(self, name) -> Self: + def with_name(self, name: str) -> Self: """Return a new path with the file name changed.""" split = self.parser.split if self.parser.sep in name: # `split(name)[0]` @@ -571,6 +574,21 @@ def parents(self) -> Sequence[Self]: return parents return super().parents + def joinpath(self, *pathsegments: JoinablePathLike) -> Self: + return self.with_segments(self.__vfspath__(), *pathsegments) + + def __truediv__(self, key: JoinablePathLike) -> Self: + try: + return self.with_segments(self.__vfspath__(), key) + except TypeError: + return NotImplemented + + def __rtruediv__(self, key: JoinablePathLike) -> Self: + try: + return self.with_segments(key, self.__vfspath__()) + except TypeError: + return NotImplemented + # === ReadablePath attributes ===================================== @property @@ -599,6 +617,32 @@ def __open_reader__(self) -> BinaryIO: def readlink(self) -> Self: _raise_unsupported(type(self).__name__, "readlink") + @overload + def copy(self, target: _WT, **kwargs: Any) -> _WT: ... + + @overload + def copy(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... + + def copy(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPath: + if not isinstance(target, UPath): + return super().copy(self.with_segments(target), **kwargs) + else: + return super().copy(target, **kwargs) + + @overload + def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... + + @overload + def copy_into(self, target_dir: SupportsPathLike | str, **kwargs: Any) -> Self: ... + + def copy_into( + self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any + ) -> _WT | UPath: + if not isinstance(target_dir, UPath): + return super().copy_into(self.with_segments(target_dir), **kwargs) + else: + return super().copy_into(target_dir, **kwargs) + # --- WritablePath attributes ------------------------------------- def symlink_to( @@ -714,7 +758,7 @@ def open( def stat( self, *, - follow_symlinks=True, + follow_symlinks: bool = True, ) -> UPathStatResult: if not follow_symlinks: warnings.warn( @@ -731,7 +775,7 @@ def lstat(self) -> UPathStatResult: def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: _raise_unsupported(type(self).__name__, "chmod") - def exists(self, *, follow_symlinks=True) -> bool: + def exists(self, *, follow_symlinks: bool = True) -> bool: return self.fs.exists(self.path) def is_dir(self) -> bool: @@ -779,7 +823,7 @@ def glob( *, case_sensitive: bool = UNSET_DEFAULT, recurse_symlinks: bool = UNSET_DEFAULT, - ) -> Iterator[UPath]: + ) -> Iterator[Self]: if case_sensitive is not UNSET_DEFAULT: warnings.warn( "UPath.glob(): case_sensitive is currently ignored.", @@ -807,7 +851,7 @@ def rglob( *, case_sensitive: bool = UNSET_DEFAULT, recurse_symlinks: bool = UNSET_DEFAULT, - ) -> Iterator[UPath]: + ) -> Iterator[Self]: if case_sensitive is not UNSET_DEFAULT: warnings.warn( "UPath.glob(): case_sensitive is currently ignored.", @@ -940,7 +984,7 @@ def resolve(self, strict: bool = False) -> Self: return self.with_segments(*_parts[:1], *resolved) - def touch(self, mode=0o666, exist_ok=True) -> None: + def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: exists = self.fs.exists(self.path) if exists and not exist_ok: raise FileExistsError(str(self)) From e4691754e73433e1a7e500d71e17703be582d315 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 14:01:15 +0200 Subject: [PATCH 06/10] upath.implementations: fix type annotations --- upath/implementations/cloud.py | 4 ++-- upath/implementations/data.py | 31 ++++++++++++++++++++++++------- upath/implementations/github.py | 9 ++++++++- upath/implementations/hdfs.py | 14 ++++++++++++-- upath/implementations/memory.py | 14 +++++++++++--- upath/implementations/smb.py | 10 ++++++++-- 6 files changed, 65 insertions(+), 17 deletions(-) diff --git a/upath/implementations/cloud.py b/upath/implementations/cloud.py index 3575e1fa..27bef5de 100644 --- a/upath/implementations/cloud.py +++ b/upath/implementations/cloud.py @@ -50,7 +50,7 @@ def root(self) -> str: return "" return self.parser.sep - def __vfspath__(self): + def __vfspath__(self) -> str: path = super().__vfspath__() if self._relative_base is None: drive = self.parser.splitdrive(path)[0] @@ -93,7 +93,7 @@ def mkdir( if "unexpected keyword argument 'create_parents'" in str(err): self.fs.mkdir(self.path) - def exists(self, *, follow_symlinks=True): + def exists(self, *, follow_symlinks: bool = True) -> bool: # required for gcsfs<2025.5.0, see: https://github.com/fsspec/gcsfs/pull/676 path = self.path if len(path) > 1: diff --git a/upath/implementations/data.py b/upath/implementations/data.py index 3bc62f74..4197f2de 100644 --- a/upath/implementations/data.py +++ b/upath/implementations/data.py @@ -1,28 +1,45 @@ from __future__ import annotations +import sys +from collections.abc import Sequence + from upath.core import UPath +from upath.types import JoinablePathLike + +if sys.version_info > (3, 11): + from typing import Self +else: + from typing_extensions import Self class DataPath(UPath): @property - def parts(self): + def parts(self) -> Sequence[str]: return (self.path,) - def __str__(self): + def __str__(self) -> str: return self.parser.join(*self._raw_urlpaths) - def with_segments(self, *pathsegments): + def with_segments(self, *pathsegments: JoinablePathLike) -> Self: raise NotImplementedError("path operation not supported by DataPath") - def with_suffix(self, suffix: str): + def with_suffix(self, suffix: str) -> Self: raise NotImplementedError("path operation not supported by DataPath") - def mkdir(self, mode=0o777, parents=False, exist_ok=False): + def mkdir( + self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False + ) -> None: raise FileExistsError(str(self)) - def write_bytes(self, data): + def write_bytes(self, data: bytes) -> int: raise NotImplementedError("DataPath does not support writing") - def write_text(self, data, **kwargs): + def write_text( + self, + data: str, + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> int: raise NotImplementedError("DataPath does not support writing") diff --git a/upath/implementations/github.py b/upath/implementations/github.py index 1d37724d..5fd400d8 100644 --- a/upath/implementations/github.py +++ b/upath/implementations/github.py @@ -2,10 +2,17 @@ GitHub file system implementation """ +import sys +from collections.abc import Iterator from collections.abc import Sequence import upath.core +if sys.version_info > (3, 11): + from typing import Self +else: + from typing_extensions import Self + class GitHubPath(upath.core.UPath): """ @@ -19,7 +26,7 @@ def path(self) -> str: return "" return pth - def iterdir(self): + def iterdir(self) -> Iterator[Self]: if self.is_file(): raise NotADirectoryError(str(self)) yield from super().iterdir() diff --git a/upath/implementations/hdfs.py b/upath/implementations/hdfs.py index dce18a33..0cabb42c 100644 --- a/upath/implementations/hdfs.py +++ b/upath/implementations/hdfs.py @@ -1,19 +1,29 @@ from __future__ import annotations +import sys +from collections.abc import Iterator + from upath.core import UPath +if sys.version_info > (3, 11): + from typing import Self +else: + from typing_extensions import Self + __all__ = ["HDFSPath"] class HDFSPath(UPath): __slots__ = () - def mkdir(self, mode=0o777, parents=False, exist_ok=False): + def mkdir( + self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False + ) -> None: if not exist_ok and self.exists(): raise FileExistsError(str(self)) super().mkdir(mode=mode, parents=parents, exist_ok=exist_ok) - def iterdir(self): + def iterdir(self) -> Iterator[Self]: if self.is_file(): raise NotADirectoryError(str(self)) yield from super().iterdir() diff --git a/upath/implementations/memory.py b/upath/implementations/memory.py index 7fb16006..350e11c8 100644 --- a/upath/implementations/memory.py +++ b/upath/implementations/memory.py @@ -1,22 +1,30 @@ from __future__ import annotations +import sys +from collections.abc import Iterator + from upath.core import UPath +if sys.version_info > (3, 11): + from typing import Self +else: + from typing_extensions import Self + __all__ = ["MemoryPath"] class MemoryPath(UPath): - def iterdir(self): + def iterdir(self) -> Iterator[Self]: if not self.is_dir(): raise NotADirectoryError(str(self)) yield from super().iterdir() @property - def path(self): + def path(self) -> str: path = super().path return "/" if path == "." else path - def __str__(self): + def __str__(self) -> str: s = super().__str__() if s.startswith("memory:///"): s = s.replace("memory:///", "memory://", 1) diff --git a/upath/implementations/smb.py b/upath/implementations/smb.py index 055ca2e6..b5bcd376 100644 --- a/upath/implementations/smb.py +++ b/upath/implementations/smb.py @@ -2,6 +2,7 @@ import sys import warnings +from collections.abc import Iterator from typing import TYPE_CHECKING from typing import Any @@ -21,7 +22,12 @@ class SMBPath(UPath): __slots__ = () - def mkdir(self, mode=0o777, parents=False, exist_ok=False): + def mkdir( + self, + mode: int = 0o777, + parents: bool = False, + exist_ok: bool = False, + ) -> None: # smbclient does not support setting mode externally if parents and not exist_ok and self.exists(): raise FileExistsError(str(self)) @@ -36,7 +42,7 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False): if not self.is_dir(): raise FileExistsError(str(self)) - def iterdir(self): + def iterdir(self) -> Iterator[Self]: if not self.is_dir(): raise NotADirectoryError(str(self)) else: From a1f501e8029f295a595067060583d07ee77bf1c7 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 14:01:43 +0200 Subject: [PATCH 07/10] upath.implementations.local: fix annotations and backport methods --- upath/implementations/local.py | 196 +++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index d11a2656..4abcf93e 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -8,6 +8,9 @@ from collections.abc import Sequence from typing import TYPE_CHECKING from typing import Any +from typing import Callable +from typing import Literal +from typing import overload from urllib.parse import SplitResult from fsspec import AbstractFileSystem @@ -19,14 +22,27 @@ from upath._protocol import compatible_protocol from upath.core import UPath from upath.core import _UPathMixin +from upath.types import UNSET_DEFAULT from upath.types import JoinablePathLike +from upath.types import PathInfo +from upath.types import ReadablePath +from upath.types import ReadablePathLike +from upath.types import SupportsPathLike +from upath.types import WritablePath if TYPE_CHECKING: + from typing import IO + from typing import BinaryIO + from typing import TextIO + from typing import TypeVar + if sys.version_info >= (3, 11): from typing import Self else: from typing_extensions import Self + _WT = TypeVar("_WT", bound="WritablePath") + __all__ = [ "LocalPath", "PosixUPath", @@ -66,6 +82,27 @@ def _warn_protocol_storage_options( ) +class _LocalPathInfo(PathInfo): + """Backported PathInfo implementation for LocalPath. + todo: currently not handling symlinks correctly. + """ + + def __init__(self, path: LocalPath) -> None: + self._path = path.path + + def exists(self, *, follow_symlinks: bool = True) -> bool: + return os.path.exists(self._path) + + def is_dir(self, *, follow_symlinks: bool = True) -> bool: + return os.path.isdir(self._path) + + def is_file(self, *, follow_symlinks: bool = True) -> bool: + return os.path.isfile(self._path) + + def is_symlink(self) -> bool: + return os.path.islink(self._path) + + class LocalPath(_UPathMixin, pathlib.Path): __slots__ = ( "_chain", @@ -147,6 +184,22 @@ def _init(self, **kwargs: Any) -> None: super()._init(**kwargs) # type: ignore[misc] self._chain = Chain(ChainSegment(str(self), "", {})) + def __vfspath__(self) -> str: + return self.__fspath__() + + def __open_reader__(self) -> BinaryIO: + return self.open("rb") + + def __open_writer__(self, mode: Literal["a", "w", "x"]) -> BinaryIO: + if mode == "w": + return self.open(mode="wb") + elif mode == "a": + return self.open(mode="ab") + elif mode == "x": + return self.open(mode="xb") + else: + raise ValueError(f"invalid mode: {mode}") + def with_segments(self, *pathsegments: str | os.PathLike[str]) -> Self: return type(self)( *pathsegments, @@ -190,6 +243,149 @@ def __rtruediv__(self, other) -> Self: else other ) + @overload # type: ignore[override] + def open( + self, + mode: Literal["r", "w", "a"] = "r", + buffering: int = ..., + encoding: str = ..., + errors: str = ..., + newline: str = ..., + **fsspec_kwargs: Any, + ) -> TextIO: ... + + @overload + def open( + self, + mode: Literal["rb", "wb", "ab", "xb"], + buffering: int = ..., + encoding: str = ..., + errors: str = ..., + newline: str = ..., + **fsspec_kwargs: Any, + ) -> BinaryIO: ... + + @overload + def open( + self, + mode: str, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + **fsspec_kwargs: Any, + ) -> IO[Any]: ... + + def open( + self, + mode: str = "r", + buffering: int = UNSET_DEFAULT, + encoding: str | None = UNSET_DEFAULT, + errors: str | None = UNSET_DEFAULT, + newline: str | None = UNSET_DEFAULT, + **fsspec_kwargs: Any, + ) -> IO[Any]: + if not fsspec_kwargs: + kwargs: dict[str, str | int | None] = {} + if buffering is not UNSET_DEFAULT: + kwargs["buffering"] = buffering + if encoding is not UNSET_DEFAULT: + kwargs["encoding"] = encoding + if errors is not UNSET_DEFAULT: + kwargs["errors"] = errors + if newline is not UNSET_DEFAULT: + kwargs["newline"] = newline + return super().open(mode, **kwargs) # type: ignore # noqa: E501 + return UPath.open.__get__(self)( + mode, + buffering=buffering, + encoding=encoding, + errors=errors, + newline=newline, + **fsspec_kwargs, + ) + + if sys.version_info < (3, 14): + + @overload + def copy(self, target: _WT, **kwargs: Any) -> _WT: ... + + @overload + def copy(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... + + def copy( + self, target: _WT | SupportsPathLike | str, **kwargs: Any + ) -> _WT | Self: + # hacky workaround for missing pathlib.Path.copy in python < 3.14 + # todo: revisit + _copy: Any = ReadablePath.copy.__get__(self) + if not isinstance(target, UPath): + return _copy(self.with_segments(str(target)), **kwargs) + else: + return _copy(target, **kwargs) + + @overload + def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... + + @overload + def copy_into( + self, target_dir: SupportsPathLike | str, **kwargs: Any + ) -> Self: ... + + def copy_into( + self, + target_dir: _WT | SupportsPathLike | str, + **kwargs: Any, + ) -> _WT | Self: + # hacky workaround for missing pathlib.Path.copy_into in python < 3.14 + # todo: revisit + _copy_into: Any = ReadablePath.copy_into.__get__(self) + if not isinstance(target_dir, UPath): + return _copy_into(self.with_segments(str(target_dir)), **kwargs) + else: + return _copy_into(target_dir, **kwargs) + + @property + def info(self) -> PathInfo: + return _LocalPathInfo(self) + + if sys.version_info < (3, 13): + + def full_match(self, pattern: str) -> bool: + # hacky workaround for missing pathlib.Path.full_match in python < 3.13 + # todo: revisit + return self.match(pattern) + + if sys.version_info < (3, 12): + + def is_junction(self) -> bool: + return False + + def walk( + self, + top_down: bool = True, + on_error: Callable[[Exception], Any] | None = None, + follow_symlinks: bool = False, + ) -> Iterator[tuple[Self, list[str], list[str]]]: + _walk = ReadablePath.walk.__get__(self) + return _walk(top_down, on_error, follow_symlinks) + + if sys.version_info < (3, 10): + + def hardlink_to(self, target: ReadablePathLike) -> None: + try: + os.link(target, self) + except AttributeError: + raise NotImplementedError + + if not hasattr(pathlib.Path, "_copy_from"): + + def _copy_from( + self, source: ReadablePath | LocalPath, follow_symlinks: bool = True + ) -> None: + _copy_from: Any = WritablePath._copy_from.__get__(self) + _copy_from(source, follow_symlinks=follow_symlinks) + UPath.register(LocalPath) From b1d5211a0de1b9bebab50dd3090a6d93630867a1 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 13:44:57 +0200 Subject: [PATCH 08/10] upath.extensions: fix method signatures and types --- upath/extensions.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/upath/extensions.py b/upath/extensions.py index d1f55dee..715ba610 100644 --- a/upath/extensions.py +++ b/upath/extensions.py @@ -96,12 +96,15 @@ def __init__( def parser(self) -> UPathParser: return self.__wrapped__.parser - def with_segments(self) -> Self: - return self._from_upath(self.__wrapped__.with_segments()) + def with_segments(self, *pathsegments: JoinablePathLike) -> Self: + return self._from_upath(self.__wrapped__.with_segments(*pathsegments)) def __str__(self) -> str: return self.__wrapped__.__str__() + def __vfspath__(self) -> str: + return self.__wrapped__.__vfspath__() + def __repr__(self) -> str: return ( f"{type(self).__name__}" @@ -425,10 +428,10 @@ def walk( ): yield self._from_upath(pth), dirnames, filenames - def copy(self, target: UPath, **kwargs: Any) -> Self: + def copy(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.copy(target, **kwargs)) - def copy_into(self, target_dir: UPath, **kwargs: Any) -> Self: + def copy_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.copy_into(target_dir, **kwargs)) def write_bytes(self, data: bytes) -> int: @@ -445,8 +448,10 @@ def write_text( data, encoding=encoding, errors=errors, newline=newline ) - def _copy_from(self, source: ReadablePath, follow_symlinks: bool = True) -> None: - self.__wrapped__._copy_from(source, follow_symlinks=follow_symlinks) + def _copy_from( + self, source: ReadablePath | Self, follow_symlinks: bool = True + ) -> None: + self.__wrapped__._copy_from(source, follow_symlinks=follow_symlinks) # type: ignore # noqa: E501 @property def anchor(self) -> str: @@ -461,7 +466,7 @@ def suffix(self) -> str: return self.__wrapped__.suffix @property - def suffixes(self) -> Sequence[str]: + def suffixes(self) -> list[str]: return self.__wrapped__.suffixes @property @@ -474,7 +479,7 @@ def with_stem(self, stem: str) -> Self: def with_suffix(self, suffix: str) -> Self: return self._from_upath(self.__wrapped__.with_suffix(suffix)) - def joinpath(self, *pathsegments: str) -> Self: + def joinpath(self, *pathsegments: JoinablePathLike) -> Self: return self._from_upath(self.__wrapped__.joinpath(*pathsegments)) def __truediv__(self, other: str | Self) -> Self: From 2eac4e815d05420bc184cddd4df10fd121235292 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 16:51:47 +0200 Subject: [PATCH 09/10] typesafety: adjust JoinablePath tests --- typesafety/test_upath_types.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/typesafety/test_upath_types.yml b/typesafety/test_upath_types.yml index efbb13c2..4f105761 100644 --- a/typesafety/test_upath_types.yml +++ b/typesafety/test_upath_types.yml @@ -1,15 +1,21 @@ -- case: upath_types_joinablepath + +- case: upath_types_joinablepath_upath disable_cache: false main: | - from pathlib import PurePath - from upath.types import CompatJoinablePath + from upath import UPath + from upath.types import JoinablePath - a: CompatJoinablePath = PurePath() + a: JoinablePath = UPath() -- case: upath_types_joinablepath_upath +- case: upath_types_joinablepathlike disable_cache: false main: | + from pathlib import PurePath + from pathlib import Path from upath import UPath - from upath.types import CompatJoinablePath + from upath.types import JoinablePathLike - a: CompatJoinablePath = UPath() + a: JoinablePathLike = "abc" + b: JoinablePathLike = PurePath() + c: JoinablePathLike = Path() + d: JoinablePathLike = UPath() From 31a41293e660ea5665aab65f645e970da3eb25a6 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 17:01:57 +0200 Subject: [PATCH 10/10] upath.types: do not register pathlib_abc ABCs on python 3.14 --- upath/types/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/upath/types/__init__.py b/upath/types/__init__.py index 01e8fe0f..f919697d 100644 --- a/upath/types/__init__.py +++ b/upath/types/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations import enum -import pathlib import sys from typing import TYPE_CHECKING from typing import Any @@ -54,11 +53,11 @@ class _DefaultValue(enum.Enum): UNSET_DEFAULT: Any = _DefaultValue.UNSET - -if sys.version_info >= (3, 14): - JoinablePath.register(pathlib.PurePath) - ReadablePath.register(pathlib.Path) - WritablePath.register(pathlib.Path) +# We can't assume this, because pathlib_abc==0.5.1 is ahead of stdlib 3.14 +# if sys.version_info >= (3, 14): +# JoinablePath.register(pathlib.PurePath) +# ReadablePath.register(pathlib.Path) +# WritablePath.register(pathlib.Path) class StatResultType(Protocol):