Skip to content

Commit d1916d1

Browse files
authored
Merge pull request #89 from ArcanaFramework/touch-ups
Handle Self in extras signature matching
2 parents 1428be7 + 735f24f commit d1916d1

File tree

9 files changed

+108
-9
lines changed

9 files changed

+108
-9
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
if __name__.startswith("fileformats."):
2+
from . import converters # noqa: F401
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import tempfile
2+
from pathlib import Path
3+
import typing as ty
4+
import pydra.mark
5+
from fileformats.core import converter, FileSet
6+
from fileformats.generic import DirectoryOf, SetOf, TypedDirectory, TypedSet
7+
8+
T = FileSet.type_var("T")
9+
10+
11+
@converter(target_format=SetOf[T], source_format=DirectoryOf[T]) # type: ignore[misc]
12+
@pydra.mark.task # type: ignore[misc]
13+
@pydra.mark.annotate({"return": {"out_file": TypedSet}}) # type: ignore[misc]
14+
def list_dir_contents(in_file: TypedDirectory) -> TypedSet:
15+
classified_set: ty.Type[TypedSet] = SetOf.__class_getitem__(*in_file.content_types) # type: ignore[assignment, arg-type]
16+
return classified_set(in_file.contents)
17+
18+
19+
@converter(target_format=DirectoryOf[T], source_format=SetOf[T]) # type: ignore[misc]
20+
@pydra.mark.annotate({"return": {"out_file": TypedDirectory}}) # type: ignore[misc]
21+
@pydra.mark.task # type: ignore[misc]
22+
def put_contents_in_dir(
23+
in_file: TypedSet,
24+
out_dir: ty.Optional[Path] = None,
25+
copy_mode: FileSet.CopyMode = FileSet.CopyMode.copy,
26+
) -> TypedDirectory:
27+
if out_dir is None:
28+
out_dir = Path(tempfile.mkdtemp())
29+
for fset in in_file.contents:
30+
fset.copy(out_dir, mode=copy_mode)
31+
classified_dir: ty.Type[TypedDirectory] = DirectoryOf.__class_getitem__( # type: ignore[assignment]
32+
*in_file.content_types # type: ignore[arg-type]
33+
)
34+
return classified_dir(out_dir)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
from pathlib import Path
3+
import typing as ty
4+
from fileformats.generic import DirectoryOf, SetOf
5+
from fileformats.text import TextFile
6+
7+
8+
@pytest.fixture
9+
def file_names() -> ty.List[str]:
10+
return ["file1.txt", "file2.txt", "file3.txt"]
11+
12+
13+
@pytest.fixture
14+
def files_dir(file_names: ty.List[str], tmp_path: Path) -> Path:
15+
out_dir = tmp_path / "out"
16+
out_dir.mkdir()
17+
for fname in file_names:
18+
(out_dir / fname).write_text(fname)
19+
return out_dir
20+
21+
22+
@pytest.fixture
23+
def test_files(files_dir: Path, file_names: ty.List[str]) -> ty.List[Path]:
24+
return [files_dir / n for n in file_names]
25+
26+
27+
def test_list_dir_contents(files_dir: Path, test_files: ty.List[Path]) -> None:
28+
text_set = SetOf[TextFile].convert(DirectoryOf[TextFile](files_dir)) # type: ignore[misc]
29+
assert sorted(t.fspath for t in text_set.contents) == test_files
30+
31+
32+
def test_put_contents_in_dir(
33+
file_names: ty.List[str], test_files: ty.List[Path]
34+
) -> None:
35+
text_dir = DirectoryOf[TextFile].convert(SetOf[TextFile](test_files)) # type: ignore[misc]
36+
assert sorted(t.name for t in text_dir.contents) == file_names

fileformats/application/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@
515515
Zlib,
516516
Zstd,
517517
)
518-
from ..text import Javascript
518+
from fileformats.text import Javascript
519519

520520
__all__ = [
521521
"__version__",

fileformats/application/serialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from fileformats.core import DataType, FileSet, extra_implementation
66
from fileformats.core.mixin import WithClassifier
7-
from ..generic import UnicodeFile
7+
from fileformats.generic import UnicodeFile
88
from fileformats.core.exceptions import FormatMismatchError
99
from fileformats.core import SampleFileGenerator
1010

fileformats/core/collection.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,6 @@ def unconstrained(cls) -> bool:
6767
"""Whether the file-format is unconstrained by extension, magic number or another
6868
constraint"""
6969
return super().unconstrained and not cls.content_types
70+
71+
def __len__(self) -> int:
72+
return len(self.contents)

fileformats/core/datatype.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ def get_format(mime_name: str) -> ty.Type[DataType]:
253253
f"Did not find '{class_name}' class in fileformats.{namespace} "
254254
f"corresponding to MIME, or MIME-like, type {mime_string}"
255255
) from None
256+
if not issubclass(klass, cls):
257+
raise FormatRecognitionError(
258+
f"Class '{klass}' does not inherit from '{cls}'"
259+
)
256260
return klass
257261

258262
@classproperty

fileformats/core/extras.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import importlib
23
import typing as ty
34
import inspect
@@ -11,6 +12,11 @@
1112
from .exceptions import FormatConversionError, FileFormatsExtrasError
1213
from .utils import import_extras_module, check_package_exists_on_pypi, add_exc_note
1314

15+
if sys.version_info < (3, 11):
16+
from typing_extensions import Self
17+
else:
18+
from typing import Self
19+
1420
if ty.TYPE_CHECKING:
1521
from pydra.engine.core import TaskBase
1622

@@ -74,15 +80,28 @@ def decorator(implementation: ExtraImplementation) -> ExtraImplementation:
7480
fsig = inspect.signature(implementation)
7581
msig_args = list(msig.parameters.values())[1:]
7682
fsig_args = list(fsig.parameters.values())[1:]
83+
dispatched_type = list(fsig.parameters.values())[0].annotation
7784
differences = []
7885

79-
def type_match(a: ty.Union[str, type], b: ty.Union[str, type]) -> bool:
86+
def type_match(mtype: ty.Union[str, type], ftype: ty.Union[str, type]) -> bool:
87+
"""Check if the types match between the method and function annotations,
88+
allowing for string annotations and `Self`
89+
90+
Parameters
91+
----------
92+
mtype : Union[str, type]
93+
the type of the method argument
94+
ftype : Union[str, type]
95+
the type of the function argument
96+
"""
8097
return (
81-
a is ty.Any # type: ignore[comparison-overlap]
82-
or a == b
83-
or inspect.isclass(a)
84-
and inspect.isclass(b)
85-
and issubclass(b, a)
98+
mtype is ty.Any # type: ignore[comparison-overlap]
99+
or mtype == ftype
100+
or inspect.isclass(ftype)
101+
and (
102+
(inspect.isclass(mtype) and issubclass(ftype, mtype))
103+
or (mtype is Self and issubclass(ftype, dispatched_type)) # type: ignore[comparison-overlap, arg-type]
104+
)
86105
)
87106

88107
mhas_kwargs = msig_args and msig_args[-1].kind == inspect.Parameter.VAR_KEYWORD

fileformats/core/fileset.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1610,7 +1610,8 @@ def move(
16101610
)
16111611
shutil.move(str(fspath), new_path)
16121612
new_paths.append(new_path)
1613-
return type(self)(new_paths)
1613+
self.fspaths = frozenset(new_paths)
1614+
return self
16141615

16151616
def _fspaths_to_copy(
16161617
self,

0 commit comments

Comments
 (0)