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

Commit bbe1c9a

Browse files
committed
Update #69 to latest code
1 parent 286f4ff commit bbe1c9a

File tree

3 files changed

+312
-16
lines changed

3 files changed

+312
-16
lines changed

pep257.py

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ def __repr__(self):
9494

9595
class Definition(Value):
9696

97-
_fields = 'name _source start end docstring children parent'.split()
97+
_fields = ['name', '_source', 'start', 'end', 'decorators', 'docstring',
98+
'children', 'parent']
9899

99100
_human = property(lambda self: humanize(type(self).__name__))
100101
kind = property(lambda self: self._human.split()[-1])
@@ -116,7 +117,8 @@ def __str__(self):
116117

117118
class Module(Definition):
118119

119-
_fields = 'name _source start end docstring children parent _all'.split()
120+
_fields = ['name', '_source', 'start', 'end', 'decorators', 'docstring',
121+
'children', 'parent', '_all']
120122
is_public = True
121123
_nest = staticmethod(lambda s: {'def': Function, 'class': Class}[s])
122124
module = property(lambda self: self)
@@ -148,6 +150,10 @@ class Method(Function):
148150

149151
@property
150152
def is_public(self):
153+
# Check if we are a setter method, and mark as private if so.
154+
for decorator in self.decorators:
155+
if decorator.name.startswith(self.name):
156+
return False
151157
name_is_public = not self.name.startswith('_') or is_magic(self.name)
152158
return self.parent.is_public and name_is_public
153159

@@ -163,6 +169,13 @@ class NestedClass(Class):
163169
is_public = False
164170

165171

172+
class Decorator(Value):
173+
174+
"""A decorator for function, method or class."""
175+
176+
_fields = 'name arguments'.split()
177+
178+
166179
class TokenKind(int):
167180
def __repr__(self):
168181
return "tk.{}".format(tk.tok_name[self])
@@ -219,6 +232,7 @@ def __call__(self, filelike, filename):
219232
self.stream = TokenStream(StringIO(src))
220233
self.filename = filename
221234
self.all = None
235+
self._decorators = []
222236
return self.parse_module()
223237

224238
current = property(lambda self: self.stream.current)
@@ -254,13 +268,57 @@ def parse_docstring(self):
254268
return docstring
255269
return None
256270

271+
def parse_decorators(self):
272+
"""Called after first @ is found.
273+
274+
Build list of current decorators and return last parsed token,
275+
which is def or class start token.
276+
"""
277+
name = []
278+
arguments = []
279+
at_arguments = False
280+
281+
while self.current is not None:
282+
if (self.current.kind == tk.NAME and
283+
self.current.value in ['def', 'class']):
284+
break
285+
elif self.current.kind == tk.OP and self.current.value == '@':
286+
# New decorator found.
287+
self._decorators.append(
288+
Decorator(''.join(name), ''.join(arguments)))
289+
name = []
290+
arguments = []
291+
at_arguments = False
292+
elif self.current.kind == tk.OP and self.current.value == '(':
293+
at_arguments = True
294+
elif self.current.kind == tk.OP and self.current.value == ')':
295+
# Ignore close parenthesis
296+
pass
297+
elif self.current.kind == tk.NEWLINE or self.current.kind == tk.NL:
298+
# Ignoe newlines
299+
pass
300+
else:
301+
# Keep accumulating decorator's name or argument.
302+
if not at_arguments:
303+
name.append(self.current.value)
304+
else:
305+
arguments.append(self.current.value)
306+
self.stream.move()
307+
308+
# Add decorator accumulated so far
309+
self._decorators.append(
310+
Decorator(''.join(name), ''.join(arguments)))
311+
257312
def parse_definitions(self, class_, all=False):
258313
"""Parse multiple defintions and yield them."""
259314
while self.current is not None:
260315
log.debug("parsing defintion list, current token is %r (%s)",
261316
self.current.kind, self.current.value)
262317
if all and self.current.value == '__all__':
263318
self.parse_all()
319+
elif self.current.kind == tk.OP and self.current.value == '@':
320+
self.consume(tk.OP)
321+
self.parse_decorators()
264322
elif self.current.value in ['def', 'class']:
265323
yield self.parse_definition(class_._nest(self.current.value))
266324
elif self.current.kind == tk.INDENT:
@@ -324,7 +382,7 @@ def parse_module(self):
324382
assert self.current is None, self.current
325383
end = self.line
326384
module = Module(self.filename, self.source, start, end,
327-
docstring, children, None, self.all)
385+
[], docstring, children, None, self.all)
328386
for child in module.children:
329387
child.parent = module
330388
log.debug("finished parsing module.")
@@ -356,17 +414,20 @@ def parse_definition(self, class_):
356414
self.leapfrog(tk.INDENT)
357415
assert self.current.kind != tk.INDENT
358416
docstring = self.parse_docstring()
417+
decorators = self._decorators
418+
self._decorators = []
359419
log.debug("parsing nested defintions.")
360420
children = list(self.parse_definitions(class_))
361421
log.debug("finished parsing nested defintions for '%s'", name)
362422
end = self.line - 1
363423
else: # one-liner definition
364424
docstring = self.parse_docstring()
425+
decorators = [] # TODO
365426
children = []
366427
end = self.line
367428
self.leapfrog(tk.NEWLINE)
368429
definition = class_(name, self.source, start, end,
369-
docstring, children, None)
430+
decorators, docstring, children, None)
370431
for child in definition.children:
371432
child.parent = definition
372433
log.debug("finished parsing %s '%s'. Next token is %r (%s)",
@@ -784,8 +845,8 @@ def check_indent(self, definition, docstring):
784845
if set(' \t') == set(''.join(indents) + indent):
785846
return Error('D206: Docstring indented with both tabs and '
786847
'spaces')
787-
if (len(indents) > 1 and min(indents[:-1]) > indent
788-
or indents[-1] > indent):
848+
if (len(indents) > 1 and min(indents[:-1]) > indent or
849+
indents[-1] > indent):
789850
return Error('D208: Docstring is over-indented')
790851
if min(indents) < indent:
791852
return Error('D207: Docstring is under-indented')

test_decorators.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""
2+
Unit test for pep257 module decorator handling.
3+
4+
Use tox or py.test to run the test suite.
5+
"""
6+
7+
try:
8+
from StringIO import StringIO
9+
except ImportError:
10+
from io import StringIO
11+
12+
import pep257
13+
14+
15+
class TestParser:
16+
"""
17+
Check parsing of Python source code.
18+
"""
19+
20+
def test_parse_class_single_decorator(self):
21+
"""
22+
Class decorator is recorded in class instance.
23+
"""
24+
code = """
25+
@first_decorator
26+
class Foo:
27+
pass
28+
"""
29+
module = pep257.parse(StringIO(code), 'dummy.py')
30+
decorators = module.children[0].decorators
31+
32+
assert 1 == len(decorators)
33+
assert 'first_decorator' == decorators[0].name
34+
assert '' == decorators[0].arguments
35+
36+
def test_parse_class_decorators(self):
37+
"""
38+
Class decorators are accumulated, together with they arguments.
39+
"""
40+
code = """
41+
@first_decorator
42+
@second.decorator(argument)
43+
@third.multi.line(
44+
decorator,
45+
key=value,
46+
)
47+
class Foo:
48+
pass
49+
"""
50+
51+
module = pep257.parse(StringIO(code), 'dummy.py')
52+
defined_class = module.children[0]
53+
decorators = defined_class.decorators
54+
55+
assert 3 == len(decorators)
56+
assert 'first_decorator' == decorators[0].name
57+
assert '' == decorators[0].arguments
58+
assert 'second.decorator' == decorators[1].name
59+
assert 'argument' == decorators[1].arguments
60+
assert 'third.multi.line' == decorators[2].name
61+
assert 'decorator,key=value,' == decorators[2].arguments
62+
63+
def test_parse_class_nested_decorator(self):
64+
"""
65+
Class decorator is recorded even for nested classes.
66+
"""
67+
code = """
68+
@parent_decorator
69+
class Foo:
70+
pass
71+
@first_decorator
72+
class NestedClass:
73+
pass
74+
"""
75+
module = pep257.parse(StringIO(code), 'dummy.py')
76+
nested_class = module.children[0].children[0]
77+
decorators = nested_class.decorators
78+
79+
assert 1 == len(decorators)
80+
assert 'first_decorator' == decorators[0].name
81+
assert '' == decorators[0].arguments
82+
83+
def test_parse_method_single_decorator(self):
84+
"""
85+
Method decorators are accumulated.
86+
"""
87+
code = """
88+
class Foo:
89+
@first_decorator
90+
def method(self):
91+
pass
92+
"""
93+
94+
module = pep257.parse(StringIO(code), 'dummy.py')
95+
defined_class = module.children[0]
96+
decorators = defined_class.children[0].decorators
97+
98+
assert 1 == len(decorators)
99+
assert 'first_decorator' == decorators[0].name
100+
assert '' == decorators[0].arguments
101+
102+
def test_parse_method_decorators(self):
103+
"""
104+
Method decorators are accumulated.
105+
"""
106+
code = """
107+
class Foo:
108+
@first_decorator
109+
@second.decorator(argument)
110+
@third.multi.line(
111+
decorator,
112+
key=value,
113+
)
114+
def method(self):
115+
pass
116+
"""
117+
118+
module = pep257.parse(StringIO(code), 'dummy.py')
119+
defined_class = module.children[0]
120+
decorators = defined_class.children[0].decorators
121+
122+
assert 3 == len(decorators)
123+
assert 'first_decorator' == decorators[0].name
124+
assert '' == decorators[0].arguments
125+
assert 'second.decorator' == decorators[1].name
126+
assert 'argument' == decorators[1].arguments
127+
assert 'third.multi.line' == decorators[2].name
128+
assert 'decorator,key=value,' == decorators[2].arguments
129+
130+
def test_parse_function_decorator(self):
131+
"""
132+
It accumulates decorators for functions.
133+
"""
134+
code = """@first_decorator
135+
def some_method(self):
136+
pass
137+
"""
138+
139+
module = pep257.parse(StringIO(code), 'dummy.py')
140+
decorators = module.children[0].decorators
141+
142+
assert 1 == len(decorators)
143+
assert 'first_decorator' == decorators[0].name
144+
assert '' == decorators[0].arguments
145+
146+
def test_parse_method_nested_decorator(self):
147+
"""
148+
Method decorators are accumulated for nested methods.
149+
"""
150+
code = """
151+
class Foo:
152+
@parent_decorator
153+
def method(self):
154+
@first_decorator
155+
def nested_method(arg):
156+
pass
157+
"""
158+
159+
module = pep257.parse(StringIO(code), 'dummy.py')
160+
defined_class = module.children[0]
161+
decorators = defined_class.children[0].children[0].decorators
162+
163+
assert 1 == len(decorators)
164+
assert 'first_decorator' == decorators[0].name
165+
assert '' == decorators[0].arguments
166+
167+
168+
class TestMethod:
169+
"""
170+
Unit test for Method class.
171+
"""
172+
173+
def makeMethod(self, name='someMethodName'):
174+
"""
175+
Return a simple method instance.
176+
"""
177+
children = []
178+
all = ['ClassName']
179+
source = 'class ClassName:\n def %s(self):\n' % (name)
180+
181+
module = pep257.Module(
182+
'module_name',
183+
source,
184+
0,
185+
1,
186+
[],
187+
'Docstring for module',
188+
[],
189+
None,
190+
all,
191+
)
192+
193+
parent = pep257.Class(
194+
'ClassName',
195+
source,
196+
0,
197+
1,
198+
[],
199+
'Docstring for class',
200+
children,
201+
module,
202+
all,
203+
)
204+
205+
return pep257.Method(
206+
name,
207+
source,
208+
0,
209+
1,
210+
[],
211+
'Docstring for method',
212+
children,
213+
parent,
214+
all,
215+
)
216+
217+
def test_is_public_normal(self):
218+
"""Methods are normally public, even if decorated."""
219+
method = self.makeMethod('methodName')
220+
method.decorators = [pep257.Decorator('some_decorator', [])]
221+
222+
assert method.is_public
223+
224+
def test_is_public_setter(self):
225+
"""Setter methods are considered private."""
226+
method = self.makeMethod('methodName')
227+
method.decorators = [
228+
pep257.Decorator('some_decorator', []),
229+
pep257.Decorator('methodName.setter', []),
230+
]
231+
232+
assert not method.is_public

0 commit comments

Comments
 (0)