From e41e6d06b1688f830507c1fc3e12b717c7e819bc Mon Sep 17 00:00:00 2001 From: Tres DuBiel Date: Tue, 16 Oct 2018 22:02:07 -0500 Subject: [PATCH 1/4] Proof of Concept using pycodestyle --- scripts/validate_docstrings.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 6588522331433..3ecdf1791f1be 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -23,6 +23,7 @@ import pydoc import inspect import importlib +import contextlib import doctest try: from io import StringIO @@ -40,6 +41,8 @@ from numpydoc.docscrape import NumpyDocString from pandas.io.formats.printing import pprint_thing +from pycodestyle import Checker + PRIVATE_CLASSES = ['NDFrame', 'IndexOpsMixin'] DIRECTIVES = ['versionadded', 'versionchanged', 'deprecated'] @@ -111,6 +114,10 @@ def get_api_items(api_doc_fd): previous_line = line +class DocstringExampleChecker(Checker): + def set_lines(self, lines): + self.lines = lines + class Docstring(object): def __init__(self, name): @@ -512,7 +519,28 @@ def validate_one(func_name): examples_errs = doc.examples_errors if examples_errs: errs.append('Examples do not pass tests') + example_checker = DocstringExampleChecker() + example_list = doctest.DocTestParser().get_examples(doc.raw_doc) + #for ex in example_list: + #lint + example_checker.set_lines([ex.source for ex in example_list]) + + #check_all will print the error to stdout, so we suppress this + with open(os.devnull, "w") as f, contextlib.redirect_stdout(f): + num_errs = example_checker.check_all() + if num_errs > 0: + for line_number, offset, code, text, doc in example_checker.report._deferred_print: + if code == 'E402': + continue + elif len(example_list)-1 < line_number: + continue + # multi-line assignments report E501: line too long + elif code == 'E501' and '\n' in example_list[line_number].source: + continue + flo = example_list[line_number-1].lineno + errs.append('{}: {} {}:{}:{}'.format(code, text, func_name, flo, offset)) + doc = Docstring(func_name) return {'type': doc.type, 'docstring': doc.clean_doc, 'deprecated': doc.deprecated, From f62128f0578b6fa65022d9380a0024732190097f Mon Sep 17 00:00:00 2001 From: Tres DuBiel Date: Tue, 16 Oct 2018 22:29:58 -0500 Subject: [PATCH 2/4] Pass Flake8 for PR --- scripts/validate_docstrings.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 3ecdf1791f1be..e80b11a80f572 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -114,6 +114,7 @@ def get_api_items(api_doc_fd): previous_line = line + class DocstringExampleChecker(Checker): def set_lines(self, lines): self.lines = lines @@ -521,24 +522,23 @@ def validate_one(func_name): errs.append('Examples do not pass tests') example_checker = DocstringExampleChecker() example_list = doctest.DocTestParser().get_examples(doc.raw_doc) - #for ex in example_list: - #lint example_checker.set_lines([ex.source for ex in example_list]) - #check_all will print the error to stdout, so we suppress this + # check_all will print the error to stdout, so we suppress this with open(os.devnull, "w") as f, contextlib.redirect_stdout(f): num_errs = example_checker.check_all() if num_errs > 0: - for line_number, offset, code, text, doc in example_checker.report._deferred_print: + # returns a tuple of error information + err_report = example_checker.report._deferred_print + for line_number, offset, code, text, doc in err_report: + # module level import not at top of file if code == 'E402': continue - elif len(example_list)-1 < line_number: - continue - # multi-line assignments report E501: line too long - elif code == 'E501' and '\n' in example_list[line_number].source: + # multi-line assignments report E501 incorrectly + elif code == 'E501': continue - flo = example_list[line_number-1].lineno - errs.append('{}: {} {}:{}:{}'.format(code, text, func_name, flo, offset)) + errs.append('{}: {} {}:{}:{}'.format( + code, text, func_name, line_number, offset)) doc = Docstring(func_name) return {'type': doc.type, From 0a7a54555d7fcef539194631c62040a4b8dbccb3 Mon Sep 17 00:00:00 2001 From: Tres DuBiel Date: Tue, 16 Oct 2018 22:37:34 -0500 Subject: [PATCH 3/4] Update whatsnew with entry --- doc/source/whatsnew/v0.24.0.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3053625721560..58f33f8a6f445 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -201,6 +201,7 @@ Other Enhancements - :meth:`Index.to_frame` now supports overriding column name(s) (:issue:`22580`). - New attribute :attr:`__git_version__` will return git commit sha of current build (:issue:`21295`). - Compatibility with Matplotlib 3.0 (:issue:`22790`). +- Validate docstring examples are PEP-8 compliant (:issue:`23154`) .. _whatsnew_0240.api_breaking: From 6a4cbd28fc04dce6bb3b86fd74b0c1f5b3751e87 Mon Sep 17 00:00:00 2001 From: Tres DuBiel Date: Tue, 16 Oct 2018 22:49:58 -0500 Subject: [PATCH 4/4] Remove debug line --- scripts/validate_docstrings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index e80b11a80f572..2b746b1d0a8fe 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -540,7 +540,6 @@ def validate_one(func_name): errs.append('{}: {} {}:{}:{}'.format( code, text, func_name, line_number, offset)) - doc = Docstring(func_name) return {'type': doc.type, 'docstring': doc.clean_doc, 'deprecated': doc.deprecated,