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')
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# -*- coding: utf-8 -*-
2+
"""This is a module."""
3+
4+
from __future__ import unicode_literals
5+
from .expected import Expectation
6+
7+
8+
expectation = Expectation()
9+
expect = expectation.expect
10+
11+
12+
def with_unicode_docstring_without_u():
13+
r"""Check unicode: \u2611."""

test_decorators.py renamed to src/tests/test_decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import textwrap
1212

13-
import pep257
13+
from .. import pep257
1414

1515

1616
class TestParser:

test_definitions.py renamed to src/tests/test_definitions.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from pep257 import (StringIO, TokenStream, Parser, Error, check,
2-
Module, Class, Method, Function, NestedFunction)
1+
import os
2+
from ..pep257 import (StringIO, TokenStream, Parser, Error, check,
3+
Module, Class, Method, Function, NestedFunction)
34

45

56
_ = type('', (), dict(__repr__=lambda *a: '_', __eq__=lambda *a: True))()
@@ -35,14 +36,22 @@ def nested_3(self):
3536
# Inconvenient comment.
3637
'a', 'b' 'c',]
3738
'''
39+
source_unicode_literals = '''
40+
from __future__ import unicode_literals
41+
'''
42+
source_multiple_future_imports = '''
43+
from __future__ import (nested_scopes as ns,
44+
unicode_literals)
45+
'''
3846

3947

4048
def test_parser():
4149
dunder_all = ('a', 'bc')
4250
module = parse(StringIO(source), 'file.py')
4351
assert len(list(module)) == 8
4452
assert Module('file.py', _, 1, len(source.split('\n')),
45-
_, '"""Module."""', _, _, dunder_all) == module
53+
_, '"""Module."""', _, _, dunder_all, {}) == \
54+
module
4655

4756
function, class_ = module.children
4857
assert Function('function', _, _, _, _, '"Function."', _,
@@ -70,12 +79,24 @@ def test_parser():
7079

7180
module = parse(StringIO(source_alt), 'file_alt.py')
7281
assert Module('file_alt.py', _, 1, len(source_alt.split('\n')),
73-
_, None, _, _, dunder_all) == module
82+
_, None, _, _, dunder_all, {}) == module
7483

7584
module = parse(StringIO(source_alt_nl_at_bracket), 'file_alt_nl.py')
7685
assert Module('file_alt_nl.py', _, 1,
7786
len(source_alt_nl_at_bracket.split('\n')), _, None, _, _,
78-
dunder_all) == module
87+
dunder_all, {}) == module
88+
89+
module = parse(StringIO(source_unicode_literals), 'file_ucl.py')
90+
assert Module('file_ucl.py', _, 1,
91+
_, _, None, _, _,
92+
_, {'unicode_literals': True}) == module
93+
94+
module = parse(StringIO(source_multiple_future_imports), 'file_mfi.py')
95+
assert Module('file_mfi.py', _, 1,
96+
_, _, None, _, _,
97+
_, {'unicode_literals': True, 'nested_scopes': True}) \
98+
== module
99+
assert module.future_imports['unicode_literals']
79100

80101

81102
def _test_module():
@@ -106,11 +127,17 @@ def test_token_stream():
106127

107128
def test_pep257():
108129
"""Run domain-specific tests from test.py file."""
109-
import test
110-
results = list(check(['test.py']))
111-
for error in results:
112-
assert isinstance(error, Error)
113-
results = set([(e.definition.name, e.message) for e in results])
114-
print('\nextra: %r' % (results - test.expected))
115-
print('\nmissing: %r' % (test.expected - results))
116-
assert test.expected == results
130+
test_cases = ('test', 'unicode_literals')
131+
for test_case in test_cases:
132+
case_module = __import__('test_cases.{0}'.format(test_case),
133+
globals=globals(),
134+
locals=locals(),
135+
fromlist=['expectation'],
136+
level=1)
137+
# from .test_cases import test
138+
results = list(check([os.path.join(os.path.dirname(__file__),
139+
'test_cases', test_case + '.py')]))
140+
for error in results:
141+
assert isinstance(error, Error)
142+
results = set([(e.definition.name, e.message) for e in results])
143+
assert case_module.expectation.expected == results

test_pep257.py renamed to src/tests/test_pep257.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
from __future__ import with_statement
66
from collections import namedtuple
7+
from functools import partial
78

89
import sys
910
import os
1011
import mock
1112
import shlex
12-
import pytest
1313
import shutil
1414
import tempfile
1515
import textwrap
1616
import subprocess
1717

18-
import pep257
18+
from .. import pep257
1919

2020
__all__ = ()
2121

@@ -53,11 +53,13 @@ def open(self, path, *args, **kwargs):
5353

5454
def invoke_pep257(self, args=""):
5555
"""Run pep257.py on the environment base folder with the given args."""
56-
pep257_location = os.path.join(os.path.dirname(__file__), 'pep257')
56+
pep257_location = os.path.join(os.path.dirname(__file__),
57+
'..', 'pep257')
5758
cmd = shlex.split("python {0} {1} {2}"
5859
.format(pep257_location, self.tempdir, args),
5960
posix=False)
60-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
61+
p = subprocess.Popen(cmd,
62+
stdout=subprocess.PIPE,
6163
stderr=subprocess.PIPE)
6264
out, err = p.communicate()
6365
return self.Result(out=out.decode('utf-8'),
@@ -72,11 +74,13 @@ def __enter__(self):
7274

7375
def __exit__(self, *args, **kwargs):
7476
shutil.rmtree(self.tempdir)
77+
pass
7578

7679

7780
def test_pep257_conformance():
78-
errors = list(pep257.check(['pep257.py', 'test_pep257.py']))
79-
print(errors)
81+
relative = partial(os.path.join, os.path.dirname(__file__))
82+
errors = list(pep257.check([relative('..', 'pep257.py'),
83+
relative('test_pep257.py')]))
8084
assert errors == []
8185

8286

@@ -90,14 +94,15 @@ def function_with_bad_docstring(foo):
9094
expected_error_codes = set(('D100', 'D400', 'D401', 'D205', 'D209',
9195
'D210'))
9296
mock_open = mock.mock_open(read_data=function_to_check)
93-
with mock.patch('pep257.tokenize_open', mock_open, create=True):
97+
from .. import pep257
98+
with mock.patch.object(pep257, 'tokenize_open', mock_open, create=True):
9499
errors = tuple(pep257.check(['filepath']))
95100
error_codes = set(error.code for error in errors)
96101
assert error_codes == expected_error_codes
97102

98103
# We need to recreate the mock, otherwise the read file is empty
99104
mock_open = mock.mock_open(read_data=function_to_check)
100-
with mock.patch('pep257.tokenize_open', mock_open, create=True):
105+
with mock.patch.object(pep257, 'tokenize_open', mock_open, create=True):
101106
errors = tuple(pep257.check(['filepath'], ignore=['D100', 'D202']))
102107
error_codes = set(error.code for error in errors)
103108
assert error_codes == expected_error_codes - set(('D100', 'D202'))

0 commit comments

Comments
 (0)