Skip to content

Commit 73dfa46

Browse files
committed
relaxed restrictions on nested classifiers
1 parent 416d547 commit 73dfa46

File tree

3 files changed

+150
-46
lines changed

3 files changed

+150
-46
lines changed

fileformats/core/mixin.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,18 @@ def my_func(file: MyFormatWithClassifiers[Integer]):
272272
allowed_classifiers : tuple[type,...], optional
273273
the allowable types (+ subclasses) for the content types. If None all types
274274
are allowed
275-
genericly_classified : bool, optional
275+
multiple_classifiers: bool, optional
276+
whether multiple classifiers can be provided (true) or just a single classifier (false)
277+
allow_optional_classifiers: bool, optional
278+
whether Optional classifiers are allowed in the classifier set. Optional classifiers are
279+
classifiers that are wrapped in typing.Optional[], indicating that the classifier
280+
may or may not be present in the format
281+
exclusive_classifiers: tuple[ty.Type[Classifier], ...], optional
282+
a set of classifiers that are exclusive, i.e. only one classifier from each
283+
by default empty.
284+
ordered_classifiers: bool, optional
285+
whether the order of the classifiers matters (true) or not (false). If false,
286+
genericly_classified: bool, optional
276287
whether the class can be classified by classifiers in any namespace (true) or just the
277288
namespace it belongs to (false). If true, then the namespace of the genericly
278289
classified class is omitted from the "mime-like" string. Note that the
@@ -380,7 +391,9 @@ def __class_getitem__(
380391
}
381392
for classifier in classifiers_to_check:
382393
for exc_classifier in repetitions:
383-
if issubclass(classifier, exc_classifier):
394+
if issubclass(
395+
origin_type(classifier), origin_type(exc_classifier)
396+
):
384397
repetitions[exc_classifier].append(classifier)
385398
repeated = [t for t in repetitions.items() if len(t[1]) > 1]
386399
if repeated:
@@ -758,3 +771,11 @@ class WithClassifier(WithClassifiers):
758771
class WithOrderedClassifiers(WithClassifiers):
759772

760773
ordered_classifiers = True
774+
775+
776+
def origin_type(tp: ty.Type) -> ty.Type:
777+
"""Get the origin type of a possibly generic type"""
778+
origin = ty.get_origin(tp)
779+
if origin is None:
780+
return tp
781+
return origin
Lines changed: 96 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import typing as ty
2+
import pytest
13
from fileformats.application import Cdfx___Xml
2-
from fileformats.core.identification import from_mime
4+
from fileformats.core import DataType
5+
from fileformats.core.identification import to_mime, from_mime
36
from fileformats.generic import DirectoryOf, FileSet
4-
from fileformats.testing import Classified, U, V
7+
from fileformats.application import Zip
8+
from fileformats.testing import Classified, A, B, U, V, X, MyFormat
59
from fileformats.vendor.testing.testing import Psi, VendorClassified, Theta, Zeta
610
from fileformats.vendor.openxmlformats_officedocument.application import (
711
Wordprocessingml_Document,
@@ -24,59 +28,108 @@ def test_mimelike_roundtrip() -> None:
2428
assert reloaded is klass
2529

2630

27-
def test_vendor_to_mime_roundtrip() -> None:
28-
assert Psi.mime_like == "testing/vnd.testing.psi"
29-
assert from_mime("testing/vnd.testing.psi") is Psi
31+
@pytest.mark.parametrize(
32+
["klass", "expected_mime"],
33+
[
34+
[Psi, "testing/vnd.testing.psi"],
35+
[
36+
VendorClassified[Zeta, Theta],
37+
"testing/[vnd.testing.theta..vnd.testing.zeta]+vnd.testing.vendor-classified",
38+
],
39+
[
40+
Classified[Zeta, Theta],
41+
"testing/[vnd.testing.theta..vnd.testing.zeta]+classified",
42+
],
43+
[VendorClassified[U, V], "testing/[u..v]+vnd.testing.vendor-classified"],
44+
[Zip[Classified[U]], "testing/u+classified+zip"],
45+
[Zip[Classified[U, X]], "testing/[u..v]+classified+zip"],
46+
[Classified[U, Zip[MyFormat]], "testing/[u..my-format+zip]+classified"],
47+
[Classified[U, MyFormat[A, B]], "testing/[u..[a..b]+my-format]+classified"],
48+
[
49+
Wordprocessingml_Document,
50+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
51+
],
52+
[
53+
DirectoryOf[Wordprocessingml_Document],
54+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document+directory",
55+
],
56+
[Cdfx___Xml, "application/vnd.ms-cdfx+xml"],
57+
[U | V, "testing/u,testing/v"],
58+
],
59+
)
60+
def test_compound_mime_roundtrip(klass: ty.Type[DataType], expected_mime: str) -> None:
61+
assert to_mime(klass, official=False) == expected_mime
62+
assert from_mime(expected_mime) is klass
63+
64+
65+
# def test_vendor_to_mime_roundtrip() -> None:
66+
# assert Psi.mime_like == "testing/vnd.testing.psi"
67+
# assert from_mime("testing/vnd.testing.psi") is Psi
68+
69+
70+
# def test_vendor_to_mime_classified_rountrip() -> None:
71+
# assert (
72+
# VendorClassified[Zeta, Theta].mime_like
73+
# == "testing/[vnd.testing.theta..vnd.testing.zeta]+vnd.testing.vendor-classified"
74+
# )
75+
# assert (
76+
# from_mime(
77+
# "testing/[vnd.testing.theta..vnd.testing.zeta]+vnd.testing.vendor-classified"
78+
# )
79+
# is VendorClassified[Zeta, Theta]
80+
# )
81+
82+
83+
# def test_vendor_to_mime_parent_classified_rountrip() -> None:
84+
# assert (
85+
# Classified[Zeta, Theta].mime_like
86+
# == "testing/[vnd.testing.theta..vnd.testing.zeta]+classified"
87+
# )
88+
# assert (
89+
# from_mime("testing/[vnd.testing.theta..vnd.testing.zeta]+classified")
90+
# is Classified[Zeta, Theta]
91+
# )
92+
93+
94+
# def test_vendor_to_mime_parent_classifiers_rountrip() -> None:
95+
# assert (
96+
# VendorClassified[U, V].mime_like
97+
# == "testing/[u..v]+vnd.testing.vendor-classified"
98+
# )
99+
# assert (
100+
# from_mime("testing/[u..v]+vnd.testing.vendor-classified")
101+
# is VendorClassified[U, V]
102+
# )
30103

31104

32-
def test_vendor_to_mime_classified_rountrip() -> None:
33-
assert (
34-
VendorClassified[Zeta, Theta].mime_like
35-
== "testing/[vnd.testing.theta..vnd.testing.zeta]+vnd.testing.vendor-classified"
36-
)
37-
assert (
38-
from_mime(
39-
"testing/[vnd.testing.theta..vnd.testing.zeta]+vnd.testing.vendor-classified"
40-
)
41-
is VendorClassified[Zeta, Theta]
42-
)
105+
# def test_double_classified_roundtrip1() -> None:
106+
# assert Zip[Classified[U]].mime_like == "testing/u+classified+zip"
107+
# assert from_mime("testing/[[u..v]+classified]+zip") is Zip[Classified[U]]
43108

44109

45-
def test_vendor_to_mime_parent_classified_rountrip() -> None:
46-
assert (
47-
Classified[Zeta, Theta].mime_like
48-
== "testing/[vnd.testing.theta..vnd.testing.zeta]+classified"
49-
)
50-
assert (
51-
from_mime("testing/[vnd.testing.theta..vnd.testing.zeta]+classified")
52-
is Classified[Zeta, Theta]
53-
)
110+
# def test_double_classified_roundtrip2() -> None:
111+
# assert Zip[Classified[U, V]].mime_like == "testing/[u..v]+classified+zip"
112+
# assert from_mime("testing/[[u..v]+classified]+zip") is Zip[Classified[U, V]]
54113

55114

56-
def test_vendor_to_mime_parent_classifiers_rountrip() -> None:
57-
assert (
58-
VendorClassified[U, V].mime_like
59-
== "testing/[u..v]+vnd.testing.vendor-classified"
60-
)
61-
assert (
62-
from_mime("testing/[u..v]+vnd.testing.vendor-classified")
63-
is VendorClassified[U, V]
64-
)
115+
# def test_double_classified_roundtrip2() -> None:
116+
# assert Zip[Classified[U, V]].mime_like == "testing/[u..v]+classified+zip"
117+
# assert from_mime("testing/[[u..v]+classified]+zip") is Zip[Classified[U, V]]
65118

66119

67-
def test_vendor_roundtrip() -> None:
120+
# def test_vendor_roundtrip() -> None:
68121

69-
mime = Wordprocessingml_Document.mime_like
70-
assert Wordprocessingml_Document is from_mime(mime)
122+
# mime = Wordprocessingml_Document.mime_like
123+
# assert Wordprocessingml_Document is from_mime(mime)
71124

72125

73-
def test_vendor_in_container_roundtrip() -> None:
126+
# def test_vendor_in_container_roundtrip() -> None:
74127

75-
mime = DirectoryOf[Wordprocessingml_Document].mime_like
76-
assert DirectoryOf[Wordprocessingml_Document] is from_mime(mime)
128+
# mime = DirectoryOf[Wordprocessingml_Document].mime_like
129+
# assert DirectoryOf[Wordprocessingml_Document] is from_mime(mime)
77130

78131

79-
def test_native_container_roundtrip() -> None:
132+
# def test_native_container_roundtrip() -> None:
80133

81-
mime = Cdfx___Xml.mime_like
82-
assert Cdfx___Xml is from_mime(mime)
134+
# mime = Cdfx___Xml.mime_like
135+
# assert Cdfx___Xml is from_mime(mime)

fileformats/core/utils.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import itertools
44
import logging
55
import os
6+
import sys
7+
import types
68
import pkgutil
79
import typing as ty
810
import urllib.error
@@ -20,6 +22,12 @@
2022
logger = logging.getLogger("fileformats")
2123

2224

25+
if sys.version_info >= (3, 10):
26+
UNION_TYPES = (ty.Union, types.UnionType)
27+
else:
28+
UNION_TYPES = (ty.Union,)
29+
30+
2331
_excluded_subpackages = set(
2432
[
2533
"core",
@@ -256,7 +264,7 @@ def get_optional_type(
256264
bool
257265
whether the type is an Optional type or not
258266
"""
259-
if ty.get_origin(type_) is None:
267+
if not is_union(type_):
260268
return type_ # type: ignore[return-value]
261269
if not allowed:
262270
raise FormatDefinitionError(
@@ -270,3 +278,25 @@ def get_optional_type(
270278
f"not {type_}"
271279
)
272280
return args[0] if args[0] is not None else args[1] # type: ignore[no-any-return]
281+
282+
283+
def is_union(type_: type, args: list[type] = None) -> bool:
284+
"""Checks whether a type is a Union, in either ty.Union[T, U] or T | U form
285+
286+
Parameters
287+
----------
288+
type_ : type
289+
the type to check
290+
args : list[type], optional
291+
required arguments of the union to check, by default (None) any args will match
292+
293+
Returns
294+
-------
295+
is_union : bool
296+
whether the type is a Union type
297+
"""
298+
if ty.get_origin(type_) in UNION_TYPES:
299+
if args is not None:
300+
return ty.get_args(type_) == args
301+
return True
302+
return False

0 commit comments

Comments
 (0)