The project I'm working on uses a somewhat complex typing system, I have a base class called RID which uses the metaclass RIDType. Custom RID types are classes which derive from RID:
class RIDType(ABCMeta):
...
class RID(metaclass=RIDType):
...
class CustomType(RID):
...
Essentially, RIDs are instances of classes deriving from RID, but I also need to use RID types which are instances of the RIDType metaclass. From what I've found online, type[RID]
would be the standard way of type hinting the RID class (or derived classes) not the instance. My question is whether it is supported to use my metaclass RIDType
as a type hint, since the classes would be considered instances of it.
This is relevant since the RID and RIDType class provide class methods which can instantiate RID instances or RID classes from a string. This is handled by lookup table in RIDType. It seems more "symmetrical" to use RIDType
instead of type[RID]
because developers would be calling methods like RIDType.from_string(...)
.
This is also relevant for a custom Pydantic validator which I previously implemented for RIDs as Annotated[RID, RIDFieldAnnotation]
, so I'm unsure whether I should define the validator for RID types as Annotated[RIDType, RIDTypeFieldAnnotation]
or Annotated[type[RID], RIDTypeFieldAnnotation]
.
Full class definitions here:
from abc import ABCMeta, abstractmethod
from . import utils
from .consts import (
BUILT_IN_TYPES,
NAMESPACE_SCHEMES,
ORN_SCHEME,
URN_SCHEME
)
class RIDType(ABCMeta):
scheme: str | None = None
namespace: str | None = None
# maps RID type strings to their classes
type_table: dict[str, type["RID"]] = dict()
def __new__(mcls, name, bases, dct):
"""Runs when RID derived classes are defined."""
cls = super().__new__(mcls, name, bases, dct)
# ignores built in RID types which aren't directly instantiated
if name in BUILT_IN_TYPES:
return cls
if not getattr(cls, "scheme", None):
raise TypeError(f"RID type '{name}' is missing 'scheme' definition")
if not isinstance(cls.scheme, str):
raise TypeError(f"RID type '{name}' 'scheme' must be of type 'str'")
if cls.scheme in NAMESPACE_SCHEMES:
if not getattr(cls, "namespace", None):
raise TypeError(f"RID type '{name}' is using namespace scheme but missing 'namespace' definition")
if not isinstance(cls.namespace, str):
raise TypeError(f"RID type '{name}' is using namespace scheme but 'namespace' is not of type 'str'")
# check for abstract method implementation
if getattr(cls, "__abstractmethods__", None):
raise TypeError(f"RID type '{name}' is missing implemenation(s) for abstract method(s) {set(cls.__abstractmethods__)}")
# save RID type to lookup table
mcls.type_table[str(cls)] = cls
return cls
@classmethod
def _new_default_type(mcls, scheme: str, namespace: str | None) -> type["RID"]:
"""Returns a new RID type deriving from DefaultType."""
if namespace:
name = "".join([s.capitalize() for s in namespace.split(".")])
else:
name = scheme.capitalize()
bases = (DefaultType,)
dct = dict(
scheme=scheme,
namespace=namespace
)
return type(name, bases, dct)
@classmethod
def from_components(mcls, scheme: str, namespace: str | None = None) -> type["RID"]:
context = utils.make_context_string(scheme, namespace)
if context in mcls.type_table:
return mcls.type_table[context]
else:
return mcls._new_default_type(scheme, namespace)
@classmethod
def from_string(mcls, string: str) -> type["RID"]:
"""Returns an RID type class from an RID context string."""
scheme, namespace, _ = utils.parse_rid_string(string, context_only=True)
return mcls.from_components(scheme, namespace)
def __str__(cls) -> str:
return utils.make_context_string(cls.scheme, cls.namespace)
# backwards compatibility
@property
def context(cls) -> str:
return str(cls)
class RID(metaclass=RIDType):
scheme: str | None = None
namespace: str | None = None
@property
def type(self):
return self.__class__
@property
def context(self):
return str(self.type)
def __str__(self) -> str:
return str(self.type) + ":" + self.reference
def __repr__(self) -> str:
return f"<{self.type.__name__} RID '{str(self)}'>"
def __eq__(self, other) -> bool:
if isinstance(other, self.__class__):
return str(self) == str(other)
else:
return False
def __hash__(self):
return hash(str(self))
@classmethod
def from_string(cls, string: str) -> "RID":
scheme, namespace, reference = utils.parse_rid_string(string)
return RIDType.from_components(scheme, namespace).from_reference(reference)
@abstractmethod
def __init__(self, *args, **kwargs):
...
@classmethod
@abstractmethod
def from_reference(cls, reference: str):
...
@property
@abstractmethod
def reference(self) -> str:
...
class ORN(RID):
scheme = ORN_SCHEME
class URN(RID):
scheme = URN_SCHEME
class DefaultType(RID):
def __init__(self, _reference):
self._reference = _reference
@classmethod
def from_reference(cls, reference):
return cls(reference)
@property
def reference(self):
return self._reference
# example RID type implementation
from rid_lib.core import ORN
from .slack_channel import SlackChannel
from .slack_workspace import SlackWorkspace
class SlackMessage(ORN):
namespace = "slack.message"
def __init__(
self,
team_id: str,
channel_id: str,
ts: str,
):
self.team_id = team_id
self.channel_id = channel_id
self.ts = ts
@property
def reference(self):
return f"{self.team_id}/{self.channel_id}/{self.ts}"
@property
def workspace(self):
return SlackWorkspace(
self.team_id
)
@property
def channel(self):
return SlackChannel(
self.team_id,
self.channel_id
)
@classmethod
def from_reference(cls, reference):
components = reference.split("/")
if len(components) == 3:
return cls(*components)
UPDATE: adding my Pydantic field validator code here too:
class RIDTypeFieldAnnotation:
@classmethod
def __get_pydantic_core_schema__(
cls,
_source_type: Any,
_handler: GetCoreSchemaHandler
) -> CoreSchema:
def validate_from_str(value: str) -> RIDType:
# str -> RID validator
return RIDType.from_string(value)
from_str_schema = core_schema.chain_schema(
[
core_schema.str_schema(),
core_schema.no_info_plain_validator_function(validate_from_str),
]
)
return core_schema.json_or_python_schema(
# str is valid type for JSON objects
json_schema=from_str_schema,
# str or RID are valid types for Python dicts
python_schema=core_schema.union_schema(
[
core_schema.is_instance_schema(RIDType),
from_str_schema
]
),
# RIDs serialized with __str__ function
serialization=core_schema.plain_serializer_function_ser_schema(str)
)
@classmethod
def __get_pydantic_json_schema__(
cls,
_core_schema: CoreSchema,
handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
return handler(core_schema.str_schema())
type RIDTypeField = Annotated[RIDType, RIDTypeFieldAnnotation]