This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: object.__new__ does not accept arguments if __bases__ is changed
Type: behavior Stage:
Components: Interpreter Core Versions: Python 3.8, Python 3.7, Python 3.6
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: VA, alexey-muranov, ethan.furman, gvanrossum, ncoghlan, r.david.murray, serhiy.storchaka
Priority: normal Keywords:

Created on 2018-02-04 22:05 by VA, last changed 2022-04-11 14:58 by admin.

Files
File name Uploaded Description Edit
bases.py VA, 2018-02-04 22:05 sample code to reproduce the problem
Messages (12)
msg311622 - (view) Author: Va (VA) Date: 2018-02-04 22:05
object.__new__ takes only the class argument, but it still accepts extra arguments if a class doesn't override __new__, and rejects them otherwise. (This is because __new__ will receive the same arguments as __init__ but __new__ shouldn't need to be overridden just to remove args)

However, if a class has a custom __new__ at one point (in a parent class), and at a later point __bases__ is changed, object.__new__ will still reject arguments, although __new__ may not be overridden anymore at that point. See attached file.

I can't check with all Python 3 versions, but the same code works fine in Python 2.
msg311656 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2018-02-05 09:25
The problem is that

    class C(B):
        pass
    C.__bases__ = (A,)

and

    class C(A):
        pass

are not fully equivalent.

In the first case type->tp_new != object_new.
msg311667 - (view) Author: Alyssa Coghlan (ncoghlan) * (Python committer) Date: 2018-02-05 14:00
I've added Guido to the thread, as my initial reaction is to propose deprecating writable __bases__ rather than trying to support it properly.

However, if we do decide to fix it, then the potential path to resolution I would suggest is:

1. Factor out all of the slot derivation code from its current location into a separate helper function
2. Adjust the descriptor for __bases__ to rerun all of that code when the bases are changed

That still wouldn't be guaranteed to work entirely reliably (since there are some actions taken in the first initialisation that make it hard for us to tell whether a method came from the class definition or was added implicitly by the class machinery, and any class decorators wouldn't be executed again, and we wouldn't be running __init_subclass__ for any of the new base classes either).
msg311679 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2018-02-05 17:44
Yeah, I think the use case for assigning to __bases__ has not been shown. I believe we've seen other situations where the initial list of base classes is used in some computation that affects how the class works, so I doubt it has ever been sound.

Though maybe the OP ("VA") has a real use case where they discovered this? Then it would be nice to know about it.
msg311696 - (view) Author: Alyssa Coghlan (ncoghlan) * (Python committer) Date: 2018-02-05 22:46
I'd also ask whether the use case can be satisfied by rebinding __class__ instead of __bases__. That's far better defined than replacing the contents of the bases list and attempting to dynamically recalculate the MRO.
msg311716 - (view) Author: Va (VA) Date: 2018-02-06 08:13
The use case is a little more complex.

I have a plugin system, with abstract interfaces. Plugins can't import each other, but plugins should be able to allowed to depend on another plugin (using string codes, still no direct imports), and even subclass another plugin's classes (and override some of their methods).

In the sample code, A and C would be 2 plugins, and B would be a helper class (with string code parameters) whose purpose is to make a temporary bridge between A and C.
A should work standalone. C would use A's code but could reimplement some of A's methods. B is a internal class that has A and C at hand, and changes C's __bases__ to point to A.

I have been suggested other solutions, like using composition (a C instance would have an "a" field pointing to an A instance) to avoid inheritance altogether, or using the "type()" function in B.__new__ to create a custom class inheriting A.
None of these solutions are really satisfying because they prevent C from using "super(...)" to refer to A methods.
Rebinding __class__ simply does not allow to override methods at all.
msg311731 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2018-02-06 16:41
I fear that this is one of those cases that will longer in the tracker
forever. We probably shouldn't have allowed assignment to __bases__, and
the behavior is slightly surprising, but fixing it would be too complicated
and there's not enough interest in getting it fixed.

On Feb 6, 2018 12:13 AM, "VA" <report@bugs.python.org> wrote:

>
> VA <d.python.dc54@indigo.re> added the comment:
>
> The use case is a little more complex.
>
> I have a plugin system, with abstract interfaces. Plugins can't import
> each other, but plugins should be able to allowed to depend on another
> plugin (using string codes, still no direct imports), and even subclass
> another plugin's classes (and override some of their methods).
>
> In the sample code, A and C would be 2 plugins, and B would be a helper
> class (with string code parameters) whose purpose is to make a temporary
> bridge between A and C.
> A should work standalone. C would use A's code but could reimplement some
> of A's methods. B is a internal class that has A and C at hand, and changes
> C's __bases__ to point to A.
>
> I have been suggested other solutions, like using composition (a C
> instance would have an "a" field pointing to an A instance) to avoid
> inheritance altogether, or using the "type()" function in B.__new__ to
> create a custom class inheriting A.
> None of these solutions are really satisfying because they prevent C from
> using "super(...)" to refer to A methods.
> Rebinding __class__ simply does not allow to override methods at all.
>
> ----------
>
> _______________________________________
> Python tracker <report@bugs.python.org>
> <https://bugs.python.org/issue32768>
> _______________________________________
>
msg311772 - (view) Author: Alyssa Coghlan (ncoghlan) * (Python committer) Date: 2018-02-07 07:39
From VA's description of the intended use case, this actually sounds a bit like a variant of https://bugs.python.org/issue29944: one reason that replacing C with a new dynamically constructed type won't work reliably is because some of the methods might have captured references to the original type via closure cells, so zero-arg super() will still fail, even if you rebind the name at the module level.

Even __set_name__ can't reliably help with that, since it would rely on every descriptor passing on the __set_name__ call to wrapped objects.

While the problem likely isn't fixable for the class-cloning case, we may be able to do something about the class-replacement case by exposing the zero-arg super() class cell as an attribute on the class object.

If we did that, then methods on the original class could be pointed at the new class by doing "original_class.__classcell__.cell_contents = new_class".
msg341692 - (view) Author: Alexey Muranov (alexey-muranov) Date: 2019-05-07 07:40
Here is a use case for writable bases:

https://stackoverflow.com/q/56007866

class Stateful:
    """
    Abstract base class for "stateful" classes.

    Subclasses must implement InitState mixin.
    """

    @staticmethod
    def __new__(cls, *args, **kwargs):
        super_new = super(__class__, __class__).__new__

        # XXX: see https://stackoverflow.com/a/9639512
        class CurrentStateProxy(cls.InitState):
            @staticmethod
            def _set_state(state_cls=cls.InitState):
                __class__.__bases__ = (state_cls,)

        class Eigenclass(CurrentStateProxy, cls):
            @staticmethod
            def __new__(cls, *args, **kwargs):
                cls.__new__ = None  # just in case
                return super_new(cls, *args, **kwargs)

        return Eigenclass(*args, **kwargs)

class StatefulThing(Stateful):
    class StateA:
        """First state mixin."""

        def say_hello(self):
            print("Hello!")
            self.hello_count += 1
            self._set_state(self.StateB)
            return True

        def say_goodbye(self):
            print("Another goodbye?")
            return False

    class StateB:
        """Second state mixin."""

        def say_hello(self):
            print("Another hello?")
            return False

        def say_goodbye(self):
            print("Goodbye!")
            self.goodbye_count += 1
            self._set_state(self.StateA)
            return True

    # This one is required by Stateful.
    class InitState(StateA):
        """Third state mixin -- the initial state."""

        def say_goodbye(self):
            print("Why?")
            return False

    def __init__(self):
        self.hello_count = self.goodbye_count = 0

    def say_hello_followed_by_goodbye(self):
        self.say_hello() and self.say_goodbye()

# ----------
# ## Demo ##
# ----------
if __name__ == "__main__":
    t1 = StatefulThing()
    t2 = StatefulThing()
    print("> t1, say hello:")
    t1.say_hello()
    print("> t2, say goodbye:")
    t2.say_goodbye()
    print("> t2, say hello:")
    t2.say_hello()
    print("> t1, say hello:")
    t1.say_hello()
    print("> t1, say hello followed by goodbye:")
    t1.say_hello_followed_by_goodbye()
    print("> t2, say goodbye:")
    t2.say_goodbye()
    print("> t2, say hello followed by goodbye:")
    t2.say_hello_followed_by_goodbye()
    print("> t1, say goodbye:")
    t1.say_goodbye()
    print("> t2, say hello:")
    t2.say_hello()
    print("---")
    print( "t1 said {} hellos and {} goodbyes."
           .format(t1.hello_count, t1.goodbye_count) )
    print( "t2 said {} hellos and {} goodbyes."
           .format(t2.hello_count, t2.goodbye_count) )

    # Expected output:
    #
    #     > t1, say hello:
    #     Hello!
    #     > t2, say goodbye:
    #     Why?
    #     > t2, say hello:
    #     Hello!
    #     > t1, say hello:
    #     Another hello?
    #     > t1, say hello followed by goodbye:
    #     Another hello?
    #     > t2, say goodbye:
    #     Goodbye!
    #     > t2, say hello followed by goodbye:
    #     Hello!
    #     Goodbye!
    #     > t1, say goodbye:
    #     Goodbye!
    #     > t2, say hello:
    #     Hello!
    #     ---
    #     t1 said 1 hellos and 1 goodbyes.
    #     t2 said 3 hellos and 2 goodbyes.
msg341693 - (view) Author: Alexey Muranov (alexey-muranov) Date: 2019-05-07 07:45
IMO "overriding" a method with itself should not change the behaviour. So it seems to me that the following is a bug:

        class C:
            def __init__(self, m):
                print(m)

        class D:
            @staticmethod
            def __new__(cls, *args, **kwargs):
                return super(__class__, __class__).__new__(cls, *args, **kwargs)

            def __init__(self, m):
                print(m)

        C(42) # fine
        D(42) # TypeError: object.__new__() takes exactly one argument

Of course such overriding makes little sense in itself, but forbidding it makes even less sense and creates bugs in more complex scenarios.
msg341782 - (view) Author: Alexey Muranov (alexey-muranov) Date: 2019-05-07 17:13
There were problems with the use case for mutable bases that i posted (see #36827).  Here is an updated version:

https://gist.github.com/alexeymuranov/04e2807eb5679ac7e36da4454a58fa7e
msg383703 - (view) Author: Ethan Furman (ethan.furman) * (Python committer) Date: 2020-12-24 20:00
A use-case for writable bases:

__init_subclass__ is called in type.__new__, which means that for Enum __init_subclass__ is called before the members have been added.  To work around this I am currently (3.10) adding in a _NoInitSubclass to the bases before type.__new__ is called, and then removing it.

A better solution to that problem would be a way to tell type.__new__ /not/ to call __init_subclass__, but I don't have that option at this point.
History
Date User Action Args
2022-04-11 14:58:57adminsetgithub: 76949
2020-12-24 20:00:05ethan.furmansetnosy: + ethan.furman
messages: + msg383703
2019-05-07 17:13:48alexey-muranovsetmessages: + msg341782
2019-05-07 07:45:43alexey-muranovsetmessages: + msg341693
2019-05-07 07:40:03alexey-muranovsetnosy: + alexey-muranov
messages: + msg341692
2018-02-07 07:39:42ncoghlansetmessages: + msg311772
2018-02-06 16:41:57gvanrossumsetmessages: + msg311731
2018-02-06 08:13:25VAsetmessages: + msg311716
2018-02-05 22:46:12ncoghlansetmessages: + msg311696
2018-02-05 17:44:24gvanrossumsetmessages: + msg311679
2018-02-05 16:12:03r.david.murraysetnosy: + r.david.murray
2018-02-05 14:00:06ncoghlansetnosy: + gvanrossum
messages: + msg311667
2018-02-05 09:25:32serhiy.storchakasetversions: + Python 3.7, Python 3.8
nosy: + ncoghlan, serhiy.storchaka

messages: + msg311656

components: + Interpreter Core
2018-02-04 22:05:12VAcreate