From 079954ef405919968fb020921232509df3b0bcc9 Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Sat, 11 Nov 2023 05:54:24 -0500 Subject: [PATCH] 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 --- src/attr/_compat.py | 107 -------------------------------------------- src/attr/_make.py | 3 +- tests/test_slots.py | 34 +------------- 3 files changed, 2 insertions(+), 142 deletions(-) diff --git a/src/attr/_compat.py b/src/attr/_compat.py index 41fcf046..26da29cf 100644 --- a/src/attr/_compat.py +++ b/src/attr/_compat.py @@ -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 diff --git a/src/attr/_make.py b/src/attr/_make.py index 553e9e55..9dd0e2e3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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 diff --git a/tests/test_slots.py b/tests/test_slots.py index f26fdb9d..89d838d3 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -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():