import io import random import string import textwrap import pytest import numpy as np import validate_docstrings validate_one = validate_docstrings.validate_one class GoodDocStrings(object): """ 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): """ 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.sample(string.ascii_lowercase, 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. Returns ------- 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']) >>> 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, pat, case=True, na=np.nan): """ 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', np.nan]) >>> s.str.contains(pat='a') 0 False 1 False 2 True 3 NaN dtype: object **Case sensitivity** With `case_sensitive` set to `False` we can match `a` with both `a` and `A`: >>> s.str.contains(pat='a', case=False) 0 True 1 False 2 True 3 NaN dtype: object **Missing values** We can fill missing values in the output using the `na` parameter: >>> s.str.contains(pat='a', na=False) 0 False 1 False 2 True 3 False dtype: bool """ pass def mode(self, axis, numeric_only): """ Ensure sphinx directives don't affect checks for trailing periods. Parameters ---------- axis : str Sentence ending in period, followed by single directive. .. versionchanged:: 0.1.2 numeric_only : bool Sentence ending in period, followed by multiple directives. .. versionadded:: 0.1.2 .. deprecated:: 0.00.0 A multiline description, which spans another line. """ pass def good_imports(self): """ Ensure import other than numpy and pandas are fine. Examples -------- This example does not import pandas or import numpy. >>> import datetime >>> datetime.MAXYEAR 9999 """ pass def no_returns(self): """ Say hello and have no returns. """ pass def empty_returns(self): """ Say hello and always return None. Since this function never returns a value, this docstring doesn't need a return section. """ def say_hello(): return "Hello World!" say_hello() if True: return else: return None class BadGenericDocStrings(object): """Everything here has a bad docstring """ 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 two_linebreaks_between_sections(self, foo): """ Test linebreaks message GL03. Note 2 blank lines before parameters section. Parameters ---------- foo : str Description of foo parameter. """ pass def linebreak_at_end_of_docstring(self, foo): """ Test linebreaks message GL03. Note extra blank line at end of docstring. Parameters ---------- foo : str Description of foo parameter. """ 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 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 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(np.ones((3, 3)), ... columns=('a', 'b', 'c')) >>> df.all(1) 0 True 1 True 2 True dtype: bool >>> df.all(bool_only=True) Series([], dtype: bool) """ pass def private_classes(self): """ This mentions NDFrame, which is not correct. """ def unknown_section(self): """ This section has an unknown section title. Unknown Section --------------- This should raise an error in the validation. """ def sections_in_wrong_order(self): """ This docstring has the sections in the wrong order. Parameters ---------- name : str This section is in the right position. Examples -------- >>> print('So far Examples is good, as it goes before Parameters') So far Examples is good, as it goes before Parameters See Also -------- function : This should generate an error, as See Also needs to go before Examples. """ def deprecation_in_wrong_order(self): """ This docstring has the deprecation warning in the wrong order. This is the extended summary. The correct order should be summary, deprecation warning, extended summary. .. deprecated:: 1.0 This should generate an error as it needs to go before extended summary. """ def method_wo_docstrings(self): 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. """ def two_paragraph_multi_line(self): """ Extends beyond one line which is not correct. Extends beyond one line, which in itself is correct but the previous short summary should still be an issue. """ class BadParameters(object): """ Everything here has a problem with its Parameters section. """ def missing_params(self, kind, **kwargs): """ Lacks kwargs in Parameters. Parameters ---------- kind : str Foo bar baz. """ def bad_colon_spacing(self, kind): """ Has bad spacing in the type line. Parameters ---------- kind: str Needs a space after kind. """ def no_description_period(self, kind): """ Forgets to add a period to the description. Parameters ---------- kind : str Doesn't end with a dot """ def no_description_period_with_directive(self, kind): """ Forgets to add a period, and also includes a directive. Parameters ---------- kind : str Doesn't end with a dot .. versionadded:: 0.00.0 """ def no_description_period_with_directives(self, kind): """ Forgets to add a period, and also includes multiple directives. Parameters ---------- kind : str Doesn't end with a dot .. versionchanged:: 0.00.0 .. deprecated:: 0.00.0 """ 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 def integer_parameter(self, kind): """ Uses integer instead of int. Parameters ---------- kind : integer Foo bar baz. """ pass def string_parameter(self, kind): """ Uses string instead of str. Parameters ---------- kind : string Foo bar baz. """ pass def boolean_parameter(self, kind): """ Uses boolean instead of bool. Parameters ---------- kind : boolean Foo bar baz. """ pass def list_incorrect_parameter_type(self, kind): """ Uses list of boolean instead of list of bool. Parameters ---------- kind : list of boolean, integer, float or string 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!" def named_single_return(self): """ Provides name but returns only one value. Returns ------- s : str A nice greeting. """ return "Hello world!" def no_capitalization(self): """ Forgets capitalization in return values description. Returns ------- foo : str The first returned string. bar : str the second returned string. """ return "Hello", "World!" def no_period_multi(self): """ Forgets period in return values description. Returns ------- foo : str The first returned string bar : str The second returned string. """ return "Hello", "World!" class BadSeeAlso(object): def desc_no_period(self): """ Return the first 5 elements of the Series. 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 """ pass def desc_first_letter_lowercase(self): """ Return the first 5 elements of the Series. 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. """ pass def prefix_pandas(self): """ Have `pandas` prefix in See Also section. See Also -------- pandas.Series.rename : Alter Series index labels or name. DataFrame.head : The first `n` rows of the caller object. """ pass class BadExamples(object): def unused_import(self): """ Examples -------- >>> import pandas as pdf >>> df = pd.DataFrame(np.ones((3, 3)), columns=('a', 'b', 'c')) """ pass def missing_whitespace_around_arithmetic_operator(self): """ Examples -------- >>> 2+5 7 """ pass def indentation_is_not_a_multiple_of_four(self): """ Examples -------- >>> if 2 + 5: ... pass """ pass def missing_whitespace_after_comma(self): """ Examples -------- >>> df = pd.DataFrame(np.ones((3,3)),columns=('a','b', 'c')) """ pass class TestValidator(object): 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 = "scripts.tests.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, capsys): errors = validate_one(self._import_path( klass='GoodDocStrings'))['errors'] assert isinstance(errors, list) assert not errors @pytest.mark.parametrize("func", [ 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', 'contains', 'mode', 'good_imports', 'no_returns', 'empty_returns']) def test_good_functions(self, capsys, func): errors = validate_one(self._import_path( klass='GoodDocStrings', func=func))['errors'] assert isinstance(errors, list) assert not errors def test_bad_class(self, capsys): errors = validate_one(self._import_path( klass='BadGenericDocStrings'))['errors'] assert isinstance(errors, list) assert errors @pytest.mark.parametrize("func", [ 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method', 'private_classes', ]) def test_bad_generic_functions(self, capsys, func): errors = validate_one(self._import_path( # noqa:F821 klass='BadGenericDocStrings', func=func))['errors'] assert isinstance(errors, list) assert errors @pytest.mark.parametrize("klass,func,msgs", [ # See Also tests ('BadGenericDocStrings', 'private_classes', ("Private classes (NDFrame) should not be mentioned in public " 'docstrings',)), ('BadGenericDocStrings', 'unknown_section', ('Found unknown section "Unknown Section".',)), ('BadGenericDocStrings', 'sections_in_wrong_order', ('Sections are in the wrong order. Correct order is: Parameters, ' 'See Also, Examples',)), ('BadGenericDocStrings', 'deprecation_in_wrong_order', ('Deprecation warning should precede extended summary',)), ('BadSeeAlso', 'desc_no_period', ('Missing period at end of description for See Also "Series.iloc"',)), ('BadSeeAlso', 'desc_first_letter_lowercase', ('should be capitalized for See Also "Series.tail"',)), # 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', ('Parameter "kind" requires a space before the colon ' 'separating the parameter name and 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',)), ('BadParameters', 'integer_parameter', ('Parameter "kind" type should use "int" instead of "integer"',)), ('BadParameters', 'string_parameter', ('Parameter "kind" type should use "str" instead of "string"',)), ('BadParameters', 'boolean_parameter', ('Parameter "kind" type should use "bool" instead of "boolean"',)), ('BadParameters', 'list_incorrect_parameter_type', ('Parameter "kind" type should use "bool" instead of "boolean"',)), ('BadParameters', 'list_incorrect_parameter_type', ('Parameter "kind" type should use "int" instead of "integer"',)), ('BadParameters', 'list_incorrect_parameter_type', ('Parameter "kind" type should use "str" instead of "string"',)), 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), ('BadReturns', 'no_description', ('Return value has no description',)), ('BadReturns', 'no_punctuation', ('Return value description should finish with "."',)), ('BadReturns', 'named_single_return', ('The first line of the Returns section should contain only the ' 'type, unless multiple values are being returned',)), ('BadReturns', 'no_capitalization', ('Return value description should start with a capital ' 'letter',)), ('BadReturns', 'no_period_multi', ('Return value description should finish with "."',)), # Examples tests ('BadGenericDocStrings', 'method', ('Do not import numpy, as it is imported automatically',)), ('BadGenericDocStrings', 'method', ('Do not import pandas, as it is imported automatically',)), ('BadGenericDocStrings', 'method_wo_docstrings', ("The object does not have a docstring",)), # See Also tests ('BadSeeAlso', 'prefix_pandas', ('pandas.Series.rename in `See Also` section ' 'does not need `pandas` prefix',)), # Examples tests ('BadExamples', 'unused_import', ("flake8 error: F401 'pandas as pdf' imported but unused",)), ('BadExamples', 'indentation_is_not_a_multiple_of_four', ('flake8 error: E111 indentation is not a multiple of four',)), ('BadExamples', 'missing_whitespace_around_arithmetic_operator', ('flake8 error: ' 'E226 missing whitespace around arithmetic operator',)), ('BadExamples', 'missing_whitespace_after_comma', ("flake8 error: E231 missing whitespace after ',' (3 times)",)), ('BadGenericDocStrings', 'two_linebreaks_between_sections', ('Double line break found; please use only one blank line to ' 'separate sections or paragraphs, and do not leave blank lines ' 'at the end of docstrings',)), ('BadGenericDocStrings', 'linebreak_at_end_of_docstring', ('Double line break found; please use only one blank line to ' 'separate sections or paragraphs, and do not leave blank lines ' 'at the end of docstrings',)), ]) def test_bad_docstrings(self, capsys, klass, func, msgs): result = validate_one(self._import_path(klass=klass, func=func)) for msg in msgs: assert msg in ' '.join(err[1] for err in result['errors']) def test_validate_all_ignore_deprecated(self, monkeypatch): monkeypatch.setattr( validate_docstrings, 'validate_one', lambda func_name: { 'docstring': 'docstring1', 'errors': [('ER01', 'err desc'), ('ER02', 'err desc'), ('ER03', 'err desc')], 'warnings': [], 'examples_errors': '', 'deprecated': True}) result = validate_docstrings.validate_all(prefix=None, ignore_deprecated=True) assert len(result) == 0 class TestApiItems(object): @property def api_doc(self): return io.StringIO(textwrap.dedent(''' .. currentmodule:: itertools Itertools --------- Infinite ~~~~~~~~ .. autosummary:: cycle count Finite ~~~~~~ .. autosummary:: chain .. currentmodule:: random Random ------ All ~~~ .. autosummary:: seed 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')]) 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')]) 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')]) def test_item_subsection(self, idx, subsection): result = list(validate_docstrings.get_api_items(self.api_doc)) assert result[idx][3] == subsection class TestMainFunction(object): def test_exit_status_for_validate_one(self, monkeypatch): monkeypatch.setattr( validate_docstrings, 'validate_one', lambda func_name: { 'docstring': 'docstring1', 'errors': [('ER01', 'err desc'), ('ER02', 'err desc'), ('ER03', 'err desc')], 'warnings': [], 'examples_errors': ''}) exit_status = validate_docstrings.main(func_name='docstring1', prefix=None, errors=[], output_format='default', ignore_deprecated=False) assert exit_status == 0 def test_exit_status_errors_for_validate_all(self, monkeypatch): monkeypatch.setattr( validate_docstrings, 'validate_all', lambda prefix, ignore_deprecated=False: { 'docstring1': {'errors': [('ER01', 'err desc'), ('ER02', 'err desc'), ('ER03', 'err desc')], 'file': 'module1.py', 'file_line': 23}, 'docstring2': {'errors': [('ER04', 'err desc'), ('ER05', 'err desc')], 'file': 'module2.py', 'file_line': 925}}) exit_status = validate_docstrings.main(func_name=None, prefix=None, errors=[], output_format='default', ignore_deprecated=False) assert exit_status == 5 def test_no_exit_status_noerrors_for_validate_all(self, monkeypatch): monkeypatch.setattr( validate_docstrings, 'validate_all', lambda prefix, ignore_deprecated=False: { 'docstring1': {'errors': [], 'warnings': [('WN01', 'warn desc')]}, 'docstring2': {'errors': []}}) exit_status = validate_docstrings.main(func_name=None, prefix=None, errors=[], output_format='default', ignore_deprecated=False) assert exit_status == 0 def test_exit_status_for_validate_all_json(self, monkeypatch): print('EXECUTED') monkeypatch.setattr( validate_docstrings, 'validate_all', lambda prefix, ignore_deprecated=False: { 'docstring1': {'errors': [('ER01', 'err desc'), ('ER02', 'err desc'), ('ER03', 'err desc')]}, 'docstring2': {'errors': [('ER04', 'err desc'), ('ER05', 'err desc')]}}) exit_status = validate_docstrings.main(func_name=None, prefix=None, errors=[], output_format='json', ignore_deprecated=False) assert exit_status == 0 def test_errors_param_filters_errors(self, monkeypatch): monkeypatch.setattr( validate_docstrings, 'validate_all', lambda prefix, ignore_deprecated=False: { 'Series.foo': {'errors': [('ER01', 'err desc'), ('ER02', 'err desc'), ('ER03', 'err desc')], 'file': 'series.py', 'file_line': 142}, 'DataFrame.bar': {'errors': [('ER01', 'err desc'), ('ER02', 'err desc')], 'file': 'frame.py', 'file_line': 598}, 'Series.foobar': {'errors': [('ER01', 'err desc')], 'file': 'series.py', 'file_line': 279}}) exit_status = validate_docstrings.main(func_name=None, prefix=None, errors=['ER01'], output_format='default', ignore_deprecated=False) assert exit_status == 3 exit_status = validate_docstrings.main(func_name=None, prefix=None, errors=['ER03'], output_format='default', ignore_deprecated=False) assert exit_status == 1