-
-
Notifications
You must be signed in to change notification settings - Fork 343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Let's get serious about handling private constructors and preventing subclassing #1021
Comments
Adding the |
Looking at #75, looks like support for 3.5 could actually be dropped soon? |
If by "soon" you mean "sometime after Debian releases Buster", then yes. |
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!)Preventing subclassing
In 3.6+, this is easy using PEP 487:
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:However, for now we still need to support Python 3.5, which is a bit trickier. On 3.5 it requires defining a custom metaclass. 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 invokestype(NormalClass).__call__(...)
(this is the same as calling any object in python!). Sincetype(NormalClass is type
, this doestype.__call__
. And thentype.__call__
implements all the logic we expect: callingNormalClass.__new__
andNormalClass.__init__
, etc.Now, we want to define a special class, where
SpecialClass(...)
raises an exception, but we can still construct new instances ofSpecialClass
– maybe viaSpecialClass._create(...)
.Combining them
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 usemetaclass=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 makeNoPublicConstructor
a subclass ofFinal
(ironic!). And then you'd use them like:The text was updated successfully, but these errors were encountered: