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

Issue:
_target_ for inner classes is incorrect
Hello there, big fan of this package.

I believe that there is a bug with the `safe_name` function, that causes the `_target_` field of dynamic configs to be wrong if the target is an inner class:

```python
# project.configs.algorithm.__init__.py

import hydra_zen
import dataclasses

class MyAlgorithm:
    @dataclasses.dataclass
    class HParams:
        lr: float = 0.01



HparamsConfig = hydra_zen.builds(MyAlgorithm.HParams)
config = HparamsConfig()

# This should be true, but it isn't! The target is actually "project.configs.algorithm.HParams"!
assert config._target_ == "project.configs.algorithm.MyAlgorithm.HParams"
```


Discussion:

Patch:
diff --git a/src/hydra_zen/structured_configs/_implementations.py b/src/hydra_zen/structured_configs/_implementations.py
index ce556de5..8d44e77d 100644
--- a/src/hydra_zen/structured_configs/_implementations.py
+++ b/src/hydra_zen/structured_configs/_implementations.py
@@ -948,6 +948,7 @@ class BuildsFn(Generic[T]):
 
         Override this to control how `builds` determines the _target_ field
         in the configs that it produces."""
+
         name = _utils.safe_name(target, repr_allowed=False)
 
         if name == _utils.UNKNOWN_NAME:
@@ -986,12 +987,14 @@ class BuildsFn(Generic[T]):
 
         if not _utils.is_classmethod(target):
             if (
-                inspect.isfunction(target)
+                (inspect.isfunction(target) or isinstance(target, type))
                 and isinstance(qualname, str)
                 and "." in qualname
                 and all(x.isidentifier() for x in qualname.split("."))
             ):
-                # this looks like it is a staticmethod. E.g. qualname: SomeClass.func
+                # This looks like it is a staticmethod or a class defined within
+                # a class namespace. E.g. qualname: SomeClass.func or
+                # SomeClass.NestedClass
                 return f"{module}.{qualname}"
             return f"{module}.{name}"
         else:
@@ -1050,7 +1053,7 @@ class BuildsFn(Generic[T]):
     @classmethod
     def _make_hydra_compatible(
         cls,
-        value: Any,
+        value: object,
         *,
         allow_zen_conversion: bool = True,
         error_prefix: str = "",
@@ -1080,7 +1083,7 @@ class BuildsFn(Generic[T]):
         # We check exhaustively for all Hydra-supported primitives below but seek to
         # speedup checks for common types here.
         if value is None or type(value) in {str, int, bool, float}:
-            return value
+            return cast(Union[None, str, int, float, bool], value)
 
         # non-str collection
         if hasattr(value, "__iter__"):
@@ -1204,7 +1207,7 @@ class BuildsFn(Generic[T]):
                 or isinstance(resolved_value, (Enum, ListConfig, DictConfig))
             )
         ):
-            return resolved_value
+            return resolved_value  # type: ignore
 
         # pydantic objects
         pydantic = sys.modules.get("pydantic")


Test patch:
diff --git a/tests/test_staticmethod_support.py b/tests/test_staticmethod_support.py
index 4739567f..1151fef5 100644
--- a/tests/test_staticmethod_support.py
+++ b/tests/test_staticmethod_support.py
@@ -1,6 +1,8 @@
 # Copyright (c) 2024 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
 
+from dataclasses import dataclass
+
 import pytest
 
 from hydra_zen import builds, instantiate, just
@@ -11,6 +13,10 @@ class A1:
     def foo(x: int):
         return "A.foo" * x
 
+    @dataclass(frozen=True)
+    class Foo:
+        y: int
+
 
 class B1(A1):
     pass
@@ -21,6 +27,13 @@ class C1(A1):
     def foo(x: int):
         return "C.foo" * x
 
+    @dataclass
+    class Foo:
+        y: int
+
+        def __post_init__(self):
+            self.y = self.y * 2
+
 
 class A2:
     class B2:
@@ -28,6 +41,16 @@ class A2:
         def f():
             return "nested"
 
+        @dataclass(frozen=True)
+        class Foo:
+            y: int
+
+
+def test_builds_inner_class():
+    assert instantiate(builds(A1.Foo, 2)).y == 2
+    assert instantiate(builds(B1.Foo, 3)).y == 3
+    assert instantiate(builds(C1.Foo, 4)).y == 8
+
 
 def test_builds_static_methods():
     assert instantiate(builds(A1.foo, 2)) == "A.foo" * 2
@@ -35,7 +58,7 @@ def test_builds_static_methods():
     assert instantiate(builds(C1.foo, 4)) == "C.foo" * 4
 
 
-@pytest.mark.parametrize("obj", [C1.foo, A2.B2.f])
+@pytest.mark.parametrize("obj", [C1.foo, A2.B2.f, A1.Foo, A2.B2.Foo])
 def test_just_static_method(obj):
     assert instantiate(just(obj)) is obj