Beispiel #1
0
 def alsoDeclare(
     self,
     target: Union[str, PanVar],
     type: Union[None, FlexiType, Literal["no_type"]],
     value: "Union[Pannable, builtins.ellipsis]" = ...,
 ) -> PanVar:
     declaretype = True
     if isinstance(target, PanVar):
         realtarget = target
     elif type is None:
         if not isinstance(value, PanExpr):
             raise Exception(
                 "alsoDeclare() requires a type, or target to be a PanVar"
                 ", or value to be a PanExpr with a known type")
         realtarget = PanVar(target, value.getPanType())
     elif type == "no_type":
         realtarget = PanVar(target, None)
         declaretype = False
     else:
         # FIXME: get rid of type: ignore here
         realtarget = PanVar(target, unflex(type))
     self._statements.append(
         AssignmentStatement(
             realtarget,
             None if value is ... else pan(value),
             declare=True,
             declaretype=declaretype,
         ))
     return realtarget
Beispiel #2
0
    def _addArg(
        self,
        target: List[Tuple[str, CrossType, Optional[PanExpr]]],
        *,
        name: str,
        crosstype: CrossType,
        default: Optional[PanExpr],
        allowomit: bool,
        nullable: bool,
    ) -> PanVar:
        assert not len(
            self._overloads), 'Added an arg after an overload was defined'

        if nullable:
            crosstype = maybe(crosstype)

        if allowomit:
            crosstype = omittable(crosstype)
            if default is None:
                default = PanOmit()
        else:
            assert not isinstance(default, PanOmit)

        target.append((name, crosstype, default))
        return PanVar(name, crosstype)
Beispiel #3
0
    def __init__(self, var: Union[str, PanVar], keytype: FlexiType,
                 valtype: FlexiType) -> None:
        super().__init__()

        keytype = unflex(keytype)
        assert isinstance(keytype,
                          CrossStr), "Only str keys are currently supported"
        realtype = CrossDict(keytype, unflex(valtype))

        if isinstance(var, str):
            self._var = PanVar(var, realtype)
        else:
            self._var = var
        self._type = realtype

        self._keys: List[Tuple[str, bool]] = []
Beispiel #4
0
    def writepy(self, w: FileWriter) -> None:
        inner = ', '.join(
            [f'{k!r}: {k}' for k, allowomit in self._keys if not allowomit])

        varstr = self._var.getPyExpr()[0]

        w.line0(f'{varstr}: {self._type.getQuotedPyType()} = {{{inner}}}')

        # now do the omittable args
        for k, allowomit in self._keys:
            if allowomit:
                # FIXME: this isn't how we want to do omitted args - we should be doing ellipsis
                expr = pannotomit(PanVar(k, None))
                w.line0(f'if {expr.getPyExpr()[0]}:')
                w.line1(f'{varstr}[{k!r}] = {k}')
Beispiel #5
0
    def _getInitSpec(
        self, lang: Literal["python", "typescript",
                            "php"]) -> Optional[FunctionSpec]:
        initdefaults = self._initdefaults

        if lang in ("typescript", "php"):
            # if the language is typescript or PHP, we don't need to assign PanLiteral default
            # values because these were already done in the class body
            initdefaults = [
                d for d in initdefaults if not isinstance(d[1], PanLiteral)
            ]

        # do we actually need the __init__() method or was it a noop?
        if not (self._initargs or initdefaults):
            return None

        initspec = FunctionSpec.getconstructor()
        for name, crosstype, pandefault in self._initargs:
            initspec.addPositionalArg(
                name,
                crosstype,
                default=NO_DEFAULT if pandefault is None else pandefault,
            )
            initspec.alsoAssign(PanProp(name, CrossAny(), None),
                                PanVar(name, None))

        if self._bases and lang == "python":
            initspec.addPositionalArg('*args', CrossAny())
            initspec.addPositionalArg('**kwargs', CrossAny())

        # also call super's init
        if self._bases:
            initspec.also(
                HardCodedStatement(
                    python='super().__init__(*args, **kwargs)',
                    typescript='super();',
                    php='parent::__construct();',
                ))
        elif self._tsbase and lang == "typescript":
            initspec.also(HardCodedStatement(typescript='super();', ))

        # do we need positional args for any of the properties?
        for name, default in initdefaults:
            initspec.alsoAssign(PanProp(name, CrossAny(), None), default)

        return initspec
Beispiel #6
0
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
Beispiel #7
0
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}"
    )
Beispiel #8
0
def _getFilterBlock(
    var_or_prop: str,
    label: str,
    spec: TypeSpec,
    names: Names,
) -> Statement:
    """
    Return a paradox Statement that will raise a TypeError on incorrect values.

    This method raises a _FilterNotPossible exception if the TypeSpec doesnt support it (e.g. for
    Dataclasses).
    """
    if isinstance(spec, NullTypeSpec):
        # just need a block that raises TypeError if the thing isn't None
        cond = ConditionalBlock(pyexpr(f'{var_or_prop} is not None'))
        cond.alsoRaise("TypeError", msg=f"{label} must be None")
        return cond

    if isinstance(spec, ScalarTypeSpec):
        ret = Statements()

        # just need to make sure the thing is an instance of the correct scalar type
        primitivename = spec.scalarType.__name__
        with ret.withCond(pyexpr(f'not isinstance({var_or_prop}, {primitivename})')) as cond:
            cond.alsoRaise("TypeError", msg=f"{label} should be of type {primitivename}")

        return ret

    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")

        # run a filter over all items
        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
            loop.also(_getFilterBlock(itemvar, f"{label}[$n]", itemspec, names))

        return ret

    if isinstance(spec, DictTypeSpec):
        ret = Statements()

        # make sure the thing came back as a dict
        with ret.withCond(pyexpr(f'not isinstance({var_or_prop}, dict)')) as cond:
            cond.alsoRaise("TypeError", msg=f"{var_or_prop} should be of type dict")

        # make sure all dict keys/values have the correct type
        keyspec = spec.keySpec
        assert isinstance(keyspec, ScalarTypeSpec)
        assert keyspec.originalType is str
        valuespec = spec.valueSpec
        valuevar = names.getNewName(var_or_prop, 'value', False)
        with ret.withFor(PanVar(valuevar, None), pyexpr(f'{var_or_prop}.values()')) as loop:
            # TODO: also if we want to provide meaningful error messages, we really want to know
            # the key of the item that was broken and include it in the error message
            loop.also(_getFilterBlock(valuevar, f"{label}[$key]", valuespec, names))

        return ret

    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
        simpleexprs: List[str] = []

        for vspec in spec.variants:
            nomatchexpr = _getTypeNoMatchExpr(var_or_prop, vspec)
            if nomatchexpr is None:
                raise _FilterNotPossible(
                    "UnionTypeSpec contains non-simple values and needs a converter block"
                )

            simpleexprs.append(nomatchexpr)

        assert simpleexprs

        # if they were all simple, we can use a single negative-if to rule out some invalid types
        ret = Statements()

        with ret.withCond(pyexpr(' and '.join(simpleexprs))) as cond:
            cond.alsoRaise("TypeError", msg=f"{var_or_prop} did not match any variant")

        return ret

    raise _FilterNotPossible(
        f"Not possible to build a Filter block for {spec}"
    )
Beispiel #9
0
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
Beispiel #10
0
 def fromPanVar(cls, var: PanVar) -> "DictBuilderStatement":
     vartype = var.getPanType()
     assert isinstance(vartype, CrossDict)
     return cls(var, vartype.getKeyType(), vartype.getValueType())
Beispiel #11
0
class DictBuilderStatement(Statement):
    _var: PanVar
    _type: CrossType

    @classmethod
    def fromPanVar(cls, var: PanVar) -> "DictBuilderStatement":
        vartype = var.getPanType()
        assert isinstance(vartype, CrossDict)
        return cls(var, vartype.getKeyType(), vartype.getValueType())

    def __init__(self, var: Union[str, PanVar], keytype: FlexiType,
                 valtype: FlexiType) -> None:
        super().__init__()

        keytype = unflex(keytype)
        assert isinstance(keytype,
                          CrossStr), "Only str keys are currently supported"
        realtype = CrossDict(keytype, unflex(valtype))

        if isinstance(var, str):
            self._var = PanVar(var, realtype)
        else:
            self._var = var
        self._type = realtype

        self._keys: List[Tuple[str, bool]] = []

    def getImportsPy(self) -> Iterable[ImportSpecPy]:
        yield 'typing', None

    def getImportsTS(self) -> Iterable[ImportSpecTS]:
        return []

    def getImportsPHP(self) -> Iterable[ImportSpecPHP]:
        return []

    def addPair(self, key: str, allowomit: bool) -> None:
        self._keys.append((key, allowomit))

    def writepy(self, w: FileWriter) -> None:
        inner = ', '.join(
            [f'{k!r}: {k}' for k, allowomit in self._keys if not allowomit])

        varstr = self._var.getPyExpr()[0]

        w.line0(f'{varstr}: {self._type.getQuotedPyType()} = {{{inner}}}')

        # now do the omittable args
        for k, allowomit in self._keys:
            if allowomit:
                # FIXME: this isn't how we want to do omitted args - we should be doing ellipsis
                expr = pannotomit(PanVar(k, None))
                w.line0(f'if {expr.getPyExpr()[0]}:')
                w.line1(f'{varstr}[{k!r}] = {k}')

    def writets(self, w: FileWriter) -> None:
        inner = ', '.join(
            [f'{k!r}: {k}' for k, allowomit in self._keys if not allowomit])

        varstr = self._var.getTSExpr()[0]

        w.line0(f'let {varstr}: {self._type.getTSType()[0]} = {{{inner}}};')

        # now do the omittable args
        for k, allowomit in self._keys:
            if allowomit:
                w.line0(f'if (typeof {k} !== "undefined") {{')
                w.line1(f'{varstr}[{k!r}] = {k};')
                w.line0(f'}}')

    def writephp(self, w: FileWriter) -> None:
        # TODO: don't import this here
        from paradox.expressions import _phpstr

        phptype = self._type.getPHPTypes()[0]
        if phptype:
            w.line0(f'/** @var {phptype} */')

        inner = ', '.join([
            _phpstr(k) + ' => $' + k for k, allowomit in self._keys
            if not allowomit
        ])

        varstr = self._var.getPHPExpr()[0]

        w.line0(f'{varstr} = [{inner}];')

        # now do the omittable args
        for k, allowomit in self._keys:
            raise Exception("omittable args aren't supported by PHP")