Repository: HypothesisWorks/hypothesis

Issue:
`builds` fails when used with `attrs` classes containing private attributes
Hi, I think I may have found a bug that occurs when using `builds` with automatic attribute inference from `attrs`-built classes that have required but "private" attributes. The bug is probably due to the fact that the name exposed via the constructor differs from the attribute name in these cases, since `attrs` by default strips the leading underscore. 

Here a minimal example:

```python
from attrs import define
from hypothesis import given, strategies as st


@define
class MyClass:
    _x: int


@given(st.builds(MyClass))
def test_class(my_obj):
    assert isinstance(my_obj, MyClass)
```

The error you get:

```python
AttributeError: 'MyClassAttributes' object has no attribute 'x'
```


Discussion:
Actually, the problem is not restricted to required arguments but also appears when explicitly requesting strategies for default argument via the `...` syntax, since the same name matching machinery is used and fails.

To be concrete, if the class was instead
```
@define
class MyClass:
    _x: int = 0
```
then neither `builds(MyClass, x=...)` nor `builds(MyClass, _x=...)` would work.

Resolving patch:
diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py
--- a/hypothesis-python/setup.py
+++ b/hypothesis-python/setup.py
@@ -96,7 +96,7 @@ def local_file(name):
     zip_safe=False,
     extras_require=extras,
     install_requires=[
-        "attrs>=19.2.0",
+        "attrs>=22.2.0",
         "exceptiongroup>=1.0.0 ; python_version<'3.11'",
         "sortedcontainers>=2.1.0,<3.0.0",
     ],
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/attrs.py b/hypothesis-python/src/hypothesis/strategies/_internal/attrs.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/attrs.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/attrs.py
@@ -21,12 +21,38 @@
 from hypothesis.utils.conventions import infer
 
 
+def get_attribute_by_alias(fields, alias, *, target=None):
+    """
+    Get an attrs attribute by its alias, rather than its name (compare
+    getattr(fields, name)).
+
+    ``target`` is used only to provide a nicer error message, and can be safely
+    omitted.
+    """
+    # attrs supports defining an alias for a field, which is the name used when
+    # defining __init__. The init args are what we pull from when determining
+    # what parameters we need to supply to the class, so it's what we need to
+    # match against as well, rather than the class-level attribute name.
+    matched_fields = [f for f in fields if f.alias == alias]
+    if not matched_fields:
+        raise TypeError(
+            f"Unexpected keyword argument {alias} for attrs class"
+            f"{f' {target}' if target else ''}. Expected one of "
+            f"{[f.name for f in fields]}"
+        )
+    # alias is used as an arg in __init__, so it is guaranteed to be unique, if
+    # it exists.
+    assert len(matched_fields) == 1
+    return matched_fields[0]
+
+
 def from_attrs(target, args, kwargs, to_infer):
     """An internal version of builds(), specialised for Attrs classes."""
     fields = attr.fields(target)
     kwargs = {k: v for k, v in kwargs.items() if v is not infer}
     for name in to_infer:
-        kwargs[name] = from_attrs_attribute(getattr(fields, name), target)
+        attrib = get_attribute_by_alias(fields, name, target=target)
+        kwargs[name] = from_attrs_attribute(attrib, target)
     # We might make this strategy more efficient if we added a layer here that
     # retries drawing if validation fails, for improved composition.
     # The treatment of timezones in datetimes() provides a precedent.


Test patch:
diff --git a/hypothesis-python/tests/cover/test_attrs_inference.py b/hypothesis-python/tests/cover/test_attrs_inference.py
--- a/hypothesis-python/tests/cover/test_attrs_inference.py
+++ b/hypothesis-python/tests/cover/test_attrs_inference.py
@@ -89,3 +89,37 @@ def test_cannot_infer(c):
 def test_cannot_infer_takes_self():
     with pytest.raises(ResolutionFailed):
         st.builds(Inferrables, has_default_factory_takes_self=...).example()
+
+
+@attr.s
+class HasPrivateAttribute:
+    _x: int = attr.ib()
+
+
+@pytest.mark.parametrize("s", [st.just(42), ...])
+def test_private_attribute(s):
+    st.builds(HasPrivateAttribute, x=s).example()
+
+
+def test_private_attribute_underscore_fails():
+    with pytest.raises(TypeError, match="unexpected keyword argument '_x'"):
+        st.builds(HasPrivateAttribute, _x=st.just(42)).example()
+
+
+def test_private_attribute_underscore_infer_fails():
+    # this has a slightly different failure case, because it goes through
+    # attrs-specific resolution logic.
+    with pytest.raises(
+        TypeError, match="Unexpected keyword argument _x for attrs class"
+    ):
+        st.builds(HasPrivateAttribute, _x=...).example()
+
+
+@attr.s
+class HasAliasedAttribute:
+    x: int = attr.ib(alias="crazyname")
+
+
+@pytest.mark.parametrize("s", [st.just(42), ...])
+def test_aliased_attribute(s):
+    st.builds(HasAliasedAttribute, crazyname=s).example()