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