Repository: python-attrs/attrs
Pull request #774 (2021-03-05 14:45:16)

Add attribs to resolve_types and fix test_hooks
This was discussed in #766 .  Let me know how you like it.  

# Pull Request Check List

This is just a friendly reminder about the most common mistakes.  Please make sure that you tick all boxes.  But please read our [contribution guide](https://www.attrs.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles!

If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do.  If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing.

- [ ] Added **tests** for changed code.
- [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py).
- [x] 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`.
- [ ] 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.
    - [x] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).  Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file.
- [ ] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/).
- [ ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d).

If you have *any* questions to *any* of the points above, just **submit and ask**!  This checklist is here to *help* you, not to deter you from contributing!


euresti: I'm not sure how detailed to be in the changelog entry.  

Patch:
diff --git a/changelog.d/774.change.rst b/changelog.d/774.change.rst
new file mode 100644
index 0000000..c9ff7a9
--- /dev/null
+++ b/changelog.d/774.change.rst
@@ -0,0 +1,1 @@
+``attr.resolve_types()`` now takes an optional *attrib* argument to work inside a ``field_transformer``.
diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi
index f3e8be8..3cbe709 100644
--- a/src/attr/__init__.pyi
+++ b/src/attr/__init__.pyi
@@ -367,6 +367,7 @@ def resolve_types(
     cls: _C,
     globalns: Optional[Dict[str, Any]] = ...,
     localns: Optional[Dict[str, Any]] = ...,
+    attribs: Optional[List[Attribute]] = ...,
 ) -> _C: ...
 
 # TODO: add support for returning a proper attrs class from the mypy plugin
diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py
index 3020026..241d4bf 100644
--- a/src/attr/_funcs.py
+++ b/src/attr/_funcs.py
@@ -349,7 +349,7 @@ def evolve(inst, **changes):
     return cls(**changes)
 
 
-def resolve_types(cls, globalns=None, localns=None):
+def resolve_types(cls, globalns=None, localns=None, attribs=None):
     """
     Resolve any strings and forward annotations in type annotations.
 
@@ -366,10 +366,13 @@ def resolve_types(cls, globalns=None, localns=None):
     :param type cls: Class to resolve.
     :param Optional[dict] globalns: Dictionary containing global variables.
     :param Optional[dict] localns: Dictionary containing local variables.
+    :param Optional[list] attribs: List of attribs for the given class.
+        This is necessary when calling from inside a ``field_transformer``
+        since *cls* is not an ``attrs`` class yet.
 
     :raise TypeError: If *cls* is not a class.
     :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
-        class.
+        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.
@@ -377,6 +380,8 @@ def resolve_types(cls, globalns=None, localns=None):
         the decorator has to come in the line **before** `attr.s`.
 
     ..  versionadded:: 20.1.0
+    ..  versionadded:: 21.1.0 *attribs*
+
     """
     try:
         # Since calling get_type_hints is expensive we cache whether we've
@@ -386,7 +391,7 @@ def resolve_types(cls, globalns=None, localns=None):
         import typing
 
         hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
-        for field in fields(cls):
+        for field in fields(cls) if attribs is None else attribs:
             if field.name in hints:
                 # Since fields have been frozen we must work around it.
                 _obj_setattr(field, "type", hints[field.name])


Test patch:
diff --git a/conftest.py b/conftest.py
index 1d98e39..a2c8d59 100644
--- a/conftest.py
+++ b/conftest.py
@@ -27,6 +27,5 @@ if sys.version_info[:2] >= (3, 10):
     collect_ignore.extend(
         [
             "tests/test_mypy.yml",
-            "tests/test_hooks.py",
         ]
     )
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index 1f58eef..5fcb100 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -17,6 +17,7 @@ class TestTransformHook:
         results = []
 
         def hook(cls, attribs):
+            attr.resolve_types(cls, attribs=attribs)
             results[:] = [(a.name, a.type) for a in attribs]
             return attribs
 
@@ -36,6 +37,7 @@ class TestTransformHook:
         results = []
 
         def hook(cls, attribs):
+            attr.resolve_types(cls, attribs=attribs)
             results[:] = [(a.name, a.type) for a in attribs]
             return attribs
 
@@ -52,6 +54,7 @@ class TestTransformHook:
         """
 
         def hook(cls, attribs):
+            attr.resolve_types(cls, attribs=attribs)
             return [a.evolve(converter=a.type) for a in attribs]
 
         @attr.s(auto_attribs=True, field_transformer=hook)
@@ -68,6 +71,7 @@ class TestTransformHook:
         """
 
         def hook(cls, attribs):
+            attr.resolve_types(cls, attribs=attribs)
             return [a for a in attribs if a.type is not int]
 
         @attr.s(auto_attribs=True, field_transformer=hook)