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

doctest_docutils.py: Upstream changes #25

Merged
merged 6 commits into from Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGES
Expand Up @@ -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: <https://github.com/pytest-dev/pytest/pull/11317>

### Packaging

- Move pytest configuration to `pyproject.toml` (#24)
Expand Down
2 changes: 1 addition & 1 deletion docs/doctest/pytest.md
Expand Up @@ -14,7 +14,7 @@ turn compatible with {mod}`doctest`.

## Using fixtures

Normal pytest convention apply, {ref}`a visible conftest.py must be available <pytest:using-fixtures>`
Normal pytest convention apply, {ref}`a visible conftest.py must be available <pytest:about-fixtures>`
for the file being tested.

This requires a understanding of `confest.py` - in the same way {ref}`pytest's vanilla doctest plugin <pytest:doctest>` does.
Expand Down
22 changes: 22 additions & 0 deletions src/doctest_docutils.py
@@ -1,4 +1,5 @@
import doctest
import functools
import linecache
import logging
import os
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions 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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 24 additions & 20 deletions src/pytest_doctest_docutils.py
Expand Up @@ -12,20 +12,20 @@
"""
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
import typing as t

import _pytest
import pytest
from _pytest import outcomes
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
Expand Down Expand Up @@ -71,8 +71,8 @@ def pytest_unconfigure() -> None:


def pytest_collect_file(
file_path: Path, parent: pytest.Collector
) -> Optional[Tuple["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(
Expand All @@ -82,7 +82,7 @@ def pytest_collect_file(
_pytest.doctest._is_main_py(file_path),
)
):
mod: Tuple[
mod: t.Union[
DocTestDocutilsFile, _pytest.doctest.DoctestModule
] = _pytest.doctest.DoctestModule.from_parent(parent, path=file_path)
return mod
Expand All @@ -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):
Expand All @@ -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:
Expand All @@ -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]
Expand All @@ -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":
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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")
Expand Down