最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

metaprogramming - Can a metaclass be used as a type hint for a class instance in Python 3.12+? - Stack Overflow

programmeradmin4浏览0评论

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]
发布评论

评论列表(0)

  1. 暂无评论