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

Issue:
TypeError with package `random`
Executing

```python
import random
instantiate(builds(random.uniform, 2, 4))
```

Results in

```
TypeError: Error instantiating 'random.Random.uniform' : uniform() missing 1 required positional argument: 'b'
```



Discussion:
This is an issue with how our `get_obj_path` works; it finds the class-method:

```python
>>> from hydra_zen.structured_configs._utils import get_obj_path
>>> get_obj_path(random.uniform)
'random.Random.uniform'
```

If we explicitly set the target's path as `random.uniform`, it works:

```python
>>> Conf = builds(random.uniform, 2, 4)
>>> Conf._target_ = "random.uniform"
>>> instantiate(Conf)
2.773687213849481
```

I'll think about how we might fix this.
The issue comes from the fact that we are using `target.__qualname__`:

https://github.com/mit-ll-responsible-ai/hydra-zen/blob/cdefb1ae1a67b08ce48f40e53161293ddea7c062/src/hydra_zen/structured_configs/_utils.py#L170-L171

which is returning `random.Random.random`. We should only be using `__qualname__` to check if a target is unreachable, we should be using `__name__` to actually set the target's path. 

Testing this:

```python
>>> import random
>>> instantiate(builds(random.uniform, 2, 4))
3.6954955818984336
```

Unfortunately, I'll have to re-work some of the logic so that we preserve the unreachable checks

https://github.com/mit-ll-responsible-ai/hydra-zen/blob/cdefb1ae1a67b08ce48f40e53161293ddea7c062/tests/test_validation.py#L316-L322



Resolving patch:
diff --git a/src/hydra_zen/structured_configs/_utils.py b/src/hydra_zen/structured_configs/_utils.py
--- a/src/hydra_zen/structured_configs/_utils.py
+++ b/src/hydra_zen/structured_configs/_utils.py
@@ -82,6 +82,7 @@ def get_args(obj: Any) -> Union[Tuple[type, ...], Tuple[List[type], type]]:
 
 
 COMMON_MODULES_WITH_OBFUSCATED_IMPORTS: Tuple[str, ...] = (
+    "random",
     "numpy",
     "numpy.random",
     "jax.numpy",
@@ -167,8 +168,6 @@ def field(
 def safe_name(obj: Any, repr_allowed=True) -> str:
     """Tries to get a descriptive name for an object. Returns '<unknown>`
     instead of raising - useful for writing descriptive/dafe error messages."""
-    if hasattr(obj, "__qualname__"):
-        return obj.__qualname__
 
     if hasattr(obj, "__name__"):
         return obj.__name__
@@ -190,8 +189,9 @@ def get_obj_path(obj: Any) -> str:
         raise AttributeError(f"{obj} does not have a `__name__` attribute")
 
     module = getattr(obj, "__module__", None)
+    qualname = getattr(obj, "__qualname__", None)
 
-    if "<" in name or module is None:
+    if (qualname is not None and "<" in qualname) or module is None:
         # NumPy's ufuncs do not have an inspectable `__module__` attribute, so we
         # check to see if the object lives in NumPy's top-level namespace.
         #
@@ -204,16 +204,16 @@ def get_obj_path(obj: Any) -> str:
         #
         # or...
         #
-        # module is None, which is apparently a thing..: numpy.random.rand.__module__ is None
+        # module is None, which is apparently a thing..:
+        # __module__ is None for both numpy.random.rand and random.random
+        #
 
         # don't use qualname for obfuscated paths
-        name = obj.__name__
         for new_module in COMMON_MODULES_WITH_OBFUSCATED_IMPORTS:
             if getattr(sys.modules.get(new_module), name, None) is obj:
                 module = new_module
                 break
-        else:  # pragma: no cover
-            name = safe_name(obj)
+        else:
             raise ModuleNotFoundError(f"{name} is not importable")
 
     return f"{module}.{name}"


Test patch:
diff --git a/tests/test_roundtrips.py b/tests/test_roundtrips.py
--- a/tests/test_roundtrips.py
+++ b/tests/test_roundtrips.py
@@ -1,6 +1,13 @@
 # Copyright (c) 2021 Massachusetts Institute of Technology
 # SPDX-License-Identifier: MIT
 
+import datetime
+import math
+import operator
+import os
+import random
+import re
+import statistics
 import string
 from dataclasses import dataclass
 from typing import Any, Dict, List
@@ -11,7 +18,6 @@
 from omegaconf import OmegaConf
 
 from hydra_zen import builds, get_target, instantiate, just, to_yaml
-
 from tests import valid_hydra_literals
 
 arbitrary_kwargs = st.dictionaries(
@@ -122,6 +128,21 @@ def local_function():
     list,
     set,
     complex,
+    isinstance,
+    all,
+    Exception,
+    random.random,
+    random.uniform,
+    random.choice,
+    random.choices,
+    re.compile,
+    re.match,
+    datetime.time,
+    datetime.timezone,
+    math.sin,
+    operator.add,
+    statistics.mean,
+    os.getcwd,
 ]
 
 
diff --git a/tests/test_third_party/test_against_numpy.py b/tests/test_third_party/test_against_numpy.py
--- a/tests/test_third_party/test_against_numpy.py
+++ b/tests/test_third_party/test_against_numpy.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: MIT
 
 import inspect
+import random
 from functools import partial
 
 import hypothesis.strategies as st
@@ -95,3 +96,17 @@ def test_ufunc_as_default_value():
 
 def test_ufunc_positional_args():
     assert instantiate(builds(np.add, 1.0, 2.0)) == 3.0
+
+
+def test_obfuscated_modules_dont_get_mixed_up():
+    # Both `numpy.random.random` and `random.random` have missing
+    # __module__ attributes, and we have to use specialized logic
+    # to fetch these.
+    #
+    # This test ensures that we never misattribute functions with
+    # the same name, to the wrong module
+    assert builds(np.random.random)._target_.startswith("numpy")
+    assert builds(random.random)._target_.startswith("random")
+
+    assert get_target(builds(np.random.random)) is np.random.random
+    assert get_target(builds(random.random)) is random.random
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: MIT
 import collections.abc as abc
 import enum
+import random
 import sys
 from dataclasses import dataclass, field as dataclass_field
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
@@ -53,7 +54,6 @@ def f():
         (1, "1"),
         (dict, "dict"),
         (C, "C"),
-        (C.f, "C.f"),
         (C(), "C as a repr"),
         ("moo", "'moo'"),
         (None, "None"),
@@ -219,3 +219,8 @@ class B:
     assert isinstance(our_field, type(their_field))
     assert hasattr(A, "x") is hasattr(B, "x")
     assert A().x == B().x
+
+
+def test_builds_random_regression():
+    # was broken in `0.3.0rc3`
+    assert 1 <= instantiate(builds(random.uniform, 1, 2)) <= 2