Skip to content

Commit 21c9b84

Browse files
authored
Merge pull request #2292 from sirosen/circular-import-test
Introduce a version of the circular import test
2 parents b5d03c9 + ef21a60 commit 21c9b84

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

tests/test_circular_imports.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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/aio-libs/aiohttp/blob/91108c9/tests/test_circular_imports.py
8+
* https://github.com/sanitizers/octomachinery/blob/be18b54/tests/circular_imports_test.py
9+
* https://github.com/pytest-dev/pytest/blob/d18c75b/testing/test_meta.py
10+
* https://twitter.com/codewithanthony/status/1229445110510735361
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import os
16+
import pkgutil
17+
import subprocess
18+
import sys
19+
from collections.abc import Iterator
20+
from itertools import chain
21+
from pathlib import Path
22+
from types import ModuleType
23+
24+
import pytest
25+
26+
import piptools
27+
from piptools.utils import PIP_VERSION
28+
29+
30+
def _find_all_importables(pkg: ModuleType) -> list[str]:
31+
"""Find all importables in the project.
32+
33+
Return them in order.
34+
"""
35+
return sorted(
36+
set(
37+
chain.from_iterable(
38+
_discover_path_importables(Path(p), pkg.__name__) for p in pkg.__path__
39+
),
40+
),
41+
)
42+
43+
44+
def _discover_path_importables(pkg_pth: Path, pkg_name: str) -> Iterator[str]:
45+
"""Yield all importables under a given path and package."""
46+
for dir_path, _d, file_names in os.walk(pkg_pth):
47+
pkg_dir_path = Path(dir_path)
48+
49+
if pkg_dir_path.parts[-1] == "__pycache__":
50+
continue
51+
52+
if all(Path(_).suffix != ".py" for _ in file_names): # pragma: no cover
53+
continue
54+
55+
rel_pt = pkg_dir_path.relative_to(pkg_pth)
56+
pkg_pref = ".".join((pkg_name,) + rel_pt.parts)
57+
yield from (
58+
pkg_path
59+
for _, pkg_path, _ in pkgutil.walk_packages(
60+
(str(pkg_dir_path),),
61+
prefix=f"{pkg_pref}.",
62+
)
63+
)
64+
65+
66+
def _allowed_deprecation_warning_filters() -> list[str]:
67+
"""
68+
Yield filters which allow for deprecation warnings based on the current
69+
test environment.
70+
"""
71+
# note that we can't use regex syntax in filters as of yet, only literals
72+
# https://github.com/python/cpython/pull/138149 allows regex usage, but is not
73+
# yet supported on all Python versions we support
74+
flags: list[str] = []
75+
if PIP_VERSION[:2] < (25, 3):
76+
flags.extend(
77+
("-W", "ignore:pkg_resources is deprecated as an API.:DeprecationWarning:")
78+
)
79+
if PIP_VERSION[:2] <= (22, 2):
80+
flags.extend(
81+
(
82+
"-W",
83+
(
84+
"ignore:path is deprecated. Use files() instead."
85+
":DeprecationWarning:"
86+
),
87+
"-W",
88+
(
89+
"ignore:Creating a LegacyVersion has been deprecated "
90+
"and will be removed in the next major release"
91+
":DeprecationWarning:"
92+
),
93+
)
94+
)
95+
return flags
96+
97+
98+
@pytest.mark.parametrize("import_path", _find_all_importables(piptools))
99+
def test_no_warnings(import_path: str) -> None:
100+
"""Verify that each importable name can be independently imported.
101+
102+
This is seeking for any import errors including ones caused
103+
by circular imports.
104+
"""
105+
import_statement = f"import {import_path!s}"
106+
# On lower pip versions, we need to allow certain deprecation warnings.
107+
flags = ("-W", "error", *_allowed_deprecation_warning_filters())
108+
command = (sys.executable, *flags, "-c", import_statement)
109+
110+
subprocess.check_call(command)

0 commit comments

Comments
 (0)