Merge lp:~jml/pyflakes/duplicate-class-defs into lp:~vcs-imports/pyflakes/main

Proposed by Jonathan Lange on 2010-12-23
Status: Merged
Merge reported by: Jonathan Lange
Merged at revision: not available
Proposed branch: lp:~jml/pyflakes/duplicate-class-defs
Merge into: lp:~vcs-imports/pyflakes/main
Diff against target: 214 lines (+102/-15)
4 files modified
pyflakes/checker.py (+16/-7)
pyflakes/messages.py (+2/-2)
pyflakes/test/test_imports.py (+0/-3)
pyflakes/test/test_other.py (+84/-3)
To merge this branch: bzr merge lp:~jml/pyflakes/duplicate-class-defs
Reviewer Review Type Date Requested Status
VCS imports 2010-12-23 Pending
Review via email: mp+44601@code.launchpad.net

Description of the Change

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.
Robert Collins (lifeless) wrote :

On Fri, Dec 24, 2010 at 6:25 AM, Jonathan Lange <email address hidden> wrote:
> Jonathan Lange has proposed merging lp:~jml/pyflakes/duplicate-class-defs into lp:pyflakes.
>
> Requested reviews:
>  VCS imports (vcs-imports)
>
> For more details, see:
> https://code.launchpad.net/~jml/pyflakes/duplicate-class-defs/+merge/44601
>
> This branch changes pyflakes to warn about more kinds duplicated definitions:
>  * reassigning to a variable without using it first

I would hate to be nagged about this. I do the following a lot:
foo = sanedefault
if condition:
    foo = somethingbecauseofcondition

>  * redefining a class with the same name as an existing class / function
>  * redefining a function with the same name as an existing class / function

Woo.

Jonathan Lange (jml) wrote :

Don't worry about that Rob, you won't get nagged, even with this branch as it stands.

48. By Jonathan Lange on 2010-12-24

Add a test allaying Rob's fears

Jonathan Lange (jml) wrote :

Oops. Got the test wrong. Accidentally asserted that it generated a warning. Fixing now.

49. By Jonathan Lange on 2010-12-24

Fixinate

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'pyflakes/checker.py'
2--- pyflakes/checker.py 2010-04-13 15:53:04 +0000
3+++ pyflakes/checker.py 2010-12-24 11:21:25 +0000
4@@ -85,6 +85,11 @@
5 """
6
7
8+class Definition(Binding):
9+ """
10+ A binding that 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/messages.py'
69--- pyflakes/messages.py 2009-07-06 13:01:14 +0000
70+++ pyflakes/messages.py 2010-12-24 11:21:25 +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/test/test_imports.py'
84--- pyflakes/test/test_imports.py 2010-04-13 15:53:04 +0000
85+++ pyflakes/test/test_imports.py 2010-12-24 11:21:25 +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/test/test_other.py'
98--- pyflakes/test/test_other.py 2010-04-13 15:53:04 +0000
99+++ pyflakes/test/test_other.py 2010-12-24 11:21:25 +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,87 @@
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+
136+
137+ def test_functionRedefinedAsClass(self):
138+ """
139+ If a function is redefined as a class, a warning is emitted.
140+ """
141+ self.flakes(
142+ '''
143+ def Foo():
144+ pass
145+ class Foo:
146+ pass
147+ ''', m.RedefinedWhileUnused)
148+
149+
150+ def test_classRedefinedAsFunction(self):
151+ """
152+ If a class is redefined as a function, a warning is emitted.
153+ """
154+ self.flakes(
155+ '''
156+ class Foo:
157+ pass
158+ def Foo():
159+ pass
160+ ''', m.RedefinedWhileUnused)
161+
162+
163+ def test_doubleAssignment(self):
164+ """
165+ If a variable is re-assigned to without being used, no warning is
166+ emitted.
167+ """
168+ self.flakes(
169+ '''
170+ x = 10
171+ x = 20
172+ ''', m.RedefinedWhileUnused)
173+ test_doubleAssignment.todo = (
174+ "Too hard to make this warn but other cases stay silent")
175+
176+
177+ def test_doubleAssignmentConditionally(self):
178+ """
179+ If a variable is re-assigned within a conditional, no warning is
180+ emitted.
181+ """
182+ self.flakes(
183+ '''
184+ x = 10
185+ if True:
186+ x = 20
187+ ''')
188+
189+
190+ def test_doubleAssignmentWithUse(self):
191+ """
192+ If a variable is re-assigned to after being used, no warning is
193+ emitted.
194+ """
195+ self.flakes(
196+ '''
197+ x = 10
198+ y = x * 2
199+ x = 20
200+ ''')
201+
202+
203 def test_comparison(self):
204 """
205 If a defined name is used on either side of any of the six comparison
206@@ -128,7 +209,7 @@
207
208 def test_identity(self):
209 """
210- If a deefined name is used on either side of an identity test, no
211+ If a defined name is used on either side of an identity test, no
212 warning is emitted.
213 """
214 self.flakes('''

Subscribers

People subscribed via source and target branches

to all changes: