Пример #1
0
def schema(obj: Type[ObjectT], *, primitive: bool = False) -> SchemaReturnT:
    """Get a JSON schema for object for the given object.

    Parameters
    ----------
    obj
        The class for which you wish to generate a JSON schema
    primitive
        Whether to return an instance of :py:class:`typic.schema.ObjectSchemaField` or
        a "primitive" (dict object).

    Examples
    --------
    >>> import typic
    >>>
    >>> @typic.klass
    ... class Foo:
    ...     bar: str
    ...
    >>> typic.schema(Foo)
    ObjectSchemaField(title='Foo', description='Foo(bar: str)', properties={'bar': StrSchemaField()}, additionalProperties=False, required=('bar',))
    >>> typic.schema(Foo, primitive=True)
    {'type': 'object', 'title': 'Foo', 'description': 'Foo(bar: str)', 'properties': {'bar': {'type': 'string'}}, 'additionalProperties': False, 'required': ['bar'], 'definitions': {}}

    """
    if obj in {FunctionType, MethodType}:
        raise ValueError("Cannot build schema for function or method.")

    annotation = resolver.resolve(obj)
    schm = schema_builder.get_field(annotation)
    try:
        setattr(obj, SCHEMA_NAME, schm)
    except (AttributeError, TypeError):
        pass
    return cast(SchemaReturnT, schm.primitive() if primitive else schm)
Пример #2
0
 def _handle_union(
     self,
     anno: Annotation,
     ro: Optional[bool],
     wo: Optional[bool],
     name: Optional[str],
     parent: Optional[Type],
 ):
     fields: List[SchemaFieldT] = []
     args = get_args(anno.un_resolved)
     for t in args:
         if t.__class__ is ForwardRef or t is parent:
             n = name or get_name(t)
             fields.append(Ref(f"#/definitions/{n}"))
             continue
         fields.append(
             self.get_field(resolver.resolve(t, namespace=parent),
                            parent=parent))
     schema = self._check_optional(
         anno,
         MultiSchemaField(
             title=name and self.defname(anno.resolved, name=name),
             anyOf=(*fields, ),
         ),
         ro,
         wo,
         name,
     )
     self.__cache[anno] = schema
     return schema
Пример #3
0
def _resolve_class(
    cls: Type[ObjectT],
    *,
    strict: StrictModeT = STRICT_MODE,
    always: bool = True,
    jsonschema: bool = True,
    serde: SerdeFlags = None,
) -> Type[WrappedObjectT]:
    # Build the namespace for the new class
    strict = cast(bool, strict)
    protos = protocols(cls, strict=strict)
    if hasattr(cls, SERDE_FLAGS_ATTR):
        pserde: SerdeFlags = getattr(cls, SERDE_FLAGS_ATTR)
        serde = pserde.merge(serde) if serde else pserde
    serde = serde or SerdeFlags()
    ns: Dict[str, Any] = {
        SERDE_FLAGS_ATTR: serde,
        TYPIC_ANNOS_NAME: protos,
    }
    if jsonschema:
        ns["schema"] = classmethod(schema)
        schema_builder.attach(cls)

    # Frozen dataclasses don't use the native setattr
    # So we wrap the init. This should be fine,
    # just slower :(
    if isfrozendataclass(cls):
        ns["__init__"] = wrap(cls.__init__, strict=strict)
    # The faster way - create a new setattr that applies the protocol for a given attr
    else:
        trans = freeze({x: y.transmute for x, y in protos.items()})

        def setattr_typed(setter):
            @functools.wraps(setter)
            def __setattr_typed__(self, name, item, *, __trans=trans, __setter=setter):
                __setter(
                    self,
                    name,
                    __trans[name](item) if name in __trans else item,
                )

            return __setattr_typed__

        ns.update(
            **{
                ORIG_SETTER_NAME: _get_setter(cls),
                "__setattr__": setattr_typed(cls.__setattr__),
            }
        )

    for name, attr in ns.items():
        setattr(cls, name, attr)
    # Get the protocol
    proto: SerdeProtocol = resolver.resolve(cls, is_strict=strict)
    # Bind it to the new class
    _bind_proto(cls, proto)
    # Track resolution state.
    setattr(cls, "__typic_resolved__", True)
    return cls
Пример #4
0
    def _handle_mapping(self,
                        proto: SerdeProtocol,
                        parent: Type = None,
                        *,
                        name: str = None,
                        **extra) -> MutableMapping:
        anno = proto.annotation
        args = anno.args
        config = extra
        config["title"] = self.defname(anno.resolved, name=name)
        doc = getattr(anno.resolved, "__doc__", None)
        if doc not in _IGNORE_DOCS:
            config["description"] = doc

        constraints = cast("MappingConstraints", proto.constraints)
        attrs = (
            ("items", "properties"),
            ("patterns", "patternProperties"),
            ("key_dependencies", "dependencies"),
        )
        for src, target in attrs:
            items = getattr(constraints, src)
            if items:
                config[target] = {
                    nm: self.get_field(
                        resolver.resolve(it.type,
                                         is_optional=it.nullable,
                                         namespace=parent),
                        parent=parent,
                    )
                    for nm, it in items.items()
                }
        config["additionalProperties"] = not constraints.total
        if args:
            config["additionalProperties"] = self.get_field(resolver.resolve(
                args[-1], namespace=parent),
                                                            parent=parent)

        return config
Пример #5
0
 def _handle_array(self,
                   proto: SerdeProtocol,
                   parent: Type = None,
                   **extra) -> MutableMapping:
     anno = proto.annotation
     args = anno.args
     has_ellipsis = args[-1] is Ellipsis if args else False
     config = extra
     if has_ellipsis:
         args = args[:-1]
     if args:
         constrs = set(
             self.get_field(resolver.resolve(x, namespace=parent),
                            parent=parent) for x in args)
         config["items"] = (
             *constrs, ) if len(constrs) > 1 else constrs.pop()
         if anno.origin in {tuple, frozenset}:
             config["additionalItems"] = False if not has_ellipsis else None
         if anno.origin in {set, frozenset}:
             config["uniqueItems"] = True
     return config
Пример #6
0
def _resolve_class(
    cls: Type[ObjectT],
    *,
    strict: StrictModeT = STRICT_MODE,
    always: bool = None,
    jsonschema: bool = True,
    serde: SerdeFlags = None,
) -> Type[WrappedObjectT[ObjectT]]:
    # Build the namespace for the new class
    strict = cast(bool, strict)
    protos = protocols(cls, strict=strict)
    if hasattr(cls, SERDE_FLAGS_ATTR):
        pserde: SerdeFlags = getattr(cls, SERDE_FLAGS_ATTR)
        if isinstance(pserde, dict):
            pserde = SerdeFlags(**pserde)
        serde = pserde.merge(serde) if serde else pserde
    serde = serde or SerdeFlags()
    ns: Dict[str, Any] = {
        SERDE_FLAGS_ATTR: serde,
        TYPIC_ANNOS_NAME: protos,
    }
    frozen = isfrozendataclass(cls)
    always = False if frozen else always
    if always is None:
        warnings.warn(
            "Keyword `always` will default to `False` in a future version. "
            "You should update your code to either explicitly declare `always=True` "
            "or update your code to not assume values will be coerced when set.",
            category=UserWarning,
            stacklevel=5,
        )
        always = True
    if jsonschema:
        ns["schema"] = classmethod(schema)
        schema_builder.attach(cls)

    # Wrap the init if
    #   a) this is a "frozen" dataclass
    #   b) we only want to coerce on init.
    # N.B.: Frozen dataclasses don't use the native setattr and can't be updated.
    if always is False:
        ns["__init__"] = wrap(cls.__init__, strict=strict)
    # For 'always', create a new setattr that applies the protocol for a given attr
    else:
        trans = freeze({x: y.transmute for x, y in protos.items()})

        def setattr_typed(setter):
            @functools.wraps(setter)
            def __setattr_typed__(self, name, item, *, __trans=trans, __setter=setter):
                __setter(
                    self,
                    name,
                    __trans[name](item) if name in __trans else item,
                )

            return __setattr_typed__

        ns.update(
            **{
                ORIG_SETTER_NAME: _get_setter(cls),
                "__setattr__": setattr_typed(cls.__setattr__),
            }
        )

    for name, attr in ns.items():
        setattr(cls, name, attr)
    # Get the protocol
    proto: SerdeProtocol = resolver.resolve(cls, is_strict=strict)
    # Bind it to the new class
    _bind_proto(cls, proto)
    # Track resolution state.
    setattr(cls, "__typic_resolved__", True)
    return cast(Type[WrappedObjectT[ObjectT]], cls)