diff --git a/pyproject.toml b/pyproject.toml index 1f1b5a73ac5..50cd52b3bbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,7 +131,8 @@ exclude = [ ] [tool.mypy] -files = ["sphinx", "utils"] +files = ["sphinx", "utils", "tests"] +exclude = ["tests/certs", "tests/js", "tests/roots"] check_untyped_defs = true disallow_incomplete_defs = true python_version = "3.9" @@ -212,6 +213,154 @@ module = [ ] disallow_any_generics = false +[[tool.mypy.overrides]] +module = [ + # tests/ +# "tests.conftest", + "tests.test_addnodes", + "tests.test_application", + "tests.test_errors", + "tests.test_events", + "tests.test_highlighting", + "tests.test_project", + "tests.test_quickstart", + "tests.test_roles", + "tests.test_search", + "tests.test_toctree", + "tests.test_versioning", +# "tests.utils", + # tests/test_builders +# "tests.test_builders.conftest", + "tests.test_builders.test_build", + "tests.test_builders.test_build_changes", + "tests.test_builders.test_build_dirhtml", + "tests.test_builders.test_build_epub", + "tests.test_builders.test_builder", + "tests.test_builders.test_build_gettext", + "tests.test_builders.test_build_html", + "tests.test_builders.test_build_html_5_output", + "tests.test_builders.test_build_html_assets", + "tests.test_builders.test_build_html_code", + "tests.test_builders.test_build_html_download", + "tests.test_builders.test_build_html_highlight", + "tests.test_builders.test_build_html_image", + "tests.test_builders.test_build_html_maths", + "tests.test_builders.test_build_html_numfig", + "tests.test_builders.test_build_html_tocdepth", + "tests.test_builders.test_build_latex", + "tests.test_builders.test_build_linkcheck", + "tests.test_builders.test_build_manpage", + "tests.test_builders.test_build_texinfo", + "tests.test_builders.test_build_text", + "tests.test_builders.test_build_warnings", + # tests/test_config + "tests.test_config.test_config", + "tests.test_config.test_correct_year", + # tests/test_directives + "tests.test_directives.test_directive_code", + "tests.test_directives.test_directive_object_description", + "tests.test_directives.test_directive_only", + "tests.test_directives.test_directive_option", + "tests.test_directives.test_directive_other", + "tests.test_directives.test_directive_patch", + "tests.test_directives.test_directives_no_typesetting", + # tests/test_domains + "tests.test_domains.test_domain_c", + "tests.test_domains.test_domain_cpp", + "tests.test_domains.test_domain_js", + "tests.test_domains.test_domain_py", + "tests.test_domains.test_domain_py_canonical", + "tests.test_domains.test_domain_py_fields", + "tests.test_domains.test_domain_py_pyfunction", + "tests.test_domains.test_domain_py_pyobject", + "tests.test_domains.test_domain_rst", + "tests.test_domains.test_domain_std", + # tests/test_environment + "tests.test_environment.test_environment", + "tests.test_environment.test_environment_indexentries", + "tests.test_environment.test_environment_record_dependencies", + "tests.test_environment.test_environment_toctree", + # tests/test_extensions + "tests.test_extensions.ext_napoleon_pep526_data_google", + "tests.test_extensions.ext_napoleon_pep526_data_numpy", + "tests.test_extensions.test_ext_apidoc", + "tests.test_extensions.test_ext_autodoc", + "tests.test_extensions.test_ext_autodoc_autoattribute", + "tests.test_extensions.test_ext_autodoc_autoclass", + "tests.test_extensions.test_ext_autodoc_autodata", + "tests.test_extensions.test_ext_autodoc_autofunction", + "tests.test_extensions.test_ext_autodoc_automodule", + "tests.test_extensions.test_ext_autodoc_autoproperty", + "tests.test_extensions.test_ext_autodoc_configs", + "tests.test_extensions.test_ext_autodoc_events", + "tests.test_extensions.test_ext_autodoc_mock", + "tests.test_extensions.test_ext_autodoc_preserve_defaults", + "tests.test_extensions.test_ext_autodoc_private_members", + "tests.test_extensions.test_ext_autosectionlabel", + "tests.test_extensions.test_ext_autosummary", + "tests.test_extensions.test_ext_coverage", + "tests.test_extensions.test_ext_doctest", + "tests.test_extensions.test_ext_duration", + "tests.test_extensions.test_extension", + "tests.test_extensions.test_ext_extlinks", + "tests.test_extensions.test_ext_githubpages", + "tests.test_extensions.test_ext_graphviz", + "tests.test_extensions.test_ext_ifconfig", + "tests.test_extensions.test_ext_imgconverter", + "tests.test_extensions.test_ext_imgmockconverter", + "tests.test_extensions.test_ext_inheritance_diagram", + "tests.test_extensions.test_ext_intersphinx", + "tests.test_extensions.test_ext_math", + "tests.test_extensions.test_ext_napoleon", + "tests.test_extensions.test_ext_napoleon_docstring", + "tests.test_extensions.test_ext_todo", + "tests.test_extensions.test_ext_viewcode", + # tests/test_intl + "tests.test_intl.test_catalogs", + "tests.test_intl.test_intl", + "tests.test_intl.test_locale", + # tests/test_markup + "tests.test_markup.test_markup", + "tests.test_markup.test_metadata", + "tests.test_markup.test_parser", + "tests.test_markup.test_smartquotes", + # tests/test_pycode + "tests.test_pycode.test_pycode", + "tests.test_pycode.test_pycode_ast", + "tests.test_pycode.test_pycode_parser", + # tests/test_theming + "tests.test_theming.test_html_theme", + "tests.test_theming.test_templating", + "tests.test_theming.test_theming", + # tests/test_transforms + "tests.test_transforms.test_transforms_move_module_targets", + "tests.test_transforms.test_transforms_post_transforms", + "tests.test_transforms.test_transforms_post_transforms_code", + "tests.test_transforms.test_transforms_reorder_nodes", + # tests/test_util + "tests.test_util.test_util", + "tests.test_util.test_util_display", + "tests.test_util.test_util_docstrings", + "tests.test_util.test_util_docutils", + "tests.test_util.test_util_fileutil", + "tests.test_util.test_util_i18n", + "tests.test_util.test_util_images", + "tests.test_util.test_util_inspect", + "tests.test_util.test_util_inventory", + "tests.test_util.test_util_logging", + "tests.test_util.test_util_matching", + "tests.test_util.test_util_nodes", + "tests.test_util.test_util_rst", + "tests.test_util.test_util_template", + "tests.test_util.test_util_typing", + "tests.test_util.typing_test_data", + # tests/test_writers + "tests.test_writers.test_api_translator", + "tests.test_writers.test_docutilsconf", + "tests.test_writers.test_writer_latex", +] +ignore_errors = true + [tool.pytest.ini_options] minversion = 6.0 addopts = [ diff --git a/tests/conftest.py b/tests/conftest.py index a722971b81a..6d5dbd2e905 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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 @@ -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'] @@ -32,11 +40,11 @@ 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()}" @@ -44,7 +52,7 @@ def pytest_report_header(config): @pytest.fixture(autouse=True) -def _cleanup_docutils(): +def _cleanup_docutils() -> Generator[None, None, None]: saved_path = sys.path yield # run the test sys.path[:] = saved_path diff --git a/tests/test_builders/conftest.py b/tests/test_builders/conftest.py index 5c54cc87928..54a388c2104 100644 --- a/tests/test_builders/conftest.py +++ b/tests/test_builders/conftest.py @@ -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: @@ -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]: yield _parse etree_cache.clear() diff --git a/tests/utils.py b/tests/utils.py index 32636b7936c..1fbd431ebec 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -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]]: + @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)