diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d25e6ccfe..88f6415e9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,6 +19,7 @@ If your pull request is a documentation fix or a trivial typo, feel free to dele - [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py). - [ ] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``). - [ ] ...and used in the stub test file `tests/typing_example.py`. + - [ ] If they've been added to `attr/__init__.pyi`, they've *also* been re-imported in `attrs/__init__.pyi`. - [ ] Updated **documentation** for changed code. - [ ] New functions/classes have to be added to `docs/api.rst` by hand. - [ ] Changes to the signature of `@attr.s()` have to be added by hand too. diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2597ce9c9..bdf3a418a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,7 @@ Whenever there is a need to break compatibility, it is announced here in the cha .. warning:: - The structure of the `attr.Attribute` class is exempt from this rule. + The structure of the `attrs.Attribute` class is exempt from this rule. It *will* change in the future, but since it should be considered read-only, that shouldn't matter. However if you intend to build extensions on top of ``attrs`` you have to anticipate that. diff --git a/MANIFEST.in b/MANIFEST.in index 398252bb9..3d68bf9c5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,8 +2,8 @@ include LICENSE *.rst *.toml *.yml *.yaml *.ini graft .github # Stubs -include src/attr/py.typed recursive-include src *.pyi +recursive-include src py.typed # Tests include tox.ini conftest.py diff --git a/README.rst b/README.rst index de2d6abaf..a2aa04bbc 100644 --- a/README.rst +++ b/README.rst @@ -32,13 +32,12 @@ For that, it gives you a class decorator and a way to declaratively define the a .. code-block:: pycon - >>> from typing import List - >>> from attr import asdict, define, make_class, Factory + >>> from attrs import asdict, define, make_class, Factory >>> @define ... class SomeClass: ... a_number: int = 42 - ... list_of_numbers: List[int] = Factory(list) + ... list_of_numbers: list[int] = Factory(list) ... ... def hard_math(self, another_number): ... return self.a_number + sum(self.list_of_numbers) * another_number @@ -85,7 +84,7 @@ Never again violate the `single responsibility principle `_ that have been introduced in version 20.1.0. The classic APIs (``@attr.s``, ``attr.ib``, ``@attr.attrs``, ``attr.attrib``, and ``attr.dataclass``) will remain indefinitely. diff --git a/changelog.d/887.breaking.rst b/changelog.d/887.breaking.rst new file mode 100644 index 000000000..98b4079ff --- /dev/null +++ b/changelog.d/887.breaking.rst @@ -0,0 +1,14 @@ +``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. +See ``attrs.define`` and ``attrs.field`` if you haven't seen our next-generation APIs yet. +A more elaborate explanation can be found `On The Core API Names `_ + +This feature is at least for one release **provisional**. +We don't *plan* on changing anything, but such a big change is unlikely to go perfectly on the first strike. + +The API docs have been mostly updated, but it will be an ongoing effort to change everything to the new APIs. +Please note that we have **not** moved -- or even removed -- anything from ``attr``! + +Please do report any bugs or documentation inconsistencies! 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/docs/api.rst b/docs/api.rst index 1dcaf978e..bb52c0697 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,27 +3,122 @@ API Reference .. currentmodule:: attr -``attrs`` works by decorating a class using `attr.define` or `attr.s` and then optionally defining attributes on the class using `attr.field`, `attr.ib`, or a type annotation. +``attrs`` works by decorating a class using `attrs.define` or `attr.s` and then optionally defining attributes on the class using `attrs.field`, `attr.ib`, or a type annotation. If you're confused by the many names, please check out `names` for clarification. What follows is the API explanation, if you'd like a more hands-on introduction, have a look at `examples`. +As of version 21.3.0, ``attrs`` consists of **two** to-level package names: + +- The classic ``attr`` that powered the venerable `attr.s` and `attr.ib` +- The modern ``attrs`` that only contains most modern APIs and relies on `attrs.define` and `attrs.field` to define your classes. + Additionally it offers some ``attr`` APIs with nicer defaults (e.g. `attrs.asdict`). + Using this namespace requires Python 3.6 or later. + +The ``attrs`` namespace is built *on top of* the ``attr`` which will *never* go away. + Core ---- - .. note:: - ``attrs`` 20.1.0 added a bunch of nicer APIs (sometimes referred to as next generation -- or NG -- APIs) that were intended to become the main way of defining classes in the future. - As of 21.1.0, they are not provisional anymore and are the **recommended** way to use ``attrs``! - The next step will be adding an importable ``attrs`` namespace. - The documentation will be updated successively. + Please not that the ``attrs`` namespace has been added in version 21.3.0. + Most of the objects are simply re-imported from ``attr``. + Therefore if a class, method, or function claims that it has been added in an older version, it is only available in the ``attr`` namespace. + +.. autodata:: attrs.NOTHING + +.. autofunction:: attrs.define + +.. function:: attrs.mutable(same_as_define) - Please have a look at :ref:`next-gen`! + Alias for `attrs.define`. + + .. versionadded:: 20.1.0 + +.. function:: attrs.frozen(same_as_define) + + Behaves the same as `attrs.define` but sets *frozen=True* and *on_setattr=None*. + + .. versionadded:: 20.1.0 -.. autodata:: attr.NOTHING +.. autofunction:: attrs.field + +.. function:: define + + Old import path for `attrs.define`. + +.. function:: mutable + + Old import path for `attrs.mutable`. + +.. function:: frozen + + Old import path for `attrs.frozen`. + +.. function:: field + + Old import path for `attrs.field`. + +.. autoclass:: attrs.Attribute + :members: evolve + + For example: + + .. doctest:: + + >>> import attr + >>> @attr.s + ... class C(object): + ... x = attr.ib() + >>> attr.fields(C).x + Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) + + +.. autofunction:: attrs.make_class + + This is handy if you want to programmatically create classes. + + For example: + + .. doctest:: + + >>> C1 = attr.make_class("C1", ["x", "y"]) + >>> C1(1, 2) + C1(x=1, y=2) + >>> C2 = attr.make_class("C2", {"x": attr.ib(default=42), + ... "y": attr.ib(default=attr.Factory(list))}) + >>> C2() + C2(x=42, y=[]) + + +.. autoclass:: attrs.Factory + + For example: + + .. doctest:: + + >>> @attr.s + ... class C(object): + ... x = attr.ib(default=attr.Factory(list)) + ... y = attr.ib(default=attr.Factory( + ... lambda self: set(self.x), + ... takes_self=True) + ... ) + >>> C() + C(x=[], y=set()) + >>> C([1, 2, 3]) + C(x=[1, 2, 3], y={1, 2, 3}) + + +Classic +~~~~~~~ + +.. data:: attr.NOTHING + + Same as `attrs.NOTHING`. .. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False, collect_by_mro=False, getstate_setstate=None, on_setattr=None, field_transformer=None, match_args=True) @@ -93,69 +188,32 @@ Core ... ValueError: x must be positive -.. autoclass:: attr.Attribute - :members: evolve - - For example: - - .. doctest:: - - >>> import attr - >>> @attr.s - ... class C(object): - ... x = attr.ib() - >>> attr.fields(C).x - Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) - -.. autofunction:: attr.make_class - This is handy if you want to programmatically create classes. - - For example: - - .. doctest:: - - >>> C1 = attr.make_class("C1", ["x", "y"]) - >>> C1(1, 2) - C1(x=1, y=2) - >>> C2 = attr.make_class("C2", {"x": attr.ib(default=42), - ... "y": attr.ib(default=attr.Factory(list))}) - >>> C2() - C2(x=42, y=[]) - - -.. autoclass:: attr.Factory - - For example: - - .. doctest:: +Exceptions +---------- - >>> @attr.s - ... class C(object): - ... x = attr.ib(default=attr.Factory(list)) - ... y = attr.ib(default=attr.Factory( - ... lambda self: set(self.x), - ... takes_self=True) - ... ) - >>> C() - C(x=[], y=set()) - >>> C([1, 2, 3]) - C(x=[1, 2, 3], y={1, 2, 3}) +All exceptions are available from both ``attr.exceptions`` and ``attrs.exceptions`` and are the same thing. +That means that it doesn't matter from from which namespace they've been raised and/or caught: +.. doctest:: -Exceptions ----------- + >>> import attrs, attr + >>> try: + ... raise attrs.exceptions.FrozenError() + ... except attr.exceptions.FrozenError: + ... print("this works!") + this works! -.. autoexception:: attr.exceptions.PythonTooOldError -.. autoexception:: attr.exceptions.FrozenError -.. autoexception:: attr.exceptions.FrozenInstanceError -.. autoexception:: attr.exceptions.FrozenAttributeError -.. autoexception:: attr.exceptions.AttrsAttributeNotFoundError -.. autoexception:: attr.exceptions.NotAnAttrsClassError -.. autoexception:: attr.exceptions.DefaultAlreadySetError -.. autoexception:: attr.exceptions.UnannotatedAttributeError -.. autoexception:: attr.exceptions.NotCallableError +.. autoexception:: attrs.exceptions.PythonTooOldError +.. autoexception:: attrs.exceptions.FrozenError +.. autoexception:: attrs.exceptions.FrozenInstanceError +.. autoexception:: attrs.exceptions.FrozenAttributeError +.. autoexception:: attrs.exceptions.AttrsAttributeNotFoundError +.. autoexception:: attrs.exceptions.NotAnAttrsClassError +.. autoexception:: attrs.exceptions.DefaultAlreadySetError +.. autoexception:: attrs.exceptions.UnannotatedAttributeError +.. autoexception:: attrs.exceptions.NotCallableError For example:: @@ -172,9 +230,12 @@ Helpers ``attrs`` comes with a bunch of helper methods that make working with it easier: -.. autofunction:: attr.cmp_using +.. autofunction:: attrs.cmp_using +.. function:: attr.cmp_using + + Same as `attrs.cmp_using`. -.. autofunction:: attr.fields +.. autofunction:: attrs.fields For example: @@ -184,14 +245,18 @@ Helpers ... class C(object): ... x = attr.ib() ... y = attr.ib() - >>> attr.fields(C) + >>> attrs.fields(C) (Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)) - >>> attr.fields(C)[1] + >>> attrs.fields(C)[1] Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) - >>> attr.fields(C).y is attr.fields(C)[1] + >>> attrs.fields(C).y is attrs.fields(C)[1] True -.. autofunction:: attr.fields_dict +.. function:: attr.fields + + Same as `attrs.fields`. + +.. autofunction:: attrs.fields_dict For example: @@ -201,15 +266,18 @@ Helpers ... class C(object): ... x = attr.ib() ... y = attr.ib() - >>> attr.fields_dict(C) + >>> attrs.fields_dict(C) {'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None)} >>> attr.fields_dict(C)['y'] Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False, inherited=False, on_setattr=None) - >>> attr.fields_dict(C)['y'] is attr.fields(C).y + >>> attrs.fields_dict(C)['y'] is attrs.fields(C).y True +.. function:: attr.fields_dict + + Same as `attrs.fields_dict`. -.. autofunction:: attr.has +.. autofunction:: attrs.has For example: @@ -223,83 +291,106 @@ Helpers >>> attr.has(object) False +.. function:: attr.has + + Same as `attrs.has`. -.. autofunction:: attr.resolve_types +.. autofunction:: attrs.resolve_types For example: .. doctest:: >>> import typing - >>> @attr.s(auto_attribs=True) + >>> @attrs.define ... class A: ... a: typing.List['A'] ... b: 'B' ... - >>> @attr.s(auto_attribs=True) + >>> @attrs.define ... class B: ... a: A ... - >>> attr.fields(A).a.type + >>> attrs.fields(A).a.type typing.List[ForwardRef('A')] - >>> attr.fields(A).b.type + >>> attrs.fields(A).b.type 'B' - >>> attr.resolve_types(A, globals(), locals()) + >>> attrs.resolve_types(A, globals(), locals()) - >>> attr.fields(A).a.type + >>> attrs.fields(A).a.type typing.List[A] - >>> attr.fields(A).b.type + >>> attrs.fields(A).b.type -.. autofunction:: attr.asdict +.. function:: attr.resolve_types + + Same as `attrs.resolve_types`. + +.. autofunction:: attrs.asdict For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - >>> attr.asdict(C(1, C(2, 3))) + >>> @attrs.define + ... class C: + ... x: int + ... y: int + >>> attrs.asdict(C(1, C(2, 3))) {'x': 1, 'y': {'x': 2, 'y': 3}} +.. autofunction:: attr.asdict -.. autofunction:: attr.astuple +.. autofunction:: attrs.astuple For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() - >>> attr.astuple(C(1,2)) + >>> @attrs.define + ... class C: + ... x = attr.field() + ... y = attr.field() + >>> attrs.astuple(C(1,2)) (1, 2) -``attrs`` includes some handy helpers for filtering the attributes in `attr.asdict` and `attr.astuple`: +.. autofunction:: attr.astuple + + +``attrs`` includes some handy helpers for filtering the attributes in `attrs.asdict` and `attrs.astuple`: + +.. autofunction:: attrs.filters.include + +.. autofunction:: attrs.filters.exclude + +.. function:: attr.filters.include + + Same as `attrs.filters.include`. + +.. function:: attr.filters.exclude -.. autofunction:: attr.filters.include + Same as `attrs.filters.exclude`. -.. autofunction:: attr.filters.exclude +See :func:`attrs.asdict` for examples. -See :func:`asdict` for examples. +All objects from ``attrs.filters`` are also available from ``attr.filters``. -.. autofunction:: attr.evolve +---- + +.. autofunction:: attrs.evolve For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib() - ... y = attr.ib() + >>> @attrs.define + ... class C: + ... x: int + ... y: int >>> i1 = C(1, 2) >>> i1 C(x=1, y=2) - >>> i2 = attr.evolve(i1, y=3) + >>> i2 = attrs.evolve(i1, y=3) >>> i2 C(x=1, y=3) >>> i1 == i2 @@ -312,22 +403,30 @@ See :func:`asdict` for examples. * attributes with ``init=False`` can't be set with ``evolve``. * the usual ``__init__`` validators will validate the new values. -.. autofunction:: validate +.. function:: attr.evolve + + Same as `attrs.evolve`. + +.. autofunction:: attrs.validate For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @attrs.define(on_setattr=attrs.setters.NO_OP) + ... class C: + ... x = attrs.field(validator=attrs.validators.instance_of(int)) >>> i = C(1) >>> i.x = "1" - >>> attr.validate(i) + >>> attrs.validate(i) Traceback (most recent call last): ... TypeError: ("'x' must be (got '1' that is a ).", ...) +.. function:: attr.validate + + Same as `attrs.validate`. + Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact: @@ -341,18 +440,19 @@ Validators can be globally disabled if you want to run them only in development Validators ---------- -``attrs`` comes with some common validators in the ``attrs.validators`` module: +``attrs`` comes with some common validators in the ``attrs.validators`` module. +All objects from ``attrs.converters`` are also available from ``attr.converters``. -.. autofunction:: attr.validators.lt +.. autofunction:: attrs.validators.lt For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.lt(42)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.lt(42)) >>> C(41) C(x=41) >>> C(42) @@ -360,15 +460,15 @@ Validators ... ValueError: ("'x' must be < 42: 42") -.. autofunction:: attr.validators.le +.. autofunction:: attrs.validators.le For example: .. doctest:: - >>> @attr.s + >>> @attrs.define ... class C(object): - ... x = attr.ib(validator=attr.validators.le(42)) + ... x = attrs.field(validator=attr.validators.le(42)) >>> C(42) C(x=42) >>> C(43) @@ -376,15 +476,15 @@ Validators ... ValueError: ("'x' must be <= 42: 43") -.. autofunction:: attr.validators.ge +.. autofunction:: attrs.validators.ge For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.ge(42)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.ge(42)) >>> C(42) C(x=42) >>> C(41) @@ -392,15 +492,15 @@ Validators ... ValueError: ("'x' must be => 42: 41") -.. autofunction:: attr.validators.gt +.. autofunction:: attrs.validators.gt For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.gt(42)) + >>> @attrs.define + ... class C: + ... x = attr.field(validator=attrs.validators.gt(42)) >>> C(43) C(x=43) >>> C(42) @@ -408,15 +508,15 @@ Validators ... ValueError: ("'x' must be > 42: 42") -.. autofunction:: attr.validators.max_len +.. autofunction:: attrs.validators.max_len For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.max_len(4)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.max_len(4)) >>> C("spam") C(x='spam') >>> C("bacon") @@ -424,16 +524,15 @@ Validators ... ValueError: ("Length of 'x' must be <= 4: 5") -.. autofunction:: attr.validators.instance_of - +.. autofunction:: attrs.validators.instance_of For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.instance_of(int)) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -445,7 +544,7 @@ Validators ... TypeError: ("'x' must be (got None that is a ).", Attribute(name='x', default=NOTHING, validator=>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), , None) -.. autofunction:: attr.validators.in_ +.. autofunction:: attrs.validators.in_ For example: @@ -455,10 +554,10 @@ Validators >>> class State(enum.Enum): ... ON = "on" ... OFF = "off" - >>> @attr.s - ... class C(object): - ... state = attr.ib(validator=attr.validators.in_(State)) - ... val = attr.ib(validator=attr.validators.in_([1, 2, 3])) + >>> @attrs.define + ... class C: + ... state = attrs.field(validator=attrs.validators.in_(State)) + ... val = attrs.field(validator=attrs.validators.in_([1, 2, 3])) >>> C(State.ON, 1) C(state=, val=1) >>> C("on", 1) @@ -470,26 +569,26 @@ Validators ... ValueError: 'val' must be in [1, 2, 3] (got 4) -.. autofunction:: attr.validators.provides +.. autofunction:: attrs.validators.provides -.. autofunction:: attr.validators.and_ +.. autofunction:: attrs.validators.and_ - For convenience, it's also possible to pass a list to `attr.ib`'s validator argument. + For convenience, it's also possible to pass a list to `attrs.field`'s validator argument. Thus the following two statements are equivalent:: - x = attr.ib(validator=attr.validators.and_(v1, v2, v3)) - x = attr.ib(validator=[v1, v2, v3]) + x = attrs.field(validator=attrs.validators.and_(v1, v2, v3)) + x = attrs.field(validator=[v1, v2, v3]) -.. autofunction:: attr.validators.optional +.. autofunction:: attrs.validators.optional For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(int))) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.optional(attr.validators.instance_of(int))) >>> C(42) C(x=42) >>> C("42") @@ -500,15 +599,15 @@ Validators C(x=None) -.. autofunction:: attr.validators.is_callable +.. autofunction:: attrs.validators.is_callable For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.is_callable()) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.is_callable()) >>> C(isinstance) C(x=) >>> C("not a callable") @@ -517,15 +616,15 @@ Validators attr.exceptions.NotCallableError: 'x' must be callable (got 'not a callable' that is a ). -.. autofunction:: attr.validators.matches_re +.. autofunction:: attrs.validators.matches_re For example: .. doctest:: - >>> @attr.s - ... class User(object): - ... email = attr.ib(validator=attr.validators.matches_re( + >>> @attrs.define + ... class User: + ... email = attrs.field(validator=attrs.validators.matches_re( ... "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")) >>> User(email="user@example.com") User(email='user@example.com') @@ -535,17 +634,17 @@ Validators ValueError: ("'email' must match regex '(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\\\.[a-zA-Z0-9-.]+$)' ('user@example.com@test.com' doesn't)", Attribute(name='email', default=NOTHING, validator=, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), re.compile('(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$)'), 'user@example.com@test.com') -.. autofunction:: attr.validators.deep_iterable +.. autofunction:: attrs.validators.deep_iterable For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.deep_iterable( - ... member_validator=attr.validators.instance_of(int), - ... iterable_validator=attr.validators.instance_of(list) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.deep_iterable( + ... member_validator=attrs.validators.instance_of(int), + ... iterable_validator=attrs.validators.instance_of(list) ... )) >>> C(x=[1, 2, 3]) C(x=[1, 2, 3]) @@ -559,18 +658,18 @@ Validators TypeError: ("'x' must be (got '3' that is a ).", Attribute(name='x', default=NOTHING, validator=> iterables of >>, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), , '3') -.. autofunction:: attr.validators.deep_mapping +.. autofunction:: attrs.validators.deep_mapping For example: .. doctest:: - >>> @attr.s - ... class C(object): - ... x = attr.ib(validator=attr.validators.deep_mapping( - ... key_validator=attr.validators.instance_of(str), - ... value_validator=attr.validators.instance_of(int), - ... mapping_validator=attr.validators.instance_of(dict) + >>> @attrs.define + ... class C: + ... x = attrs.field(validator=attrs.validators.deep_mapping( + ... key_validator=attrs.validators.instance_of(str), + ... value_validator=attrs.validators.instance_of(int), + ... mapping_validator=attrs.validators.instance_of(dict) ... )) >>> C(x={"a": 1, "b": 2}) C(x={'a': 1, 'b': 2}) @@ -589,17 +688,19 @@ Validators Validators can be both globally and locally disabled: -.. autofunction:: attr.validators.set_disabled +.. autofunction:: attrs.validators.set_disabled -.. autofunction:: attr.validators.get_disabled +.. autofunction:: attrs.validators.get_disabled -.. autofunction:: attr.validators.disabled +.. autofunction:: attrs.validators.disabled Converters ---------- -.. autofunction:: attr.converters.pipe +All objects from ``attrs.converters`` are also available from ``attr.converters``. + +.. autofunction:: attrs.converters.pipe For convenience, it's also possible to pass a list to `attr.ib`'s converter argument. @@ -608,7 +709,7 @@ Converters x = attr.ib(converter=attr.converter.pipe(c1, c2, c3)) x = attr.ib(converter=[c1, c2, c3]) -.. autofunction:: attr.converters.optional +.. autofunction:: attrs.converters.optional For example: @@ -623,7 +724,7 @@ Converters C(x=42) -.. autofunction:: attr.converters.default_if_none +.. autofunction:: attrs.converters.default_if_none For example: @@ -638,7 +739,7 @@ Converters C(x='') -.. autofunction:: attr.converters.to_bool +.. autofunction:: attrs.converters.to_bool For example: @@ -665,22 +766,23 @@ Converters Setters ------- -These are helpers that you can use together with `attr.s`'s and `attr.ib`'s ``on_setattr`` arguments. +These are helpers that you can use together with `attrs.define`'s and `attrs.fields`'s ``on_setattr`` arguments. +All setters in ``attrs.setters`` are also available from ``attr.setters``. -.. autofunction:: attr.setters.frozen -.. autofunction:: attr.setters.validate -.. autofunction:: attr.setters.convert -.. autofunction:: attr.setters.pipe -.. autodata:: attr.setters.NO_OP +.. autofunction:: attrs.setters.frozen +.. autofunction:: attrs.setters.validate +.. autofunction:: attrs.setters.convert +.. autofunction:: attrs.setters.pipe +.. autodata:: attrs.setters.NO_OP For example, only ``x`` is frozen here: .. doctest:: - >>> @attr.s(on_setattr=attr.setters.frozen) - ... class C(object): - ... x = attr.ib() - ... y = attr.ib(on_setattr=attr.setters.NO_OP) + >>> @attrs.define(on_setattr=attr.setters.frozen) + ... class C: + ... x = attr.field() + ... y = attr.field(on_setattr=attr.setters.NO_OP) >>> c = C(1, 2) >>> c.y = 3 >>> c.y @@ -688,53 +790,9 @@ These are helpers that you can use together with `attr.s`'s and `attr.ib`'s ``on >>> c.x = 4 Traceback (most recent call last): ... - attr.exceptions.FrozenAttributeError: () - - N.B. Please use `attr.s`'s *frozen* argument to freeze whole classes; it is more efficient. - - -.. _next-gen: - -Next Generation APIs --------------------- - -These are Python 3.6 and later-only, and keyword-only APIs that call `attr.s` with different default values. - -The most notable differences are: - -- automatically detect whether or not *auto_attribs* should be `True` -- *slots=True* (see :term:`slotted classes` for potentially surprising behaviors) -- *auto_exc=True* -- *auto_detect=True* -- *eq=True*, but *order=False* -- Converters and validators are run when you set an attribute (*on_setattr=[attr.setters.convert, attr.setters.validate*]). -- Some options that aren't relevant to Python 3 have been dropped. - -Please note that these are *defaults* and you're free to override them, just like before. - -Since the Python ecosystem has settled on the term ``field`` for defining attributes, we have also added `attr.field` as a substitute for `attr.ib`. - -.. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. - -.. note:: - - `attr.s` and `attr.ib` (and their serious business cousins) aren't going anywhere. - The new APIs build on top of them. - -.. autofunction:: attr.define -.. function:: mutable(same_as_define) - - Alias for `attr.define`. - - .. versionadded:: 20.1.0 - -.. function:: frozen(same_as_define) - - Behaves the same as `attr.define` but sets *frozen=True* and *on_setattr=None*. - - .. versionadded:: 20.1.0 + attrs.exceptions.FrozenAttributeError: () -.. autofunction:: attr.field + N.B. Please use `attrs.define`'s *frozen* argument (or `attrs.frozen`) to freeze whole classes; it is more efficient. Deprecated APIs diff --git a/docs/comparison.rst b/docs/comparison.rst index 87a47d2f1..760124ca3 100644 --- a/docs/comparison.rst +++ b/docs/comparison.rst @@ -62,5 +62,5 @@ For NumPy arrays it would look like this:: .. warning:: - Please note that *eq* and *order* are set *independently*, because *order* is `False` by default in `modern APIs `. + Please note that *eq* and *order* are set *independently*, because *order* is `False` by default in `attrs.define` (but not in `attr.s`). You can set both at once by using the *cmp* argument that we've undeprecated just for this use-case. diff --git a/docs/examples.rst b/docs/examples.rst index 075f4e0c6..fd6feb549 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -419,7 +419,7 @@ Therefore if you use ``@default``, it is *not* enough to annotate said attribute ... TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None, kw_only=False), , '42') -Please note that if you use `attr.s` (and not `define`) to define your class, validators only run on initialization by default. +Please note that if you use `attr.s` (and not `attrs.define`) to define your class, validators only run on initialization by default. This behavior can be changed using the ``on_setattr`` argument. Check out `validators` for more details. @@ -492,7 +492,7 @@ Types >>> fields(C).x.type -If you don't mind annotating *all* attributes, you can even drop the `field` and assign default values instead: +If you don't mind annotating *all* attributes, you can even drop the `attrs.field` and assign default values instead: .. doctest:: @@ -521,8 +521,8 @@ If you don't mind annotating *all* attributes, you can even drop the `field` and The generated ``__init__`` method will have an attribute called ``__annotations__`` that contains this type information. -If your annotations contain forward references, -you can resolve these after all references have been defined by using :func:`attr.resolve_types`. +If your annotations contain strings (e.g. forward references), +you can resolve these after all references have been defined by using :func:`attrs.resolve_types`. This will replace the *type* attribute in the respective fields. .. doctest:: @@ -564,7 +564,7 @@ Slots ----- :term:`Slotted classes ` have several advantages on CPython. -Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of using `define` or passing ``slots=True`` to `attr.s`: +Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of using `attrs.define` or passing ``slots=True`` to `attr.s`: .. doctest:: @@ -624,11 +624,11 @@ Other Goodies ------------- Sometimes you may want to create a class programmatically. -``attrs`` won't let you down and gives you `attr.make_class` : +``attrs`` won't let you down and gives you `attrs.make_class` : .. doctest:: - >>> from attr import fields, make_class + >>> from attrs import fields, make_class >>> @define ... class C1: ... x = field() @@ -654,7 +654,7 @@ You can still have power over the attributes if you pass a dictionary of name: ` >>> i.y [] -If you need to dynamically make a class with `attr.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: +If you need to dynamically make a class with `attrs.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: .. doctest:: diff --git a/docs/extending.rst b/docs/extending.rst index d229f1595..57eaee94e 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -2,7 +2,7 @@ Extending ========= Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute. -It is a tuple of `attr.Attribute` carrying meta-data about each attribute. +It is a tuple of `attrs.Attribute` carrying meta-data about each attribute. So it is fairly simple to build your own decorators on top of ``attrs``: @@ -21,7 +21,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``: .. warning:: - The `define`/`attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place! + The `attrs.define`/`attr.s` decorator **must** be applied first because it puts ``__attrs_attrs__`` in place! That means that is has to come *after* your decorator because:: @a @@ -205,13 +205,13 @@ Its main purpose is to automatically add converters to attributes based on their This hook must have the following signature: -.. function:: your_hook(cls: type, fields: list[attr.Attribute]) -> list[attr.Attribute] +.. function:: your_hook(cls: type, fields: list[attrs.Attribute]) -> list[attrs.Attribute] :noindex: - *cls* is your class right *before* it is being converted into an attrs class. This means it does not yet have the ``__attrs_attrs__`` attribute. -- *fields* is a list of all :class:`attr.Attribute` instances that will later be set to ``__attrs_attrs__``. +- *fields* is a list of all `attrs.Attribute` instances that will later be set to ``__attrs_attrs__``. You can modify these attributes any way you want: You can add converters, change types, and even remove attributes completely or create new ones! @@ -288,7 +288,7 @@ However, the result can not always be serialized since most data types will rema To help you with this, `attr.asdict` allows you to pass a *value_serializer* hook. It has the signature -.. function:: your_hook(inst: type, field: attr.Attribute, value: typing.Any) -> typing.Any +.. function:: your_hook(inst: type, field: attrs.Attribute, value: typing.Any) -> typing.Any :noindex: .. doctest:: diff --git a/docs/how-does-it-work.rst b/docs/how-does-it-work.rst index 08367cbfd..f89974054 100644 --- a/docs/how-does-it-work.rst +++ b/docs/how-does-it-work.rst @@ -10,8 +10,9 @@ Boilerplate ``attrs`` certainly isn't the first library that aims to simplify class definition in Python. But its **declarative** approach combined with **no runtime overhead** lets it stand out. -Once you apply the ``@attr.s`` decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s. +Once you apply the ``@attrs.define`` (or ``@attr.s``) decorator to a class, ``attrs`` searches the class object for instances of ``attr.ib``\ s. Internally they're a representation of the data passed into ``attr.ib`` along with a counter to preserve the order of the attributes. +Alternatively, it's possible to define them using :doc:`types`. In order to ensure that subclassing works as you'd expect it to work, ``attrs`` also walks the class hierarchy and collects the attributes of all base classes. Please note that ``attrs`` does *not* call ``super()`` *ever*. @@ -41,7 +42,7 @@ No magic, no meta programming, no expensive introspection at runtime. Everything until this point happens exactly *once* when the class is defined. As soon as a class is done, it's done. And it's just a regular Python class like any other, except for a single ``__attrs_attrs__`` attribute that ``attrs`` uses internally. -Much of the information is accessible via `attr.fields` and other functions which can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like `attr.asdict`). +Much of the information is accessible via `attrs.fields` and other functions which can be used for introspection or for writing your own tools and decorators on top of ``attrs`` (like `attrs.asdict`). And once you start instantiating your classes, ``attrs`` is out of your way completely. @@ -53,11 +54,11 @@ This **static** approach was very much a design goal of ``attrs`` and what I str Immutability ------------ -In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises an `attr.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute. +In order to give you immutability, ``attrs`` will attach a ``__setattr__`` method to your class that raises an `attrs.exceptions.FrozenInstanceError` whenever anyone tries to set an attribute. -The same is true if you choose to freeze individual attributes using the `attr.setters.frozen` *on_setattr* hook -- except that the exception becomes `attr.exceptions.FrozenAttributeError`. +The same is true if you choose to freeze individual attributes using the `attrs.setters.frozen` *on_setattr* hook -- except that the exception becomes `attrs.exceptions.FrozenAttributeError`. -Both errors subclass `attr.exceptions.FrozenError`. +Both errors subclass `attrs.exceptions.FrozenError`. ----- diff --git a/docs/init.rst b/docs/init.rst index 4b2697898..fb276ded8 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -51,7 +51,7 @@ One thing people tend to find confusing is the treatment of private attributes t .. doctest:: - >>> import inspect, attr + >>> import inspect, attr, attrs >>> from attr import define >>> @define ... class C: @@ -162,13 +162,13 @@ If the value does not pass the validator's standards, it just raises an appropri ... ValueError: x must be smaller or equal to 42 -Again, it's important that the decorated method doesn't have the same name as the attribute and that the `field()` helper is used. +Again, it's important that the decorated method doesn't have the same name as the attribute and that the `attrs.field()` helper is used. Callables ~~~~~~~~~ -If you want to re-use your validators, you should have a look at the ``validator`` argument to `field`. +If you want to re-use your validators, you should have a look at the ``validator`` argument to `attrs.field`. It takes either a callable or a list of callables (usually functions) and treats them as validators that receive the same arguments as with the decorator approach. @@ -181,7 +181,7 @@ Since the validators run *after* the instance is initialized, you can refer to o ... raise ValueError("'x' has to be smaller than 'y'!") >>> @define ... class C: - ... x = field(validator=[attr.validators.instance_of(int), + ... x = field(validator=[attrs.validators.instance_of(int), ... x_smaller_than_y]) ... y = field() >>> C(x=3, y=4) @@ -191,10 +191,10 @@ Since the validators run *after* the instance is initialized, you can refer to o ... ValueError: 'x' has to be smaller than 'y'! -This example also shows of some syntactic sugar for using the `attr.validators.and_` validator: if you pass a list, all validators have to pass. +This example also shows of some syntactic sugar for using the `attrs.validators.and_` validator: if you pass a list, all validators have to pass. -``attrs`` won't intercept your changes to those attributes but you can always call `attr.validate` on any instance to verify that it's still valid: -When using `define` or :func:`~attr.frozen`, ``attrs`` will run the validators even when setting the attribute. +``attrs`` won't intercept your changes to those attributes but you can always call `attrs.validate` on any instance to verify that it's still valid: +When using `attrs.define` or `attrs.frozen`, ``attrs`` will run the validators even when setting the attribute. .. doctest:: @@ -210,7 +210,7 @@ When using `define` or :func:`~attr.frozen`, ``attrs`` will run the validators e >>> @define ... class C: - ... x = field(validator=attr.validators.instance_of(int)) + ... x = field(validator=attrs.validators.instance_of(int)) >>> C(42) C(x=42) >>> C("42") @@ -225,7 +225,7 @@ If you define validators both ways for an attribute, they are both ran: >>> @define ... class C: - ... x = field(validator=attr.validators.instance_of(int)) + ... x = field(validator=attrs.validators.instance_of(int)) ... @x.validator ... def fits_byte(self, attribute, value): ... if not 0 <= value < 256: @@ -243,10 +243,10 @@ If you define validators both ways for an attribute, they are both ran: And finally you can disable validators globally: - >>> attr.validators.set_disabled(True) + >>> attrs.validators.set_disabled(True) >>> C("128") C(x='128') - >>> attr.validators.set_disabled(False) + >>> attrs.validators.set_disabled(False) >>> C("128") Traceback (most recent call last): ... @@ -254,7 +254,7 @@ And finally you can disable validators globally: You can achieve the same by using the context manager: - >>> with attr.validators.disabled(): + >>> with attrs.validators.disabled(): ... C("128") C(x='128') >>> C("128") @@ -408,7 +408,7 @@ Please note that you can't directly set attributes on frozen classes: >>> FrozenBroken(1) Traceback (most recent call last): ... - attr.exceptions.FrozenInstanceError: can't set attribute + attrs.exceptions.FrozenInstanceError: can't set attribute If you need to set attributes on a frozen class, you'll have to resort to the `same trick ` as ``attrs`` and use :meth:`object.__setattr__`: diff --git a/docs/names.rst b/docs/names.rst index abfdba480..addd4ed16 100644 --- a/docs/names.rst +++ b/docs/names.rst @@ -1,7 +1,7 @@ On The Core API Names ===================== -You may be surprised seeing ``attrs`` classes being created using `attr.define` and with type annotated fields, instead of `attr.s` and `attr.ib()`. +You may be surprised seeing ``attrs`` classes being created using `attrs.define` and with type annotated fields, instead of `attr.s` and `attr.ib()`. Or, you wonder why the web and talks are full of this weird `attr.s` and `attr.ib` -- including people having strong opinions about it and using ``attr.attrs`` and ``attr.attrib`` instead. @@ -13,14 +13,15 @@ TL;DR We recommend our modern APIs for new code: -- `define()` to define a new class, -- `mutable()` is an alias for `define()`, -- :func:`~attr.frozen` is an alias for ``define(frozen=True)`` -- and `field()` to define an attribute. +- `attrs.define()` to define a new class, +- `attrs.mutable()` is an alias for `attrs.define()`, +- `attrs.frozen()` is an alias for ``define(frozen=True)`` +- and `attrs.field()` to define an attribute. They have been added in ``attrs`` 20.1.0, they are expressive, and they have modern defaults like slots and type annotation awareness switched on by default. They are only available in Python 3.6 and later. Sometimes they're referred to as *next-generation* or *NG* APIs. +As of ``attrs`` 21.3.0 you can also import them from the ``attrs`` package namespace. The traditional APIs `attr.s` / `attr.ib`, their serious business aliases ``attr.attrs`` / ``attr.attrib``, and the never-documented, but popular ``attr.dataclass`` easter egg will stay **forever**. @@ -48,7 +49,7 @@ But it was really just a way to say ``attrs`` and ``attrib``\ [#attr]_. Some people hated this cutey API from day one, which is why we added aliases for them that we called *serious business*: ``@attr.attrs`` and ``attr.attrib()``. Fans of them usually imported the names and didn't use the package name in the first place. -Unfortunately, the ``attr`` package name started creaking the moment we added `attr.Factory`, since it couldn’t be morphed into something meaningful in any way. +Unfortunately, the ``attr`` package name started creaking the moment we added ``attr.Factory``, since it couldn’t be morphed into something meaningful in any way. A problem that grew worse over time, as more APIs and even modules were added. But overall, ``attrs`` in this shape was a **huge** success -- especially after glyph's blog post `The One Python Library Everyone Needs `_ in August 2016 and `pytest `_ adopting it. @@ -96,7 +97,7 @@ We've spent years alone explaining that defining attributes using type annotatio Finally we've decided to take the `Go route `_: instead of fiddling with the old APIs -- whose names felt anachronistic anyway -- we'd define new ones, with better defaults. -So in July 2018, we `looked for better names `_ and came up with `define`, `field`, and friends. +So in July 2018, we `looked for better names `_ and came up with `attr.define`, `attr.field`, and friends. Then in January 2019, we `started looking for inconvenient defaults `_ that we now could fix without any repercussions. These APIs proved to be vastly popular, so we've finally changed the documentation to them in November of 2021. @@ -104,8 +105,12 @@ These APIs proved to be vastly popular, so we've finally changed the documentati All of this took way too long, of course. One reason is the COVID-19 pandemic, but also our fear to fumble this historic chance to fix our APIs. +Finally, in December 2021, we've added the ``attrs`` package namespace. + We hope you like the result:: + from attrs import define + @define class Point: x: int diff --git a/docs/types.rst b/docs/types.rst index 5a71c393e..a05d35f2a 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -78,7 +78,7 @@ pyright ``attrs`` provides support for pyright_ though the dataclass_transform_ specification. This provides static type inference for a subset of ``attrs`` equivalent to standard-library ``dataclasses``, -and requires explicit type annotations using the :ref:`next-gen` or ``@attr.s(auto_attribs=True)`` API. +and requires explicit type annotations using the `attrs.define` or ``@attr.s(auto_attribs=True)`` API. Given the following definition, ``pyright`` will generate static type signatures for ``SomeClass`` attribute access, ``__init__``, ``__eq__``, and comparison methods:: diff --git a/pyproject.toml b/pyproject.toml index 93145c9e3..b34ed515a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,10 @@ fail-under = 100 whitelist-regex = ["test_.*"] +[tool.check-wheel-contents] +toplevel = ["attr", "attrs"] + + [tool.isort] profile = "attrs" diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 2af76b7a8..c0a212650 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -447,6 +447,7 @@ def make_class( # these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! def asdict( inst: Any, recurse: bool = ..., diff --git a/src/attr/_config.py b/src/attr/_config.py index 6503f6fb0..546b43870 100644 --- a/src/attr/_config.py +++ b/src/attr/_config.py @@ -11,7 +11,7 @@ def set_run_validators(run): Set whether or not validators are run. By default, they are run. .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attr.validators.set_disabled()` + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` instead. """ if not isinstance(run, bool): @@ -25,7 +25,7 @@ def get_run_validators(): Return whether or not validators are run. .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attr.validators.get_disabled()` + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` instead. """ return _run_validators diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index 6ea2de0a0..2f5fae92b 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -25,7 +25,7 @@ def asdict( ``attrs``-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is - called with the `attr.Attribute` as the first argument and the + called with the `attrs.Attribute` as the first argument and the value as the second argument. :param callable dict_factory: A callable to produce dictionaries from. For example, to produce ordered dictionaries instead of normal Python @@ -204,7 +204,7 @@ def astuple( ``attrs``-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is - called with the `attr.Attribute` as the first argument and the + called with the `attrs.Attribute` as the first argument and the value as the second argument. :param callable tuple_factory: A callable to produce tuples from. For example, to produce lists instead of tuples. @@ -314,7 +314,9 @@ def assoc(inst, **changes): class. .. deprecated:: 17.1.0 - Use `evolve` instead. + Use `attrs.evolve` instead if you can. + This function will not be removed du to the slightly different approach + compared to `attrs.evolve`. """ import warnings @@ -393,8 +395,8 @@ class and you didn't pass any attribs. :raise NameError: If types cannot be resolved because of missing variables. :returns: *cls* so you can use this function also as a class decorator. - Please note that you have to apply it **after** `attr.s`. That means - the decorator has to come in the line **before** `attr.s`. + Please note that you have to apply it **after** `attrs.define`. That + means the decorator has to come in the line **before** `attrs.define`. .. versionadded:: 20.1.0 .. versionadded:: 21.1.0 *attribs* diff --git a/src/attr/_make.py b/src/attr/_make.py index 990786954..4b0d667d3 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -147,11 +147,11 @@ def attrib( is used and no value is passed while instantiating or the attribute is excluded using ``init=False``. - If the value is an instance of `Factory`, its callable will be + If the value is an instance of `attrs.Factory`, its callable will be used to construct a new value (useful for mutable data types like lists or dicts). - If a default is not set (or set manually to `attr.NOTHING`), a value + If a default is not set (or set manually to `attrs.NOTHING`), a value *must* be supplied when instantiating; otherwise a `TypeError` will be raised. @@ -164,7 +164,7 @@ def attrib( :param validator: `callable` that is called by ``attrs``-generated ``__init__`` methods after the instance has been initialized. They - receive the initialized instance, the `Attribute`, and the + receive the initialized instance, the :func:`~attrs.Attribute`, and the passed value. The return value is *not* inspected so the validator has to throw an @@ -237,10 +237,10 @@ def attrib( parameter is ignored). :param on_setattr: Allows to overwrite the *on_setattr* setting from `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. - Set to `attr.setters.NO_OP` to run **no** `setattr` hooks for this + Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this attribute -- regardless of the setting in `attr.s`. :type on_setattr: `callable`, or a list of callables, or `None`, or - `attr.setters.NO_OP` + `attrs.setters.NO_OP` .. versionadded:: 15.2.0 *convert* .. versionadded:: 16.3.0 *metadata* @@ -1286,7 +1286,7 @@ def attrs( *cmp*, or *hash* overrides whatever *auto_detect* would determine. *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - a `PythonTooOldError`. + an `attrs.exceptions.PythonTooOldError`. :param bool repr: Create a ``__repr__`` method with a human readable representation of ``attrs`` attributes.. @@ -1373,7 +1373,7 @@ def attrs( If you assign a value to those attributes (e.g. ``x: int = 42``), that value becomes the default value like if it were passed using - ``attr.ib(default=42)``. Passing an instance of `Factory` also + ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also works as expected in most cases (see warning below). Attributes annotated as `typing.ClassVar`, and attributes that are @@ -1445,7 +1445,7 @@ def attrs( the callable. If a list of callables is passed, they're automatically wrapped in an - `attr.setters.pipe`. + `attrs.setters.pipe`. :param Optional[callable] field_transformer: A function that is called with the original class object and all @@ -2037,7 +2037,7 @@ def fields(cls): :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` class. - :rtype: tuple (with name accessors) of `attr.Attribute` + :rtype: tuple (with name accessors) of `attrs.Attribute` .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields by name. @@ -2064,7 +2064,7 @@ def fields_dict(cls): class. :rtype: an ordered dict where keys are attribute names and values are - `attr.Attribute`\\ s. This will be a `dict` if it's + `attrs.Attribute`\\ s. This will be a `dict` if it's naturally ordered like on Python 3.6+ or an :class:`~collections.OrderedDict` otherwise. @@ -2951,7 +2951,7 @@ class Factory(object): """ Stores a factory callable. - If passed as the default value to `attr.ib`, the factory is used to + If passed as the default value to `attrs.field`, the factory is used to generate a new value. :param callable factory: A callable that takes either none or exactly one diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py index 843447173..27adb0f52 100644 --- a/src/attr/_next_gen.py +++ b/src/attr/_next_gen.py @@ -3,11 +3,12 @@ `attr.ib` with different default values. """ -from functools import partial -from attr.exceptions import UnannotatedAttributeError +from functools import partial from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple from ._make import ( NOTHING, _frozen_setattrs, @@ -15,6 +16,7 @@ attrib, attrs, ) +from .exceptions import UnannotatedAttributeError def define( @@ -43,8 +45,23 @@ def define( r""" Define an ``attrs`` class. - The behavioral differences to `attr.s` are the handling of the - *auto_attribs* option: + Differences to the classic `attr.s` that it uses underneath: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - If *frozen* is `False`, run converters and validators when setting an + attribute by default. + - *slots=True* (see :term:`slotted classes` for potentially surprising + behaviors) + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *match_args=True* + - Some options that were only relevant on Python 2 or were kept around for + backwards-compatibility have been removed. + + Please note that these are all defaults and you can change them as you + wish. :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves exactly like `attr.s`. If left `None`, `attr.s` will try to guess: @@ -54,8 +71,7 @@ def define( 2. Otherwise it assumes *auto_attribs=False* and tries to collect `attr.ib`\ s. - and that mutable classes (``frozen=False``) convert and validate on - ``__setattr__``. + For now, please refer to `attr.s` for the rest of the parameters. .. versionadded:: 20.1.0 .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. @@ -168,3 +184,31 @@ 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 *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, retain_collection_types=True + ) diff --git a/src/attr/converters.py b/src/attr/converters.py index 366b8728a..1dd341e44 100644 --- a/src/attr/converters.py +++ b/src/attr/converters.py @@ -14,9 +14,10 @@ __all__ = [ - "pipe", - "optional", "default_if_none", + "optional", + "pipe", + "to_bool", ] @@ -65,14 +66,14 @@ def default_if_none(default=NOTHING, factory=None): result of *factory*. :param default: Value to be used if ``None`` is passed. Passing an instance - of `attr.Factory` is supported, however the ``takes_self`` option + of `attrs.Factory` is supported, however the ``takes_self`` option is *not*. :param callable factory: A callable that takes no parameters whose result is used if ``None`` is passed. :raises TypeError: If **neither** *default* or *factory* is passed. :raises TypeError: If **both** *default* and *factory* are passed. - :raises ValueError: If an instance of `attr.Factory` is passed with + :raises ValueError: If an instance of `attrs.Factory` is passed with ``takes_self=True``. .. versionadded:: 18.2.0 diff --git a/src/attr/filters.py b/src/attr/filters.py index ae5248568..5c88280e5 100644 --- a/src/attr/filters.py +++ b/src/attr/filters.py @@ -23,7 +23,7 @@ def include(*what): Include *what*. :param what: What to include. - :type what: `list` of `type` or `attr.Attribute`\\ s + :type what: `list` of `type` or `attrs.Attribute`\\ s :rtype: `callable` """ @@ -40,7 +40,7 @@ def exclude(*what): Exclude *what*. :param what: What to exclude. - :type what: `list` of classes or `attr.Attribute`\\ s. + :type what: `list` of classes or `attrs.Attribute`\\ s. :rtype: `callable` """ diff --git a/src/attr/validators.py b/src/attr/validators.py index 3896d8346..62fcc7e1d 100644 --- a/src/attr/validators.py +++ b/src/attr/validators.py @@ -127,7 +127,7 @@ def instance_of(type): :type type: type or tuple of types :raises TypeError: With a human readable error message, the attribute - (of type `attr.Attribute`), the expected type, and the value it + (of type `attrs.Attribute`), the expected type, and the value it got. """ return _InstanceOfValidator(type) @@ -250,7 +250,7 @@ def provides(interface): :type interface: ``zope.interface.Interface`` :raises TypeError: With a human readable error message, the attribute - (of type `attr.Attribute`), the expected interface, and the + (of type `attrs.Attribute`), the expected interface, and the value it got. """ return _ProvidesValidator(interface) @@ -323,7 +323,7 @@ def in_(options): :type options: list, tuple, `enum.Enum`, ... :raises ValueError: With a human readable error message, the attribute (of - type `attr.Attribute`), the expected options, and the value it + type `attrs.Attribute`), the expected options, and the value it got. .. versionadded:: 17.1.0 @@ -362,7 +362,7 @@ def is_callable(): .. versionadded:: 19.1.0 :raises `attr.exceptions.NotCallableError`: With a human readable error - message containing the attribute (`attr.Attribute`) name, + message containing the attribute (`attrs.Attribute`) name, and the value it got. """ return _IsCallableValidator() diff --git a/src/attrs/__init__.py b/src/attrs/__init__.py new file mode 100644 index 000000000..7c8c11f04 --- /dev/null +++ b/src/attrs/__init__.py @@ -0,0 +1,68 @@ +from attr import ( + NOTHING, + Attribute, + Factory, + __author__, + __copyright__, + __description__, + __doc__, + __email__, + __license__, + __title__, + __url__, + __version__, + __version_info__, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._next_gen import asdict, astuple + +from . import converters, exceptions, filters, setters, validators + + +__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/src/attrs/__init__.pyi b/src/attrs/__init__.pyi new file mode 100644 index 000000000..7426fa5dd --- /dev/null +++ b/src/attrs/__init__.pyi @@ -0,0 +1,63 @@ +from typing import ( + Any, + Callable, + Dict, + Mapping, + Optional, + Sequence, + Tuple, + Type, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import _FilterType +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import define as define +from attr import evolve as evolve +from attr import Factory as Factory +from attr import exceptions as exceptions +from attr import field as field +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import frozen as frozen +from attr import has as has +from attr import make_class as make_class +from attr import mutable as mutable +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators + +# TODO: see definition of attr.asdict/astuple +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[ + Callable[[type, Attribute[Any], Any], Any] + ] = ..., + tuple_keys: bool = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... diff --git a/src/attrs/converters.py b/src/attrs/converters.py new file mode 100644 index 000000000..c2b3cfb26 --- /dev/null +++ b/src/attrs/converters.py @@ -0,0 +1 @@ +from attr.converters import * # noqa diff --git a/src/attrs/exceptions.py b/src/attrs/exceptions.py new file mode 100644 index 000000000..2b2bc3c04 --- /dev/null +++ b/src/attrs/exceptions.py @@ -0,0 +1 @@ +from attr.exceptions import * # noqa diff --git a/src/attrs/filters.py b/src/attrs/filters.py new file mode 100644 index 000000000..cb843cac5 --- /dev/null +++ b/src/attrs/filters.py @@ -0,0 +1 @@ +from attr.filters import * # noqa diff --git a/src/attrs/py.typed b/src/attrs/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/attrs/setters.py b/src/attrs/setters.py new file mode 100644 index 000000000..348aa3b1b --- /dev/null +++ b/src/attrs/setters.py @@ -0,0 +1 @@ +from attr.setters import * # noqa diff --git a/src/attrs/validators.py b/src/attrs/validators.py new file mode 100644 index 000000000..ad46fbb03 --- /dev/null +++ b/src/attrs/validators.py @@ -0,0 +1 @@ +from attr.validators import * # noqa diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py index a2ed7fe67..7f5aff75c 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,21 +350,89 @@ 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 d = D() assert d.x == d.xx() + + +class TestAsTuple: + def test_smoke(self): + """ + `attrs.astuple` only changes defaults, so we just call it and compare. + """ + inst = C("foo", 42) + + assert attrs.astuple(inst) == _attr.astuple(inst) + + +class TestAsDict: + def test_smoke(self): + """ + `attrs.asdict` only changes defaults, so we just call it and compare. + """ + inst = C("foo", {(1,): 42}) + + assert attrs.asdict(inst) == _attr.asdict( + inst, retain_collection_types=True + ) + + +class TestImports: + """ + Verify our re-imports and mirroring works. + """ + + def test_converters(self): + """ + Importing from attrs.converters works. + """ + from attrs.converters import optional + + assert optional is _attr.converters.optional + + def test_exceptions(self): + """ + Importing from attrs.exceptions works. + """ + from attrs.exceptions import FrozenError + + assert FrozenError is _attr.exceptions.FrozenError + + def test_filters(self): + """ + Importing from attrs.filters works. + """ + from attrs.filters import include + + assert include is _attr.filters.include + + def test_setters(self): + """ + Importing from attrs.setters works. + """ + from attrs.setters import pipe + + assert pipe is _attr.setters.pipe + + def test_validators(self): + """ + Importing from attrs.validators works. + """ + from attrs.validators import and_ + + assert and_ is _attr.validators.and_ diff --git a/tests/typing_example.py b/tests/typing_example.py index 3fced27ee..efacda2a2 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Tuple, Union import attr +import attrs # Typing via "type" Argument --- @@ -59,6 +60,14 @@ class FF: z: Any = attr.ib() +@attrs.define +class FFF: + z: int + + +FFF(1) + + # Inheritance -- @@ -96,6 +105,19 @@ class Error(Exception): str(e) +@attrs.define +class Error2(Exception): + x: int + + +try: + raise Error2(1) +except Error as e: + e.x + e.args + str(e) + + # Converters # XXX: Currently converters can only be functions so none of this works # although the stubs should be correct. @@ -179,7 +201,7 @@ class Validated: validator=attr.validators.instance_of((int, str)) ) k: Union[int, str, C] = attr.ib( - validator=attr.validators.instance_of((int, C, str)) + validator=attrs.validators.instance_of((int, C, str)) ) @@ -188,9 +210,17 @@ class Validated2: num: int = attr.field(validator=attr.validators.ge(0)) +@attrs.define +class Validated3: + num: int = attr.field(validator=attr.validators.ge(0)) + + with attr.validators.disabled(): Validated2(num=-1) +with attrs.validators.disabled(): + Validated3(num=-1) + try: attr.validators.set_disabled(True) Validated2(num=-1) @@ -207,6 +237,14 @@ class WithCustomRepr: d: bool = attr.ib(repr=str) +@attrs.define +class WithCustomRepr2: + a: int = attrs.field(repr=True) + b: str = attrs.field(repr=False) + c: str = attrs.field(repr=lambda value: "c is for cookie") + d: bool = attrs.field(repr=str) + + # Check some of our own types @attr.s(eq=True, order=False) class OrderFlags: @@ -228,16 +266,43 @@ class ValidatedSetter: ) +@attrs.define(on_setattr=attr.setters.validate) +class ValidatedSetter2: + a: int + b: str = attrs.field(on_setattr=attrs.setters.NO_OP) + c: bool = attrs.field(on_setattr=attrs.setters.frozen) + d: int = attrs.field( + on_setattr=[attrs.setters.convert, attrs.setters.validate] + ) + e: bool = attrs.field( + on_setattr=attrs.setters.pipe( + attrs.setters.convert, attrs.setters.validate + ) + ) + + # field_transformer def ft_hook(cls: type, attribs: List[attr.Attribute]) -> List[attr.Attribute]: return attribs +# field_transformer +def ft_hook2( + cls: type, attribs: List[attrs.Attribute] +) -> List[attrs.Attribute]: + return attribs + + @attr.s(field_transformer=ft_hook) class TransformedAttrs: x: int +@attrs.define(field_transformer=ft_hook2) +class TransformedAttrs2: + x: int + + # Auto-detect @attr.s(auto_detect=True) class AutoDetect: @@ -276,6 +341,11 @@ class NGFrozen: a.evolve(repr=False) +attrs.fields(NGFrozen).x.evolve(eq=False) +a = attrs.fields(NGFrozen).x +a.evolve(repr=False) + + @attr.s(collect_by_mro=True) class MRO: pass @@ -288,6 +358,17 @@ class FactoryTest: c: List[int] = attr.ib(default=attr.Factory((lambda s: s.a), True)) +@attrs.define +class FactoryTest2: + a: List[int] = attrs.field(default=attrs.Factory(list)) + b: List[Any] = attrs.field(default=attrs.Factory(list, False)) + c: List[int] = attrs.field(default=attrs.Factory((lambda s: s.a), True)) + + +attrs.asdict(FactoryTest2()) +attr.asdict(FactoryTest(), tuple_keys=True) + + # Check match_args stub @attr.s(match_args=False) class MatchArgs: @@ -297,3 +378,41 @@ class MatchArgs: attr.asdict(FactoryTest()) attr.asdict(FactoryTest(), retain_collection_types=False) + + +# Check match_args stub +@attrs.define(match_args=False) +class MatchArgs2: + a: int + b: int + + +# NG versions of asdict/astuple +attrs.asdict(MatchArgs2(1, 2)) +attrs.astuple(MatchArgs2(1, 2)) + + +def importing_from_attr() -> None: + """ + Use a function to keep the ns clean. + """ + from attr.converters import optional + from attr.exceptions import FrozenError + from attr.filters import include + from attr.setters import frozen + from attr.validators import and_ + + assert optional and FrozenError and include and frozen and and_ + + +def importing_from_attrs() -> None: + """ + Use a function to keep the ns clean. + """ + from attrs.converters import optional + from attrs.exceptions import FrozenError + from attrs.filters import include + from attrs.setters import frozen + from attrs.validators import and_ + + assert optional and FrozenError and include and frozen and and_