Hi,
I tried sending this before, but it I accidentally used HTML and it looks like it got stripped. See if this works better...

I have been seeing lots of R0903 "too few public methods" with classes that legitimately have none - context managers mostly. I have implemented a fix that suppresses that warning when it detects a well-formed set of __x__ methods, and as a by-product can warn when a set is only partly implemented. The diff is below. Hope this is the right procedure for offering a patch, and hope it meets the grade.


Thanks,
Pete.


diff -r f673b9738dcd checkers/design_analysis.py
--- a/checkers/design_analysis.py    Mon Jan 09 21:25:10 2012 +0100
+++ b/checkers/design_analysis.py    Sun Feb 05 15:31:26 2012 +0000
@@ -30,6 +30,41 @@
 # regexp for ignored argument name
 IGNORED_ARGUMENT_NAMES = re.compile('_.*')

+class SpecialMethodChecker:
+    """A functor that checks for consistency of a set of special methods"""
+    def __init__(self, methods_found, on_error):
+        """ Stores the set of __x__ method names that were found in the
+        class and a callable that will be called with args to R0024 if
+        the check fails"""
+        self.methods_found = methods_found
+        self.on_error = on_error
+
+    def __call__(self, methods_required, protocol):
+ """Checks the set of method names given to __init__ against the set required.
+        If they are all present, returns true.
+        If they are all absent, returns false.
+        If some are present, reports the error and returns false"""
+        required_methods_found = methods_required & self.methods_found
+        if required_methods_found == methods_required:
+            return True
+
+        if required_methods_found != set():
+ required_methods_missing = methods_required - self.methods_found
+            self.on_error((protocol,
+                           list(required_methods_found),
+                           list(required_methods_missing)))
+        return False
+
+SPECIAL_METHODS = [('Context manager', ('__enter__',
+                                        '__exit__')),
+                   ('Container', ('__len__',
+                                  '__getitem__',
+                                  '__setitem__',
+                                  '__delitem__',
+                                  '__iter__')),
+                   ('Callable', ('__call__',)),
+]
+
 def class_is_abstract(klass):
"""return true if the given class node should be considered as an abstract
     class
@@ -76,6 +111,9 @@
               ancestor.'),
     'R0923': ('Interface not implemented',
'Used when an interface class is not implemented anywhere.'),
+    'R0924': ('Badly implemented %s, implements %s but not %s',
+ 'A class implements some of the special methods for a particular \
+               protocol, but not all of them')
     }


@@ -223,9 +261,12 @@
     def leave_class(self, node):
         """check number of public methods"""
         nb_public_methods = 0
+        special_methods_found=set()
         for method in node.methods():
             if not method.name.startswith('_'):
                 nb_public_methods += 1
+            if method.name.startswith("__"):
+                special_methods_found.add(method.name)
         # Does the class contain less than 20 public methods ?
         if nb_public_methods > self.config.max_public_methods:
             self.add_message('R0904', node=node,
@@ -234,6 +275,14 @@
         # stop here for exception, metaclass and interface classes
         if node.type != 'class':
             return
+        # Does the class implement special methods consitently?
+        # If so, don't enforce minimum public methods.
+        check_special = SpecialMethodChecker(special_methods_found,
+ lambda args: self.add_message ('R0924', node=node, args = args)) + protocols = [check_special(set(p[1]), p[0]) for p in SPECIAL_METHODS]
+        if True in protocols:
+            return
+
         # Does the class contain more than 5 public methods ?
         if nb_public_methods < self.config.min_public_methods:
             self.add_message('R0903', node=node,
diff -r f673b9738dcd test/input/func_special_methods.py
--- /dev/null    Thu Jan 01 00:00:00 1970 +0000
+++ b/test/input/func_special_methods.py    Sun Feb 05 15:31:26 2012 +0000
@@ -0,0 +1,47 @@
+#pylint: disable=C0111
+__revision__ = None
+
+class ContextManager:
+    def __enter__(self):
+        pass
+    def __exit__(self, *args):
+        pass
+    def __init__(self):
+        pass
+
+class BadContextManager:
+    def __enter__(self):
+        pass
+    def __init__(self):
+        pass
+
+class Container:
+    def __init__(self):
+        pass
+    def __len__(self):
+        return 0
+    def __getitem__(self, key):
+        pass
+    def __setitem__(self, key, value):
+        pass
+    def __delitem__(self, key, value):
+        pass
+    def __iter__(self):
+        pass
+
+class BadContainer:
+    def __init__(self):
+        pass
+    def __len__(self):
+        return 0
+    def __setitem__(self, key, value):
+        pass
+    def __iter__(self):
+        pass
+
+class Callable:
+    def __call__(self):
+        pass
+    def __init__(self):
+        pass
+
diff -r f673b9738dcd test/messages/func_special_methods.txt
--- /dev/null    Thu Jan 01 00:00:00 1970 +0000
+++ b/test/messages/func_special_methods.txt Sun Feb 05 15:31:26 2012 +0000
@@ -0,0 +1,5 @@
+R: 12:BadContextManager: Badly implemented Context manager, implements ['__enter__'] but not ['__exit__']
+R: 12:BadContextManager: Too few public methods (0/2)
+R: 32:BadContainer: Badly implemented Container, implements ['__setitem__', '__iter__', '__len__'] but not ['__delitem__', '__getitem__']
+R: 32:BadContainer: Too few public methods (0/2)
+

_______________________________________________
Python-Projects mailing list
[email protected]
http://lists.logilab.org/mailman/listinfo/python-projects

Reply via email to