Skip to content

Commit 9de4852

Browse files
c0llab0rat0rntninja
authored andcommitted
Fix typing on FSNodeEntry
1 parent b3a7a3d commit 9de4852

File tree

4 files changed

+130
-90
lines changed

4 files changed

+130
-90
lines changed

ipfshttpclient/filescanner.py

Lines changed: 78 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,47 @@
3737
HAVE_FWALK_BYTES = HAVE_FWALK and sys.version_info >= (3, 7)
3838

3939

40+
class FSNodeType(enum.Enum):
41+
FILE = enum.auto()
42+
DIRECTORY = enum.auto()
43+
44+
45+
#XXX: This should be a generic `ty.NamedTuple` subclass, but GH/python/mypy#685 …
46+
class FSNodeEntry(ty.Generic[AnyStr]):
47+
type: FSNodeType
48+
path: AnyStr
49+
relpath: AnyStr
50+
name: AnyStr
51+
parentfd: ty.Optional[int]
52+
53+
def __init__(
54+
self,
55+
type: FSNodeType,
56+
path: AnyStr,
57+
relpath: AnyStr,
58+
name: AnyStr,
59+
parentfd: ty.Optional[int]) -> None:
60+
self.type = type
61+
self.path = path
62+
self.relpath = relpath
63+
self.name = name
64+
self.parentfd = parentfd
65+
66+
def __repr__(self) -> str:
67+
return (
68+
f'FSNodeEntry('
69+
f'type={self.type!r}, '
70+
f'path={self.path!r}, '
71+
f'relpath={self.relpath!r}, '
72+
f'name={self.name!r}, '
73+
f'parentfd={self.parentfd!r}'
74+
f')'
75+
)
76+
77+
def __str__(self) -> str:
78+
return str(self.path)
79+
80+
4081
class Matcher(ty.Generic[AnyStr], metaclass=abc.ABCMeta):
4182
"""Represents a type that can match on file paths and decide whether they
4283
should be included in some file scanning/adding operation"""
@@ -466,26 +507,10 @@ def _recursive_matcher_from_spec(spec: match_spec_t[AnyStr], *,
466507
raise MatcherSpecInvalidError(spec)
467508

468509

469-
if ty.TYPE_CHECKING:
470-
from .filescanner_ty import FSNodeType, FSNodeEntry
471-
else:
472-
class FSNodeType(enum.Enum):
473-
FILE = enum.auto()
474-
DIRECTORY = enum.auto()
475-
476-
FSNodeEntry = ty.NamedTuple("FSNodeEntry", [
477-
("type", FSNodeType),
478-
("path", AnyStr),
479-
("relpath", AnyStr),
480-
("name", AnyStr),
481-
("parentfd", ty.Optional[int])
482-
])
483-
484-
485-
class walk(ty.Generator[FSNodeEntry, ty.Any, None], ty.Generic[AnyStr]):
510+
class walk(ty.Generator[FSNodeEntry[AnyStr], ty.Any, None], ty.Generic[AnyStr]):
486511
__slots__ = ("_generator", "_close_fd")
487512

488-
_generator: ty.Generator[FSNodeEntry, None, None]
513+
_generator: ty.Generator[FSNodeEntry[AnyStr], ty.Any, None]
489514
_close_fd: ty.Optional[int]
490515

491516
def __init__(
@@ -577,7 +602,7 @@ def __init__(
577602
def __iter__(self) -> 'walk[AnyStr]':
578603
return self
579604

580-
def __next__(self) -> FSNodeEntry:
605+
def __next__(self) -> FSNodeEntry[AnyStr]:
581606
return next(self._generator)
582607

583608
def __enter__(self) -> 'walk[AnyStr]':
@@ -586,21 +611,21 @@ def __enter__(self) -> 'walk[AnyStr]':
586611
def __exit__(self, *a: ty.Any) -> None:
587612
self.close()
588613

589-
def send(self, value: ty.Any) -> FSNodeEntry:
614+
def send(self, value: ty.Any) -> FSNodeEntry[AnyStr]:
590615
return self._generator.send(value)
591616

592617
@ty.overload
593618
def throw(self, typ: ty.Type[BaseException], # noqa: E704
594619
val: ty.Union[BaseException, object] = ...,
595-
tb: ty.Optional[types.TracebackType] = ...) -> FSNodeEntry: ...
620+
tb: ty.Optional[types.TracebackType] = ...) -> FSNodeEntry[AnyStr]: ...
596621

597622
@ty.overload
598623
def throw(self, typ: BaseException, val: None = ..., # noqa: E704
599-
tb: ty.Optional[types.TracebackType] = ...) -> FSNodeEntry: ...
624+
tb: ty.Optional[types.TracebackType] = ...) -> FSNodeEntry[AnyStr]: ...
600625

601626
def throw(self, typ: ty.Union[ty.Type[BaseException], BaseException],
602627
val: ty.Union[BaseException, object] = None,
603-
tb: ty.Optional[types.TracebackType] = None) -> FSNodeEntry:
628+
tb: ty.Optional[types.TracebackType] = None) -> FSNodeEntry[AnyStr]:
604629
try:
605630
if isinstance(typ, type):
606631
bt = ty.cast(ty.Type[BaseException], typ) # type: ignore[redundant-cast]
@@ -653,7 +678,11 @@ def _walk_wide(
653678
dot: AnyStr,
654679
directory: ty.Union[AnyStr, int],
655680
follow_symlinks: bool
656-
) -> ty.Iterator[ty.Tuple[AnyStr, ty.List[AnyStr], ty.List[AnyStr], ty.Optional[int]]]:
681+
) -> ty.Generator[
682+
ty.Tuple[AnyStr, ty.List[AnyStr], ty.List[AnyStr], ty.Optional[int]],
683+
ty.Any,
684+
None
685+
]:
657686
"""
658687
Return a four-part tuple just like os.fwalk does, even if we won't use os.fwalk.
659688
@@ -673,7 +702,7 @@ def _walk(
673702
matcher: Matcher[AnyStr],
674703
follow_symlinks: bool,
675704
intermediate_dirs: bool
676-
) -> ty.Generator[FSNodeEntry, ty.Any, None]:
705+
) -> ty.Generator[FSNodeEntry[AnyStr], ty.Any, None]:
677706
separator = self._walk_separator(matcher=matcher, directory_str=directory_str)
678707

679708
# TODO: Because os.fsencode can return a byte array, we need to refactor how we use 'sep'
@@ -692,12 +721,12 @@ def _walk(
692721

693722
# Always report the top-level directory even if nothing therein is matched
694723
reported_directories.add(utils.maybe_fsencode("", sep))
695-
yield FSNodeEntry( # type: ignore[misc] # mypy bug: gh/python/mypy#685
696-
type = FSNodeType.DIRECTORY,
697-
path = prefix[:-len(sep)], # type: ignore[arg-type]
698-
relpath = dot, # type: ignore[arg-type]
699-
name = dot, # type: ignore[arg-type]
700-
parentfd = None
724+
yield FSNodeEntry(
725+
type=FSNodeType.DIRECTORY,
726+
path=prefix[:-len(sep)],
727+
relpath=dot,
728+
name=dot,
729+
parentfd=None
701730
)
702731

703732
walk_iter = self._walk_wide(dot=dot, directory=directory, follow_symlinks=follow_symlinks)
@@ -729,32 +758,32 @@ def _walk(
729758
parent_dirpath = sep.join(parts[0:(end_offset + 1)])
730759
if parent_dirpath not in reported_directories:
731760
reported_directories.add(parent_dirpath)
732-
yield FSNodeEntry( # type: ignore[misc] # mypy bug: gh/python/mypy#685
733-
type = FSNodeType.DIRECTORY,
734-
path = (prefix + parent_dirpath), # type: ignore[arg-type]
735-
relpath = parent_dirpath, # type: ignore[arg-type]
736-
name = parts[end_offset], # type: ignore[arg-type]
737-
parentfd = None
761+
yield FSNodeEntry(
762+
type=FSNodeType.DIRECTORY,
763+
path=(prefix + parent_dirpath),
764+
relpath=parent_dirpath,
765+
name=parts[end_offset],
766+
parentfd=None
738767
)
739768
intermediates_reported = True
740769

741770
# Report the target file or directory
742771
if is_dir:
743772
reported_directories.add(filepath)
744-
yield FSNodeEntry( # type: ignore[misc] # mypy bug: gh/python/mypy#685
745-
type = FSNodeType.DIRECTORY,
746-
path = (prefix + filepath), # type: ignore[arg-type]
747-
relpath = filepath, # type: ignore[arg-type]
748-
name = filename, # type: ignore[arg-type]
749-
parentfd = dirfd
773+
yield FSNodeEntry(
774+
type=FSNodeType.DIRECTORY,
775+
path=(prefix + filepath),
776+
relpath=filepath,
777+
name=filename,
778+
parentfd=dirfd
750779
)
751780
else:
752-
yield FSNodeEntry( # type: ignore[misc] # mypy bug: gh/python/mypy#685
753-
type = FSNodeType.FILE,
754-
path = (prefix + filepath), # type: ignore[arg-type]
755-
relpath = filepath, # type: ignore[arg-type]
756-
name = filename, # type: ignore[arg-type]
757-
parentfd = dirfd
781+
yield FSNodeEntry(
782+
type=FSNodeType.FILE,
783+
path=(prefix + filepath),
784+
relpath=filepath,
785+
name=filename,
786+
parentfd=dirfd
758787
)
759788
finally:
760789
# Make sure the file descriptors bound by `os.fwalk` are freed on error

ipfshttpclient/filescanner_ty.pyi

Lines changed: 0 additions & 15 deletions
This file was deleted.

ipfshttpclient/multipart.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -410,39 +410,39 @@ def __init__(self, directory: ty.Union[ty.AnyStr, utils.PathLike[ty.AnyStr], int
410410
def _body(self) -> gen_bytes_t:
411411
"""Streams the contents of the selected directory as binary chunks."""
412412
try:
413-
for type, path, relpath, name, parentfd in self.scanner:
414-
relpath_unicode = os.fsdecode(relpath).replace(os.path.sep, "/")
413+
for item in self.scanner:
414+
relpath_unicode = os.fsdecode(item.relpath).replace(os.path.sep, "/")
415415
short_path = self.name + (("/" + relpath_unicode) if relpath_unicode != "." else "")
416416

417-
if type is filescanner.FSNodeType.FILE:
417+
if item.type is filescanner.FSNodeType.FILE:
418418
try:
419419
# Only regular files and directories can be uploaded
420-
if parentfd is not None:
421-
stat_data = os.stat(name, dir_fd=parentfd, follow_symlinks=self.follow_symlinks)
420+
if item.parentfd is not None:
421+
stat_data = os.stat(item.name, dir_fd=item.parentfd, follow_symlinks=self.follow_symlinks)
422422
else:
423-
stat_data = os.stat(path, follow_symlinks=self.follow_symlinks)
423+
stat_data = os.stat(item.path, follow_symlinks=self.follow_symlinks)
424424
if not stat.S_ISREG(stat_data.st_mode):
425425
continue
426426

427427
absolute_path: ty.Optional[str] = None
428428
if self.abspath is not None:
429-
absolute_path = os.fsdecode(os.path.join(self.abspath, relpath))
429+
absolute_path = os.fsdecode(os.path.join(self.abspath, item.relpath))
430430

431-
if parentfd is None:
432-
f_path_or_desc: ty.Union[ty.AnyStr, int] = path
431+
if item.parentfd is None:
432+
f_path_or_desc: ty.Union[ty.AnyStr, int] = item.path
433433
else:
434-
f_path_or_desc = os.open(name, os.O_RDONLY | os.O_CLOEXEC, dir_fd=parentfd)
434+
f_path_or_desc = os.open(item.name, os.O_RDONLY | os.O_CLOEXEC, dir_fd=item.parentfd)
435435
# Stream file to client
436436
with open(f_path_or_desc, "rb") as file:
437437
yield from self._gen_file(short_path, absolute_path, file)
438438
except OSError as e:
439439
print(e)
440440
# File might have disappeared between `os.walk()` and `open()`
441441
pass
442-
elif type is filescanner.FSNodeType.DIRECTORY:
442+
elif item.type is filescanner.FSNodeType.DIRECTORY:
443443
# Generate directory as special empty file
444444
yield from self._gen_file(short_path, content_type="application/x-directory")
445-
445+
446446
yield from self._gen_end()
447447
finally:
448448
self.scanner.close()

test/unit/test_filescanner.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,29 @@
88
from datetime import datetime
99
from ipfshttpclient import filescanner
1010

11+
from ipfshttpclient.filescanner import FSNodeEntry
12+
from ipfshttpclient.filescanner import FSNodeType
13+
1114

1215
TEST_FILE_DIR: str = os.path.join(os.path.dirname(__file__), "..", "functional")
1316

1417

18+
def test_fs_node_entry_as_repr() -> None:
19+
entry = FSNodeEntry(type=FSNodeType.FILE, path='b', relpath='c', name='d', parentfd=123)
20+
21+
assert (
22+
repr(entry)
23+
==
24+
"FSNodeEntry(type=<FSNodeType.FILE: 1>, path='b', relpath='c', name='d', parentfd=123)"
25+
)
26+
27+
28+
def test_fs_node_entry_as_str() -> None:
29+
entry = FSNodeEntry(type=FSNodeType.FILE, path='b', relpath='c', name='d', parentfd=123)
30+
31+
assert str(entry) == 'b'
32+
33+
1534
@pytest.mark.parametrize("pattern,expected,kwargs", [
1635
("literal", [r"(?![.])(?s:literal)\Z"], {}),
1736
(b"literal", [br"(?![.])(?s:literal)\Z"], {}),
@@ -191,27 +210,34 @@ def test_walk_instaclose(mocker):
191210
close_spy.assert_called_once()
192211

193212

194-
@pytest.mark.parametrize("path,pattern,kwargs,expected", [
195-
(TEST_FILE_DIR + os.path.sep + "fake_dir_almost_empty" + os.path.sep, None, {}, [
196-
(filescanner.FSNodeType.DIRECTORY, ".", "."),
197-
(filescanner.FSNodeType.FILE, ".gitignore", ".gitignore"),
213+
@pytest.mark.parametrize("path,pattern,expected", [
214+
(TEST_FILE_DIR + os.path.sep + "fake_dir_almost_empty" + os.path.sep, None, [
215+
(FSNodeType.DIRECTORY, ".", "."),
216+
(FSNodeType.FILE, ".gitignore", ".gitignore"),
198217
]),
199-
(TEST_FILE_DIR + os.path.sep + "fake_dir", ["test2", "test3"], {}, [
200-
(filescanner.FSNodeType.DIRECTORY, ".", "."),
201-
(filescanner.FSNodeType.DIRECTORY, "test2", "test2"),
202-
(filescanner.FSNodeType.DIRECTORY, "test3", "test3"),
218+
(TEST_FILE_DIR + os.path.sep + "fake_dir", ["test2", "test3"], [
219+
(FSNodeType.DIRECTORY, ".", "."),
220+
(FSNodeType.DIRECTORY, "test2", "test2"),
221+
(FSNodeType.DIRECTORY, "test3", "test3"),
203222
]),
204223
])
205-
def test_walk(monkeypatch, path: str, pattern: None, kwargs: ty.Dict[str, bool], expected: ty.List[filescanner.FSNodeEntry]):
206-
result = [(e.type, e.relpath, e.name) for e in filescanner.walk(path, pattern, **kwargs)]
207-
assert sorted(result, key=lambda r: r[1]) == expected
224+
def test_walk(
225+
monkeypatch,
226+
path: str,
227+
pattern: ty.Optional[filescanner.match_spec_t[str]],
228+
expected: ty.List[filescanner.FSNodeEntry[str]]
229+
) -> None:
230+
def assert_results() -> None:
231+
result = [(e.type, e.relpath, e.name) for e in filescanner.walk(path, pattern)]
232+
assert sorted(result, key=lambda r: r[1]) == expected
233+
234+
assert_results()
208235

209236
# Check again with plain `os.walk` if the current platform supports `os.fwalk`
210237
if filescanner.HAVE_FWALK:
211238
monkeypatch.setattr(filescanner, "HAVE_FWALK", False)
212-
213-
result = [(e.type, e.relpath, e.name) for e in filescanner.walk(path, pattern, **kwargs)]
214-
assert sorted(result, key=lambda r: r[1]) == expected
239+
240+
assert_results()
215241

216242

217243
def test_supports_fd():

0 commit comments

Comments
 (0)