Repository: python-attrs/attrs
Pull request #607 (2020-01-03 13:24:16)

Add auto-detection of self-implemented methods
This adds the feature to auto-detect existing methods so the creation doesn't have to be prevented by hand. This has been confusing our users for a while (see #324, #416).  It should become `True` by default when the new APIs emerge (attr.auto/attrs.define – this is actually one of the last blockers for me to start working on them).

Typing requires an update to the mypy plugin to work correctly (currently it says `tests/typing_example.py:193: error: Name '__init__' already defined on line 190`).

There is really only one controversial open question: if a method is detected, should it just prevent the one method or all of the related ones? E.g. if I define `__eq__`, what about `__ne__`?

I have decided to take the presence of _any_ method that belongs to a flag to set the flag to False because it's easier to reason about. I'm open to discussion tho.

euresti: Is the concept with `__init__` that it replaces the auto-created init, or that it gets turned into `post_attr_init_`?
hynek: Replaces. In the case of init we could consider special-case it to optionally create a `__attrs_init__` that the user can call whenever to fix the “how can I call super()” problem forever.
wsanchez: > There is really only one controversial open question: if a method is detected, should it just prevent the one method or all of the related ones? E.g. if I define `__eq__`, what about `__ne__`?

As a user, I would expect it to work like `functools.total_ordering`.
hynek: > > There is really only one controversial open question: if a method is detected, should it just prevent the one method or all of the related ones? E.g. if I define `__eq__`, what about `__ne__`?
> 
> As a user, I would expect it to work like `functools.total_ordering`.

Hm that makes it a lot more complex to both implement and reason about. 🤔 The current approach is about detecting implied settings, yours would be very different and then the option should be called `do_no_overwrite_own` or something?
wsanchez: > > > There is really only one controversial open question: if a method is detected, should it just prevent the one method or all of the related ones? E.g. if I define `__eq__`, what about `__ne__`?
> > 
> > 
> > As a user, I would expect it to work like `functools.total_ordering`.
> 
> Hm that makes it a lot more complex to both implement and reason about. 🤔 The current approach is about detecting implied settings, yours would be very different and then the option should be called `do_no_overwrite_own` or something?

Yeah, that's harder, but I think it does fall solidly into the "remove boilerplate" goal of `attrs`.

That said, if we want to keep things simple, then I'd definitely favor disabling all related methods.  My reasoning there is that I suspect that the alternative is complicated in a worse way than `total_ordering`:

If, for example, you only see `__eq__` in a class and you only disable `__eq__` and leave the `attrs`-generated `__ne__` in place, then we'd have to document the implementation of `__ne__` (e.g. `NotImplemented` if a different class, or inverse of `__eq__`) and call that part of the API.  That seems straightforward enough, I guess (though committing to an implementation, even a very simple one, makes me nervous).

But then what happens if you instead find a class that only defines `__ne__`?  Does `__eq__` simply invert that or do what it currently does?
hynek: Yeah this is exactly why I’m apprehensive of the granular version. I’m already seeing the issues and questions from confused people. 

Couldn’t people actually _use_ total_ordering?
wsanchez: > Yeah this is exactly why I’m apprehensive of the granular version. I’m already seeing the issues and questions from confused people.

For sure.  What I mean is that for you "should it just prevent the one method or all of the related ones?" question, I would opt for the latter. Otherwise, you either have to explain how what is left is implemented.

> Couldn’t people actually use total_ordering?

Sure.  Couldn't `auto_detect` actually use total_ordering? :-)
hynek: > > Couldn’t people actually use total_ordering?
> 
> Sure. Couldn't `auto_detect` actually use `total_ordering`? :-)

It could but it would introduce an unprecedented amount of magic. I really prefer to add one line and be clear about what's happening here.

Unless I'm missing something, I think I've made up my mind here. I should add a functional test that verifies that people actually _can_ use `total_ordering`.
hynek: I have added an example for `@total_ordering` in 2cd16a2fe825887008084b618c0e3bcba2a88b55.
hynek: Honestly to me this is much less about the complexity to implement it (that's a one-time deal mostly) and much more about the clarity and obviousness when looking at attrs-using code.

I don't think expecting people to add one line to their code is too much to ask, if it makes the whole thing more explicit and clear about what happens. Also once we implement #602 (which after this lands might be a very good opportunity), the users can _directly_ deduct `attrs` behavior from the settings.

`attrs` was always about saving boilerplate but it also was always about being explicit and clear.  Here we have to choose what is more important and my gut feeling is telling me that clarity wins.
pganssle: Well, my contention is that there are two very intuitive options here and I think *either* option would be surprising to some people because the semantics of this are simply not obvious.

One problem I have with the "specifying any one method flips the default for *all* methods" version is that it seems very unlikely that I would want that behavior, so I'd want to have to be explicit about it. If you make it so that specifying `__lt__` silently flips `ord=True` to `ord=False`, I think you'll get a lot of users confused as to why `x > y` is not working as they expected.

I think some people *will* want to explicitly define a partial ordering, but those people will know that they want it, and they'll either implement all the methods or they can explicitly provide `ord=False`.

Given the asymmetry in expectations, I would think that the intuitive options are:

1. Specifying a partial ordering / partial equality with auto detection and `eq`/`ord` not explicitly set raises an exception (so anyone who thinks it works like the second option are put on notice).
2. Specifying a partial ordering / partial equality with auto detection defines a total ordering in terms of the user-defined methods.

I suspect that if you go with the first option, you'll get a bunch of people who say, "Why raise an exception, there's not really another thing you'd want to do here!" Of course, people thoughtlessly complain about everything with non-obvious semantics as if the semantics should be obvious, so that's not necessarily a lot to go on.

That said, maybe I'm completely missing some edge cases where this could lead to major footguns, so I'm totally willing to be convinced.

As I said before, if we go with option 2 (throwing in the ambiguous case), we *should* have the option to switch to option 1 in the future in a backwards-compatible way if it turns out I'm right about the asymmetry in use cases and intuition.
wsanchez: I've come around to agreeing with @hynek about users just using `total_ordering` explicitly if that's what they want.

I'm sympathetic to the argument that if `ord=true` (or unspecified, since it's normally the default), that rather than altering the default behavior when you add an `__lt__` method, raising an exception may be more correct in a sense. But `attrs` changes the defaults in other similar cases, so I don't think that we should inconsistent about that in this one.
pganssle: Hm, I feel like I must be missing something, because my most recent post started out conceding the point and as I started trying to write out my reasoning for that I ended up going in exactly the opposite direction.

For me the issue comes down to what behavior users would want and what they would expect. I would think that most people who define a partial ordering *want* a total ordering. I can think of very few scenarios where that isn't the case, and in all of those scenarios, I'd be trying to craft specific semantics and I would *test those semantics explicitly*.

To me it seems like the fraction of people who don't want total order extrapolation, are expecting that defining a partial order will set `ord=False` *and* who don't carefully test their implementation is very low. On the other hand, in addition to the prior of "the user wants total ordering" being much higher, I think the fraction of those people who don't carefully test the ordering semantics is higher as well, so I'd expect a lot of broken classes being deployed unless `ord=None` + partial ordering = total ordering OR `ord=None` + partial ordering = exception.

But maybe my priors are way off. Do you disagree with my premises, my reasoning or am I maybe missing another line of reasoning here?

(Note: I will admit that the "should you generate `__eq__` when only `__ne__`" question almost had me change my mind here, since generating `__eq__` from `__ne__` seems weird, but having `eq` do something other than `ord` would be very bad. In the end, I decided that these practical concerns override my squeamishness about generating an `__ne__`).
hynek: As Paul wrote correctly: no matter what way we go, _someone_ is going to be confused. I think this is the premise that everything starts from.

---

There has been a lot of text and I'm a bit lost, but it seems to me, that Paul is arguing for an implicit total ordering?

As I've laid out before, I don't like it because it it's too much implicit behavior. I _like_ being able to look at an `attrs` class and _know_ what's happening without considering all kinds of implications and I think asking the user to add one line `@functools.total_ordering` is a good compromise here.

Making the existence of methods switch off a switch is a very clear semantic that can be easily communicated: "**if** you implement `__eq__` **then** `eq=False`". This is not true if we just skip implementing certain methods based on their presence. The value of `eq` suddenly means nothing anymore. There's no way we make it the behavior introspectable in any way.

This feels harder to understand and harder to reason about – especially if we magically start applying a total ordering to the class.

But maybe I'm just misunderstanding? We def – at least partially – argue about different things here.

---

Random nit: in future APIs `ord` will be False by default I think. Most people don't need it so it's unnecessary baggage.
pganssle: > There has been a lot of text and I'm a bit lost, but it seems to me, that Paul is arguing for an implicit total ordering?

I like implicit total ordering as the behavior of `ord=True` (and the default behavior if `ord` defaults to `True`) when a partial ordering is defined, but I am not sure I totally grok the arguments against it since yes it is a bit magical but I think in the sense that it will magically do what almost everyone wants and probably won't bother the people who don't want that behavior. I'm definitely willing to concede that you and @wsanchez understand the ways the semantics can get hairy better than me, so my position is "I'm probably wrong about this."

The thing I am more confident about is that if you don't go with total ordering, I think that unless `ord` is explicitly set to `False`, I think it should be an exception to define a partial ordering, because I think almost no one actually *wants* a partial ordering, and it's better to loudly fail and say, "If you really want a partial ordering, set `ord=False`, if you want a total ordering, using `functools.total_ordering`".

<hr>

> Making the existence of methods switch off a switch is a very clear semantic that can be easily communicated: "**if** you implement `__eq__` **then** `eq=False`". This is not true if we just skip implementing certain methods based on their presence. The value of `eq` suddenly means nothing anymore. There's no way we make it the behavior introspectable in any way.

This is very fair, and I think I'm starting to understand the concern here. I was thinking that `eq=True` would mean "this class is guaranteed to define both `__eq__` and `__ne__`" and `ord=True` would mean "this class is guaranteed to define a total ordering" and not necessarily "if x is `True`/`False`, attrs generates these methods". I think that's fairly easy to grok, but if you think that what people care about is what methods `attr.s` will generate or if exposing that information in a public interface is a critical design goal, then I concede the point about total ordering (though I'm still not quite convinced on the point of silent vs. exception).
hynek: Paul, to sum it up (and me stopping procrastinating on this): you would be (begrudgingly) OK with the current implementation, if we raise a Warning if the the user forgets to implement certain methods.

Am I understanding you correctly?
pganssle: @hynek Yeah, that sounds about right.
hynek: So I've implemented the warning and realized that there's a problem: we'd force the decorator order on the users.

```python
@attr.s(auto_detect=True)
@total_ordering
class C:
    ...
```

would be fine while

```python
@total_ordering
@attr.s(auto_detect=True)
class C:
    ...
```

would annoy the users.

I'm not really sure what to do here? 
pganssle: Hm... That is indeed annoying.

I think it really depends on something I don't have a huge amount of information about, which is the baseline rates of this kind of confusion. If a lot of people are going to be silently getting broken behavior from this because they don't know they need to use `functools.total_ordering` with `auto_attrib=True` if they want to define a custom comparison, then it's probably justified to warn people who do the decorator in the wrong order (possibly with an additional line like, "Make sure the @attr.s decorator is on top to avoid this warning").

If almost everyone who writes one knows to use `functools.total_ordering` but tends to put it outside the `attr.s` decorator, then it's just annoying people for no reason.

Luckily, I don't think either decision is irreversible (at least when deciding between warning / nothing rather than deciding between exception / warning / nothing). You can always start raising a warning later if you find people are having this problem a lot, or you can start with the warning and remove it if people complain a lot.

I think you *probably* have a better handle than me on what attrs users will want, so I'm confident that whatever you choose is right. Sorry if all I've managed to add to this thread was delays and extra noise.
hynek: > You can always start raising a warning later if you find people are having this problem a lot, or you can start with the warning and remove it if people complain a lot.

Yeah I think we'll go without for now – with a fat warning (that nobody will read).

> I think you probably have a better handle than me on what attrs users will want, so I'm confident that whatever you choose is right.

With 26 million downloads per month, I don't think there's such a thing as an “attrs user”. :)  I guess the best thing I can do is to build something _I_ want to use.

> Sorry if all I've managed to add to this thread was delays and extra noise.

Heh delays caused by food for thought are _always_ very welcome.
hynek: Since I believe we have agreement on the design/API, I hope someone will find the time to give the implementation a review. ❤️🐶
hynek: OK I don't think waiting any further will make anyone come forward so I'm just YOLOing it in.  Otherwise I'll never get Operation `import attrs` done.

Patch:
diff --git a/changelog.d/607.change.rst b/changelog.d/607.change.rst
new file mode 100644
index 0000000..e785ae1
--- /dev/null
+++ b/changelog.d/607.change.rst
@@ -0,0 +1,5 @@
+``attrs`` can now automatically detect your own implementations and infer ``init=False``, ``repr=False``, ``eq=False``, ``order=False``, and ``hash=False`` if you set ``@attr.s(auto_detect=True)``.
+``attrs`` will ignore inherited methods.
+If the argument implies more than one method (e.g. ``eq=True`` creates both ``__eq__`` and ``__ne__``), it's enough for *one* of them to exist and ``attrs`` will create *neither*.
+
+This feature requires Python 3.
diff --git a/docs/api.rst b/docs/api.rst
index 535d3af..2384092 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -16,7 +16,7 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
 Core
 ----
 
-.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=None, hash=None, init=True, 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)
+.. 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)
 
    .. note::
 
diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index 38f16f0..19c5a81 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -187,6 +187,7 @@ def attrs(
     auto_exc: bool = ...,
     eq: Optional[bool] = ...,
     order: Optional[bool] = ...,
+    auto_detect: bool = ...,
 ) -> _C: ...
 @overload
 def attrs(
@@ -207,6 +208,7 @@ def attrs(
     auto_exc: bool = ...,
     eq: Optional[bool] = ...,
     order: Optional[bool] = ...,
+    auto_detect: bool = ...,
 ) -> Callable[[_C], _C]: ...
 
 # TODO: add support for returning NamedTuple from the mypy plugin
diff --git a/src/attr/_make.py b/src/attr/_make.py
index c28c625..9637062 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -218,7 +218,7 @@ def attrib(
     .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
     .. versionadded:: 19.2.0 *eq* and *order*
     """
-    eq, order = _determine_eq_order(cmp, eq, order)
+    eq, order = _determine_eq_order(cmp, eq, order, True)
 
     if hash is not None and hash is not True and hash is not False:
         raise TypeError(
@@ -308,20 +308,32 @@ def _is_class_var(annot):
     return str(annot).startswith(_classvar_prefixes)
 
 
-def _get_annotations(cls):
+def _has_own_attribute(cls, attrib_name):
     """
-    Get annotations for *cls*.
+    Check whether *cls* defines *attrib_name* (and doesn't just inherit it).
+
+    Requires Python 3.
     """
-    anns = getattr(cls, "__annotations__", None)
-    if anns is None:
-        return {}
+    attr = getattr(cls, attrib_name, _sentinel)
+    if attr is _sentinel:
+        return False
 
-    # Verify that the annotations aren't merely inherited.
     for base_cls in cls.__mro__[1:]:
-        if anns is getattr(base_cls, "__annotations__", None):
-            return {}
+        a = getattr(base_cls, attrib_name, None)
+        if attr is a:
+            return False
+
+    return True
+
+
+def _get_annotations(cls):
+    """
+    Get annotations for *cls*.
+    """
+    if _has_own_attribute(cls, "__annotations__"):
+        return cls.__annotations__
 
-    return anns
+    return {}
 
 
 def _counter_getter(e):
@@ -754,10 +766,10 @@ _CMP_DEPRECATION = (
 )
 
 
-def _determine_eq_order(cmp, eq, order):
+def _determine_eq_order(cmp, eq, order, default_eq):
     """
     Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
-    values of eq and order.
+    values of eq and order.  If *eq* is None, set it to *default_eq*.
     """
     if cmp is not None and any((eq is not None, order is not None)):
         raise ValueError("Don't mix `cmp` with `eq' and `order`.")
@@ -768,9 +780,10 @@ def _determine_eq_order(cmp, eq, order):
 
         return cmp, cmp
 
-    # If left None, equality is on and ordering mirrors equality.
+    # If left None, equality is set to the specified default and ordering
+    # mirrors equality.
     if eq is None:
-        eq = True
+        eq = default_eq
 
     if order is None:
         order = eq
@@ -781,14 +794,38 @@ def _determine_eq_order(cmp, eq, order):
     return eq, order
 
 
+def _determine_whether_to_implement(cls, flag, auto_detect, dunders):
+    """
+    Check whether we should implement a set of methods for *cls*.
+
+    *flag* is the argument passed into @attr.s like 'init', *auto_detect* the
+    same as passed into @attr.s and *dunders* is a tuple of attribute names
+    whose presence signal that the user has implemented it themselves.
+
+    auto_detect must be False on Python 2.
+    """
+    if flag is True or flag is None and auto_detect is False:
+        return True
+
+    if flag is False:
+        return False
+
+    # Logically, flag is None and auto_detect is True here.
+    for dunder in dunders:
+        if _has_own_attribute(cls, dunder):
+            return False
+
+    return True
+
+
 def attrs(
     maybe_cls=None,
     these=None,
     repr_ns=None,
-    repr=True,
+    repr=None,
     cmp=None,
     hash=None,
-    init=True,
+    init=None,
     slots=False,
     frozen=False,
     weakref_slot=True,
@@ -799,6 +836,7 @@ def attrs(
     auto_exc=False,
     eq=None,
     order=None,
+    auto_detect=False,
 ):
     r"""
     A class decorator that adds `dunder
@@ -823,6 +861,32 @@ def attrs(
     :param str repr_ns: When using nested classes, there's no way in Python 2
         to automatically detect that.  Therefore it's possible to set the
         namespace explicitly for a more meaningful ``repr`` output.
+    :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,
+        *order*, and *hash* arguments explicitly, assume they are set to
+        ``True`` **unless any** of the involved methods for one of the
+        arguments is implemented in the *current* class (i.e. it is *not*
+        inherited from some base class).
+
+        So for example by implementing ``__eq__`` on a class yourself,
+        ``attrs`` will deduce ``eq=False`` and won't create *neither*
+        ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible
+        ``__ne__`` by default, so it *should* be enough to only implement
+        ``__eq__`` in most cases).
+
+        .. warning::
+
+           If you prevent ``attrs`` from creating the ordering methods for you
+           (``order=False``, e.g. by implementing ``__le__``), it becomes
+           *your* responsibility to make sure its ordering is sound. The best
+           way is to use the `functools.total_ordering` decorator.
+
+
+        Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
+        *cmp*, or *hash* overrides whatever *auto_detect* would determine.
+
+        *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises
+        a `PythonTooOldError`.
+
     :param bool repr: Create a ``__repr__`` method with a human readable
         representation of ``attrs`` attributes..
     :param bool str: Create a ``__str__`` method that is identical to
@@ -891,8 +955,8 @@ def attrs(
 
     :param bool weakref_slot: Make instances weak-referenceable.  This has no
         effect unless ``slots`` is also enabled.
-    :param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes
-        (Python 3.6 and later only) from the class body.
+    :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated
+        attributes (Python 3.6 and later only) from the class body.
 
         In this case, you **must** annotate every field.  If ``attrs``
         encounters a field that is set to an `attr.ib` but lacks a type
@@ -957,8 +1021,15 @@ def attrs(
     .. versionadded:: 19.1.0 *auto_exc*
     .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
     .. versionadded:: 19.2.0 *eq* and *order*
+    .. versionadded:: 20.1.0 *auto_detect*
     """
-    eq, order = _determine_eq_order(cmp, eq, order)
+    if auto_detect and PY2:
+        raise PythonTooOldError(
+            "auto_detect only works on Python 3 and later."
+        )
+
+    eq_, order_ = _determine_eq_order(cmp, eq, order, None)
+    hash_ = hash  # workaround the lack of nonlocal
 
     def wrap(cls):
 
@@ -978,16 +1049,31 @@ def attrs(
             cache_hash,
             is_exc,
         )
-
-        if repr is True:
+        if _determine_whether_to_implement(
+            cls, repr, auto_detect, ("__repr__",)
+        ):
             builder.add_repr(repr_ns)
         if str is True:
             builder.add_str()
-        if eq is True and not is_exc:
+
+        eq = _determine_whether_to_implement(
+            cls, eq_, auto_detect, ("__eq__", "__ne__")
+        )
+        if not is_exc and eq is True:
             builder.add_eq()
-        if order is True and not is_exc:
+        if not is_exc and _determine_whether_to_implement(
+            cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
+        ):
             builder.add_order()
 
+        if (
+            hash_ is None
+            and auto_detect is True
+            and _has_own_attribute(cls, "__hash__")
+        ):
+            hash = False
+        else:
+            hash = hash_
         if hash is not True and hash is not False and hash is not None:
             # Can't use `hash in` because 1 == True for example.
             raise TypeError(
@@ -1015,7 +1101,9 @@ def attrs(
                 )
             builder.make_unhashable()
 
-        if init is True:
+        if _determine_whether_to_implement(
+            cls, init, auto_detect, ("__init__",)
+        ):
             builder.add_init()
         else:
             if cache_hash:
@@ -1832,7 +1920,7 @@ class Attribute(object):
         eq=None,
         order=None,
     ):
-        eq, order = _determine_eq_order(cmp, eq, order)
+        eq, order = _determine_eq_order(cmp, eq, order, True)
 
         # Cache this descriptor here to speed things up later.
         bound_setattr = _obj_setattr.__get__(self, Attribute)
@@ -2178,7 +2266,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
         attributes_arguments["eq"],
         attributes_arguments["order"],
     ) = _determine_eq_order(
-        cmp, attributes_arguments.get("eq"), attributes_arguments.get("order")
+        cmp,
+        attributes_arguments.get("eq"),
+        attributes_arguments.get("order"),
+        True,
     )
 
     return _attrs(these=cls_dict, **attributes_arguments)(type_)
diff --git a/src/attr/exceptions.py b/src/attr/exceptions.py
index d1b7618..d3d8add 100644
--- a/src/attr/exceptions.py
+++ b/src/attr/exceptions.py
@@ -51,7 +51,8 @@ class UnannotatedAttributeError(RuntimeError):
 
 class PythonTooOldError(RuntimeError):
     """
-    An ``attrs`` feature requiring a more recent python version has been used.
+    It was attempted to use an ``attrs`` feature that requires a newer Python
+    version.
 
     .. versionadded:: 18.2.0
     """


Test patch:
diff --git a/tests/test_make.py b/tests/test_make.py
index a9c7f41..96ef8a3 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -5,6 +5,7 @@ Tests for `attr._make`.
 from __future__ import absolute_import, division, print_function
 
 import copy
+import functools
 import gc
 import inspect
 import itertools
@@ -29,6 +30,7 @@ from attr._make import (
     _ClassBuilder,
     _CountingAttr,
     _determine_eq_order,
+    _determine_whether_to_implement,
     _transform_attrs,
     and_,
     fields,
@@ -1588,16 +1590,16 @@ class TestMakeOrder:
 class TestDetermineEqOrder(object):
     def test_default(self):
         """
-        If all are set to None, do the default: True, True
+        If all are set to None, set both eq and order to the passed default.
         """
-        assert (True, True) == _determine_eq_order(None, None, None)
+        assert (42, 42) == _determine_eq_order(None, None, None, 42)
 
     @pytest.mark.parametrize("eq", [True, False])
     def test_order_mirrors_eq_by_default(self, eq):
         """
         If order is None, it mirrors eq.
         """
-        assert (eq, eq) == _determine_eq_order(None, eq, None)
+        assert (eq, eq) == _determine_eq_order(None, eq, None, True)
 
     def test_order_without_eq(self):
         """
@@ -1606,7 +1608,7 @@ class TestDetermineEqOrder(object):
         with pytest.raises(
             ValueError, match="`order` can only be True if `eq` is True too."
         ):
-            _determine_eq_order(None, False, True)
+            _determine_eq_order(None, False, True, True)
 
     @given(cmp=booleans(), eq=optional_bool, order=optional_bool)
     def test_mix(self, cmp, eq, order):
@@ -1618,7 +1620,7 @@ class TestDetermineEqOrder(object):
         with pytest.raises(
             ValueError, match="Don't mix `cmp` with `eq' and `order`."
         ):
-            _determine_eq_order(cmp, eq, order)
+            _determine_eq_order(cmp, eq, order, True)
 
     def test_cmp_deprecated(self):
         """
@@ -1669,3 +1671,346 @@ class TestDocs:
                 A.__qualname__
             )
             assert expected == method.__doc__
+
+
+@pytest.mark.skipif(not PY2, reason="Needs to be only caught on Python 2.")
+def test_auto_detect_raises_on_py2():
+    """
+    Trying to pass auto_detect=True to attr.s raises PythonTooOldError.
+    """
+    with pytest.raises(PythonTooOldError):
+        attr.s(auto_detect=True)
+
+
+class BareC(object):
+    pass
+
+
+class BareSlottedC(object):
+    __slots__ = ()
+
+
+@pytest.mark.skipif(PY2, reason="Auto-detection is Python 3-only.")
+class TestAutoDetect:
+    @pytest.mark.parametrize("C", (BareC, BareSlottedC))
+    def test_determine_detects_non_presence_correctly(self, C):
+        """
+        On an empty class, nothing should be detected.
+        """
+        assert True is _determine_whether_to_implement(
+            C, None, True, ("__init__",)
+        )
+        assert True is _determine_whether_to_implement(
+            C, None, True, ("__repr__",)
+        )
+        assert True is _determine_whether_to_implement(
+            C, None, True, ("__eq__", "__ne__")
+        )
+        assert True is _determine_whether_to_implement(
+            C, None, True, ("__le__", "__lt__", "__ge__", "__gt__")
+        )
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_make_all_by_default(self, slots, frozen):
+        """
+        If nothing is there to be detected, imply init=True, repr=True,
+        hash=None, eq=True, order=True.
+        """
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+        i = C(1)
+        o = object()
+
+        assert i.__init__ is not o.__init__
+        assert i.__repr__ is not o.__repr__
+        assert i.__eq__ is not o.__eq__
+        assert i.__ne__ is not o.__ne__
+        assert i.__le__ is not o.__le__
+        assert i.__lt__ is not o.__lt__
+        assert i.__ge__ is not o.__ge__
+        assert i.__gt__ is not o.__gt__
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_detect_auto_init(self, slots, frozen):
+        """
+        If auto_detect=True and an __init__ exists, don't write one.
+        """
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class CI(object):
+            x = attr.ib()
+
+            def __init__(self):
+                object.__setattr__(self, "x", 42)
+
+        assert 42 == CI().x
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_detect_auto_repr(self, slots, frozen):
+        """
+        If auto_detect=True and an __repr__ exists, don't write one.
+        """
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __repr__(self):
+                return "hi"
+
+        assert "hi" == repr(C(42))
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_detect_auto_hash(self, slots, frozen):
+        """
+        If auto_detect=True and an __hash__ exists, don't write one.
+        """
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __hash__(self):
+                return 0xC0FFEE
+
+        assert 0xC0FFEE == hash(C(42))
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_detect_auto_eq(self, slots, frozen):
+        """
+        If auto_detect=True and an __eq__ or an __ne__, exist, don't write one.
+        """
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __eq__(self, o):
+                raise ValueError("worked")
+
+        with pytest.raises(ValueError, match="worked"):
+            C(1) == C(1)
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class D(object):
+            x = attr.ib()
+
+            def __ne__(self, o):
+                raise ValueError("worked")
+
+        with pytest.raises(ValueError, match="worked"):
+            D(1) != D(1)
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_detect_auto_order(self, slots, frozen):
+        """
+        If auto_detect=True and an __ge__, __gt__, __le__, or and __lt__ exist,
+        don't write one.
+
+        It's surprisingly difficult to test this programmatically, so we do it
+        by hand.
+        """
+
+        def assert_not_set(cls, ex, meth_name):
+            __tracebackhide__ = True
+
+            a = getattr(cls, meth_name)
+            if meth_name == ex:
+                assert a == 42
+            else:
+                assert a is getattr(object, meth_name)
+
+        def assert_none_set(cls, ex):
+            __tracebackhide__ = True
+
+            for m in ("le", "lt", "ge", "gt"):
+                assert_not_set(cls, ex, "__" + m + "__")
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class LE(object):
+            __le__ = 42
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class LT(object):
+            __lt__ = 42
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class GE(object):
+            __ge__ = 42
+
+        @attr.s(auto_detect=True, slots=slots, frozen=frozen)
+        class GT(object):
+            __gt__ = 42
+
+        assert_none_set(LE, "__le__")
+        assert_none_set(LT, "__lt__")
+        assert_none_set(GE, "__ge__")
+        assert_none_set(GT, "__gt__")
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_override_init(self, slots, frozen):
+        """
+        If init=True is passed, ignore __init__.
+        """
+
+        @attr.s(init=True, auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __init__(self):
+                pytest.fail("should not be called")
+
+        assert C(1) == C(1)
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_override_repr(self, slots, frozen):
+        """
+        If repr=True is passed, ignore __repr__.
+        """
+
+        @attr.s(repr=True, auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __repr__(self):
+                pytest.fail("should not be called")
+
+        assert "C(x=1)" == repr(C(1))
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_override_hash(self, slots, frozen):
+        """
+        If hash=True is passed, ignore __hash__.
+        """
+
+        @attr.s(hash=True, auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __hash__(self):
+                pytest.fail("should not be called")
+
+        assert hash(C(1))
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    def test_override_eq(self, slots, frozen):
+        """
+        If eq=True is passed, ignore __eq__ and __ne__.
+        """
+
+        @attr.s(eq=True, auto_detect=True, slots=slots, frozen=frozen)
+        class C(object):
+            x = attr.ib()
+
+            def __eq__(self, o):
+                pytest.fail("should not be called")
+
+            def __ne__(self, o):
+                pytest.fail("should not be called")
+
+        assert C(1) == C(1)
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("frozen", [True, False])
+    @pytest.mark.parametrize(
+        "eq,order,cmp",
+        [
+            (True, None, None),
+            (True, True, None),
+            (None, True, None),
+            (None, None, True),
+        ],
+    )
+    def test_override_order(self, slots, frozen, eq, order, cmp, recwarn):
+        """
+        If order=True is passed, ignore __le__, __lt__, __gt__, __ge__.
+
+        eq=True and cmp=True both imply order=True so test it too.
+        """
+
+        def meth(self, o):
+            pytest.fail("should not be called")
+
+        @attr.s(
+            cmp=cmp,
+            order=order,
+            eq=eq,
+            auto_detect=True,
+            slots=slots,
+            frozen=frozen,
+        )
+        class C(object):
+            x = attr.ib()
+            __le__ = __lt__ = __gt__ = __ge__ = meth
+
+        assert C(1) < C(2)
+        assert C(1) <= C(2)
+        assert C(2) > C(1)
+        assert C(2) >= C(1)
+
+        if cmp:
+            assert 1 == len(recwarn.list)
+        else:
+            assert 0 == len(recwarn.list)
+
+    @pytest.mark.parametrize("slots", [True, False])
+    @pytest.mark.parametrize("first", [True, False])
+    def test_total_ordering(self, slots, first):
+        """
+        functools.total_ordering works as expected if an order method and an eq
+        method are detected.
+
+        Ensure the order doesn't matter.
+        """
+
+        class C(object):
+            x = attr.ib()
+            own_eq_called = attr.ib(default=False)
+            own_le_called = attr.ib(default=False)
+
+            def __eq__(self, o):
+                self.own_eq_called = True
+                return self.x == o.x
+
+            def __le__(self, o):
+                self.own_le_called = True
+                return self.x <= o.x
+
+        if first:
+            C = functools.total_ordering(
+                attr.s(auto_detect=True, slots=slots)(C)
+            )
+        else:
+            C = attr.s(auto_detect=True, slots=slots)(
+                functools.total_ordering(C)
+            )
+
+        c1, c2 = C(1), C(2)
+
+        assert c1 < c2
+        assert c1.own_le_called
+
+        c1, c2 = C(1), C(2)
+
+        assert c2 > c1
+        assert c2.own_le_called
+
+        c1, c2 = C(1), C(2)
+
+        assert c2 != c1
+        assert c1 == c1
+
+        assert c1.own_eq_called
diff --git a/tests/typing_example.py b/tests/typing_example.py
index c2690e2..ede74fc 100644
--- a/tests/typing_example.py
+++ b/tests/typing_example.py
@@ -183,3 +183,13 @@ class WithCustomRepr:
 class OrderFlags:
     a = attr.ib(eq=False, order=False)
     b = attr.ib(eq=True, order=True)
+
+
+# Auto-detect
+# XXX: needs support in mypy
+# @attr.s(auto_detect=True)
+# class AutoDetect:
+#     x: int
+
+#     def __init__(self, x: int):
+#         self.x = x