Skip to content

Commit c622f2a

Browse files
authored
Merge pull request #53 from ArcanaFramework/sub-package-generic-classifies
handle classified formats are split across parent/child packages
2 parents 1bc9b95 + e3dd3dd commit c622f2a

File tree

5 files changed

+122
-38
lines changed

5 files changed

+122
-38
lines changed

fileformats/core/classifier.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .utils import classproperty
2+
from .exceptions import FileFormatsError
23

34

45
class Classifier:
@@ -9,3 +10,15 @@ class Classifier:
910
def type_name(cls):
1011
"""Name of type to be used in __repr__. Defined here so it can be overridden"""
1112
return cls.__name__
13+
14+
@classproperty
15+
def namespace(cls):
16+
"""The "namespace" the format belongs to under the "fileformats" umbrella
17+
namespace"""
18+
module_parts = cls.__module__.split(".")
19+
if module_parts[0] != "fileformats":
20+
raise FileFormatsError(
21+
f"Cannot create reversible MIME type for {cls} as it is not in the "
22+
"fileformats namespace"
23+
)
24+
return module_parts[1].replace("_", "-")

fileformats/core/datatype.py

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import itertools
66
from .converter import SubtypeVar
77
from .exceptions import (
8-
FileFormatsError,
98
FormatMismatchError,
109
FormatConversionError,
1110
FormatRecognitionError,
1211
)
1312
from .utils import (
1413
classproperty,
1514
subpackages,
15+
add_exc_note,
1616
to_mime_format_name,
1717
from_mime_format_name,
1818
IANA_MIME_TYPE_REGISTRIES,
@@ -51,18 +51,6 @@ def matches(cls, values) -> bool:
5151
else:
5252
return True
5353

54-
@classproperty
55-
def namespace(cls):
56-
"""The "namespace" the format belongs to under the "fileformats" umbrella
57-
namespace"""
58-
module_parts = cls.__module__.split(".")
59-
if module_parts[0] != "fileformats":
60-
raise FileFormatsError(
61-
f"Cannot create reversible MIME type for {cls} as it is not in the "
62-
"fileformats namespace"
63-
)
64-
return module_parts[1].replace("_", "-")
65-
6654
@classproperty
6755
def all_types(self):
6856
return itertools.chain(FileSet.all_formats, Field.all_fields)
@@ -195,34 +183,52 @@ def from_mime(cls, mime_string):
195183
klass = getattr(module, class_name)
196184
except AttributeError:
197185
if "+" in format_name:
198-
qualifier_names, classified_name = format_name.split("+")
199-
try:
200-
classifiers = [
201-
getattr(module, from_mime_format_name(q))
202-
for q in qualifier_names.split(".")
203-
]
204-
except AttributeError:
205-
raise FormatRecognitionError(
206-
f"Could not load classifiers [{qualifier_names}] from "
207-
f"fileformats.{namespace}, corresponding to MIME, "
208-
f"or MIME-like, type {mime_string}"
209-
) from None
210-
try:
211-
classified = getattr(
212-
module, from_mime_format_name(classified_name)
186+
if "_" in namespace:
187+
parent_namespace = namespace.split("_")[0]
188+
parent_module = importlib.import_module(
189+
"fileformats." + parent_namespace
213190
)
214-
except AttributeError:
191+
else:
192+
parent_namespace = parent_module = None
193+
194+
def get_format(mime_name):
195+
name = from_mime_format_name(mime_name)
196+
try:
197+
return getattr(module, name)
198+
except AttributeError:
199+
if parent_module:
200+
try:
201+
return getattr(parent_module, name)
202+
except AttributeError:
203+
pass
204+
err_msg_part = f" or fileformats.{parent_namespace}"
205+
else:
206+
err_msg_part = ""
207+
raise FormatRecognitionError(
208+
f"Could not load format class {name} (from "
209+
f"'{mime_name}') fileformats.{namespace}"
210+
f"{err_msg_part} corresponding "
211+
f"to MIME, or MIME-like, type {mime_string}"
212+
) from None
213+
214+
classifiers_str, classified_name = format_name.split("+")
215+
classifiers = [get_format(c) for c in classifiers_str.split(".")]
216+
try:
217+
classified = get_format(classified_name)
218+
except FormatRecognitionError as e:
215219
try:
216220
classified = cls.generically_classifies_by_name[
217221
classified_name
218222
]
219223
except KeyError:
220-
raise FormatRecognitionError(
221-
f"Could not load classified class '{classified_name}' from "
222-
f"fileformats.{namespace} or list of generic types "
223-
f"({list(cls.generically_classifies_by_name)}), "
224-
f"corresponding to MIME, or MIME-like, type {mime_string}"
225-
) from None
224+
add_exc_note(
225+
e,
226+
(
227+
"neither list of generic types "
228+
f"({list(cls.generically_classifies_by_name)})"
229+
),
230+
)
231+
raise e
226232
klass = classified[classifiers]
227233
else:
228234
raise FormatRecognitionError(

fileformats/core/mixin.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,11 +627,18 @@ def namespace(cls): # pylint: disable=no-self-argument
627627
if len(namespaces) == 1:
628628
return next(iter(namespaces))
629629
else:
630+
# Handle subpackage namespaces and parent, e.g. medimage & medimage-fsl
631+
namespaces = sorted(namespaces)
632+
if (
633+
len(namespaces) == 2
634+
and namespaces[1].split("-")[0] == namespaces[0]
635+
):
636+
return namespaces[1]
630637
msg = (
631638
"Cannot create reversible MIME type for because did not find a "
632639
f"common namespace between all classifiers {list(cls.classifiers)}"
633640
)
634-
if cls.generically_classifies:
641+
if not cls.generically_classifies:
635642
msg += (
636643
f" and (non genericly classified) base class {cls.unclassified}"
637644
)

fileformats/core/tests/test_mime.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from fileformats.generic import FileSet
22
from fileformats.core.utils import from_mime
3+
from fileformats.testing import Classified, U, V
4+
from fileformats.testing_subpackage import Psi, SubpackageClassified, Zeta, Theta
35

46

57
def test_mime_roundtrip():
6-
78
for klass in FileSet.all_formats:
89
mimetype = klass.mime_type
910
assert isinstance(mimetype, str)
@@ -12,9 +13,44 @@ def test_mime_roundtrip():
1213

1314

1415
def test_mimelike_roundtrip():
15-
1616
for klass in FileSet.all_formats:
1717
mimetype = klass.mime_like
1818
assert isinstance(mimetype, str)
1919
reloaded = from_mime(mimetype)
2020
assert reloaded is klass
21+
22+
23+
def test_subpackage_to_mime_roundtrip():
24+
assert Psi.mime_like == "testing-subpackage/psi"
25+
assert from_mime("testing-subpackage/psi") is Psi
26+
27+
28+
def test_subpackage_to_mime_classified_rountrip():
29+
assert (
30+
SubpackageClassified[Zeta, Theta].mime_like
31+
== "testing-subpackage/theta.zeta+subpackage-classified"
32+
)
33+
assert (
34+
from_mime("testing-subpackage/theta.zeta+subpackage-classified")
35+
is SubpackageClassified[Zeta, Theta]
36+
)
37+
38+
39+
def test_subpackage_to_mime_parent_classified_rountrip():
40+
assert (
41+
Classified[Zeta, Theta].mime_like == "testing-subpackage/theta.zeta+classified"
42+
)
43+
assert (
44+
from_mime("testing-subpackage/theta.zeta+classified") is Classified[Zeta, Theta]
45+
)
46+
47+
48+
def test_subpackage_to_mime_parent_classifiers_rountrip():
49+
assert (
50+
SubpackageClassified[U, V].mime_like
51+
== "testing-subpackage/u.v+subpackage-classified"
52+
)
53+
assert (
54+
from_mime("testing-subpackage/u.v+subpackage-classified")
55+
is SubpackageClassified[U, V]
56+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from fileformats.core import Classifier, DataType
2+
from fileformats.core.mixin import WithClassifiers
3+
4+
5+
class SubpackageClassified(WithClassifiers, DataType):
6+
classifiers_attr_name = "classifiers"
7+
classifiers = ()
8+
multiple_classifiers = True
9+
allowed_classifiers = (Classifier,)
10+
generically_classifies = False
11+
12+
13+
class Psi(DataType):
14+
pass
15+
16+
17+
class Zeta(Classifier):
18+
pass
19+
20+
21+
class Theta(Classifier):
22+
pass

0 commit comments

Comments
 (0)