Repository: iris-hep/func_adl

Issue:
Error message when missing [] on method is not helpful.
When compiling this code:

```python
from func_adl_servicex_xaodr22 import FuncADLQueryPHYSLITE, cpp_float

query = (
    FuncADLQueryPHYSLITE()
    .Select(lambda e: e.Vertices("BPHY4Quads"))
    .Select(
        lambda vtxs: {
            "x": vtxs.Select(lambda v: v.x()),
            "QUAD_Muon0": vtxs.Select(lambda v: v.auxdataConst("MuonLinks")[0].pt())
        }
    )
)
```
(It needs something like `[cpp_float]` after the `auxdataConst` before the `("MuonLinks")`).

The error message is a huge stack dump with, at the end:

```
File c:\Users\gordo\Code\iris-hep\bls_example\.venv\Lib\site-packages\func_adl\type_based_replacement.py:586, in remap_by_types.<locals>.type_transformer.type_follow_in_callbacks(self, m_name, call_site_info, r_node)
    578 # Next, we need to figure out what the return type is. There are multiple
    579 # ways, unfortunately, that we can get the return-type. We might be able to
    580 # do simple type resolution, but we might also have to depend on call-backs to
   (...)
    583 
    584 # If this is a known collection class, we can use call-backs to follow it.
    585 if get_origin(call_site_info.obj_type) in _g_collection_classes:
--> 586     rtn_value = self.process_method_call_on_stream_obj(
    587         _g_collection_classes[get_origin(call_site_info.obj_type)],  # type: ignore
    588         m_name,
...
   2501     # In this case we skip the first parameter of the underlying
   2502     # function (usually `self` or `cls`).
   2503     sig = _get_signature_of(obj.__func__)

TypeError: <property object at 0x0000024BAD82EDE0> is not a callable object
```

The error message should be better!

Discussion:

Fix (patch):
diff --git a/func_adl/type_based_replacement.py b/func_adl/type_based_replacement.py
index 93eb8f6..e3a7e6c 100644
--- a/func_adl/type_based_replacement.py
+++ b/func_adl/type_based_replacement.py
@@ -329,6 +329,13 @@ def _fill_in_default_arguments(func: Callable, call: ast.Call) -> Tuple[ast.Call
     Returns:
         Tuple[ast.Call, Type]: The modified call site and return type.
     """
+    if not callable(func):
+        raise ValueError(
+            f"The name '{ast.unparse(call.func)}' in the expression '{ast.unparse(call)}' is "
+            f"not actually a function or method ({func}). Could it need to be parameterized"
+            f" with a '[cpp_vfloat]' or similar?"
+        )
+
     sig = inspect.signature(func)
     i_arg = 0
     arg_array = list(call.args)


Test changes:
diff --git a/tests/test_type_based_replacement.py b/tests/test_type_based_replacement.py
index cc6f18b..e87e1c4 100644
--- a/tests/test_type_based_replacement.py
+++ b/tests/test_type_based_replacement.py
@@ -1011,6 +1011,32 @@ def test_index_callback_1arg():
     assert param_1_capture == "fork"
 
 
+def test_callback_arg_missing():
+    "Indexed callback - make sure error message is sensible with missing []"
+
+    param_1_capture = None
+
+    def my_callback(
+        s: ObjectStream[T], a: ast.Call, param_1: str
+    ) -> Tuple[ObjectStream[T], ast.Call, Type]:
+        nonlocal param_1_capture
+        param_1_capture = param_1
+        return (s.MetaData({"k": "stuff"}), a, float)
+
+    class TEvent:
+        @func_adl_parameterized_call(my_callback)
+        @property
+        def info(self): ...  # noqa
+
+    s = ast_lambda("e.info(55)")
+    objs = ObjectStream[TEvent](ast.Name(id="e", ctx=ast.Load()))
+
+    with pytest.raises(ValueError) as e:
+        remap_by_types(objs, {"e": TEvent}, s)
+
+    assert "info" in str(e)
+
+
 class NameToConstantTransformer(ast.NodeTransformer):
     def __init__(self, target_id, replacement_value):
         self.target_id = target_id