sigmatch documents that some callables cannot be introspected — that is expected behaviour, not a bug. This issue is a request to widen one specific corner of that limitation: built-in exception classes (ValueError, RuntimeError, Exception, BaseException, KeyError, TypeError, …).
Today PossibleCallMatcher(...).match(cls, raise_exception=False) returns False for every built-in exception class regardless of the requested arity (raise_exception=True raises SignatureNotFoundError), because inspect.signature cannot introspect C-extension types and BaseException's family lives in C. As a result, callers that use sigmatch to validate "the user passed a callable I will call with N positional arguments" cannot accept built-in exceptions — even though calling ValueError('x'), RuntimeError('x', 'y'), and so on is well-defined Python.
This bites at least one downstream library (we hit it ourselves) when accepting user-supplied exception classes via configuration: the matcher rejects the built-ins that users would naturally reach for first. Working around it requires duplicating sigmatch's logic in a wrapper that special-cases SignatureNotFoundError for exception classes.
It would be a nice, well-scoped improvement to teach the matcher about this one specific shape.
What it looks like today
from sigmatch import PossibleCallMatcher
PossibleCallMatcher('.').match(ValueError, raise_exception=False) # → False
PossibleCallMatcher('..').match(ValueError, raise_exception=False) # → False
# Meanwhile, all of these are valid Python calls:
ValueError()
ValueError('a')
ValueError('a', 'b')
Suggested behaviour with tests
If the feature is added, these tests describe what we would expect from the new behaviour. They also double as a starting point for the test suite.
import pytest
from sigmatch import PossibleCallMatcher
@pytest.mark.parametrize(
'exception_class',
[
BaseException,
Exception,
ValueError,
RuntimeError,
TypeError,
KeyError,
StopIteration,
],
)
@pytest.mark.parametrize('arity', ['', '.', '..', '...'])
def test_match_accepts_every_arity_for_builtin_exception_classes(exception_class, arity):
"""Built-in exception classes accept any number of positional arguments at
the C level (`BaseException(*args)`), so `PossibleCallMatcher` would ideally
accept them for every arity check the user might request."""
assert PossibleCallMatcher(arity).match(exception_class, raise_exception=False) is True
@pytest.mark.parametrize(
('exception_class', 'arity'),
[
# A custom subclass without __init__ inherits BaseException's variadic signature,
# so it should behave the same as the built-ins above.
(type('NoOverride', (BaseException,), {}), '.'),
(type('NoOverride', (BaseException,), {}), '..'),
# A custom subclass with a single positional argument matches only '.'.
(type('OneArg', (BaseException,), {'__init__': lambda self, message: BaseException.__init__(self, message)}), '.'),
],
)
def test_match_handles_user_defined_exception_classes(exception_class, arity):
"""User-defined exception classes should behave consistently with the
built-in ones: introspectable signatures are honoured, non-introspectable
ones (e.g. inherited from BaseException without an override) are treated
as variadic. Pairs with the built-in test above to cover exception
classes as a complete category of inputs."""
assert PossibleCallMatcher(arity).match(exception_class, raise_exception=False) is True
def test_match_rejects_user_defined_exception_with_clearly_incompatible_signature():
"""An override that requires more positional arguments than the matcher
is asked about must still be rejected for user-defined exceptions, so the
new behaviour does not become permissive enough to mask real mistakes."""
class TwoRequired(BaseException):
def __init__(self, code, message):
super().__init__(message)
assert PossibleCallMatcher('.').match(TwoRequired, raise_exception=False) is False
Possible directions (not prescriptive)
A few sketches that came to mind. None of them is a recommendation — the right choice depends on the library's design and how the maintainer wants to scope the change.
-
Detect built-in exception classes specifically. Inside match(), if inspect.signature(target) raises and isinstance(target, type) and issubclass(target, BaseException), treat it as variadic and accept any arity. Narrow, easy to reason about, and provably safe because every built-in BaseException subclass accepts *args at the C level (BaseException(), BaseException('a'), BaseException('a', 'b', …) are all valid).
-
Treat any non-introspectable C-extension type as variadic. If inspect.signature raises ValueError: no signature found for builtin type, accept the call as matching. Broader — covers exceptions plus other builtin classes — but possibly too permissive for callables whose true signature happens to be narrow.
-
Expose an explicit "trust this target" knob. Keep current behaviour by default but let callers opt into "treat this target as variadic". Trades the silent-correctness improvement for an explicit API surface.
Note on test coverage
When adding the feature, it would be worth covering both built-in exception classes and user-defined BaseException subclasses (with and without an __init__ override). Together they cover the full category "exception classes as callables" in sigmatch's input space, and the matcher's behaviour for the two groups should be consistent: introspectable subclasses respect their declared signature; non-introspectable ones (built-ins, and custom subclasses that simply inherit from BaseException without overriding __init__) match every arity.
Environment
- sigmatch:
0.0.10
- Python:
3.14.0
- OS: macOS (Darwin 24.0.0)
sigmatchdocuments that some callables cannot be introspected — that is expected behaviour, not a bug. This issue is a request to widen one specific corner of that limitation: built-in exception classes (ValueError,RuntimeError,Exception,BaseException,KeyError,TypeError, …).Today
PossibleCallMatcher(...).match(cls, raise_exception=False)returnsFalsefor every built-in exception class regardless of the requested arity (raise_exception=TrueraisesSignatureNotFoundError), becauseinspect.signaturecannot introspect C-extension types andBaseException's family lives in C. As a result, callers that usesigmatchto validate "the user passed a callable I will call with N positional arguments" cannot accept built-in exceptions — even though callingValueError('x'),RuntimeError('x', 'y'), and so on is well-defined Python.This bites at least one downstream library (we hit it ourselves) when accepting user-supplied exception classes via configuration: the matcher rejects the built-ins that users would naturally reach for first. Working around it requires duplicating sigmatch's logic in a wrapper that special-cases
SignatureNotFoundErrorfor exception classes.It would be a nice, well-scoped improvement to teach the matcher about this one specific shape.
What it looks like today
Suggested behaviour with tests
If the feature is added, these tests describe what we would expect from the new behaviour. They also double as a starting point for the test suite.
Possible directions (not prescriptive)
A few sketches that came to mind. None of them is a recommendation — the right choice depends on the library's design and how the maintainer wants to scope the change.
Detect built-in exception classes specifically. Inside
match(), ifinspect.signature(target)raises andisinstance(target, type) and issubclass(target, BaseException), treat it as variadic and accept any arity. Narrow, easy to reason about, and provably safe because every built-inBaseExceptionsubclass accepts*argsat the C level (BaseException(),BaseException('a'),BaseException('a', 'b', …)are all valid).Treat any non-introspectable C-extension type as variadic. If
inspect.signatureraisesValueError: no signature found for builtin type, accept the call as matching. Broader — covers exceptions plus other builtin classes — but possibly too permissive for callables whose true signature happens to be narrow.Expose an explicit "trust this target" knob. Keep current behaviour by default but let callers opt into "treat this target as variadic". Trades the silent-correctness improvement for an explicit API surface.
Note on test coverage
When adding the feature, it would be worth covering both built-in exception classes and user-defined
BaseExceptionsubclasses (with and without an__init__override). Together they cover the full category "exception classes as callables" insigmatch's input space, and the matcher's behaviour for the two groups should be consistent: introspectable subclasses respect their declared signature; non-introspectable ones (built-ins, and custom subclasses that simply inherit fromBaseExceptionwithout overriding__init__) match every arity.Environment
0.0.103.14.0