diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000000..61cb5ad822d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +branch = True +source = aiohttp, tests +omit = site-packages diff --git a/CHANGES/6591.misc b/CHANGES/6591.misc new file mode 100644 index 00000000000..2f422f43b21 --- /dev/null +++ b/CHANGES/6591.misc @@ -0,0 +1 @@ +Changed importing Gunicorn to happen on-demand, decreasing import time by ~53% -- :user:`Dreamsorcerer`. diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 5f3e4dd950e..a14f2acf83c 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,6 +1,6 @@ __version__ = "4.0.0a2.dev0" -from typing import Tuple +from typing import TYPE_CHECKING, Tuple from . import hdrs as hdrs from .client import ( @@ -103,6 +103,13 @@ TraceResponseChunkReceivedParams as TraceResponseChunkReceivedParams, ) +if TYPE_CHECKING: + # At runtime these are lazy-loaded at the bottom of the file. + from .worker import ( + GunicornUVLoopWebWorker as GunicornUVLoopWebWorker, + GunicornWebWorker as GunicornWebWorker, + ) + __all__: Tuple[str, ...] = ( "hdrs", # client @@ -203,11 +210,28 @@ "TraceRequestRedirectParams", "TraceRequestStartParams", "TraceResponseChunkReceivedParams", + # workers (imported lazily with __getattr__) + "GunicornUVLoopWebWorker", + "GunicornWebWorker", ) -try: - from .worker import GunicornUVLoopWebWorker, GunicornWebWorker - __all__ += ("GunicornWebWorker", "GunicornUVLoopWebWorker") -except ImportError: # pragma: no cover - pass +def __dir__() -> Tuple[str, ...]: + return __all__ + ("__author__", "__doc__") + + +def __getattr__(name: str) -> object: + global GunicornUVLoopWebWorker, GunicornWebWorker + + # Importing gunicorn takes a long time (>100ms), so only import if actually needed. + if name in ("GunicornUVLoopWebWorker", "GunicornWebWorker"): + try: + from .worker import GunicornUVLoopWebWorker as guv, GunicornWebWorker as gw + except ImportError: + return None + + GunicornUVLoopWebWorker = guv # type: ignore[misc] + GunicornWebWorker = gw # type: ignore[misc] + return guv if name == "GunicornUVLoopWebWorker" else gw + + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 5c522dd6d6f..9164f90ae8f 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -151,7 +151,7 @@ pyjwt==2.3.0 # via gidgethub pyparsing==2.4.7 # via packaging -pytest==7.1.3 +pytest==6.2.5 # via # -r requirements/lint.txt # -r requirements/test.txt diff --git a/requirements/lint.txt b/requirements/lint.txt index 1ead32d72f8..22b7956e283 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -2,5 +2,5 @@ aioredis==2.0.1 mypy==0.981; implementation_name=="cpython" pre-commit==2.17.0 -pytest==7.1.3 +pytest==6.2.5 slotscheck==0.8.0 diff --git a/requirements/test.txt b/requirements/test.txt index d23596cef7c..c011d4e44e4 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,7 +6,7 @@ freezegun==1.1.0 mypy==0.981; implementation_name=="cpython" mypy-extensions==0.4.3; implementation_name=="cpython" proxy.py ~= 2.4.4rc3 -pytest==7.1.3 +pytest==6.2.5 pytest-cov==3.0.0 pytest-mock==3.6.1 python-on-whales==0.36.1 diff --git a/setup.cfg b/setup.cfg index 3d51b56ff38..d270e811d97 100644 --- a/setup.cfg +++ b/setup.cfg @@ -104,11 +104,6 @@ exclude_lines = @abc.abstractmethod @abstractmethod -[coverage:run] -branch = True -source = aiohttp, tests -omit = site-packages - [tool:pytest] addopts = # show 10 slowest invocations: diff --git a/tests/test___all__.py b/tests/test___all__.py deleted file mode 100644 index 6c7d855e592..00000000000 --- a/tests/test___all__.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Any - - -def test___all__(pytester: Any) -> None: - """See https://github.com/aio-libs/aiohttp/issues/6197""" - pytester.makepyfile( - test_a=""" - from aiohttp import * - """ - ) - result = pytester.runpytest("-vv") - result.assert_outcomes(passed=0, errors=0) - - -def test_web___all__(pytester: Any) -> None: - pytester.makepyfile( - test_b=""" - from aiohttp.web import * - """ - ) - result = pytester.runpytest("-vv") - result.assert_outcomes(passed=0, errors=0) diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 00000000000..4514f4c44a9 --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,52 @@ +import os +import platform +import sys +from pathlib import Path + +import pytest + + +def test___all__(pytester: pytest.Pytester) -> None: + """See https://github.com/aio-libs/aiohttp/issues/6197""" + pytester.makepyfile( + test_a=""" + from aiohttp import * + assert 'GunicornWebWorker' in globals() + """ + ) + result = pytester.runpytest("-vv") + result.assert_outcomes(passed=0, errors=0) + + +def test_web___all__(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + test_b=""" + from aiohttp.web import * + """ + ) + result = pytester.runpytest("-vv") + result.assert_outcomes(passed=0, errors=0) + + +@pytest.mark.skipif( + not sys.platform.startswith("linux") or platform.python_implementation() == "PyPy", + reason="Timing is more reliable on Linux", +) +def test_import_time(pytester: pytest.Pytester) -> None: + """Check that importing aiohttp doesn't take too long. + + Obviously, the time may vary on different machines and may need to be adjusted + from time to time, but this should provide an early warning if something is + added that significantly increases import time. + """ + root = Path(__file__).parent.parent + old_path = os.environ.get("PYTHONPATH") + os.environ["PYTHONPATH"] = os.pathsep.join([str(root)] + sys.path) + r = pytester.run(sys.executable, "-We", "-c", "import aiohttp", timeout=0.41) + if old_path is None: + os.environ.pop("PYTHONPATH") + else: + os.environ["PYTHONPATH"] = old_path + + assert not r.stdout.str() + assert not r.stderr.str()