Skip to content

Commit 0af07fb

Browse files
authored
ENH: Handle None types (#742)
* added support for None to be implicitly interpreted as NoneType in type-checking/coercing * fix up type checking of none-types * write file in dir input change * implemented UnionType type-parsing support * added register_serializer for types.UnionType * fixed error in runtime error * reverted test change * fixed exception pattern matching to cover head and latest release of fileformats * adopted ghislain's suggestion
1 parent e52e32b commit 0af07fb

File tree

5 files changed

+255
-13
lines changed

5 files changed

+255
-13
lines changed

pydra/engine/specs.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -991,8 +991,8 @@ def get_value(
991991
f"named '{node.checksum}' in any of the cache locations.\n"
992992
+ "\n".join(str(p) for p in set(node.cache_locations))
993993
+ f"\n\nThis is likely due to hash changes in '{self.name}' node inputs. "
994-
f"Current values and hashes: {self.inputs}, "
995-
f"{self.inputs._hashes}\n\n"
994+
f"Current values and hashes: {node.inputs}, "
995+
f"{node.inputs._hashes}\n\n"
996996
"Set loglevel to 'debug' in order to track hash changes "
997997
"throughout the execution of the workflow.\n\n "
998998
"These issues may have been caused by `bytes_repr()` methods "

pydra/utils/hash.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Generic object hashing dispatch"""
22

3+
import sys
34
import os
45
import struct
56
from datetime import datetime
67
import typing as ty
8+
import types
79
from pathlib import Path
810
from collections.abc import Mapping
911
from functools import singledispatch
@@ -467,6 +469,10 @@ def type_name(tp):
467469
yield b")"
468470

469471

472+
if sys.version_info >= (3, 10):
473+
register_serializer(types.UnionType)(bytes_repr_type)
474+
475+
470476
@register_serializer(FileSet)
471477
def bytes_repr_fileset(
472478
fileset: FileSet, cache: Cache

pydra/utils/tests/test_hash.py

+18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22
import os
3+
import sys
34
from hashlib import blake2b
45
from pathlib import Path
56
import time
@@ -200,6 +201,14 @@ def test_bytes_special_form1():
200201
assert obj_repr == b"type:(typing.Union[type:(builtins.int)type:(builtins.float)])"
201202

202203

204+
@pytest.mark.skipif(condition=sys.version_info < (3, 10), reason="requires python3.10")
205+
def test_bytes_special_form1a():
206+
obj_repr = join_bytes_repr(int | float)
207+
assert (
208+
obj_repr == b"type:(types.UnionType[type:(builtins.int)type:(builtins.float)])"
209+
)
210+
211+
203212
def test_bytes_special_form2():
204213
obj_repr = join_bytes_repr(ty.Any)
205214
assert re.match(rb"type:\(typing.Any\)", obj_repr)
@@ -212,6 +221,15 @@ def test_bytes_special_form3():
212221
)
213222

214223

224+
@pytest.mark.skipif(condition=sys.version_info < (3, 10), reason="requires python3.10")
225+
def test_bytes_special_form3a():
226+
obj_repr = join_bytes_repr(Path | None)
227+
assert (
228+
obj_repr
229+
== b"type:(types.UnionType[type:(pathlib.Path)type:(builtins.NoneType)])"
230+
)
231+
232+
215233
def test_bytes_special_form4():
216234
obj_repr = join_bytes_repr(ty.Type[Path])
217235
assert obj_repr == b"type:(builtins.type[type:(pathlib.Path)])"

pydra/utils/tests/test_typing.py

+202-1
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,26 @@ def test_type_check_basic15():
120120
TypeParser(ty.Union[Path, File, float])(lz(int))
121121

122122

123+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
124+
def test_type_check_basic15a():
125+
TypeParser(Path | File | float)(lz(int))
126+
127+
123128
def test_type_check_basic16():
124129
with pytest.raises(
125130
TypeError, match="Cannot coerce <class 'float'> to any of the union types"
126131
):
127132
TypeParser(ty.Union[Path, File, bool, int])(lz(float))
128133

129134

135+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
136+
def test_type_check_basic16a():
137+
with pytest.raises(
138+
TypeError, match="Cannot coerce <class 'float'> to any of the union types"
139+
):
140+
TypeParser(Path | File | bool | int)(lz(float))
141+
142+
130143
def test_type_check_basic17():
131144
TypeParser(ty.Sequence)(lz(ty.Tuple[int, ...]))
132145

@@ -194,6 +207,12 @@ def test_type_check_fail2():
194207
TypeParser(ty.Union[Path, File])(lz(int))
195208

196209

210+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
211+
def test_type_check_fail2a():
212+
with pytest.raises(TypeError, match="to any of the union types"):
213+
TypeParser(Path | File)(lz(int))
214+
215+
197216
def test_type_check_fail3():
198217
with pytest.raises(TypeError, match="doesn't match any of the explicit inclusion"):
199218
TypeParser(ty.Sequence, coercible=[(ty.Sequence, ty.Sequence)])(
@@ -312,13 +331,32 @@ def test_type_coercion_basic12():
312331
assert TypeParser(ty.Union[Path, File, int], coercible=[(ty.Any, ty.Any)])(1.0) == 1
313332

314333

334+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
335+
def test_type_coercion_basic12a():
336+
with pytest.raises(TypeError, match="explicitly excluded"):
337+
TypeParser(
338+
list,
339+
coercible=[(ty.Sequence, ty.Sequence)],
340+
not_coercible=[(str, ty.Sequence)],
341+
)("a-string")
342+
343+
assert TypeParser(Path | File | int, coercible=[(ty.Any, ty.Any)])(1.0) == 1
344+
345+
315346
def test_type_coercion_basic13():
316347
assert (
317348
TypeParser(ty.Union[Path, File, bool, int], coercible=[(ty.Any, ty.Any)])(1.0)
318349
is True
319350
)
320351

321352

353+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
354+
def test_type_coercion_basic13a():
355+
assert (
356+
TypeParser(Path | File | bool | int, coercible=[(ty.Any, ty.Any)])(1.0) is True
357+
)
358+
359+
322360
def test_type_coercion_basic14():
323361
assert TypeParser(ty.Sequence, coercible=[(ty.Any, ty.Any)])((1, 2, 3)) == (
324362
1,
@@ -404,6 +442,12 @@ def test_type_coercion_fail2():
404442
TypeParser(ty.Union[Path, File], coercible=[(ty.Any, ty.Any)])(1)
405443

406444

445+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
446+
def test_type_coercion_fail2a():
447+
with pytest.raises(TypeError, match="to any of the union types"):
448+
TypeParser(Path | File, coercible=[(ty.Any, ty.Any)])(1)
449+
450+
407451
def test_type_coercion_fail3():
408452
with pytest.raises(TypeError, match="doesn't match any of the explicit inclusion"):
409453
TypeParser(ty.Sequence, coercible=[(ty.Sequence, ty.Sequence)])(
@@ -446,7 +490,7 @@ def f(x: ty.List[File], y: ty.Dict[str, ty.List[File]]):
446490
TypeParser(ty.List[str])(task.lzout.a) # pylint: disable=no-member
447491
with pytest.raises(
448492
TypeError,
449-
match="Cannot coerce <class 'fileformats.generic.File'> into <class 'int'>",
493+
match="Cannot coerce <class 'fileformats\.generic.*\.File'> into <class 'int'>",
450494
):
451495
TypeParser(ty.List[int])(task.lzout.a) # pylint: disable=no-member
452496

@@ -469,6 +513,27 @@ def test_matches_type_union():
469513
assert not TypeParser.matches_type(ty.Union[int, bool, str], ty.Union[int, bool])
470514

471515

516+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
517+
def test_matches_type_union_a():
518+
assert TypeParser.matches_type(int | bool | str, int | bool | str)
519+
assert TypeParser.matches_type(int | bool, int | bool | str)
520+
assert not TypeParser.matches_type(int | bool | str, int | bool)
521+
522+
523+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
524+
def test_matches_type_union_b():
525+
assert TypeParser.matches_type(int | bool | str, ty.Union[int, bool, str])
526+
assert TypeParser.matches_type(int | bool, ty.Union[int, bool, str])
527+
assert not TypeParser.matches_type(int | bool | str, ty.Union[int, bool])
528+
529+
530+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
531+
def test_matches_type_union_c():
532+
assert TypeParser.matches_type(ty.Union[int, bool, str], int | bool | str)
533+
assert TypeParser.matches_type(ty.Union[int, bool], int | bool | str)
534+
assert not TypeParser.matches_type(ty.Union[int, bool, str], int | bool)
535+
536+
472537
def test_matches_type_dict():
473538
COERCIBLE = [(str, Path), (Path, str), (int, float)]
474539

@@ -713,18 +778,61 @@ def test_union_is_subclass1():
713778
assert TypeParser.is_subclass(ty.Union[Json, Yaml], ty.Union[Json, Yaml, Xml])
714779

715780

781+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
782+
def test_union_is_subclass1a():
783+
assert TypeParser.is_subclass(Json | Yaml, Json | Yaml | Xml)
784+
785+
786+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
787+
def test_union_is_subclass1b():
788+
assert TypeParser.is_subclass(Json | Yaml, ty.Union[Json, Yaml, Xml])
789+
790+
791+
## Up to here!
792+
793+
794+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
795+
def test_union_is_subclass1c():
796+
assert TypeParser.is_subclass(ty.Union[Json, Yaml], Json | Yaml | Xml)
797+
798+
716799
def test_union_is_subclass2():
717800
assert not TypeParser.is_subclass(ty.Union[Json, Yaml, Xml], ty.Union[Json, Yaml])
718801

719802

803+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
804+
def test_union_is_subclass2a():
805+
assert not TypeParser.is_subclass(Json | Yaml | Xml, Json | Yaml)
806+
807+
808+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
809+
def test_union_is_subclass2b():
810+
assert not TypeParser.is_subclass(ty.Union[Json, Yaml, Xml], Json | Yaml)
811+
812+
813+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
814+
def test_union_is_subclass2c():
815+
assert not TypeParser.is_subclass(Json | Yaml | Xml, ty.Union[Json, Yaml])
816+
817+
720818
def test_union_is_subclass3():
721819
assert TypeParser.is_subclass(Json, ty.Union[Json, Yaml])
722820

723821

822+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
823+
def test_union_is_subclass3a():
824+
assert TypeParser.is_subclass(Json, Json | Yaml)
825+
826+
724827
def test_union_is_subclass4():
725828
assert not TypeParser.is_subclass(ty.Union[Json, Yaml], Json)
726829

727830

831+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
832+
def test_union_is_subclass4a():
833+
assert not TypeParser.is_subclass(Json | Yaml, Json)
834+
835+
728836
def test_generic_is_subclass1():
729837
assert TypeParser.is_subclass(ty.List[int], list)
730838

@@ -737,6 +845,56 @@ def test_generic_is_subclass3():
737845
assert not TypeParser.is_subclass(ty.List[float], ty.List[int])
738846

739847

848+
def test_none_is_subclass1():
849+
assert TypeParser.is_subclass(None, ty.Union[int, None])
850+
851+
852+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
853+
def test_none_is_subclass1a():
854+
assert TypeParser.is_subclass(None, int | None)
855+
856+
857+
def test_none_is_subclass2():
858+
assert not TypeParser.is_subclass(None, ty.Union[int, float])
859+
860+
861+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
862+
def test_none_is_subclass2a():
863+
assert not TypeParser.is_subclass(None, int | float)
864+
865+
866+
def test_none_is_subclass3():
867+
assert TypeParser.is_subclass(ty.Tuple[int, None], ty.Tuple[int, None])
868+
869+
870+
def test_none_is_subclass4():
871+
assert TypeParser.is_subclass(None, None)
872+
873+
874+
def test_none_is_subclass5():
875+
assert not TypeParser.is_subclass(None, int)
876+
877+
878+
def test_none_is_subclass6():
879+
assert not TypeParser.is_subclass(int, None)
880+
881+
882+
def test_none_is_subclass7():
883+
assert TypeParser.is_subclass(None, type(None))
884+
885+
886+
def test_none_is_subclass8():
887+
assert TypeParser.is_subclass(type(None), None)
888+
889+
890+
def test_none_is_subclass9():
891+
assert TypeParser.is_subclass(type(None), type(None))
892+
893+
894+
def test_none_is_subclass10():
895+
assert TypeParser.is_subclass(type(None), type(None))
896+
897+
740898
@pytest.mark.skipif(
741899
sys.version_info < (3, 9), reason="Cannot subscript tuple in < Py3.9"
742900
)
@@ -780,3 +938,46 @@ def test_type_is_instance3():
780938

781939
def test_type_is_instance4():
782940
assert TypeParser.is_instance(Json, type)
941+
942+
943+
def test_type_is_instance5():
944+
assert TypeParser.is_instance(None, None)
945+
946+
947+
def test_type_is_instance6():
948+
assert TypeParser.is_instance(None, type(None))
949+
950+
951+
def test_type_is_instance7():
952+
assert not TypeParser.is_instance(None, int)
953+
954+
955+
def test_type_is_instance8():
956+
assert not TypeParser.is_instance(1, None)
957+
958+
959+
def test_type_is_instance9():
960+
assert TypeParser.is_instance(None, ty.Union[int, None])
961+
962+
963+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
964+
def test_type_is_instance9a():
965+
assert TypeParser.is_instance(None, int | None)
966+
967+
968+
def test_type_is_instance10():
969+
assert TypeParser.is_instance(1, ty.Union[int, None])
970+
971+
972+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
973+
def test_type_is_instance10a():
974+
assert TypeParser.is_instance(1, int | None)
975+
976+
977+
def test_type_is_instance11():
978+
assert not TypeParser.is_instance(None, ty.Union[int, str])
979+
980+
981+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="No UnionType < Py3.10")
982+
def test_type_is_instance11a():
983+
assert not TypeParser.is_instance(None, int | str)

0 commit comments

Comments
 (0)