Skip to content

Commit

Permalink
Merge pull request #150 from Erotemic/dev/1.1.3
Browse files Browse the repository at this point in the history
Setup dev branch for 1.1.3
  • Loading branch information
Erotemic authored Jan 30, 2024
2 parents cde9aab + d729f72 commit 6b4abfb
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 99 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ We are currently working on porting this changelog to the specifications in
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Version 1.1.3 -

### Fixed
* `modname_to_modpath` now handles cases where editable packages have modules where the name is different than the package.
* Update `xdoctest.plugin` to support pytest 8.0
* Fixed deprecated usage of `ast.Num`


## Version 1.1.2 - Released 2023-010-25

Expand Down
7 changes: 4 additions & 3 deletions requirements/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
# pytest (that are also compatible with xdoctest) are for each legacy python
# major.minor version.
# See ~/local/tools/supported_python_versions_pip.py for helper script
pytest >= 6.2.5 ; python_version >= '3.10.0' # Python 3.10+
pytest >= 4.6.0 ; python_version < '3.10.0' and python_version >= '3.7.0' # Python 3.7-3.9
pytest >= 4.6.0 ; python_version < '3.7.0' and python_version >= '3.6.0' # Python 3.6

pytest >= 6.2.5 ; python_version >= '3.10.0' # Python 3.10+
pytest >= 4.6.0 ; python_version < '3.10.0' and python_version >= '3.7.0' # Python 3.7-3.9
pytest >= 4.6.0 ; python_version < '3.7.0' and python_version >= '3.6.0' # Python 3.6

pytest-cov >= 3.0.0 ; python_version >= '3.6.0' # Python 3.6+

Expand Down
2 changes: 1 addition & 1 deletion src/xdoctest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def fib(n):
mkinit xdoctest --nomods
'''

__version__ = '1.1.2'
__version__ = '1.1.3'


# Expose only select submodules
Expand Down
2 changes: 1 addition & 1 deletion src/xdoctest/directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ def extract(cls, text):
>>> # xdoctest: +REQUIRES(module:pytest)
>>> text = '# xdoctest: does_not_exist, skip'
>>> import pytest
>>> with pytest.warns(None) as record:
>>> with pytest.warns(Warning) as record:
>>> print(', '.join(list(map(str, Directive.extract(text)))))
<Directive(+SKIP)>
Expand Down
7 changes: 7 additions & 0 deletions src/xdoctest/doctest_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,13 @@ def run(self, verbose=None, on_error=None):

return summary

@property
def globs(self):
"""
Alias for ``global_namespace`` for pytest 8.0 compatability
"""
return self.global_namespace

@property
def cmdline(self):
"""
Expand Down
20 changes: 14 additions & 6 deletions src/xdoctest/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
global / local variable context.
PS1:
The original meaning is "Prompt String 1". In the context of xdoctest,
instead of referring to the prompt prefix, we use PS1 to refer to a
line that starts a "logical block" of code. In the original doctest
module these all had to be prefixed with ">>>". In xdoctest the prefix
is used to simply denote the code is part of a doctest. It does not
necessarily mean a new "logical block" is starting.
The original meaning is "Prompt String 1". For details see:
[SE32096]_ [BashPS1]_ [CustomPrompt]_ [GeekPrompt]_. In the context of
xdoctest, instead of referring to the prompt prefix, we use PS1 to
refer to a line that starts a "logical block" of code. In the original
doctest module these all had to be prefixed with ">>>". In xdoctest the
prefix is used to simply denote the code is part of a doctest. It does
not necessarily mean a new "logical block" is starting.
PS2:
The original meaning is "Prompt String 2". In the context of xdoctest,
Expand All @@ -32,6 +33,13 @@
While I do believe this AST-based code is a significant improvement over the
RE-based builtin doctest parser, I acknowledge that I'm not an AST expert and
there is room for improvement here.
References:
.. [SE32096] https://unix.stackexchange.com/questions/32096/why-is-bashs-prompt-variable-called-ps1
.. [BashPS1] https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#index-PS1
.. [CustomPrompt] https://wiki.archlinux.org/title/Bash/Prompt_customization
.. [GeekPrompt] https://web.archive.org/web/20230824025647/https://www.thegeekstuff.com/2008/09/bash-shell-take-control-of-ps1-ps2-ps3-ps4-and-prompt_command/
"""
import ast
import sys
Expand Down
121 changes: 86 additions & 35 deletions src/xdoctest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@
from _pytest import fixtures

try:
from packaging.version import parse as parse_version
_PYTEST_IS_GE_620 = parse_version(pytest.__version__) >= parse_version('6.2.0')
from packaging.version import parse as Version
except ImportError: # nocover
from distutils.version import LooseVersion
_PYTEST_IS_GE_620 = LooseVersion(pytest.__version__) >= LooseVersion('6.2.0')
from distutils.version import LooseVersion as Version

_PYTEST_IS_GE_620 = Version(pytest.__version__) >= Version('6.2.0')
_PYTEST_IS_GE_800 = Version(pytest.__version__) >= Version('8.0.0')


if _PYTEST_IS_GE_800:
from typing import Dict
from _pytest.fixtures import TopRequest


# def print(text):
# """ Hack so we can get stdout when debugging the plugin file """
Expand Down Expand Up @@ -185,33 +192,77 @@ def toterminal(self, tw):


class XDoctestItem(pytest.Item):
def __init__(self, name, parent, example=None):
def __init__(self,
name,
parent,
runner=None,
dtest=None):
"""
Args:
name (str):
parent (Any | None):
example (xdoctest.doctest_example.DocTest):
dtest (xdoctest.doctest_example.DocTest):
"""
super(XDoctestItem, self).__init__(name, parent)
self.cls = XDoctestItem
self.example = example
self.dtest = dtest
self.obj = None
self.fixture_request = None
if _PYTEST_IS_GE_800:
# Stuff needed for fixture support in pytest > 8.0.
fm = self.session._fixturemanager
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
self._fixtureinfo = fixtureinfo
self.fixturenames = fixtureinfo.names_closure
self._initrequest()
else:
self.fixture_request = None

if _PYTEST_IS_GE_800:
@classmethod
def from_parent( # type: ignore
cls,
parent,
name,
runner=None,
dtest=None,
):
# incompatible signature due to imposed limits on subclass
"""The public named constructor."""
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)

@property
def example(self):
"""
Backwards compatability with older pytest versions
"""
return self.dtest

def _initrequest(self) -> None:
assert _PYTEST_IS_GE_800
self.funcargs: Dict[str, object] = {}
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]

def setup(self):
if self.example is not None:
self.fixture_request = _setup_fixtures(self)
global_namespace = dict(getfixture=self.fixture_request.getfixturevalue)
for name, value in self.fixture_request.getfixturevalue('xdoctest_namespace').items():
global_namespace[name] = value
self.example.global_namespace.update(global_namespace)
if _PYTEST_IS_GE_800:
self._request._fillfixtures()
globs = dict(getfixture=self._request.getfixturevalue)
for name, value in self._request.getfixturevalue("xdoctest_namespace").items():
globs[name] = value
self.dtest.globs.update(globs)
else:
if self.dtest is not None:
self.fixture_request = _setup_fixtures(self)
global_namespace = dict(getfixture=self.fixture_request.getfixturevalue)
for name, value in self.fixture_request.getfixturevalue('xdoctest_namespace').items():
global_namespace[name] = value
self.dtest.global_namespace.update(global_namespace)

def runtest(self):
if self.example.is_disabled(pytest=True):
if self.dtest.is_disabled(pytest=True):
pytest.skip('doctest encountered global skip directive')
# verbose = self.example.config['verbose']
self.example.run(on_error='raise')
if not self.example.anything_ran():
# verbose = self.dtest.config['verbose']
self.dtest.run(on_error='raise')
if not self.dtest.anything_ran():
pytest.skip('doctest is empty or all parts were skipped')

def repr_failure(self, excinfo):
Expand All @@ -222,13 +273,13 @@ def repr_failure(self, excinfo):
# Returns:
# ReprFailXDoctest | str | _pytest._code.code.TerminalRepr:
"""
example = self.example
if example.exc_info is not None:
lineno = example.failed_lineno()
type = example.exc_info[0]
dtest = self.dtest
if dtest.exc_info is not None:
lineno = dtest.failed_lineno()
type = dtest.exc_info[0]
message = type.__name__
reprlocation = code.ReprFileLocation(example.fpath, lineno, message)
lines = example.repr_failure()
reprlocation = code.ReprFileLocation(dtest.fpath, lineno, message)
lines = dtest.repr_failure()

return ReprFailXDoctest(reprlocation, lines)
else:
Expand All @@ -239,7 +290,7 @@ def reportinfo(self):
Returns:
Tuple[str, int, str]
"""
return self.fspath, self.example.lineno, "[xdoctest] %s" % self.name
return self.fspath, self.dtest.lineno, "[xdoctest] %s" % self.name


class _XDoctestBase(pytest.Module):
Expand Down Expand Up @@ -282,15 +333,15 @@ def collect(self):
_example_iter = core.parse_docstr_examples(
text, name, fpath=filename, style=style)

for example in _example_iter:
example.global_namespace.update(global_namespace)
example.config.update(self._examp_conf)
for dtest in _example_iter:
dtest.global_namespace.update(global_namespace)
dtest.config.update(self._examp_conf)
if hasattr(XDoctestItem, 'from_parent'):
yield XDoctestItem.from_parent(
self, name=name, example=example)
self, name=name, dtest=dtest)
else:
# direct construction is deprecated
yield XDoctestItem(name, self, example)
yield XDoctestItem(name, self, dtest=dtest)


class XDoctestModule(_XDoctestBase):
Expand All @@ -311,15 +362,15 @@ def collect(self):
else:
raise

for example in examples:
example.config.update(self._examp_conf)
name = example.unique_callname
for dtest in examples:
dtest.config.update(self._examp_conf)
name = dtest.unique_callname
if hasattr(XDoctestItem, 'from_parent'):
yield XDoctestItem.from_parent(
self, name=name, example=example)
self, name=name, dtest=dtest)
else:
# direct construction is deprecated
yield XDoctestItem(name, self, example)
yield XDoctestItem(name, self, dtest=dtest)


def _setup_fixtures(xdoctest_item):
Expand Down
6 changes: 3 additions & 3 deletions src/xdoctest/static_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,9 +757,9 @@ def _parse_static_node_value(node):
"""
Extract a constant value from a node if possible
"""
# TODO: ast.Constant for 3.8
if isinstance(node, ast.Num):
value = node.n
import numbers
if (isinstance(node, ast.Constant) and isinstance(node.value, numbers.Number) if IS_PY_GE_308 else isinstance(node, ast.Num)):
value = node.value if IS_PY_GE_308 else node.n
elif (isinstance(node, ast.Constant) and isinstance(node.value, str) if IS_PY_GE_308 else isinstance(node, ast.Str)):
value = node.value if IS_PY_GE_308 else node.s
elif isinstance(node, ast.List):
Expand Down
Loading

0 comments on commit 6b4abfb

Please sign in to comment.