Skip to content

Adding --validate option __main__ and run new validation #240

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

Merged
merged 4 commits into from
Nov 13, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ script:
cd dist
pip install numpydoc* -v
- pytest -v --pyargs numpydoc
# Making sure the command line options work
- python -m numpydoc numpydoc.tests.test_main._capture_stdout
- echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
- python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
- echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
# Build documentation
- |
cd ../doc
make SPHINXOPTS=$SPHINXOPTS html
Expand Down
13 changes: 9 additions & 4 deletions doc/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
Validating NumpyDoc docstrings
==============================

One tool for validating docstrings is to see how an object's dosctring
translates to Restructured Text. Using numpydoc as a command-line tool
facilitates this. For example to see the Restructured Text generated
for ``numpy.ndarray``, use:
To see the Restructured Text generated for an object, the ``numpydoc`` module
can be called. For example, to do it for ``numpy.ndarray``, use:

.. code-block:: bash

$ python -m numpydoc numpy.ndarray

This will validate that the docstring can be built.

For an exhaustive validation of the formatting of the docstring, use the
``--validate`` parameter. This will report the errors detected, such as
incorrect capitalization, wrong order of the sections, and many other
issues.
4 changes: 4 additions & 0 deletions numpydoc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
This package provides the numpydoc Sphinx extension for handling docstrings
formatted according to the NumPy documentation format.
"""
__version__ = '1.0.0.dev0'


Expand Down
49 changes: 28 additions & 21 deletions numpydoc/__main__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
"""
Implementing `python -m numpydoc` functionality.
"""
import sys
import argparse
import importlib
import ast

from .docscrape_sphinx import get_doc_object
from .validate import validate, Docstring


def main(argv=None):
def render_object(import_path, config=None):
"""Test numpydoc docstring generation for a given object"""
# TODO: Move Docstring._load_obj to a better place than validate
print(get_doc_object(Docstring(import_path).obj,
config=dict(config or [])))
return 0


def validate_object(import_path):
exit_status = 0
results = validate(import_path)
for err_code, err_desc in results["errors"]:
exit_status += 1
print(':'.join([import_path, err_code, err_desc]))
return exit_status


if __name__ == '__main__':
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('import_path', help='e.g. numpy.ndarray')

Expand All @@ -20,25 +39,13 @@ def _parse_config(s):
action='append',
help='key=val where val will be parsed by literal_eval, '
'e.g. -c use_plots=True. Multiple -c can be used.')
args = ap.parse_args(argv)
ap.add_argument('--validate', action='store_true',
help='validate the object and report errors')
args = ap.parse_args()

parts = args.import_path.split('.')

for split_point in range(len(parts), 0, -1):
try:
path = '.'.join(parts[:split_point])
obj = importlib.import_module(path)
except ImportError:
continue
break
if args.validate:
exit_code = validate_object(args.import_path)
else:
raise ImportError('Could not resolve {!r} to an importable object'
''.format(args.import_path))

for part in parts[split_point:]:
obj = getattr(obj, part)
exit_code = render_object(args.import_path, args.config)

print(get_doc_object(obj, config=dict(args.config or [])))

if __name__ == '__main__':
main()
sys.exit(exit_code)
159 changes: 96 additions & 63 deletions numpydoc/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,80 +1,113 @@
from __future__ import print_function

from contextlib import contextmanager
import os
import sys
import tempfile
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
import io
import pytest
import numpydoc
import numpydoc.__main__

from numpydoc.__main__ import main

def _capture_stdout(func_name, *args, **kwargs):
"""
Return stdout of calling `func_name`.

PACKAGE_CODE = """
'''This package has test stuff'''
"""
This docstring should be perfect, as it is used to test the
validation with a docstring without errors.

Parameters
----------
func_name : callable
Function to be called.
*args, **kwargs
Will be passed to `func_name`.

Returns
-------
str
The content that the function printed.

See Also
--------
sys.stdout : Python's file handler for stdout.

Examples
--------
>>> _capture_stdout(print, 'hello world')
'hello world'
"""
f = io.StringIO()
sys.stdout, old_stdout = f, sys.stdout
try:
func_name(*args, **kwargs)
return f.getvalue().strip('\n\r')
finally:
sys.stdout = old_stdout

MODULE_CODE = """
'''This module has test stuff'''

def foo(a, b=5):
'''Hello world
def _docstring_with_errors():
"""
this docstring should report some errors

Parameters
----------
something : foo
bar
something_else
bar
'''
"""
made_up_param : str
"""
pass


@contextmanager
def _mock_module(pkg_name):
try:
tempdir = tempfile.mkdtemp()
os.mkdir(os.path.join(tempdir, pkg_name))
with open(os.path.join(tempdir, pkg_name, '__init__.py'), 'w') as f:
print(PACKAGE_CODE, file=f)
with open(os.path.join(tempdir, pkg_name, 'module.py'), 'w') as f:
print(MODULE_CODE, file=f)

sys.path.insert(0, tempdir)
yield tempdir
finally:
try:
os.path.rmdir(tempdir)
sys.path.remove(tempdir)
except:
pass
def _invalid_docstring():
"""
This docstring should break the parsing.

See Also
--------
: this is invalid
"""
pass

def _capture_main(*args):
f = StringIO()
sys.stdout, old_stdout = f, sys.stdout
try:
main(args)
return f.getvalue().strip('\n\r')
finally:
sys.stdout = old_stdout

def test_renders_package_docstring():
out = _capture_stdout(numpydoc.__main__.render_object,
'numpydoc')
assert out.startswith('This package provides the numpydoc Sphinx')


def test_renders_module_docstring():
out = _capture_stdout(numpydoc.__main__.render_object,
'numpydoc.__main__')
assert out.startswith('Implementing `python -m numpydoc` functionality.')


def test_renders_function_docstring():
out = _capture_stdout(numpydoc.__main__.render_object,
'numpydoc.tests.test_main._capture_stdout')
assert out.startswith('Return stdout of calling')


def test_render_object_returns_correct_exit_status():
exit_status = numpydoc.__main__.render_object(
'numpydoc.tests.test_main._capture_stdout')
assert exit_status == 0

with pytest.raises(numpydoc.docscrape.ParseError):
numpydoc.__main__.render_object(
'numpydoc.tests.test_main._invalid_docstring')


def test_validate_detects_errors():
out = _capture_stdout(numpydoc.__main__.validate_object,
'numpydoc.tests.test_main._docstring_with_errors')
assert 'SS02' in out
assert 'Summary does not start with a capital letter' in out

exit_status = numpydoc.__main__.validate_object(
'numpydoc.tests.test_main._docstring_with_errors')
assert exit_status > 0

def test_main():
# TODO: does not currently check that numpydoc transformations are applied

assert (_capture_main('numpydoc.__main__.main') ==
main.__doc__.strip())
def test_validate_perfect_docstring():
out = _capture_stdout(numpydoc.__main__.validate_object,
'numpydoc.tests.test_main._capture_stdout')
assert out == ''

# check it works with modules not imported from __init__
with _mock_module('somepackage1'):
out = _capture_main('somepackage1.module.foo')
assert out.startswith('Hello world\n')
with _mock_module('somepackage2'):
out = _capture_main('somepackage2.module')
assert out.startswith('This module has test')
with _mock_module('somepackage3'):
out = _capture_main('somepackage3')
assert out.startswith('This package has test')
exit_status = numpydoc.__main__.validate_object(
'numpydoc.tests.test_main._capture_stdout')
assert exit_status == 0
2 changes: 1 addition & 1 deletion numpydoc/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def _load_obj(name):
>>> Docstring._load_obj('datetime.datetime')
<class 'datetime.datetime'>
"""
for maxsplit in range(1, name.count(".") + 1):
for maxsplit in range(0, name.count(".") + 1):
module, *func_parts = name.rsplit(".", maxsplit)
try:
obj = importlib.import_module(module)
Expand Down