In Trio we're pretty careful about our public API, because if it takes off then we want to defend ourselves against Hyrum's law. And because some of its internal state really is pretty delicate.
One consequence: we're kind of protective of some internal classes. For example, nurseries and cancel scopes both originally had no public class at all, because we didn't want to have a public constructor, or allow subclassing. (Even if not everyone shares my opinion that subclassing is a bad idea in general, I think it is at least generally agreed that you shouldn't subclass a type unless that type was intentionally written to enable subclassing, and these types definitely were not.)
Now, cancel scopes have gained a public constructor, and become a public class. And we want to make nurseries a public class too, to enable type annotations (#778). And I forget where now, but I saw someone post some sample code that subclassed CancelScope, and it was scary :-). So, we need to rethink this.
So, proposal: Come up with one metaclass/decorator/something that prevents subclassing, and one that prevents subclassing + hides the constructor. Use these for CancelScope, Nursery, etc. (And maybe everything else too!)
raiseTypeError("subclassing not supported")
You could also easily define a @final decorator to inject this into a class when it's defined. PEP 591 proposes to add a similar @final decorator, which works statically, rather than dynamically. You could combine the two using something like:
It's not as easy to inject a metaclass using a decorator (it requires reconstructing the class object), but it's easy to use like:
No public constructor
Say we have a normal class:
When we call NormalClass(...), that invokes type(NormalClass).__call__(...) (this is the same as calling any object in python!). Since type(NormalClass is type, this does type.__call__. And then type.__call__ implements all the logic we expect: calling NormalClass.__new__ and NormalClass.__init__, etc.
Now, we want to define a special class, where SpecialClass(...) raises an exception, but we can still construct new instances of SpecialClass – maybe via SpecialClass._create(...).
def__call__(self, *args, **kwargs):
raiseTypeError("no public constructor")
def_create(self, *args, **kwargs):
# the class body looks totally normal# you just have to construct it using SpecialClass._create
On 3.6+, then it's pretty easy to treat these as orthogonal: you can use a decorator to inject __init_subclass__, and then separately use metaclass=NoPublicConstructor. And if PEP 591 is accepted then this is probably ideal. So defining a class with both features would look like:
To support 3.5, they both have to use a metaclass, and a class can only have one metaclass. In practice, everywhere we want to use NoPublicConstructor, we also want to forbid subclassing. So the simplest thing would be to make NoPublicConstructor a subclass of Final (ironic!). And then you'd use them like:
# Final class with public firstname.lastname@example.org# purely for mypy's benefitclassFinalClass(metaclass=Final):
# Final class with no public email@example.com# purely for mypy's benefitclassExtraSpecialClass(metaclass=NoPublicConstructor):
The text was updated successfully, but these errors were encountered:
Adding the good first issue tag... it's a bit more complicated than we usually like for good first issues, but with pycon sprints coming up I want to err on the side of having more issues tagged rather than less :-)
We have these metaclasses now (in #1043). There are still a ton of places where we should be using them where we aren't yet, but that's an ongoing project and we have #1044 for that, so I guess I'll close this in favor of #1044.