Remove work-arounds for setting cell_contents (#1201)
* Remove work-arounds for setting cell_contents Simple assignment to cell_contents works starting from Python 3.7[1]. This is also the minimum supported version of Python according to pyproject.toml. It follows that we don't need complex work-arounds that were needed in the past. Also tested on pypy 3.8. [1] https://bugs.python.org/issue30486 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
parent
ea1037cc14
commit
079954ef40
|
@ -4,8 +4,6 @@ import inspect
|
|||
import platform
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
import warnings
|
||||
|
||||
from collections.abc import Mapping, Sequence # noqa: F401
|
||||
from typing import _GenericAlias
|
||||
|
@ -26,16 +24,6 @@ else:
|
|||
from typing import Protocol # noqa: F401
|
||||
|
||||
|
||||
def just_warn(*args, **kw):
|
||||
warnings.warn(
|
||||
"Running interpreter doesn't sufficiently support code object "
|
||||
"introspection. Some features like bare super() or accessing "
|
||||
"__class__ will not work with slotted classes.",
|
||||
RuntimeWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
class _AnnotationExtractor:
|
||||
"""
|
||||
Extract type annotations from a callable, returning None whenever there
|
||||
|
@ -76,101 +64,6 @@ class _AnnotationExtractor:
|
|||
return None
|
||||
|
||||
|
||||
def make_set_closure_cell():
|
||||
"""Return a function of two arguments (cell, value) which sets
|
||||
the value stored in the closure cell `cell` to `value`.
|
||||
"""
|
||||
# pypy makes this easy. (It also supports the logic below, but
|
||||
# why not do the easy/fast thing?)
|
||||
if PYPY:
|
||||
|
||||
def set_closure_cell(cell, value):
|
||||
cell.__setstate__((value,))
|
||||
|
||||
return set_closure_cell
|
||||
|
||||
# Otherwise gotta do it the hard way.
|
||||
|
||||
try:
|
||||
if sys.version_info >= (3, 8):
|
||||
|
||||
def set_closure_cell(cell, value):
|
||||
cell.cell_contents = value
|
||||
|
||||
else:
|
||||
# Create a function that will set its first cellvar to `value`.
|
||||
def set_first_cellvar_to(value):
|
||||
x = value
|
||||
return
|
||||
|
||||
# This function will be eliminated as dead code, but
|
||||
# not before its reference to `x` forces `x` to be
|
||||
# represented as a closure cell rather than a local.
|
||||
def force_x_to_be_a_cell(): # pragma: no cover
|
||||
return x
|
||||
|
||||
# Extract the code object and make sure our assumptions about
|
||||
# the closure behavior are correct.
|
||||
co = set_first_cellvar_to.__code__
|
||||
if co.co_cellvars != ("x",) or co.co_freevars != ():
|
||||
raise AssertionError # pragma: no cover
|
||||
|
||||
# Convert this code object to a code object that sets the
|
||||
# function's first _freevar_ (not cellvar) to the argument.
|
||||
args = [co.co_argcount]
|
||||
args.append(co.co_kwonlyargcount)
|
||||
args.extend(
|
||||
[
|
||||
co.co_nlocals,
|
||||
co.co_stacksize,
|
||||
co.co_flags,
|
||||
co.co_code,
|
||||
co.co_consts,
|
||||
co.co_names,
|
||||
co.co_varnames,
|
||||
co.co_filename,
|
||||
co.co_name,
|
||||
co.co_firstlineno,
|
||||
co.co_lnotab,
|
||||
# These two arguments are reversed:
|
||||
co.co_cellvars,
|
||||
co.co_freevars,
|
||||
]
|
||||
)
|
||||
set_first_freevar_code = types.CodeType(*args)
|
||||
|
||||
def set_closure_cell(cell, value):
|
||||
# Create a function using the set_first_freevar_code,
|
||||
# whose first closure cell is `cell`. Calling it will
|
||||
# change the value of that cell.
|
||||
setter = types.FunctionType(
|
||||
set_first_freevar_code, {}, "setter", (), (cell,)
|
||||
)
|
||||
# And call it to set the cell.
|
||||
setter(value)
|
||||
|
||||
# Make sure it works on this interpreter:
|
||||
def make_func_with_cell():
|
||||
x = None
|
||||
|
||||
def func():
|
||||
return x # pragma: no cover
|
||||
|
||||
return func
|
||||
|
||||
cell = make_func_with_cell().__closure__[0]
|
||||
set_closure_cell(cell, 100)
|
||||
if cell.cell_contents != 100:
|
||||
raise AssertionError # pragma: no cover
|
||||
|
||||
except Exception: # noqa: BLE001
|
||||
return just_warn
|
||||
else:
|
||||
return set_closure_cell
|
||||
|
||||
|
||||
set_closure_cell = make_set_closure_cell()
|
||||
|
||||
# Thread-local global to track attrs instances which are already being repr'd.
|
||||
# This is needed because there is no other (thread-safe) way to pass info
|
||||
# about the instances that are already being repr'd through the call stack
|
||||
|
|
|
@ -18,7 +18,6 @@ from ._compat import (
|
|||
PY310,
|
||||
_AnnotationExtractor,
|
||||
get_generic_base,
|
||||
set_closure_cell,
|
||||
)
|
||||
from .exceptions import (
|
||||
DefaultAlreadySetError,
|
||||
|
@ -909,7 +908,7 @@ class _ClassBuilder:
|
|||
pass
|
||||
else:
|
||||
if match:
|
||||
set_closure_cell(cell, cls)
|
||||
cell.cell_contents = cls
|
||||
|
||||
return cls
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ Unit tests for slots-related functionality.
|
|||
"""
|
||||
|
||||
import pickle
|
||||
import sys
|
||||
import types
|
||||
import weakref
|
||||
|
||||
from unittest import mock
|
||||
|
@ -15,7 +13,7 @@ import pytest
|
|||
|
||||
import attr
|
||||
|
||||
from attr._compat import PYPY, just_warn, make_set_closure_cell
|
||||
from attr._compat import PYPY
|
||||
|
||||
|
||||
# Pympler doesn't work on PyPy.
|
||||
|
@ -478,36 +476,6 @@ class TestClosureCellRewriting:
|
|||
|
||||
assert D.statmethod() is D
|
||||
|
||||
@pytest.mark.skipif(PYPY, reason="set_closure_cell always works on PyPy")
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info >= (3, 8),
|
||||
reason="can't break CodeType.replace() via monkeypatch",
|
||||
)
|
||||
def test_code_hack_failure(self, monkeypatch):
|
||||
"""
|
||||
Keeps working if function/code object introspection doesn't work
|
||||
on this (nonstandard) interpreter.
|
||||
|
||||
A warning is emitted that points to the actual code.
|
||||
"""
|
||||
# This is a pretty good approximation of the behavior of
|
||||
# the actual types.CodeType on Brython.
|
||||
monkeypatch.setattr(types, "CodeType", lambda: None)
|
||||
func = make_set_closure_cell()
|
||||
|
||||
with pytest.warns(RuntimeWarning) as wr:
|
||||
func()
|
||||
|
||||
w = wr.pop()
|
||||
assert __file__ == w.filename
|
||||
assert (
|
||||
"Running interpreter doesn't sufficiently support code object "
|
||||
"introspection. Some features like bare super() or accessing "
|
||||
"__class__ will not work with slotted classes.",
|
||||
) == w.message.args
|
||||
|
||||
assert just_warn is func
|
||||
|
||||
|
||||
@pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython")
|
||||
def test_not_weakrefable():
|
||||
|
|
Loading…
Reference in New Issue