Skip to content

Commit

Permalink
Merge branch 'main' into add-mypy-const
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Aug 18, 2022
2 parents 96c1473 + 49ec731 commit c227b2f
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 24 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Expand Up @@ -26,12 +26,12 @@ repos:
files: \.py$

- repo: https://github.com/asottile/yesqa
rev: v1.3.0
rev: v1.4.0
hooks:
- id: yesqa

- repo: https://github.com/PyCQA/flake8
rev: 5.0.2
rev: 5.0.4
hooks:
- id: flake8

Expand Down
2 changes: 2 additions & 0 deletions changelog.d/1009.change.rst
@@ -0,0 +1,2 @@
``attrs``'s pickling methods now use dicts instead of tuples.
That is safer and more robust across differnt versions of a class.
7 changes: 4 additions & 3 deletions src/attr/_make.py
Expand Up @@ -922,7 +922,7 @@ def slots_getstate(self):
"""
Automatically created by attrs.
"""
return tuple(getattr(self, name) for name in state_attr_names)
return {name: getattr(self, name) for name in state_attr_names}

hash_caching_enabled = self._cache_hash

Expand All @@ -931,8 +931,9 @@ def slots_setstate(self, state):
Automatically created by attrs.
"""
__bound_setattr = _obj_setattr.__get__(self)
for name, value in zip(state_attr_names, state):
__bound_setattr(name, value)
for name in state_attr_names:
if name in state:
__bound_setattr(name, state[name])

# The hash code cache is not included when the object is
# serialized, but it still needs to be initialized to None to
Expand Down
36 changes: 22 additions & 14 deletions tests/test_pyright.py
Expand Up @@ -4,43 +4,51 @@
import os.path
import shutil
import subprocess
import sys

import pytest

import attr
import attrs


_found_pyright = shutil.which("pyright")
pytestmark = pytest.mark.skipif(not _found_pyright, reason="Requires pyright.")
pytestmark = [
pytest.mark.skipif(
sys.version_info < (3, 7), reason="Requires Python 3.7+."
),
pytest.mark.skipif(
shutil.which("pyright") is None, reason="Requires pyright."
),
]


@attr.s(frozen=True)
@attrs.frozen
class PyrightDiagnostic:
severity = attr.ib()
message = attr.ib()
severity: str
message: str


def parse_pyright_output(test_file):
pyright = subprocess.run(
["pyright", "--outputjson", str(test_file)], capture_output=True
)

pyright_result = json.loads(pyright.stdout)

diagnostics = {
return {
PyrightDiagnostic(d["severity"], d["message"])
for d in pyright_result["generalDiagnostics"]
}

return diagnostics


def test_pyright_baseline():
"""The __dataclass_transform__ decorator allows pyright to determine
attrs decorated class types.
"""
diagnostics = parse_pyright_output(
os.path.dirname(__file__) + "/dataclass_transform_example.py"
)
The __dataclass_transform__ decorator allows pyright to determine attrs
decorated class types.
"""

test_file = os.path.dirname(__file__) + "/dataclass_transform_example.py"

diagnostics = parse_pyright_output(test_file)

# Expected diagnostics as per pyright 1.1.135
expected_diagnostics = {
Expand Down
60 changes: 60 additions & 0 deletions tests/test_slots.py
Expand Up @@ -9,6 +9,8 @@
import types
import weakref

from unittest import mock

import pytest

import attr
Expand Down Expand Up @@ -743,3 +745,61 @@ def f(self):

assert B(11).f == 121
assert B(17).f == 289


@attr.s(slots=True)
class A:
x = attr.ib()
b = attr.ib()
c = attr.ib()


@pytest.mark.parametrize("cls", [A])
def test_slots_unpickle_after_attr_removed(cls):
"""
We don't assign attributes we don't have anymore if the class has
removed it.
"""
a = cls(1, 2, 3)
a_pickled = pickle.dumps(a)
a_unpickled = pickle.loads(a_pickled)
assert a_unpickled == a

@attr.s(slots=True)
class NEW_A:
x = attr.ib()
c = attr.ib()

with mock.patch(f"{__name__}.A", NEW_A):
new_a = pickle.loads(a_pickled)

assert new_a.x == 1
assert new_a.c == 3
assert not hasattr(new_a, "b")


@pytest.mark.parametrize("cls", [A])
def test_slots_unpickle_after_attr_added(cls, frozen):
"""
We don't assign attribute we haven't had before if the class has one added.
"""
a = cls(1, 2, 3)
a_pickled = pickle.dumps(a)
a_unpickled = pickle.loads(a_pickled)

assert a_unpickled == a

@attr.s(slots=True, frozen=frozen)
class NEW_A:
x = attr.ib()
b = attr.ib()
d = attr.ib()
c = attr.ib()

with mock.patch(f"{__name__}.A", NEW_A):
new_a = pickle.loads(a_pickled)

assert new_a.x == 1
assert new_a.b == 2
assert new_a.c == 3
assert not hasattr(new_a, "d")
9 changes: 4 additions & 5 deletions tox.ini
@@ -1,17 +1,17 @@
# Keep docs in sync with docs env and .readthedocs.yml.
[gh-actions]
python =
3.6: py36
3.6: py36, mypy
3.7: py37
3.8: py38, changelog
3.9: py39, pyright
3.10: py310, manifest, typing, docs
3.10: py310, manifest, mypy, docs
3.11: py311
pypy-3: pypy3


[tox]
envlist = typing,pre-commit,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
envlist = mypy,pre-commit,py36,py37,py38,py39,py310,py311,pypy3,pyright,manifest,docs,pypi-description,changelog,coverage-report
isolated_build = True


Expand Down Expand Up @@ -89,8 +89,7 @@ skip_install = true
commands = towncrier build --version UNRELEASED --draft


[testenv:typing]
basepython = python3.10
[testenv:mypy]
deps = mypy>=0.902
commands =
mypy src/attrs/__init__.pyi src/attr/__init__.pyi src/attr/_typing_compat.pyi src/attr/_version_info.pyi src/attr/converters.pyi src/attr/exceptions.pyi src/attr/filters.pyi src/attr/setters.pyi src/attr/validators.pyi
Expand Down

0 comments on commit c227b2f

Please sign in to comment.