Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attr.define/mutable/frozen #666

Merged
merged 11 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ jobs:
- name: "Run tox targets for ${{ matrix.python-version }}"
run: "python -m tox"

- uses: "actions/setup-python@v2"
with:
python-version: "3.8"

- name: "Combine coverage"
run: |
set -xe
python -m pip install coverage[toml]
python -m coverage combine
python -m coverage xml
if: "contains(env.USING_COVERAGE, matrix.python-version)"
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/408.deprecation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ If this is a problem for you for some reason, please report it to our bug tracke

The old ``attr`` namespace isn't going anywhere and its defaults are not changing – this is a purely additive measure.
Please check out the linked issue for more details.

These new APIs have been added *provisionally* as part of `#666 <https://github.com/python-attrs/attrs/pull/666>`_ so you can try them out today and provide feedback.
Learn more in the `API docs <https://www.attrs.org/en/stable/api.html#provisional-apis>`_.
5 changes: 5 additions & 0 deletions changelog.d/666.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**Provisional** APIs called ``attr.define()``, ``attr.mutable()``, and ``attr.frozen()`` have been added.

They are only available on Python 3.6 and later, and call ``attr.s()`` with different default values.

If nothing comes up, they will become the official way for creating classes in 20.2.0 (see above).
6 changes: 5 additions & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ def pytest_configure(config):
collect_ignore = []
if sys.version_info[:2] < (3, 6):
collect_ignore.extend(
["tests/test_annotations.py", "tests/test_init_subclass.py"]
[
"tests/test_annotations.py",
"tests/test_init_subclass.py",
"tests/test_next_gen.py",
]
)
47 changes: 47 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
Core
----


.. warning::
As of ``attrs`` 20.1.0, it also ships with a bunch of provisional APIs that are intended to become the main way of defining classes in the future.

Please have a look at :ref:`prov`.

.. autodata:: attr.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)
Expand Down Expand Up @@ -580,6 +586,47 @@ These are helpers that you can use together with `attr.s`'s and `attr.ib`'s ``on
N.B. Please use `attr.s`'s *frozen* argument to freeze whole classes; it is more efficient.


.. _prov:

Provisional APIs
----------------

These are Python 3.6 and later-only, keyword-only, and **provisional** 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`)
- *auto_exc=True*
- *auto_detect=True*
- *eq=True*, but *order=False*
- Validators run when you set an attribute (*on_setattr=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.

----

Their behavior is scheduled to become part of the upcoming ``import attrs`` that will introduce a new namespace with nicer names and nicer defaults (see `#408 <https://github.com/python-attrs/attrs/issues/408>`_ and `#487 <https://github.com/python-attrs/attrs/issues/487>`_).

Therefore your constructive feedback in the linked issues above is strongly encouraged!

.. note::
Provisional doesn't mean we will remove it (although it will be deprecated once the final form is released), but that it might change if we receive relevant feedback.

`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:: attr.mutable(same_as_define)

Alias for `attr.define`.

.. function:: attr.frozen(same_as_define)

Behaves the same as `attr.define` but sets *frozen=True* and *on_setattr=None*.


Deprecated APIs
---------------

Expand Down
8 changes: 7 additions & 1 deletion src/attr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import absolute_import, division, print_function

import sys

from functools import partial

from . import converters, exceptions, filters, setters, validators
Expand Down Expand Up @@ -39,7 +41,6 @@
ib = attr = attrib
dataclass = partial(attrs, auto_attribs=True) # happy Easter ;)


__all__ = [
"Attribute",
"Factory",
Expand Down Expand Up @@ -68,3 +69,8 @@
"validate",
"validators",
]

if sys.version_info[:2] >= (3, 6):
hynek marked this conversation as resolved.
Show resolved Hide resolved
from ._next_gen import define, frozen, mutable

__all__.extend((define, frozen, mutable))
86 changes: 86 additions & 0 deletions src/attr/_next_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
This is a Python 3.6 and later-only, keyword-only, and **provisional** API that
calls `attr.s` with different default values.

Provisional APIs that shall become "import attrs" one glorious day.
"""

from functools import partial

from attr.exceptions import UnannotatedAttributeError

from . import setters
from ._make import attrs


def define(
maybe_cls=None,
*,
these=None,
repr=None,
hash=None,
init=None,
slots=True,
frozen=False,
weakref_slot=True,
str=False,
auto_attribs=None,
kw_only=False,
cache_hash=False,
auto_exc=True,
eq=True,
order=False,
auto_detect=True,
getstate_setstate=None,
on_setattr=setters.validate
):
r"""
The only behavioral difference is the handling of the *auto_attribs*
option:

: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:

1. If all attributes are annotated and no `attr.ib` is found, it assumes
*auto_attribs=True*.
2. Otherwise it assumes *auto_attribs=False* and tries to collect
`attr.ib`\ s.


.. versionadded:: 20.1.0
"""

def do_it(auto_attribs):
return attrs(
maybe_cls=maybe_cls,
these=these,
repr=repr,
hash=hash,
init=init,
slots=slots,
frozen=frozen,
weakref_slot=weakref_slot,
str=str,
auto_attribs=auto_attribs,
kw_only=kw_only,
cache_hash=cache_hash,
auto_exc=auto_exc,
eq=eq,
order=order,
auto_detect=auto_detect,
collect_by_mro=True,
getstate_setstate=getstate_setstate,
on_setattr=on_setattr,
)

if auto_attribs is not None:
return do_it(auto_attribs)

try:
return do_it(True)
except UnannotatedAttributeError:
return do_it(False)


mutable = define
frozen = partial(define, frozen=True, on_setattr=None)
hynek marked this conversation as resolved.
Show resolved Hide resolved
135 changes: 135 additions & 0 deletions tests/test_next_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Python 3-only integration tests for provisional next generation APIs.
"""

import pytest

import attr


@attr.define
class C:
x: str
y: int


class TestNextGen:
def test_simple(self):
"""
Instantiation works.
"""
C("1", 2)

def test_no_slots(self):
"""
slots can be deactivated.
"""

@attr.define(slots=False)
class NoSlots:
x: int

ns = NoSlots(1)

assert {"x": 1} == getattr(ns, "__dict__")

def test_validates(self):
"""
Validators at __init__ and __setattr__ work.
"""

@attr.define
class Validated:
x: int = attr.ib(validator=attr.validators.instance_of(int))

v = Validated(1)

with pytest.raises(TypeError):
Validated(None)

with pytest.raises(TypeError):
v.x = "1"

def test_no_order(self):
"""
Order is off by default but can be added.
"""
with pytest.raises(TypeError):
C("1", 2) < C("2", 3)

@attr.define(order=True)
class Ordered:
x: int

assert Ordered(1) < Ordered(2)

def test_override_auto_attribs_true(self):
"""
Don't guess if auto_attrib is set explicitly.

Having an unannotated attr.ib fails.
"""
with pytest.raises(attr.exceptions.UnannotatedAttributeError):

@attr.define(auto_attribs=True)
class ThisFails:
x = attr.ib()
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.
"""

@attr.define(auto_attribs=False)
class NoFields:
x: int
y: int

assert NoFields() == NoFields()

def test_auto_attribs_detect(self):
"""
define correctly detects if a class lacks type annotations.
"""

@attr.define
class OldSchool:
x = attr.ib()

assert OldSchool(1) == OldSchool(1)

def test_exception(self):
"""
Exceptions are detected and correctly handled.
"""

@attr.define
class E(Exception):
msg: str
other: int

with pytest.raises(E) as ei:
raise E("yolo", 42)

e = ei.value

assert ("yolo", 42) == e.args
assert "yolo" == e.msg
assert 42 == e.other

def test_frozen(self):
"""
attr.frozen freezes classes.
"""

@attr.frozen
class F:
x: str

f = F(1)

with pytest.raises(attr.exceptions.FrozenInstanceError):
f.x = 2