Skip to content

Commit 0a2f3ef

Browse files
authored
D401: Allow multiple imperative forms of the same stem. (PyCQA#382)
* Allow multiple imperative forms of the same stem. * Update phrasing of violation. * Select best imperative by longest prefix match * Fixed test after merge. * Added line to release notes. * Code review fixes.
1 parent cc5a96b commit 0a2f3ef

File tree

9 files changed

+93
-14
lines changed

9 files changed

+93
-14
lines changed

docs/release_notes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Release Notes
44
**pydocstyle** version numbers follow the
55
`Semantic Versioning <http://semver.org/>`_ specification.
66

7+
Current Development Version
8+
---------------------------
9+
10+
Bug Fixes
11+
12+
* D401: Fixed a false positive where one stem had multiple imperative forms,
13+
e.g., init and initialize / initiate (#382).
14+
715
4.0.0 - July 6th, 2019
816
---------------------------
917

src/pydocstyle/checker.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .parser import (Package, Module, Class, NestedClass, Definition, AllError,
1414
Method, Function, NestedFunction, Parser, StringIO,
1515
ParseError)
16-
from .utils import log, is_blank, pairwise
16+
from .utils import log, is_blank, pairwise, common_prefix_length
1717
from .wordlists import IMPERATIVE_VERBS, IMPERATIVE_BLACKLIST, stem
1818

1919

@@ -440,16 +440,20 @@ def check_imperative_mood(self, function, docstring): # def context
440440
return violations.D401b(first_word)
441441

442442
try:
443-
correct_form = IMPERATIVE_VERBS.get(stem(check_word))
443+
correct_forms = IMPERATIVE_VERBS.get(stem(check_word))
444444
except UnicodeDecodeError:
445445
# This is raised when the docstring contains unicode
446446
# characters in the first word, but is not a unicode
447447
# string. In which case D302 will be reported. Ignoring.
448448
return
449449

450-
if correct_form and correct_form != check_word:
450+
if correct_forms and check_word not in correct_forms:
451+
best = max(
452+
correct_forms,
453+
key=lambda f: common_prefix_length(check_word, f)
454+
)
451455
return violations.D401(
452-
correct_form.capitalize(),
456+
best.capitalize(),
453457
first_word
454458
)
455459

src/pydocstyle/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,16 @@ def pairwise(
2525
a, b = tee(iterable)
2626
_ = next(b, default_value)
2727
return zip_longest(a, b, fillvalue=default_value)
28+
29+
30+
def common_prefix_length(a: str, b: str) -> int:
31+
"""Return the length of the longest common prefix of a and b.
32+
33+
>>> common_prefix_length('abcd', 'abce')
34+
3
35+
36+
"""
37+
for common, (ca, cb) in enumerate(zip(a, b)):
38+
if ca != cb:
39+
return common
40+
return min(len(a), len(b))

src/pydocstyle/violations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def create_error(
125125
self,
126126
error_code: str,
127127
error_desc: str,
128-
error_context: Optional[str]=None,
128+
error_context: Optional[str] = None,
129129
) -> Callable[[Iterable[str]], Error]:
130130
"""Create an error, register it to this group and return it."""
131131
# TODO: check prefix
@@ -219,7 +219,7 @@ def to_rst(cls) -> str:
219219
D400 = D4xx.create_error('D400', 'First line should end with a period',
220220
'not {0!r}')
221221
D401 = D4xx.create_error('D401', 'First line should be in imperative mood',
222-
"'{0}', not '{1}'")
222+
"perhaps '{0}', not '{1}'")
223223
D401b = D4xx.create_error('D401', 'First line should be in imperative mood; '
224224
'try rephrasing', "found '{0}'")
225225
D402 = D4xx.create_error('D402', 'First line should not be the function\'s '

src/pydocstyle/wordlists.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import re
88
import pkgutil
99
import snowballstemmer
10-
from typing import Iterator
10+
from typing import Iterator, Dict, Set
1111

1212

1313
#: Regular expression for stripping comments from the wordlists
@@ -36,7 +36,14 @@ def load_wordlist(name: str) -> Iterator[str]:
3636

3737

3838
#: A dict mapping stemmed verbs to the imperative form
39-
IMPERATIVE_VERBS = {stem(v): v for v in load_wordlist('imperatives.txt')}
39+
def make_imperative_verbs_dict(wordlist: Iterator[str]) -> Dict[str, Set[str]]:
40+
imperative_verbs = {} # type: Dict[str, Set[str]]
41+
for word in wordlist:
42+
imperative_verbs.setdefault(stem(word), set()).add(word)
43+
return imperative_verbs
44+
45+
46+
IMPERATIVE_VERBS = make_imperative_verbs_dict(load_wordlist('imperatives.txt'))
4047

4148
#: Words that are forbidden to appear as the first word in a docstring
4249
IMPERATIVE_BLACKLIST = set(load_wordlist('imperatives_blacklist.txt'))

src/tests/test_cases/noqa.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def docstring_bad_ignore_one(): # noqa: D400,D401,D415
1616
pass
1717

1818

19-
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
19+
@expect("D401: First line should be in imperative mood "
20+
"(perhaps 'Run', not 'Runs')")
2021
def docstring_ignore_some_violations_but_catch_D401(): # noqa: E501,D400,D415
2122
"""Runs something"""
2223
pass

src/tests/test_cases/sections.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def ignore_non_actual_section(): # noqa: D416
177177

178178
@expect(_D213)
179179
@expect("D401: First line should be in imperative mood "
180-
"('Return', not 'Returns')")
180+
"(perhaps 'Return', not 'Returns')")
181181
@expect("D400: First line should end with a period (not 's')")
182182
@expect("D415: First line should end with a period, question "
183183
"mark, or exclamation point (not 's')")

src/tests/test_cases/test.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ def lwnlkjl():
287287
"""Summary"""
288288

289289

290-
@expect("D401: First line should be in imperative mood ('Return', not "
291-
"'Returns')")
290+
@expect("D401: First line should be in imperative mood "
291+
"(perhaps 'Return', not 'Returns')")
292292
def liouiwnlkjl():
293293
"""Returns foo."""
294294

@@ -361,7 +361,8 @@ def inner_function():
361361

362362

363363
@expect("D400: First line should end with a period (not 'g')")
364-
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
364+
@expect("D401: First line should be in imperative mood "
365+
"(perhaps 'Run', not 'Runs')")
365366
@expect("D415: First line should end with a period, question mark, "
366367
"or exclamation point (not 'g')")
367368
def docstring_bad():
@@ -379,12 +380,29 @@ def docstring_bad_ignore_one(): # noqa: D400,D401,D415
379380
pass
380381

381382

382-
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
383+
@expect("D401: First line should be in imperative mood "
384+
"(perhaps 'Run', not 'Runs')")
383385
def docstring_ignore_some_violations_but_catch_D401(): # noqa: E501,D400,D415
384386
"""Runs something"""
385387
pass
386388

387389

390+
@expect(
391+
"D401: First line should be in imperative mood "
392+
"(perhaps 'Initiate', not 'Initiates')"
393+
)
394+
def docstring_initiates():
395+
"""Initiates the process."""
396+
397+
398+
@expect(
399+
"D401: First line should be in imperative mood "
400+
"(perhaps 'Initialize', not 'Initializes')"
401+
)
402+
def docstring_initializes():
403+
"""Initializes the process."""
404+
405+
388406
@wraps(docstring_bad_ignore_one)
389407
def bad_decorated_function():
390408
"""Bad (E501) but decorated"""

src/tests/test_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Unit test for pydocstyle utils.
2+
3+
Use tox or py.test to run the test suite.
4+
"""
5+
from pydocstyle import utils
6+
7+
8+
__all__ = ()
9+
10+
11+
def test_common_prefix():
12+
"""Test common prefix length of two strings."""
13+
assert utils.common_prefix_length('abcd', 'abce') == 3
14+
15+
16+
def test_no_common_prefix():
17+
"""Test common prefix length of two strings that have no common prefix."""
18+
assert utils.common_prefix_length('abcd', 'cdef') == 0
19+
20+
21+
def test_differ_length():
22+
"""Test common prefix length of two strings differing in length."""
23+
assert utils.common_prefix_length('abcd', 'ab') == 2
24+
25+
26+
def test_empty_string():
27+
"""Test common prefix length of two strings, one of them empty."""
28+
assert utils.common_prefix_length('abcd', '') == 0

0 commit comments

Comments
 (0)