I was trying to create an id, but I don’t think this is expressible in python’s type system right now. Before making any feature requests, I figured I’d ask if there’s something here that I’m missing.
Here’s what I’m doing
class Meta(type):
def __new__(cls, name, bases, class_dict, prefix: str):
cls.prefix = prefix
return super().__new__(cls, name, bases, class_dict)
class Foo(metaclass=Meta, prefix="foo"):
pass
@dataclass
class Id[T]:
value: str
# This doesn't work, just stating what I'd like to be able to do
@classmethod
def generate[U, type[U]: Meta](cls, meta_cls: type[U]) -> Id[U]:
return cls(meta_cls.prefix)
The problem seems to be that I have no way of constraining the type of a type variable. Any ideas for doing it?
I think you are overcomplicating this. You want an instance of Meta, not the Meta class being passed in, since prefix gets set on the instance, so wrapping it in type again is not doing what you want.
For the specific api I’m trying to build, I don’t actually have an instance of the class. I do actually just want the type of the class. In fact, the way that I implemented this, only the class has access to the prefix and the length.
There a small bug here with the metaclass as well that I needed to fix so that the prefix is set on the class instead of the metaclass instance:
class _Unset:
pass
_UNSET = _Unset
class Meta(type):
def __new__(cls, name, bases, class_dict, prefix: str | _Unset = _UNSET):
# conditional needed for dataclasses for some reason
if not isinstance(prefix, _Unset):
class_dict["__id_prefix__"] = prefix
return super().__new__(cls, name, bases, class_dict)
The way that I handle generate is to simply use a cast. Not ideal, but it’s the best I can think of with my knowledge of python’s current feature set.
Or is perhaps the part that’s bothering you, that you will end up with Id[type[Foo]] rather than Id[Foo]?
That part makes a lot more sense to me. You could probably use a callback protocol to achieve most of the effect, since you only seem to care about the attribute being there, not necessarily having access to the class itself.
But the best solution is probably to get rid of the metaclass entirely. You’re not doing anything that can’t be handled by __init_subclass__.
class Prefixed:
def __init_subclass__(cls, prefix: str, **kwargs):
super().__init_subclass__(**kwargs)
cls.prefix = prefix
class Foo(Prefixed, prefix="foo"):
pass
@dataclass
class Id[T]:
value: str
@classmethod
def generate[U: Prefixed](cls, prefixed_cls: type[U]) -> Id[U]:
return cls(prefixed_cls.prefix)
Yup, I didn’t want the extra type in the id. And yes, I did end up at using a class + inheritence instead of a metaclass myself, but my version was way more complicated. Specifically, I was using a function to generate the class. Your way is so much cleaner. Thanks. That helps a lot!