Skip to content

Commit 93d0164

Browse files
Fix validate_activation crash on builtin functions
validate_activation in neat/activations.py had an internal contradiction: the isinstance check on line 104 explicitly admitted BuiltinFunctionType, but line 109 then accessed function.__code__.co_argcount, which crashes with AttributeError on C-implemented CPython builtins because they do not have a __code__ attribute. Calling config.genome_config.add_activation('my_abs', abs) — the exact pattern documented in docs/customization.rst — raised: AttributeError: 'builtin_function_or_method' object has no attribute '__code__' instead of the documented InvalidActivationFunction. The sister function validate_aggregation in neat/aggregations.py already handled this case correctly using inspect.signature with a TypeError/ValueError fallback for builtins. Rewrite validate_activation to mirror that pattern: try inspect.signature first, fall back to accepting BuiltinFunctionType when signature inspection fails, and use signature.bind(object()) to verify the callable accepts exactly one positional argument. This is also a strict upgrade over an __code__-based check: inspect can actually introspect most CPython builtins (abs, len, sum, round, divmod all return Signature objects) and can therefore correctly reject wrong-arity builtins like divmod, which the old check could never have caught. Delete the misleading "# avoid deprecated use of inspect" comment. The inspect module is not deprecated — that comment appears to be a fossil from when this code used inspect.getargspec (which was removed in Python 3.11). inspect.signature is the current, non-deprecated API and is already used successfully by validate_aggregation in the same codebase. Add 8 regression tests in tests/test_coverage_gaps.py under a new TestValidateActivation class. validate_activation previously had zero test coverage, which is why this bug escaped notice. Tests cover: plain functions, lambdas, builtins with introspectable signatures (abs — the original regression), builtins without introspectable signatures (max, min — fallback path), wrong-arity pure functions, zero-arg functions, wrong-arity builtins (divmod), and non-callable objects. Full suite: 629 passed (was 621), 6 skipped, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b660ed8 commit 93d0164

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

neat/activations.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
and code for adding new user-defined ones
55
"""
66

7+
import inspect
78
import math
89
import types
910

@@ -106,8 +107,22 @@ def validate_activation(function):
106107
types.LambdaType)):
107108
raise InvalidActivationFunction("A function object is required.")
108109

109-
if function.__code__.co_argcount != 1: # avoid deprecated use of `inspect`
110-
raise InvalidActivationFunction("A single-argument function is required.")
110+
try:
111+
signature = inspect.signature(function)
112+
except (TypeError, ValueError) as exc:
113+
# CPython builtins (e.g. max, min) may lack an introspectable
114+
# signature. Mirror validate_aggregation: accept them and let
115+
# any arity errors surface at use time.
116+
if isinstance(function, types.BuiltinFunctionType):
117+
return
118+
raise InvalidActivationFunction(
119+
"Unable to inspect activation callable signature.") from exc
120+
121+
try:
122+
signature.bind(object())
123+
except TypeError as exc:
124+
raise InvalidActivationFunction(
125+
"A single-argument function is required.") from exc
111126

112127

113128
class ActivationFunctionSet:

tests/test_coverage_gaps.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import neat
1717
from neat.activations import (
1818
elu_activation, lelu_activation, selu_activation,
19+
validate_activation, InvalidActivationFunction,
1920
)
2021
from neat.aggregations import AggregationFunctionSet, validate_aggregation, InvalidAggregationFunction
2122
from neat.config import ConfigParameter, UnknownConfigItemError
@@ -86,6 +87,66 @@ def test_selu_zero(self):
8687
assert selu_activation(0.0) == 0.0
8788

8889

90+
# ──────────────────────────────────────────────────────────────────────
91+
# validate_activation signature handling (mirrors validate_aggregation)
92+
# ──────────────────────────────────────────────────────────────────────
93+
94+
class TestValidateActivation:
95+
"""Regression tests for validate_activation.
96+
97+
Previously the function accessed ``function.__code__.co_argcount``
98+
directly, which raised ``AttributeError`` on CPython builtins (which
99+
lack a ``__code__`` attribute) even though the isinstance check admitted
100+
``BuiltinFunctionType``. It now uses ``inspect.signature`` + ``bind``,
101+
matching ``validate_aggregation``.
102+
"""
103+
104+
def test_accepts_plain_function(self):
105+
def f(z):
106+
return z
107+
validate_activation(f) # should not raise
108+
109+
def test_accepts_lambda(self):
110+
validate_activation(lambda z: z) # should not raise
111+
112+
def test_accepts_builtin_with_introspectable_signature(self):
113+
# This is the regression test for the original bug.
114+
# Before the fix, validate_activation(abs) raised
115+
# AttributeError: 'builtin_function_or_method' object has no
116+
# attribute '__code__'.
117+
validate_activation(abs) # should not raise
118+
119+
def test_accepts_builtin_without_introspectable_signature(self):
120+
# CPython's ``max`` and ``min`` raise ValueError from
121+
# inspect.signature. The fallback should accept them.
122+
validate_activation(max)
123+
validate_activation(min)
124+
125+
def test_rejects_two_arg_function_with_invalid_activation_function(self):
126+
def f(a, b):
127+
return a + b
128+
with pytest.raises(InvalidActivationFunction):
129+
validate_activation(f)
130+
131+
def test_rejects_zero_arg_function_with_invalid_activation_function(self):
132+
def f():
133+
return 0.0
134+
with pytest.raises(InvalidActivationFunction):
135+
validate_activation(f)
136+
137+
def test_rejects_two_arg_builtin_with_invalid_activation_function(self):
138+
# divmod has an introspectable signature (x, y, /) — should be
139+
# rejected cleanly, not propagate a TypeError or AttributeError.
140+
with pytest.raises(InvalidActivationFunction):
141+
validate_activation(divmod)
142+
143+
def test_rejects_non_callable_with_invalid_activation_function(self):
144+
with pytest.raises(InvalidActivationFunction):
145+
validate_activation(42)
146+
with pytest.raises(InvalidActivationFunction):
147+
validate_activation("not a function")
148+
149+
89150
# ──────────────────────────────────────────────────────────────────────
90151
# Aggregation: deprecation warning on __getitem__ (lines 98-100)
91152
# ──────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)