From 75b9322f7794191cf4fd7f8f07d3ecd7af4543a8 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 14 Oct 2023 07:26:19 -0500 Subject: [PATCH 1/6] _from_module: Add caching from pytest upstream - https://github.com/pytest-dev/pytest/commit/e787d2ed48297cdd1ae95f9cccb31fc999ab1ad5 - https://github.com/pytest-dev/pytest/pull/11317 - https://github.com/python/cpython/blob/1c26f1ce6c3965b1f7395b60bf45d7fc0bc6de57/Lib/doctest.py#L948-L975 - https://github.com/python/cpython/blob/0c56c0502270ebe29461fe5cab3ee4d6111da650/Lib/doctest.py#L954-L981 --- src/doctest_docutils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/doctest_docutils.py b/src/doctest_docutils.py index b18d6d3..ff06d33 100644 --- a/src/doctest_docutils.py +++ b/src/doctest_docutils.py @@ -1,4 +1,5 @@ import doctest +import functools import linecache import logging import os @@ -362,6 +363,27 @@ def condition(node: Node) -> bool: if test is not None: tests.append(test) + if sys.version_info < (3, 13): + + def _from_module( + self, module: t.Optional[t.Union[str, types.ModuleType]], object: object + ) -> bool: + """`cached_property` objects are never considered a part + of the 'current module'. As such they are skipped by doctest. + Here we override `_from_module` to check the underlying + function instead. https://github.com/python/cpython/issues/107995 + """ + if isinstance(object, functools.cached_property): + object = object.func + + # Type ignored because this is a private function. + return t.cast( + bool, super()._from_module(module, object) # type:ignore[misc] + ) + + else: # pragma: no cover + pass + def _get_test( self, string: str, From 6351aa2ca06408fd05b28032671235e5ac5303fa Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 14 Oct 2023 07:44:03 -0500 Subject: [PATCH 2/6] pytest_doctest_docutils: Typing fixes --- src/pytest_doctest_docutils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pytest_doctest_docutils.py b/src/pytest_doctest_docutils.py index 2d26d34..6cd5d3f 100644 --- a/src/pytest_doctest_docutils.py +++ b/src/pytest_doctest_docutils.py @@ -17,7 +17,7 @@ import types from io import StringIO from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple, Type +from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple, Type, Union import _pytest import pytest @@ -72,7 +72,7 @@ def pytest_unconfigure() -> None: def pytest_collect_file( file_path: Path, parent: pytest.Collector -) -> Optional[Tuple["DocTestDocutilsFile", "_pytest.doctest.DoctestModule"]]: +) -> Optional[Union["DocTestDocutilsFile", "_pytest.doctest.DoctestModule"]]: config = parent.config if file_path.suffix == ".py": if config.option.doctestmodules and not any( @@ -82,7 +82,7 @@ def pytest_collect_file( _pytest.doctest._is_main_py(file_path), ) ): - mod: Tuple[ + mod: Union[ DocTestDocutilsFile, _pytest.doctest.DoctestModule ] = _pytest.doctest.DoctestModule.from_parent(parent, path=file_path) return mod From 2669e0abb56e9b2010567d2f4017b7775cbc3828 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 14 Oct 2023 07:46:12 -0500 Subject: [PATCH 3/6] refactor!(pytest_doctest_docutils): Use namespaced typings --- src/pytest_doctest_docutils.py | 44 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/pytest_doctest_docutils.py b/src/pytest_doctest_docutils.py index 6cd5d3f..70adcc5 100644 --- a/src/pytest_doctest_docutils.py +++ b/src/pytest_doctest_docutils.py @@ -12,12 +12,12 @@ """ import bdb import doctest +import io import logging +import pathlib import sys import types -from io import StringIO -from pathlib import Path -from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple, Type, Union +import typing as t import _pytest import pytest @@ -25,7 +25,7 @@ from _pytest.outcomes import OutcomeException from doctest_docutils import DocutilsDocTestFinder, setup -if TYPE_CHECKING: +if t.TYPE_CHECKING: from doctest import _Out from _pytest.config.argparsing import Parser @@ -71,8 +71,8 @@ def pytest_unconfigure() -> None: def pytest_collect_file( - file_path: Path, parent: pytest.Collector -) -> Optional[Union["DocTestDocutilsFile", "_pytest.doctest.DoctestModule"]]: + file_path: pathlib.Path, parent: pytest.Collector +) -> t.Optional[t.Union["DocTestDocutilsFile", "_pytest.doctest.DoctestModule"]]: config = parent.config if file_path.suffix == ".py": if config.option.doctestmodules and not any( @@ -82,7 +82,7 @@ def pytest_collect_file( _pytest.doctest._is_main_py(file_path), ) ): - mod: Union[ + mod: t.Union[ DocTestDocutilsFile, _pytest.doctest.DoctestModule ] = _pytest.doctest.DoctestModule.from_parent(parent, path=file_path) return mod @@ -91,14 +91,16 @@ def pytest_collect_file( return None -def _is_doctest(config: pytest.Config, path: Path, parent: pytest.Collector) -> bool: +def _is_doctest( + config: pytest.Config, path: pathlib.Path, parent: pytest.Collector +) -> bool: if path.suffix in (".rst", ".md") and parent.session.isinitpath(path): return True globs = config.getoption("doctestglob") or ["*.rst", "*.md"] return any(path.match(path_pattern=glob) for glob in globs) -def _init_runner_class() -> Type["doctest.DocTestRunner"]: +def _init_runner_class() -> t.Type["doctest.DocTestRunner"]: import doctest class PytestDoctestRunner(doctest.DebugRunner): @@ -110,8 +112,8 @@ class PytestDoctestRunner(doctest.DebugRunner): def __init__( self, - checker: Optional["doctest.OutputChecker"] = None, - verbose: Optional[bool] = None, + checker: t.Optional["doctest.OutputChecker"] = None, + verbose: t.Optional[bool] = None, optionflags: int = 0, continue_on_failure: bool = True, ) -> None: @@ -137,7 +139,9 @@ def report_unexpected_exception( out: "_Out", test: "doctest.DocTest", example: "doctest.Example", - exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType], + exc_info: t.Tuple[ + t.Type[BaseException], BaseException, types.TracebackType + ], ) -> None: if isinstance(exc_info[1], OutcomeException): raise exc_info[1] @@ -154,8 +158,8 @@ def report_unexpected_exception( def _get_runner( - checker: Optional["doctest.OutputChecker"] = None, - verbose: Optional[bool] = None, + checker: t.Optional["doctest.OutputChecker"] = None, + verbose: t.Optional[bool] = None, optionflags: int = 0, continue_on_failure: bool = True, ) -> "doctest.DocTestRunner": @@ -175,9 +179,9 @@ def _get_runner( class DocutilsDocTestRunner(doctest.DocTestRunner): def summarize( # type: ignore - self, out: "_Out", verbose: Optional[bool] = None - ) -> Tuple[int, int]: - string_io = StringIO() + self, out: "_Out", verbose: t.Optional[bool] = None + ) -> t.Tuple[int, int]: + string_io = io.StringIO() old_stdout = sys.stdout sys.stdout = string_io try: @@ -188,8 +192,8 @@ def summarize( # type: ignore return res def _DocTestRunner__patched_linecache_getlines( - self, filename: str, module_globals: Any = None - ) -> Any: + self, filename: str, module_globals: t.Any = None + ) -> t.Any: # this is overridden from DocTestRunner adding the try-except below m = self._DocTestRunner__LINECACHE_FILENAME_RE.match(filename) # type: ignore if m and m.group("name") == self.test.name: @@ -206,7 +210,7 @@ def _DocTestRunner__patched_linecache_getlines( class DocTestDocutilsFile(pytest.Module): - def collect(self) -> Iterable["DoctestItem"]: + def collect(self) -> t.Iterable["DoctestItem"]: setup() encoding = self.config.getini("doctest_encoding") From a1d1dc11720a50d18199d9faba8a623ac4f2152a Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 14 Oct 2023 07:46:57 -0500 Subject: [PATCH 4/6] refactor(linkify_issues): Import typing as namespace --- src/linkify_issues.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linkify_issues.py b/src/linkify_issues.py index 054093c..2815721 100644 --- a/src/linkify_issues.py +++ b/src/linkify_issues.py @@ -1,5 +1,5 @@ import re -from typing import TypedDict +import typing as t from docutils import nodes from sphinx.application import Sphinx @@ -57,7 +57,7 @@ def condition(node: nodes.Node) -> bool: node.parent.replace(node, retnodes) -class SetupDict(TypedDict): +class SetupDict(t.TypedDict): version: str parallel_read_safe: bool parallel_write_safe: bool From 0dd45abfa6391107be0c4918336fd33039d29d8e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 14 Oct 2023 07:56:43 -0500 Subject: [PATCH 5/6] docs(pytest): Fix link See also: docs/doctest/pytest.md:17: WARNING: undefined label: 'pytest:using-fixtures' --- docs/doctest/pytest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctest/pytest.md b/docs/doctest/pytest.md index 5366aad..d5e7907 100644 --- a/docs/doctest/pytest.md +++ b/docs/doctest/pytest.md @@ -14,7 +14,7 @@ turn compatible with {mod}`doctest`. ## Using fixtures -Normal pytest convention apply, {ref}`a visible conftest.py must be available ` +Normal pytest convention apply, {ref}`a visible conftest.py must be available ` for the file being tested. This requires a understanding of `confest.py` - in the same way {ref}`pytest's vanilla doctest plugin ` does. From 90e83a3e23a839887c01ecf0f42f8534e61f50dd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 14 Oct 2023 07:53:11 -0500 Subject: [PATCH 6/6] docs(CHANGES): Note updates --- CHANGES | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES b/CHANGES index 2143f16..d3e795a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,24 @@ To install the unreleased gp-libs version, see [developmental releases](https:// $ pip install --user --upgrade --pre gp-libs ``` +## gp-libs 0.0.3 (unreleased) + +### Fixes + +#### doctest_docutils + +- `_from_module`: Backport {func}`functools.cached_property` support (#25) + +#### pytest_doctest_docutils + +- `pytest_collect_file`: Typing fix (#25) + +### Development + +- Use `import typing as t` everywhere (#25) + + See also: + ### Packaging - Move pytest configuration to `pyproject.toml` (#24)