Skip to content

Commit 7137bac

Browse files
authored
Merge pull request #118 from ArcanaFramework/updates-convertible-from
added write_text method to UnicodeFile
2 parents 05c684f + 10fb103 commit 7137bac

File tree

15 files changed

+180
-79
lines changed

15 files changed

+180
-79
lines changed

docs/source/developer/extras.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ to do a generic conversion between all image types,
134134
from pathlib import Path
135135
import tempfile
136136
import pydra.mark
137-
import pydra.engine.specs
138137
from fileformats.core import converter
139138
from .raster import RasterImage, Bitmap, Gif, Jpeg, Png, Tiff
140139
@@ -144,9 +143,8 @@ to do a generic conversion between all image types,
144143
@converter(target_format=Jpeg, output_format=Jpeg)
145144
@converter(target_format=Png, output_format=Png)
146145
@converter(target_format=Tiff, output_format=Tiff)
147-
@pydra.mark.task
148-
@pydra.mark.annotate({"return": {"out_file": RasterImage}})
149-
def convert_image(in_file: RasterImage, output_format: type, out_dir: ty.Optional[Path] = None):
146+
@python.define(outputs=["out_file"])
147+
def convert_image(in_file: RasterImage, output_format: type, out_dir: Path | None = None) -> RasterImage:
150148
data_array = in_file.load()
151149
if out_dir is None:
152150
out_dir = Path(tempfile.mkdtemp())

extras/fileformats/extras/core/tests/test_converter.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import typing as ty
2-
import attrs
31
from pathlib import Path
2+
3+
import attrs
44
import pytest
55
from pydra.compose import python, shell
6-
from fileformats.generic import File, FileSet
7-
from fileformats.testing import Foo, Bar, Baz, Qux
8-
from fileformats.testing import ConvertibleToFile, ConcreteClass, AnotherConcreteClass
6+
7+
from conftest import write_test_file
98
from fileformats.core import converter
109
from fileformats.core.exceptions import FormatConversionError
11-
from conftest import write_test_file
10+
from fileformats.generic import File, FileSet
11+
from fileformats.testing import (
12+
AnotherConcreteClass,
13+
Bar,
14+
Baz,
15+
ConcreteClass,
16+
ConvertibleToFile,
17+
Foo,
18+
Qux,
19+
)
1220

1321

1422
@converter
@@ -95,13 +103,13 @@ def test_convert_mapped_conversion(work_dir):
95103
@pytest.mark.parametrize(
96104
["klass", "convertible_from"],
97105
[
98-
[Bar, ty.Union[Bar, Baz, Foo]],
99-
[Qux, ty.Union[Foo, Qux]],
106+
[Bar, Bar | Baz | Foo],
107+
[Qux, Foo | Qux],
100108
[Foo, Foo],
101109
[Baz, Baz],
102110
[
103111
ConvertibleToFile,
104-
ty.Union[AnotherConcreteClass, ConcreteClass, ConvertibleToFile],
112+
AnotherConcreteClass | ConcreteClass | ConvertibleToFile,
105113
],
106114
],
107115
)
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
1-
from fileformats.core import converter
2-
from fileformats.testing import AbstractFile, ConvertibleToFile
31
from pydra.compose import python
42

3+
from fileformats.core import converter
4+
from fileformats.testing import AbstractFile, ConvertibleToFile, EncodedText
5+
from fileformats.text import TextFile
6+
57

68
@converter # pyright: ignore[reportArgumentType]
79
@python.define(outputs=["out_file"]) # type: ignore[misc]
810
def ConvertibleToConverter(in_file: AbstractFile) -> ConvertibleToFile:
911
return ConvertibleToFile.sample()
12+
13+
14+
@converter # pyright: ignore[reportArgumentType]
15+
@python.define(outputs=["out_file"]) # type: ignore[misc]
16+
def EncodedFromTextConverter(in_file: TextFile) -> EncodedText:
17+
contents = in_file.read_text()
18+
# Encode by shifting ASCII codes forward by 1
19+
encoded_contents = "".join(chr(ord(c) + 1) for c in contents)
20+
out_file = EncodedText.sample()
21+
out_file.write_text(encoded_contents)
22+
return out_file
23+
24+
25+
@converter # pyright: ignore[reportArgumentType]
26+
@python.define(outputs=["out_file"]) # type: ignore[misc]
27+
def EncodedToTextConverter(in_file: EncodedText) -> TextFile:
28+
contents = in_file.read_text()
29+
# Decode by shifting ASCII codes back by 1
30+
decoded_contents = "".join(chr(ord(c) - 1) for c in contents)
31+
out_file = TextFile.sample()
32+
out_file.write_text(decoded_contents)
33+
return out_file
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
from . import load_save # noqa: F401
1+
from . import load_save
2+
3+
__all__ = ["load_save"]

extras/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ application = [
4444
"pydicom >=2.3",
4545
"medimages4tests",
4646
]
47+
audio = []
4748
image = [
4849
"imageio >=2.24.0",
4950
]
51+
model = []
52+
text = []
53+
video = []
5054
vendor_openxmlformats_officedocument = [
5155
"python-docx >= 1.2.0",
5256
]

fileformats/core/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from ._version import __version__
22
from .classifier import Classifier
3-
from .datatype import DataType
3+
from .datatype import DataType, FieldPrimitive
44
from .mock import MockMixin
5-
from .fileset import FileSet
5+
from .fileset import FileSet, FileSetPrimitive
66
from .field import Field
77
from .identification import (
88
to_mime,
@@ -19,6 +19,8 @@
1919
"Classifier",
2020
"DataType",
2121
"FileSet",
22+
"FieldPrimitive",
23+
"FileSetPrimitive",
2224
"MockMixin",
2325
"Field",
2426
"to_mime",

fileformats/core/datatype.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
from __future__ import annotations
22

3+
import decimal
34
import importlib
45
import itertools
6+
import sys
57
import typing as ty
68
from abc import ABCMeta
79
from inspect import isclass
810

11+
if sys.version_info >= (3, 10):
12+
from typing import TypeAlias
13+
else:
14+
from typing_extensions import TypeAlias
15+
916
from fileformats.core.typing import Self
1017

1118
from .classifier import Classifier
@@ -26,6 +33,19 @@
2633
if ty.TYPE_CHECKING:
2734
from .converter_helpers import Converter
2835

36+
FieldPrimitive: TypeAlias = ty.Union[
37+
str,
38+
int,
39+
float,
40+
bool,
41+
decimal.Decimal,
42+
ty.Sequence[str],
43+
ty.Sequence[int],
44+
ty.Sequence[float],
45+
ty.Sequence[bool],
46+
ty.Sequence[decimal.Decimal],
47+
]
48+
2949

3050
class DataType(Classifier, metaclass=ABCMeta):
3151
"""
@@ -78,9 +98,9 @@ def all_types(self) -> ty.Iterator[ty.Type[DataType]]:
7898
return itertools.chain(FileSet.all_formats, Field.all_fields)
7999

80100
@classmethod
81-
def subclasses(cls) -> ty.Generator[ty.Type[Self], None, None]:
101+
def subclasses(cls, **kwargs: ty.Any) -> ty.Generator[ty.Type[Self], None, None]:
82102
"""Iterate over all installed subclasses"""
83-
for subpkg in subpackages():
103+
for subpkg in subpackages(**kwargs):
84104
for attr_name in dir(subpkg):
85105
attr = getattr(subpkg, attr_name)
86106
if (
@@ -95,7 +115,7 @@ def subclasses(cls) -> ty.Generator[ty.Type[Self], None, None]:
95115
def get_converter(
96116
cls,
97117
source_format: ty.Type[DataType],
98-
) -> "Converter | None":
118+
) -> "ty.Optional[Converter]":
99119
if issubclass(source_format, cls):
100120
return None
101121
else:

fileformats/core/fileset.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import inspect
55
import itertools
66
import logging
7+
import operator
78
import os
89
import shutil
910
import struct
11+
import sys
1012
import tempfile
1113
import typing as ty
1214
from collections import Counter
@@ -16,7 +18,13 @@
1618
from pathlib import Path
1719
from warnings import warn
1820

21+
if sys.version_info >= (3, 10):
22+
from typing import TypeAlias
23+
else:
24+
from typing_extensions import TypeAlias
25+
1926
from fileformats.core.typing import Self
27+
from fileformats.core.utils import _excluded_subpackages
2028

2129
from .classifier import Classifier
2230
from .datatype import DataType
@@ -43,6 +51,19 @@
4351
from .converter_helpers import Converter
4452

4553

54+
class SupportsDunderLT(ty.Protocol):
55+
def __lt__(self, other: ty.Any) -> bool:
56+
...
57+
58+
59+
class SupportsDunderGT(ty.Protocol):
60+
def __gt__(self, other: ty.Any) -> bool:
61+
...
62+
63+
64+
FileSetPrimitive: TypeAlias = ty.Union[os.PathLike[str], ty.Sequence[os.PathLike[str]]]
65+
66+
4667
FILE_CHUNK_LEN_DEFAULT = 8192
4768

4869

@@ -542,7 +563,7 @@ def convert(
542563
def get_converter(
543564
cls,
544565
source_format: ty.Type[DataType],
545-
) -> "Converter | None":
566+
) -> "ty.Optional[Converter]":
546567
"""Get a converter that converts from the source format type
547568
into the format specified by the class
548569
@@ -572,12 +593,9 @@ def get_converter(
572593
return None
573594
# trigger loading of standard converters for target format
574595
converters = cls.get_converters_dict()
575-
try:
576-
unclassified = source_format.unclassified # type: ignore
577-
except AttributeError:
578-
import_extras_module(source_format)
579-
else:
580-
import_extras_module(unclassified)
596+
# import extras modules
597+
source_format._import_extras_module() # type: ignore[attr-defined]
598+
cls._import_extras_module()
581599
try:
582600
converter = converters[source_format]
583601
except KeyError:
@@ -613,6 +631,15 @@ def get_converter(
613631
converters[source_format] = converter
614632
return converter
615633

634+
@classmethod
635+
def _import_extras_module(cls) -> None:
636+
try:
637+
unclassified = cls.unclassified # type: ignore
638+
except AttributeError:
639+
import_extras_module(cls)
640+
else:
641+
import_extras_module(unclassified)
642+
616643
@classmethod
617644
def get_converters_dict(
618645
cls, klass: ty.Optional[ty.Type[DataType]] = None
@@ -633,15 +660,17 @@ def get_converters_dict(
633660
@classmethod
634661
def convertible_from(
635662
cls,
636-
only_namespace_parents: bool = True,
637-
union_sort_key: ty.Callable[[DataType], ty.Any] = attrgetter("__name__"),
663+
union_sort_key: ty.Callable[
664+
[ty.Type[DataType]],
665+
ty.Union[SupportsDunderLT, SupportsDunderGT],
666+
] = attrgetter("__name__"),
638667
) -> ty.Type["DataType"]:
639668
"""Union of types that can be converted to this type, including the current type.
640669
If there are no other types that can be converted to this type, return the current type
641670
642671
Parameters
643672
----------
644-
only_namespace_parents: bool
673+
include_generic: bool
645674
If True, only consider parent classes in the same namespace for conversion.
646675
union_sort_key : callable[[DataType], Any], optional
647676
A function used to sort the union of types. Defaults to sorting by the type name.
@@ -652,13 +681,14 @@ def convertible_from(
652681
The type or union of types that can be converted to this type.
653682
"""
654683

655-
ns = cls.namespace
656684
datatypes: ty.List[ty.Type[DataType]] = [cls]
657-
for fformat in FileSet.subclasses():
658-
if issubclass(cls, fformat) and (
659-
fformat.namespace == ns or not only_namespace_parents
660-
):
661-
datatypes.extend(fformat.get_converters_dict().keys())
685+
cls._import_extras_module()
686+
exclude_subpackages = copy(_excluded_subpackages)
687+
exclude_subpackages.discard(cls.namespace)
688+
for subcls in FileSet.subclasses(exclude=exclude_subpackages):
689+
if issubclass(subcls, cls):
690+
subcls._import_extras_module()
691+
datatypes.extend(subcls.get_converters_dict().keys())
662692
if len(datatypes) == 1:
663693
return cls
664694
concrete_datatypes = set()
@@ -675,8 +705,8 @@ def add_concrete(datatype: ty.Type[DataType]) -> None:
675705
for datatype in datatypes:
676706
add_concrete(datatype)
677707
# TODO: Might want to sort datatypes in a more specific order
678-
return ty.Union.__getitem__( # pyright: ignore[reportAttributeAccessIssue]
679-
tuple(sorted(concrete_datatypes, key=union_sort_key))
708+
return functools.reduce(
709+
operator.or_, sorted(concrete_datatypes, key=union_sort_key)
680710
) # type: ignore[return-value]
681711

682712
@classmethod

fileformats/core/identification.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import functools
12
import inspect
23
import operator
34
import re
@@ -102,7 +103,7 @@ def from_mime(
102103
item_mime = item_mime[1:-1]
103104
return ty.List[from_mime(item_mime)] # type: ignore
104105
if "," in mime_str:
105-
return ty.Union.__getitem__(tuple(from_mime(t) for t in mime_str.split(","))) # type: ignore
106+
return functools.reduce(operator.or_, (from_mime(t) for t in mime_str.split(","))) # type: ignore
106107
return fileformats.core.DataType.from_mime(mime_str)
107108

108109

0 commit comments

Comments
 (0)