Skip to content

type_bridge.models.role

role

Role descriptor for TypeDB relation role players.

Role

Role(role_name, player_type, *additional_player_types, cardinality=None)

Descriptor for relation role players with type safety.

Generic type T represents the type (Entity or Relation) that can play this role. TypeDB supports both entities and relations as role players.

Example

Entity as role player

class Employment(Relation): employee: Role[Person] = Role("employee", Person) employer: Role[Company] = Role("employer", Company)

Relation as role player

class Permission(Relation): permitted_subject: Role[Subject] = Role("permitted_subject", Subject) permitted_access: Role[Access] = Role("permitted_access", Access) # Access is a Relation

Initialize a role.

Parameters:

Name Type Description Default
role_name str

The name of the role in TypeDB

required
player_type type[T]

The type (Entity or Relation) that can play this role

required
additional_player_types type[T]

Optional additional types allowed to play this role

()
cardinality Card | None

Optional cardinality constraint for the role (e.g., Card(2, 2) for exactly 2)

None

Raises:

Type Description
ReservedWordError

If role_name is a TypeQL reserved word

TypeError

If player type is a library base class (Entity, Relation, TypeDBType)

Source code in type_bridge/models/role.py
def __init__(
    self,
    role_name: str,
    player_type: type[T],
    *additional_player_types: type[T],
    cardinality: Card | None = None,
):
    """Initialize a role.

    Args:
        role_name: The name of the role in TypeDB
        player_type: The type (Entity or Relation) that can play this role
        additional_player_types: Optional additional types allowed to play this role
        cardinality: Optional cardinality constraint for the role (e.g., Card(2, 2) for exactly 2)

    Raises:
        ReservedWordError: If role_name is a TypeQL reserved word
        TypeError: If player type is a library base class (Entity, Relation, TypeDBType)
    """
    # Validate role name doesn't conflict with TypeQL reserved words
    validate_reserved_word(role_name, "role")

    self.role_name = role_name
    self.cardinality = cardinality
    unique_types: list[type[T]] = []
    for typ in (player_type, *additional_player_types):
        # Validate that we're not using library base classes directly
        self._validate_player_type(typ)
        if typ not in unique_types:
            unique_types.append(typ)

    if not unique_types:
        # Should be impossible because player_type is required, but keeps type checkers happy
        raise ValueError("Role requires at least one player type")

    self.player_entity_types: tuple[type[T], ...] = tuple(unique_types)
    first_entity_type = unique_types[0]
    self.player_entity_type = first_entity_type
    # Get type name from the entity class(es)
    self.player_types = tuple(pt.get_type_name() for pt in self.player_entity_types)
    self.player_type = first_entity_type.get_type_name()
    self.attr_name: str | None = None

is_multi_player property

is_multi_player

Check if this role allows multiple players.

Returns True if cardinality allows more than one player (max > 1 or unbounded).

__set_name__

__set_name__(owner, name)

Called when role is assigned to a class.

Source code in type_bridge/models/role.py
def __set_name__(self, owner: type, name: str) -> None:
    """Called when role is assigned to a class."""
    self.attr_name = name

__get__

__get__(obj: None, objtype: type) -> RoleRef[T]
__get__(obj: Any, objtype: type) -> T
__get__(obj, objtype)

Get role player from instance or RoleRef from class.

When accessed from the class (obj is None), returns RoleRef for type-safe query building (e.g., Employment.employee.age.gt(Age(30))). When accessed from an instance, returns the entity playing the role.

Source code in type_bridge/models/role.py
def __get__(self, obj: Any, objtype: type) -> T | RoleRef[T]:
    """Get role player from instance or RoleRef from class.

    When accessed from the class (obj is None), returns RoleRef for
    type-safe query building (e.g., Employment.employee.age.gt(Age(30))).
    When accessed from an instance, returns the entity playing the role.
    """
    if obj is None:
        from type_bridge.fields.role import RoleRef

        return RoleRef(
            role_name=self.role_name,
            player_types=self.player_entity_types,
        )
    return obj.__dict__.get(self.attr_name)

__set__

__set__(obj, value)

Set role player(s) on instance.

For roles with cardinality > 1, accepts a list of entities. For single-player roles, accepts a single entity.

Source code in type_bridge/models/role.py
def __set__(self, obj: Any, value: T | list[T]) -> None:
    """Set role player(s) on instance.

    For roles with cardinality > 1, accepts a list of entities.
    For single-player roles, accepts a single entity.
    """
    if isinstance(value, list):
        # Multi-player role - validate each item in the list
        if not self.is_multi_player:
            raise TypeError(
                f"Role '{self.role_name}' does not allow multiple players. "
                f"Use cardinality=Card(...) to enable multi-player roles."
            )
        for item in value:
            if not isinstance(item, self.player_entity_types):
                allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
                raise TypeError(
                    f"Role '{self.role_name}' expects types ({allowed}), "
                    f"got {type(item).__name__} in list"
                )
        obj.__dict__[self.attr_name] = value
    else:
        # Single player
        if not isinstance(value, self.player_entity_types):
            allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
            raise TypeError(
                f"Role '{self.role_name}' expects types ({allowed}), got {type(value).__name__}"
            )
        obj.__dict__[self.attr_name] = value

multi classmethod

multi(role_name, player_type, *additional_player_types, cardinality=None)

Define a role playable by multiple entity types.

Parameters:

Name Type Description Default
role_name str

The name of the role in TypeDB

required
player_type type[T]

The first entity type that can play this role

required
additional_player_types type[T]

Additional entity types allowed to play this role

()
cardinality Card | None

Optional cardinality constraint for the role

None
Source code in type_bridge/models/role.py
@classmethod
def multi(
    cls,
    role_name: str,
    player_type: type[T],
    *additional_player_types: type[T],
    cardinality: Card | None = None,
) -> Role[T]:
    """Define a role playable by multiple entity types.

    Args:
        role_name: The name of the role in TypeDB
        player_type: The first entity type that can play this role
        additional_player_types: Additional entity types allowed to play this role
        cardinality: Optional cardinality constraint for the role
    """
    if len((player_type, *additional_player_types)) < 2:
        raise ValueError("Role.multi requires at least two player types")
    return cls(role_name, player_type, *additional_player_types, cardinality=cardinality)

__get_pydantic_core_schema__ classmethod

__get_pydantic_core_schema__(source_type, handler)

Define how Pydantic should validate Role fields.

Accepts either: - A single entity instance for single-player roles - A list of entity instances for multi-player roles (cardinality > 1)

Uses a custom validator that checks class names instead of isinstance, to handle generated code in different modules where the same class name exists but as a different Python object.

Source code in type_bridge/models/role.py
@classmethod
def __get_pydantic_core_schema__(
    cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
    """Define how Pydantic should validate Role fields.

    Accepts either:
    - A single entity instance for single-player roles
    - A list of entity instances for multi-player roles (cardinality > 1)

    Uses a custom validator that checks class names instead of isinstance,
    to handle generated code in different modules where the same class name
    exists but as a different Python object.
    """
    import types

    from pydantic_core import core_schema

    # Extract the entity type(s) from Role[T]
    # Handle both Role[Entity] and Role[Entity1 | Entity2] unions
    allowed_names: set[str] = set()

    if hasattr(source_type, "__args__") and source_type.__args__:
        for arg in source_type.__args__:
            # Check if it's a union type (e.g., Document | Email)
            if isinstance(arg, types.UnionType) or (
                hasattr(arg, "__origin__") and arg.__origin__ is type(int | str)
            ):
                # It's a union - get the individual types
                if hasattr(arg, "__args__"):
                    for union_arg in arg.__args__:
                        if hasattr(union_arg, "__name__"):
                            allowed_names.add(union_arg.__name__)
            elif hasattr(arg, "__name__"):
                allowed_names.add(arg.__name__)

    def validate_role_player(value: Any) -> Any:
        """Validate that value is an allowed entity type by class name.

        Checks the full inheritance chain (MRO) to support subclasses.
        E.g., if Document is allowed and Report is a subclass of Document,
        Report instances are accepted.
        """
        if not allowed_names:
            # No type constraints - allow anything
            return value

        def is_allowed_type(obj: Any) -> bool:
            """Check if obj's class or any base class matches allowed names."""
            # Check entire MRO (Method Resolution Order) for inheritance support
            for cls in type(obj).__mro__:
                if cls.__name__ in allowed_names:
                    return True
            return False

        if isinstance(value, list):
            # List of entities for multi-player roles
            for item in value:
                if not is_allowed_type(item):
                    raise ValueError(
                        f"Expected one of {allowed_names}, got {type(item).__name__}"
                    )
            return value
        else:
            # Single entity
            if not is_allowed_type(value):
                raise ValueError(f"Expected one of {allowed_names}, got {type(value).__name__}")
            return value

    return core_schema.no_info_plain_validator_function(validate_role_player)