Summary
annotate_logs injects annotated_logger as a keyword argument unless the decorated function has a *args parameter. A function with positional-only parameters and no *args therefore raises TypeError on every call.
The code that makes this decision is _determine_signature_adjustments in annotated_logger/__init__.py:
by_index = False # pragma: no mutate
# Check for a var positional or positional only
# If present that means we'll have values in args when invoking
# but, if not everything will be in kwargs
for v in written_signature.parameters.values():
if v.kind == inspect.Parameter.VAR_POSITIONAL:
by_index = True
The comment says "var positional or positional only", but the loop only tests for VAR_POSITIONAL. POSITIONAL_ONLY is never checked, so by_index stays False and _inject_by_kwarg is used.
Reproduction
from annotated_logger import AnnotatedAdapter, AnnotatedLogger
annotated_logger = AnnotatedLogger()
annotate_logs = annotated_logger.annotate_logs
@annotate_logs(_typing_self=False, _typing_requested=True)
def positional_only(annotated_logger: AnnotatedAdapter, a: int, /, b: int) -> int:
"""Positional-only parameters, no *args."""
return a * b
@annotate_logs(_typing_self=False, _typing_requested=True)
def positional_only_with_var_args(
annotated_logger: AnnotatedAdapter, a: int, /, *rest: int
) -> int:
"""Same, but the *args flips injection to by-index."""
return a * sum(rest)
print("with *args ->", positional_only_with_var_args(2, 3, 4)) # works
print("without *args ->", positional_only(3, 4)) # raises
Actual result:
TypeError: positional_only() got some positional-only arguments passed as keyword arguments: 'annotated_logger'
raised from wrap_function at annotated_logger/__init__.py:762. The start log is emitted first, then the call fails and is logged as Uncaught Exception in logged function, so the failure surfaces as a runtime error in the decorated function rather than at decoration time.
Expected: the call succeeds and logs start/success, as it does for the *args variant.
Suggested fix
Include POSITIONAL_ONLY in the check, matching what the comment already describes:
if v.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.POSITIONAL_ONLY):
by_index = True
I haven't checked how _inject_by_index behaves for every combination of self/cls, provided=True, and positional-only parameters, so the fix may need more than the one-line change, plus test cases in test/test_decorator.py alongside the existing positional_only example (which currently uses keyword-only parameters after *, not positional-only ones before /).
Environment
- annotated-logger 1.3.5 (
VERSION in annotated_logger/__init__.py)
- Python 3.13.12, makefun 1.16.0, macOS
Summary
annotate_logsinjectsannotated_loggeras a keyword argument unless the decorated function has a*argsparameter. A function with positional-only parameters and no*argstherefore raisesTypeErroron every call.The code that makes this decision is
_determine_signature_adjustmentsinannotated_logger/__init__.py:The comment says "var positional or positional only", but the loop only tests for
VAR_POSITIONAL.POSITIONAL_ONLYis never checked, soby_indexstaysFalseand_inject_by_kwargis used.Reproduction
Actual result:
raised from
wrap_functionatannotated_logger/__init__.py:762. Thestartlog is emitted first, then the call fails and is logged asUncaught Exception in logged function, so the failure surfaces as a runtime error in the decorated function rather than at decoration time.Expected: the call succeeds and logs
start/success, as it does for the*argsvariant.Suggested fix
Include
POSITIONAL_ONLYin the check, matching what the comment already describes:I haven't checked how
_inject_by_indexbehaves for every combination ofself/cls,provided=True, and positional-only parameters, so the fix may need more than the one-line change, plus test cases intest/test_decorator.pyalongside the existingpositional_onlyexample (which currently uses keyword-only parameters after*, not positional-only ones before/).Environment
VERSIONinannotated_logger/__init__.py)