diff --git a/scripts/tests/test_validate_docstrings.py b/scripts/tests/test_validate_docstrings.py index 27c63e3ba3a79..fc3f7381a6f1a 100644 --- a/scripts/tests/test_validate_docstrings.py +++ b/scripts/tests/test_validate_docstrings.py @@ -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 diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 6588522331433..8aee40ca321cf 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -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') """ - 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,7 +251,7 @@ 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 @@ -257,7 +259,7 @@ def end_blank_lines(self): @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() @@ -265,17 +267,17 @@ def double_blank_lines(self): @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): @@ -283,16 +285,17 @@ def needs_summary(self): @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,37 +433,43 @@ 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: @@ -461,67 +477,90 @@ def validate_one(func_name): 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,19 +578,23 @@ 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 @@ -559,27 +602,27 @@ def validate_all(): 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() @@ -587,35 +630,33 @@ def header(title, width=80, char='#'): 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))