Skip to content

Commit b3e6bdf

Browse files
ap--normanrz
andauthored
Fix python311 compatibility (#69)
* Add Python 3.11 testing * upath.core: fix python3.11 compatibility mkdir was the one method broken by moving away from _accessors in Python3.11 * upath.core: explictly disable more methods broken by Python3.11 * tests: add tests for mkdir exist_ok * tests: cloud filesystems require a file for dirs * tests: mark unsupported http tests as skipped * upath.implementations.hdfs: fix mkdir to correctly handle exist_ok * upath.core: catch FileExistsError in mkdir and fix mode kwarg * upath.implementations.cloud: fix exist_ok=False for mkdir * ci: use newest setup-python and checkout actions * black formatting * nox: add workaround for missing py311 aiohttp wheels * noxfile: lint only the upath package * tests: better name for empty dir support flag Co-authored-by: Norman Rzepka <[email protected]>
1 parent 484f7e7 commit b3e6bdf

File tree

11 files changed

+144
-35
lines changed

11 files changed

+144
-35
lines changed

.github/workflows/python.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ jobs:
77
runs-on: ${{ matrix.os }}
88
strategy:
99
matrix:
10-
python-version: [3.7, 3.8, 3.9, "3.10"]
10+
python-version: [3.7, 3.8, 3.9, "3.10", "3.11-dev"]
1111
os: [ubuntu-latest, windows-latest]
1212

1313
steps:
14-
- uses: actions/checkout@v2
14+
- uses: actions/checkout@v3
1515
- name: Set up Python ${{ matrix.python-version }}
16-
uses: actions/setup-python@v1
16+
uses: actions/setup-python@v4
1717
with:
1818
python-version: ${{ matrix.python-version }}
1919
- name: Install dependencies

noxfile.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import sys
2+
13
import nox
24
from pathlib import Path
35

@@ -17,7 +19,7 @@ def black(session):
1719
@nox.session()
1820
def lint(session):
1921
session.install("flake8")
20-
session.run(*"flake8".split())
22+
session.run("flake8", "upath")
2123

2224

2325
@nox.session()
@@ -27,6 +29,12 @@ def install(session):
2729

2830
@nox.session()
2931
def smoke(session):
32+
if (3, 10) < sys.version_info <= (3, 11, 0, "final"):
33+
# workaround for missing aiohttp wheels for py3.11
34+
session.install("aiohttp", "--no-binary", "aiohttp", env={
35+
"AIOHTTP_NO_EXTENSIONS": "1"
36+
})
37+
3038
session.install(
3139
"pytest",
3240
"adlfs",

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
description="pathlib api extended to use fsspec backends",
1717
long_description=long_description,
1818
long_description_content_type="text/markdown",
19-
license="MIT"
19+
license="MIT",
2020
)

upath/core.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, parsed_url: ParseResult, **kwargs):
2929
def _format_path(self, path: "UPath") -> str:
3030
return path.path
3131

32-
def open(self, path, mode='r', *args, **kwargs):
32+
def open(self, path, mode="r", *args, **kwargs):
3333
return self._fs.open(self._format_path(path), mode, *args, **kwargs)
3434

3535
def stat(self, path, **kwargs):
@@ -185,6 +185,9 @@ def parent(self):
185185
def stat(self):
186186
return self._accessor.stat(self)
187187

188+
def samefile(self, other_path):
189+
raise NotImplementedError
190+
188191
def iterdir(self):
189192
"""Iterate over the files in this directory. Does not yield any
190193
result for the special paths '.' and '..'.
@@ -223,18 +226,36 @@ def relative_to(self, *other):
223226
output._kwargs = self._kwargs
224227
return output
225228

229+
def _scandir(self):
230+
# provided in Python3.11 but not required in fsspec glob implementation
231+
raise NotImplementedError
232+
226233
def glob(self, pattern):
227234
path_pattern = self.joinpath(pattern)
228235
for name in self._accessor.glob(self, path_pattern):
229236
name = self._sub_path(name)
230237
name = name.split(self._flavour.sep)
231238
yield self._make_child(name)
232239

240+
def rglob(self, pattern):
241+
path_pattern = self.joinpath("**", pattern)
242+
for name in self._accessor.glob(self, path_pattern):
243+
name = self._sub_path(name)
244+
name = name.split(self._flavour.sep)
245+
yield self._make_child(name)
246+
233247
def _sub_path(self, name):
234248
# only want the path name with iterdir
235249
sp = self.path
236250
return re.sub(f"^({sp}|{sp[1:]})/", "", name)
237251

252+
def absolute(self):
253+
# fsspec paths are always absolute
254+
return self
255+
256+
def resolve(self, strict=False):
257+
raise NotImplementedError
258+
238259
def exists(self):
239260
"""
240261
Whether this path exists.
@@ -290,6 +311,9 @@ def is_block_device(self):
290311
def is_char_device(self):
291312
return False
292313

314+
def is_absolute(self):
315+
return True
316+
293317
def unlink(self, missing_ok=False):
294318
if not self.exists():
295319
if not missing_ok:
@@ -308,13 +332,25 @@ def rmdir(self, recursive=True):
308332
raise NotDirectoryError
309333
self._accessor.rm(self, recursive=recursive)
310334

311-
def chmod(self, mod):
335+
def chmod(self, mode, *, follow_symlinks=True):
312336
raise NotImplementedError
313337

314338
def rename(self, target):
315339
# can be implemented, but may be tricky
316340
raise NotImplementedError
317341

342+
def replace(self, target):
343+
raise NotImplementedError
344+
345+
def symlink_to(self, target, target_is_directory=False):
346+
raise NotImplementedError
347+
348+
def hardlink_to(self, target):
349+
raise NotImplementedError
350+
351+
def link_to(self, target):
352+
raise NotImplementedError
353+
318354
def cwd(self):
319355
raise NotImplementedError
320356

@@ -342,6 +378,28 @@ def readlink(self):
342378
def touch(self, truncate=True, **kwargs):
343379
self._accessor.touch(self, truncate=truncate, **kwargs)
344380

381+
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
382+
"""
383+
Create a new directory at this given path.
384+
"""
385+
if parents:
386+
self._accessor.mkdir(
387+
self,
388+
create_parents=True,
389+
exist_ok=exist_ok,
390+
mode=mode,
391+
)
392+
else:
393+
try:
394+
self._accessor.mkdir(
395+
self,
396+
create_parents=False,
397+
mode=mode,
398+
)
399+
except FileExistsError:
400+
if not exist_ok or not self.is_dir():
401+
raise
402+
345403
@classmethod
346404
def _from_parts(cls, args, url=None, **kwargs):
347405
obj = object.__new__(cls)
@@ -426,7 +484,7 @@ def with_suffix(self, suffix):
426484
f = self._flavour
427485
if f.sep in suffix or f.altsep and f.altsep in suffix:
428486
raise ValueError("Invalid suffix %r" % (suffix,))
429-
if suffix and not suffix.startswith('.') or suffix == '.':
487+
if suffix and not suffix.startswith(".") or suffix == ".":
430488
raise ValueError("Invalid suffix %r" % (suffix))
431489
name = self.name
432490
if not name:
@@ -435,17 +493,24 @@ def with_suffix(self, suffix):
435493
if not old_suffix:
436494
name = name + suffix
437495
else:
438-
name = name[:-len(old_suffix)] + suffix
439-
return self._from_parsed_parts(self._drv, self._root,
440-
self._parts[:-1] + [name], url=self._url)
496+
name = name[: -len(old_suffix)] + suffix
497+
return self._from_parsed_parts(
498+
self._drv, self._root, self._parts[:-1] + [name], url=self._url
499+
)
441500

442501
def with_name(self, name):
443502
"""Return a new path with the file name changed."""
444503
if not self.name:
445504
raise ValueError("%r has an empty name" % (self,))
446505
drv, root, parts = self._flavour.parse_parts((name,))
447-
if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep]
448-
or drv or root or len(parts) != 1):
506+
if (
507+
not name
508+
or name[-1] in [self._flavour.sep, self._flavour.altsep]
509+
or drv
510+
or root
511+
or len(parts) != 1
512+
):
449513
raise ValueError("Invalid name %r" % (name))
450-
return self._from_parsed_parts(self._drv, self._root,
451-
self._parts[:-1] + [name], url=self._url)
514+
return self._from_parsed_parts(
515+
self._drv, self._root, self._parts[:-1] + [name], url=self._url
516+
)

upath/implementations/cloud.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ def _format_path(self, path):
99
"""
1010
return f"{path._url.netloc}/{path.path.lstrip('/')}"
1111

12+
def mkdir(self, path, create_parents=True, **kwargs):
13+
if (
14+
not create_parents
15+
and not kwargs.get("exist_ok", False)
16+
and self._fs.exists(self._format_path(path))
17+
):
18+
raise FileExistsError
19+
return super().mkdir(path, create_parents=create_parents, **kwargs)
20+
1221

1322
# project is not part of the path, but is part of the credentials
1423
class CloudPath(upath.core.UPath):

upath/implementations/hdfs.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ def touch(self, path, **kwargs):
1010
kwargs.pop("truncate", None)
1111
super().touch(path, **kwargs)
1212

13+
def mkdir(self, path, create_parents=True, **kwargs):
14+
pth = self._format_path(path)
15+
if create_parents:
16+
return self._fs.makedirs(pth, **kwargs)
17+
else:
18+
if not kwargs.get("exist_ok", False) and self._fs.exists(pth):
19+
raise FileExistsError
20+
return self._fs.mkdir(pth, create_parents=create_parents, **kwargs)
21+
1322

1423
class HDFSPath(upath.core.UPath):
1524
_default_accessor = _HDFSAccessor

upath/tests/cases.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010

1111
class BaseTests:
12+
SUPPORTS_EMPTY_DIRS = True
13+
1214
def test_cwd(self):
1315
with pytest.raises(NotImplementedError):
1416
self.path.cwd()
@@ -124,8 +126,25 @@ def test_lstat(self):
124126
def test_mkdir(self):
125127
new_dir = self.path.joinpath("new_dir")
126128
new_dir.mkdir()
129+
if not self.SUPPORTS_EMPTY_DIRS:
130+
new_dir.joinpath(".file").touch()
127131
assert new_dir.exists()
128132

133+
def test_mkdir_exists_ok_true(self):
134+
new_dir = self.path.joinpath("new_dir_may_exists")
135+
new_dir.mkdir()
136+
if not self.SUPPORTS_EMPTY_DIRS:
137+
new_dir.joinpath(".file").touch()
138+
new_dir.mkdir(exist_ok=True)
139+
140+
def test_mkdir_exists_ok_false(self):
141+
new_dir = self.path.joinpath("new_dir_may_not_exists")
142+
new_dir.mkdir()
143+
if not self.SUPPORTS_EMPTY_DIRS:
144+
new_dir.joinpath(".file").touch()
145+
with pytest.raises(FileExistsError):
146+
new_dir.mkdir(exist_ok=False)
147+
129148
def test_open(self):
130149
pass
131150

upath/tests/implementations/test_azure.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
@skip_on_windows
1111
@pytest.mark.usefixtures("path")
1212
class TestAzurePath(BaseTests):
13+
SUPPORTS_EMPTY_DIRS = False
14+
1315
@pytest.fixture(autouse=True, scope="function")
1416
def path(self, azurite_credentials, azure_fixture):
1517
account_name, connection_string = azurite_credentials
@@ -24,14 +26,8 @@ def path(self, azurite_credentials, azure_fixture):
2426
def test_is_AzurePath(self):
2527
assert isinstance(self.path, AzurePath)
2628

27-
def test_mkdir(self):
28-
new_dir = self.path / "new_dir"
29-
new_dir.mkdir()
30-
(new_dir / "test.txt").touch()
31-
assert new_dir.exists()
32-
3329
def test_rmdir(self):
34-
new_dir = self.path / "new_dir"
30+
new_dir = self.path / "new_dir_rmdir"
3531
new_dir.mkdir()
3632
path = new_dir / "test.txt"
3733
path.write_text("hello")

upath/tests/implementations/test_gcs.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
@skip_on_windows
1111
@pytest.mark.usefixtures("path")
1212
class TestGCSPath(BaseTests):
13+
SUPPORTS_EMPTY_DIRS = False
14+
1315
@pytest.fixture(autouse=True, scope="function")
1416
def path(self, gcs_fixture):
1517
path, endpoint_url = gcs_fixture
@@ -19,11 +21,6 @@ def path(self, gcs_fixture):
1921
def test_is_GCSPath(self):
2022
assert isinstance(self.path, GCSPath)
2123

22-
def test_mkdir(self):
23-
new_dir = self.path.joinpath("new_dir")
24-
new_dir.joinpath("test.txt").touch()
25-
assert new_dir.exists()
26-
2724
def test_rmdir(self):
2825
dirname = "rmdir_test"
2926
mock_dir = self.path.joinpath(dirname)

upath/tests/implementations/test_http.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,27 @@ def path(self, http_fixture):
3434
def test_work_at_root(self):
3535
assert "folder" in (f.name for f in self.path.parent.iterdir())
3636

37+
@pytest.mark.skip
3738
def test_mkdir(self):
3839
pass
3940

41+
@pytest.mark.skip
42+
def test_mkdir_exists_ok_false(self):
43+
pass
44+
45+
@pytest.mark.skip
46+
def test_mkdir_exists_ok_true(self):
47+
pass
48+
49+
@pytest.mark.skip
4050
def test_touch_unlink(self):
4151
pass
4252

53+
@pytest.mark.skip
4354
def test_write_bytes(self, pathlib_base):
4455
pass
4556

57+
@pytest.mark.skip
4658
def test_write_text(self, pathlib_base):
4759
pass
4860

0 commit comments

Comments
 (0)