Skip to content

For issue #23135 Validate the description of the See Also #23188

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

Closed
wants to merge 1 commit into from
Closed
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
252 changes: 172 additions & 80 deletions scripts/tests/test_validate_docstrings.py
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import numpy as np

import validate_docstrings

validate_one = validate_docstrings.validate_one

from pandas.util.testing import capture_stderr
@@ -18,7 +19,7 @@ class GoodDocStrings(object):
script without any errors.
"""

def plot(self, kind, color='blue', **kwargs):
def plot(self, kind, color="blue", **kwargs):
"""
Generate a plot.
@@ -218,6 +219,9 @@ def mode(self, axis, numeric_only):
"""
pass

def SeeAlsoFormatting(self):
pass


class BadGenericDocStrings(object):
"""Everything here has a bad docstring
@@ -335,7 +339,6 @@ def method(self, foo=None, bar=None):


class BadSummaries(object):

def wrong_line(self):
"""Exists on the wrong line"""
pass
@@ -457,7 +460,6 @@ def blank_lines(self, kind):


class BadReturns(object):

def return_not_documented(self):
"""
Lacks section for Returns
@@ -502,8 +504,31 @@ def no_punctuation(self):
return "Hello world!"


class TestValidator(object):
class BadSeeAlso(object):
"""
Everything here has a problem with its See Also section.
"""

def no_period_see_also(self):
"""
Provides type and description but no period.
See Also
-------
str : Lorem ipsum
"""

def no_capital_first_see_also(self):
"""
Provides type and description first letter is not capitalized.
See Also
-------
str : lorem ipsum.
"""


class TestValidator(object):
def _import_path(self, klass=None, func=None):
"""
Build the required import path for tests in this module.
@@ -532,86 +557,144 @@ def _import_path(self, klass=None, func=None):

@capture_stderr
def test_good_class(self):
errors = validate_one(self._import_path(
klass='GoodDocStrings'))['errors']
errors = validate_one(self._import_path(klass="GoodDocStrings"))["errors"]
assert isinstance(errors, list)
assert not errors

@capture_stderr
@pytest.mark.parametrize("func", [
'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1',
'contains', 'mode'])
@pytest.mark.parametrize(
"func",
[
"plot",
"sample",
"random_letters",
"sample_values",
"head",
"head1",
"contains",
"mode",
],
)
def test_good_functions(self, func):
errors = validate_one(self._import_path(
klass='GoodDocStrings', func=func))['errors']
errors = validate_one(self._import_path(klass="GoodDocStrings", func=func))[
"errors"
]
assert isinstance(errors, list)
assert not errors

@capture_stderr
def test_bad_class(self):
errors = validate_one(self._import_path(
klass='BadGenericDocStrings'))['errors']
errors = validate_one(self._import_path(klass="BadGenericDocStrings"))["errors"]
assert isinstance(errors, list)
assert errors

@capture_stderr
@pytest.mark.parametrize("func", [
'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method'])
@pytest.mark.parametrize(
"func", ["func", "astype", "astype1", "astype2", "astype3", "plot", "method"]
)
def test_bad_generic_functions(self, func):
errors = validate_one(self._import_path( # noqa:F821
klass='BadGenericDocStrings', func=func))['errors']
errors = validate_one(
self._import_path(klass="BadGenericDocStrings", func=func) # noqa:F821
)["errors"]
assert isinstance(errors, list)
assert errors

@pytest.mark.parametrize("klass,func,msgs", [
# Summary tests
('BadSummaries', 'wrong_line',
('should start in the line immediately after the opening quotes',)),
('BadSummaries', 'no_punctuation',
('Summary does not end with a period',)),
('BadSummaries', 'no_capitalization',
('Summary does not start with a capital letter',)),
('BadSummaries', 'no_capitalization',
('Summary must start with infinitive verb',)),
('BadSummaries', 'multi_line',
('Summary should fit in a single line.',)),
('BadSummaries', 'two_paragraph_multi_line',
('Summary should fit in a single line.',)),
# Parameters tests
('BadParameters', 'missing_params',
('Parameters {**kwargs} not documented',)),
('BadParameters', 'bad_colon_spacing',
('Parameters {kind} not documented',
'Unknown parameters {kind: str}',
'Parameter "kind: str" has no type')),
('BadParameters', 'no_description_period',
('Parameter "kind" description should finish with "."',)),
('BadParameters', 'no_description_period_with_directive',
('Parameter "kind" description should finish with "."',)),
('BadParameters', 'parameter_capitalization',
('Parameter "kind" description should start with a capital letter',)),
pytest.param('BadParameters', 'blank_lines', ('No error yet?',),
marks=pytest.mark.xfail),
# Returns tests
('BadReturns', 'return_not_documented', ('No Returns section found',)),
('BadReturns', 'yield_not_documented', ('No Yields section found',)),
pytest.param('BadReturns', 'no_type', ('foo',),
marks=pytest.mark.xfail),
pytest.param('BadReturns', 'no_description', ('foo',),
marks=pytest.mark.xfail),
pytest.param('BadReturns', 'no_punctuation', ('foo',),
marks=pytest.mark.xfail)
])
@pytest.mark.parametrize(
"klass,func,msgs",
[
# Summary tests
(
"BadSummaries",
"wrong_line",
("should start in the line immediately after the opening quotes",),
),
("BadSummaries", "no_punctuation", ("Summary does not end with a period",)),
(
"BadSummaries",
"no_capitalization",
("Summary does not start with a capital letter",),
),
(
"BadSummaries",
"no_capitalization",
("Summary must start with infinitive verb",),
),
("BadSummaries", "multi_line", ("Summary should fit in a single line.",)),
(
"BadSummaries",
"two_paragraph_multi_line",
("Summary should fit in a single line.",),
),
# Parameters tests
(
"BadParameters",
"missing_params",
("Parameters {**kwargs} not documented",),
),
(
"BadParameters",
"bad_colon_spacing",
(
"Parameters {kind} not documented",
"Unknown parameters {kind: str}",
'Parameter "kind: str" has no type',
),
),
(
"BadParameters",
"no_description_period",
('Parameter "kind" description should finish with "."',),
),
(
"BadParameters",
"no_description_period_with_directive",
('Parameter "kind" description should finish with "."',),
),
(
"BadParameters",
"parameter_capitalization",
('Parameter "kind" description should start with a capital letter',),
),
pytest.param(
"BadParameters",
"blank_lines",
("No error yet?",),
marks=pytest.mark.xfail,
),
# Returns tests
("BadReturns", "return_not_documented", ("No Returns section found",)),
("BadReturns", "yield_not_documented", ("No Yields section found",)),
pytest.param("BadReturns", "no_type", ("foo",), marks=pytest.mark.xfail),
pytest.param(
"BadReturns", "no_description", ("foo",), marks=pytest.mark.xfail
),
pytest.param(
"BadReturns", "no_punctuation", ("foo",), marks=pytest.mark.xfail
),
# See Also tests
(
"BadSeeAlso",
"no_period_see_also",
("No period at the end of the See Also.",),
),
(
"BadSeeAlso",
"no_capital_first_see_also",
("First letter of the See Also is not capitalized.",),
),
],
)
def test_bad_examples(self, capsys, klass, func, msgs):
result = validate_one(self._import_path(klass=klass, func=func)) # noqa:F821
for msg in msgs:
assert msg in ' '.join(result['errors'])
assert msg in " ".join(result["errors"])


class ApiItems(object):
@property
def api_doc(self):
return io.StringIO('''
return io.StringIO(
"""
.. currentmodule:: itertools
Itertools
@@ -644,41 +727,50 @@ def api_doc(self):
seed
randint
''')

@pytest.mark.parametrize('idx,name', [(0, 'itertools.cycle'),
(1, 'itertools.count'),
(2, 'itertools.chain'),
(3, 'random.seed'),
(4, 'random.randint')])
"""
)

@pytest.mark.parametrize(
"idx,name",
[
(0, "itertools.cycle"),
(1, "itertools.count"),
(2, "itertools.chain"),
(3, "random.seed"),
(4, "random.randint"),
],
)
def test_item_name(self, idx, name):
result = list(validate_docstrings.get_api_items(self.api_doc))
assert result[idx][0] == name

@pytest.mark.parametrize('idx,func', [(0, 'cycle'),
(1, 'count'),
(2, 'chain'),
(3, 'seed'),
(4, 'randint')])
@pytest.mark.parametrize(
"idx,func",
[(0, "cycle"), (1, "count"), (2, "chain"), (3, "seed"), (4, "randint")],
)
def test_item_function(self, idx, func):
result = list(validate_docstrings.get_api_items(self.api_doc))
assert callable(result[idx][1])
assert result[idx][1].__name__ == func

@pytest.mark.parametrize('idx,section', [(0, 'Itertools'),
(1, 'Itertools'),
(2, 'Itertools'),
(3, 'Random'),
(4, 'Random')])
@pytest.mark.parametrize(
"idx,section",
[
(0, "Itertools"),
(1, "Itertools"),
(2, "Itertools"),
(3, "Random"),
(4, "Random"),
],
)
def test_item_section(self, idx, section):
result = list(validate_docstrings.get_api_items(self.api_doc))
assert result[idx][2] == section

@pytest.mark.parametrize('idx,subsection', [(0, 'Infinite'),
(1, 'Infinite'),
(2, 'Finite'),
(3, 'All'),
(4, 'All')])
@pytest.mark.parametrize(
"idx,subsection",
[(0, "Infinite"), (1, "Infinite"), (2, "Finite"), (3, "All"), (4, "All")],
)
def test_item_subsection(self, idx, subsection):
result = list(validate_docstrings.get_api_items(self.api_doc))
assert result[idx][3] == subsection
381 changes: 211 additions & 170 deletions scripts/validate_docstrings.py
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@
import inspect
import importlib
import doctest

try:
from io import StringIO
except ImportError:
@@ -36,13 +37,13 @@
import pandas
from pandas.compat import signature

sys.path.insert(1, os.path.join(BASE_PATH, 'doc', 'sphinxext'))
sys.path.insert(1, os.path.join(BASE_PATH, "doc", "sphinxext"))
from numpydoc.docscrape import NumpyDocString
from pandas.io.formats.printing import pprint_thing


PRIVATE_CLASSES = ['NDFrame', 'IndexOpsMixin']
DIRECTIVES = ['versionadded', 'versionchanged', 'deprecated']
PRIVATE_CLASSES = ["NDFrame", "IndexOpsMixin"]
DIRECTIVES = ["versionadded", "versionchanged", "deprecated"]


def get_api_items(api_doc_fd):
@@ -72,42 +73,46 @@ def get_api_items(api_doc_fd):
The name of the subsection in the API page where the object item is
located.
"""
previous_line = current_section = current_subsection = ''
previous_line = current_section = current_subsection = ""
position = None
for line in api_doc_fd:
line = line.strip()
if len(line) == len(previous_line):
if set(line) == set('-'):
if set(line) == set("-"):
current_section = previous_line
continue
if set(line) == set('~'):
if set(line) == set("~"):
current_subsection = previous_line
continue

if line.startswith('.. currentmodule::'):
current_module = line.replace('.. currentmodule::', '').strip()
if line.startswith(".. currentmodule::"):
current_module = line.replace(".. currentmodule::", "").strip()
continue

if line == '.. autosummary::':
position = 'autosummary'
if line == ".. autosummary::":
position = "autosummary"
continue

if position == 'autosummary':
if line == '':
position = 'items'
if position == "autosummary":
if line == "":
position = "items"
continue

if position == 'items':
if line == '':
if position == "items":
if line == "":
position = None
continue
item = line.strip()
func = importlib.import_module(current_module)
for part in item.split('.'):
for part in item.split("."):
func = getattr(func, part)

yield ('.'.join([current_module, item]), func,
current_section, current_subsection)
yield (
".".join([current_module, item]),
func,
current_section,
current_subsection,
)

previous_line = line

@@ -118,7 +123,7 @@ def __init__(self, name):
obj = self._load_obj(name)
self.obj = obj
self.code_obj = self._to_original_callable(obj)
self.raw_doc = obj.__doc__ or ''
self.raw_doc = obj.__doc__ or ""
self.clean_doc = pydoc.getdoc(obj)
self.doc = NumpyDocString(self.clean_doc)

@@ -145,9 +150,9 @@ def _load_obj(name):
>>> Docstring._load_obj('pandas.Series')
<class 'pandas.core.series.Series'>
"""
for maxsplit in range(1, name.count('.') + 1):
for maxsplit in range(1, name.count(".") + 1):
# TODO when py3 only replace by: module, *func_parts = ...
func_name_split = name.rsplit('.', maxsplit)
func_name_split = name.rsplit(".", maxsplit)
module = func_name_split[0]
func_parts = func_name_split[1:]
try:
@@ -157,9 +162,8 @@ def _load_obj(name):
else:
continue

if 'module' not in locals():
raise ImportError('No module can be imported '
'from "{}"'.format(name))
if "module" not in locals():
raise ImportError("No module can be imported " 'from "{}"'.format(name))

for part in func_parts:
obj = getattr(obj, part)
@@ -177,7 +181,7 @@ def _to_original_callable(obj):
while True:
if inspect.isfunction(obj) or inspect.isclass(obj):
f = inspect.getfile(obj)
if f.startswith('<') and f.endswith('>'):
if f.startswith("<") and f.endswith(">"):
return None
return obj
if inspect.ismethod(obj):
@@ -196,8 +200,7 @@ def type(self):
@property
def is_function_or_method(self):
# TODO(py27): remove ismethod
return (inspect.isfunction(self.obj)
or inspect.ismethod(self.obj))
return inspect.isfunction(self.obj) or inspect.ismethod(self.obj)

@property
def source_file_name(self):
@@ -231,16 +234,15 @@ def source_file_def_line(self):

@property
def github_url(self):
url = 'https://github.com/pandas-dev/pandas/blob/master/'
url += '{}#L{}'.format(self.source_file_name,
self.source_file_def_line)
url = "https://github.com/pandas-dev/pandas/blob/master/"
url += "{}#L{}".format(self.source_file_name, self.source_file_def_line)
return url

@property
def start_blank_lines(self):
i = None
if self.raw_doc:
for i, row in enumerate(self.raw_doc.split('\n')):
for i, row in enumerate(self.raw_doc.split("\n")):
if row.strip():
break
return i
@@ -249,50 +251,51 @@ def start_blank_lines(self):
def end_blank_lines(self):
i = None
if self.raw_doc:
for i, row in enumerate(reversed(self.raw_doc.split('\n'))):
for i, row in enumerate(reversed(self.raw_doc.split("\n"))):
if row.strip():
break
return i

@property
def double_blank_lines(self):
prev = True
for row in self.raw_doc.split('\n'):
for row in self.raw_doc.split("\n"):
if not prev and not row.strip():
return True
prev = row.strip()
return False

@property
def summary(self):
return ' '.join(self.doc['Summary'])
return " ".join(self.doc["Summary"])

@property
def num_summary_lines(self):
return len(self.doc['Summary'])
return len(self.doc["Summary"])

@property
def extended_summary(self):
if not self.doc['Extended Summary'] and len(self.doc['Summary']) > 1:
return ' '.join(self.doc['Summary'])
return ' '.join(self.doc['Extended Summary'])
if not self.doc["Extended Summary"] and len(self.doc["Summary"]) > 1:
return " ".join(self.doc["Summary"])
return " ".join(self.doc["Extended Summary"])

@property
def needs_summary(self):
return not (bool(self.summary) and bool(self.extended_summary))

@property
def doc_parameters(self):
return collections.OrderedDict((name, (type_, ''.join(desc)))
for name, type_, desc
in self.doc['Parameters'])
return collections.OrderedDict(
(name, (type_, "".join(desc)))
for name, type_, desc in self.doc["Parameters"]
)

@property
def signature_parameters(self):
if inspect.isclass(self.obj):
if hasattr(self.obj, '_accessors') and (
self.name.split('.')[-1] in
self.obj._accessors):
if hasattr(self.obj, "_accessors") and (
self.name.split(".")[-1] in self.obj._accessors
):
# accessor classes have a signature but don't want to show this
return tuple()
try:
@@ -307,7 +310,7 @@ def signature_parameters(self):
if sig.keywords:
params.append("**" + sig.keywords)
params = tuple(params)
if params and params[0] in ('self', 'cls'):
if params and params[0] in ("self", "cls"):
return params[1:]
return params

@@ -318,16 +321,21 @@ def parameter_mismatches(self):
doc_params = tuple(self.doc_parameters)
missing = set(signature_params) - set(doc_params)
if missing:
errs.append(
'Parameters {} not documented'.format(pprint_thing(missing)))
errs.append("Parameters {} not documented".format(pprint_thing(missing)))
extra = set(doc_params) - set(signature_params)
if extra:
errs.append('Unknown parameters {}'.format(pprint_thing(extra)))
if (not missing and not extra and signature_params != doc_params
and not (not signature_params and not doc_params)):
errs.append('Wrong parameters order. ' +
'Actual: {!r}. '.format(signature_params) +
'Documented: {!r}'.format(doc_params))
errs.append("Unknown parameters {}".format(pprint_thing(extra)))
if (
not missing
and not extra
and signature_params != doc_params
and not (not signature_params and not doc_params)
):
errs.append(
"Wrong parameters order. "
+ "Actual: {!r}. ".format(signature_params)
+ "Documented: {!r}".format(doc_params)
)

return errs

@@ -342,48 +350,50 @@ def parameter_desc(self, param):
desc = self.doc_parameters[param][1]
# Find and strip out any sphinx directives
for directive in DIRECTIVES:
full_directive = '.. {}'.format(directive)
full_directive = ".. {}".format(directive)
if full_directive in desc:
# Only retain any description before the directive
desc = desc[:desc.index(full_directive)]
desc = desc[: desc.index(full_directive)]
return desc

@property
def see_also(self):
return collections.OrderedDict((name, ''.join(desc))
for name, desc, _
in self.doc['See Also'])
return collections.OrderedDict(
(name, "".join(desc)) for name, desc, _ in self.doc["See Also"]
)

@property
def examples(self):
return self.doc['Examples']
return self.doc["Examples"]

@property
def returns(self):
return self.doc['Returns']
return self.doc["Returns"]

@property
def yields(self):
return self.doc['Yields']
return self.doc["Yields"]

@property
def method_source(self):
try:
return inspect.getsource(self.obj)
except TypeError:
return ''
return ""

@property
def first_line_ends_in_dot(self):
if self.doc:
return self.doc.split('\n')[0][-1] == '.'
return self.doc.split("\n")[0][-1] == "."

@property
def deprecated(self):
pattern = re.compile('.. deprecated:: ')
return (self.name.startswith('pandas.Panel')
or bool(pattern.search(self.summary))
or bool(pattern.search(self.extended_summary)))
pattern = re.compile(".. deprecated:: ")
return (
self.name.startswith("pandas.Panel")
or bool(pattern.search(self.summary))
or bool(pattern.search(self.extended_summary))
)

@property
def mentioned_private_classes(self):
@@ -394,8 +404,8 @@ def examples_errors(self):
flags = doctest.NORMALIZE_WHITESPACE | doctest.IGNORE_EXCEPTION_DETAIL
finder = doctest.DocTestFinder()
runner = doctest.DocTestRunner(optionflags=flags)
context = {'np': numpy, 'pd': pandas}
error_msgs = ''
context = {"np": numpy, "pd": pandas}
error_msgs = ""
for test in finder.find(self.raw_doc, self.name, globs=context):
f = StringIO()
runner.run(test, out=f.write)
@@ -423,105 +433,134 @@ def validate_one(func_name):
errs = []
wrns = []
if doc.start_blank_lines != 1:
errs.append('Docstring text (summary) should start in the line '
'immediately after the opening quotes (not in the same '
'line, or leaving a blank line in between)')
errs.append(
"Docstring text (summary) should start in the line "
"immediately after the opening quotes (not in the same "
"line, or leaving a blank line in between)"
)
if doc.end_blank_lines != 1:
errs.append('Closing quotes should be placed in the line after '
'the last text in the docstring (do not close the '
'quotes in the same line as the text, or leave a '
'blank line between the last text and the quotes)')
errs.append(
"Closing quotes should be placed in the line after "
"the last text in the docstring (do not close the "
"quotes in the same line as the text, or leave a "
"blank line between the last text and the quotes)"
)
if doc.double_blank_lines:
errs.append('Use only one blank line to separate sections or '
'paragraphs')
errs.append("Use only one blank line to separate sections or " "paragraphs")

if not doc.summary:
errs.append('No summary found (a short summary in a single line '
'should be present at the beginning of the docstring)')
errs.append(
"No summary found (a short summary in a single line "
"should be present at the beginning of the docstring)"
)
else:
if not doc.summary[0].isupper():
errs.append('Summary does not start with a capital letter')
if doc.summary[-1] != '.':
errs.append('Summary does not end with a period')
errs.append("Summary does not start with a capital letter")
if doc.summary[-1] != ".":
errs.append("Summary does not end with a period")
if doc.summary != doc.summary.lstrip():
errs.append('Summary contains heading whitespaces.')
elif (doc.is_function_or_method
and doc.summary.split(' ')[0][-1] == 's'):
errs.append('Summary must start with infinitive verb, '
'not third person (e.g. use "Generate" instead of '
'"Generates")')
errs.append("Summary contains heading whitespaces.")
elif doc.is_function_or_method and doc.summary.split(" ")[0][-1] == "s":
errs.append(
"Summary must start with infinitive verb, "
'not third person (e.g. use "Generate" instead of '
'"Generates")'
)
if doc.num_summary_lines > 1:
errs.append("Summary should fit in a single line.")
if not doc.extended_summary:
wrns.append('No extended summary found')
wrns.append("No extended summary found")

param_errs = doc.parameter_mismatches
for param in doc.doc_parameters:
if not param.startswith("*"): # Check can ignore var / kwargs
if not doc.parameter_type(param):
param_errs.append('Parameter "{}" has no type'.format(param))
else:
if doc.parameter_type(param)[-1] == '.':
param_errs.append('Parameter "{}" type should '
'not finish with "."'.format(param))
if doc.parameter_type(param)[-1] == ".":
param_errs.append(
'Parameter "{}" type should '
'not finish with "."'.format(param)
)

if not doc.parameter_desc(param):
param_errs.append('Parameter "{}" '
'has no description'.format(param))
param_errs.append('Parameter "{}" ' "has no description".format(param))
else:
if not doc.parameter_desc(param)[0].isupper():
param_errs.append('Parameter "{}" description '
'should start with a '
'capital letter'.format(param))
if doc.parameter_desc(param)[-1] != '.':
param_errs.append('Parameter "{}" description '
'should finish with "."'.format(param))
param_errs.append(
'Parameter "{}" description '
"should start with a "
"capital letter".format(param)
)
if doc.parameter_desc(param)[-1] != ".":
param_errs.append(
'Parameter "{}" description ' 'should finish with "."'.format(param)
)
if param_errs:
errs.append('Errors in parameters section')
errs.append("Errors in parameters section")
for param_err in param_errs:
errs.append('\t{}'.format(param_err))
errs.append("\t{}".format(param_err))

if doc.is_function_or_method:
if not doc.returns and "return" in doc.method_source:
errs.append('No Returns section found')
errs.append("No Returns section found")
if not doc.yields and "yield" in doc.method_source:
errs.append('No Yields section found')
errs.append("No Yields section found")

mentioned_errs = doc.mentioned_private_classes
if mentioned_errs:
errs.append('Private classes ({}) should not be mentioned in public '
'docstring.'.format(mentioned_errs))
errs.append(
"Private classes ({}) should not be mentioned in public "
"docstring.".format(mentioned_errs)
)

if not doc.see_also:
wrns.append('See Also section not found')
wrns.append("See Also section not found")
else:
for rel_name, rel_desc in doc.see_also.items():
if not rel_desc:
errs.append('Missing description for '
'See Also "{}" reference'.format(rel_name))
errs.append(
"Missing description for "
'See Also "{}" reference'.format(rel_name)
)
else:
if rel_desc[0].upper() != rel_desc[0]:
errs.append(
"Description should start capital letter "
'See also "{}" reference'.format(rel_name)
)
if rel_desc[-1] != ".":
errs.append(
"Description should finish with a period "
'See Also "{}" reference'.format(rel_name)
)

for line in doc.raw_doc.splitlines():
if re.match("^ *\t", line):
errs.append('Tabs found at the start of line "{}", '
'please use whitespace only'.format(line.lstrip()))
errs.append(
'Tabs found at the start of line "{}", '
"please use whitespace only".format(line.lstrip())
)

examples_errs = ''
examples_errs = ""
if not doc.examples:
wrns.append('No examples section found')
wrns.append("No examples section found")
else:
examples_errs = doc.examples_errors
if examples_errs:
errs.append('Examples do not pass tests')
errs.append("Examples do not pass tests")

return {'type': doc.type,
'docstring': doc.clean_doc,
'deprecated': doc.deprecated,
'file': doc.source_file_name,
'file_line': doc.source_file_def_line,
'github_link': doc.github_url,
'errors': errs,
'warnings': wrns,
'examples_errors': examples_errs}
return {
"type": doc.type,
"docstring": doc.clean_doc,
"deprecated": doc.deprecated,
"file": doc.source_file_name,
"file_line": doc.source_file_def_line,
"github_link": doc.github_url,
"errors": errs,
"warnings": wrns,
"examples_errors": examples_errs,
}


def validate_all():
@@ -539,83 +578,85 @@ def validate_all():
seen = {}

# functions from the API docs
api_doc_fname = os.path.join(BASE_PATH, 'doc', 'source', 'api.rst')
api_doc_fname = os.path.join(BASE_PATH, "doc", "source", "api.rst")
with open(api_doc_fname) as f:
api_items = list(get_api_items(f))
for func_name, func_obj, section, subsection in api_items:
doc_info = validate_one(func_name)
result[func_name] = doc_info

shared_code_key = doc_info['file'], doc_info['file_line']
shared_code = seen.get(shared_code_key, '')
result[func_name].update({'in_api': True,
'section': section,
'subsection': subsection,
'shared_code_with': shared_code})
shared_code_key = doc_info["file"], doc_info["file_line"]
shared_code = seen.get(shared_code_key, "")
result[func_name].update(
{
"in_api": True,
"section": section,
"subsection": subsection,
"shared_code_with": shared_code,
}
)

seen[shared_code_key] = func_name

# functions from introspecting Series, DataFrame and Panel
api_item_names = set(list(zip(*api_items))[0])
for class_ in (pandas.Series, pandas.DataFrame, pandas.Panel):
for member in inspect.getmembers(class_):
func_name = 'pandas.{}.{}'.format(class_.__name__, member[0])
if (not member[0].startswith('_')
and func_name not in api_item_names):
func_name = "pandas.{}.{}".format(class_.__name__, member[0])
if not member[0].startswith("_") and func_name not in api_item_names:
doc_info = validate_one(func_name)
result[func_name] = doc_info
result[func_name]['in_api'] = False
result[func_name]["in_api"] = False

return result


def main(func_name, fd):
def header(title, width=80, char='#'):
def header(title, width=80, char="#"):
full_line = char * width
side_len = (width - len(title) - 2) // 2
adj = '' if len(title) % 2 == 0 else ' '
title_line = '{side} {title}{adj} {side}'.format(side=char * side_len,
title=title,
adj=adj)
adj = "" if len(title) % 2 == 0 else " "
title_line = "{side} {title}{adj} {side}".format(
side=char * side_len, title=title, adj=adj
)

return '\n{full_line}\n{title_line}\n{full_line}\n\n'.format(
full_line=full_line, title_line=title_line)
return "\n{full_line}\n{title_line}\n{full_line}\n\n".format(
full_line=full_line, title_line=title_line
)

if func_name is None:
json_doc = validate_all()
fd.write(json.dumps(json_doc))
else:
doc_info = validate_one(func_name)

fd.write(header('Docstring ({})'.format(func_name)))
fd.write('{}\n'.format(doc_info['docstring']))
fd.write(header('Validation'))
if doc_info['errors']:
fd.write('Errors found:\n')
for err in doc_info['errors']:
fd.write('\t{}\n'.format(err))
if doc_info['warnings']:
fd.write('Warnings found:\n')
for wrn in doc_info['warnings']:
fd.write('\t{}\n'.format(wrn))

if not doc_info['errors']:
fd.write(header("Docstring ({})".format(func_name)))
fd.write("{}\n".format(doc_info["docstring"]))
fd.write(header("Validation"))
if doc_info["errors"]:
fd.write("Errors found:\n")
for err in doc_info["errors"]:
fd.write("\t{}\n".format(err))
if doc_info["warnings"]:
fd.write("Warnings found:\n")
for wrn in doc_info["warnings"]:
fd.write("\t{}\n".format(wrn))

if not doc_info["errors"]:
fd.write('Docstring for "{}" correct. :)\n'.format(func_name))

if doc_info['examples_errors']:
fd.write(header('Doctests'))
fd.write(doc_info['examples_errors'])


if __name__ == '__main__':
func_help = ('function or method to validate (e.g. pandas.DataFrame.head) '
'if not provided, all docstrings are validated and returned '
'as JSON')
argparser = argparse.ArgumentParser(
description='validate pandas docstrings')
argparser.add_argument('function',
nargs='?',
default=None,
help=func_help)
if doc_info["examples_errors"]:
fd.write(header("Doctests"))
fd.write(doc_info["examples_errors"])


if __name__ == "__main__":
func_help = (
"function or method to validate (e.g. pandas.DataFrame.head) "
"if not provided, all docstrings are validated and returned "
"as JSON"
)
argparser = argparse.ArgumentParser(description="validate pandas docstrings")
argparser.add_argument("function", nargs="?", default=None, help=func_help)
args = argparser.parse_args()
sys.exit(main(args.function, sys.stdout))