Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fbf7312

Browse files
committedNov 5, 2021
Add a pytest-based test for catching import loops
1 parent fba75a1 commit fbf7312

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed
 

‎tests/test_circular_imports.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Tests for circular imports in all local packages and modules.
2+
3+
This ensures all internal packages can be imported right away without
4+
any need to import some other module before doing so.
5+
6+
This module is based on an idea that pytest uses for self-testing:
7+
* https://github.com/sanitizers/octomachinery/blob/be18b54/tests/circular_imports_test.py
8+
* https://github.com/pytest-dev/pytest/blob/d18c75b/testing/test_meta.py
9+
* https://twitter.com/codewithanthony/status/1229445110510735361
10+
"""
11+
from itertools import chain
12+
from pathlib import Path
13+
from types import ModuleType
14+
from typing import Generator, List
15+
import os
16+
import pkgutil
17+
import subprocess
18+
import sys
19+
20+
import pytest
21+
22+
import proxy
23+
24+
25+
def _find_all_importables(pkg: ModuleType) -> List[str]:
26+
"""Find all importables in the project.
27+
28+
Return them in order.
29+
"""
30+
return sorted(
31+
set(
32+
chain.from_iterable(
33+
_discover_path_importables(Path(p), pkg.__name__)
34+
# FIXME: Unignore after upgrading to `mypy > 0.910`. The fix
35+
# FIXME: is in the `master` branch of upstream since Aug 4,
36+
# FIXME: 2021 but has not yet been included in any releases.
37+
# Refs:
38+
# * https://github.com/python/mypy/issues/1422
39+
# * https://github.com/python/mypy/pull/9454
40+
for p in pkg.__path__ # type: ignore[attr-defined]
41+
),
42+
),
43+
)
44+
45+
46+
def _discover_path_importables(
47+
pkg_pth: Path, pkg_name: str,
48+
) -> Generator[str, None, None]:
49+
"""Yield all importables under a given path and package."""
50+
for dir_path, _d, file_names in os.walk(pkg_pth):
51+
pkg_dir_path = Path(dir_path)
52+
53+
if pkg_dir_path.parts[-1] == '__pycache__':
54+
continue
55+
56+
if all(Path(_).suffix != '.py' for _ in file_names):
57+
continue
58+
59+
rel_pt = pkg_dir_path.relative_to(pkg_pth)
60+
pkg_pref = '.'.join((pkg_name,) + rel_pt.parts)
61+
yield from (
62+
pkg_path
63+
for _, pkg_path, _ in pkgutil.walk_packages(
64+
(str(pkg_dir_path),), prefix=f'{pkg_pref}.',
65+
)
66+
)
67+
68+
69+
# FIXME: Ignore is necessary for as long as pytest hasn't figured out their
70+
# FIXME: typing for the `parametrize` mark.
71+
# Refs:
72+
# * https://github.com/pytest-dev/pytest/issues/7469#issuecomment-918345196
73+
# * https://github.com/pytest-dev/pytest/issues/3342
74+
@pytest.mark.parametrize( # type: ignore[misc]
75+
'import_path',
76+
_find_all_importables(proxy),
77+
)
78+
def test_no_warnings(import_path: str) -> None:
79+
"""Verify that exploding importables doesn't explode.
80+
81+
This is seeking for any import errors including ones caused
82+
by circular imports.
83+
"""
84+
imp_cmd = (
85+
sys.executable,
86+
'-W', 'error',
87+
'-c', f'import {import_path!s}',
88+
)
89+
90+
subprocess.check_call(imp_cmd)

‎tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ minversion = 3.21.0
77
deps =
88
-rrequirements.txt
99
-rrequirements-testing.txt
10+
-rrequirements-tunnel.txt
1011
# NOTE: The command is invoked by the script name and not via
1112
# NOTE: `{envpython} -m pytest` because it'd add CWD into $PYTHONPATH
1213
# NOTE: testing the project from the Git checkout

0 commit comments

Comments
 (0)
Please sign in to comment.