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

Update qiskit.utils.wrap_method for Python 3.11 #9310

Merged
merged 16 commits into from
Jan 10, 2023
Merged

Conversation

wshanks
Copy link
Contributor

@wshanks wshanks commented Dec 20, 2022

Summary

Fixed the behavior of qiskit.utils.wrap_method when applied to a classmethod that is on type. Code changes in Python 3.11 and 3.11.1 revealed that wrap_method did not avoid calling a classmethod desscriptor's __get__ method. wrap_method was changed to use inspect.getattr_static to avoid calling __get__.

Details and comments

The poor handling of classmethod by wrap_method was first noticed with Python 3.11.1 as described in #9291. qiskit.test.decorators.enforce_subclasses_call wraps QiskitBaseTestCase.__init_subclass__ using wrap_method. Prior to 3.11.1, this meant wrapping object.__init_subclass__ (which is a builtin method that behaves differently), but in 3.11.1 unittest.TestCase added an __init_subclass__ method. This change meant that now wrap_method was wrapping a "real" classmethod (__init_subclass__ is a special case but it behaves enough like a classmethod for this issue).

In Python 3.11.0, the behavior of the bound method descriptor changed (specifically, it was this PR and the change to funcobject.c there). The following code illustrates the difference in behavior:

class A:
    def m(self):
        pass


print(A().m.__get__(None, A))

In Python 3.10, this gives

<bound method A.m of <__main__.A object at 0x7f942bc8ace0>>

while in Python 3.11 it gives

<function A.m at 0x7f432b0daa20>

So in Python 3.10 and earlier, the bound method's descriptor returns itself. With Python 3.11 though, the descriptor returns the unbound function.

Prior to the change here, wrap_method used object.__getattribute__ and type.__getattribute__ to retrieve the descriptor for the method being wrapped so that its __get__ could be called by the wrapping descriptor. For methods that are not in dir(type), it used object.__getattribute__(cls, name) to retrieve the descriptor in a loop over every cls in the wrapped class's MRO passing on AttributeError. object.__getattribute__(cls, name) checks type(cls) (so type) for a descriptor and otherwise checks cls.__dict__ for name, raising an AttributeError if name is not found (the code is here). By doing this loop over the MRO, name is eventually found. Note, however, that object.__getattribute__ follows a different path if name is a descriptor in type(cls) (so in type). To avoid this different path, wrap_method checked name against dir(type) and used type.__getattribute__ instead. type.__getattribute__(cls, name) loops over the MRO of cls looking for name and when it finds it calls .__get__(None, cls) on it (the code is here). This call returns a bound method version of the classmethod with cls bound and this is where the Python 3.11 change in behavior comes in. In qiskit.utils.classtools._WrappedMethod, __get__ tries to delegate to the wrapped method by calling .__get__ on it but for this type case that means calling .__get__ on a bound method instead of the classmethod and so for 3.11 getting back an unbound function that expects an extra first argument containing the cls.

So in Python 3.11.1 with the addition of __init_subclass__ to unittest.TestCase, the wrap_method call around __init_subclass__ on QiskitBaseTestCase meant that the definition of QiskitTestCase(QiskitBaseTestCase) now tried to call the wrapped __init_subclass__ as an unbound function without passing it the required cls argument and this led to the error. In 3.11.0, the handling of the descriptor did not matter because object.__init_subclass__ was the method being wrapped and it has a *args, **kwargs signature and does nothing, so it did not matter that it was invoked incorrectly (see here).

Closes #9291

@qiskit-bot
Copy link
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

One or more of the the following people are requested to review this:

  • @Qiskit/terra-core

@wshanks wshanks changed the title Py311 Reenable Python 3.11.1 tests Dec 20, 2022
@Cryoris
Copy link
Contributor

Cryoris commented Dec 20, 2022

Nice this seems to work! The 3.11 tests are failing due to NumPy, but that should be fixed now.

@coveralls
Copy link

coveralls commented Dec 20, 2022

Pull Request Test Coverage Report for Build 3878635934

  • 1 of 1 (100.0%) changed or added relevant line in 1 file are covered.
  • 6 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.04%) to 84.569%

Files with Coverage Reduction New Missed Lines %
qiskit/pulse/library/waveform.py 3 91.49%
src/sabre_swap/mod.rs 3 98.84%
Totals Coverage Status
Change from base Build 3878466191: -0.04%
Covered Lines: 64024
Relevant Lines: 75706

💛 - Coveralls

@wshanks wshanks added the Changelog: Bugfix Include in the "Fixed" section of the changelog label Dec 20, 2022
@wshanks wshanks changed the title Reenable Python 3.11.1 tests Update qiskit.utils.wrap_method for Python 3.11.1 Dec 20, 2022
@wshanks wshanks changed the title Update qiskit.utils.wrap_method for Python 3.11.1 Update qiskit.utils.wrap_method for Python 3.11 Dec 20, 2022
@wshanks wshanks marked this pull request as ready for review December 20, 2022 19:35
qiskit/utils/classtools.py Outdated Show resolved Hide resolved
@wshanks wshanks marked this pull request as draft December 20, 2022 22:21
@wshanks
Copy link
Contributor Author

wshanks commented Dec 20, 2022

While this lets the tests run, I think it is not quite right in general.

@wshanks wshanks marked this pull request as ready for review December 22, 2022 20:43
@wshanks
Copy link
Contributor Author

wshanks commented Dec 22, 2022

Now I think the change is correct.

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM, I didn't know about getattr_static but that looks like exactly what we were trying to hack together before (and what was causing issues for us on 3.11.1). I'm going to hold off on automerge until @jakelishman can take a look though since he wrote all of this decorator code (and I've lost most of the context I previously had for it).

Copy link
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thanks Will. I think I didn't know about getattr_static when I wrote that first implementation, which is part of why it got super hacky.

@mergify mergify bot merged commit 0344a1c into Qiskit:main Jan 10, 2023
@wshanks wshanks deleted the py311 branch January 10, 2023 00:28
@mtreinish mtreinish added the stable backport potential The bug might be minimal and/or import enough to be port to stable label Jan 10, 2023
mergify bot pushed a commit that referenced this pull request Jan 10, 2023
* Revert "[Test] Pin maximum python version in CI to <3.11.1 (#9296)"

This reverts commit 07e0a2f.

* Do not treat __init_subclass__ as a special type method

* Release note

* Apply suggestions from code review

Co-authored-by: Julien Gacon <[email protected]>

* Use inspect.getattr_static to bypass descriptor call

* Update release note

* Update wrap_method test

* Adjust wording on release note

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 0344a1c)
king-p3nguin pushed a commit to king-p3nguin/qiskit-terra that referenced this pull request Jan 11, 2023
* Revert "[Test] Pin maximum python version in CI to <3.11.1 (Qiskit#9296)"

This reverts commit 07e0a2f.

* Do not treat __init_subclass__ as a special type method

* Release note

* Apply suggestions from code review

Co-authored-by: Julien Gacon <[email protected]>

* Use inspect.getattr_static to bypass descriptor call

* Update release note

* Update wrap_method test

* Adjust wording on release note

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Cryoris added a commit to Cryoris/qiskit-terra that referenced this pull request Jan 12, 2023
* Revert "[Test] Pin maximum python version in CI to <3.11.1 (Qiskit#9296)"

This reverts commit 07e0a2f.

* Do not treat __init_subclass__ as a special type method

* Release note

* Apply suggestions from code review

Co-authored-by: Julien Gacon <[email protected]>

* Use inspect.getattr_static to bypass descriptor call

* Update release note

* Update wrap_method test

* Adjust wording on release note

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
jakelishman pushed a commit to jakelishman/qiskit-terra that referenced this pull request Jan 12, 2023
* Revert "[Test] Pin maximum python version in CI to <3.11.1 (Qiskit#9296)"

This reverts commit 07e0a2f.

* Do not treat __init_subclass__ as a special type method

* Release note

* Apply suggestions from code review

Co-authored-by: Julien Gacon <[email protected]>

* Use inspect.getattr_static to bypass descriptor call

* Update release note

* Update wrap_method test

* Adjust wording on release note

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 0344a1c)
@jakelishman jakelishman mentioned this pull request Jan 12, 2023
mergify bot pushed a commit that referenced this pull request Jan 12, 2023
* Fix NumPy 1.24.0 compatibility and pin `coverage<7.0` (#9305)

* fix Kraus from (array, None)

* fix triu_to_dense test

* fix instruction comparison

* skip snobfit if numpy 1.24.0 or above is installed

Co-authored-by: ElePT <[email protected]>

* pin coverage <7.0

* add links to Kraus and snobfit issues

* retrigger CI

Co-authored-by: ElePT <[email protected]>
(cherry picked from commit 9733fc0)

* Update `qiskit.utils.wrap_method` for Python 3.11 (#9310)

* Revert "[Test] Pin maximum python version in CI to <3.11.1 (#9296)"

This reverts commit 07e0a2f.

* Do not treat __init_subclass__ as a special type method

* Release note

* Apply suggestions from code review

Co-authored-by: Julien Gacon <[email protected]>

* Use inspect.getattr_static to bypass descriptor call

* Update release note

* Update wrap_method test

* Adjust wording on release note

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 0344a1c)

* Relax constraints on jupyter-core and ipywidgets (#9364)

These were originally added in #9105 and #9272 respectively, but the
original problem package `seaborn` has released since then, which may
have fixed things.

This removes some now-unnecessary suppressions from image-related
packages, and adds the new suppression for the pyzmq problem, which is
Jupyter's domain to handle.  The extra environment variable in the
images test run is to eagerly move to new default behaviour starting in
jupyter-core 6; there is no need for us to pin the package too low,
since this warning is just encouraging people to proactively test the
new behaviour, and it doesn't cause our suite problems.

* Refactor coverage CI workflow (#9361)

This relaxes the constraint on `coverage` added in #9305.  The issue
there is actually the now unmaintained `coveragepy-lcov` package is not
compatible with Coverage.py 7.0.  However, we only needed
`coveragepy-lcov` to convert Coverage's format into LCOV, which is a
feature Coverage has had itself since version 6.0.

This commit also updates some parts of the coverage workflow that were
old:

- there are new versions of the Actions `checkout` and `setup-python`,
  which swap to using Node 16 rather than Node 12, which is deprecated
  in GHA
- `grcov` is packaged and installable from `cargo` now, rather than
  needing a manual hard-coded pull from GitHub
- we can have `grcov` only keep the parts we care about immediately,
  rather than converting everything and later discarding it

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 7955d92)

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: Will Shanks <[email protected]>
ElePT pushed a commit to ElePT/qiskit-algorithms that referenced this pull request Jul 27, 2023
* Revert "[Test] Pin maximum python version in CI to <3.11.1 (Qiskit/qiskit#9296)"

This reverts commit 07e0a2fc79bada7c1fbf0594f4ad33934f70b7e2.

* Do not treat __init_subclass__ as a special type method

* Release note

* Apply suggestions from code review

Co-authored-by: Julien Gacon <[email protected]>

* Use inspect.getattr_static to bypass descriptor call

* Update release note

* Update wrap_method test

* Adjust wording on release note

Co-authored-by: Julien Gacon <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: Bugfix Include in the "Fixed" section of the changelog stable backport potential The bug might be minimal and/or import enough to be port to stable
Projects
None yet
Development

Successfully merging this pull request may close these issues.

qiskit.test.decorators.enforce_subclasses_call machinery is broken in 3.11.1
6 participants