Repository: mit-ll-responsible-ai/hydra-zen

Issue:
Building an inherited classmethod yields method from parent
```python
class A:
    @classmethod
    def foo(cls):
        return cls.bar()
    @classmethod
    def bar(self):
        raise NotImplementedError()
        
class B(A):
    @classmethod
    def bar(self):
        return 1
```
```python    
>>> cfg = builds(B.foo, zen_partial=True)    
>>> instantiate(cfg)
functools.partial(<bound method A.foo of <class '__main__.A'>>)
```

Discussion:

Patch:
diff --git a/docs/source/changes.rst b/docs/source/changes.rst
index 3be891da..c6219f6b 100644
--- a/docs/source/changes.rst
+++ b/docs/source/changes.rst
@@ -64,11 +64,15 @@ installed. (See :pull:`261`)
 
 Deprecations
 ------------
-:pull:`263` deprecates the ``builds_bases`` argument in ``make_custom_builds``. It will 
+:pull:`263` deprecates the ``builds_bases`` argument in :func:`~hydra_zen.make_custom_builds`. It will 
 be removed in hydra-zen v0.8.0. Users will need to specify ``builds_bases`` on a 
 per-config basis via ``builds``.
 
 
+Bug Fixes
+---------
+- ``hydra_zen.builds(<Child.class-method>)`` would create a config with the wrong target if ``<class-method>`` was defined on a parent of ``Child``. See :issue:`265`.
+
 Improvements
 ------------
 - Fixed internal protocol of ``partial`` to be compatible with latest type-shed annotations.
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
index 5f721dc6..85da45a1 100644
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -260,14 +260,23 @@ def get_obj_path(obj: Any) -> str:
     if not is_classmethod(obj):
         return f"{module}.{name}"
     else:
-        return f"{module}.{qualname}"
+        # __qualname__ reflects name of class that originaly defines classmethod.
+        # Does not point to child in case of inheritance.
+        #
+        # obj.__self__ -> parent object
+        # obj.__name__ -> name of classmethod
+        return f"{get_obj_path(obj.__self__)}.{obj.__name__}"
 
 
 NoneType = type(None)
 
 
 def sanitized_type(
-    type_: Any, *, primitive_only: bool = False, wrap_optional: bool = False
+    type_: Any,
+    *,
+    primitive_only: bool = False,
+    wrap_optional: bool = False,
+    nested: bool = False,
 ) -> type:
     """Returns ``type_`` unchanged if it is supported as an annotation by hydra,
     otherwise returns ``Any``.
@@ -322,19 +331,19 @@ def sanitized_type(
             return Union[optional_type, NoneType]  # type: ignore
 
         if origin is list or origin is List:
-            return List[sanitized_type(args[0], primitive_only=no_nested_container)] if args else type_  # type: ignore
+            return List[sanitized_type(args[0], primitive_only=no_nested_container, nested=True)] if args else type_  # type: ignore
 
         if origin is dict or origin is Dict:
             return (
                 Dict[
-                    sanitized_type(args[0], primitive_only=True),  # type: ignore
-                    sanitized_type(args[1], primitive_only=no_nested_container),  # type: ignore
+                    sanitized_type(args[0], primitive_only=True, nested=True),  # type: ignore
+                    sanitized_type(args[1], primitive_only=no_nested_container, nested=True),  # type: ignore
                 ]
                 if args
                 else type_
             )
 
-        if origin is tuple or origin is Tuple:
+        if (origin is tuple or origin is Tuple) and not nested:
             # hydra silently supports tuples of homogenous types
             # It has some weird behavior. It treats `Tuple[t1, t2, ...]` as `List[t1]`
             # It isn't clear that we want to perpetrate this on our end..
@@ -348,7 +357,7 @@ def sanitized_type(
             has_ellipses = Ellipsis in unique_args
 
             _unique_type = (
-                sanitized_type(args[0], primitive_only=no_nested_container)
+                sanitized_type(args[0], primitive_only=no_nested_container, nested=True)
                 if len(unique_args) == 1 or (len(unique_args) == 2 and has_ellipses)
                 else Any
             )


Test patch:
diff --git a/tests/test_signature_parsing.py b/tests/test_signature_parsing.py
index a55a55ea..add9ea21 100644
--- a/tests/test_signature_parsing.py
+++ b/tests/test_signature_parsing.py
@@ -582,3 +582,32 @@ def test_inheritance_populates_init_field():
 
     # x is 'filled' via inheritance
     Conf2()
+
+
+class A265:
+    @classmethod
+    def foo(cls):
+        return cls.bar()
+
+    @classmethod
+    def bar(cls):
+        raise NotImplementedError()
+
+
+class B265(A265):
+    @classmethod
+    def bar(cls):
+        return 1
+
+
+class C265(B265):
+    @classmethod
+    def bar(cls):
+        return "hello"
+
+
+@pytest.mark.parametrize("Cls", [B265, C265])
+def test_builds_of_inherited_classmethod(Cls):
+    # https://github.com/mit-ll-responsible-ai/hydra-zen/issues/265
+
+    assert instantiate(builds(Cls.foo)) == Cls.foo()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 63b497ae..b4617460 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -157,6 +157,7 @@ NoneType = type(None)
             List[List[int]],
             List[Any] if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES else List[List[int]],
         ),
+        (List[Tuple[int, int]], List[Any]),
         (List[T], List[Any]),
         (Dict[str, float], Dict[str, float]),
         (Dict[C, int], Dict[Any, int]),
@@ -191,6 +192,8 @@ NoneType = type(None)
             List[List[Type[int]]],
             List[Any] if not HYDRA_SUPPORTS_NESTED_CONTAINER_TYPES else List[List[Any]],
         ),
+        (Tuple[Tuple[int, ...], ...], Tuple[Any, ...]),
+        (Optional[Tuple[Tuple[int, ...], ...]], Optional[Tuple[Any, ...]]),
     ],
 )
 def test_sanitized_type_expected_behavior(in_type, expected_type):