diff --git a/887.breaking.rst b/887.breaking.rst new file mode 100644 index 000000000..a8401f6ca --- /dev/null +++ b/887.breaking.rst @@ -0,0 +1,6 @@ +``import attrs`` has finally landed! +As of this release, you can finally import ``attrs`` using its proper name. + +Not all names from the ``attr`` namespace have been transferred; most notably ``attr.s`` and ``attr.ib`` are missing. + +This feature is at least for one release **provisional**. diff --git a/conftest.py b/conftest.py index 14ee0c10b..f3e7556be 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,8 @@ from __future__ import absolute_import, division, print_function -import sys - from hypothesis import HealthCheck, settings -from attr._compat import PY310 +from attr._compat import PY36, PY310 def pytest_configure(config): @@ -16,7 +14,7 @@ def pytest_configure(config): collect_ignore = [] -if sys.version_info[:2] < (3, 6): +if not PY36: collect_ignore.extend( [ "tests/test_annotations.py", diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 843447173..39c93e8b6 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -5,9 +5,9 @@ from functools import partial -from attr.exceptions import UnannotatedAttributeError - from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple from ._make import ( NOTHING, _frozen_setattrs, @@ -15,6 +15,7 @@ attrib, attrs, ) +from .exceptions import UnannotatedAttributeError def define( @@ -168,3 +169,29 @@ def field( order=order, on_setattr=on_setattr, ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as the dict_factory. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple`` is always used as the tuple_factory. + + .. versionadded:: 21.3.0 + """ + return _astuple(inst=inst, recurse=recurse, filter=filter) diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py new file mode 100644 index 000000000..4e5cee1f7 --- /dev/null +++ b/src/attrs/__init__.py @@ -0,0 +1,71 @@ +from attr import ( + NOTHING, + Attribute, + Factory, + __author__, + __copyright__, + __description__, + __doc__, + __email__, + __license__, + __title__, + __url__, + __version__, + __version_info__, + assoc, + cmp_using, + converters, + define, + evolve, + exceptions, + field, + fields, + fields_dict, + filters, + frozen, + has, + make_class, + mutable, + resolve_types, + setters, + validate, + validators, +) +from attr._next_gen import asdict, astuple + + +__all__ = [ + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "Attribute", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "Factory", + "field", + "fields_dict", + "fields", + "filters", + "frozen", + "has", + "make_class", + "mutable", + "NOTHING", + "resolve_types", + "setters", + "validate", + "validators", +] diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index a2ed7fe67..98764d1f1 100644 --- a/tests/test_next_gen.py +++ b/tests/test_next_gen.py @@ -8,10 +8,11 @@ import pytest -import attr +import attr as _attr # don't use it by accident +import attrs -@attr.define +@attrs.define class C: x: str y: int @@ -29,7 +30,7 @@ def test_no_slots(self): slots can be deactivated. """ - @attr.define(slots=False) + @attrs.define(slots=False) class NoSlots: x: int @@ -42,9 +43,9 @@ def test_validates(self): Validators at __init__ and __setattr__ work. """ - @attr.define + @attrs.define class Validated: - x: int = attr.field(validator=attr.validators.instance_of(int)) + x: int = attrs.field(validator=attrs.validators.instance_of(int)) v = Validated(1) @@ -61,7 +62,7 @@ def test_no_order(self): with pytest.raises(TypeError): C("1", 2) < C("2", 3) - @attr.define(order=True) + @attrs.define(order=True) class Ordered: x: int @@ -71,23 +72,23 @@ def test_override_auto_attribs_true(self): """ Don't guess if auto_attrib is set explicitly. - Having an unannotated attr.ib/attr.field fails. + Having an unannotated attrs.ib/attrs.field fails. """ - with pytest.raises(attr.exceptions.UnannotatedAttributeError): + with pytest.raises(attrs.exceptions.UnannotatedAttributeError): - @attr.define(auto_attribs=True) + @attrs.define(auto_attribs=True) class ThisFails: - x = attr.field() + x = attrs.field() y: int def test_override_auto_attribs_false(self): """ Don't guess if auto_attrib is set explicitly. - Annotated fields that don't carry an attr.ib are ignored. + Annotated fields that don't carry an attrs.ib are ignored. """ - @attr.define(auto_attribs=False) + @attrs.define(auto_attribs=False) class NoFields: x: int y: int @@ -99,16 +100,16 @@ def test_auto_attribs_detect(self): define correctly detects if a class lacks type annotations. """ - @attr.define + @attrs.define class OldSchool: - x = attr.field() + x = attrs.field() assert OldSchool(1) == OldSchool(1) # Test with maybe_cls = None - @attr.define() + @attrs.define() class OldSchool2: - x = attr.field() + x = attrs.field() assert OldSchool2(1) == OldSchool2(1) @@ -117,10 +118,10 @@ def test_auto_attribs_detect_fields_and_annotations(self): define infers auto_attribs=True if fields have type annotations """ - @attr.define + @attrs.define class NewSchool: x: int - y: list = attr.field() + y: list = attrs.field() @y.validator def _validate_y(self, attribute, value): @@ -130,14 +131,14 @@ def _validate_y(self, attribute, value): assert NewSchool(1, 1) == NewSchool(1, 1) with pytest.raises(ValueError): NewSchool(1, -1) - assert list(attr.fields_dict(NewSchool).keys()) == ["x", "y"] + assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"] def test_auto_attribs_partially_annotated(self): """ define infers auto_attribs=True if any type annotations are found """ - @attr.define + @attrs.define class NewSchool: x: int y: list @@ -145,7 +146,7 @@ class NewSchool: # fields are defined for any annotated attributes assert NewSchool(1, []) == NewSchool(1, []) - assert list(attr.fields_dict(NewSchool).keys()) == ["x", "y"] + assert list(attrs.fields_dict(NewSchool).keys()) == ["x", "y"] # while the unannotated attributes are left as class vars assert NewSchool.z == 10 @@ -156,14 +157,14 @@ def test_auto_attribs_detect_annotations(self): define correctly detects if a class has type annotations. """ - @attr.define + @attrs.define class NewSchool: x: int assert NewSchool(1) == NewSchool(1) # Test with maybe_cls = None - @attr.define() + @attrs.define() class NewSchool2: x: int @@ -174,7 +175,7 @@ def test_exception(self): Exceptions are detected and correctly handled. """ - @attr.define + @attrs.define class E(Exception): msg: str other: int @@ -190,16 +191,16 @@ class E(Exception): def test_frozen(self): """ - attr.frozen freezes classes. + attrs.frozen freezes classes. """ - @attr.frozen + @attrs.frozen class F: x: str f = F(1) - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): f.x = 2 def test_auto_detect_eq(self): @@ -209,7 +210,7 @@ def test_auto_detect_eq(self): Regression test for #670. """ - @attr.define + @attrs.define class C: def __eq__(self, o): raise ValueError() @@ -219,35 +220,35 @@ def __eq__(self, o): def test_subclass_frozen(self): """ - It's possible to subclass an `attr.frozen` class and the frozen-ness is - inherited. + It's possible to subclass an `attrs.frozen` class and the frozen-ness + is inherited. """ - @attr.frozen + @attrs.frozen class A: a: int - @attr.frozen + @attrs.frozen class B(A): b: int - @attr.define(on_setattr=attr.setters.NO_OP) + @attrs.define(on_setattr=attrs.setters.NO_OP) class C(B): c: int assert B(1, 2) == B(1, 2) assert C(1, 2, 3) == C(1, 2, 3) - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): A(1).a = 1 - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): B(1, 2).a = 1 - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): B(1, 2).b = 2 - with pytest.raises(attr.exceptions.FrozenInstanceError): + with pytest.raises(attrs.exceptions.FrozenInstanceError): C(1, 2, 3).c = 3 def test_catches_frozen_on_setattr(self): @@ -256,7 +257,7 @@ def test_catches_frozen_on_setattr(self): immutability is inherited. """ - @attr.define(frozen=True) + @attrs.define(frozen=True) class A: pass @@ -264,7 +265,7 @@ class A: ValueError, match="Frozen classes can't use on_setattr." ): - @attr.define(frozen=True, on_setattr=attr.setters.validate) + @attrs.define(frozen=True, on_setattr=attrs.setters.validate) class B: pass @@ -276,17 +277,17 @@ class B: ), ): - @attr.define(on_setattr=attr.setters.validate) + @attrs.define(on_setattr=attrs.setters.validate) class C(A): pass @pytest.mark.parametrize( "decorator", [ - partial(attr.s, frozen=True, slots=True, auto_exc=True), - attr.frozen, - attr.define, - attr.mutable, + partial(_attr.s, frozen=True, slots=True, auto_exc=True), + attrs.frozen, + attrs.define, + attrs.mutable, ], ) def test_discard_context(self, decorator): @@ -298,7 +299,7 @@ def test_discard_context(self, decorator): @decorator class MyException(Exception): - x: str = attr.ib() + x: str = attrs.field() with pytest.raises(MyException) as ei: try: @@ -314,9 +315,9 @@ def test_converts_and_validates_by_default(self): If no on_setattr is set, assume setters.convert, setters.validate. """ - @attr.define + @attrs.define class C: - x: int = attr.field(converter=int) + x: int = attrs.field(converter=int) @x.validator def _v(self, _, value): @@ -341,7 +342,7 @@ def test_mro_ng(self): See #428 """ - @attr.define + @attrs.define class A: x: int = 10 @@ -349,18 +350,18 @@ class A: def xx(self): return 10 - @attr.define + @attrs.define class B(A): y: int = 20 - @attr.define + @attrs.define class C(A): x: int = 50 def xx(self): return 50 - @attr.define + @attrs.define class D(B, C): pass