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

Commit f3f90a4

Browse files
committed
Merge pull request #134 from Nurdok/future-imports
Parse __future__ imports and suppress D302 when `unicode_literals` is imported
2 parents 23cf188 + 1372b38 commit f3f90a4

File tree

15 files changed

+158
-44
lines changed

15 files changed

+158
-44
lines changed

docs/release_notes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ New Features
1212
previously resulted in D100 errors ("Missing docstring in public module")
1313
will now result in D104 (#105, #127).
1414

15+
Bug Fixes
16+
17+
* On Python 2.x, D302 ("Use u""" for Unicode docstrings") is not reported
18+
if `unicode_literals` is imported from `__future__` (#113, #134).
19+
1520

1621
0.6.0 - July 20th, 2015
1722
---------------------------

requirements/tests.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pytest==2.7.2
2+
pytest-pep8
3+
mock

setup.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import with_statement
2+
import os
23
from setuptools import setup
34

45

5-
with open('pep257.py') as f:
6+
with open(os.path.join('src', 'pep257.py')) as f:
67
for line in f:
78
if line.startswith('__version__'):
89
version = eval(line.split('=')[-1])
@@ -25,6 +26,7 @@
2526
'License :: OSI Approved :: MIT License',
2627
],
2728
keywords='PEP 257, pep257, PEP 8, pep8, docstrings',
29+
package_dir={'': 'src'},
2830
py_modules=['pep257'],
29-
scripts=['pep257'],
31+
scripts=['src/pep257'],
3032
)

src/__init__.py

Whitespace-only changes.

pep257 renamed to src/pep257

File renamed without changes.

pep257.py renamed to src/pep257.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from optparse import OptionParser
2222
from re import compile as re
2323
import itertools
24+
from collections import defaultdict
2425

2526
try: # Python 3.x
2627
from ConfigParser import RawConfigParser
@@ -100,9 +101,9 @@ def __eq__(self, other):
100101
return other and vars(self) == vars(other)
101102

102103
def __repr__(self):
103-
kwargs = ', '.join('{}={!r}'.format(field, getattr(self, field))
104+
kwargs = ', '.join('{0}={1!r}'.format(field, getattr(self, field))
104105
for field in self._fields)
105-
return '{}({})'.format(self.__class__.__name__, kwargs)
106+
return '{0}({1})'.format(self.__class__.__name__, kwargs)
106107

107108

108109
class Definition(Value):
@@ -131,7 +132,7 @@ def __str__(self):
131132
class Module(Definition):
132133

133134
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
134-
'children', 'parent', '_all')
135+
'children', 'parent', '_all', 'future_imports')
135136
is_public = True
136137
_nest = staticmethod(lambda s: {'def': Function, 'class': Class}[s])
137138
module = property(lambda self: self)
@@ -197,7 +198,7 @@ class Decorator(Value):
197198

198199
class TokenKind(int):
199200
def __repr__(self):
200-
return "tk.{}".format(tk.tok_name[self])
201+
return "tk.{0}".format(tk.tok_name[self])
201202

202203

203204
class Token(Value):
@@ -251,6 +252,8 @@ def __call__(self, filelike, filename):
251252
self.stream = TokenStream(StringIO(src))
252253
self.filename = filename
253254
self.all = None
255+
# TODO: what about Python 3.x?
256+
self.future_imports = defaultdict(lambda: False)
254257
self._accumulated_decorators = []
255258
return self.parse_module()
256259

@@ -349,6 +352,8 @@ def parse_definitions(self, class_, all=False):
349352
elif self.current.kind == tk.DEDENT:
350353
self.consume(tk.DEDENT)
351354
return
355+
elif self.current.value == 'from':
356+
self.parse_from_import_statement()
352357
else:
353358
self.stream.move()
354359

@@ -410,6 +415,7 @@ def parse_module(self):
410415
[], docstring, children, None, self.all)
411416
for child in module.children:
412417
child.parent = module
418+
module.future_imports = self.future_imports
413419
log.debug("finished parsing module.")
414420
return module
415421

@@ -460,6 +466,44 @@ def parse_definition(self, class_):
460466
self.current.value)
461467
return definition
462468

469+
def parse_from_import_statement(self):
470+
"""Parse a 'from x import y' statement.
471+
472+
The purpose is to find __future__ statements.
473+
474+
"""
475+
log.debug('parsing from/import statement.')
476+
assert self.current.value == 'from', self.current.value
477+
self.stream.move()
478+
if self.current.value != '__future__':
479+
return
480+
self.stream.move()
481+
assert self.current.value == 'import', self.current.value
482+
self.stream.move()
483+
if self.current.value == '(':
484+
self.consume(tk.OP)
485+
expected_end_kind = tk.OP
486+
else:
487+
expected_end_kind = tk.NEWLINE
488+
while self.current.kind != expected_end_kind:
489+
if self.current.kind != tk.NAME:
490+
self.stream.move()
491+
continue
492+
log.debug("parsing import, token is %r (%s)",
493+
self.current.kind, self.current.value)
494+
log.debug('found future import: %s', self.current.value)
495+
self.future_imports[self.current.value] = True
496+
self.consume(tk.NAME)
497+
log.debug("parsing import, token is %r (%s)",
498+
self.current.kind, self.current.value)
499+
if self.current.kind == tk.NAME:
500+
self.consume(tk.NAME) # as
501+
self.consume(tk.NAME) # new name, irrelevant
502+
if self.current.value == ',':
503+
self.consume(tk.OP)
504+
log.debug("parsing import, token is %r (%s)",
505+
self.current.kind, self.current.value)
506+
463507

464508
class Error(object):
465509

@@ -1123,6 +1167,9 @@ def check_unicode_docstring(self, definition, docstring):
11231167
For Unicode docstrings, use u"""Unicode triple-quoted strings""".
11241168
11251169
'''
1170+
if definition.module.future_imports['unicode_literals']:
1171+
return
1172+
11261173
# Just check that docstring is unicode, check_triple_double_quotes
11271174
# ensures the correct quotes.
11281175
if docstring and sys.version_info[0] <= 2:

src/tests/__init__.py

Whitespace-only changes.

src/tests/test_cases/__init__.py

Whitespace-only changes.

src/tests/test_cases/expected.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class Expectation(object):
2+
"""Hold expectation for pep257 violations in tests."""
3+
4+
def __init__(self):
5+
self.expected = set([])
6+
7+
def expect(self, *args):
8+
"""Decorator that expects a certain PEP 257 violation."""
9+
def none(_):
10+
return None
11+
12+
if len(args) == 1:
13+
return lambda f: (self.expected.add((f.__name__, args[0])) or
14+
none(f()) or f)
15+
self.expected.add(args)

test.py renamed to src/tests/test_cases/test.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
# encoding: utf-8
22
# No docstring, so we can test D100
33
import sys
4+
from .expected import Expectation
45

56

6-
expected = set([])
7-
8-
9-
def expect(*args):
10-
"""Decorator that expects a certain PEP 257 violation."""
11-
def none(a):
12-
return None
13-
14-
if len(args) == 1:
15-
return lambda f: expected.add((f.__name__, args[0])) or none(f()) or f
16-
expected.add(args)
17-
7+
expectation = Expectation()
8+
expect = expectation.expect
189

1910
expect('class_', 'D101: Missing docstring in public class')
2011

@@ -286,4 +277,12 @@ def a_following_valid_function(x):
286277
287278
"""
288279

289-
expect('test.py', 'D100: Missing docstring in public module')
280+
281+
def outer_function():
282+
"""Do something."""
283+
def inner_function():
284+
"""Do inner something."""
285+
return 0
286+
287+
expect(__file__ if __file__[-1] != 'c' else __file__[:-1],
288+
'D100: Missing docstring in public module')

0 commit comments

Comments
 (0)