Skip to content
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

Changing cls.__bases__ must ensure proper metaclass inheritance #66118

Open
abusalimov mannequin opened this issue Jul 4, 2014 · 6 comments
Open

Changing cls.__bases__ must ensure proper metaclass inheritance #66118

abusalimov mannequin opened this issue Jul 4, 2014 · 6 comments
Labels
3.8 only security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@abusalimov
Copy link
Mannequin

abusalimov mannequin commented Jul 4, 2014

BPO 21919
Nosy @gvanrossum, @rhettinger, @tirkarthi

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = 'https://github.com/gvanrossum'
closed_at = None
created_at = <Date 2014-07-04.20:45:25.320>
labels = ['interpreter-core', 'type-bug', '3.8']
title = 'Changing cls.__bases__ must ensure proper metaclass inheritance'
updated_at = <Date 2018-09-28.18:34:40.580>
user = 'https://bugs.python.org/abusalimov'

bugs.python.org fields:

activity = <Date 2018-09-28.18:34:40.580>
actor = 'gvanrossum'
assignee = 'gvanrossum'
closed = False
closed_date = None
closer = None
components = ['Interpreter Core']
creation = <Date 2014-07-04.20:45:25.320>
creator = 'abusalimov'
dependencies = []
files = []
hgrepos = []
issue_num = 21919
keywords = []
message_count = 6.0
messages = ['222312', '236753', '237072', '326532', '326650', '326651']
nosy_count = 4.0
nosy_names = ['gvanrossum', 'rhettinger', 'abusalimov', 'xtreak']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue21919'
versions = ['Python 3.8']

@abusalimov
Copy link
Mannequin Author

abusalimov mannequin commented Jul 4, 2014

When a new class is constructed Python checks for possible metaclass conflicts within bases and an explicitly specified one, if any, choosing the best available (the most specialized) one. That is the following implication is expected:

issubclass(B, A) => issubclass(type(B), type(A))

However, changing __bases__ attribute can break this invariant silently without an error.

>>> class O(object):
...     pass
... 
>>> class M(type):
...     pass
... 
>>> class N(type):
...     pass
... 
>>> class A(O, metaclass=M):
...     pass
... 
>>> class B(O, metaclass=N):
...     pass
... 
>>> B.__bases__ = (A,)
>>> B.__mro__
(<class '__main__.B'>, <class '__main__.A'>, <class '__main__.O'>, <class 'object'>)
>>> type(B)
<class '__main__.N'>
>>> type(A)
<class '__main__.M'>
>>> issubclass(B, A)
True
>>> issubclass(type(B), type(A))
False

Trying to derive from B now makes things look pretty weird:

>>> class C(A, metaclass=N):
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
>>> class D(B, A): pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
>>> class E(B, metaclass=N):
...     pass
... 
>>> type(E)
<class '__main__.N'>

That is one can extend a class but not its base (and not a class along its base). This effectively allows to bypass metaclass checks (by introducing a dummy class with the default metaclass, deriving it from a desired class with an inappropriate metaclass by changing __bases__ and using it instead of the desired class).

This behavior is observed in 2.7, 3.2 and 3.4.

I would expect the same check for metaclass conflicts when changing __bases__ as upon creating a new class:

>>> # EXPECTED:
... 
>>> B.__bases__ = (A,)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

@abusalimov abusalimov mannequin added interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error labels Jul 4, 2014
@BreamoreBoy
Copy link
Mannequin

BreamoreBoy mannequin commented Feb 27, 2015

@eldar sorry that this issue slipped our net.

@abusalimov
Copy link
Mannequin Author

abusalimov mannequin commented Mar 2, 2015

@mark, that OK, for the issue without a patch. :)

I could make a patch, but I'm not sure whether the proposed behavior is right. It could be considered arguable, I guess...

@tirkarthi
Copy link
Member

Thanks for the report and patience. This behavior is still reproducible on master as of f55c64c . I am adding Raymond as part of triaging who might have a better explanation about this. Raymond, feel free to remove yourself if this is not relevant.

A slightly cleaned up version of the program with repl statements removed for reference :

class O(object):
     pass

class M(type):
     pass

class N(type):
     pass

class A(O, metaclass=M):
     pass

class B(O, metaclass=N):
     pass

print(B.__bases__)
print(B.__mro__)

print(type(B))
print(type(A))
print(issubclass(type(B), type(A)))

class C(A, metaclass=N):
     pass

class D(B, A):
     pass

class E(B, metaclass=N):
     pass

@rhettinger
Copy link
Contributor

I am adding Raymond as part of triaging who might have a better explanation about this.

Guido, is this something we care about? There is value in checking for metaclass conflicts when a class is created (to catch plausible mistakes and to assure that the right __new__() and __init__() methods run). But post-creation, it's unclear whether there is value in rerunning checks and whether its even possible to know how other base classes might have affected the class creation.

Do this go in the "consenting adults" category (in the same way that we allow the __class__ attribute to be changed on instances without trying to verify whether it makes sense)?

@rhettinger rhettinger added the 3.8 only security fixes label Sep 28, 2018
@gvanrossum
Copy link
Member

As long as you can't crash CPython with this, this is the responsibility of the code that assigns to __bases__. If they don't take care, the program's behavior is undefined.

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
@gvanrossum gvanrossum removed their assignment Mar 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.8 only security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants