Skip to content

Commit eac1caf

Browse files
committed
PyCQA#129 - Check section headers capitalization and underline
1 parent e9aab69 commit eac1caf

File tree

1 file changed

+104
-1
lines changed

1 file changed

+104
-1
lines changed

src/pydocstyle.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,10 @@ def to_rst(cls):
717717
'"signature"')
718718
D403 = D4xx.create_error('D403', 'First word of the first line should be '
719719
'properly capitalized', '%r, not %r')
720-
720+
D404 = D4xx.create_error('D404', 'Section name should be properly capitalized',
721+
'%r, not %r')
722+
D405 = D4xx.create_error('D405', 'Section underline should match the length of '
723+
'the section\'s name', 'len(%r) == %r, not %r')
721724

722725
class AttrDict(dict):
723726
def __getattr__(self, item):
@@ -1711,6 +1714,100 @@ def SKIP_check_return_type(self, function, docstring):
17111714
if 'return' not in docstring.lower():
17121715
return Error()
17131716

1717+
@check_for(Function)
1718+
def check_numpy(self, function, docstring):
1719+
"""D403: First word of the first line should be properly capitalized.
1720+
1721+
The [first line of a] docstring is a phrase ending in a period.
1722+
1723+
"""
1724+
SECTIONS = ['Summary',
1725+
'Extended Summary',
1726+
'Parameters',
1727+
'Returns',
1728+
'Yields',
1729+
'Raises',
1730+
'Other Parameters',
1731+
'See Also',
1732+
'Notes',
1733+
'References',
1734+
'Examples']
1735+
1736+
if not docstring:
1737+
return
1738+
1739+
ds = DocstringStream(docstring)
1740+
if ds.line_number < 2:
1741+
return
1742+
1743+
_ = ds.consume_line() # Skipping the first line
1744+
curr_line = ds.consume_line()
1745+
1746+
while curr_line is not None:
1747+
for section in SECTIONS:
1748+
if section.lower() == curr_line.strip().lower():
1749+
if len(curr_line) > len(curr_line.lstrip()):
1750+
return D208()
1751+
if section not in curr_line:
1752+
return D404(section, curr_line.strip())
1753+
1754+
curr_line = ds.consume_line()
1755+
if curr_line.rstrip() != "-" * len(section):
1756+
return D405(section, len(section),
1757+
len(curr_line.rstrip()))
1758+
curr_line = ds.consume_line()
1759+
1760+
1761+
class DocstringStream(object):
1762+
"""Reads numpy conventions."""
1763+
1764+
def __init__(self, docstring):
1765+
self._lines = ast.literal_eval(docstring).split('\n')
1766+
self._base_indent = self._find_indent_level(docstring)
1767+
self._line_index = 0
1768+
1769+
self._handlers = {'parameters': self._consume_parameters_section}
1770+
self.line_number = len(self._lines)
1771+
1772+
def consume_line(self):
1773+
if self._line_index >= len(self._lines):
1774+
return None
1775+
try:
1776+
return self.peek_current_line()
1777+
finally:
1778+
self._line_index += 1
1779+
1780+
def peek_current_line(self):
1781+
# First line is not indented
1782+
if self._line_index == 0:
1783+
return self._lines[self._line_index]
1784+
1785+
return self._lines[self._line_index][self._base_indent:]
1786+
1787+
def peek_next_line(self):
1788+
if self._line_index + 1 >= self.line_number:
1789+
return None
1790+
1791+
return self._lines[self._line_index + 1][self._base_indent:]
1792+
1793+
def _verify_section_header(self, section_name):
1794+
curr_line = self.peek_current_line()
1795+
1796+
def _consume_parameters_section(self):
1797+
pass
1798+
1799+
1800+
@staticmethod
1801+
def _find_indent_level(docstring):
1802+
lines = docstring.split('\n')
1803+
if len(lines) > 1:
1804+
last_line = lines[-1]
1805+
if last_line.endswith('"""'):
1806+
return last_line.find('"""')
1807+
else:
1808+
return last_line.find("'''")
1809+
return 0
1810+
17141811

17151812
def main(use_pep257=False):
17161813
try:
@@ -1722,6 +1819,12 @@ def main(use_pep257=False):
17221819
def main_pep257():
17231820
main(use_pep257=True)
17241821

1822+
def foo():
1823+
"""A.
1824+
1825+
Parameters
1826+
---------
1827+
"""
17251828

17261829
if __name__ == '__main__':
17271830
main()

0 commit comments

Comments
 (0)