From 4673475ec7098a7b4203ca716cdda8c029618fc6 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 8 Mar 2018 13:05:54 -0800 Subject: [PATCH 01/24] Added Py27 support for validation script --- scripts/validate_docstrings.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 8425882f07be1..e64d8f692ac33 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -46,7 +46,7 @@ def _load_obj(obj_name): for maxsplit in range(1, obj_name.count('.') + 1): # TODO when py3 only replace by: module, *func_parts = ... - func_name_split = obj_name.rsplit('.', maxsplit=maxsplit) + func_name_split = obj_name.rsplit('.', maxsplit) module = func_name_split[0] func_parts = func_name_split[1:] try: @@ -186,12 +186,11 @@ def signature_parameters(self): # accessor classes have a signature, but don't want to show this return tuple() try: - signature = inspect.signature(self.method_obj) + params = self.method_obj.__code__.co_varnames except (TypeError, ValueError): # Some objects, mainly in C extensions do not support introspection # of the signature return tuple() - params = tuple(signature.parameters.keys()) if params and params[0] in ('self', 'cls'): return params[1:] return params @@ -264,8 +263,7 @@ def examples_errors(self): error_msgs = '' for test in finder.find(self.raw_doc, self.method_name, globs=context): f = StringIO() - with contextlib.redirect_stdout(f): - runner.run(test) + runner.run(test, out=f) error_msgs += f.getvalue() return error_msgs From 9abc0042387e609ad2f30633a2139f0a8851b37b Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 8 Mar 2018 14:10:11 -0800 Subject: [PATCH 02/24] Removed contextlib import --- scripts/validate_docstrings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index e64d8f692ac33..894b97163ab5e 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -20,7 +20,6 @@ import functools import collections import argparse -import contextlib import pydoc import inspect import importlib From 752c6dbe27917d4ff9f44ef7ec350a5d39bdddf3 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 8 Mar 2018 19:32:36 -0800 Subject: [PATCH 03/24] Added test for script validator --- pandas/tests/scripts/__init__.py | 0 .../tests/scripts/test_validate_docstrings.py | 327 ++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 pandas/tests/scripts/__init__.py create mode 100644 pandas/tests/scripts/test_validate_docstrings.py diff --git a/pandas/tests/scripts/__init__.py b/pandas/tests/scripts/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py new file mode 100644 index 0000000000000..c58ef3566896c --- /dev/null +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -0,0 +1,327 @@ +import os +import sys + +import numpy +import pytest + + +class GoodDocStrings(object): + """ + Collection of good docstrings - be sure to update the tests as new + examples are added here + """ + + def plot(self, kind, color='blue', **kwargs): + """ + Generate a plot. + + Render the data in the Series as a matplotlib plot of the + specified kind. + + Parameters + ---------- + kind : str + Kind of matplotlib plot. + color : str, default 'blue' + Color name or rgb code. + **kwargs + These parameters will be passed to the matplotlib plotting + function. + """ + pass + + def sample(self): + """ + Generate and return a random number. + + The value is sampled from a continuous uniform distribution between + 0 and 1. + + Returns + ------- + float + Random number generated. + """ + return random.random() + + def random_letters(self): + """ + Generate and return a sequence of random letters. + + The length of the returned string is also random, and is also + returned. + + Returns + ------- + length : int + Length of the returned string. + letters : str + String of random letters. + """ + length = random.randint(1, 10) + letters = ''.join(random.choice(string.ascii_lowercase) + for i in range(length)) + return length, letters + + def sample_values(self): + """ + Generate an infinite sequence of random numbers. + + The values are sampled from a continuous uniform distribution between + 0 and 1. + + Yields + ------ + float + Random number generated. + """ + while True: + yield random.random() + + def head(self): + """Return the first 5 elements of the Series. + + This function is mainly useful to preview the values of the + Series without displaying the whole of it. + + Returns + ------- + Series + Subset of the original series with the 5 first values. + + See Also + -------- + Series.tail : Return the last 5 elements of the Series. + Series.iloc : Return a slice of the elements in the Series, + which can also be used to return the first or last n. + """ + return self.iloc[:5] + + def head1(self, n=5): + """Return the first elements of the Series. + + This function is mainly useful to preview the values of the + Series without displaying the whole of it. + + Parameters + ---------- + n : int + Number of values to return. + + Return + ------ + pandas.Series + Subset of the original series with the n first values. + + See Also + -------- + tail : Return the last n elements of the Series. + + Examples + -------- + >>> s = pd.Series(['Ant', 'Bear', 'Cow', 'Dog', 'Falcon', + ... 'Lion', 'Monkey', 'Rabbit', 'Zebra']) + >>> s.head() + 0 Ant + 1 Bear + 2 Cow + 3 Dog + 4 Falcon + dtype: object + + With the `n` parameter, we can change the number of returned rows: + + >>> s.head(n=3) + 0 Ant + 1 Bear + 2 Cow + dtype: object + """ + return self.iloc[:n] + + def contains(self, pattern, case_sensitive=True, na=numpy.nan): + """ + Return whether each value contains `pattern`. + + In this case, we are illustrating how to use sections, even + if the example is simple enough and does not require them. + + Examples + -------- + >>> s = pd.Series('Antelope', 'Lion', 'Zebra', numpy.nan) + >>> s.contains(pattern='a') + 0 False + 1 False + 2 True + 3 NaN + dtype: bool + + **Case sensitivity** + + With `case_sensitive` set to `False` we can match `a` with both + `a` and `A`: + + >>> s.contains(pattern='a', case_sensitive=False) + 0 True + 1 False + 2 True + 3 NaN + dtype: bool + + **Missing values** + + We can fill missing values in the output using the `na` parameter: + + >>> s.contains(pattern='a', na=False) + 0 False + 1 False + 2 True + 3 False + dtype: bool + """ + pass + + def plot2(self): + """ + Generate a plot with the `Series` data. + + Examples + -------- + + .. plot:: + :context: close-figs + + >>> s = pd.Series([1, 2, 3]) + >>> s.plot() + """ + pass + +class BadDocStrings(object): + + def func(self): + + """Some function. + + With several mistakes in the docstring. + + It has a blank like after the signature `def func():`. + + The text 'Some function' should go in the line after the + opening quotes of the docstring, not in the same line. + + There is a blank line between the docstring and the first line + of code `foo = 1`. + + The closing quotes should be in the next line, not in this one.""" + + foo = 1 + bar = 2 + return foo + bar + + def astype(self, dtype): + """ + Casts Series type. + + Verb in third-person of the present simple, should be infinitive. + """ + pass + + def astype1(self, dtype): + """ + Method to cast Series type. + + Does not start with verb. + """ + pass + + def astype2(self, dtype): + """ + Cast Series type + + Missing dot at the end. + """ + pass + + def astype3(self, dtype): + """ + Cast Series type from its current type to the new type defined in + the parameter dtype. + + Summary is too verbose and doesn't fit in a single line. + """ + pass + + def plot(self, kind, **kwargs): + """ + Generate a plot. + + Render the data in the Series as a matplotlib plot of the + specified kind. + + Note the blank line between the parameters title and the first + parameter. Also, note that after the name of the parameter `kind` + and before the colon, a space is missing. + + Also, note that the parameter descriptions do not start with a + capital letter, and do not finish with a dot. + + Finally, the `**kwargs` parameter is missing. + + Parameters + ---------- + + kind: str + kind of matplotlib plot + """ + pass + + def method(self, foo=None, bar=None): + """ + A sample DataFrame method. + + Do not import numpy and pandas. + + Try to use meaningful data, when it makes the example easier + to understand. + + Try to avoid positional arguments like in `df.method(1)`. They + can be all right if previously defined with a meaningful name, + like in `present_value(interest_rate)`, but avoid them otherwise. + + When presenting the behavior with different parameters, do not place + all the calls one next to the other. Instead, add a short sentence + explaining what the example shows. + + Examples + -------- + >>> import numpy as np + >>> import pandas as pd + >>> df = pd.DataFrame(numpy.random.randn(3, 3), + ... columns=('a', 'b', 'c')) + >>> df.method(1) + 21 + >>> df.method(bar=14) + 123 + """ + pass + +class TestValidator(object): + + @pytest.fixture(autouse=True, scope="class") + def import_scripts(self): + up = os.path.dirname + file_dir = up(os.path.abspath(__file__)) + script_dir = os.path.join(up(up(up(file_dir))), 'scripts') + sys.path.append(script_dir) + from validate_docstrings import validate_one + globals()['validate_one'] = validate_one + yield + sys.path.pop() + del globals()['validate_one'] + + @pytest.mark.parametrize("func", [ + 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', + 'contains', 'plot2']) + def test_good_functions(self, func): + assert validate_one('pandas.tests.scripts.test_validate_docstrings' + '.GoodDocStrings.' + func) == 0 From 3eaf3baa863245581ff465595bc76c541a2ea47b Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 8 Mar 2018 20:02:12 -0800 Subject: [PATCH 04/24] Fixed writer arg to doctest.runner --- scripts/validate_docstrings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 894b97163ab5e..6820aa99d4b29 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -262,7 +262,7 @@ def examples_errors(self): error_msgs = '' for test in finder.find(self.raw_doc, self.method_name, globs=context): f = StringIO() - runner.run(test, out=f) + runner.run(test, out=f.write) error_msgs += f.getvalue() return error_msgs From 876337f22ad073c301f7fd891f34ca55bf8b5d4f Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 12 Mar 2018 11:57:09 -0700 Subject: [PATCH 05/24] Py27 compat and updated tests / logic --- .../tests/scripts/test_validate_docstrings.py | 87 +++++++++++-------- scripts/validate_docstrings.py | 61 ++++++++++--- 2 files changed, 98 insertions(+), 50 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index c58ef3566896c..2a9538862b215 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -1,7 +1,7 @@ import os import sys -import numpy +import numpy as np import pytest @@ -79,7 +79,8 @@ def sample_values(self): yield random.random() def head(self): - """Return the first 5 elements of the Series. + """ + Return the first 5 elements of the Series. This function is mainly useful to preview the values of the Series without displaying the whole of it. @@ -98,7 +99,8 @@ def head(self): return self.iloc[:5] def head1(self, n=5): - """Return the first elements of the Series. + """ + Return the first elements of the Series. This function is mainly useful to preview the values of the Series without displaying the whole of it. @@ -108,9 +110,9 @@ def head1(self, n=5): n : int Number of values to return. - Return - ------ - pandas.Series + Returns + ------- + Series Subset of the original series with the n first values. See Also @@ -119,8 +121,7 @@ def head1(self, n=5): Examples -------- - >>> s = pd.Series(['Ant', 'Bear', 'Cow', 'Dog', 'Falcon', - ... 'Lion', 'Monkey', 'Rabbit', 'Zebra']) + >>> s = pd.Series(['Ant', 'Bear', 'Cow', 'Dog', 'Falcon']) >>> s.head() 0 Ant 1 Bear @@ -139,40 +140,49 @@ def head1(self, n=5): """ return self.iloc[:n] - def contains(self, pattern, case_sensitive=True, na=numpy.nan): + def contains(self, pat, case=True, na=np.nan): """ - Return whether each value contains `pattern`. + Return whether each value contains `pat`. In this case, we are illustrating how to use sections, even if the example is simple enough and does not require them. + Parameters + ---------- + pat : str + Pattern to check for within each element. + case : bool, default True + Whether check should be done with case sensitivity. + na : object, default np.nan + Fill value for missing data. + Examples -------- - >>> s = pd.Series('Antelope', 'Lion', 'Zebra', numpy.nan) - >>> s.contains(pattern='a') + >>> s = pd.Series(['Antelope', 'Lion', 'Zebra', np.nan]) + >>> s.str.contains(pat='a') 0 False 1 False 2 True 3 NaN - dtype: bool + dtype: object **Case sensitivity** With `case_sensitive` set to `False` we can match `a` with both `a` and `A`: - >>> s.contains(pattern='a', case_sensitive=False) + >>> s.str.contains(pat='a', case=False) 0 True 1 False 2 True 3 NaN - dtype: bool + dtype: object **Missing values** We can fill missing values in the output using the `na` parameter: - >>> s.contains(pattern='a', na=False) + >>> s.str.contains(pat='a', na=False) 0 False 1 False 2 True @@ -181,20 +191,6 @@ def contains(self, pattern, case_sensitive=True, na=numpy.nan): """ pass - def plot2(self): - """ - Generate a plot with the `Series` data. - - Examples - -------- - - .. plot:: - :context: close-figs - - >>> s = pd.Series([1, 2, 3]) - >>> s.plot() - """ - pass class BadDocStrings(object): @@ -285,7 +281,7 @@ def method(self, foo=None, bar=None): to understand. Try to avoid positional arguments like in `df.method(1)`. They - can be all right if previously defined with a meaningful name, + can be alright if previously defined with a meaningful name, like in `present_value(interest_rate)`, but avoid them otherwise. When presenting the behavior with different parameters, do not place @@ -296,12 +292,15 @@ def method(self, foo=None, bar=None): -------- >>> import numpy as np >>> import pandas as pd - >>> df = pd.DataFrame(numpy.random.randn(3, 3), + >>> df = pd.DataFrame(np.ones((3, 3)), ... columns=('a', 'b', 'c')) - >>> df.method(1) - 21 - >>> df.method(bar=14) - 123 + >>> df.all(1) + 0 True + 1 True + 2 True + dtype: bool + >>> df.all(bool_only=True) + Series([], dtype: bool) """ pass @@ -309,6 +308,14 @@ class TestValidator(object): @pytest.fixture(autouse=True, scope="class") def import_scripts(self): + """ + Because the scripts directory is above the top level pandas package + we need to hack sys.path to know where to find that directory for + import. The below traverses up the file system to find the scripts + directory, adds to location to sys.path and imports the required + module into the global namespace before as part of class setup, + reverting those changes on teardown. + """ up = os.path.dirname file_dir = up(os.path.abspath(__file__)) script_dir = os.path.join(up(up(up(file_dir))), 'scripts') @@ -321,7 +328,13 @@ def import_scripts(self): @pytest.mark.parametrize("func", [ 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', - 'contains', 'plot2']) + 'contains']) def test_good_functions(self, func): assert validate_one('pandas.tests.scripts.test_validate_docstrings' '.GoodDocStrings.' + func) == 0 + + @pytest.mark.parametrize("func", [ + 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) + def test_bad_functions(self, func): + assert validate_one('pandas.tests.scripts.test_validate_docstrings' + '.BadDocStrings.' + func) > 0 diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 6820aa99d4b29..8f16a3ad8fe07 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -34,6 +34,7 @@ sys.path.insert(0, os.path.join(BASE_PATH)) import pandas +from pandas.compat import signature sys.path.insert(1, os.path.join(BASE_PATH, 'doc', 'sphinxext')) from numpydoc.docscrape import NumpyDocString @@ -185,11 +186,17 @@ def signature_parameters(self): # accessor classes have a signature, but don't want to show this return tuple() try: - params = self.method_obj.__code__.co_varnames + sig = signature(self.method_obj) except (TypeError, ValueError): # Some objects, mainly in C extensions do not support introspection # of the signature return tuple() + params = sig.args + if sig.varargs: + params.append("*" + sig.varargs) + if sig.keywords: + params.append("**" + sig.keywords) + params = tuple(params) if params and params[0] in ('self', 'cls'): return params[1:] return params @@ -237,6 +244,10 @@ def examples(self): def returns(self): return self.doc['Returns'] + @property + def method_source(self): + return inspect.getsource(self.method_obj) + @property def first_line_ends_in_dot(self): if self.doc: @@ -376,6 +387,19 @@ def validate_all(): def validate_one(func_name): + """ + Validate the docstring for the given func_name + + Parameters + ---------- + func_name : function + Function whose docstring will be evaluated + + Returns + ------- + int + The number of errors found in the `func_name` docstring + """ func_obj = _load_obj(func_name) doc = Docstring(func_name, func_obj) @@ -383,6 +407,7 @@ def validate_one(func_name): sys.stderr.write('{}\n'.format(doc.clean_doc)) 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 ' @@ -410,16 +435,17 @@ def validate_one(func_name): 'not third person (e.g. use "Generate" instead of ' '"Generates")') if not doc.extended_summary: - errs.append('No extended summary found') + wrns.append('No extended summary found') param_errs = doc.parameter_mismatches for param in doc.doc_parameters: - 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 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 not doc.parameter_desc(param): param_errs.append('Parameter "{}" ' @@ -437,8 +463,12 @@ def validate_one(func_name): for param_err in param_errs: errs.append('\t{}'.format(param_err)) - if not doc.returns: - errs.append('No returns section found') + if not doc.returns and "return" in doc.method_source: + errs.append('No Returns section found') + if "yield" in doc.method_source: + # numpydoc is not correctly parsing Yields sections, so + # best we can do is warn the user to lookout for this... + wrns.append('Yield found in source - please make sure to document!') mentioned_errs = doc.mentioned_private_classes if mentioned_errs: @@ -446,7 +476,7 @@ def validate_one(func_name): 'docstring.'.format(mentioned_errs)) if not doc.see_also: - errs.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: @@ -454,7 +484,7 @@ def validate_one(func_name): 'See Also "{}" reference'.format(rel_name)) examples_errs = '' if not doc.examples: - errs.append('No examples section found') + wrns.append('No examples section found') else: examples_errs = doc.examples_errors if examples_errs: @@ -465,7 +495,12 @@ def validate_one(func_name): sys.stderr.write('Errors found:\n') for err in errs: sys.stderr.write('\t{}\n'.format(err)) - else: + if wrns: + sys.stderr.write('Warnings found:\n') + for wrn in wrns: + sys.stderr.write('\t{}\n'.format(wrn)) + + if not errs: sys.stderr.write('Docstring for "{}" correct. :)\n'.format(func_name)) if examples_errs: From d0e0ad6a8e25f36e215b64344fefa8f50a28a80b Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 12 Mar 2018 23:23:18 -0700 Subject: [PATCH 06/24] Added skipif for no sphinx --- pandas/tests/scripts/test_validate_docstrings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index 2a9538862b215..ad45ed893ac22 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -3,7 +3,9 @@ import numpy as np import pytest - + +import pandas.util._test_decorators as td + class GoodDocStrings(object): """ @@ -304,6 +306,8 @@ def method(self, foo=None, bar=None): """ pass + +@td.skip_if_no('sphinx') class TestValidator(object): @pytest.fixture(autouse=True, scope="class") From f123a87155a59a87ec6a8d685b053079edf42666 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Tue, 13 Mar 2018 10:43:29 -0700 Subject: [PATCH 07/24] Ported Yields section to numpydoc --- doc/sphinxext/numpydoc/docscrape.py | 22 ++++-- doc/sphinxext/numpydoc/docscrape_sphinx.py | 11 +-- .../numpydoc/tests/test_docscrape.py | 79 +++++++++++++++++++ doc/sphinxext/numpydoc/traitsdoc.py | 3 +- scripts/validate_docstrings.py | 10 ++- 5 files changed, 110 insertions(+), 15 deletions(-) diff --git a/doc/sphinxext/numpydoc/docscrape.py b/doc/sphinxext/numpydoc/docscrape.py index 38cb62581ae76..8ff4094816922 100755 --- a/doc/sphinxext/numpydoc/docscrape.py +++ b/doc/sphinxext/numpydoc/docscrape.py @@ -96,6 +96,7 @@ def __init__(self, docstring, config={}): 'Extended Summary': [], 'Parameters': [], 'Returns': [], + 'Yields': [], 'Raises': [], 'Warns': [], 'Other Parameters': [], @@ -287,11 +288,22 @@ def _parse(self): self._doc.reset() self._parse_summary() - for (section,content) in self._read_sections(): + sections = list(self._read_sections()) + section_names = set([section for section, content in sections]) + + has_returns = 'Returns' in section_names + has_yields = 'Yields' in section_names + # We could do more tests, but we are not. Arbitrarily. + if has_returns and has_yields: + msg = 'Docstring contains both a Returns and Yields section.' + raise ValueError(msg) + + for (section, content) in sections: if not section.startswith('..'): section = ' '.join(s.capitalize() for s in section.split(' ')) - if section in ('Parameters', 'Returns', 'Raises', 'Warns', - 'Other Parameters', 'Attributes', 'Methods'): + if section in ('Parameters', 'Returns', 'Yields', 'Raises', + 'Warns', 'Other Parameters', 'Attributes', + 'Methods'): self[section] = self._parse_param_list(content) elif section.startswith('.. index::'): self['index'] = self._parse_index(section, content) @@ -390,8 +402,8 @@ def __str__(self, func_role=''): out += self._str_signature() out += self._str_summary() out += self._str_extended_summary() - for param_list in ('Parameters', 'Returns', 'Other Parameters', - 'Raises', 'Warns'): + for param_list in ('Parameters', 'Returns', 'Yields', + 'Other Parameters', 'Raises', 'Warns'): out += self._str_param_list(param_list) out += self._str_section('Warnings') out += self._str_see_also(func_role) diff --git a/doc/sphinxext/numpydoc/docscrape_sphinx.py b/doc/sphinxext/numpydoc/docscrape_sphinx.py index 127ed49c106ad..c711725f5272e 100755 --- a/doc/sphinxext/numpydoc/docscrape_sphinx.py +++ b/doc/sphinxext/numpydoc/docscrape_sphinx.py @@ -47,12 +47,12 @@ def _str_summary(self): def _str_extended_summary(self): return self['Extended Summary'] + [''] - def _str_returns(self): + def _str_returns(self, name='Returns'): out = [] - if self['Returns']: - out += self._str_field_list('Returns') + if self[name]: + out += self._str_field_list(name) out += [''] - for param, param_type, desc in self['Returns']: + for param, param_type, desc in self[name]: if param_type: out += self._str_indent(['**%s** : %s' % (param.strip(), param_type)]) @@ -227,7 +227,8 @@ def __str__(self, indent=0, func_role="obj"): out += self._str_summary() out += self._str_extended_summary() out += self._str_param_list('Parameters') - out += self._str_returns() + out += self._str_returns('Returns') + out += self._str_returns('Yields') for param_list in ('Other Parameters', 'Raises', 'Warns'): out += self._str_param_list(param_list) out += self._str_warnings() diff --git a/doc/sphinxext/numpydoc/tests/test_docscrape.py b/doc/sphinxext/numpydoc/tests/test_docscrape.py index b412124d774bb..64cdff9d63c04 100755 --- a/doc/sphinxext/numpydoc/tests/test_docscrape.py +++ b/doc/sphinxext/numpydoc/tests/test_docscrape.py @@ -1,6 +1,7 @@ # -*- encoding:utf-8 -*- from __future__ import division, absolute_import, print_function +import re import sys, textwrap from numpydoc.docscrape import NumpyDocString, FunctionDoc, ClassDoc @@ -122,6 +123,20 @@ ''' doc = NumpyDocString(doc_txt) +doc_yields_txt = """ +Test generator + +Yields +------ +a : int + The number of apples. +b : int + The number of bananas. +int + The number of unknowns. +""" +doc_yields = NumpyDocString(doc_yields_txt) + def test_signature(): assert doc['Signature'].startswith('numpy.multivariate_normal(') @@ -164,6 +179,36 @@ def test_returns(): assert desc[0].startswith('This is not a real') assert desc[-1].endswith('anonymous return values.') +def test_yields(): + section = doc_yields['Yields'] + assert_equal(len(section), 3) + truth = [('a', 'int', 'apples.'), + ('b', 'int', 'bananas.'), + ('int', '', 'unknowns.')] + for (arg, arg_type, desc), (arg_, arg_type_, end) in zip(section, truth): + assert_equal(arg, arg_) + assert_equal(arg_type, arg_type_) + assert desc[0].startswith('The number of') + assert desc[0].endswith(end) + +def test_returnyield(): + doc_text = """ +Test having returns and yields. + +Returns +------- +int + The number of apples. + +Yields +------ +a : int + The number of apples. +b : int + The number of bananas. +""" + assert_raises(ValueError, NumpyDocString, doc_text) + def test_notes(): assert doc['Notes'][0].startswith('Instead') assert doc['Notes'][-1].endswith('definite.') @@ -192,6 +237,24 @@ def non_blank_line_by_line_compare(a,b): raise AssertionError("Lines %s of a and b differ: " "\n>>> %s\n<<< %s\n" % (n,line,b[n])) + + +def _strip_blank_lines(s): + "Remove leading, trailing and multiple blank lines" + s = re.sub(r'^\s*\n', '', s) + s = re.sub(r'\n\s*$', '', s) + s = re.sub(r'\n\s*\n', r'\n\n', s) + return s + + +def line_by_line_compare(a, b): + a = textwrap.dedent(a) + b = textwrap.dedent(b) + a = [l.rstrip() for l in _strip_blank_lines(a).split('\n')] + b = [l.rstrip() for l in _strip_blank_lines(b).split('\n')] + assert_list_equal(a, b) + + def test_str(): non_blank_line_by_line_compare(str(doc), """numpy.multivariate_normal(mean, cov, shape=None, spam=None) @@ -302,6 +365,22 @@ def test_str(): :refguide: random;distributions, random;gauss""") +def test_yield_str(): + line_by_line_compare(str(doc_yields), +"""Test generator + +Yields +------ +a : int + The number of apples. +b : int + The number of bananas. +int + The number of unknowns. + +.. index:: """) + + def test_sphinx_str(): sphinx_doc = SphinxDocString(doc_txt) non_blank_line_by_line_compare(str(sphinx_doc), diff --git a/doc/sphinxext/numpydoc/traitsdoc.py b/doc/sphinxext/numpydoc/traitsdoc.py index 596c54eb389a3..2468565a6ca1e 100755 --- a/doc/sphinxext/numpydoc/traitsdoc.py +++ b/doc/sphinxext/numpydoc/traitsdoc.py @@ -61,6 +61,7 @@ def __init__(self, cls, modulename='', func_doc=SphinxFunctionDoc): 'Extended Summary': [], 'Parameters': [], 'Returns': [], + 'Yields': [], 'Raises': [], 'Warns': [], 'Other Parameters': [], @@ -89,7 +90,7 @@ def __str__(self, indent=0, func_role="func"): out += self._str_summary() out += self._str_extended_summary() for param_list in ('Parameters', 'Traits', 'Methods', - 'Returns','Raises'): + 'Returns', 'Yields', 'Raises'): out += self._str_param_list(param_list) out += self._str_see_also("obj") out += self._str_section('Notes') diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 8f16a3ad8fe07..c50e92a0d565b 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -244,6 +244,10 @@ def examples(self): def returns(self): return self.doc['Returns'] + @property + def yields(self): + return self.doc['Yields'] + @property def method_source(self): return inspect.getsource(self.method_obj) @@ -465,10 +469,8 @@ def validate_one(func_name): if not doc.returns and "return" in doc.method_source: errs.append('No Returns section found') - if "yield" in doc.method_source: - # numpydoc is not correctly parsing Yields sections, so - # best we can do is warn the user to lookout for this... - wrns.append('Yield found in source - please make sure to document!') + if not doc.yields and "yield" in doc.method_source: + errs.append('No Yields section found!') mentioned_errs = doc.mentioned_private_classes if mentioned_errs: From c463ea9cb6d27bc00f3dec6debee340cb0702293 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Tue, 13 Mar 2018 10:46:37 -0700 Subject: [PATCH 08/24] LINT fixes --- .../tests/scripts/test_validate_docstrings.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index ad45ed893ac22..aa31baf66b36c 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -44,7 +44,7 @@ def sample(self): float Random number generated. """ - return random.random() + return random.random() # noqa: F821 def random_letters(self): """ @@ -60,8 +60,8 @@ def random_letters(self): letters : str String of random letters. """ - length = random.randint(1, 10) - letters = ''.join(random.choice(string.ascii_lowercase) + length = random.randint(1, 10) # noqa: F821 + letters = ''.join(random.choice(string.ascii_lowercase) # noqa: F821 for i in range(length)) return length, letters @@ -78,7 +78,7 @@ def sample_values(self): Random number generated. """ while True: - yield random.random() + yield random.random() # noqa: F821 def head(self): """ @@ -329,16 +329,16 @@ def import_scripts(self): yield sys.path.pop() del globals()['validate_one'] - + @pytest.mark.parametrize("func", [ 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', 'contains']) def test_good_functions(self, func): - assert validate_one('pandas.tests.scripts.test_validate_docstrings' - '.GoodDocStrings.' + func) == 0 + assert validate_one('pandas.tests.scripts.test_validate_' # noqa: F821 + 'docstrings.GoodDocStrings.' + func) == 0 @pytest.mark.parametrize("func", [ 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) def test_bad_functions(self, func): - assert validate_one('pandas.tests.scripts.test_validate_docstrings' - '.BadDocStrings.' + func) > 0 + assert validate_one('pandas.tests.scripts.test_validate_' # noqa: F821 + 'docstrings.BadDocStrings.' + func) > 0 From 103a6787823269203f1fe4c23a477416d014a7a9 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Tue, 13 Mar 2018 22:44:48 -0700 Subject: [PATCH 09/24] Fixed LINT issue with script --- scripts/validate_docstrings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index c50e92a0d565b..0f3005af18c27 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -448,8 +448,8 @@ def validate_one(func_name): 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)) + param_errs.append('Parameter "{}" type should ' + 'not finish with "."'.format(param)) if not doc.parameter_desc(param): param_errs.append('Parameter "{}" ' From 06fa6b3e3788ee24d2fc8562faac20fa1c8f5e05 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 15 Mar 2018 09:51:21 -0700 Subject: [PATCH 10/24] Refactored code, added class docstring test --- .../tests/scripts/test_validate_docstrings.py | 41 ++++++++++++++++--- scripts/validate_docstrings.py | 9 ++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index aa31baf66b36c..cc8c0360167e2 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -9,8 +9,10 @@ class GoodDocStrings(object): """ - Collection of good docstrings - be sure to update the tests as new - examples are added here + Collection of good doc strings. + + This class contains a lot of docstrings that should pass the validation + script without any errors. """ def plot(self, kind, color='blue', **kwargs): @@ -330,15 +332,42 @@ def import_scripts(self): sys.path.pop() del globals()['validate_one'] + def _import_path(self, klass=None, func=None): + """ + Build the required import path for tests in this module. + + Parameters + ---------- + klass : str + Class name of object in module. + func : str + Function name of object in module. + + Returns + ------- + str + Import path of specified object in this module + """ + base_path = 'pandas.tests.scripts.test_validate_docstrings' + if klass: + base_path = '.'.join([base_path, klass]) + if func: + base_path = '.'.join([base_path, func]) + + return base_path + + def test_good_class(self): + assert validate_one(self._import_path(klass='GoodDocStrings')) == 0 + @pytest.mark.parametrize("func", [ 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', 'contains']) def test_good_functions(self, func): - assert validate_one('pandas.tests.scripts.test_validate_' # noqa: F821 - 'docstrings.GoodDocStrings.' + func) == 0 + assert validate_one(self._import_path(klass='GoodDocStrings', + func=func)) == 0 @pytest.mark.parametrize("func", [ 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) def test_bad_functions(self, func): - assert validate_one('pandas.tests.scripts.test_validate_' # noqa: F821 - 'docstrings.BadDocStrings.' + func) > 0 + assert validate_one(self._import_path(klass='BadDocStrings', + func=func)) > 0 diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 0f3005af18c27..1bffeae0c60e6 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -467,10 +467,11 @@ def validate_one(func_name): for param_err in param_errs: errs.append('\t{}'.format(param_err)) - if not doc.returns and "return" in doc.method_source: - errs.append('No Returns section found') - if not doc.yields and "yield" in doc.method_source: - errs.append('No Yields section found!') + if doc.is_function_or_method: + if not doc.returns and "return" in doc.method_source: + errs.append('No Returns section found') + if not doc.yields and "yield" in doc.method_source: + errs.append('No Yields section found') mentioned_errs = doc.mentioned_private_classes if mentioned_errs: From 84945756200052685b5c6958f1ec027110ba5a33 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 15 Mar 2018 09:57:00 -0700 Subject: [PATCH 11/24] Introduced new class structure --- .../tests/scripts/test_validate_docstrings.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index cc8c0360167e2..e53681888ea4c 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -196,7 +196,9 @@ def contains(self, pat, case=True, na=np.nan): pass -class BadDocStrings(object): +class BadGenericDocStrings(object): + """Everything here has a bad docstring + """ def func(self): @@ -308,6 +310,36 @@ def method(self, foo=None, bar=None): """ pass + def missing_params(self, kind, **kwargs): + """ + Kwargs argument is missing in parameters section. + + Parameters + ---------- + kind : str + Kind of matplotlib plot. + """ + pass + + def missing_param_colon_spacing(self, kind, kind2): + """ + Bad spacing around colo spacing in Parameters. + + Parameters + ---------- + kind: str + Needs space before. + kind2 :str + Needs space after. + """ + + +class BadParameters(): + """ + Everything here has a problem with its Parameters section. + """ + pass + @td.skip_if_no('sphinx') class TestValidator(object): @@ -366,8 +398,12 @@ def test_good_functions(self, func): assert validate_one(self._import_path(klass='GoodDocStrings', func=func)) == 0 + def test_bad_class(self): + assert validate_one(self._import_path( + klass='BadGenericDocStrings')) > 0 + @pytest.mark.parametrize("func", [ 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) def test_bad_functions(self, func): - assert validate_one(self._import_path(klass='BadDocStrings', + assert validate_one(self._import_path(klass='BadGenericDocStrings', func=func)) > 0 From fc47d3d7c00b306757924d51e5694bf8fdb84959 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 15 Mar 2018 11:33:48 -0700 Subject: [PATCH 12/24] Added bad examples to tests --- .../tests/scripts/test_validate_docstrings.py | 177 ++++++++++++++++-- scripts/validate_docstrings.py | 6 +- 2 files changed, 166 insertions(+), 17 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index e53681888ea4c..046cb32fa2b4e 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -310,35 +310,139 @@ def method(self, foo=None, bar=None): """ pass + +class BadSummaries(object): + + def wrong_line(self): + """Exists on the wrong line""" + pass + + def no_punctuation(self): + """ + Has the right line but forgets punctuation + """ + pass + + def no_capitalization(self): + """ + provides a lowercase summary. + """ + pass + + def no_infinitive(self): + """ + Started with a verb that is not infinitive. + """ + + def multi_line(self): + """ + Extends beyond one line + which is not correct. + """ + + +class BadParameters(object): + """ + Everything here has a problem with its Parameters section. + """ + def missing_params(self, kind, **kwargs): """ - Kwargs argument is missing in parameters section. + Lacks kwargs in Parameters. Parameters ---------- kind : str - Kind of matplotlib plot. + Foo bar baz. """ - pass - def missing_param_colon_spacing(self, kind, kind2): + def bad_colon_spacing(self, kind): """ - Bad spacing around colo spacing in Parameters. + Has bad spacing in the type line. Parameters ---------- kind: str - Needs space before. - kind2 :str - Needs space after. + Needs a space after kind. """ + def no_description_period(self, kind): + """ + Forgets to add a period to the description. -class BadParameters(): - """ - Everything here has a problem with its Parameters section. - """ - pass + Parameters + ---------- + kind : str + Doesn't end with a dot + """ + + def parameter_capitalization(self, kind): + """ + Forgets to capitalize the description. + + Parameters + ---------- + kind : str + this is not capitalized. + """ + + def blank_lines(self, kind): + """ + Adds a blank line after the section header. + + Parameters + ---------- + + kind : str + Foo bar baz. + """ + pass + + +class BadReturns(object): + + def return_not_documented(self): + """ + Lacks section for Returns + """ + return "Hello world!" + + def yield_not_documented(self): + """ + Lacks section for Yields + """ + yield "Hello world!" + + def no_type(self): + """ + Returns documented but without type. + + Returns + ------- + Some value. + """ + return "Hello world!" + + def no_description(self): + """ + Provides type but no descrption. + + Returns + ------- + str + """ + return "Hello world!" + + def no_punctuation(self): + """ + Provides type and description but no period. + + Returns + ------- + str + A nice greeting + """ + return "Hello world!" @td.skip_if_no('sphinx') @@ -404,6 +508,51 @@ def test_bad_class(self): @pytest.mark.parametrize("func", [ 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) - def test_bad_functions(self, func): + def test_bad_generic_functions(self, func): assert validate_one(self._import_path(klass='BadGenericDocStrings', func=func)) > 0 + + @pytest.mark.parametrize("func,msgs", [ + ('wrong_line', ('should start in the line immediately after the ' + 'opening quotes',)), + ('no_punctuation', ('Summary does not end with a period',)), + ('no_capitalization', ('Summary does not start with a capital ' + 'letter',)), + ('no_capitalization', ('Summary must start with infinitive verb',)), + ('multi_line', ('a short summary in a single line should be ' + 'present',))]) + def test_bad_summaries(self, capsys, func, msgs): + validate_one(self._import_path(klass='BadSummaries', func=func)) + err = capsys.readouterr().err + for msg in msgs: + assert msg in err + + @pytest.mark.parametrize("func,msgs", [ + ('missing_params', ('Parameters {\'**kwargs\'} not documented',)), + ('bad_colon_spacing', ('Parameters {\'kind\'} not documented', + 'Unknown parameters {\'kind: str\'}', + 'Parameter "kind: str" has no type')), + ('no_description_period', ('Parameter "kind" description should ' + 'finish with "."',)), + ('parameter_capitalization', ('Parameter "kind" description should ' + 'start with a capital letter',)), + pytest.param('blank_lines', ('No error yet?',), + marks=pytest.mark.xfail) + ]) + def test_bad_params(self, capsys, func, msgs): + validate_one(self._import_path(klass='BadParameters', func=func)) + err = capsys.readouterr().err + for msg in msgs: + assert msg in err + + @pytest.mark.parametrize("func,msgs", [ + ('return_not_documented', ('No Returns section found',)), + ('yield_not_documented', ('No Yields section found',)), + pytest.param('no_type', ('foo',), marks=pytest.mark.xfail), + pytest.param('no_description', ('foo',), marks=pytest.mark.xfail), + pytest.param('no_punctuation', ('foo',), marks=pytest.mark.xfail)]) + def test_bad_returns(self, capsys, func, msgs): + validate_one(self._import_path(klass='BadReturns', func=func)) + err = capsys.readouterr().err + for msg in msgs: + assert msg in err diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 1bffeae0c60e6..d7fc91bc6299d 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -430,9 +430,9 @@ def validate_one(func_name): 'should be present at the beginning of the docstring)') else: if not doc.summary[0].isupper(): - errs.append('Summary does not start with capital') + errs.append('Summary does not start with a capital letter') if doc.summary[-1] != '.': - errs.append('Summary does not end with dot') + errs.append('Summary does not end with a period') if (doc.is_function_or_method and doc.summary.split(' ')[0][-1] == 's'): errs.append('Summary must start with infinitive verb, ' @@ -457,7 +457,7 @@ def validate_one(func_name): else: if not doc.parameter_desc(param)[0].isupper(): param_errs.append('Parameter "{}" description ' - 'should start with ' + 'should start with a ' 'capital letter'.format(param)) if doc.parameter_desc(param)[-1] != '.': param_errs.append('Parameter "{}" description ' From bed1fc4058680fa95f6ac62291b799ff9dc771c7 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 15 Mar 2018 11:40:49 -0700 Subject: [PATCH 13/24] Parametrized all tests --- .../tests/scripts/test_validate_docstrings.py | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index 046cb32fa2b4e..683f3a60907a7 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -512,47 +512,43 @@ def test_bad_generic_functions(self, func): assert validate_one(self._import_path(klass='BadGenericDocStrings', func=func)) > 0 - @pytest.mark.parametrize("func,msgs", [ - ('wrong_line', ('should start in the line immediately after the ' - 'opening quotes',)), - ('no_punctuation', ('Summary does not end with a period',)), - ('no_capitalization', ('Summary does not start with a capital ' - 'letter',)), - ('no_capitalization', ('Summary must start with infinitive verb',)), - ('multi_line', ('a short summary in a single line should be ' - 'present',))]) - def test_bad_summaries(self, capsys, func, msgs): - validate_one(self._import_path(klass='BadSummaries', func=func)) - err = capsys.readouterr().err - for msg in msgs: - assert msg in err - - @pytest.mark.parametrize("func,msgs", [ - ('missing_params', ('Parameters {\'**kwargs\'} not documented',)), - ('bad_colon_spacing', ('Parameters {\'kind\'} not documented', - 'Unknown parameters {\'kind: str\'}', - 'Parameter "kind: str" has no type')), - ('no_description_period', ('Parameter "kind" description should ' - 'finish with "."',)), - ('parameter_capitalization', ('Parameter "kind" description should ' - 'start with a capital letter',)), - pytest.param('blank_lines', ('No error yet?',), + @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', + ('a short summary in a single line should be present',)), + # 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', '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) ]) - def test_bad_params(self, capsys, func, msgs): - validate_one(self._import_path(klass='BadParameters', func=func)) - err = capsys.readouterr().err - for msg in msgs: - assert msg in err - - @pytest.mark.parametrize("func,msgs", [ - ('return_not_documented', ('No Returns section found',)), - ('yield_not_documented', ('No Yields section found',)), - pytest.param('no_type', ('foo',), marks=pytest.mark.xfail), - pytest.param('no_description', ('foo',), marks=pytest.mark.xfail), - pytest.param('no_punctuation', ('foo',), marks=pytest.mark.xfail)]) - def test_bad_returns(self, capsys, func, msgs): - validate_one(self._import_path(klass='BadReturns', func=func)) + def test_bad_examples(self, capsys, klass, func, msgs): + validate_one(self._import_path(klass=klass, func=func)) err = capsys.readouterr().err for msg in msgs: assert msg in err From 956c8cbcb79de7f024ee6b314c512638b48b5ed3 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Thu, 15 Mar 2018 11:44:15 -0700 Subject: [PATCH 14/24] LINT fixes --- pandas/tests/scripts/test_validate_docstrings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index 683f3a60907a7..e8d61c6fda1f8 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -493,14 +493,15 @@ def _import_path(self, klass=None, func=None): return base_path def test_good_class(self): - assert validate_one(self._import_path(klass='GoodDocStrings')) == 0 + assert validate_one(self._import_path( + klass='GoodDocStrings')) == 0 # noqa: F821 @pytest.mark.parametrize("func", [ 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', 'contains']) def test_good_functions(self, func): assert validate_one(self._import_path(klass='GoodDocStrings', - func=func)) == 0 + func=func)) == 0 # noqa: F821 def test_bad_class(self): assert validate_one(self._import_path( @@ -510,7 +511,7 @@ def test_bad_class(self): 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) def test_bad_generic_functions(self, func): assert validate_one(self._import_path(klass='BadGenericDocStrings', - func=func)) > 0 + func=func)) > 0 # noqa: F821 @pytest.mark.parametrize("klass,func,msgs", [ # Summary tests @@ -548,7 +549,7 @@ def test_bad_generic_functions(self, func): marks=pytest.mark.xfail) ]) def test_bad_examples(self, capsys, klass, func, msgs): - validate_one(self._import_path(klass=klass, func=func)) + validate_one(self._import_path(klass=klass, func=func)) # noqa: F821 err = capsys.readouterr().err for msg in msgs: assert msg in err From 0fb52d88c0ed3a3b91e105a72123b228ce4df38c Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Fri, 16 Mar 2018 08:47:37 -0700 Subject: [PATCH 15/24] LINT fixup --- pandas/tests/scripts/test_validate_docstrings.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index e8d61c6fda1f8..d41ed257f33f7 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -493,25 +493,25 @@ def _import_path(self, klass=None, func=None): return base_path def test_good_class(self): - assert validate_one(self._import_path( - klass='GoodDocStrings')) == 0 # noqa: F821 + assert validate_one(self._import_path( # noqa: F821 + klass='GoodDocStrings')) == 0 @pytest.mark.parametrize("func", [ 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', 'contains']) def test_good_functions(self, func): - assert validate_one(self._import_path(klass='GoodDocStrings', - func=func)) == 0 # noqa: F821 + assert validate_one(self._import_path( # noqa: F821 + klass='GoodDocStrings', func=func)) == 0 def test_bad_class(self): - assert validate_one(self._import_path( + assert validate_one(self._import_path( # noqa: F821 klass='BadGenericDocStrings')) > 0 @pytest.mark.parametrize("func", [ 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method']) def test_bad_generic_functions(self, func): - assert validate_one(self._import_path(klass='BadGenericDocStrings', - func=func)) > 0 # noqa: F821 + assert validate_one(self._import_path( # noqa:F821 + klass='BadGenericDocStrings', func=func)) > 0 @pytest.mark.parametrize("klass,func,msgs", [ # Summary tests @@ -549,7 +549,7 @@ def test_bad_generic_functions(self, func): marks=pytest.mark.xfail) ]) def test_bad_examples(self, capsys, klass, func, msgs): - validate_one(self._import_path(klass=klass, func=func)) # noqa: F821 + validate_one(self._import_path(klass=klass, func=func)) # noqa:F821 err = capsys.readouterr().err for msg in msgs: assert msg in err From a9e33a1e1480ba889cca704be07279acb750676b Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Sun, 29 Jul 2018 14:05:52 -0700 Subject: [PATCH 16/24] Removed errant newline removal --- doc/sphinxext/numpydoc/tests/test_docscrape.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinxext/numpydoc/tests/test_docscrape.py b/doc/sphinxext/numpydoc/tests/test_docscrape.py index 71681293c7682..2fb4eb5ab277e 100644 --- a/doc/sphinxext/numpydoc/tests/test_docscrape.py +++ b/doc/sphinxext/numpydoc/tests/test_docscrape.py @@ -232,6 +232,7 @@ def test_returnyield(): The number of apples. b : int The number of bananas. + """ assert_raises(ValueError, NumpyDocString, doc_text) From e6bba28ec3c2c0868cad7c85ebc14f19d97a0ca6 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Sun, 29 Jul 2018 14:59:08 -0700 Subject: [PATCH 17/24] Fixed issue with _accessors --- scripts/validate_docstrings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 463687689bc85..9fccb6b5b7467 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -181,11 +181,12 @@ def doc_parameters(self): @property def signature_parameters(self): - if (inspect.isclass(self.method_obj) + if inspect.isclass(self.method_obj): + if (hasattr(self.method_obj, '_accessors') and self.method_name.split('.')[-1] in self.method_obj._accessors): - # accessor classes have a signature, but don't want to show this - return tuple() + # accessor classes have a signature but don't want to show this + return tuple() try: sig = signature(self.method_obj) except (TypeError, ValueError): From 7948d9132c6018083135f58ebe3ee2bea9a6343e Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Sun, 29 Jul 2018 15:25:08 -0700 Subject: [PATCH 18/24] Py27 compat --- scripts/validate_docstrings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 9fccb6b5b7467..981ce4bc98d77 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -107,7 +107,8 @@ def __len__(self): @property def is_function_or_method(self): - return inspect.isfunction(self.method_obj) + # TODO(py27): revert ismethod to isfunction + return inspect.ismethod(self.method_obj) @property def source_file_name(self): From e1ec8646abfd50d121429482830d9712e864ebd0 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 30 Jul 2018 21:41:00 -0700 Subject: [PATCH 19/24] Parameter compat --- pandas/tests/scripts/test_validate_docstrings.py | 6 +++--- scripts/validate_docstrings.py | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index d41ed257f33f7..18c8639b4f0fa 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -527,10 +527,10 @@ def test_bad_generic_functions(self, func): ('a short summary in a single line should be present',)), # Parameters tests ('BadParameters', 'missing_params', - ('Parameters {\'**kwargs\'} not documented',)), + ('Parameters {**kwargs} not documented',)), ('BadParameters', 'bad_colon_spacing', - ('Parameters {\'kind\'} not documented', - 'Unknown parameters {\'kind: str\'}', + ('Parameters {kind} not documented', + 'Unknown parameters {kind: str}', 'Parameter "kind: str" has no type')), ('BadParameters', 'no_description_period', ('Parameter "kind" description should finish with "."',)), diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 981ce4bc98d77..988e767db7494 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -38,6 +38,7 @@ 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'] @@ -107,8 +108,9 @@ def __len__(self): @property def is_function_or_method(self): - # TODO(py27): revert ismethod to isfunction - return inspect.ismethod(self.method_obj) + # TODO(py27): remove ismethod + return (inspect.isfunction(self.method_obj) + or inspect.ismethod(self.method_obj)) @property def source_file_name(self): @@ -211,10 +213,11 @@ def parameter_mismatches(self): doc_params = tuple(self.doc_parameters) missing = set(signature_params) - set(doc_params) if missing: - errs.append('Parameters {!r} not documented'.format(missing)) + errs.append( + 'Parameters {} not documented'.format(pprint_thing(missing))) extra = set(doc_params) - set(signature_params) if extra: - errs.append('Unknown parameters {!r}'.format(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. ' + From 099b74721d466b568ece23a312b4677ac165ad26 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Tue, 31 Jul 2018 18:23:50 -0700 Subject: [PATCH 20/24] LINT fixup --- scripts/validate_docstrings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py index 988e767db7494..cdea2d8b83abd 100755 --- a/scripts/validate_docstrings.py +++ b/scripts/validate_docstrings.py @@ -185,9 +185,9 @@ def doc_parameters(self): @property def signature_parameters(self): if inspect.isclass(self.method_obj): - if (hasattr(self.method_obj, '_accessors') - and self.method_name.split('.')[-1] in - self.method_obj._accessors): + if hasattr(self.method_obj, '_accessors') and ( + self.method_name.split('.')[-1] in + self.method_obj._accessors): # accessor classes have a signature but don't want to show this return tuple() try: From 64410f05f3bb73fe139d5e0437351b12d8223217 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Sat, 11 Aug 2018 12:38:13 -0700 Subject: [PATCH 21/24] Purposely failing test to ensure CI coverage --- pandas/tests/scripts/test_validate_docstrings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index 18c8639b4f0fa..d012c218ae631 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -553,3 +553,6 @@ def test_bad_examples(self, capsys, klass, func, msgs): err = capsys.readouterr().err for msg in msgs: assert msg in err + + def test_this_fails(self): + raise NotImplementedError From 33d6827545f385abdf2fecfbdcfdcacb68cf7584 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 13 Aug 2018 11:04:15 -0700 Subject: [PATCH 22/24] Removed sphinx skipif decorator --- pandas/tests/scripts/test_validate_docstrings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index d012c218ae631..cbcb7b12f6602 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -445,7 +445,6 @@ def no_punctuation(self): return "Hello world!" -@td.skip_if_no('sphinx') class TestValidator(object): @pytest.fixture(autouse=True, scope="class") From 2e51e4949b82208da14948b3dc5feaf93fc2a698 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 13 Aug 2018 12:28:01 -0700 Subject: [PATCH 23/24] Removed purposely failing test --- pandas/tests/scripts/test_validate_docstrings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index cbcb7b12f6602..f220404035431 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -552,6 +552,3 @@ def test_bad_examples(self, capsys, klass, func, msgs): err = capsys.readouterr().err for msg in msgs: assert msg in err - - def test_this_fails(self): - raise NotImplementedError From 1fb5405a8d4fb1d5acfd215e5046bb091048b872 Mon Sep 17 00:00:00 2001 From: Will Ayd Date: Mon, 13 Aug 2018 13:39:55 -0700 Subject: [PATCH 24/24] LINT fixup --- pandas/tests/scripts/test_validate_docstrings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/tests/scripts/test_validate_docstrings.py b/pandas/tests/scripts/test_validate_docstrings.py index f220404035431..1d35d5d30bba3 100644 --- a/pandas/tests/scripts/test_validate_docstrings.py +++ b/pandas/tests/scripts/test_validate_docstrings.py @@ -4,8 +4,6 @@ import numpy as np import pytest -import pandas.util._test_decorators as td - class GoodDocStrings(object): """