Repository: iris-hep/func_adl

Issue:
Support class enums when doing resolution
When we are working with a class like `VxType.VertexType.PriVtx` the problem is that `VxType` is a class, and so it is a class ctor (function), and doesn' thave `VertexType` directly in there. So we have to do another level of indirection.

```python
        if hasattr(value, "value") and hasattr(value.value, node.attr):
            new_value = getattr(value.value, node.attr)
            # When 3.10 is not supported, replace with EnumType
            if isinstance(value.value, Enum.__class__):
                new_value = new_value.value
            return ast.Constant(value=new_value)
```

Discussion:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/iris-hep/func_adl/pull/186?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 97.44%. Comparing base [(`ed624ca`)](https://app.codecov.io/gh/iris-hep/func_adl/commit/ed624ca912e6ad70ff32acc14a6cb8a52a321492?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep) to head [(`7e5f18f`)](https://app.codecov.io/gh/iris-hep/func_adl/commit/7e5f18f4c0e07242bc2fa22fa449656fb0f476a1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master     #186   +/-   ##
=======================================
  Coverage   97.44%   97.44%           
=======================================
  Files          15       15           
  Lines        1369     1369           
=======================================
  Hits         1334     1334           
  Misses         35       35           
```

| [Flag](https://app.codecov.io/gh/iris-hep/func_adl/pull/186/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/iris-hep/func_adl/pull/186/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep) | `97.44% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/iris-hep/func_adl/pull/186?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=iris-hep).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/func_adl/type_based_replacement.py b/func_adl/type_based_replacement.py
index e3a7e6c..d6eb09e 100644
--- a/func_adl/type_based_replacement.py
+++ b/func_adl/type_based_replacement.py
@@ -906,7 +906,8 @@ def remap_by_types(
             elif node.id in _global_functions:
                 self._found_types[node] = Callable
             else:
-                logging.getLogger(__name__).warning(f"Unknown type for name {node.id}")
+                if not getattr(node, "_ignore", False):
+                    logging.getLogger(__name__).warning(f"Unknown type for name {node.id}")
                 self._found_types[node] = Any
             return node
 
diff --git a/func_adl/util_ast.py b/func_adl/util_ast.py
index d4af1ba..e6a90c6 100644
--- a/func_adl/util_ast.py
+++ b/func_adl/util_ast.py
@@ -1,13 +1,13 @@
-from __future__ import annotations
-
 import ast
+import copy
+import importlib
 import inspect
 import tokenize
 from collections import defaultdict
 from dataclasses import is_dataclass
 from enum import Enum
 from types import ModuleType
-from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Union, cast
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
 
 
 def as_literal(p: Union[str, int, float, bool, None]) -> ast.Constant:
@@ -252,6 +252,13 @@ def rewrite_func_as_lambda(f: ast.FunctionDef) -> ast.Lambda:
     return ast.Lambda(args, ret.value)  # type: ignore
 
 
+class _mark_ignore_name(ast.NodeTransformer):
+    def visit_Name(self, node: ast.Name) -> Any:
+        new_node = cast(ast.Expr, ast.parse(node.id).body[0]).value
+        new_node._ignore = True  # type: ignore
+        return new_node
+
+
 class _rewrite_captured_vars(ast.NodeTransformer):
     def __init__(self, cv: inspect.ClosureVars):
         self._lookup_dict: Dict[str, Any] = dict(cv.nonlocals)
@@ -293,7 +300,23 @@ class _rewrite_captured_vars(ast.NodeTransformer):
             new_value = getattr(value.value, node.attr)
             # When 3.10 is not supported, replace with EnumType
             if isinstance(value.value, Enum.__class__):
-                new_value = new_value.value
+                # Sometimes we need to prepend a namespace. We look
+                # for secret info here, and then prepend if necessary.
+                # But no one else knows about this, so  we need to mark this
+                # as "ignore".
+                enum_mod = importlib.import_module(new_value.__module__)
+                additional_ns = getattr(enum_mod, "_object_cpp_as_py_namespace", None)
+                if additional_ns is None:
+                    return node
+
+                if len(additional_ns) > 0:
+                    ns_node = cast(
+                        ast.Expr, ast.parse(f"{additional_ns}.{ast.unparse(node)}").body[0]
+                    ).value
+                else:
+                    ns_node = copy.copy(node)
+
+                return _mark_ignore_name().visit(ns_node)
             return ast.Constant(value=new_value)
 
         # If we fail, then just move on.
@@ -448,9 +471,7 @@ class _token_runner:
                 break
         return None, None
 
-    def tokens_till(
-        self, stop_condition: Dict[int, List[str]]
-    ) -> Generator[tokenize.Token, None, None]:  # type: ignore
+    def tokens_till(self, stop_condition: Dict[int, List[str]]):
         """Yield tokens until we find a stop condition.
 
         * Properly tracks parentheses, etc.


Test patch:
diff --git a/tests/test_type_based_replacement.py b/tests/test_type_based_replacement.py
index e87e1c4..ec603a0 100644
--- a/tests/test_type_based_replacement.py
+++ b/tests/test_type_based_replacement.py
@@ -3,7 +3,7 @@ import copy
 import inspect
 import logging
 from inspect import isclass
-from typing import Any, Callable, Iterable, Optional, Tuple, Type, TypeVar, cast
+from typing import Any, Callable, Iterable, Optional, Tuple, Type, TypeVar, Union, cast
 
 import pytest
 
@@ -128,8 +128,8 @@ class Event:
     def MyLambdaCallback(self, cb: Callable) -> int: ...  # noqa
 
 
-def return_type_test(expr: str, arg_type: type, expected_type: type):
-    s = ast_lambda(expr)
+def return_type_test(expr: Union[str, ast.expr], arg_type: type, expected_type: type):
+    s = expr if isinstance(expr, ast.expr) else ast_lambda(expr)
     objs = ObjectStream(ast.Name(id="e", ctx=ast.Load()), arg_type)
 
     _, _, expr_type = remap_by_types(objs, {"e": arg_type}, s)
@@ -251,6 +251,17 @@ def test_subscript_any():
     return_type_test("e[0]", Any, Any)  # type: ignore
 
 
+def test_ast_marked_ignore(caplog):
+    "Make sure that a ast marked ignore does not generate a warning"
+    a = ast.parse("donttalk").body[0].value  # type: ignore
+    a._ignore = True
+
+    with caplog.at_level(logging.WARNING):
+        return_type_test(a, Any, Any)  # type: ignore
+
+    assert len(caplog.messages) == 0
+
+
 def test_collection():
     "A simple collection"
     s = ast_lambda("e.Jets('default')")
diff --git a/tests/test_util_ast.py b/tests/test_util_ast.py
index 0cf3085..5b1e238 100644
--- a/tests/test_util_ast.py
+++ b/tests/test_util_ast.py
@@ -292,9 +292,76 @@ def test_parse_lambda_class_enum():
             VALUE = 20
 
     r = parse_as_ast(lambda x: x > forkit.MyEnum.VALUE)
-    r_true = parse_as_ast(lambda x: x > 20)
 
-    assert ast.unparse(r) == ast.unparse(r_true)
+    assert "VALUE" in ast.unparse(r)
+
+
+def test_parse_lambda_with_implied_ns():
+    "Test adding the special attribute to the module to prefix a namespace"
+    # Add the attribute to the module
+    global _object_cpp_as_py_namespace
+    _object_cpp_as_py_namespace = "aweful"
+
+    try:
+
+        class forkit:
+            class MyEnum(Enum):
+                VALUE = 20
+
+        r = parse_as_ast(lambda x: x > forkit.MyEnum.VALUE)
+
+        assert "aweful.forkit.MyEnum.VALUE" in ast.unparse(r)
+
+        found_it = False
+
+        class check_it(ast.NodeVisitor):
+            def visit_Name(self, node: ast.Name):
+                nonlocal found_it
+                if node.id == "aweful":
+                    found_it = True
+                    assert hasattr(node, "_ignore")
+                    assert node._ignore  # type: ignore
+
+        check_it().visit(r)
+        assert found_it
+
+    finally:
+        # Remove the attribute from the module
+        del _object_cpp_as_py_namespace
+
+
+def test_parse_lambda_with_implied_ns_empty():
+    "Test adding the special attribute to the module to prefix a namespace"
+    # Add the attribute to the module
+    global _object_cpp_as_py_namespace
+    _object_cpp_as_py_namespace = ""
+
+    try:
+
+        class forkit:
+            class MyEnum(Enum):
+                VALUE = 20
+
+        r = parse_as_ast(lambda x: x > forkit.MyEnum.VALUE)
+
+        assert "forkit.MyEnum.VALUE" in ast.unparse(r)
+
+        found_it = False
+
+        class check_it(ast.NodeVisitor):
+            def visit_Name(self, node: ast.Name):
+                nonlocal found_it
+                if node.id == "forkit":
+                    found_it = True
+                    assert hasattr(node, "_ignore")
+                    assert node._ignore  # type: ignore
+
+        check_it().visit(r)
+        assert found_it
+
+    finally:
+        # Remove the attribute from the module
+        del _object_cpp_as_py_namespace
 
 
 def test_parse_lambda_class_constant_in_module():