Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
`__doctest_requires__` metadata. Bare module or distribution requirements
remain zero-dependency; version-constrained requirements use the optional
`packaging` dependency.
* Added `xdoctest.stdlib_doctest`, the documented adapter package for
stdlib `doctest` objects, checkers, and option flags. It exposes checker
and optionflag registration,
runtime-state conversion, structured stdlib-doctest intake helpers, and the
supporting `StdlibExampleLike`, `RuntimeState`, and `DocTest` types used by
those public signatures.

### Fixed
* Fixed issue #181 where comment indentation could cause parsing issues.
Expand Down
1 change: 1 addition & 0 deletions docs/source/auto/xdoctest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Subpackages
:maxdepth: 4

xdoctest.docstr
xdoctest.stdlib_doctest
xdoctest.utils

Submodules
Expand Down
50 changes: 50 additions & 0 deletions docs/source/auto/xdoctest.stdlib_doctest.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
xdoctest.stdlib_doctest package
===============================

.. automodule:: xdoctest.stdlib_doctest

Supporting types
----------------

.. autoclass:: xdoctest.stdlib_doctest.StdlibExampleLike
:noindex:
:show-inheritance:

.. autoclass:: xdoctest.stdlib_doctest.RuntimeState
:noindex:
:show-inheritance:

.. autoclass:: xdoctest.stdlib_doctest.DocTest
:noindex:
:show-inheritance:

.. autoclass:: xdoctest.stdlib_doctest.OutputChecker
:noindex:
:show-inheritance:

Registration
------------

.. autofunction:: xdoctest.stdlib_doctest.register_optionflag
:noindex:

.. autofunction:: xdoctest.stdlib_doctest.register_checker
:noindex:

.. autofunction:: xdoctest.stdlib_doctest.resolve_checker
:noindex:

Conversion
----------

.. autofunction:: xdoctest.stdlib_doctest.optionflags_to_runtime_state
:noindex:

.. autofunction:: xdoctest.stdlib_doctest.runtime_state_to_optionflags
:noindex:

.. autofunction:: xdoctest.stdlib_doctest.from_examples
:noindex:

.. autofunction:: xdoctest.stdlib_doctest.from_stdlib_doctest
:noindex:
14 changes: 7 additions & 7 deletions src/xdoctest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,11 @@ def fib(n):
__submodules__ = [
'runner',
'exceptions',
'stdlib_doctest',
]


from xdoctest import docstr, utils
from xdoctest import docstr, stdlib_doctest, utils
from xdoctest.exceptions import (
DoctestParseError,
ExistingEventLoopError,
Expand All @@ -332,7 +333,7 @@ def fib(n):
doctest_callable,
doctest_module,
)
from xdoctest.directive_facade import (
from xdoctest.stdlib_doctest import (
BLANKLINE_MARKER,
DONT_ACCEPT_BLANKLINE,
ELLIPSIS,
Expand All @@ -345,19 +346,17 @@ def fib(n):
IGNORE_WANT,
NORMALIZE_REPR,
NORMALIZE_WHITESPACE,
OutputChecker,
REPORT_CDIFF,
REPORT_NDIFF,
REPORT_UDIFF,
SHOW_WARNINGS,
SKIP,
optionflags_to_runtime_state,
register_optionflag,
runtime_state_to_optionflags,
)
from xdoctest.checker_facade import (
OutputChecker,
register_checker,
register_optionflag,
resolve_checker,
runtime_state_to_optionflags,
)

__all__ = [
Expand All @@ -369,6 +368,7 @@ def fib(n):
'doctest_callable',
'utils',
'docstr',
'stdlib_doctest',
'__version__',
'BLANKLINE_MARKER',
'ELLIPSIS_MARKER',
Expand Down
43 changes: 31 additions & 12 deletions src/xdoctest/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,29 @@ def _coerce_runstate(
return runstate
if runstate is None:
return directive.RuntimeState()
from xdoctest import checker_facade
from xdoctest.stdlib_doctest import _checker, _optionflags

optionflags = checker_facade.runtime_state_to_optionflags(runstate)
normalized = checker_facade.optionflags_to_runtime_state(optionflags)
optionflags = _optionflags.runtime_state_to_optionflags(runstate)
normalized = _optionflags.optionflags_to_runtime_state(optionflags)
normalized.set_output_checker(
str(runstate.get('_output_checker', 'xdoctest'))
)
return normalized


def _stdlib_checker_want(want: str) -> str:
"""Restore the line ending expected by stdlib output checkers.

Xdoctest stores parsed wants without a trailing newline, while
:mod:`doctest` passes non-empty ``Example.want`` values to output
checkers with their terminating newline intact. Foreign checkers use the
stdlib protocol, so normalize only at that boundary.
"""
if want and not want.endswith('\n'):
return want + '\n'
return want


def check_output(
got: str,
want: str,
Expand Down Expand Up @@ -336,10 +349,14 @@ def check_output(
checker_name = runstate.get_output_checker()
if checker_name == 'xdoctest':
return _xdoctest_check_output(got, want, runstate)
from xdoctest import checker_facade
optionflags = checker_facade.runtime_state_to_optionflags(runstate)
output_checker = checker_facade.resolve_checker(checker_name, runstate)
return bool(output_checker.check_output(want, got, optionflags))
from xdoctest.stdlib_doctest import _checker, _optionflags
optionflags = _optionflags.runtime_state_to_optionflags(runstate)
output_checker = _checker.resolve_checker(checker_name, runstate)
return bool(
output_checker.check_output(
_stdlib_checker_want(want), got, optionflags
)
)


def _check_match(
Expand Down Expand Up @@ -843,16 +860,18 @@ def output_difference(
# A foreign checker may provide its own difference rendering
# (e.g. to display fixed-up wants); fall back to the native
# renderer when it inherits the facade default.
from xdoctest import checker_facade
output_checker = checker_facade.resolve_checker(
from xdoctest.stdlib_doctest import _checker, _optionflags
output_checker = _checker.resolve_checker(
checker_name, runstate
)
if (
output_checker.__class__.output_difference
is not checker_facade.OutputChecker.output_difference
is not _checker.OutputChecker.output_difference
):
example = doctest.Example(source='', want=self.want)
optionflags = checker_facade.runtime_state_to_optionflags(
example = doctest.Example(
source='', want=_stdlib_checker_want(self.want)
)
optionflags = _optionflags.runtime_state_to_optionflags(
runstate
)
return output_checker.output_difference(
Expand Down
10 changes: 5 additions & 5 deletions src/xdoctest/directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,10 +517,10 @@ def update(self, directives: list[Directive]) -> None:
continue

if key not in self._global_state:
from xdoctest import directive_facade
from xdoctest.stdlib_doctest import _optionflags

if directive_facade.is_registered_optionflag(key):
flag = directive_facade.get_optionflag(key)
if _optionflags.is_registered_optionflag(key):
flag = _optionflags.get_optionflag(key)
if action == 'assign':
if value:
self.add_output_checker_flags(flag, inline=bool(directive.inline))
Expand Down Expand Up @@ -1127,9 +1127,9 @@ def parse_directive_optstr(

name = name.upper()
if name not in COMMANDS:
from xdoctest import directive_facade
from xdoctest.stdlib_doctest import _optionflags

if not directive_facade.is_registered_optionflag(name):
if not _optionflags.is_registered_optionflag(name):
msg = 'Unknown directive: {!r}'.format(optpart)
warnings.warn(msg)
return None
Expand Down
6 changes: 3 additions & 3 deletions src/xdoctest/doctest_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,10 +1311,10 @@ def run(
# state so local negative directives can override configured defaults;
# only checker-specific flags remain in the raw bitmask.
default_state = self.config['default_runtime_state']
from xdoctest import directive_facade
from xdoctest.stdlib_doctest import _optionflags

native_defaults = directive.RuntimeState(default_state).to_dict()
runstate = self._runstate = directive_facade.optionflags_to_runtime_state(
runstate = self._runstate = _optionflags.optionflags_to_runtime_state(
int(self.config.get('output_checker_flags', 0)),
cast(directive.RuntimeStateDict, native_defaults),
)
Expand Down Expand Up @@ -1504,7 +1504,7 @@ def run(
finally:
asyncio_runner = None
# Execute the doctest code. ``_part_context`` is an
# extension point used by the stdlib_compat intake
# extension point used by the stdlib-doctest intake
# seam to apply per-part warning policy without
# rewriting source. Default is a no-op.
try:
Expand Down
147 changes: 147 additions & 0 deletions src/xdoctest/stdlib_doctest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
r"""
Adapter API for stdlib :mod:`doctest` objects and protocols.

Tools that already produce or consume stdlib :mod:`doctest` objects can use
this package to run them through xdoctest without depending on the layout of
the private adapter modules.

Quick start
-----------

A third-party collector that already produces stdlib :mod:`doctest` objects
can register its checker and pass those objects directly to xdoctest:

Example:
>>> import doctest
>>> import xdoctest.stdlib_doctest as stdlib_doctest
>>> fix = stdlib_doctest.register_optionflag('_STDLIB_DOCTEST_DOCS_FIX')
>>> class ThirdPartyChecker(stdlib_doctest.OutputChecker):
... def check_output(self, want, got, optionflags):
... if optionflags & fix:
... want = want.replace('L', '')
... got = got.replace('L', '')
... return super().check_output(want, got, optionflags)
>>> checker_name = '_stdlib_doctest_docs_checker'
>>> stdlib_doctest.register_checker(checker_name, ThirdPartyChecker)
>>> example = doctest.Example(
... source='print("10")\n',
... want='10L\n',
... lineno=0,
... options={fix: True},
... )
>>> converted = stdlib_doctest.from_examples(
... [example],
... name='third-party-example',
... config={'output_checker': checker_name},
... )
>>> isinstance(converted, stdlib_doctest.DocTest)
True
>>> converted.run(verbose=0, on_error='raise')['passed']
True

Registration
------------

``register_optionflag``
Register a stdlib-style option flag by name, optionally binding it to an
xdoctest runtime-state key. The returned bit is shared with
:func:`doctest.register_optionflag`.

``register_checker``
Register a stdlib-shaped output checker under a configuration name.
Select it for a test with ``DocTest.config['output_checker']``.

``resolve_checker``
Resolve the native checker or a previously registered foreign checker.

Conversion
----------

``optionflags_to_runtime_state`` and ``runtime_state_to_optionflags``
Convert between stdlib option-flag integers and xdoctest's structured
runtime state.

``from_examples`` and ``from_stdlib_doctest``
Convert stdlib-shaped examples or a complete :class:`doctest.DocTest`
into a runnable :class:`DocTest` while preserving source locations,
namespaces, option boundaries, and expected output.

Supporting types
----------------

``StdlibExampleLike``
Structural protocol accepted by :func:`from_examples`.

``RuntimeState``
Structured runtime state accepted and returned by the conversion helpers.

``DocTest``
Runnable xdoctest object returned by the intake helpers.

``OutputChecker``
xdoctest's native matcher exposed through the stdlib checker interface for
composition by foreign checkers.

The overlapping registration, conversion, checker, and optionflag names remain
available at the top-level :mod:`xdoctest` namespace for compatibility. This
package is the documented home of the stdlib-doctest adapter.
"""

from xdoctest.directive import RuntimeState
from xdoctest.doctest_example import DocTest

from ._checker import OutputChecker, register_checker, resolve_checker
from ._convert import StdlibExampleLike, from_examples, from_stdlib_doctest
from ._optionflags import (
BLANKLINE_MARKER,
DONT_ACCEPT_BLANKLINE,
ELLIPSIS,
ELLIPSIS_MARKER,
FLOAT_CMP,
IGNORE_EXCEPTION_DETAIL,
IGNORE_OUTPUT,
IGNORE_WARNINGS,
IGNORE_WHITESPACE,
IGNORE_WANT,
NORMALIZE_REPR,
NORMALIZE_WHITESPACE,
REPORT_CDIFF,
REPORT_NDIFF,
REPORT_UDIFF,
SHOW_WARNINGS,
SKIP,
optionflags_to_runtime_state,
register_optionflag,
runtime_state_to_optionflags,
)

__all__ = [
'BLANKLINE_MARKER',
'DONT_ACCEPT_BLANKLINE',
'DocTest',
'ELLIPSIS',
'ELLIPSIS_MARKER',
'FLOAT_CMP',
'IGNORE_EXCEPTION_DETAIL',
'IGNORE_OUTPUT',
'IGNORE_WARNINGS',
'IGNORE_WHITESPACE',
'IGNORE_WANT',
'NORMALIZE_REPR',
'NORMALIZE_WHITESPACE',
'OutputChecker',
'REPORT_CDIFF',
'REPORT_NDIFF',
'REPORT_UDIFF',
'RuntimeState',
'SHOW_WARNINGS',
'SKIP',
'StdlibExampleLike',
'from_examples',
'from_stdlib_doctest',
'optionflags_to_runtime_state',
'register_checker',
'register_optionflag',
'resolve_checker',
'runtime_state_to_optionflags',
]
Loading
Loading