summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
authorBas van Beek <b.f.van.beek@vu.nl>2021-09-30 11:13:06 +0200
committerBas van Beek <b.f.van.beek@vu.nl>2021-09-30 11:51:51 +0200
commitafd99d7c76b4caeee419d09f1d2bed661226a59e (patch)
treea924700798b5c2a2f02e223adf67f07c8bd3a631 /numpy
parent1cadccc6012124e38e02a78b39bf268123d272c1 (diff)
downloadnumpy-afd99d7c76b4caeee419d09f1d2bed661226a59e.tar.gz
STY: Introduce various linting fixes to `numpy.typing`
* Introduce various linting fixes to `numpy.typing` * Add annotations to a few internal testing functions * Switch to PEP 585 `builtins` annotations where possible
Diffstat (limited to 'numpy')
-rw-r--r--numpy/typing/__init__.py54
-rw-r--r--numpy/typing/_add_docstring.py15
-rw-r--r--numpy/typing/_dtype_like.py19
-rw-r--r--numpy/typing/_extended_precision.py3
-rw-r--r--numpy/typing/mypy_plugin.py36
-rw-r--r--numpy/typing/tests/test_runtime.py4
-rw-r--r--numpy/typing/tests/test_typing.py108
7 files changed, 165 insertions, 74 deletions
diff --git a/numpy/typing/__init__.py b/numpy/typing/__init__.py
index 1e366eb34..d5cfbf5ac 100644
--- a/numpy/typing/__init__.py
+++ b/numpy/typing/__init__.py
@@ -114,8 +114,9 @@ runtime, they're not necessarily considered as sub-classes.
Timedelta64
~~~~~~~~~~~
-The `~numpy.timedelta64` class is not considered a subclass of `~numpy.signedinteger`,
-the former only inheriting from `~numpy.generic` while static type checking.
+The `~numpy.timedelta64` class is not considered a subclass of
+`~numpy.signedinteger`, the former only inheriting from `~numpy.generic`
+while static type checking.
0D arrays
~~~~~~~~~
@@ -154,8 +155,10 @@ API
# NOTE: The API section will be appended with additional entries
# further down in this file
+from __future__ import annotations
+
from numpy import ufunc
-from typing import TYPE_CHECKING, List, final
+from typing import TYPE_CHECKING, final
if not TYPE_CHECKING:
__all__ = ["ArrayLike", "DTypeLike", "NBitBase", "NDArray"]
@@ -166,14 +169,14 @@ else:
#
# Declare to mypy that `__all__` is a list of strings without assigning
# an explicit value
- __all__: List[str]
- __path__: List[str]
+ __all__: list[str]
+ __path__: list[str]
@final # Disallow the creation of arbitrary `NBitBase` subclasses
class NBitBase:
"""
- An object representing `numpy.number` precision during static type checking.
+ A type representing `numpy.number` precision during static type checking.
Used exclusively for the purpose static type checking, `NBitBase`
represents the base of a hierarchical set of subclasses.
@@ -184,9 +187,9 @@ class NBitBase:
Examples
--------
- Below is a typical usage example: `NBitBase` is herein used for annotating a
- function that takes a float and integer of arbitrary precision as arguments
- and returns a new float of whichever precision is largest
+ Below is a typical usage example: `NBitBase` is herein used for annotating
+ a function that takes a float and integer of arbitrary precision
+ as arguments and returns a new float of whichever precision is largest
(*e.g.* ``np.float16 + np.int64 -> np.float64``).
.. code-block:: python
@@ -226,14 +229,29 @@ class NBitBase:
# Silence errors about subclassing a `@final`-decorated class
-class _256Bit(NBitBase): ... # type: ignore[misc]
-class _128Bit(_256Bit): ... # type: ignore[misc]
-class _96Bit(_128Bit): ... # type: ignore[misc]
-class _80Bit(_96Bit): ... # type: ignore[misc]
-class _64Bit(_80Bit): ... # type: ignore[misc]
-class _32Bit(_64Bit): ... # type: ignore[misc]
-class _16Bit(_32Bit): ... # type: ignore[misc]
-class _8Bit(_16Bit): ... # type: ignore[misc]
+class _256Bit(NBitBase): # type: ignore[misc]
+ pass
+
+class _128Bit(_256Bit): # type: ignore[misc]
+ pass
+
+class _96Bit(_128Bit): # type: ignore[misc]
+ pass
+
+class _80Bit(_96Bit): # type: ignore[misc]
+ pass
+
+class _64Bit(_80Bit): # type: ignore[misc]
+ pass
+
+class _32Bit(_64Bit): # type: ignore[misc]
+ pass
+
+class _16Bit(_32Bit): # type: ignore[misc]
+ pass
+
+class _8Bit(_16Bit): # type: ignore[misc]
+ pass
from ._nested_sequence import _NestedSequence
@@ -363,7 +381,7 @@ else:
_GUFunc_Nin2_Nout1 = ufunc
# Clean up the namespace
-del TYPE_CHECKING, final, List, ufunc
+del TYPE_CHECKING, final, ufunc
if __doc__ is not None:
from ._add_docstring import _docstrings
diff --git a/numpy/typing/_add_docstring.py b/numpy/typing/_add_docstring.py
index 846b67042..10d77f516 100644
--- a/numpy/typing/_add_docstring.py
+++ b/numpy/typing/_add_docstring.py
@@ -50,16 +50,17 @@ def _parse_docstrings() -> str:
new_lines.append("")
else:
new_lines.append(f"{indent}{line}")
- s = "\n".join(new_lines)
- # Done.
- type_list_ret.append(f""".. data:: {name}\n :value: {value}\n {s}""")
+ s = "\n".join(new_lines)
+ s_block = f""".. data:: {name}\n :value: {value}\n {s}"""
+ type_list_ret.append(s_block)
return "\n".join(type_list_ret)
add_newdoc('ArrayLike', 'typing.Union[...]',
"""
- A `~typing.Union` representing objects that can be coerced into an `~numpy.ndarray`.
+ A `~typing.Union` representing objects that can be coerced
+ into an `~numpy.ndarray`.
Among others this includes the likes of:
@@ -88,7 +89,8 @@ add_newdoc('ArrayLike', 'typing.Union[...]',
add_newdoc('DTypeLike', 'typing.Union[...]',
"""
- A `~typing.Union` representing objects that can be coerced into a `~numpy.dtype`.
+ A `~typing.Union` representing objects that can be coerced
+ into a `~numpy.dtype`.
Among others this includes the likes of:
@@ -101,7 +103,8 @@ add_newdoc('DTypeLike', 'typing.Union[...]',
See Also
--------
:ref:`Specifying and constructing data types <arrays.dtypes.constructing>`
- A comprehensive overview of all objects that can be coerced into data types.
+ A comprehensive overview of all objects that can be coerced
+ into data types.
Examples
--------
diff --git a/numpy/typing/_dtype_like.py b/numpy/typing/_dtype_like.py
index 0955f5b18..fdfb955b5 100644
--- a/numpy/typing/_dtype_like.py
+++ b/numpy/typing/_dtype_like.py
@@ -1,4 +1,14 @@
-from typing import Any, List, Sequence, Tuple, Union, Type, TypeVar, Protocol, TypedDict
+from typing import (
+ Any,
+ List,
+ Sequence,
+ Tuple,
+ Union,
+ Type,
+ TypeVar,
+ Protocol,
+ TypedDict,
+)
import numpy as np
@@ -55,18 +65,23 @@ class _DTypeDictBase(TypedDict):
names: Sequence[str]
formats: Sequence[_DTypeLikeNested]
+
# Mandatory + optional keys
class _DTypeDict(_DTypeDictBase, total=False):
+ # Only `str` elements are usable as indexing aliases,
+ # but `titles` can in principle accept any object
offsets: Sequence[int]
- titles: Sequence[Any] # Only `str` elements are usable as indexing aliases, but all objects are legal
+ titles: Sequence[Any]
itemsize: int
aligned: bool
+
# A protocol for anything with the dtype attribute
class _SupportsDType(Protocol[_DType_co]):
@property
def dtype(self) -> _DType_co: ...
+
# Would create a dtype[np.void]
_VoidDTypeLike = Union[
# (flexible_dtype, itemsize)
diff --git a/numpy/typing/_extended_precision.py b/numpy/typing/_extended_precision.py
index 0900bc659..edc1778ce 100644
--- a/numpy/typing/_extended_precision.py
+++ b/numpy/typing/_extended_precision.py
@@ -1,4 +1,5 @@
-"""A module with platform-specific extended precision `numpy.number` subclasses.
+"""A module with platform-specific extended precision
+`numpy.number` subclasses.
The subclasses are defined here (instead of ``__init__.pyi``) such
that they can be imported conditionally via the numpy's mypy plugin.
diff --git a/numpy/typing/mypy_plugin.py b/numpy/typing/mypy_plugin.py
index 091980d65..5421d6bfa 100644
--- a/numpy/typing/mypy_plugin.py
+++ b/numpy/typing/mypy_plugin.py
@@ -33,7 +33,8 @@ To enable the plugin, one must add it to their mypy `configuration file`_:
from __future__ import annotations
-import typing as t
+from collections.abc import Iterable
+from typing import Final, TYPE_CHECKING, Callable
import numpy as np
@@ -44,15 +45,15 @@ try:
from mypy.nodes import MypyFile, ImportFrom, Statement
from mypy.build import PRI_MED
- _HookFunc = t.Callable[[AnalyzeTypeContext], Type]
+ _HookFunc = Callable[[AnalyzeTypeContext], Type]
MYPY_EX: None | ModuleNotFoundError = None
except ModuleNotFoundError as ex:
MYPY_EX = ex
-__all__: t.List[str] = []
+__all__: list[str] = []
-def _get_precision_dict() -> t.Dict[str, str]:
+def _get_precision_dict() -> dict[str, str]:
names = [
("_NBitByte", np.byte),
("_NBitShort", np.short),
@@ -73,7 +74,7 @@ def _get_precision_dict() -> t.Dict[str, str]:
return ret
-def _get_extended_precision_list() -> t.List[str]:
+def _get_extended_precision_list() -> list[str]:
extended_types = [np.ulonglong, np.longlong, np.longdouble, np.clongdouble]
extended_names = {
"uint128",
@@ -107,13 +108,13 @@ def _get_c_intp_name() -> str:
#: A dictionary mapping type-aliases in `numpy.typing._nbit` to
#: concrete `numpy.typing.NBitBase` subclasses.
-_PRECISION_DICT: t.Final = _get_precision_dict()
+_PRECISION_DICT: Final = _get_precision_dict()
#: A list with the names of all extended precision `np.number` subclasses.
-_EXTENDED_PRECISION_LIST: t.Final = _get_extended_precision_list()
+_EXTENDED_PRECISION_LIST: Final = _get_extended_precision_list()
#: The name of the ctypes quivalent of `np.intp`
-_C_INTP: t.Final = _get_c_intp_name()
+_C_INTP: Final = _get_c_intp_name()
def _hook(ctx: AnalyzeTypeContext) -> Type:
@@ -124,8 +125,8 @@ def _hook(ctx: AnalyzeTypeContext) -> Type:
return api.named_type(name_new)
-if t.TYPE_CHECKING or MYPY_EX is None:
- def _index(iterable: t.Iterable[Statement], id: str) -> int:
+if TYPE_CHECKING or MYPY_EX is None:
+ def _index(iterable: Iterable[Statement], id: str) -> int:
"""Identify the first ``ImportFrom`` instance the specified `id`."""
for i, value in enumerate(iterable):
if getattr(value, "id", None) == id:
@@ -137,7 +138,7 @@ if t.TYPE_CHECKING or MYPY_EX is None:
def _override_imports(
file: MypyFile,
module: str,
- imports: t.List[t.Tuple[str, t.Optional[str]]],
+ imports: list[tuple[str, None | str]],
) -> None:
"""Override the first `module`-based import with new `imports`."""
# Construct a new `from module import y` statement
@@ -145,7 +146,7 @@ if t.TYPE_CHECKING or MYPY_EX is None:
import_obj.is_top_level = True
# Replace the first `module`-based import statement with `import_obj`
- for lst in [file.defs, file.imports]: # type: t.List[Statement]
+ for lst in [file.defs, file.imports]: # type: list[Statement]
i = _index(lst, module)
lst[i] = import_obj
@@ -153,7 +154,8 @@ if t.TYPE_CHECKING or MYPY_EX is None:
"""A mypy plugin for handling versus numpy-specific typing tasks."""
def get_type_analyze_hook(self, fullname: str) -> None | _HookFunc:
- """Set the precision of platform-specific `numpy.number` subclasses.
+ """Set the precision of platform-specific `numpy.number`
+ subclasses.
For example: `numpy.int_`, `numpy.longlong` and `numpy.longdouble`.
"""
@@ -161,7 +163,9 @@ if t.TYPE_CHECKING or MYPY_EX is None:
return _hook
return None
- def get_additional_deps(self, file: MypyFile) -> t.List[t.Tuple[int, str, int]]:
+ def get_additional_deps(
+ self, file: MypyFile
+ ) -> list[tuple[int, str, int]]:
"""Handle all import-based overrides.
* Import platform-specific extended-precision `numpy.number`
@@ -184,11 +188,11 @@ if t.TYPE_CHECKING or MYPY_EX is None:
)
return ret
- def plugin(version: str) -> t.Type[_NumpyPlugin]:
+ def plugin(version: str) -> type[_NumpyPlugin]:
"""An entry-point for mypy."""
return _NumpyPlugin
else:
- def plugin(version: str) -> t.Type[_NumpyPlugin]:
+ def plugin(version: str) -> type[_NumpyPlugin]:
"""An entry-point for mypy."""
raise MYPY_EX
diff --git a/numpy/typing/tests/test_runtime.py b/numpy/typing/tests/test_runtime.py
index 151b06bed..5b5df49dc 100644
--- a/numpy/typing/tests/test_runtime.py
+++ b/numpy/typing/tests/test_runtime.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import sys
-from typing import get_type_hints, Union, Tuple, NamedTuple, get_args, get_origin
+from typing import get_type_hints, Union, NamedTuple, get_args, get_origin
import pytest
import numpy as np
@@ -12,7 +12,7 @@ import numpy.typing as npt
class TypeTup(NamedTuple):
typ: type
- args: Tuple[type, ...]
+ args: tuple[type, ...]
origin: None | type
diff --git a/numpy/typing/tests/test_typing.py b/numpy/typing/tests/test_typing.py
index 81863c780..d5338d2b1 100644
--- a/numpy/typing/tests/test_typing.py
+++ b/numpy/typing/tests/test_typing.py
@@ -1,10 +1,13 @@
+from __future__ import annotations
+
import importlib.util
import itertools
import os
import re
import shutil
from collections import defaultdict
-from typing import Optional, IO, Dict, List
+from collections.abc import Iterator
+from typing import IO, TYPE_CHECKING
import pytest
import numpy as np
@@ -21,6 +24,10 @@ except ImportError:
else:
NO_MYPY = False
+if TYPE_CHECKING:
+ # We need this as annotation, but it's located in a private namespace.
+ # As a compromise, do *not* import it during runtime
+ from _pytest.mark.structures import ParameterSet
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
PASS_DIR = os.path.join(DATA_DIR, "pass")
@@ -32,7 +39,7 @@ CACHE_DIR = os.path.join(DATA_DIR, ".mypy_cache")
#: A dictionary with file names as keys and lists of the mypy stdout as values.
#: To-be populated by `run_mypy`.
-OUTPUT_MYPY: Dict[str, List[str]] = {}
+OUTPUT_MYPY: dict[str, list[str]] = {}
def _key_func(key: str) -> str:
@@ -62,7 +69,10 @@ def run_mypy() -> None:
NUMPY_TYPING_TEST_CLEAR_CACHE=0 pytest numpy/typing/tests
"""
- if os.path.isdir(CACHE_DIR) and bool(os.environ.get("NUMPY_TYPING_TEST_CLEAR_CACHE", True)):
+ if (
+ os.path.isdir(CACHE_DIR)
+ and bool(os.environ.get("NUMPY_TYPING_TEST_CLEAR_CACHE", True))
+ ):
shutil.rmtree(CACHE_DIR)
for directory in (PASS_DIR, REVEAL_DIR, FAIL_DIR, MISC_DIR):
@@ -85,7 +95,7 @@ def run_mypy() -> None:
OUTPUT_MYPY.update((k, list(v)) for k, v in iterator if k)
-def get_test_cases(directory):
+def get_test_cases(directory: str) -> Iterator[ParameterSet]:
for root, _, files in os.walk(directory):
for fname in files:
if os.path.splitext(fname)[-1] == ".py":
@@ -103,7 +113,7 @@ def get_test_cases(directory):
@pytest.mark.slow
@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
@pytest.mark.parametrize("path", get_test_cases(PASS_DIR))
-def test_success(path):
+def test_success(path) -> None:
# Alias `OUTPUT_MYPY` so that it appears in the local namespace
output_mypy = OUTPUT_MYPY
if path in output_mypy:
@@ -115,7 +125,7 @@ def test_success(path):
@pytest.mark.slow
@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
@pytest.mark.parametrize("path", get_test_cases(FAIL_DIR))
-def test_fail(path):
+def test_fail(path: str) -> None:
__tracebackhide__ = True
with open(path) as fin:
@@ -138,7 +148,10 @@ def test_fail(path):
for i, line in enumerate(lines):
lineno = i + 1
- if line.startswith('#') or (" E:" not in line and lineno not in errors):
+ if (
+ line.startswith('#')
+ or (" E:" not in line and lineno not in errors)
+ ):
continue
target_line = lines[lineno - 1]
@@ -162,14 +175,19 @@ Observed error: {!r}
"""
-def _test_fail(path: str, error: str, expected_error: Optional[str], lineno: int) -> None:
+def _test_fail(
+ path: str,
+ error: str,
+ expected_error: None | str,
+ lineno: int,
+) -> None:
if expected_error is None:
raise AssertionError(_FAIL_MSG1.format(lineno, error))
elif error not in expected_error:
raise AssertionError(_FAIL_MSG2.format(lineno, expected_error, error))
-def _construct_format_dict():
+def _construct_format_dict() -> dict[str, str]:
dct = {k.split(".")[-1]: v.replace("numpy", "numpy.typing") for
k, v in _PRECISION_DICT.items()}
@@ -193,12 +211,18 @@ def _construct_format_dict():
"float96": "numpy.floating[numpy.typing._96Bit]",
"float128": "numpy.floating[numpy.typing._128Bit]",
"float256": "numpy.floating[numpy.typing._256Bit]",
- "complex64": "numpy.complexfloating[numpy.typing._32Bit, numpy.typing._32Bit]",
- "complex128": "numpy.complexfloating[numpy.typing._64Bit, numpy.typing._64Bit]",
- "complex160": "numpy.complexfloating[numpy.typing._80Bit, numpy.typing._80Bit]",
- "complex192": "numpy.complexfloating[numpy.typing._96Bit, numpy.typing._96Bit]",
- "complex256": "numpy.complexfloating[numpy.typing._128Bit, numpy.typing._128Bit]",
- "complex512": "numpy.complexfloating[numpy.typing._256Bit, numpy.typing._256Bit]",
+ "complex64": ("numpy.complexfloating"
+ "[numpy.typing._32Bit, numpy.typing._32Bit]"),
+ "complex128": ("numpy.complexfloating"
+ "[numpy.typing._64Bit, numpy.typing._64Bit]"),
+ "complex160": ("numpy.complexfloating"
+ "[numpy.typing._80Bit, numpy.typing._80Bit]"),
+ "complex192": ("numpy.complexfloating"
+ "[numpy.typing._96Bit, numpy.typing._96Bit]"),
+ "complex256": ("numpy.complexfloating"
+ "[numpy.typing._128Bit, numpy.typing._128Bit]"),
+ "complex512": ("numpy.complexfloating"
+ "[numpy.typing._256Bit, numpy.typing._256Bit]"),
"ubyte": f"numpy.unsignedinteger[{dct['_NBitByte']}]",
"ushort": f"numpy.unsignedinteger[{dct['_NBitShort']}]",
@@ -217,9 +241,14 @@ def _construct_format_dict():
"single": f"numpy.floating[{dct['_NBitSingle']}]",
"double": f"numpy.floating[{dct['_NBitDouble']}]",
"longdouble": f"numpy.floating[{dct['_NBitLongDouble']}]",
- "csingle": f"numpy.complexfloating[{dct['_NBitSingle']}, {dct['_NBitSingle']}]",
- "cdouble": f"numpy.complexfloating[{dct['_NBitDouble']}, {dct['_NBitDouble']}]",
- "clongdouble": f"numpy.complexfloating[{dct['_NBitLongDouble']}, {dct['_NBitLongDouble']}]",
+ "csingle": ("numpy.complexfloating"
+ f"[{dct['_NBitSingle']}, {dct['_NBitSingle']}]"),
+ "cdouble": ("numpy.complexfloating"
+ f"[{dct['_NBitDouble']}, {dct['_NBitDouble']}]"),
+ "clongdouble": (
+ "numpy.complexfloating"
+ f"[{dct['_NBitLongDouble']}, {dct['_NBitLongDouble']}]"
+ ),
# numpy.typing
"_NBitInt": dct['_NBitInt'],
@@ -231,14 +260,16 @@ def _construct_format_dict():
#: A dictionary with all supported format keys (as keys)
#: and matching values
-FORMAT_DICT: Dict[str, str] = _construct_format_dict()
+FORMAT_DICT: dict[str, str] = _construct_format_dict()
-def _parse_reveals(file: IO[str]) -> List[str]:
- """Extract and parse all ``" # E: "`` comments from the passed file-like object.
+def _parse_reveals(file: IO[str]) -> list[str]:
+ """Extract and parse all ``" # E: "`` comments from the passed
+ file-like object.
- All format keys will be substituted for their respective value from `FORMAT_DICT`,
- *e.g.* ``"{float64}"`` becomes ``"numpy.floating[numpy.typing._64Bit]"``.
+ All format keys will be substituted for their respective value
+ from `FORMAT_DICT`, *e.g.* ``"{float64}"`` becomes
+ ``"numpy.floating[numpy.typing._64Bit]"``.
"""
string = file.read().replace("*", "")
@@ -250,7 +281,8 @@ def _parse_reveals(file: IO[str]) -> List[str]:
# there is the risk of accidentally grabbing dictionaries and sets
key_set = set(re.findall(r"\{(.*?)\}", comments))
kwargs = {
- k: FORMAT_DICT.get(k, f"<UNRECOGNIZED FORMAT KEY {k!r}>") for k in key_set
+ k: FORMAT_DICT.get(k, f"<UNRECOGNIZED FORMAT KEY {k!r}>") for
+ k in key_set
}
fmt_str = comments.format(**kwargs)
@@ -260,7 +292,10 @@ def _parse_reveals(file: IO[str]) -> List[str]:
@pytest.mark.slow
@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
@pytest.mark.parametrize("path", get_test_cases(REVEAL_DIR))
-def test_reveal(path):
+def test_reveal(path: str) -> None:
+ """Validate that mypy correctly infers the return-types of
+ the expressions in `path`.
+ """
__tracebackhide__ = True
with open(path) as fin:
@@ -290,18 +325,33 @@ Observed reveal: {!r}
"""
-def _test_reveal(path: str, reveal: str, expected_reveal: str, lineno: int) -> None:
+def _test_reveal(
+ path: str,
+ reveal: str,
+ expected_reveal: str,
+ lineno: int,
+) -> None:
+ """Error-reporting helper function for `test_reveal`."""
if reveal not in expected_reveal:
- raise AssertionError(_REVEAL_MSG.format(lineno, expected_reveal, reveal))
+ raise AssertionError(
+ _REVEAL_MSG.format(lineno, expected_reveal, reveal)
+ )
@pytest.mark.slow
@pytest.mark.skipif(NO_MYPY, reason="Mypy is not installed")
@pytest.mark.parametrize("path", get_test_cases(PASS_DIR))
-def test_code_runs(path):
+def test_code_runs(path: str) -> None:
+ """Validate that the code in `path` properly during runtime."""
path_without_extension, _ = os.path.splitext(path)
dirname, filename = path.split(os.sep)[-2:]
- spec = importlib.util.spec_from_file_location(f"{dirname}.{filename}", path)
+
+ spec = importlib.util.spec_from_file_location(
+ f"{dirname}.{filename}", path
+ )
+ assert spec is not None
+ assert spec.loader is not None
+
test_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_module)