def test_FuncSpec_typing_List(): from typing import List from bifrostrpc.typing import Advanced, FuncSpec def accept_list(things: List[int]) -> None: return None def return_list() -> List[int]: return [] def list_of_lists(things: List[List[List[int]]]) -> List[List[List[str]]]: return [] # "Any" return type is not allowed FuncSpec(accept_list, Advanced()) FuncSpec(return_list, Advanced()) FuncSpec(list_of_lists, Advanced())
def test_FuncSpec_typing_Any(): from typing import Any from bifrostrpc import TypeNotSupportedError from bifrostrpc.typing import Advanced, FuncSpec def return_any() -> Any: return None def accept_any(a: Any) -> None: return None # "Any" return type is not allowed with raises(TypeNotSupportedError): FuncSpec(return_any, Advanced()) # "Any" argument type is not allowed with raises(TypeNotSupportedError): FuncSpec(accept_any, Advanced())
def generateClient( dest: FilePython, *, classname: str, funcspecs: List[Tuple[str, FuncSpec]], adv: Advanced, flavour: Flavour, ) -> None: dest.filecomment(HEADER) # we're always going to need typing module dest.contents.alsoImportPy("typing") appendFailureModeClasses(dest) # make copies of all our dataclasses for dc in adv.getAllDataclasses(): dcspec = _getDataclassSpec(dc, adv) dest.contents.also(dcspec) # generate function wrappers dest.contents.also(_generateWrappers(classname, funcspecs, adv, flavour)) dest.writefile() dest.makepretty()
def test_FuncSpec_scalar(): from bifrostrpc.typing import Advanced, FuncSpec, ScalarTypeSpec def scalar_fn(i: int, s: str, b: bool) -> None: return None # "Any" return type is not allowed spec = FuncSpec(scalar_fn, Advanced()) assert isinstance(spec.argSpecs['i'], ScalarTypeSpec) assert isinstance(spec.argSpecs['s'], ScalarTypeSpec) assert isinstance(spec.argSpecs['b'], ScalarTypeSpec)
def test_get_int_type_spec() -> None: from bifrostrpc.typing import Advanced from bifrostrpc.typing import getTypeSpec from bifrostrpc.typing import ScalarTypeSpec adv = Advanced() ts = getTypeSpec(int, adv) assert isinstance(ts, ScalarTypeSpec) assert ts.scalarType is int assert ts.originalType is int assert ts.typeName == 'int' MyInt = NewType('MyInt', int) adv.addNewType(MyInt) ts = getTypeSpec(MyInt, adv) assert isinstance(ts, ScalarTypeSpec) assert ts.scalarType is int assert ts.originalType is MyInt assert ts.typeName == 'MyInt' MyInt2 = NewType('MyInt2', MyInt) adv.addNewType(MyInt2) ts = getTypeSpec(MyInt2, adv) assert isinstance(ts, ScalarTypeSpec) assert ts.scalarType is int assert ts.originalType is MyInt2 assert ts.typeName == 'MyInt2'
def test_get_Literal_type_spec() -> None: from bifrostrpc.typing import Advanced from bifrostrpc.typing import getTypeSpec from bifrostrpc.typing import LiteralTypeSpec # need type: ignore here because mypy can't work out what Literal is due to # import fallback mechanism above Five = NewType('Five', Literal[5]) # type: ignore Hello = NewType('Hello', Literal["hello"]) # type: ignore adv = Advanced() adv.addNewType(Five) adv.addNewType(Hello) ts = getTypeSpec(Literal[5], adv) assert isinstance(ts, LiteralTypeSpec) assert ts.expected == 5 assert ts.expectedType is int ts = getTypeSpec(Five, adv) assert isinstance(ts, LiteralTypeSpec) assert ts.expected == 5 assert ts.expectedType is int ts = getTypeSpec(Hello, adv) assert isinstance(ts, LiteralTypeSpec) assert ts.expected == "hello" assert ts.expectedType is str
def test_get_str_type_spec() -> None: from bifrostrpc.typing import Advanced from bifrostrpc.typing import getTypeSpec from bifrostrpc.typing import ScalarTypeSpec adv = Advanced() ts = getTypeSpec(str, adv) assert isinstance(ts, ScalarTypeSpec) assert ts.scalarType is str assert ts.originalType is str assert ts.typeName == 'str' MyStr = NewType('MyStr', str) adv.addNewType(MyStr) ts = getTypeSpec(MyStr, adv) assert isinstance(ts, ScalarTypeSpec) assert ts.scalarType is str assert ts.originalType is MyStr assert ts.typeName == 'MyStr' MyStr2 = NewType('MyStr2', MyStr) adv.addNewType(MyStr2) ts = getTypeSpec(MyStr2, adv) assert isinstance(ts, ScalarTypeSpec) assert ts.scalarType is str assert ts.originalType is MyStr2 assert ts.typeName == 'MyStr2'
def _generateAdvancedTypes(dest: FileTS, adv: Advanced) -> None: for name, baseType, children in adv.getNewTypeDetails(): try: tsprimitive = PRIMITIVES[baseType.__name__] except KeyError: raise Exception( f'Cannot generate a typescript alias matching {name}' f'; no known primitive type for {baseType.__name__}') typeExpr = tsprimitive + ' & {readonly brand?: unique symbol}' if children: typeExpr = ' | '.join(children) + ' | (' + typeExpr + ')' dest.contents.blank() dest.contents.also(tsexpr(f'export type {name} = {typeExpr}')) for dc in adv.getAllDataclasses(): dest.contents.blank() iface = InterfaceSpec(dc.__name__, tsexport=True, appendto=dest.contents) for field in dataclasses.fields(dc): generated = _generateCrossType(getTypeSpec(field.type, adv), adv) iface.addProperty(field.name, generated)
def _generateType(spec: TypeSpec, adv: Advanced, addimport: Callable[[str], None]) -> str: if isinstance(spec, NullTypeSpec): return 'None' if isinstance(spec, ScalarTypeSpec): return spec.typeName if isinstance(spec, ListTypeSpec): itemtype = _generateType(spec.itemSpec, adv, addimport) return f'typing.List[{itemtype}]' if isinstance(spec, DictTypeSpec): keytype = _generateType(spec.keySpec, adv, addimport) valuetype = _generateType(spec.valueSpec, adv, addimport) return f'typing.Dict[{keytype}, {valuetype}]' if isinstance(spec, DataclassTypeSpec): if not adv.hasDataclass(spec.class_): raise Exception( f'Cannot generate a python type for unknown dataclass {spec.class_.__name__}') return spec.class_.__name__ if isinstance(spec, UnionTypeSpec): joined = ", ".join([ _generateType(variantspec, adv, addimport) for variantspec in spec.variants ]) return f'typing.Union[{joined}]' if isinstance(spec, LiteralTypeSpec): if spec.expectedType is bool: raise Exception("TODO: test this code path") # noqa addimport('typing_extensions') # pylint: disable=unreachable return f'typing_extensions.Literal[{spec.expected!r}]' if spec.expectedType is int: raise Exception("TODO: test this code path") # noqa addimport('typing_extensions') # pylint: disable=unreachable return f'typing_extensions.Literal[{spec.expected}]' if spec.expectedType is not str: raise Exception(f"Unexpected literal type {spec.expectedType.__name__}") addimport('typing_extensions') return f'typing_extensions.Literal[{spec.expected!r}]' raise Exception(f"TODO: generate a type for {spec!r}")
def test_Union_type_spec_does_not_collapse_int_bool() -> None: """Some versions of python express this bug. 'Union[int, bool]' becomes just 'int' See https://stackoverflow.com/questions/60154326/unable-to-create-unionbool-int-type """ from bifrostrpc.typing import Advanced from bifrostrpc.typing import getTypeSpec # test handling of a simple Union[int, bool] _assert_union_variants( getTypeSpec(Union[int, bool], Advanced()), ScalarTester(int, int, 'int'), ScalarTester(bool, bool, 'bool'), )
def test_get_Dict_type_spec() -> None: from bifrostrpc import TypeNotSupportedError from bifrostrpc.typing import Advanced from bifrostrpc.typing import DictTypeSpec from bifrostrpc.typing import ScalarTypeSpec from bifrostrpc.typing import getTypeSpec # test handling of a simple Dict[str, int] ts = getTypeSpec(Dict[str, int], Advanced()) assert isinstance(ts, DictTypeSpec) assert isinstance(ts.keySpec, ScalarTypeSpec) assert ts.keySpec.scalarType is str assert ts.keySpec.originalType is str assert ts.keySpec.typeName == 'str' assert isinstance(ts.valueSpec, ScalarTypeSpec) assert ts.valueSpec.scalarType is int assert ts.valueSpec.originalType is int assert ts.valueSpec.typeName == 'int' # test handling of a more complex Dict[str, Dict[str, CustomType]] MyStr = NewType('MyStr', str) MyStr2 = NewType('MyStr2', MyStr) adv = Advanced() adv.addNewType(MyStr) adv.addNewType(MyStr2) ts = getTypeSpec(Dict[str, Dict[str, MyStr2]], adv) assert isinstance(ts, DictTypeSpec) assert isinstance(ts.keySpec, ScalarTypeSpec) assert isinstance(ts.valueSpec, DictTypeSpec) assert isinstance(ts.valueSpec.keySpec, ScalarTypeSpec) assert isinstance(ts.valueSpec.valueSpec, ScalarTypeSpec) assert ts.keySpec.scalarType is str assert ts.keySpec.originalType is str assert ts.keySpec.typeName == 'str' assert ts.valueSpec.keySpec.scalarType is str assert ts.valueSpec.keySpec.originalType is str assert ts.valueSpec.keySpec.typeName == 'str' assert ts.valueSpec.valueSpec.scalarType is str assert ts.valueSpec.valueSpec.originalType is MyStr2 assert ts.valueSpec.valueSpec.typeName == 'MyStr2' with raises(TypeNotSupportedError): getTypeSpec(Dict[MyStr2, str], adv)
def _getTypeNoMatchExpr(var_or_prop: str, spec: TypeSpec) -> Optional[str]: if isinstance(spec, NullTypeSpec): return f"{var_or_prop} !== null" if isinstance(spec, ScalarTypeSpec): tsscalar = PRIMITIVES[spec.scalarType.__name__] return f'typeof {var_or_prop} !== "{tsscalar}"' if isinstance(spec, LiteralTypeSpec): # a literal type's type definition is identical to how you would write it as an expression valueexpr = _generateType(spec, Advanced()) return f'{var_or_prop} !== {valueexpr}' if isinstance(spec, DataclassTypeSpec): # not possible return None if isinstance(spec, ListTypeSpec): # not possible return None raise Exception(f'TODO: no code to get a type-match expr for {spec!r}')
def __init__(self, targets: List[Callable[..., Any]]): self._targets = {fn.__name__: fn for fn in targets} self._adv: Advanced = Advanced() self._spec: Dict[str, FuncSpec] = {} self._factory: Dict[Type[Any], Callable[[], Any]] = {}
def _getConverterBlock( var_or_prop: str, outname: str, label: str, spec: TypeSpec, names: Names, adv: Advanced, ) -> Statement: """ Return a paradox Statement that will convert var_or_prop to the right type. The converted value is assigned to `outname`. A TypeError is raised by the generated code block if `var_or_prop` can't be converted. """ assert outname != var_or_prop if not names.isAssignable(outname): raise Exception( f"Can't generate a converter to build {spec}" f" when {outname} is not assignable" ) if isinstance(spec, ListTypeSpec): ret = Statements() # make sure the thing came back as a list with ret.withCond(pyexpr(f'not isinstance({var_or_prop}, list)')) as cond: cond.alsoRaise("TypeError", msg=f"{label} should be of type list") ret.alsoAssign(PanVar(outname, None), pyexpr('[]')) # add converts for the items - we know filter blocks aren't possible because if they were, # we wouldn't be using _getConverterBlock on a ListTypeSpec itemspec = spec.itemSpec itemvar = names.getNewName(var_or_prop, 'item', False) with ret.withFor(PanVar(itemvar, None), pyexpr(var_or_prop)) as loop: # TODO: also if we want to provide meaningful error messages, we really want to know # the idx of the item that was broken and include it in the error message itemvar2 = names.getNewName(var_or_prop, 'item_converted', True) loop.also(_getConverterBlock( itemvar, itemvar2, f"{label}[$n]", itemspec, names, adv, )) loop.also(pyexpr(f"{outname}.append({itemvar2})")) return ret if isinstance(spec, DataclassTypeSpec): if not adv.hasDataclass(spec.class_): raise Exception( f'Cannot generate a converter for unknown dataclass {spec.class_.__name__}') ret = Statements() ret.remark(f'try and build a {spec.class_.__name__} from {label}') ret.alsoAssign(PanVar(outname, None), PanCall( f'{spec.class_.__name__}.fromDict', PanVar(var_or_prop, None), pan(label), )) return ret if isinstance(spec, UnionTypeSpec): ret = Statements() # make a list of simple expressions that can be used to verify simple types quickly, and a # list of TypeSpecs for which simple expressions aren't possible notsimple: List[TypeSpec] = [] simpleexprs: List[str] = [] for vspec in spec.variants: nomatchexpr = _getTypeNoMatchExpr(var_or_prop, vspec) if nomatchexpr is None: notsimple.append(vspec) else: simpleexprs.append(nomatchexpr) # if they were all simple, we can use a single negative-if to rule out some invalid types if simpleexprs: innerstmt: Statements = ConditionalBlock(pyexpr(' and '.join(simpleexprs))) ret.also(innerstmt) else: innerstmt = ret if notsimple: # make sure we can assign a new value to var_or_prop if var_or_prop != 'result' and not names.isAssignable(var_or_prop): raise Exception(f"Overwriting {var_or_prop} won't work") # use a nested function for flow-control ... mostly so we can use 'return' statements # to break out of the function early if we find a matching type checkervar = names.getNewName('', 'value', True) checkername = names.getNewName('', 'checker', False) innercheck = FunctionSpec(checkername, CrossAny()) innercheck.addPositionalArg(checkervar, CrossAny()) innerstmt.also(innercheck) innerstmt.alsoAssign(PanVar(var_or_prop, None), PanCall(checkername, PanVar(var_or_prop, None))) innerstmt = innercheck for vspec in notsimple: innerstmt.remark('add a try/except for each vspec') with innerstmt.withTryBlock() as tryblock: filterblock = convertblock = None try: filterblock = _getFilterBlock(checkervar, label, vspec, names) except _FilterNotPossible: convertedvar = names.getNewName(checkervar, 'converted', True) convertblock = _getConverterBlock( checkervar, convertedvar, label, vspec, names, adv, ) if filterblock: tryblock.also(filterblock) tryblock.alsoReturn(PanVar(checkervar, None)) else: assert convertblock is not None tryblock.also(convertblock) tryblock.alsoReturn(PanVar(convertedvar, None)) with tryblock.withCatchBlock('TypeError') as catchblock: catchblock.remark('ignore TypeError -contine on to next variant') catchblock.also(pyexpr('pass')) innerstmt.alsoRaise("TypeError", msg=f"{var_or_prop} did not match any variant") return ret raise Exception( f"No code to generate a converter block for {var_or_prop} using {spec!r}" )
def _generateType(spec: TypeSpec, adv: Advanced) -> str: if isinstance(spec, NullTypeSpec): return 'null' if isinstance(spec, LiteralTypeSpec): if spec.expectedType is bool: raise Exception("TODO: test this code path") # noqa return 'true' if spec.expected else 'false' # pylint: disable=unreachable if spec.expectedType is int: raise Exception("TODO: test this code path") # noqa return str(spec.expected) # pylint: disable=unreachable if spec.expectedType is not str: raise Exception( f"Unexpected literal type {spec.expectedType.__name__}") if not re.match(r'^[a-zA-Z0-9_.,\-]+$', cast(str, spec.expected)): raise Exception( f"Literal {spec.expected!r} is too complex to rebuild in typescript" ) return '"' + cast(str, spec.expected) + '"' if isinstance(spec, ScalarTypeSpec): typeName = spec.typeName if adv.hasNewType(spec.originalType) or adv.hasExternalType( spec.originalType): return typeName try: return PRIMITIVES[typeName] except KeyError: raise Exception( f'Cannot generate a typescript alias matching {typeName}' f'; no known primitive type for {typeName}') if isinstance(spec, DataclassTypeSpec): if not adv.hasDataclass(spec.class_): raise Exception( f'Cannot generate a typescript type for unknown dataclass {spec.class_.__name__}' ) return spec.class_.__name__ if isinstance(spec, ListTypeSpec): itemstr = _generateType(spec.itemSpec, adv) if re.match(r'^\w+$', itemstr): return itemstr + '[]' return 'Array<' + itemstr + '>' if isinstance(spec, UnionTypeSpec): return ' | '.join( [_generateType(variantspec, adv) for variantspec in spec.variants]) if isinstance(spec, DictTypeSpec): assert isinstance(spec.keySpec, ScalarTypeSpec) and spec.keySpec.scalarType is str keytype = _generateType(spec.keySpec, adv) valuetype = _generateType(spec.valueSpec, adv) return f"{{[k: {keytype}]: {valuetype}}}" raise Exception(f"TODO: generate a type for {spec!r}")
def test_get_List_type_spec() -> None: from bifrostrpc import TypeNotSupportedError from bifrostrpc.typing import Advanced from bifrostrpc.typing import ListTypeSpec from bifrostrpc.typing import ScalarTypeSpec from bifrostrpc.typing import getTypeSpec # test handling of a simple List[int] ts = getTypeSpec(List[int], Advanced()) assert isinstance(ts, ListTypeSpec) assert isinstance(ts.itemSpec, ScalarTypeSpec) assert ts.itemSpec.scalarType is int assert ts.itemSpec.originalType is int assert ts.itemSpec.typeName == 'int' # test handling of a more complex List[List[CustomType]] MyStr = NewType('MyStr', str) MyStr2 = NewType('MyStr2', MyStr) adv = Advanced() adv.addNewType(MyStr) adv.addNewType(MyStr2) ts = getTypeSpec(List[List[MyStr2]], adv) assert isinstance(ts, ListTypeSpec) assert isinstance(ts.itemSpec, ListTypeSpec) assert isinstance(ts.itemSpec.itemSpec, ScalarTypeSpec) assert ts.itemSpec.itemSpec.scalarType is str assert ts.itemSpec.itemSpec.originalType is MyStr2 assert ts.itemSpec.itemSpec.typeName == 'MyStr2' UserID = NewType('UserID', int) Users = NewType('Users', List[UserID]) adv = Advanced() adv.addNewType(UserID) adv.addNewType(Users) # TODO: we don't yet support a NewType wrapping a List like this with raises(TypeNotSupportedError): ts = getTypeSpec(Users, adv) assert isinstance(ts, ListTypeSpec) assert isinstance(ts.itemSpec, ListTypeSpec) assert isinstance(ts.itemSpec.itemSpec, ScalarTypeSpec) assert ts.itemSpec.itemSpec.scalarType is UserID assert ts.itemSpec.itemSpec.originalType is int assert ts.itemSpec.itemSpec.typeName == 'UserID'
def _generateConverter( ts: RawTypescript, var_or_prop: str, spec: TypeSpec, names: Names, adv: Advanced, indent: str, ) -> None: if isinstance(spec, NullTypeSpec): # make sure the thing is null ts.rawline(f'{indent}if ({var_or_prop} !== null) {{') ts.rawline( f'{indent} throw new TypeError("{var_or_prop} should be null");') ts.rawline(f'{indent}}}') return if isinstance(spec, ScalarTypeSpec): tsscalar = PRIMITIVES[spec.scalarType.__name__] # make sure the thing has the correct type ts.rawline(f'{indent}if (typeof {var_or_prop} !== "{tsscalar}") {{') ts.rawline( f'{indent} throw new TypeError("{var_or_prop} should be of type {tsscalar}");' ) ts.rawline(f'{indent}}}') return if isinstance(spec, ListTypeSpec): # make sure the thing is an array ts.rawline(f'{indent}if (!Array.isArray({var_or_prop})) {{') ts.rawline( f'{indent} throw new TypeError("{var_or_prop} should be an Array");' ) ts.rawline(f'{indent}}}') # make sure all items have the correct type itemspec = spec.itemSpec itemvar = names.getNewName(var_or_prop, 'item', False) # TODO: if we actually need to *convert* a type, then we probably need to use # for (let idx in var) { itemvar = var[idx]; ... } # or possibly # var = var.map(function(var_item) { return convert(var_item); })... ts.rawline(f'{indent}for (let {itemvar} of {var_or_prop}) {{') # TODO: also if we want to provide meaningful error messages, we really want to know the # idx of the item that was broken and include it in the error message _generateConverter(ts, itemvar, itemspec, names, adv, indent + ' ') ts.rawline(f'{indent}}}') return if isinstance(spec, DataclassTypeSpec): if not adv.hasDataclass(spec.class_): raise Exception( f'Cannot generate a typescript type for unknown dataclass {spec.class_.__name__}' ) ts.rawline( f"{indent}// make sure {var_or_prop} is an object that has properties" ) ts.rawline(f"{indent}if (!{var_or_prop}) {{") msg = f'{var_or_prop} must be an object that satisfies {spec.class_.__name__} interface' ts.rawline(f"{indent} throw new TypeError('{msg}');") ts.rawline(f"{indent}}}") ts.rawline(f'{indent}// verify each member of {spec.class_.__name__}') for name, fieldspec in spec.fieldSpecs.items(): propexpr = var_or_prop + '.' + name _generateConverter(ts, propexpr, fieldspec, names, adv, indent) return if isinstance(spec, UnionTypeSpec): # make a list of simple expressions that can be used to verify simple types quickly, and a # list of TypeSpecs for which simple expressions aren't possible notsimple: List[TypeSpec] = [] simpleexprs: List[str] = [] for vspec in spec.variants: nomatchexpr = _getTypeNoMatchExpr(var_or_prop, vspec) if nomatchexpr is None: notsimple.append(vspec) else: simpleexprs.append(nomatchexpr) joinedexpr = ' && '.join(simpleexprs) # if they were all simple, we can use a single negative-if to match invalid types # TODO: this won't work if we have no simpleexprs ts.rawline(f'{indent}if ({joinedexpr}) {{') # use a nested function for flow-control ... mostly so we can use 'return' statements # to break out of the function early if we find a matching type ts.rawline(f'{indent} (function() {{') # add a try/except for each vspec # TODO: this fix will need some more serious testing for vspec in notsimple: ename = names.getNewName(var_or_prop, 'error', False) ts.rawline(f'{indent} try {{') _generateConverter(ts, var_or_prop, vspec, names, adv, indent + ' ') ts.rawline( f'{indent} return; // {var_or_prop} matches this variant') ts.rawline(f'{indent} }} catch ({ename}) {{') ts.rawline(f'{indent} if ({ename} instanceof TypeError) {{') ts.rawline( f'{indent} // ignore type-error - continue on to next variant' ) ts.rawline(f'{indent} }} else {{') ts.rawline( f'{indent} throw {ename}; // re-throw any real errors') ts.rawline(f'{indent} }}') ts.rawline(f'{indent} }}') # end try/catch ts.rawline( f'{indent} throw new TypeError("{var_or_prop} did not match any variant");' ) ts.rawline(f'{indent} }})();') ts.rawline(f'{indent}}}') return if isinstance(spec, DictTypeSpec): assert isinstance(spec.keySpec, ScalarTypeSpec) and spec.keySpec.scalarType is str # NOTE: we don't bother typechecking the keys since we know they will be strings coming # from JSON ts.rawline( f"{indent}// make sure {var_or_prop} is an object that matches the dict spec" ) ts.rawline(f"{indent}if (!{var_or_prop}) {{") msg = f'{var_or_prop} must be an object' ts.rawline(f"{indent} throw new TypeError('{msg}');") ts.rawline(f"{indent}}}") ts.rawline(f'{indent}// verify each item of the dict') keyvar = names.getNewName(var_or_prop, 'key', False) ts.rawline(f'{indent}for (let {keyvar} in {var_or_prop}) {{') itemvar = var_or_prop + '[' + keyvar + ']' _generateConverter(ts, itemvar, spec.valueSpec, names, adv, indent + ' ') ts.rawline(f'{indent}}}') return raise Exception( f"TODO: generate a converter for {var_or_prop} using {spec!r}")
def _importExternalTypes(dest: FileTS, adv: Advanced) -> None: for name, (tsmodule, ) in adv.getExternalTypeDetails(): # XXX: dirty hack get rid of this if name in ('TagID', 'CategoryName', 'CategoryDesc', 'CategoryID'): continue dest.contents.alsoImportTS(tsmodule, [name])
def test_get_Union_type_spec() -> None: from bifrostrpc.typing import Advanced from bifrostrpc.typing import getTypeSpec # test handling of a simple Union[str, int, None] _assert_union_variants( getTypeSpec(Union[str, int, None], Advanced()), is_null_spec, ScalarTester(str, str, 'str'), ScalarTester(int, int, 'int'), ) # test handling of Optional[] _assert_union_variants( getTypeSpec(Optional[str], Advanced()), is_null_spec, ScalarTester(str, str, 'str'), ) # test handling of nested Unions - note that nested unions are collapsed _assert_union_variants( getTypeSpec(Union[str, Union[int, None]], Advanced()), is_null_spec, ScalarTester(str, str, 'str'), ScalarTester(int, int, 'int'), ) _assert_union_variants( getTypeSpec(Union[str, Optional[int]], Advanced()), is_null_spec, ScalarTester(str, str, 'str'), ScalarTester(int, int, 'int'), ) # test Union containing more complex types: Union[List[], Dict[]] _assert_union_variants( getTypeSpec(Union[List[int], Dict[str, int]], Advanced()), ListTester(ScalarTester(int, int, 'int')), DictTester(ScalarTester(int, int, 'int')), ) # test Union containing some NewTypes MyInt = NewType('MyInt', int) MyStr = NewType('MyStr', str) MyInt2 = NewType('MyInt2', MyInt) MyStr2 = NewType('MyStr2', MyStr) adv = Advanced() adv.addNewType(MyInt) adv.addNewType(MyStr) adv.addNewType(MyInt2) adv.addNewType(MyStr2) _assert_union_variants( getTypeSpec(Union[List[MyInt2], Dict[str, MyStr2]], adv), ListTester(ScalarTester(MyInt2, int, 'MyInt2')), DictTester(ScalarTester(MyStr2, str, 'MyStr2')), )