Skip to content

Commit

Permalink
Remove work-arounds for setting cell_contents (#1201)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Nov 11, 2023
1 parent ea1037c commit 079954e
Show file tree
Hide file tree
Showing 3 changed files with 2 additions and 142 deletions.
107 changes: 0 additions & 107 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import platform
import sys
import threading
import types
import warnings

from collections.abc import Mapping, Sequence # noqa: F401
from typing import _GenericAlias
Expand All @@ -26,16 +24,6 @@
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
Expand Down Expand Up @@ -76,101 +64,6 @@ def get_return_type(self):
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
Expand Down
3 changes: 1 addition & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
PY310,
_AnnotationExtractor,
get_generic_base,
set_closure_cell,
)
from .exceptions import (
DefaultAlreadySetError,
Expand Down Expand Up @@ -909,7 +908,7 @@ def _create_slots_class(self):
pass
else:
if match:
set_closure_cell(cell, cls)
cell.cell_contents = cls

return cls

Expand Down
34 changes: 1 addition & 33 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
"""

import pickle
import sys
import types
import weakref

from unittest import mock
Expand All @@ -15,7 +13,7 @@

import attr

from attr._compat import PYPY, just_warn, make_set_closure_cell
from attr._compat import PYPY


# Pympler doesn't work on PyPy.
Expand Down Expand Up @@ -478,36 +476,6 @@ def statmethod():

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():
Expand Down

0 comments on commit 079954e

Please sign in to comment.