Repository: python-attrs/attrs
Pull request #669 (2020-08-20 13:56:01)

Add attr.field
Almost forgot the last part of the NG API puzzle because it doesn't change substantially.

Does this look OK so far?

euresti: Looks good. But you are not making my mypy job easy!!  :)  J/K progress is important.
hynek: > Looks good. But you are not making my mypy job easy!! :) J/K progress is important.

Heh I assure you that I feel genuinely sorry whenever I do this. Your struggle is always on my mind. /o\

Patch:
diff --git a/changelog.d/669.change.rst b/changelog.d/669.change.rst
new file mode 100644
index 0000000..9b83d90
--- /dev/null
+++ b/changelog.d/669.change.rst
@@ -0,0 +1,5 @@
+We have also provisionally added ``attr.field()`` that supplants ``attr.ib()``.
+It also requires at least Python 3.6 and is keyword-only.
+Other than that, it only dropped a few arguments, but changed no defaults.
+
+As with ``attr.s()``: ``attr.ib()`` is not going anywhere.
diff --git a/docs/api.rst b/docs/api.rst
index 7797e65..63640dc 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -622,10 +622,16 @@ Therefore your constructive feedback in the linked issues above is strongly enco
 
    Alias for `attr.define`.
 
+   .. versionadded:: 20.1.0
+
 .. function:: attr.frozen(same_as_define)
 
    Behaves the same as `attr.define` but sets *frozen=True* and *on_setattr=None*.
 
+   .. versionadded:: 20.1.0
+
+.. autofunction:: attr.field
+
 
 Deprecated APIs
 ---------------
diff --git a/src/attr/__init__.py b/src/attr/__init__.py
index 572daaf..1475976 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -71,6 +71,6 @@ __all__ = [
 ]
 
 if sys.version_info[:2] >= (3, 6):
-    from ._next_gen import define, frozen, mutable
+    from ._next_gen import define, field, frozen, mutable
 
-    __all__.extend((define, frozen, mutable))
+    __all__.extend((define, field, frozen, mutable))
diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index f3d6bb6..0869914 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -182,6 +182,77 @@ def attrib(
     on_setattr: Optional[_OnSetAttrArgType] = ...,
 ) -> Any: ...
 @overload
+def field(
+    *,
+    default: None = ...,
+    validator: None = ...,
+    repr: _ReprArgType = ...,
+    hash: Optional[bool] = ...,
+    init: bool = ...,
+    metadata: Optional[Mapping[Any, Any]] = ...,
+    converter: None = ...,
+    factory: None = ...,
+    kw_only: bool = ...,
+    eq: Optional[bool] = ...,
+    order: Optional[bool] = ...,
+    on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> Any: ...
+
+# This form catches an explicit None or no default and infers the type from the
+# other arguments.
+@overload
+def field(
+    *,
+    default: None = ...,
+    validator: Optional[_ValidatorArgType[_T]] = ...,
+    repr: _ReprArgType = ...,
+    hash: Optional[bool] = ...,
+    init: bool = ...,
+    metadata: Optional[Mapping[Any, Any]] = ...,
+    converter: Optional[_ConverterType] = ...,
+    factory: Optional[Callable[[], _T]] = ...,
+    kw_only: bool = ...,
+    eq: Optional[bool] = ...,
+    order: Optional[bool] = ...,
+    on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> _T: ...
+
+# This form catches an explicit default argument.
+@overload
+def field(
+    *,
+    default: _T,
+    validator: Optional[_ValidatorArgType[_T]] = ...,
+    repr: _ReprArgType = ...,
+    hash: Optional[bool] = ...,
+    init: bool = ...,
+    metadata: Optional[Mapping[Any, Any]] = ...,
+    converter: Optional[_ConverterType] = ...,
+    factory: Optional[Callable[[], _T]] = ...,
+    kw_only: bool = ...,
+    eq: Optional[bool] = ...,
+    order: Optional[bool] = ...,
+    on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> _T: ...
+
+# This form covers type=non-Type: e.g. forward references (str), Any
+@overload
+def field(
+    *,
+    default: Optional[_T] = ...,
+    validator: Optional[_ValidatorArgType[_T]] = ...,
+    repr: _ReprArgType = ...,
+    hash: Optional[bool] = ...,
+    init: bool = ...,
+    metadata: Optional[Mapping[Any, Any]] = ...,
+    converter: Optional[_ConverterType] = ...,
+    factory: Optional[Callable[[], _T]] = ...,
+    kw_only: bool = ...,
+    eq: Optional[bool] = ...,
+    order: Optional[bool] = ...,
+    on_setattr: Optional[_OnSetAttrArgType] = ...,
+) -> Any: ...
+@overload
 def attrs(
     maybe_cls: _C,
     these: Optional[Dict[str, Any]] = ...,
diff --git a/src/attr/_next_gen.py b/src/attr/_next_gen.py
index 8312d21..b0aefe3 100644
--- a/src/attr/_next_gen.py
+++ b/src/attr/_next_gen.py
@@ -10,7 +10,7 @@ from functools import partial
 from attr.exceptions import UnannotatedAttributeError
 
 from . import setters
-from ._make import attrs
+from ._make import NOTHING, attrib, attrs
 
 
 def define(
@@ -32,7 +32,7 @@ def define(
     order=False,
     auto_detect=True,
     getstate_setstate=None,
-    on_setattr=setters.validate
+    on_setattr=setters.validate,
 ):
     r"""
     The only behavioral difference is the handling of the *auto_attribs*
@@ -84,3 +84,40 @@ def define(
 
 mutable = define
 frozen = partial(define, frozen=True, on_setattr=None)
+
+
+def field(
+    *,
+    default=NOTHING,
+    validator=None,
+    repr=True,
+    hash=None,
+    init=True,
+    metadata=None,
+    converter=None,
+    factory=None,
+    kw_only=False,
+    eq=None,
+    order=None,
+    on_setattr=None,
+):
+    """
+    Identical to `attr.ib`, except keyword-only and with some arguments
+    removed.
+
+    .. versionadded:: 20.1.0
+    """
+    return attrib(
+        default=default,
+        validator=validator,
+        repr=repr,
+        hash=hash,
+        init=init,
+        metadata=metadata,
+        converter=converter,
+        factory=factory,
+        kw_only=kw_only,
+        eq=eq,
+        order=order,
+        on_setattr=on_setattr,
+    )


Test patch:
diff --git a/tests/test_next_gen.py b/tests/test_next_gen.py
index c1a85a9..e350498 100644
--- a/tests/test_next_gen.py
+++ b/tests/test_next_gen.py
@@ -40,7 +40,7 @@ class TestNextGen:
 
         @attr.define
         class Validated:
-            x: int = attr.ib(validator=attr.validators.instance_of(int))
+            x: int = attr.field(validator=attr.validators.instance_of(int))
 
         v = Validated(1)
 
@@ -67,13 +67,13 @@ class TestNextGen:
         """
         Don't guess if auto_attrib is set explicitly.
 
-        Having an unannotated attr.ib fails.
+        Having an unannotated attr.ib/attr.field fails.
         """
         with pytest.raises(attr.exceptions.UnannotatedAttributeError):
 
             @attr.define(auto_attribs=True)
             class ThisFails:
-                x = attr.ib()
+                x = attr.field()
                 y: int
 
     def test_override_auto_attribs_false(self):
@@ -97,7 +97,7 @@ class TestNextGen:
 
         @attr.define
         class OldSchool:
-            x = attr.ib()
+            x = attr.field()
 
         assert OldSchool(1) == OldSchool(1)
 
diff --git a/tests/typing_example.py b/tests/typing_example.py
index 1f7854a..878837b 100644
--- a/tests/typing_example.py
+++ b/tests/typing_example.py
@@ -211,7 +211,7 @@ class ValidatedSetter:
 # Provisional APIs
 @attr.define(order=True)
 class NGClass:
-    x: int
+    x: int = attr.field(default=42)
 
 
 # XXX: needs support in mypy