Bug
Callable-valued dataclass fields behave oddly under attr-call syntax.
If a dataclass field stores a callable, then:
x.foo retrieves a function-like placeholder
x.foo(1) raises TypeError: 'function' object is not callable
f = x.foo; f(1) reaches normal external-function dispatch instead
So x.foo(1) is not behaving like “load the field value, then call it”. Instead, dataclass attr-call syntax is treating field names as data-only and rejecting the call before normal external dispatch happens.
Minimal repro
from __future__ import annotations
import dataclasses
from collections.abc import Callable
from typing import Any
import pydantic_monty
def run(
code: str,
*,
inputs: dict[str, Any] | None = None,
external_functions: dict[str, Callable[..., Any]] | None = None,
) -> Any:
input_names = list(inputs) if inputs else None
return pydantic_monty.Monty(code, inputs=input_names).run(
inputs=inputs,
external_functions=external_functions,
)
def host_inc(value: int) -> int:
return value + 1
@dataclasses.dataclass
class Box:
foo: object
x = Box(host_inc)
inputs = {'x': x}
registered = {
'inputs': inputs,
'external_functions': {'host_inc': host_inc},
}
assert run('repr(x.foo)', inputs=inputs) == "<function 'host_inc' external>"
assert run('type(x.foo).__name__', inputs=inputs) == 'function'
try:
run('x.foo(1)', inputs=inputs)
except pydantic_monty.MontyRuntimeError as exc:
assert str(exc) == "TypeError: 'function' object is not callable"
else:
assert False, 'expected x.foo(1) to fail'
try:
run('f = x.foo\nf(1)', inputs=inputs)
except RuntimeError as exc:
assert str(exc) == "External function 'host_inc' called but no external_functions provided"
else:
assert False, 'expected f(1) without external_functions to fail'
assert run('f = x.foo\nf(1)', **registered) == 2
Actual behavior
x.foo behaves like a function-like external placeholder, but x.foo(1) does not call it.
Instead, the dataclass attr-call path sees that foo exists in attrs and immediately raises TypeError using the stored value’s type.
Expected behavior
One of these should be true consistently:
- either callable-valued fields should behave like normal values, so
x.foo(1) behaves like tmp = x.foo; tmp(1), or
- callable-valued dataclass fields should be explicitly unsupported, with docs/comments reflecting that
Right now the behavior is surprising because direct lookup and separated call suggest a callable, but attr-call syntax rejects it.
Comment mismatch
The comment on py_call_attr in crates/monty/src/types/dataclass.rs currently says:
/// Otherwise handles the call directly:
/// - Attributes that exist in attrs but aren't callable produce `TypeError`
/// - Private/dunder attributes that aren't in attrs produce `AttributeError`
That is not quite accurate.
The implementation does not check whether the field value is callable. If the attribute exists in attrs, it always raises TypeError based on the stored value’s type:
if let Some(value) = self.get(vm.heap).attrs.get_by_str(method_name, vm.heap, vm.interns) {
let type_name = value.py_type(vm);
Err(ExcType::type_error_not_callable_object(type_name))
}
So a more accurate description would be that attrs-backed fields are treated as data values, not methods, and attr-call syntax rejects them even if the stored value is callable-like.
Likely source
HeapRead<Dataclass>::py_call_attr in:
crates/monty/src/types/dataclass.rs
It does:
- public missing attr ->
MethodCall
- present attr -> always
TypeError
- private missing attr ->
AttributeError
which explains the discrepancy between x.foo(1) and f = x.foo; f(1).
Notes
I reproduced this locally on main with:
ignoreme/workflow/repro_dataclass_callable_field_attr_call.py
Bug
Callable-valued dataclass fields behave oddly under attr-call syntax.
If a dataclass field stores a callable, then:
x.fooretrieves a function-like placeholderx.foo(1)raisesTypeError: 'function' object is not callablef = x.foo; f(1)reaches normal external-function dispatch insteadSo
x.foo(1)is not behaving like “load the field value, then call it”. Instead, dataclass attr-call syntax is treating field names as data-only and rejecting the call before normal external dispatch happens.Minimal repro
Actual behavior
x.foobehaves like a function-like external placeholder, butx.foo(1)does not call it.Instead, the dataclass attr-call path sees that
fooexists inattrsand immediately raisesTypeErrorusing the stored value’s type.Expected behavior
One of these should be true consistently:
x.foo(1)behaves liketmp = x.foo; tmp(1), orRight now the behavior is surprising because direct lookup and separated call suggest a callable, but attr-call syntax rejects it.
Comment mismatch
The comment on
py_call_attrincrates/monty/src/types/dataclass.rscurrently says:That is not quite accurate.
The implementation does not check whether the field value is callable. If the attribute exists in
attrs, it always raisesTypeErrorbased on the stored value’s type:So a more accurate description would be that attrs-backed fields are treated as data values, not methods, and attr-call syntax rejects them even if the stored value is callable-like.
Likely source
HeapRead<Dataclass>::py_call_attrin:crates/monty/src/types/dataclass.rsIt does:
MethodCallTypeErrorAttributeErrorwhich explains the discrepancy between
x.foo(1)andf = x.foo; f(1).Notes
I reproduced this locally on
mainwith:ignoreme/workflow/repro_dataclass_callable_field_attr_call.py