Skip to content

Commit

Permalink
Fixed typed variable positional/keyword arguments causing compilation…
Browse files Browse the repository at this point in the history
… errors on Python < 3.9

Fixes #330.
  • Loading branch information
agronholm committed Apr 8, 2023
1 parent bfbfc52 commit f5cf181
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 18 deletions.
2 changes: 2 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This library adheres to `Semantic Versioning 2.0 <https://semver.org/#semantic-v
- The ``.pyc`` files now use a version-based optimization suffix in the file names so as
not to cause the interpreter to load potentially faulty/incompatible cached bytecode
generated by older versions
- Fixed typed variable positional and keyword arguments causing compilation errors on
Python 3.7 and 3.8

**4.0.0rc1** (2023-04-02)

Expand Down
40 changes: 22 additions & 18 deletions src/typeguard/_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,35 +604,39 @@ def visit_FunctionDef(
else:
container = self._get_import("typing", "Tuple")

arg_annotations[node.args.vararg.arg] = Subscript(
container,
Tuple(
[
self._convert_annotation(node.args.vararg.annotation),
Constant(Ellipsis),
],
ctx=Load(),
),
subscript_slice: Tuple | Index = Tuple(
[
self._convert_annotation(node.args.vararg.annotation),
Constant(Ellipsis),
],
ctx=Load(),
)
if sys.version_info < (3, 9):
subscript_slice = Index(subscript_slice, ctx=Load())

arg_annotations[node.args.vararg.arg] = Subscript(
container, subscript_slice, ctx=Load()
)

if node.args.kwarg and node.args.kwarg.annotation:
if sys.version_info >= (3, 9):
container = Name("dict", ctx=Load())
else:
container = self._get_import("typing", "Dict")

arg_annotations[node.args.kwarg.arg] = Subscript(
container,
Tuple(
[
Name("str", ctx=Load()),
self._convert_annotation(node.args.kwarg.annotation),
],
ctx=Load(),
),
subscript_slice = Tuple(
[
Name("str", ctx=Load()),
self._convert_annotation(node.args.kwarg.annotation),
],
ctx=Load(),
)
if sys.version_info < (3, 9):
subscript_slice = Index(subscript_slice, ctx=Load())

arg_annotations[node.args.kwarg.arg] = Subscript(
container, subscript_slice, ctx=Load()
)

if arg_annotations:
self._memo.variable_annotations.update(arg_annotations)
Expand Down
8 changes: 8 additions & 0 deletions tests/dummymodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Any,
AsyncGenerator,
Callable,
Dict,
Generator,
List,
Sequence,
Expand Down Expand Up @@ -298,3 +299,10 @@ class Inner:
@typechecked
def override_typecheck_fail_callback(self, value: int) -> None:
pass


@typechecked
def typed_variable_args(
*args: str, **kwargs: int
) -> Tuple[Tuple[str, ...], Dict[str, int]]:
return args, kwargs
23 changes: 23 additions & 0 deletions tests/test_instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,26 @@ def test_outer_class_typecheck_fail_callback(self, dummymodule, capsys):
def test_inner_class_no_overrides(self, dummymodule):
with pytest.raises(TypeCheckError):
dummymodule.OverrideClass.Inner().override_typecheck_fail_callback("foo")


class TestVariableArguments:
def test_success(self, dummymodule):
assert dummymodule.typed_variable_args("foo", "bar", a=1, b=8) == (
("foo", "bar"),
{"a": 1, "b": 8},
)

def test_args_fail(self, dummymodule):
with pytest.raises(
TypeCheckError,
match=r'item 0 of argument "args" \(tuple\) is not an instance of str',
):
dummymodule.typed_variable_args(1, a=1, b=8)

def test_kwargs_fail(self, dummymodule):
with pytest.raises(
TypeCheckError,
match=r'value of key \'a\' of argument "kwargs" \(dict\) is not an '
r"instance of int",
):
dummymodule.typed_variable_args("foo", "bar", a="baz")

0 comments on commit f5cf181

Please sign in to comment.