Skip to content

Commit bbe0855

Browse files
author
Bruno Grande
authored
Merge pull request #5 from Sage-Bionetworks-Workflows/bgrande/tests-and-suites
Implement `TestABC` and `SuiteABC` as well as subclasses for the testing logic
2 parents 5e2ca71 + c431cb4 commit bbe0855

File tree

21 files changed

+798
-44
lines changed

21 files changed

+798
-44
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ repos:
5353
- repo: https://github.com/pre-commit/mirrors-mypy
5454
rev: 'v0.991'
5555
hooks:
56-
- id: mypy
56+
- id: mypy

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ build-backend = "setuptools.build_meta"
99
version_scheme = "no-guess-dev"
1010

1111
[tool.mypy]
12+
13+
[[tool.mypy.overrides]]
14+
module = "dcqc"
15+
disallow_untyped_calls = true
16+
disallow_untyped_defs = true
17+
disallow_incomplete_defs = true
18+
check_untyped_defs = true
19+
1220
[[tool.mypy.overrides]]
1321
module = "synapseclient.*"
1422
ignore_missing_imports = true
23+
24+
[[tool.mypy.overrides]]
25+
module = "dcqc.suites.suites"
26+
disable_error_code = "assignment"

src/dcqc/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212

1313
from fs.opener import registry
1414

15+
# isort: off
16+
17+
# Import suites to ensure that they are defined and thus discoverable
18+
# It is located here to avoid a circular import
19+
from dcqc.suites import suite_abc # isort: skip
20+
from dcqc.suites import suites # isort: skip
21+
22+
# isort: on
23+
1524
from dcqc.filesystems.openers import SynapseFSOpener
1625

1726
# Set default logging handler to avoid "No handler found" warnings

src/dcqc/enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from enum import Enum
2+
3+
4+
class TestStatus(Enum):
5+
NONE = "pending"
6+
FAIL = "failed"
7+
PASS = "passed"
8+
SKIP = "skipped"

src/dcqc/file.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from pathlib import Path, PurePosixPath
99
from typing import Any, Optional
1010

11-
from dcqc.mixins import SerializableMixin
11+
from fs.base import FS
12+
13+
from dcqc.mixins import SerializableMixin, SerializedObject
1214
from dcqc.utils import open_parent_fs
1315

1416

@@ -29,7 +31,7 @@ def __init__(self, name: str, file_extensions: Collection[str]):
2931
self.register_file_type(self)
3032

3133
@classmethod
32-
def register_file_type(cls, self):
34+
def register_file_type(cls, self: FileType) -> None:
3335
name = self.name.lower()
3436
if name in cls._registry:
3537
message = f"File type ({name}) is already registered ({self._registry})."
@@ -79,10 +81,11 @@ def __init__(
7981
self.metadata = dict(metadata)
8082
self.type = self._pop_file_type()
8183
self.file_name = self._get_file_name()
84+
self._fs: Optional[FS]
8285
self._fs = None
8386

8487
@property
85-
def fs(self):
88+
def fs(self) -> FS:
8689
if self._fs is None:
8790
fname = self.file_name
8891
fs, bname = open_parent_fs(self.url)
@@ -97,7 +100,7 @@ def _pop_file_type(self) -> str:
97100
del self.metadata["file_type"]
98101
return file_type
99102

100-
def _get_file_name(self):
103+
def _get_file_name(self) -> str:
101104
path = PurePosixPath(self.url)
102105
return path.name
103106

@@ -112,7 +115,7 @@ def get_metadata(self, key: str) -> Any:
112115
raise ValueError(message)
113116
return self.metadata[key]
114117

115-
def is_local(self, url: Optional[str] = None):
118+
def is_local(self, url: Optional[str] = None) -> bool:
116119
url = url or self.url
117120
return self.LOCAL_REGEX.fullmatch(url) is not None
118121

@@ -182,7 +185,7 @@ def stage(self, destination: Optional[str] = None, overwrite: bool = False) -> s
182185
self.url = destination
183186
return self.url
184187

185-
def to_dict(self):
188+
def to_dict(self) -> SerializedObject:
186189
return asdict(self)
187190

188191
@classmethod

src/dcqc/filesystems/openers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import os
22

33
from fs.opener import Opener
4+
from fs.opener.parse import ParseResult
45

56
from dcqc.filesystems.synapsefs import SynapseFS
67

78

89
class SynapseFSOpener(Opener):
910
protocols = ["syn"]
1011

11-
def open_fs(self, fs_url, parse_result, writeable, create, cwd):
12+
def open_fs(
13+
self,
14+
fs_url: str,
15+
parse_result: ParseResult,
16+
writeable: bool,
17+
create: bool,
18+
cwd: str,
19+
) -> SynapseFS:
1220
auth_token = os.environ.get("SYNAPSE_AUTH_TOKEN")
1321
root = parse_result.resource
1422
fs = SynapseFS(root, auth_token)

src/dcqc/filesystems/remote_file.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,78 @@
22

33
import io
44
import os
5-
from typing import BinaryIO
5+
from array import array
6+
from collections.abc import Callable, Iterable
7+
from mmap import mmap
8+
from pickle import PickleBuffer
9+
from typing import IO, TYPE_CHECKING, Any, BinaryIO, Optional, Union
10+
11+
from fs.mode import Mode
12+
13+
if TYPE_CHECKING:
14+
from ctypes import _CData
15+
16+
LinesType = Iterable[
17+
Union[
18+
bytes, Union[bytearray, memoryview, array[Any], mmap, _CData, PickleBuffer]
19+
]
20+
]
621

722

823
class RemoteFile(io.IOBase, BinaryIO):
924
"""Proxy for a remote file."""
1025

11-
def __init__(self, f, filename, mode, on_close=None):
26+
def __init__(
27+
self,
28+
f: IO,
29+
filename: str,
30+
mode: Mode,
31+
on_close: Optional[Callable] = None,
32+
):
1233
self._f = f
1334
self.__filename = filename
1435
self.__mode = mode
1536
self._on_close = on_close
1637

17-
def __enter__(self):
38+
def __enter__(self) -> RemoteFile:
1839
return self
1940

20-
def __exit__(self, exc_type, exc_value, traceback):
41+
def __exit__(self, exc_type, exc_value, traceback) -> None: # type: ignore
2142
self.close()
2243

2344
@property
24-
def raw(self):
45+
def raw(self) -> IO:
2546
return self._f
2647

27-
def close(self):
48+
def close(self) -> None:
2849
if self._on_close is not None:
2950
self._on_close(self)
3051

3152
@property
32-
def closed(self):
53+
def closed(self) -> bool:
3354
return self._f.closed
3455

3556
@property
36-
def mode(self):
57+
def mode(self) -> str:
3758
return self._f.mode
3859

39-
def fileno(self):
60+
def fileno(self) -> int:
4061
return self._f.fileno()
4162

42-
def flush(self):
43-
return self._f.flush()
63+
def flush(self) -> None:
64+
self._f.flush()
4465

4566
# def isatty(self):
4667
# return self._f.asatty()
4768

48-
def readable(self):
69+
def readable(self) -> bool:
4970
return self.__mode.reading
5071

51-
def readline(self, limit=-1):
72+
def readline(self, limit: Optional[int] = -1) -> bytes:
73+
limit = limit or -1
5274
return self._f.readline(limit)
5375

54-
def readlines(self, hint=-1):
76+
def readlines(self, hint: int = -1) -> list[bytes]:
5577
if hint == -1:
5678
return self._f.readlines(hint)
5779
else:
@@ -64,42 +86,42 @@ def readlines(self, hint=-1):
6486
break
6587
return lines
6688

67-
def seek(self, offset, whence=os.SEEK_SET):
89+
def seek(self, offset: int, whence: int = os.SEEK_SET) -> int:
6890
if whence not in (os.SEEK_CUR, os.SEEK_END, os.SEEK_SET):
6991
raise ValueError("invalid value for 'whence'")
7092
self._f.seek(offset, whence)
7193
return self._f.tell()
7294

73-
def seekable(self):
95+
def seekable(self) -> bool:
7496
return True
7597

76-
def tell(self):
98+
def tell(self) -> int:
7799
return self._f.tell()
78100

79-
def writable(self):
101+
def writable(self) -> bool:
80102
return self.__mode.writing
81103

82-
def writelines(self, lines):
104+
def writelines(self, lines: LinesType) -> None:
83105
return self._f.writelines(lines)
84106

85-
def read(self, n=-1):
107+
def read(self, n: int = -1) -> bytes:
86108
if not self.__mode.reading:
87109
raise IOError("not open for reading")
88110
return self._f.read(n)
89111

90112
# def readall(self):
91113
# return self._f.readall()
92114

93-
def readinto(self, b):
94-
return self._f.readinto(b)
115+
# def readinto(self, b):
116+
# return self._f.readinto(b)
95117

96-
def write(self, b):
118+
def write(self, b: bytes) -> int:
97119
if not self.__mode.writing:
98120
raise IOError("not open for reading")
99121
self._f.write(b)
100122
return len(b)
101123

102-
def truncate(self, size=None):
124+
def truncate(self, size: Optional[int] = None) -> int:
103125
if size is None:
104126
size = self._f.tell()
105127
self._f.truncate(size)

src/dcqc/filesystems/synapsefs.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from contextlib import contextmanager
99
from pathlib import Path, PurePosixPath
1010
from tempfile import NamedTemporaryFile, TemporaryDirectory, mkdtemp
11-
from typing import TYPE_CHECKING, Any, BinaryIO, Optional, Type
11+
from typing import TYPE_CHECKING, Any, BinaryIO, Generator, Optional, Type
1212

1313
from fs import ResourceType
1414
from fs.base import FS
@@ -40,7 +40,7 @@
4040

4141

4242
@contextmanager
43-
def synapse_errors(path):
43+
def synapse_errors(path: str) -> Generator:
4444
"""A context manager for mapping ``synapseclient`` errors to ``fs`` errors."""
4545
try:
4646
yield
@@ -116,12 +116,13 @@ def synapse(self) -> Synapse:
116116
Synapse: Authenticated Synapse client
117117
"""
118118
if not hasattr(self._local, "synapse"):
119-
# Override cache with temporary directory to avoid unwanted side effects
119+
# Override cache with temporary directory
120120
self.synapse_args["cache_root_dir"] = mkdtemp()
121121
synapse = Synapse(**self.synapse_args)
122122
synapse.login(authToken=self.auth_token)
123123
self._local.synapse = synapse
124-
# Clear the Synapse cache to ensure up-to-date
124+
# Clear the Synapse cache to avoid unwanted side effects. More info here:
125+
# https://github.com/Sage-Bionetworks-Workflows/py-dcqc/pull/3#discussion_r1068443214
125126
self._local.synapse.cache.purge(after_date=0)
126127
return self._local.synapse
127128

@@ -237,7 +238,11 @@ def _path_to_synapse_id(
237238

238239
return current_entity
239240

240-
def _synapse_id_to_entity(self, synapse_id: str, download_file=False) -> Entity:
241+
def _synapse_id_to_entity(
242+
self,
243+
synapse_id: str,
244+
download_file: bool = False,
245+
) -> Entity:
241246
"""Retrieve and validate (meta)data for a Synapse entity
242247
243248
Args:
@@ -261,7 +266,7 @@ def _synapse_id_to_entity(self, synapse_id: str, download_file=False) -> Entity:
261266
raise ResourceInvalid(message)
262267
return entity
263268

264-
def _path_to_entity(self, path: str, download_file=False) -> Entity:
269+
def _path_to_entity(self, path: str, download_file: bool = False) -> Entity:
265270
"""Perform the validation and retrieval steps for a Synapse entity.
266271
267272
Arguments:
@@ -534,7 +539,7 @@ def openbin(
534539
temp_dir = TemporaryDirectory()
535540
temp_path = temp_dir.name
536541

537-
def on_close(remote_file):
542+
def on_close(remote_file: RemoteFile) -> None:
538543
"""Called when the S3 file closes, to upload data."""
539544
# If the file is empty, add a null byte to bypass
540545
# Synapse restriction on empty files
@@ -579,7 +584,7 @@ def on_close(remote_file):
579584

580585
return RemoteFile(target_file, file_name, mode_obj, on_close)
581586

582-
def remove(self, path: str):
587+
def remove(self, path: str) -> None:
583588
"""Remove a file from the filesystem.
584589
585590
Arguments:
@@ -600,7 +605,7 @@ def remove(self, path: str):
600605

601606
self.synapse.delete(entity)
602607

603-
def removedir(self, path: str):
608+
def removedir(self, path: str) -> None:
604609
"""Remove a directory from the filesystem.
605610
606611
Arguments:
@@ -639,7 +644,7 @@ def removedir(self, path: str):
639644

640645
self.synapse.delete(entity)
641646

642-
def setinfo(self, path: str, info: RawInfo):
647+
def setinfo(self, path: str, info: RawInfo) -> None:
643648
"""Set info on a resource.
644649
645650
This method is the complement to `~fs.base.FS.getinfo`

src/dcqc/suites/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)