diff --git a/src/pytest_sphinx.py b/src/pytest_sphinx.py index d232118..27622b6 100644 --- a/src/pytest_sphinx.py +++ b/src/pytest_sphinx.py @@ -47,6 +47,11 @@ class SphinxDoctestDirectives(enum.Enum): DOCTEST = 5 +class DirectiveSyntax(enum.Enum): + RST = 1 + MYST = 2 + + _DIRECTIVES_W_OPTIONS = ( SphinxDoctestDirectives.TESTOUTPUT, SphinxDoctestDirectives.DOCTEST, @@ -79,7 +84,7 @@ def pytest_collect_file( def _is_doctest(config: Config, path: Path, parent: Union[Session, Package]) -> bool: - if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path): + if path.suffix in (".txt", ".rst", ".md") and parent.session.isinitpath(path): return True globs = config.getoption("doctestglob") or ["test*.txt"] assert isinstance(globs, list) @@ -95,7 +100,7 @@ def _is_doctest(config: Config, path: Path, parent: Union[Session, Package]) -> _OPTION_DIRECTIVE_RE = re.compile(r':options:\s*([^\n\'"]*)$') _OPTION_SKIPIF_RE = re.compile(r':skipif:\s*([^\n\'"]*)$') -_DIRECTIVE_RE = re.compile( +_RST_DIRECTIVE_RE = re.compile( r""" \s*\.\.\s (?P(testcode|testoutput|testsetup|testcleanup|doctest)) @@ -106,6 +111,28 @@ def _is_doctest(config: Config, path: Path, parent: Union[Session, Package]) -> re.VERBOSE, ) +_MYST_DIRECTIVE_RE = re.compile( + r""" + \s*``` + {(?P(testcode|testoutput|testsetup|testcleanup|doctest))} + \s* + (?P([^\n'"]*)) + $ + """, + re.VERBOSE, +) + +_SYNTAX_TO_DIRECTIVE_RE = { + DirectiveSyntax.RST: _RST_DIRECTIVE_RE, + DirectiveSyntax.MYST: _MYST_DIRECTIVE_RE, +} + +_FILE_EXTENSION_TO_SYNTAX = { + ".rst": DirectiveSyntax.RST, + ".md": DirectiveSyntax.MYST, + ".py": DirectiveSyntax.RST, +} + def _split_into_body_and_options( section_content: str, @@ -222,7 +249,7 @@ def __init__( self.options = options -def get_sections(docstring: str) -> List[Union[Any, Section]]: +def get_sections(docstring: str, syntax: DirectiveSyntax) -> List[Union[Any, Section]]: lines = textwrap.dedent(docstring).splitlines() sections = [] @@ -248,7 +275,7 @@ def add_match( except IndexError: break - match = _DIRECTIVE_RE.match(line) + match = _SYNTAX_TO_DIRECTIVE_RE[syntax].match(line) if match: group = match.groupdict() directive = getattr(SphinxDoctestDirectives, group["directive"].upper()) @@ -263,7 +290,11 @@ def add_match( except IndexError: add_match(directive, i, j, groups) break - if block_line.lstrip() and _get_indentation(block_line) <= indentation: + if ( + syntax is DirectiveSyntax.RST + and block_line.lstrip() + and _get_indentation(block_line) <= indentation + ) or (syntax is DirectiveSyntax.MYST and block_line.lstrip() == "```"): add_match(directive, i, j, groups) i = j - 1 break @@ -272,7 +303,9 @@ def add_match( def docstring2examples( - docstring: str, globs: Optional[GlobDict] = None + docstring: str, + syntax: DirectiveSyntax = DirectiveSyntax.RST, + globs: Optional[GlobDict] = None, ) -> List[Union[Any, doctest.Example]]: """ Parse all sphinx test directives in the docstring and create a @@ -283,7 +316,7 @@ def docstring2examples( if globs is None: globs = {} - sections = get_sections(docstring) + sections = get_sections(docstring, syntax) def get_testoutput_section_data( section: "Section", @@ -527,6 +560,7 @@ def collect(self) -> Iterator[_pytest.doctest.DoctestItem]: encoding = self.config.getini("doctest_encoding") text = self.fspath.read_text(encoding) name = self.fspath.basename + file_extension = Path(self.fspath).suffix optionflags = _pytest.doctest.get_optionflags(self) # type:ignore runner = SphinxDocTestRunner( @@ -535,8 +569,11 @@ def collect(self) -> Iterator[_pytest.doctest.DoctestItem]: checker=_pytest.doctest._get_checker(), ) + syntax = _FILE_EXTENSION_TO_SYNTAX[file_extension] + examples = docstring2examples(text, syntax=syntax) + test = doctest.DocTest( - examples=docstring2examples(text), + examples=examples, globs={}, name=name, filename=name,