Introspecting python types and their annotations
Within the strcs
package are a bunch of code for understanding python
type annotations and class objects. These are used to introspect the types
that are provided when creating objects and those found on the fields
of classes.
The main entry point for these helpers is via strcs.Type
objects which
use the other helpers to normalise the complexities inherit in the way python
type annotations work at runtime.
This includes the ability to understand and extract from optionals and annotations, the ability to understand type vars and how they work across inheritance, the ability to order type annotations by complexity, and the ability to determine the fields defined by a class.
Types
- class strcs.Type
Wraps any object to provide an interface for introspection. Used to represent python types and type annotations.
Usage is:
import strcs type_cache = strcs.TypeCache() typ = strcs.Type.create(int, cache=type_cache) typ2 = strcs.Type.create(int | None, cache=type_cache) typ3 = type_cache.disassemble(int | None) typ4 = typ.disassemble(int | None) ...
- class Missing
A representation for the absence of a type. (Used when understanding type variables)
- disassemble: Disassembler
Object for creating new Type classes without having to pass around the type cache
- original: object
The original object being wrapped
- extracted: T
The extracted type if this object is optional or annotated or a
typing.NewType
object
- type_alias: NewType | None
The type alias used to reference this type if created with one
- optional_inner: bool
True when the object is an annotated optional
- optional_outer: bool
True when the object is an optional
- annotated: strcs.disassemble._extract.IsAnnotated | None
The typing.Annotated object if this object is annotated
- annotations: collections.abc.Sequence[object] | None
The metadata in the annotation if the object is a typing.Annotated
- classmethod create(typ: object, *, expect: type[U] | None = None, cache: TypeCache) Type[U]
Used to create a
strc.Type
.The
expect
parameter is purely for making it easier to say what type this is wrapping.
- for_display() str
Return a string that will look close to how the developer writes the original object.
- reassemble(resolved: object, *, with_annotation: bool = True, with_optional: bool = True) object
Return a type annotation for the provided object using the optional and annotation on this instance.
This takes into account both
optional_inner
andoptional_outer
.This method takes in arguments to say whether to include annotation and optional or not.
- property is_annotated: bool
True if this object was annotated
- property is_type_alias: bool
True if this object is a
typing.NewType
object
- property optional: bool
True if this object has either inner or outer optional
- origin
if this type was created using a
tp.NewType
object, then that is returned.Otherwise if
typing.get_origin(self.extracted)
is a python type, then return that.Otherwise if
self.extracted
is a python type then return that.Otherwise return
type(self.extracted)
This is memoized.
- origin_type
Gets the result of
self.origin
. If the result is atp.NewType
then the type represented by that alias is returned, otherwise the origin is.This is memoized.
- is_union
True if this type is a union. This works for the various ways it is possible to create a union typing annotation.
This is memoized.
- without_optional
Return a
strcs.Type
for this instance but without any optionals.This is memoized.
- without_annotation
Return a
strcs.Type
for this instance but without any annotation.This is memoized.
- nonoptional_union_types
Return a tuple of
strcs.Type
objects represented by this object except for anyNone
.Return an empty tuple if this object is not already a union.
This is memoized.
- score
Return a :class:
strcs.disassembled.Score
instance for this.This is memoized.
- relevant_types
Return a sequence of python types relevant to this instance.
This includes
type(None)
if this is optional.If this is a union, then returns all the types in that union, otherwise will contain only
self.extracted
if that is already a python type.This is memoized.
- property has_fields: bool
Return if this is an object representing a class with fields.
- property fields_from: object
Return the object to get fields from.
If this is a union, return
self.extracted
, otherwise returnself.origin
- fields_getter
Return an appropriate function used to get fields from
self.fields_from
.Will be
strcs.disassemble.fields_from_attrs()
if we are wrapping an attrs class.A
strcs.disassemble.fields_from_dataclasses()
if wrapping a dataclasses class.Or a
strcs.disassemble.fields_from_class()
if we are wrapping a python type that isn’tstrcs.NotSpecifiedMeta
or a builtin type.Otherwise
None
This is memoized and all callables are returned as a partial passing in the type cache on this instance.
- raw_fields
Return a sequence of fields for this type without resolving any type vars.
Will return an empty list if this Type is not for something with fields.
This is memoized.
- property fields: Sequence[Field]
Return a sequence of fields for this type after resolving any type vars.
This property itself isn’t memoized, but it’s using a memoized property on
self.mro
.Will return an empty list if this is a union.
- find_generic_subtype(*want: type) Sequence[Type]
Match provided types with the filled type vars.
This lets the user ask for the types on this typing annotation whilst also checking those types match expected types.
- is_type_for(instance: object) TypeGuard[T]
Whether this type represents the type for some object. Uses the
isinstance
check on thestrcs.InstanceCheck
for this object.
- is_equivalent_type_for(value: object) TypeGuard[T]
Whether this type is equivalent to the passed in value.
True if this type is the type for that value.
Otherwise true if the passed in value is a subclass of this type using
issubclass
check on thestrcs.InstanceCheck
for this object.
- ann
Return an object that fulfills
strcs.AdjustableMeta
orstrcs.AdjustableCreator
given any annotation on this type.If there is an annotation and it matches either those protocols already then it is returned as is.
If it’s a
strcs.MetaAnnotation
orstrcs.MergedMetaAnnotation
or a simple callable then astrcs.Ann
instance is made from it and that is returned.Note that currently only the first value in the annotation will be looked at.
This is memoized.
- resolve_types(*, _resolved: set[strcs.disassemble._base.Type] | None = None)
Used by
strcs.resolve_types
to resolve any stringified type annotations on the original/extracted on this instance.This function will modify types such that they are annotated with objects rather than strings.
- func_from(options: list[tuple['Type', 'ConvertFunction']]) Optional[ConvertFunction]
Given a list of types to creators, choose the most appropriate function to create this type from.
It will go through the list such that the most specific matches are looked at first.
There are two passes of the options. In the first pass subclasses are not considered matches. In the second pass they are.
- property checkable_as_type: type[T]
Return
self.checkable
, but the return type of this function is a python type of the inner type represented by thisstrcs.Type
- checkable
Return an instance of
strcs.InstanceCheck
for this instance.This is memoized.
- class strcs.TypeCache
The
TypeCache
is used to memoize thestrcs.Type
objects that get created because the creation ofstrcs.Type
objects is very deterministic.It can be treated like a mutable mapping:
# Note though that most usage should be about passing around a type cache # and not needing to interact with it directly. type_cache = strcs.TypeCache() typ = type_cache.disassemble(int) assert type_cache[int] is typ assert int in type_cache assert list(type_cache) == [(type, int)] # Can delete individual types del type_cache[int] assert int not in type_cache # Can clear all types type_cache.clear()
- disassemble: Disassembler
Used to create new Types using this type cache
- protocol strcs.disassemble.Disassembler
Used to disassemble some type using an existing type cache
Classes that implement this protocol must have the following methods / attributes:
- __call__(typ: type[U]) Type[U]
- __call__(typ: Type[U]) Type[U]
- __call__(typ: object) Type[object]
Used to disassemble some type using an existing type cache
Pass in expect to alter the type that the static type checker sees
- typed(expect: type[U], typ: object) Type[U]
Return a new
strcs.Type
for the provided object using this type cache and the expected type.
- class strcs.MRO
This class represents the type Hierarchy of an object.
Using this class it is possible to figure out the hierarchy of the underlying classes and the typevars for this object and all objects in it’s hierarchy.
As well as match the class against fields and determine closest type match for typevars.
An instance of this object may be created using the
create
classmethod:import my_code import strcs type_cache = strcs.TypeCache() mro = strcs.MRO.create(my_code.my_class, type_cache)
Or via the
mro
property on astrcs.Type
instance:import my_code import strcs type_cache = strcs.TypeCache() typ = type_cache.disassemble(my_code.my_class) mro = typ.mro
- class Referal(owner: type, typevar: TypeVar, value: object)
An
attrs
class used to represent when one type var is defined in terms of another.
- all_vars
Return an ordered tuple of all the type vars that make up this object taking into account type vars on this object as well as all inherited objects.
- classmethod create(start: object | None, type_cache: TypeCache) MRO
Given some object and a type cache, return a filled instance of the MRO.
- fields
Return a sequence of
strcs.Field
objects representing the fields on the object, after resolving the type vars.Type vars are resolved by understanding how type variables are filled out on the outer object, and how that relates to type variables being used to make up other type variables.
- find_subtypes(*want: type) Sequence[Type]
This method will take in a tuple of expected types and return a matching list of types from the type variables on the object.
The wanted types must be parents of the filled type variables and in the correct order for the function to not raise an error.
For example:
import typing as tp import strcs T = tp.TypeVar("T") class A: pass class B(A): pass class C(A): pass class One(tp.Generic[T]): pass type_cache = strcs.TypeCache() typ1 = type_cache.disassemble(One[B]) typ2 = type_cache.disassemble(One[C]) assert typ1.mro.find_subtypes(A) == (B, ) assert typ2.mro.find_subtypes(A) == (C, )
- raw_fields
Return a sequence of
strcs.Field
objects representing all the annotated fields on the class, including those it receives from it’s ancestors.These field objects will not contain converted types (any type variables are not resolved).
The
fields
method on theMRO
object will take these fields and resolve the type vars.
- signature_for_display
Return a string representing the filled types for this object.
For example:
import typing as tp import strcs T = tp.TypeVar("T") U = tp.TypeVar("U") class One(tp.Generic[T]): pass class Two(tp.Generic[U], One[U]): pass type_cache = strcs.TypeCache() typ1 = type_cache.disassemble(One) typ2 = type_cache.disassemble(One[int]) typ3 = type_cache.disassemble(Two[str]) assert typ1.mro.signature_for_display == "~T" assert typ2.mro.signature_for_display == "int" assert typ3.mro.signature_for_display == "str"
It is used by
strcs.Type
in it’sfor_display
method to give a string representation of the object it is wrapping.
- typevars
Return an ordered dictionary mapping all the typevars in this objects MRO with their value.
The keys to the dictionary are a tuple of (class, typevar).
So for example, getting the typevars for
Two
in the following example:import typing as tp import strcs T = tp.TypeVar("T") T = tp.TypeVar("U") class One(tp.Generic[T]): pass class Two(tp.Generic[U], One[U]): pass type_cache = strcs.TypeCache() typevars = type_cache.disassemble(Two).mro.typevars
Will result in having:
{ (Two, U): strcs.Type.Missing, (One, T): MRO.Referal(owner=Two, typevar=U, value=strcs.Type.Missing) }
And for
Two[str]
:{ (Two, U): str, (One, T): MRO.Referal(owner=Two, typevar=U, value=str) }
- class strcs.InstanceCheck
Returned from the
checkable
property on astrcs.Type
. This object can be used wherever you’d otherwise want to treat thestrcs.Type
object as a pythontype
regardless of whether it’s in a union, or annotated, or a generic or some other non-type python object.It will be different depending on whether the type is for a union or not.
In either cases, the checkable will also have a
Meta
object on it containing information.The behaviour of the checkable is based around the information on
Meta
For both
The following methods called with the
checkable
object will be equivalent to calling the function with theextracted
object fromstrcs.Type
typing.get_origin
typing.get_args
typing.get_type_hints
unless calling this with theextracted
raises aTypeError
. In those cases, an empty dictionary will be returned instead.attrs.fields
attrs.has
dataclasses.fields
dataclasses.isdataclass
type
hash
For objects that aren’t unions or an optional type
For these, the
check_against
will be the return ofdisassembled.origin
If
typing.get_origin(extracted)
is a type, then thatotherwise if
extracted
is a type, then thatotherwise
type(extracted)
The following behaviour is implemented:
repr(checkable)
==repr(check_against)
isinstance(obj, checkable)
will return true if the object is None and we are optional, otherwise it will is equivalent toisinstance(obj, check_against)
obj == checkable
==obj == check_against
checkable(...)
is equivalent toextracted(...)
issubclass(obj, checkable)
is equivalent toissubclass(obj, check_against)
and works with other checkable instances
For Unions
For these, the
check_against
will a tuple of checkable instances for each of the items in the union. Note that a union that is one item withNone
is considered an optional of that one item rather than a union.The following behaviour is implemented:
repr(checkable)
will return a pipe separated string of all the checkable instances for each non-none item in the unionisinstance(obj, checkable)
will return true if the object is equivalent for any of the types in the unionobj == checkable
==any(obj == ch for ch in check_against)
checkable(...)
will raise an errorissubclass(obj, checkable)
is equivalent toissubclass(obj, check_against)
and works with other checkable instances
Properties
- class Meta
- disassembled: Type
The original
strcs.Type
object
- extracted: object
The extracted object from the
strcs.Type
- optional: bool
True if the value being wrapped is a
typing.Optional
or Union withNone
- original: object
The original object wrapped by the
strcs.Type
- typ: object
Either the extracted type from the original or it’s
typing.get_origin
value if that’s not a type
- union_types: tuple['Type', ...] | None
A tuple of the types in the union if it’s a union, otherwise
None
- without_annotation: object
The original object given to
strcs.Type
without a wrappingtyping.Annotation
- without_optional: object
The original object given to
strcs.Type
without a wrappingOptional
Extraction
- class strcs.disassemble.IsAnnotated(*args, **kwargs)
Protocol class used to determine if an object is a typing.Annotated
Usage is:
import typing as tp typ = tp.Annotated[int, "one"] assert IsAnnotated.has(typ)
These kind of objects have three properties we care about specifically:
__args__
Will be a one item tuple with the first value the type was instantiated with
__metadata__
Will be a tuple of all other items passed into the annotated typ variable
copy_with(tuple) -> type
Returns a new annotated type variable with the first value of the passed in tuple as the type, with the existing metadata.
- strcs.disassemble.extract_optional(typ: T) tuple[bool, T]
Given a type annotation, return a boolean indicating whether the type is optional, and a new type without the optional:
from strcs.disassemble import extract_optional import typing as tp assert extract_optional(tp.Optional[int]) == (True, int) assert extract_optional(int | None) == (True, int) assert extract_optional(int | str | None) == (True, int | str) assert extract_optional(int) == (False, int) assert extract_optional(int | str) == (False, int | str) assert extract_optional(tp.Annotated[int | str | None, "one"]) == (False, tp.Annotated[int | str | None, "one"])
- strcs.disassemble.extract_annotation(typ: T) tuple[T, strcs.disassemble._extract.IsAnnotated | None, collections.abc.Sequence[object] | None]
Given a type annotation return it without any annotation with the annotation it had:
from strcs.disassemble import extract_annotation import typing as tp assert extract_annotation(int) == (int, None, None) assert extract_annotation(int | None) == (int | None, None, None) with_annotation = tp.Annotated[int, "one", "two"] assert extract_annotation(with_annotation) == (int, with_annotation, ("one", "two"))
Fields
- class strcs.Field
A container representing a single field on a class. Used to replicate the field functionality used in attrs and dataclasses.
- default: Optional[Callable[[], object | None]]
A callable returning the default value for the field
- disassembled_type: Type[T]
The type of the field in a
strcs.Type
object
- kind: int
The inspect.Parameter kind of the field
- name: str
The name of the field
- original_owner: object
The class that originally defined this field
- owner: object
The class that was being inspected to make this field
- property type: object
Return the original object used to make the
strcs.Type
in thedisassembled_type
field.
- strcs.disassemble.fields_from_class(type_cache: TypeCache, typ: type) Sequence[Field]
Given some class, return a sequence of
strcs.Field
objects.Done by looking at the signature of the object as if it were a callable.
- strcs.disassemble.fields_from_attrs(type_cache: TypeCache, typ: type) Sequence[Field]
Given some attrs type, return a sequence of
strcs.Field
objects.Take into account when a field has
default
orfactory
options.Also take into account field aliases, as well as underscore and double underscore prefixed fields.
- strcs.disassemble.fields_from_dataclasses(type_cache: TypeCache, typ: type) Sequence[Field]
Given some dataclasses.dataclass type return a sequence of
strcs.Field
objects.Take into account when fields have
default
ordefault_factory
options.
Scores
It is useful to be able to sort type annotations by complexity so that when determining if a creator should be used for a particular type, more specific creators are considered before less specific creators.
To achieve this, strcs
has strcs.disassemble.Score
objects that are
returned from the score
property on a strcs.Type
and are used when
sorting a sequence of strcs.Type
objects.
- class strcs.disassemble.Score
A score is a representation of the complexity of a type. The more data held by this object, the more complex the type is.
The order of these fields indicate how important they are in determining whether any type is more complex than another.
- type_alias_name: str
Name provided to the type if it’s a
typing.NewType
object
- annotated_union: tuple[strcs.disassemble._score.Score, ...]
If this object is an annotated union, then annotated_union will contain the scores of each part of the union instead of self.union
This is so that an optional union with an annotation is considered more complex than one without an annotation
- union_optional: bool
Whether this is a union that contains a None
- union_length: int
How many items are in the union
- union: tuple[strcs.disassemble._score.Score, ...]
The scores of each part that makes up the union, or empty if not a union or is an annotated union
- annotated: bool
Whether this type is annotated
- custom: bool
Whether this type is user defined
- optional: bool
Whether this type is optional
- mro_length: int
How many items are in the mro of the type
- typevars_length: int
How many type vars are defined for the type if it is a generic
- typevars_filled: tuple[bool, ...]
A boolean in order for each type var saying whether that type var has a value or not
- typevars: tuple[strcs.disassemble._score.Score, ...]
A score for each type var on the type
- origin_mro: tuple[strcs.disassemble._score.ScoreOrigin, ...]
A score origin for each object in the mro of the type
- classmethod create(typ: Type) Score
Used to create a score for a given
strcs.Type
. This is used by thescore
property on thestrcs.Type
object.
- for_display(indent=' ') str
Return a human readable string representing the score.
- class strcs.disassemble.ScoreOrigin
A container used by
strcs.Score
for data related to the MRO of a type- custom: bool
Whether this class is user defined (True) or standard library (False), decided by whether the module is ‘builtins’
- package: str
The package this comes from
- module: str
The module this comes from
- name: str
The name of the class
- classmethod create(typ: type) ScoreOrigin
Used to create a ScoreOrigin from a python type.
- for_display(indent='') str
Return a human friendly string representing this ScoreOrigin.