Skip to content

Commit ed7f72d

Browse files
datapythonistalarsoner
authored andcommitted
Adding --validate option __main__ and run new validation (#240)
* Adding --validate option __main__ and run new validation * Fixing main tests, and minor improvements to the code * Fixing travis negation of exit status
1 parent 472898e commit ed7f72d

File tree

6 files changed

+144
-89
lines changed

6 files changed

+144
-89
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ script:
2323
cd dist
2424
pip install numpydoc* -v
2525
- pytest -v --pyargs numpydoc
26+
# Making sure the command line options work
27+
- python -m numpydoc numpydoc.tests.test_main._capture_stdout
28+
- echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
29+
- python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
30+
- echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
31+
# Build documentation
2632
- |
2733
cd ../doc
2834
make SPHINXOPTS=$SPHINXOPTS html

doc/validation.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22
Validating NumpyDoc docstrings
33
==============================
44

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

108
.. code-block:: bash
119
1210
$ python -m numpydoc numpy.ndarray
11+
12+
This will validate that the docstring can be built.
13+
14+
For an exhaustive validation of the formatting of the docstring, use the
15+
``--validate`` parameter. This will report the errors detected, such as
16+
incorrect capitalization, wrong order of the sections, and many other
17+
issues.

numpydoc/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
"""
2+
This package provides the numpydoc Sphinx extension for handling docstrings
3+
formatted according to the NumPy documentation format.
4+
"""
15
__version__ = '1.0.0.dev0'
26

37

numpydoc/__main__.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
1+
"""
2+
Implementing `python -m numpydoc` functionality.
3+
"""
4+
import sys
15
import argparse
2-
import importlib
36
import ast
47

58
from .docscrape_sphinx import get_doc_object
9+
from .validate import validate, Docstring
610

711

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

19+
20+
def validate_object(import_path):
21+
exit_status = 0
22+
results = validate(import_path)
23+
for err_code, err_desc in results["errors"]:
24+
exit_status += 1
25+
print(':'.join([import_path, err_code, err_desc]))
26+
return exit_status
27+
28+
29+
if __name__ == '__main__':
1130
ap = argparse.ArgumentParser(description=__doc__)
1231
ap.add_argument('import_path', help='e.g. numpy.ndarray')
1332

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

25-
parts = args.import_path.split('.')
26-
27-
for split_point in range(len(parts), 0, -1):
28-
try:
29-
path = '.'.join(parts[:split_point])
30-
obj = importlib.import_module(path)
31-
except ImportError:
32-
continue
33-
break
46+
if args.validate:
47+
exit_code = validate_object(args.import_path)
3448
else:
35-
raise ImportError('Could not resolve {!r} to an importable object'
36-
''.format(args.import_path))
37-
38-
for part in parts[split_point:]:
39-
obj = getattr(obj, part)
49+
exit_code = render_object(args.import_path, args.config)
4050

41-
print(get_doc_object(obj, config=dict(args.config or [])))
42-
43-
if __name__ == '__main__':
44-
main()
51+
sys.exit(exit_code)

numpydoc/tests/test_main.py

Lines changed: 96 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,113 @@
1-
from __future__ import print_function
2-
3-
from contextlib import contextmanager
4-
import os
51
import sys
6-
import tempfile
7-
try:
8-
from StringIO import StringIO
9-
except ImportError:
10-
from io import StringIO
2+
import io
3+
import pytest
4+
import numpydoc
5+
import numpydoc.__main__
116

12-
from numpydoc.__main__ import main
137

8+
def _capture_stdout(func_name, *args, **kwargs):
9+
"""
10+
Return stdout of calling `func_name`.
1411
15-
PACKAGE_CODE = """
16-
'''This package has test stuff'''
17-
"""
12+
This docstring should be perfect, as it is used to test the
13+
validation with a docstring without errors.
14+
15+
Parameters
16+
----------
17+
func_name : callable
18+
Function to be called.
19+
*args, **kwargs
20+
Will be passed to `func_name`.
21+
22+
Returns
23+
-------
24+
str
25+
The content that the function printed.
26+
27+
See Also
28+
--------
29+
sys.stdout : Python's file handler for stdout.
30+
31+
Examples
32+
--------
33+
>>> _capture_stdout(print, 'hello world')
34+
'hello world'
35+
"""
36+
f = io.StringIO()
37+
sys.stdout, old_stdout = f, sys.stdout
38+
try:
39+
func_name(*args, **kwargs)
40+
return f.getvalue().strip('\n\r')
41+
finally:
42+
sys.stdout = old_stdout
1843

19-
MODULE_CODE = """
20-
'''This module has test stuff'''
2144

22-
def foo(a, b=5):
23-
'''Hello world
45+
def _docstring_with_errors():
46+
"""
47+
this docstring should report some errors
2448
2549
Parameters
2650
----------
27-
something : foo
28-
bar
29-
something_else
30-
bar
31-
'''
32-
"""
51+
made_up_param : str
52+
"""
53+
pass
3354

3455

35-
@contextmanager
36-
def _mock_module(pkg_name):
37-
try:
38-
tempdir = tempfile.mkdtemp()
39-
os.mkdir(os.path.join(tempdir, pkg_name))
40-
with open(os.path.join(tempdir, pkg_name, '__init__.py'), 'w') as f:
41-
print(PACKAGE_CODE, file=f)
42-
with open(os.path.join(tempdir, pkg_name, 'module.py'), 'w') as f:
43-
print(MODULE_CODE, file=f)
44-
45-
sys.path.insert(0, tempdir)
46-
yield tempdir
47-
finally:
48-
try:
49-
os.path.rmdir(tempdir)
50-
sys.path.remove(tempdir)
51-
except:
52-
pass
56+
def _invalid_docstring():
57+
"""
58+
This docstring should break the parsing.
5359
60+
See Also
61+
--------
62+
: this is invalid
63+
"""
64+
pass
5465

55-
def _capture_main(*args):
56-
f = StringIO()
57-
sys.stdout, old_stdout = f, sys.stdout
58-
try:
59-
main(args)
60-
return f.getvalue().strip('\n\r')
61-
finally:
62-
sys.stdout = old_stdout
6366

67+
def test_renders_package_docstring():
68+
out = _capture_stdout(numpydoc.__main__.render_object,
69+
'numpydoc')
70+
assert out.startswith('This package provides the numpydoc Sphinx')
71+
72+
73+
def test_renders_module_docstring():
74+
out = _capture_stdout(numpydoc.__main__.render_object,
75+
'numpydoc.__main__')
76+
assert out.startswith('Implementing `python -m numpydoc` functionality.')
77+
78+
79+
def test_renders_function_docstring():
80+
out = _capture_stdout(numpydoc.__main__.render_object,
81+
'numpydoc.tests.test_main._capture_stdout')
82+
assert out.startswith('Return stdout of calling')
83+
84+
85+
def test_render_object_returns_correct_exit_status():
86+
exit_status = numpydoc.__main__.render_object(
87+
'numpydoc.tests.test_main._capture_stdout')
88+
assert exit_status == 0
89+
90+
with pytest.raises(numpydoc.docscrape.ParseError):
91+
numpydoc.__main__.render_object(
92+
'numpydoc.tests.test_main._invalid_docstring')
93+
94+
95+
def test_validate_detects_errors():
96+
out = _capture_stdout(numpydoc.__main__.validate_object,
97+
'numpydoc.tests.test_main._docstring_with_errors')
98+
assert 'SS02' in out
99+
assert 'Summary does not start with a capital letter' in out
100+
101+
exit_status = numpydoc.__main__.validate_object(
102+
'numpydoc.tests.test_main._docstring_with_errors')
103+
assert exit_status > 0
64104

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

68-
assert (_capture_main('numpydoc.__main__.main') ==
69-
main.__doc__.strip())
106+
def test_validate_perfect_docstring():
107+
out = _capture_stdout(numpydoc.__main__.validate_object,
108+
'numpydoc.tests.test_main._capture_stdout')
109+
assert out == ''
70110

71-
# check it works with modules not imported from __init__
72-
with _mock_module('somepackage1'):
73-
out = _capture_main('somepackage1.module.foo')
74-
assert out.startswith('Hello world\n')
75-
with _mock_module('somepackage2'):
76-
out = _capture_main('somepackage2.module')
77-
assert out.startswith('This module has test')
78-
with _mock_module('somepackage3'):
79-
out = _capture_main('somepackage3')
80-
assert out.startswith('This package has test')
111+
exit_status = numpydoc.__main__.validate_object(
112+
'numpydoc.tests.test_main._capture_stdout')
113+
assert exit_status == 0

numpydoc/validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def _load_obj(name):
152152
>>> Docstring._load_obj('datetime.datetime')
153153
<class 'datetime.datetime'>
154154
"""
155-
for maxsplit in range(1, name.count(".") + 1):
155+
for maxsplit in range(0, name.count(".") + 1):
156156
module, *func_parts = name.rsplit(".", maxsplit)
157157
try:
158158
obj = importlib.import_module(module)

0 commit comments

Comments
 (0)