Improve checking of metaclass methods's first arg. Closes #4014.

  • Add C0204 for __new__ method of metaclass.
  • Add valid-metaclass-classmethod-first-arg config option.
  • Move a test, improve it and fix another one.
authorFELD Boris <lothiraldan@gmail.com>
changeset4b75e06211ab
branchdefault
phasepublic
hiddenno
parent revision#a8e0d01488a1 fix test broken by 27930e5ee87e
child revision#4d59dfcc0acc Add test and code for handling __all__ with pylint. Closes #4685.
files modified by this revision
ChangeLog
checkers/classes.py
test/input/func_e0214.py
test/input/func_first_arg.py
test/input/func_noerror_mcs_attr_access.py
test/messages/func_e0203.txt
test/messages/func_e0214.txt
test/messages/func_first_arg.txt
# HG changeset patch
# User FELD Boris <lothiraldan@gmail.com>
# Date 1348056243 -7200
# Wed Sep 19 14:04:03 2012 +0200
# Node ID 4b75e06211ab7e321e28b16d5b6309ee85c74a4b
# Parent a8e0d01488a17d9f5e5f5a5f05aee66dfce5f9a4
Improve checking of metaclass methods's first arg. Closes #4014.

* Add C0204 for __new__ method of metaclass.
* Add valid-metaclass-classmethod-first-arg config option.
* Move a test, improve it and fix another one.

diff --git a/ChangeLog b/ChangeLog
@@ -6,10 +6,13 @@
1        new W0711 message (patch by Tim Hatch)
2 
3      * #100654: fix grammatical error for W0332 message (using 'l' as
4        long int identifier)
5 
6 +    * #4014: Improve checking of metaclass methods first args, introducing
7 +      new C0204 message (patch by lothiraldan@gmail.com finalized by sthenault)
8 +
9      * fix cross-interpreter issue (non compatible access to __builtins__)
10 
11      * stop including tests files in distribution, they causes crash when
12        installed with python3 (#72022, #82417, #76910)
13 
diff --git a/checkers/classes.py b/checkers/classes.py
@@ -1,6 +1,6 @@
14 -# Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE).
15 +# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE).
16  # http://www.logilab.fr/ -- mailto:contact@logilab.fr
17  #
18  # This program is free software; you can redistribute it and/or modify it under
19  # the terms of the GNU General Public License as published by the Free Software
20  # Foundation; either version 2 of the License, or (at your option) any later
@@ -63,20 +63,30 @@
21                first argument has no argument defined.'),
22      'E0213': ('Method should have "self" as first argument',
23                'Used when a method has an attribute different the "self" as\
24                first argument. This is considered as an error since this is\
25                a so common convention that you shouldn\'t break it!'),
26 -    'C0202': ('Class method should have %s as first argument', # E0212
27 -              'Used when a class method has an attribute different than "cls"\
28 -              as first argument, to easily differentiate them from regular \
29 -              instance methods.'),
30 -    'C0203': ('Metaclass method should have "mcs" as first argument', # E0214
31 -              'Used when a metaclass method has an attribute different the \
32 -              "mcs" as first argument.'),
33 +    'C0202': ('Class method %s should have %s as first argument', # E0212
34 +              'Used when a class method has a first argument named differently '
35 +              'than the value specified in valid-classmethod-first-arg option '
36 +              '(default to "cls"), recommended to easily differentiate them '
37 +              'from regular instance methods.'),
38 +    'C0203': ('Metaclass method %s should have %s as first argument', # E0214
39 +              'Used when a metaclass method has a first agument named '
40 +              'differently than the value specified in valid-classmethod-first'
41 +              '-arg option (default to "cls"), recommended to easily '
42 +              'differentiate them from regular instance methods.'),
43 +    'C0204': ('Metaclass class method %s should have %s as first argument',
44 +              'Used when a metaclass class method has a first argument named '
45 +              'differently than the value specified in valid-metaclass-'
46 +              'classmethod-first-arg option (default to "mcs"), recommended to '
47 +              'easily differentiate them from regular instance methods.'),
48 
49      'W0211': ('Static method with %r as first argument',
50 -              'Used when a static method has "self" or "cls" as first argument.'
51 +              'Used when a static method has "self" or a value specified in '
52 +              'valid-classmethod-first-arg option or '
53 +              'valid-metaclass-classmethod-first-arg option as first argument.'
54                ),
55      'R0201': ('Method could be a function',
56                'Used when a method doesn\'t use its bound instance, and so could\
57                be written as a function.'
58                ),
@@ -163,10 +173,17 @@
59                   'type' : 'csv',
60                   'metavar' : '<argument names>',
61                   'help' : 'List of valid names for the first argument in \
62  a class method.'}
63                  ),
64 +               ('valid-metaclass-classmethod-first-arg',
65 +                {'default' : ('mcs',),
66 +                 'type' : 'csv',
67 +                 'metavar' : '<argument names>',
68 +                 'help' : 'List of valid names for the first argument in \
69 +a metaclass class method.'}
70 +                ),
71 
72                 )
73 
74      def __init__(self, linter=None):
75          BaseChecker.__init__(self, linter)
@@ -402,47 +419,68 @@
76 
77      def _check_first_arg_for_type(self, node, metaclass=0):
78          """check the name of first argument, expect:
79 
80          * 'self' for a regular method
81 -        * 'cls' for a class method
82 -        * 'mcs' for a metaclass
83 +        * 'cls' for a class method or a metaclass regular method (actually
84 +          valid-classmethod-first-arg value)
85 +        * 'mcs' for a metaclass class method (actually
86 +          valid-metaclass-classmethod-first-arg)
87          * not one of the above for a static method
88          """
89          # don't care about functions with unknown argument (builtins)
90          if node.args.args is None:
91              return
92          first_arg = node.args.args and node.argnames()[0]
93          self._first_attrs.append(first_arg)
94          first = self._first_attrs[-1]
95          # static method
96          if node.type == 'staticmethod':
97 -            if first_arg in ('self', 'cls', 'mcs'):
98 +            if (first_arg == 'self' or
99 +                first_arg in self.config.valid_classmethod_first_arg or
100 +                first_arg in self.config.valid_metaclass_classmethod_first_arg):
101                  self.add_message('W0211', args=first, node=node)
102 +                return
103              self._first_attrs[-1] = None
104          # class / regular method with no args
105          elif not node.args.args:
106              self.add_message('E0211', node=node)
107 -        # metaclass method
108 +        # metaclass
109          elif metaclass:
110 -            if first != 'mcs':
111 -                self.add_message('C0203', node=node)
112 -        # class method
113 -        elif node.type == 'classmethod':
114 -            if first not in self.config.valid_classmethod_first_arg:
115 -                if len(self.config.valid_classmethod_first_arg) == 1:
116 -                    valid = repr(self.config.valid_classmethod_first_arg[0])
117 -                else:
118 -                    valid = ', '.join(
119 -                      repr(v)
120 -                      for v in self.config.valid_classmethod_first_arg[:-1])
121 -                    valid = '%s or %r' % (
122 -                        valid, self.config.valid_classmethod_first_arg[-1])
123 -                self.add_message('C0202', args=valid, node=node)
124 -        # regular method without self as argument
125 -        elif first != 'self':
126 -            self.add_message('E0213', node=node)
127 +            # metaclass __new__ or classmethod
128 +            if node.type == 'classmethod':
129 +                self._check_first_arg_config(first,
130 +                    self.config.valid_metaclass_classmethod_first_arg, node,
131 +                    'C0204', node.name)
132 +            # metaclass regular method
133 +            else:
134 +                self._check_first_arg_config(first,
135 +                    self.config.valid_classmethod_first_arg, node, 'C0203',
136 +                    node.name)
137 +        # regular class
138 +        else:
139 +            # class method
140 +            if node.type == 'classmethod':
141 +                self._check_first_arg_config(first,
142 +                    self.config.valid_classmethod_first_arg, node, 'C0202',
143 +                    node.name)
144 +            # regular method without self as argument
145 +            elif first != 'self':
146 +                self.add_message('E0213', node=node)
147 +
148 +    def _check_first_arg_config(self, first, config, node, message,
149 +                                method_name):
150 +        if first not in config:
151 +            if len(config) == 1:
152 +                valid = repr(config[0])
153 +            else:
154 +                valid = ', '.join(
155 +                  repr(v)
156 +                  for v in config[:-1])
157 +                valid = '%s or %r' % (
158 +                    valid, config[-1])
159 +            self.add_message(message, args=(method_name, valid), node=node)
160 
161      def _check_bases_classes(self, node):
162          """check that the given class node implements abstract methods from
163          base classes
164          """
diff --git a/test/input/func_e0214.py b/test/input/func_e0214.py
@@ -1,18 +0,0 @@
165 -"""mcs test"""
166 -
167 -__revision__ = 1
168 -
169 -class MetaClass(type):
170 -    """a very intersting metaclass"""
171 -    def __new__(mcs, name, bases, cdict):
172 -        print mcs, name, bases, cdict
173 -        return type.__new__(mcs, name, bases, cdict)
174 -
175 -    def whatever(self):
176 -        """should have mcs has first arg"""
177 -        print self
178 -
179 -    def whatever_really(hop):
180 -        """could have anything has first arg"""
181 -        print hop
182 -    whatever_really = staticmethod(whatever_really)
diff --git a/test/input/func_first_arg.py b/test/input/func_first_arg.py
@@ -0,0 +1,42 @@
183 +# pylint: disable=C0111, W0232
184 +"""check for methods first arguments
185 +"""
186 +
187 +__revision__ = 0
188 +
189 +
190 +class Obj:
191 +    # C0202, classmethod
192 +    def __new__(something):
193 +        pass
194 +
195 +    # C0202, classmethod
196 +    def class1(cls):
197 +        pass
198 +    class1 = classmethod(class1)
199 +
200 +    def class2(other):
201 +        pass
202 +    class2 = classmethod(class2)
203 +
204 +
205 +class Meta(type):
206 +    # C0204, metaclass __new__
207 +    def __new__(other, name, bases, dct):
208 +        pass
209 +
210 +    # C0203, metaclass method
211 +    def method1(cls):
212 +        pass
213 +
214 +    def method2(other):
215 +        pass
216 +
217 +    # C0205, metaclass classmethod
218 +    def class1(mcs):
219 +        pass
220 +    class1 = classmethod(class1)
221 +
222 +    def class2(other):
223 +        pass
224 +    class2 = classmethod(class2)
diff --git a/test/input/func_noerror_mcs_attr_access.py b/test/input/func_noerror_mcs_attr_access.py
@@ -4,14 +4,14 @@
225 
226  __revision__ = 'yo'
227 
228  class Meta(type):
229      """the meta class"""
230 -    def __init__(mcs, name, bases, dictionary):
231 -        super(Meta, mcs).__init__(name, bases, dictionary)
232 -        print mcs, mcs._meta_args
233 -        delattr(mcs, '_meta_args')
234 +    def __init__(cls, name, bases, dictionary):
235 +        super(Meta, cls).__init__(name, bases, dictionary)
236 +        print cls, cls._meta_args
237 +        delattr(cls, '_meta_args')
238 
239  class Test(object):
240      """metaclassed class"""
241      __metaclass__ = Meta
242      _meta_args = ('foo', 'bar')
diff --git a/test/messages/func_e0203.txt b/test/messages/func_e0203.txt
@@ -1,1 +1,1 @@
243 -C: 12:Abcd.abcd: Class method should have 'cls' as first argument
244 +C: 12:Abcd.abcd: Class method abcd should have 'cls' as first argument
diff --git a/test/messages/func_e0214.txt b/test/messages/func_e0214.txt
@@ -1,2 +0,0 @@
245 -C: 11:MetaClass.whatever: Metaclass method should have "mcs" as first argument
246 -
diff --git a/test/messages/func_first_arg.txt b/test/messages/func_first_arg.txt
@@ -0,0 +1,5 @@
247 +C: 10:Obj.__new__: Class method __new__ should have 'cls' as first argument
248 +C: 18:Obj.class2: Class method class2 should have 'cls' as first argument
249 +C: 25:Meta.__new__: Metaclass class method __new__ should have 'mcs' as first argument
250 +C: 32:Meta.method2: Metaclass method method2 should have 'cls' as first argument
251 +C: 40:Meta.class2: Metaclass class method class2 should have 'mcs' as first argument