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

Commit e1fd706

Browse files
authored
Merge pull request #187 from Nurdok/dunder_all_import_bugfix
Proper parsing of imports to avoid misunderstanding __all__ imports
2 parents 5410b35 + 1df1399 commit e1fd706

File tree

6 files changed

+76
-5
lines changed

6 files changed

+76
-5
lines changed

docs/release_notes.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ New Features
1313
the summary of a multi-line docstring starts at the first line,
1414
respectively at the second line (#174).
1515

16-
* Added D404 - First word of the docstring should not be `This`. It is turned
16+
* Added D404 - First word of the docstring should not be "This". It is turned
1717
off by default (#183).
1818

1919
Bug Fixes
2020

2121
* The error code D300 is now also being reported if a docstring has
2222
uppercase literals (``R`` or ``U``) as prefix (#176).
2323

24+
* Fixed a bug where an ``__all__`` error was reported when ``__all__`` was
25+
imported from another module with a different name (#182, #187).
26+
2427
1.0.0 - January 30th, 2016
2528
--------------------------
2629

src/pydocstyle.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import ast
1414
import copy
1515
import logging
16+
import textwrap
1617
import tokenize as tk
1718
from itertools import takewhile, dropwhile, chain
1819
from re import compile as re
@@ -486,20 +487,48 @@ def parse_definition(self, class_):
486487
self.current.value)
487488
return definition
488489

490+
def check_current(self, kind=None, value=None):
491+
msg = textwrap.dedent("""
492+
Unexpected token at line {self.line}:
493+
494+
In file: {self.filename}
495+
496+
Got kind {self.current.kind!r}
497+
Got value {self.current.value}
498+
""".format(self=self))
499+
kind_valid = self.current.kind == kind if kind else True
500+
value_valid = self.current.value == value if value else True
501+
assert kind_valid and value_valid, msg
502+
489503
def parse_from_import_statement(self):
490504
"""Parse a 'from x import y' statement.
491505
492506
The purpose is to find __future__ statements.
493507
494508
"""
495509
log.debug('parsing from/import statement.')
510+
is_future_import = self._parse_from_import_source()
511+
self._parse_from_import_names(is_future_import)
512+
513+
def _parse_from_import_source(self):
514+
"""Parse the 'from x import' part in a 'from x import y' statement.
515+
516+
Return true iff `x` is __future__.
517+
"""
496518
assert self.current.value == 'from', self.current.value
497519
self.stream.move()
498-
if self.current.value != '__future__':
499-
return
520+
is_future_import = self.current.value == '__future__'
500521
self.stream.move()
522+
while (self.current.kind in (tk.DOT, tk.NAME, tk.OP) and
523+
self.current.value != 'import'):
524+
self.stream.move()
525+
self.check_current(value='import')
501526
assert self.current.value == 'import', self.current.value
502527
self.stream.move()
528+
return is_future_import
529+
530+
def _parse_from_import_names(self, is_future_import):
531+
"""Parse the 'y' part in a 'from x import y' statement."""
503532
if self.current.value == '(':
504533
self.consume(tk.OP)
505534
expected_end_kind = tk.OP
@@ -512,8 +541,9 @@ def parse_from_import_statement(self):
512541
continue
513542
log.debug("parsing import, token is %r (%s)",
514543
self.current.kind, self.current.value)
515-
log.debug('found future import: %s', self.current.value)
516-
self.future_imports[self.current.value] = True
544+
if is_future_import:
545+
log.debug('found future import: %s', self.current.value)
546+
self.future_imports[self.current.value] = True
517547
self.consume(tk.NAME)
518548
log.debug("parsing import, token is %r (%s)",
519549
self.current.kind, self.current.value)

src/tests/test_cases/all_import.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""A valid module docstring."""
2+
3+
from .all_import_aux import __all__
4+
from .expected import Expectation
5+
6+
expectation = Expectation()
7+
expect = expectation.expect
8+
9+
10+
@expect("D103: Missing docstring in public function")
11+
def public_func():
12+
pass
13+
14+
15+
@expect("D103: Missing docstring in public function")
16+
def this():
17+
pass

src/tests/test_cases/all_import_as.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""A valid module docstring."""
2+
3+
from .all_import_aux import __all__ as not_dunder_all
4+
from .expected import Expectation
5+
6+
expectation = Expectation()
7+
expect = expectation.expect
8+
9+
__all__ = ('public_func', )
10+
11+
12+
@expect("D103: Missing docstring in public function")
13+
def public_func():
14+
pass
15+
16+
17+
def private_func():
18+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__all__ = ('this', 'is', 'a', 'helper', 'file')

src/tests/test_definitions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,8 @@ def test_token_stream():
260260
'capitalization',
261261
'comment_after_def_bug',
262262
'multi_line_summary_start',
263+
'all_import',
264+
'all_import_as',
263265
])
264266
def test_pep257(test_case):
265267
"""Run domain-specific tests from test.py file."""

0 commit comments

Comments
 (0)