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 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_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 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 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')), )
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 _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 _getDataclassSpec( dc: Type[Any], adv: Advanced, ) -> ClassSpec: name = dc.__name__ cls = ClassSpec(name, isdataclass=True) for field in dataclasses.fields(dc): fieldspec = getTypeSpec(field.type, adv) cls.addProperty(field.name, _generateCrossType(fieldspec, adv)) # the dataclass needs a deserialization method, too fromdict = cls.createMethod('fromDict', CrossNewType(name, quoted=True), isstaticmethod=True) fromdict.addPositionalArg('data', CrossAny()) fromdict.addPositionalArg('label', str) # constructor part 1 - ensure the provided data is a dict with fromdict.withCond(pyexpr('not isinstance(data, dict)')) as cond: cond.alsoRaise("TypeError", expr=pyexpr('f"{label} must be a dict"')) # constructor part 2 - ensure the __dataclass__ item is present with fromdict.withCond(pyexpr(f'data.get("__dataclass__") != {name!r}')) as cond: # Tell pylint not to sorry about the use of %-string formatting here - # using an f-string to generate an f-string is too error prone: # pylint: disable=C0209 cond.alsoRaise("TypeError", expr=pyexpr('f"{label}[\'__dataclass__\'] must be %s"' % repr(name))) names = Names() buildargs: List[PanExpr] = [] # validate each property item for field in dataclasses.fields(dc): # use a try/catch to assign the dict key to a local variable before we filter/convert it. # This allows us to trap the KeyError quickly and explicitly fname = field.name varname = names.getNewName('', fname, True) v_var = PanVar(varname, CrossAny()) with fromdict.withTryBlock() as tryblock: tryblock.alsoDeclare(v_var, None, pyexpr(f'data[{fname!r}]')) with tryblock.withCatchBlock('KeyError') as caught: # Tell pylint not to sorry about the use of %-string formatting # here - using an f-string to generate an f-string is too error # prone: # pylint: disable=C0209 caught.alsoRaise("TypeError", expr=pyexpr('f"{label}[\'%s\'] is missing"' % fname)) fieldspec = getTypeSpec(field.type, adv) # check the local variable's type try: fromdict.also(_getFilterBlock(varname, f"data[{fname!r}]", fieldspec, names)) except _FilterNotPossible: stmts = Statements() copyname = names.getNewName(varname, 'converted', True) stmts.also(_getConverterBlock( varname, copyname, f"data[{fname!r}]", fieldspec, names, adv, )) stmts.alsoAssign(v_var, PanVar(copyname, CrossAny())) fromdict.also(stmts) buildargs.append(v_var) fromdict.alsoReturn(PanCall(name, *buildargs)) return cls