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

Commit f7c12a8

Browse files
committed
Merge pull request #165 from Nurdok/D403
Added D403: First word of first line should be properly capitalized
2 parents c3f3757 + 68a7171 commit f7c12a8

File tree

6 files changed

+97
-24
lines changed

6 files changed

+97
-24
lines changed

docs/release_notes.rst

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ New Features
1313
classes are considered public if their names are not prepended with an
1414
underscore and if their parent class is public, recursively (#13, #146).
1515

16+
* Added the D403 error code - "First word of the first line should be
17+
properly capitalized". This new error is turned on by default (#164, #165).
1618

1719
Bug Fixes
1820

19-
* Fixed an issue where a `NameError` was raised when parsing complex defintions
20-
of `__all__` (#142, #143).
21+
* Fixed an issue where a ``NameError`` was raised when parsing complex defintions
22+
of ``__all__`` (#142, #143).
23+
24+
* Fixed a bug where D202 was falsely reported when a function with just a
25+
docstring and no content was followed by a comment (#165).
2126

2227

2328
0.7.0 - October 9th, 2015
@@ -26,21 +31,21 @@ Bug Fixes
2631
New Features
2732

2833
* Added the D104 error code - "Missing docstring in public package". This new
29-
error is turned on by default. Missing docstring in `__init__.py` files which
34+
error is turned on by default. Missing docstring in ``__init__.py`` files which
3035
previously resulted in D100 errors ("Missing docstring in public module")
3136
will now result in D104 (#105, #127).
3237

3338
* Added the D105 error code - "Missing docstring in magic method'. This new
3439
error is turned on by default. Missing docstrings in magic method which
3540
previously resulted in D102 error ("Missing docstring in public method")
3641
will now result in D105. Note that exceptions to this rule are variadic
37-
magic methods - specifically `__init__`, `__call__` and `__new__`, which
42+
magic methods - specifically ``__init__``, ``__call__`` and ``__new__``, which
3843
will be considered non-magic and missing docstrings in them will result
3944
in D102 (#60, #139).
4045

4146
* Support the option to exclude all error codes. Running pep257 with
42-
`--select=` (or `select=` in the configuration file) will exclude all errors
43-
which could then be added one by one using `add-select`. Useful for projects
47+
``--select=`` (or ``select=`` in the configuration file) will exclude all errors
48+
which could then be added one by one using ``add-select``. Useful for projects
4449
new to pep257 (#132, #135).
4550

4651
* Added check D211: No blank lines allowed before class docstring. This change

src/pep257.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import os
1717
import sys
18+
import ast
1819
import copy
1920
import logging
2021
import tokenize as tk
@@ -118,7 +119,6 @@ class Definition(Value):
118119
module = property(lambda self: self.parent.module)
119120
all = property(lambda self: self.module.all)
120121
_slice = property(lambda self: slice(self.start - 1, self.end))
121-
source = property(lambda self: ''.join(self._source[self._slice]))
122122
is_class = False
123123

124124
def __iter__(self):
@@ -128,6 +128,17 @@ def __iter__(self):
128128
def _publicity(self):
129129
return {True: 'public', False: 'private'}[self.is_public]
130130

131+
@property
132+
def source(self):
133+
"""Return the source code for the definition."""
134+
full_src = self._source[self._slice]
135+
136+
def is_empty_or_comment(line):
137+
return line.strip() == '' or line.strip().startswith('#')
138+
139+
filtered_src = dropwhile(is_empty_or_comment, reversed(full_src))
140+
return ''.join(reversed(list(filtered_src)))
141+
131142
def __str__(self):
132143
return 'in %s %s `%s`' % (self._publicity, self._human, self.name)
133144

@@ -429,7 +440,7 @@ def parse_module(self):
429440
return module
430441

431442
def parse_definition(self, class_):
432-
"""Parse a defintion and return its value in a `class_` object."""
443+
"""Parse a definition and return its value in a `class_` object."""
433444
start = self.line
434445
self.consume(tk.NAME)
435446
name = self.current.value
@@ -456,9 +467,9 @@ def parse_definition(self, class_):
456467
docstring = self.parse_docstring()
457468
decorators = self._accumulated_decorators
458469
self._accumulated_decorators = []
459-
log.debug("parsing nested defintions.")
470+
log.debug("parsing nested definitions.")
460471
children = list(self.parse_definitions(class_))
461-
log.debug("finished parsing nested defintions for '%s'", name)
472+
log.debug("finished parsing nested definitions for '%s'", name)
462473
end = self.line - 1
463474
else: # one-liner definition
464475
docstring = self.parse_docstring()
@@ -682,6 +693,8 @@ def to_rst(cls):
682693
'%r, not %r')
683694
D402 = D4xx.create_error('D402', 'First line should not be the function\'s '
684695
'"signature"')
696+
D403 = D4xx.create_error('D403', 'First word of the first line should be '
697+
'properly capitalized', '%r, not %r')
685698

686699

687700
class AttrDict(dict):
@@ -1361,7 +1374,7 @@ def check_docstring_missing(self, definition, docstring):
13611374
13621375
"""
13631376
if (not docstring and definition.is_public or
1364-
docstring and is_blank(eval(docstring))):
1377+
docstring and is_blank(ast.literal_eval(docstring))):
13651378
codes = {Module: D100, Class: D101, NestedClass: D101,
13661379
Method: (lambda: D105() if is_magic(definition.name)
13671380
else D102()),
@@ -1377,7 +1390,7 @@ def check_one_liners(self, definition, docstring):
13771390
13781391
"""
13791392
if docstring:
1380-
lines = eval(docstring).split('\n')
1393+
lines = ast.literal_eval(docstring).split('\n')
13811394
if len(lines) > 1:
13821395
non_empty_lines = sum(1 for l in lines if not is_blank(l))
13831396
if non_empty_lines == 1:
@@ -1390,7 +1403,6 @@ def check_no_blank_before(self, function, docstring): # def
13901403
There's no blank line either before or after the docstring.
13911404
13921405
"""
1393-
# NOTE: This does not take comments into account.
13941406
# NOTE: This does not take into account functions with groups of code.
13951407
if docstring:
13961408
before, _, after = function.source.partition(docstring)
@@ -1448,7 +1460,7 @@ def check_blank_after_summary(self, definition, docstring):
14481460
14491461
"""
14501462
if docstring:
1451-
lines = eval(docstring).strip().split('\n')
1463+
lines = ast.literal_eval(docstring).strip().split('\n')
14521464
if len(lines) > 1:
14531465
post_summary_blanks = list(map(is_blank, lines[1:]))
14541466
blanks_count = sum(takewhile(bool, post_summary_blanks))
@@ -1487,7 +1499,8 @@ def check_newline_after_last_paragraph(self, definition, docstring):
14871499
14881500
"""
14891501
if docstring:
1490-
lines = [l for l in eval(docstring).split('\n') if not is_blank(l)]
1502+
lines = [l for l in ast.literal_eval(docstring).split('\n')
1503+
if not is_blank(l)]
14911504
if len(lines) > 1:
14921505
if docstring.split("\n")[-1].strip() not in ['"""', "'''"]:
14931506
return D209()
@@ -1496,7 +1509,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
14961509
def check_surrounding_whitespaces(self, definition, docstring):
14971510
"""D210: No whitespaces allowed surrounding docstring text."""
14981511
if docstring:
1499-
lines = eval(docstring).split('\n')
1512+
lines = ast.literal_eval(docstring).split('\n')
15001513
if lines[0].startswith(' ') or \
15011514
len(lines) == 1 and lines[0].endswith(' '):
15021515
return D210()
@@ -1514,8 +1527,8 @@ def check_triple_double_quotes(self, definition, docstring):
15141527
""" quotes in its body.
15151528
15161529
'''
1517-
if docstring and '"""' in eval(docstring) and docstring.startswith(
1518-
("'''", "r'''", "u'''", "ur'''")):
1530+
if (docstring and '"""' in ast.literal_eval(docstring) and
1531+
docstring.startswith(("'''", "r'''", "u'''", "ur'''"))):
15191532
# Allow ''' quotes if docstring contains """, because otherwise """
15201533
# quotes could not be expressed inside docstring. Not in PEP 257.
15211534
return
@@ -1563,7 +1576,7 @@ def check_ends_with_period(self, definition, docstring):
15631576
15641577
"""
15651578
if docstring:
1566-
summary_line = eval(docstring).strip().split('\n')[0]
1579+
summary_line = ast.literal_eval(docstring).strip().split('\n')[0]
15671580
if not summary_line.endswith('.'):
15681581
return D400(summary_line[-1])
15691582

@@ -1577,7 +1590,7 @@ def check_imperative_mood(self, function, docstring): # def context
15771590
15781591
"""
15791592
if docstring:
1580-
stripped = eval(docstring).strip()
1593+
stripped = ast.literal_eval(docstring).strip()
15811594
if stripped:
15821595
first_word = stripped.split()[0]
15831596
if first_word.endswith('s') and not first_word.endswith('ss'):
@@ -1592,10 +1605,22 @@ def check_no_signature(self, function, docstring): # def context
15921605
15931606
"""
15941607
if docstring:
1595-
first_line = eval(docstring).strip().split('\n')[0]
1608+
first_line = ast.literal_eval(docstring).strip().split('\n')[0]
15961609
if function.name + '(' in first_line.replace(' ', ''):
15971610
return D402()
15981611

1612+
@check_for(Function)
1613+
def check_capitalized(self, function, docstring):
1614+
"""D403: First word of the first line should be properly capitalized.
1615+
1616+
The [first line of a] docstring is a phrase ending in a period.
1617+
1618+
"""
1619+
if docstring:
1620+
first_word = ast.literal_eval(docstring).split()[0]
1621+
if first_word != first_word.capitalize():
1622+
return D403(first_word.capitalize(), first_word)
1623+
15991624
# Somewhat hard to determine if return value is mentioned.
16001625
# @check(Function)
16011626
def SKIP_check_return_type(self, function, docstring):
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""A valid module docstring."""
2+
3+
from .expected import Expectation
4+
5+
expectation = Expectation()
6+
expect = expectation.expect
7+
8+
9+
@expect("D403: First word of the first line should be properly capitalized "
10+
"('Do', not 'do')")
11+
def not_capitalized():
12+
"""do something."""
13+
14+
15+
# Make sure empty docstrings don't generate capitalization errors.
16+
@expect("D103: Missing docstring in public function")
17+
def empty_docstring():
18+
""""""
19+
20+
21+
@expect("D403: First word of the first line should be properly capitalized "
22+
"('Get', not 'GET')")
23+
def all_caps():
24+
"""GET the request."""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Check for a bug in parsing comments after definitions."""
2+
3+
from .expected import Expectation
4+
5+
expectation = Expectation()
6+
expect = expectation.expect
7+
8+
9+
def should_be_ok():
10+
"""Just a function without violations."""
11+
12+
13+
# This is a comment that triggers a bug that causes the previous function
14+
# to generate a D202 error.

src/tests/test_definitions.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,19 @@ def test_token_stream():
255255

256256
def test_pep257():
257257
"""Run domain-specific tests from test.py file."""
258-
test_cases = ('test', 'unicode_literals', 'nested_class')
258+
test_cases = (
259+
'test',
260+
'unicode_literals',
261+
'nested_class',
262+
'capitalization',
263+
'comment_after_def_bug',
264+
)
259265
for test_case in test_cases:
260266
case_module = __import__('test_cases.{0}'.format(test_case),
261267
globals=globals(),
262268
locals=locals(),
263269
fromlist=['expectation'],
264270
level=1)
265-
# from .test_cases import test
266271
results = list(check([os.path.join(os.path.dirname(__file__),
267272
'test_cases', test_case + '.py')],
268273
select=set(ErrorRegistry.get_error_codes())))

src/tests/test_pep257.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def function_with_bad_docstring(foo):
135135
return foo
136136
''')
137137
expected_error_codes = set(('D100', 'D400', 'D401', 'D205', 'D209',
138-
'D210'))
138+
'D210', 'D403'))
139139
mock_open = mock.mock_open(read_data=function_to_check)
140140
from .. import pep257
141141
with mock.patch.object(pep257, 'tokenize_open', mock_open, create=True):

0 commit comments

Comments
 (0)