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

Commit 2b02b18

Browse files
committed
Merge pull request #107 from glennmatthews/master
Make setters private (rework of #69)
2 parents 9b7a2a6 + 84ab3f2 commit 2b02b18

File tree

4 files changed

+295
-14
lines changed

4 files changed

+295
-14
lines changed

docs/release_notes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Release Notes
55
Current Development Version
66
---------------------------
77

8+
Bug Fixes
9+
10+
* Property setter and deleter methods are now treated as private and do not
11+
require docstrings separate from the main property method (#69, #107).
812

913
0.5.0 - March 14th, 2015
1014
------------------------

pep257.py

Lines changed: 68 additions & 4 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,11 @@ class Method(Function):
148150

149151
@property
150152
def is_public(self):
153+
# Check if we are a setter/deleter method, and mark as private if so.
154+
for decorator in self.decorators:
155+
# Given 'foo', match 'foo.bar' but not 'foobar' or 'sfoo'
156+
if re(r"^{0}\.".format(self.name)).match(decorator.name):
157+
return False
151158
name_is_public = not self.name.startswith('_') or is_magic(self.name)
152159
return self.parent.is_public and name_is_public
153160

@@ -163,6 +170,13 @@ class NestedClass(Class):
163170
is_public = False
164171

165172

173+
class Decorator(Value):
174+
175+
"""A decorator for function, method or class."""
176+
177+
_fields = 'name arguments'.split()
178+
179+
166180
class TokenKind(int):
167181
def __repr__(self):
168182
return "tk.{}".format(tk.tok_name[self])
@@ -219,6 +233,7 @@ def __call__(self, filelike, filename):
219233
self.stream = TokenStream(StringIO(src))
220234
self.filename = filename
221235
self.all = None
236+
self._accumulated_decorators = []
222237
return self.parse_module()
223238

224239
current = property(lambda self: self.stream.current)
@@ -254,13 +269,59 @@ def parse_docstring(self):
254269
return docstring
255270
return None
256271

272+
def parse_decorators(self):
273+
"""Called after first @ is found.
274+
275+
Parse decorators into self._accumulated_decorators.
276+
Continue to do so until encountering the 'def' or 'class' start token.
277+
"""
278+
name = []
279+
arguments = []
280+
at_arguments = False
281+
282+
while self.current is not None:
283+
if (self.current.kind == tk.NAME and
284+
self.current.value in ['def', 'class']):
285+
# Done with decorators - found function or class proper
286+
break
287+
elif self.current.kind == tk.OP and self.current.value == '@':
288+
# New decorator found. Store the decorator accumulated so far:
289+
self._accumulated_decorators.append(
290+
Decorator(''.join(name), ''.join(arguments)))
291+
# Now reset to begin accumulating the new decorator:
292+
name = []
293+
arguments = []
294+
at_arguments = False
295+
elif self.current.kind == tk.OP and self.current.value == '(':
296+
at_arguments = True
297+
elif self.current.kind == tk.OP and self.current.value == ')':
298+
# Ignore close parenthesis
299+
pass
300+
elif self.current.kind == tk.NEWLINE or self.current.kind == tk.NL:
301+
# Ignore newlines
302+
pass
303+
else:
304+
# Keep accumulating current decorator's name or argument.
305+
if not at_arguments:
306+
name.append(self.current.value)
307+
else:
308+
arguments.append(self.current.value)
309+
self.stream.move()
310+
311+
# Add decorator accumulated so far
312+
self._accumulated_decorators.append(
313+
Decorator(''.join(name), ''.join(arguments)))
314+
257315
def parse_definitions(self, class_, all=False):
258316
"""Parse multiple defintions and yield them."""
259317
while self.current is not None:
260318
log.debug("parsing defintion list, current token is %r (%s)",
261319
self.current.kind, self.current.value)
262320
if all and self.current.value == '__all__':
263321
self.parse_all()
322+
elif self.current.kind == tk.OP and self.current.value == '@':
323+
self.consume(tk.OP)
324+
self.parse_decorators()
264325
elif self.current.value in ['def', 'class']:
265326
yield self.parse_definition(class_._nest(self.current.value))
266327
elif self.current.kind == tk.INDENT:
@@ -324,7 +385,7 @@ def parse_module(self):
324385
assert self.current is None, self.current
325386
end = self.line
326387
module = Module(self.filename, self.source, start, end,
327-
docstring, children, None, self.all)
388+
[], docstring, children, None, self.all)
328389
for child in module.children:
329390
child.parent = module
330391
log.debug("finished parsing module.")
@@ -356,17 +417,20 @@ def parse_definition(self, class_):
356417
self.leapfrog(tk.INDENT)
357418
assert self.current.kind != tk.INDENT
358419
docstring = self.parse_docstring()
420+
decorators = self._accumulated_decorators
421+
self._accumulated_decorators = []
359422
log.debug("parsing nested defintions.")
360423
children = list(self.parse_definitions(class_))
361424
log.debug("finished parsing nested defintions for '%s'", name)
362425
end = self.line - 1
363426
else: # one-liner definition
364427
docstring = self.parse_docstring()
428+
decorators = [] # TODO
365429
children = []
366430
end = self.line
367431
self.leapfrog(tk.NEWLINE)
368432
definition = class_(name, self.source, start, end,
369-
docstring, children, None)
433+
decorators, docstring, children, None)
370434
for child in definition.children:
371435
child.parent = definition
372436
log.debug("finished parsing %s '%s'. Next token is %r (%s)",

test_decorators.py

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

0 commit comments

Comments
 (0)