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 and optional_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

mro

Return a strcs.MRO instance from self.extracted

This is memoized.

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 a tp.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 any None.

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 return self.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’t strcs.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 the strcs.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 the strcs.InstanceCheck for this object.

ann

Return an object that fulfills strcs.AdjustableMeta or strcs.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 or strcs.MergedMetaAnnotation or a simple callable then a strcs.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 this strcs.Type

checkable

Return an instance of strcs.InstanceCheck for this instance.

This is memoized.

class strcs.TypeCache

The TypeCache is used to memoize the strcs.Type objects that get created because the creation of strcs.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

type_cache: TypeCache
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 a strcs.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 the MRO 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’s for_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 a strcs.Type. This object can be used wherever you’d otherwise want to treat the strcs.Type object as a python type 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 the extracted object from strcs.Type

  • typing.get_origin

  • typing.get_args

  • typing.get_type_hints unless calling this with the extracted raises a TypeError. 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 of disassembled.origin

  • If typing.get_origin(extracted) is a type, then that

  • otherwise if extracted is a type, then that

  • otherwise 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 to isinstance(obj, check_against)

  • obj == checkable == obj == check_against

  • checkable(...) is equivalent to extracted(...)

  • issubclass(obj, checkable) is equivalent to issubclass(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 with None 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 union

  • isinstance(obj, checkable) will return true if the object is equivalent for any of the types in the union

  • obj == checkable == any(obj == ch for ch in check_against)

  • checkable(...) will raise an error

  • issubclass(obj, checkable) is equivalent to issubclass(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 with None

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 wrapping typing.Annotation

without_optional: object

The original object given to strcs.Type without a wrapping Optional

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.

clone() Field[T]

Return a clone of this object.

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 the disassembled_type field.

with_replaced_type(typ: Type[U]) Field[U]

Return a clone of this field, but with the provided type.

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 or factory 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 or default_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 the score property on the strcs.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.