Skip to content

Commit

Permalink
Update qiskit.utils.wrap_method for Python 3.11 (Qiskit#9310)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Jan 12, 2023
1 parent 95aacfc commit 00b3442
Show file tree
Hide file tree
Showing 4 changed files with 18 additions and 27 deletions.
13 changes: 4 additions & 9 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ parameters:
- name: "supportedPythonVersions"
displayName: "All supported versions of Python"
type: object
# TODO remove the explicit 3.11.0 pin and set it to 3.11. The pin is currently added
# since 3.11.1 breaks CI, see Qiskit/qiskit-terra#9291.
default: ["3.7", "3.8", "3.9", "3.10", "3.11.0"]
default: ["3.7", "3.8", "3.9", "3.10", "3.11"]

- name: "minimumPythonVersion"
displayName: "Minimum supported version of Python"
Expand All @@ -45,7 +43,7 @@ parameters:
- name: "maximumPythonVersion"
displayName: "Maximum supported version of Python"
type: string
default: "3.11.0"
default: "3.11"

- name: "minimumRustVersion"
displayName: "Minimum supported version of Rust"
Expand Down Expand Up @@ -111,9 +109,7 @@ stages:

- template: ".azure/test-macos.yml"
parameters:
# TODO Manually setting this to exclude 3.11 completely, since 3.11.1 breaks
# (see Qiskit/qiskit-terra#9291) and 3.11.0 is not available on azure for MacOS.
pythonVersion: ["3.7", "3.8", "3.9", "3.10"]
pythonVersion: ${{ version }}

- template: ".azure/test-windows.yml"
parameters:
Expand Down Expand Up @@ -185,8 +181,7 @@ stages:

- template: ".azure/test-macos.yml"
parameters:
# TODO remove pin to 3.10 and reset to ${{ parameters.maximumPythonVersion }}
pythonVersion: "3.10"
pythonVersion: ${{ parameters.maximumPythonVersion }}

- template: ".azure/test-windows.yml"
parameters:
Expand Down
19 changes: 2 additions & 17 deletions qiskit/utils/classtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@
_MAGIC_STATICMETHODS = {"__new__"}
_MAGIC_CLASSMETHODS = {"__init_subclass__", "__prepare__"}

# `type` itself has several methods (mostly dunders). When we are wrapping those names, we need to
# make sure that we don't interfere with `type.__getattribute__`'s handling that circumvents the
# normal inheritance rules when appropriate.
_TYPE_METHODS = set(dir(type))


class _lift_to_method: # pylint: disable=invalid-name
"""A decorator that ensures that an input callable object implements ``__get__``. It is
Expand Down Expand Up @@ -146,16 +141,6 @@ def wrap_method(cls: Type, name: str, *, before: Callable = None, after: Callabl
# The best time to apply decorators to methods is before they are bound (e.g. by using function
# decorators during the class definition), but if we're making a class decorator, we can't do
# that. We need the actual definition of the method, so we have to dodge the normal output of
# `type.__getattribute__`, which evalutes descriptors if it finds them, unless the name we're
# looking for is defined on `type` itself. In that case, we need the attribute getter to
# correctly return the underlying object, not the one that `type` defines for its own purposes.
attribute_getter = type.__getattribute__ if name in _TYPE_METHODS else object.__getattribute__
for cls_ in inspect.getmro(cls):
try:
method = attribute_getter(cls_, name)
break
except AttributeError:
pass
else:
raise ValueError(f"Method '{name}' is not defined for class '{cls.__name__}'")
# `type.__getattribute__`, which evalutes descriptors if it finds them.
method = inspect.getattr_static(cls, name)
setattr(cls, name, _WrappedMethod(method, before, after))
11 changes: 11 additions & 0 deletions releasenotes/notes/wrap-method-311-147d254d4b40e805.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
fixes:
- |
Fixed handling of some ``classmethod``s by
:func:`~qiskit.utils.wrap_method` in Python 3.11. Previously, in Python
3.11, ``wrap_method`` would wrap the unbounded function associated with the
``classmethod`` and then fail when invoked because the class object usually
bound to the ``classmethod`` was not passed to the function. Starting in
Python 3.11.1, this issue affected :class:`~qiskit.test.QiskitTestCase`,
preventing it from being imported by other test code. Fixed `#9291
<https://github.com/Qiskit/qiskit-terra/issues/9291>`__.
2 changes: 1 addition & 1 deletion test/python/utils/test_classtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,5 +529,5 @@ def test_raises_on_invalid_name(self):
class Dummy:
pass

with self.assertRaisesRegex(ValueError, "Method 'bad' is not defined for class 'Dummy'"):
with self.assertRaisesRegex(AttributeError, "bad"):
wrap_method(Dummy, "bad", before=lambda self: None)

0 comments on commit 00b3442

Please sign in to comment.