Merge lp:~jml/divmod.org/duplicate-class-defs into lp:divmod.org

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 2685
Proposed branch: lp:~jml/divmod.org/duplicate-class-defs
Merge into: lp:divmod.org
Diff against target: 212 lines (+100/-15)
4 files modified
Pyflakes/pyflakes/checker.py (+16/-7)
Pyflakes/pyflakes/messages.py (+2/-2)
Pyflakes/pyflakes/test/test_imports.py (+0/-3)
Pyflakes/pyflakes/test/test_other.py (+82/-3)
To merge this branch: bzr merge lp:~jml/divmod.org/duplicate-class-defs
Reviewer Review Type Date Requested Status
Christopher Armstrong (community) code Approve
Divmod-dev Pending
Review via email: mp+80813@code.launchpad.net

Description of the change

This merge proposal lived at https://code.launchpad.net/~jml/pyflakes/duplicate-class-defs/+merge/44601 for a while, but after Tristan Seligmann's announcement of the release of pyflakes 0.5.0, I've moved it to this project, which seems more appropriate.

This branch changes pyflakes to warn about more kinds duplicated definitions:
  * reassigning to a variable without using it first
  * redefining a class with the same name as an existing class / function
  * redefining a function with the same name as an existing class / function

And so forth.

In the branch, I deleted one existing test that seemed to run counter to this desired behaviour.

I've simplified the "check for redefinition" logic so that it checks for any rebindings. I had to be careful not to tread on the special Importation checks/warnings and the Argument re-use warnings. The way I did this was to make a special kind of binding type called Definition, and make Assignment, FunctionDefinition and ClassDefinition inherit from this.

Note that the new code will *not* catch things like this::

  def f(x):
    x = 1

Perhaps it should.

Below is the review discussion from the divmod ticket, which may one day be available on http://divmod.org/trac/ticket/3034.

Agreed. I'll fix this up & resubmit a patch.

===

Okay. If this bothers people, I guess they can rename the class they're smashing to something else.

The special handling of ClassDefinition (mirroring the existing special handling of FunctionDefinition) seems weird though. What about this?

$ cat | pyflakes
def foo(): pass
class foo: pass
$
Seems as warning-worthy as two functions or two classes. I wonder why it doesn't just check bindings. Is this not worth a warning?

$ cat | pyflakes
x = 10
x = 10
$
These all seem like they should be handled like imports are handled:

$ cat | pyflakes
from twisted import foo
from twisted import foo
<stdin>:2: redefinition of unused 'foo' from line 1
<stdin>:2: 'foo' imported but unused

===

Warning on class re-definitions is consistent with warning about function re-definitions:

cat | ./bin/pyflakes
def f():
  pass
x = f()
def f():
  pass
<stdin>:4: redefinition of function 'f' from line 1
With class redefinitions, the only times I've ever used the facility to redefine a class in a Python module, it has been by mistake.

Both observations lend weight to including my patch, although I'll admit it's a matter of judgment.

I *almost* wish pyflakes had some kind of configuration system.

===

This looks like a good thing to fix. I don't know about this though:

$ cat | pyflakes
class foo: pass
class foo(foo): pass
<stdin>:2: redefinition of class 'foo' from line 1
The class being redefined was used before its name was rebound, at least. Worth a warning? Or will someone do this intentionally and be upset at the warning?

It's even less clear in other equivalent cases:

$ cat | pyflakes
class foo: pass
x = foo()
class foo: pass
<stdin>:3: redefinition of class 'foo' from line 1
Any thoughts?

To post a comment you must log in.
Revision history for this message
Christopher Armstrong (radix) wrote :

[1] "A binding that that defines a function or class" -> duplicated "that".

[2] test_other.py line 133: there are four lines of whitespace here, between two methods, instead of 2.

Thanks for replacing the dumb test-for-bad-behavior with a TODOed test for the desired behavior.

I grabbed the branch, manually verified some behaviors (like class foo(foo) not emitting a warning), ran the unit tests, and reviewed the code itself.

This looks good. With the above mentioned lexical changes you have my +1.

I don't believe I have commit access to the trunk branch, so you'll have to get someone else who trusts my review to commit it. Sorry :-(

review: Approve (code)
Revision history for this message
Jonathan Lange (jml) wrote :

Thanks, I've made the lexical changes described. Will hassle ~divmod-dev.

2686. By Jonathan Lange

Remove duplicate 'that' and excess whitespace.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Pyflakes/pyflakes/checker.py'
2--- Pyflakes/pyflakes/checker.py 2010-04-13 14:53:04 +0000
3+++ Pyflakes/pyflakes/checker.py 2011-11-17 16:26:23 +0000
4@@ -85,6 +85,11 @@
5 """
6
7
8+class Definition(Binding):
9+ """
10+ A binding that defines a function or class.
11+ """
12+
13
14 class Assignment(Binding):
15 """
16@@ -97,7 +102,12 @@
17
18
19
20-class FunctionDefinition(Binding):
21+class FunctionDefinition(Definition):
22+ pass
23+
24+
25+
26+class ClassDefinition(Definition):
27 pass
28
29
30@@ -350,11 +360,6 @@
31 - if `reportRedef` is True (default), rebinding while unused will be
32 reported.
33 '''
34- if (isinstance(self.scope.get(value.name), FunctionDefinition)
35- and isinstance(value, FunctionDefinition)):
36- self.report(messages.RedefinedFunction,
37- lineno, value.name, self.scope[value.name].source.lineno)
38-
39 if not isinstance(self.scope, ClassScope):
40 for scope in self.scopeStack[::-1]:
41 existing = scope.get(value.name)
42@@ -366,11 +371,15 @@
43 self.report(messages.RedefinedWhileUnused,
44 lineno, value.name, scope[value.name].source.lineno)
45
46+ existing = self.scope.get(value.name)
47 if isinstance(value, UnBinding):
48 try:
49 del self.scope[value.name]
50 except KeyError:
51 self.report(messages.UndefinedName, lineno, value.name)
52+ elif isinstance(existing, Definition) and not existing.used:
53+ self.report(messages.RedefinedWhileUnused,
54+ lineno, value.name, existing.source.lineno)
55 else:
56 self.scope[value.name] = value
57
58@@ -582,7 +591,7 @@
59 for stmt in node.body:
60 self.handleNode(stmt, node)
61 self.popScope()
62- self.addBinding(node.lineno, Binding(node.name, node))
63+ self.addBinding(node.lineno, ClassDefinition(node.name, node))
64
65 def ASSIGN(self, node):
66 self.handleNode(node.value, node)
67
68=== modified file 'Pyflakes/pyflakes/messages.py'
69--- Pyflakes/pyflakes/messages.py 2009-07-06 12:01:14 +0000
70+++ Pyflakes/pyflakes/messages.py 2011-11-17 16:26:23 +0000
71@@ -68,8 +68,8 @@
72 self.message_args = (name,)
73
74
75-class RedefinedFunction(Message):
76- message = 'redefinition of function %r from line %r'
77+class Redefined(Message):
78+ message = 'redefinition of %r from line %r'
79 def __init__(self, filename, lineno, name, orig_lineno):
80 Message.__init__(self, filename, lineno)
81 self.message_args = (name, orig_lineno)
82
83=== modified file 'Pyflakes/pyflakes/test/test_imports.py'
84--- Pyflakes/pyflakes/test/test_imports.py 2010-04-13 14:53:04 +0000
85+++ Pyflakes/pyflakes/test/test_imports.py 2011-11-17 16:26:23 +0000
86@@ -479,9 +479,6 @@
87 fu
88 ''', m.RedefinedWhileUnused)
89
90- def test_ignoreNonImportRedefinitions(self):
91- self.flakes('a = 1; a = 2')
92-
93 def test_importingForImportError(self):
94 self.flakes('''
95 try:
96
97=== modified file 'Pyflakes/pyflakes/test/test_other.py'
98--- Pyflakes/pyflakes/test/test_other.py 2010-04-13 14:53:04 +0000
99+++ Pyflakes/pyflakes/test/test_other.py 2011-11-17 16:26:23 +0000
100@@ -33,7 +33,7 @@
101 self.flakes('''
102 def a(): pass
103 def a(): pass
104- ''', m.RedefinedFunction)
105+ ''', m.RedefinedWhileUnused)
106
107 def test_redefinedClassFunction(self):
108 """
109@@ -44,7 +44,7 @@
110 class A:
111 def a(): pass
112 def a(): pass
113- ''', m.RedefinedFunction)
114+ ''', m.RedefinedWhileUnused)
115
116 def test_functionDecorator(self):
117 """
118@@ -109,6 +109,85 @@
119 ''')
120
121
122+ def test_classRedefinition(self):
123+ """
124+ If a class is defined twice in the same module, a warning is emitted.
125+ """
126+ self.flakes(
127+ '''
128+ class Foo:
129+ pass
130+ class Foo:
131+ pass
132+ ''', m.RedefinedWhileUnused)
133+
134+
135+ def test_functionRedefinedAsClass(self):
136+ """
137+ If a function is redefined as a class, a warning is emitted.
138+ """
139+ self.flakes(
140+ '''
141+ def Foo():
142+ pass
143+ class Foo:
144+ pass
145+ ''', m.RedefinedWhileUnused)
146+
147+
148+ def test_classRedefinedAsFunction(self):
149+ """
150+ If a class is redefined as a function, a warning is emitted.
151+ """
152+ self.flakes(
153+ '''
154+ class Foo:
155+ pass
156+ def Foo():
157+ pass
158+ ''', m.RedefinedWhileUnused)
159+
160+
161+ def test_doubleAssignment(self):
162+ """
163+ If a variable is re-assigned to without being used, no warning is
164+ emitted.
165+ """
166+ self.flakes(
167+ '''
168+ x = 10
169+ x = 20
170+ ''', m.RedefinedWhileUnused)
171+ test_doubleAssignment.todo = (
172+ "Too hard to make this warn but other cases stay silent")
173+
174+
175+ def test_doubleAssignmentConditionally(self):
176+ """
177+ If a variable is re-assigned within a conditional, no warning is
178+ emitted.
179+ """
180+ self.flakes(
181+ '''
182+ x = 10
183+ if True:
184+ x = 20
185+ ''')
186+
187+
188+ def test_doubleAssignmentWithUse(self):
189+ """
190+ If a variable is re-assigned to after being used, no warning is
191+ emitted.
192+ """
193+ self.flakes(
194+ '''
195+ x = 10
196+ y = x * 2
197+ x = 20
198+ ''')
199+
200+
201 def test_comparison(self):
202 """
203 If a defined name is used on either side of any of the six comparison
204@@ -128,7 +207,7 @@
205
206 def test_identity(self):
207 """
208- If a deefined name is used on either side of an identity test, no
209+ If a defined name is used on either side of an identity test, no
210 warning is emitted.
211 """
212 self.flakes('''

Subscribers

People subscribed via source and target branches

to all changes: