Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lint] whitelist all test files except configuration and utils in tests #12109

Merged
merged 13 commits into from
Mar 16, 2024
80 changes: 79 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,85 @@ exclude = [
]

[tool.mypy]
files = ["sphinx", "utils"]
files = ["sphinx", "utils", "tests"]
exclude = [
picnixz marked this conversation as resolved.
Show resolved Hide resolved
"tests/certs", "tests/js", "tests/roots",
"^tests/__init__\\.py$",
"^tests/test_\\w+/__init__\\.py$",
# tests/test_builders
"^tests/test_builders/test_build\\.py$",
"^tests/test_builders/test_build_(changes|dirhtml|epub|gettext)\\.py$",
"^tests/test_builders/test_build_html\\.py$",
"^tests/test_builders/test_build_html_5_output\\.py$",
"""(?x)(^tests/test_builders/test_build_html_(
assets|code|download|highlight|image|maths|numfig|tocdepth
)\\.py)$""",
"""(?x)(^tests/test_builders/test_build_(
latex|linkcheck|manpage|texinfo|text|warnings
)\\.py)$""",
"^tests/test_builders/test_builder\\.py$",
# tests/test_config
"^tests/test_config/test_(config|correct_year)\\.py$",
# tests/test_directives
"""(?x)(^tests/test_directives/test_directive_(
code|object_description|only|option|other|patch
)\\.py)$""",
"^tests/test_directives/test_directives_no_typesetting\\.py$",
# tests/test_domains
"^tests/test_domains/test_domain_(c|cpp|js)\\.py$",
"^tests/test_domains/test_domain_py\\.py$",
"^tests/test_domains/test_domain_py_(canonical|fields|pyfunction|pyobject)\\.py$",
"^tests/test_domains/test_domain_(rst|std)\\.py$",
# tests/test_environment
"^tests/test_environment/test_environment\\.py$",
"""(?x)(^tests/test_environment/test_environment_(
indexentries|record_dependencies|toctree
)\\.py)$""",
# tests/test_extensions
"^tests/test_extensions/ext_napoleon_pep526_data_(google|numpy)\\.py$",
"^tests/test_extensions/test_ext_(apidoc|autodoc)\\.py$",
"""(?x)(^tests/test_extensions/test_ext_autodoc_(
auto(attribute|class|data|function|module|property)|
configs|events|mock|preserve_defaults|private_members
)\\.py)$""",
"""(?x)(^tests/test_extensions/test_ext_(
autosectionlabel|autosummary|coverage|doctest|duration|
extlinks|githubpages|graphviz|ifconfig|imgconverter|
imgmockconverter|inheritance_diagram|intersphinx|math|
napoleon|napoleon_docstring|todo|viewcode
)\\.py)$""",
"^tests/test_extensions/test_extension\\.py$",
# tests/test_intl
"^tests/test_intl/test_(catalogs|intl|locale)\\.py$",
# tests/test_markup
"^tests/test_markup/test_(markup|metadata|parser|smartquotes)\\.py$",
# tests/test_pycode
"^tests/test_pycode/test_pycode\\.py$",
"^tests/test_pycode/test_pycode_(ast|parser)\\.py$$",
# tests/test_theming
"^tests/test_theming/test_(html_theme|templating|theming)\\.py$",
# tests/test_transforms
"^tests/test_transforms/test_transforms_move_module_targets\\.py$",
"^tests/test_transforms/test_transforms_post_transforms\\.py$",
"^tests/test_transforms/test_transforms_post_transforms_code\\.py$",
"^tests/test_transforms/test_transforms_reorder_nodes\\.py$",
# tests/test_util
"^tests/test_util/test_util\\.py$",
"""(?x)(^tests/test_util/test_util_(
display|docstrings|docutils|fileutil|i18n|images|inspect|
inventory|logging|matching|nodes|rst|template|typing
)\\.py)$""",
"^tests/test_util/typing_test_data\\.py$",
# tests/test_writers
"^tests/test_writers/test_api_translator\\.py$",
"^tests/test_writers/test_docutilsconf\\.py$",
"^tests/test_writers/test_writer_latex\\.py$",
# tests/
"""(?x)(^tests/test_(
addnodes|application|errors|events|highlighting|project|
quickstart|roles|search|toctree|versioning
)\\.py)$""",
]
check_untyped_defs = true
disallow_incomplete_defs = true
python_version = "3.9"
Expand Down
18 changes: 13 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING

import docutils
import pytest
Expand All @@ -10,8 +13,13 @@
import sphinx.pycode
from sphinx.testing.util import _clean_up_global_state

if TYPE_CHECKING:
from collections.abc import Generator


def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'):
def _init_console(
locale_dir: str | None = sphinx.locale._LOCALE_DIR, catalog: str = 'sphinx',
) -> tuple[sphinx.locale.NullTranslations, bool]:
"""Monkeypatch ``init_console`` to skip its action.

Some tests rely on warning messages in English. We don't want
Expand All @@ -23,7 +31,7 @@ def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'):

sphinx.locale.init_console = _init_console

pytest_plugins = 'sphinx.testing.fixtures'
pytest_plugins = ['sphinx.testing.fixtures']

# Exclude 'roots' dirs for pytest test collector
collect_ignore = ['roots']
Expand All @@ -32,19 +40,19 @@ def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'):


@pytest.fixture(scope='session')
def rootdir():
def rootdir() -> Path:
return Path(__file__).parent.resolve() / 'roots'


def pytest_report_header(config):
def pytest_report_header(config: pytest.Config) -> str:
header = f"libraries: Sphinx-{sphinx.__display_version__}, docutils-{docutils.__version__}"
if hasattr(config, '_tmp_path_factory'):
header += f"\nbase tmp_path: {config._tmp_path_factory.getbasetemp()}"
return header


@pytest.fixture(autouse=True)
def _cleanup_docutils():
def _cleanup_docutils() -> Generator[None, None, None]:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
saved_path = sys.path
yield # run the test
sys.path[:] = saved_path
Expand Down
8 changes: 5 additions & 3 deletions tests/test_builders/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from html5lib import HTMLParser

if TYPE_CHECKING:
from collections.abc import Callable, Generator
from pathlib import Path
from xml.etree.ElementTree import Element

etree_cache = {}
etree_cache: dict[Path, Element] = {}


def _parse(fname: Path) -> HTMLParser:
def _parse(fname: Path) -> Element:
if fname in etree_cache:
return etree_cache[fname]
with fname.open('rb') as fp:
Expand All @@ -21,6 +23,6 @@ def _parse(fname: Path) -> HTMLParser:


@pytest.fixture(scope='package')
def cached_etree_parse():
def cached_etree_parse() -> Generator[Callable[[Path], Element], None, None]:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
yield _parse
etree_cache.clear()
49 changes: 33 additions & 16 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,71 @@
from __future__ import annotations

import contextlib
import http.server
import pathlib
import threading
from http.server import ThreadingHTTPServer
from pathlib import Path
from ssl import PROTOCOL_TLS_SERVER, SSLContext
from threading import Thread
from typing import TYPE_CHECKING, TypeVar

import filelock

if TYPE_CHECKING:
from collections.abc import Callable, Generator
from contextlib import AbstractContextManager
from socketserver import BaseRequestHandler
from typing import Any, Final

# Generated with:
# $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \
# -keyout cert.pem -addext "subjectAltName = DNS:localhost"
TESTS_ROOT = pathlib.Path(__file__).parent
CERT_FILE = str(TESTS_ROOT / "certs" / "cert.pem")
TESTS_ROOT: Final[Path] = Path(__file__).parent
CERT_FILE: Final[str] = str(TESTS_ROOT / "certs" / "cert.pem")

# File lock for tests
LOCK_PATH = str(TESTS_ROOT / 'test-server.lock')
LOCK_PATH: Final[str] = str(TESTS_ROOT / 'test-server.lock')


class HttpServerThread(threading.Thread):
def __init__(self, handler, *args, **kwargs):
class HttpServerThread(Thread):
def __init__(self, handler: type[BaseRequestHandler], /, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.server = http.server.ThreadingHTTPServer(("localhost", 7777), handler)
self.server = ThreadingHTTPServer(("localhost", 7777), handler)

def run(self):
def run(self) -> None:
self.server.serve_forever(poll_interval=0.001)

def terminate(self):
def terminate(self) -> None:
self.server.shutdown()
self.server.server_close()
self.join()


class HttpsServerThread(HttpServerThread):
def __init__(self, handler, *args, **kwargs):
def __init__(
self, handler: type[BaseRequestHandler], /, *args: Any, **kwargs: Any,
) -> None:
super().__init__(handler, *args, **kwargs)
sslcontext = SSLContext(PROTOCOL_TLS_SERVER)
sslcontext.load_cert_chain(CERT_FILE)
self.server.socket = sslcontext.wrap_socket(self.server.socket, server_side=True)


def create_server(thread_class):
def server(handler):
_T_co = TypeVar('_T_co', bound=HttpServerThread, covariant=True)


def create_server(
server_thread_class: type[_T_co],
) -> Callable[[type[BaseRequestHandler]], AbstractContextManager[_T_co]]:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
@contextlib.contextmanager
def server(handler_class: type[BaseRequestHandler]) -> Generator[_T_co, None, None]:
lock = filelock.FileLock(LOCK_PATH)
with lock:
server_thread = thread_class(handler, daemon=True)
server_thread = server_thread_class(handler_class, daemon=True)
server_thread.start()
try:
yield server_thread
finally:
server_thread.terminate()
return contextlib.contextmanager(server)
return server


http_server = create_server(HttpServerThread)
Expand Down