Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit 8afde1b

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/numpy-conventions' into PyCQA/master
2 parents 2748636 + 01f8ac4 commit 8afde1b

File tree

3 files changed

+223
-19
lines changed

3 files changed

+223
-19
lines changed

src/pydocstyle.py

Lines changed: 212 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from re import compile as re
1919
import itertools
2020
from collections import defaultdict, namedtuple, Set
21+
from functools import partial
2122

2223
try: # Python 3.x
2324
from ConfigParser import RawConfigParser
@@ -119,6 +120,7 @@ class Definition(Value):
119120
all = property(lambda self: self.module.all)
120121
_slice = property(lambda self: slice(self.start - 1, self.end))
121122
is_class = False
123+
is_function = False
122124

123125
def __iter__(self):
124126
return chain([self], *self.children)
@@ -161,6 +163,9 @@ class Package(Module):
161163

162164
class Function(Definition):
163165

166+
is_function = True
167+
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
168+
'children', 'parent', 'parameters')
164169
_nest = staticmethod(lambda s: {'def': NestedFunction,
165170
'class': NestedClass}[s])
166171

@@ -448,6 +453,8 @@ def parse_definition(self, class_):
448453
self.stream.move()
449454
if self.current.kind == tk.OP and self.current.value == '(':
450455
parenthesis_level = 0
456+
arguments = []
457+
is_default_assignment = False
451458
while True:
452459
if self.current.kind == tk.OP:
453460
if self.current.value == '(':
@@ -456,6 +463,14 @@ def parse_definition(self, class_):
456463
parenthesis_level -= 1
457464
if parenthesis_level == 0:
458465
break
466+
elif self.current.value == '=':
467+
is_default_assignment = True
468+
elif self.current.kind == tk.NAME:
469+
if is_default_assignment:
470+
is_default_assignment = False
471+
else:
472+
arguments.append(self.current.value)
473+
459474
self.stream.move()
460475
if self.current.kind != tk.OP or self.current.value != ':':
461476
self.leapfrog(tk.OP, value=":")
@@ -477,8 +492,11 @@ def parse_definition(self, class_):
477492
children = []
478493
end = self.line
479494
self.leapfrog(tk.NEWLINE)
480-
definition = class_(name, self.source, start, end,
481-
decorators, docstring, children, None)
495+
496+
creator = partial(class_, name, self.source, start, end,
497+
decorators, docstring, children, None)
498+
499+
definition = creator(arguments) if class_.is_function else creator()
482500
for child in definition.children:
483501
child.parent = definition
484502
log.debug("finished parsing %s '%s'. Next token is %r (%s)",
@@ -683,6 +701,8 @@ def to_rst(cls):
683701
'at the first line')
684702
D213 = D2xx.create_error('D213', 'Multi-line docstring summary should start '
685703
'at the second line')
704+
D214 = D2xx.create_error('D214', 'Section or section underline is '
705+
'over-indented', 'in section %r')
686706

687707
D3xx = ErrorRegistry.create_group('D3', 'Quotes Issues')
688708
D300 = D3xx.create_error('D300', 'Use """triple double quotes"""',
@@ -701,6 +721,12 @@ def to_rst(cls):
701721
'properly capitalized', '%r, not %r')
702722
D404 = D4xx.create_error('D404', 'First word of the docstring should not '
703723
'be `This`')
724+
D405 = D4xx.create_error('D405', 'Section name should be properly capitalized',
725+
'%r, not %r')
726+
D406 = D4xx.create_error('D406', 'Section name should not end with a colon',
727+
'%r, not %r')
728+
D407 = D4xx.create_error('D407', 'Section underline should match the length '
729+
'of the section\'s name', 'len(%r) == %r')
704730

705731

706732
class AttrDict(dict):
@@ -709,10 +735,12 @@ def __getattr__(self, item):
709735

710736

711737
conventions = AttrDict({
712-
'pep257': set(ErrorRegistry.get_error_codes()) - set(['D203',
713-
'D212',
714-
'D213',
715-
'D404'])
738+
'pep257': set(ErrorRegistry.get_error_codes()) - set(['D203', 'D212',
739+
'D213', 'D214',
740+
'D404', 'D405',
741+
'D406', 'D407']),
742+
'numpy': set(ErrorRegistry.get_error_codes()) - set(['D203', 'D212',
743+
'D213', 'D402'])
716744
})
717745

718746

@@ -1264,7 +1292,7 @@ def check(filenames, select=None, ignore=None):
12641292
try:
12651293
with tokenize_open(filename) as file:
12661294
source = file.read()
1267-
for error in PEP257Checker().check_source(source, filename):
1295+
for error in ConventionChecker().check_source(source, filename):
12681296
code = getattr(error, 'code', None)
12691297
if code in checked_codes:
12701298
yield error
@@ -1354,7 +1382,7 @@ def decorator(f):
13541382
return decorator
13551383

13561384

1357-
class PEP257Checker(object):
1385+
class ConventionChecker(object):
13581386
"""Checker for PEP 257.
13591387
13601388
D10x: Missing docstrings
@@ -1364,13 +1392,27 @@ class PEP257Checker(object):
13641392
13651393
"""
13661394

1395+
ALL_NUMPY_SECTIONS = ['Short Summary',
1396+
'Extended Summary',
1397+
'Parameters',
1398+
'Returns',
1399+
'Yields',
1400+
'Other Parameters',
1401+
'Raises',
1402+
'See Also',
1403+
'Notes',
1404+
'References',
1405+
'Examples',
1406+
'Attributes',
1407+
'Methods']
1408+
13671409
def check_source(self, source, filename):
13681410
module = parse(StringIO(source), filename)
13691411
for definition in module:
13701412
for check in self.checks:
13711413
terminate = False
13721414
if isinstance(definition, check._check_for):
1373-
error = check(None, definition, definition.docstring)
1415+
error = check(self, definition, definition.docstring)
13741416
errors = error if hasattr(error, '__iter__') else [error]
13751417
for error in errors:
13761418
if error is not None:
@@ -1498,6 +1540,13 @@ def check_blank_after_summary(self, definition, docstring):
14981540
if blanks_count != 1:
14991541
return D205(blanks_count)
15001542

1543+
@staticmethod
1544+
def _get_docstring_indent(definition, docstring):
1545+
"""Return the indentation of the docstring's opening quotes."""
1546+
before_docstring, _, _ = definition.source.partition(docstring)
1547+
_, _, indent = before_docstring.rpartition('\n')
1548+
return indent
1549+
15011550
@check_for(Definition)
15021551
def check_indent(self, definition, docstring):
15031552
"""D20{6,7,8}: The entire docstring should be indented same as code.
@@ -1507,8 +1556,7 @@ def check_indent(self, definition, docstring):
15071556
15081557
"""
15091558
if docstring:
1510-
before_docstring, _, _ = definition.source.partition(docstring)
1511-
_, _, indent = before_docstring.rpartition('\n')
1559+
indent = self._get_docstring_indent(definition, docstring)
15121560
lines = docstring.split('\n')
15131561
if len(lines) > 1:
15141562
lines = lines[1:] # First line does not need indent.
@@ -1709,6 +1757,145 @@ def SKIP_check_return_type(self, function, docstring):
17091757
if 'return' not in docstring.lower():
17101758
return Error()
17111759

1760+
@check_for(Definition)
1761+
def check_numpy_content(self, definition, docstring):
1762+
"""Check the content of the docstring for numpy conventions."""
1763+
pass
1764+
1765+
def check_numpy_parameters(self, section, content, definition, docstring):
1766+
print "LALALAL"
1767+
yield
1768+
1769+
def _check_numpy_section(self, section, content, definition, docstring):
1770+
"""Check the content of the docstring for numpy conventions."""
1771+
method_name = "check_numpy_%s" % section
1772+
if hasattr(self, method_name):
1773+
gen_func = getattr(self, method_name)
1774+
1775+
for err in gen_func(section, content, definition, docstring):
1776+
yield err
1777+
else:
1778+
print "Now checking numpy section %s" % section
1779+
for l in content:
1780+
print "##", l
1781+
1782+
1783+
@check_for(Definition)
1784+
def check_numpy(self, definition, docstring):
1785+
"""Parse the general structure of a numpy docstring and check it."""
1786+
if not docstring:
1787+
return
1788+
1789+
lines = docstring.split("\n")
1790+
if len(lines) < 2:
1791+
# It's not a multiple lined docstring
1792+
return
1793+
1794+
lines_generator = ScrollableGenerator(lines[1:]) # Skipping first line
1795+
indent = self._get_docstring_indent(definition, docstring)
1796+
1797+
current_section = None
1798+
curr_section_lines = []
1799+
start_collecting_lines = False
1800+
1801+
for line in lines_generator:
1802+
for section in self.ALL_NUMPY_SECTIONS:
1803+
with_colon = section.lower() + ':'
1804+
if line.strip().lower() in [section.lower(), with_colon]:
1805+
# There's a chance that this line is a numpy section
1806+
try:
1807+
next_line = lines_generator.next()
1808+
except StopIteration:
1809+
# It probably isn't :)
1810+
return
1811+
1812+
if ''.join(set(next_line.strip())) == '-':
1813+
# The next line contains only dashes, there's a good
1814+
# chance that it's a numpy section
1815+
1816+
if (leading_space(line) > indent or
1817+
leading_space(next_line) > indent):
1818+
yield D214(section)
1819+
1820+
if section not in line:
1821+
# The capitalized section string is not in the line,
1822+
# meaning that the word appears there but not
1823+
# properly capitalized.
1824+
yield D405(section, line.strip())
1825+
elif line.strip().lower() == with_colon:
1826+
# The section name should not end with a colon.
1827+
yield D406(section, line.strip())
1828+
1829+
if next_line.strip() != "-" * len(section):
1830+
# The length of the underlining dashes does not
1831+
# match the length of the section name.
1832+
yield D407(section, len(section))
1833+
1834+
# At this point, we're done with the structured part of
1835+
# the section and its underline.
1836+
# We will not collect the content of each section and
1837+
# let section handlers deal with it.
1838+
1839+
if current_section is not None:
1840+
for err in self._check_numpy_section(
1841+
current_section,
1842+
curr_section_lines,
1843+
definition,
1844+
docstring):
1845+
yield err
1846+
1847+
start_collecting_lines = True
1848+
current_section = section.lower()
1849+
curr_section_lines = []
1850+
else:
1851+
# The next line does not contain only dashes, so it's
1852+
# not likely to be a section header.
1853+
lines_generator.scroll_back()
1854+
1855+
if current_section is not None:
1856+
if start_collecting_lines:
1857+
start_collecting_lines = False
1858+
else:
1859+
curr_section_lines.append(line)
1860+
1861+
if current_section is not None:
1862+
for err in self._check_numpy_section(current_section,
1863+
curr_section_lines,
1864+
definition,
1865+
docstring):
1866+
yield err
1867+
1868+
1869+
class ScrollableGenerator(object):
1870+
"""A generator over a list that can be moved back during iteration."""
1871+
1872+
def __init__(self, list_like):
1873+
self._list_like = list_like
1874+
self._index = 0
1875+
1876+
def __iter__(self):
1877+
return self
1878+
1879+
def next(self):
1880+
"""Generate the next item or raise StopIteration."""
1881+
try:
1882+
return self._list_like[self._index]
1883+
except IndexError:
1884+
raise StopIteration()
1885+
finally:
1886+
self._index += 1
1887+
1888+
def scroll_back(self, num=1):
1889+
"""Move the generator `num` items backwards."""
1890+
if num < 0:
1891+
raise ValueError('num cannot be a negative number')
1892+
self._index = max(0, self._index - num)
1893+
1894+
def clone(self):
1895+
"""Return a copy of the generator set to the same item index."""
1896+
obj_copy = self.__class__(self._list_like)
1897+
obj_copy._index = self._index
1898+
17121899

17131900
def main(use_pep257=False):
17141901
try:
@@ -1721,5 +1908,19 @@ def main_pep257():
17211908
main(use_pep257=True)
17221909

17231910

1911+
def foo():
1912+
"""A.
1913+
1914+
Parameters
1915+
----------
1916+
1917+
This is a string that defines some things.
1918+
1919+
Returns
1920+
-------
1921+
1922+
Nothing.
1923+
"""
1924+
17241925
if __name__ == '__main__':
17251926
main()

src/tests/test_definitions.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class class_(object):
2424
"""Class."""
2525
def method_1(self):
2626
"""Method."""
27-
def method_2(self):
27+
def method_2(self,
28+
param=None):
2829
def nested_3(self):
2930
"""Nested."""
3031
'''
@@ -142,25 +143,26 @@ def test_parser():
142143

143144
function, class_ = module.children
144145
assert Function('function', _, _, _, _, '"Function."', _,
145-
module) == function
146+
module, []) == function
146147
assert Class('class_', _, _, _, _, '"""Class."""', _, module) == class_
147148

148149
nested_1, nested_2 = function.children
149150
assert NestedFunction('nested_1', _, _, _, _,
150-
'"""Nested."""', _, function) == nested_1
151+
'"""Nested."""', _, function, []) == nested_1
151152
assert NestedFunction('nested_2', _, _, _, _, None, _,
152-
function) == nested_2
153+
function, []) == nested_2
153154
assert nested_1.is_public is False
154155

155156
method_1, method_2 = class_.children
156157
assert method_1.parent == method_2.parent == class_
157158
assert Method('method_1', _, _, _, _, '"""Method."""', _,
158-
class_) == method_1
159-
assert Method('method_2', _, _, _, _, None, _, class_) == method_2
159+
class_, ['self']) == method_1
160+
assert Method('method_2', _, _, _, _, None, _,
161+
class_, ['self', 'param']) == method_2
160162

161163
nested_3, = method_2.children
162164
assert NestedFunction('nested_3', _, _, _, _,
163-
'"""Nested."""', _, method_2) == nested_3
165+
'"""Nested."""', _, method_2, ['self']) == nested_3
164166
assert nested_3.module == module
165167
assert nested_3.all == dunder_all
166168

src/tests/test_integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ def test_illegal_convention(env):
358358
out, err, code = env.invoke('--convention=illegal_conv')
359359
assert code == 2
360360
assert "Illegal convention 'illegal_conv'." in err
361-
assert 'Possible conventions: pep257' in err
361+
assert 'Possible conventions' in err
362+
assert 'pep257' in err
362363

363364

364365
def test_empty_select_cli(env):

0 commit comments

Comments
 (0)