import re
[docs]
class ArityError(TypeError):
"""
Custom exception for handling function arity mismatches.
ArityError parses TypeError exceptions to determine if they're caused by
incorrect function arity (wrong number of arguments). It categorizes errors
as under-application (too few arguments) or over-application (too many
arguments), and tracks whether the error involves keyword arguments.
Attributes:
is_arity_error (bool): True if the error is an arity-related TypeError.
underapplied (bool): True if too few arguments were provided.
overapplied (bool): True if too many arguments were provided.
kwarg_error (bool): True if the error involves keyword arguments.
unexpected_kwargs (list): List of unexpected keyword argument names.
Example:
>>> def foo(a, b):
... return a + b
>>> try:
... foo(1)
... except TypeError as e:
... arity_err = ArityError(e)
... print(arity_err.underapplied)
True
"""
[docs]
def __init__(self, e: Exception):
"""
Initialize ArityError by parsing a TypeError exception.
Args:
e: The exception to parse. Should be a TypeError from a
function call with incorrect arity.
"""
super().__init__(str(e))
self.is_arity_error = False
self.underapplied = False
self.overapplied = False
self.kwarg_error = False
self.unexpected_kwargs = []
self._parse_error(e)
def _parse_error(self, e: Exception):
"""
Parse the exception to determine arity error type.
Uses regex patterns to match common TypeError messages related to
function arity and categorize them appropriately.
Args:
e: The exception to parse.
"""
if not isinstance(e, TypeError):
return
message = str(e.args[0]).lower()
arg_error_patterns = [
(r"expected (\d+) arguments?,? got (\d+)", self._match_expected_received),
(r"takes (\d+) positional arguments? but (\d+) were given", self._match_expected_received),
(
r"missing (\d+) required positional arguments?: ((?:'[\w_]+'(?:, )?)+)",
self._handle_underapplication_args,
),
(r"must have at least (\w+) arguments.", self._handle_underapplication_args),
(r"got multiple values for argument '(.*?)'", self._handle_overapplication_args),
]
kwarg_error_patterns = [
(
r"missing (\d+) required keyword-only arguments?: ((?:'[\w_]+'(?:, )?)+)",
self._handle_underapplication_args,
),
(r"got an unexpected keyword argument '(.*?)'", self._handle_overapplication_kwargs),
]
for pattern, handler in arg_error_patterns + kwarg_error_patterns:
match = re.search(pattern, message)
if match:
self.is_arity_error = True
handler(match)
break
def _match_expected_received(self, match):
"""
Handle errors with explicit expected/received argument counts.
Compares expected vs received argument counts and delegates to
appropriate under/over-application handler.
Args:
match: Regex match object containing expected and received counts.
"""
if int(match.group(1)) > int(match.group(2)):
self._handle_underapplication_args(match)
else:
self._handle_overapplication_args(match)
def _handle_underapplication_args(self, _):
"""Mark error as underapplication (too few arguments)."""
self.underapplied = True
def _handle_overapplication_args(self, _):
"""Mark error as overapplication (too many arguments)."""
self.overapplied = True
def _handle_underapplication_kwargs(self, _):
"""Mark error as underapplication with missing keyword arguments."""
self.underapplied = True
self.kwarg_error = True
def _handle_overapplication_kwargs(self, match):
"""
Mark error as overapplication with unexpected keyword arguments.
Args:
match: Regex match object containing the unexpected kwarg name.
"""
self.overapplied = True
self.kwarg_error = True
self.unexpected_kwargs.append(match.group(1))
[docs]
def __bool__(self):
"""
Check if the error is a valid arity error.
Returns:
bool: True if this is a recognized arity error, False otherwise.
"""
return self.is_arity_error