def _pan_nodef(val: Union[Pannable, NoDefault]) -> Optional[PanExpr]: if val is NO_DEFAULT: return None # NO_DEFAULT should be the only possible instance of NoDefault. This assertion is really just # to satisfy mypy assert not isinstance(val, NoDefault) return pan(val)
def withFor( self, assign: PanVar, expr: Pannable, ) -> 'Iterator[ForLoopBlock]': loop = ForLoopBlock(assign, pan(expr)) self._statements.append(loop) yield loop
def _generateWrappers( classname: str, funcspecs: List[Tuple[str, FuncSpec]], adv: Advanced, flavour: Flavour, ) -> ClassSpec: cls = ClassSpec( classname, isabstract=flavour == 'abstract', # TODO: we really should have a docstring for this ) # add an dispatch() function that is used for all methods cls.alsoImportPy('typing') dispatchfn = cls.createMethod( '_dispatch', unionof(CrossNewType('ApiFailure'), CrossAny()), isabstract=flavour == 'abstract', ) # the method that should be called dispatchfn.addPositionalArg('method', str) # a dict of params to pass to the method dispatchfn.addPositionalArg('params', dictof(str, CrossAny())) # converter will be called with the result of the method call. # It may modify result before returning it. It may raise a TypeError # if any part of result does not match the method's return type. dispatchfn.addPositionalArg('converter', CrossCallable([CrossAny()], CrossAny())) if flavour == 'requests': # TODO: finish this dispatchfn.alsoRaise( msg="TODO: perform the request using requests library" ) else: assert flavour == 'abstract' # TODO: abstract function should get '...' body automatically? # dispatchfn.also('...') for name, funcspec in funcspecs: retspec = funcspec.getReturnSpec() # build a custom converter function for this method conv = FunctionSpec('_converter', CrossAny()) v_result = conv.addPositionalArg('result', CrossAny()) names = Names() try: filterblock = _getFilterBlock('result', '$DATA', retspec, names) conv.also(filterblock) except _FilterNotPossible: names.getSpecificName('converted', True) conv.also(_getConverterBlock('result', 'converted', '$DATA', retspec, names, adv)) conv.alsoReturn(PanVar('converted', None)) else: conv.alsoReturn(v_result) rettype = unionof(CrossNewType('ApiFailure'), _generateCrossType(retspec, adv)) method = cls.createMethod(name, rettype) for argname, spec in funcspec.getArgSpecs().items(): method.addPositionalArg(argname, _generateCrossType(spec, adv)) v_args = PanVar('args', dictof(str, CrossAny())) argnames = DictBuilderStatement.fromPanVar(v_args) for n in funcspec.getArgSpecs().keys(): # TODO: dataclasses aren't automatically JSON serializable, so we need to raise an # error if we try to generate a client that has a dataclass argument type argnames.addPair(n, False) method.also(argnames) method.blank() method.remark( 'include [__dataclass__] in returned values so that we can rebuild dataclasses', ) method.alsoAssign(v_args["__showdataclass__"], True) method.also(conv) method.alsoReturn(PanCall( 'self._dispatch', pan(name), v_args, pyexpr('_converter'), )) # TODO: rather than writing to the file pointer, we should really be returning the class return cls
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 alsoAssign( self, var: Union[PanVar, PanIndexAccess, PanKeyAccess], expr: Pannable, ) -> None: self._statements.append(AssignmentStatement(var, pan(expr)))
def alsoAppend(self, list_: Pannable, value: Pannable) -> None: self._statements.append(ListAppendStatement(pan(list_), pan(value)))
def alsoReturn(self, expr: Pannable) -> None: self._statements.append(ReturnStatement(pan(expr)))