diff options
author | Torsten Marek <shlomme@gmail.com> | 2014-04-09 09:04:32 +0200 |
---|---|---|
committer | Torsten Marek <shlomme@gmail.com> | 2014-04-09 09:04:32 +0200 |
commit | 4d9e83a9c8e04383da565bfa188c583e193750c1 (patch) | |
tree | 368e2344ab831e0c108ee1a179825604fd7344da | |
parent | 413a6b712da2afa80f74b7c740df5cfb41294cae (diff) | |
download | pylint-4d9e83a9c8e04383da565bfa188c583e193750c1.tar.gz |
Added support for enforcing multiple, but consistent name styles for different name types inside a single module.
-rw-r--r-- | ChangeLog | 4 | ||||
-rw-r--r-- | checkers/base.py | 37 | ||||
-rw-r--r-- | doc/index.rst | 1 | ||||
-rw-r--r-- | doc/options.rst | 128 | ||||
-rw-r--r-- | test/test_base.py | 67 |
5 files changed, 236 insertions, 1 deletions
@@ -10,6 +10,10 @@ ChangeLog for Pylint * Add new warning 'eval-used', checking that the builtin function `eval` was used. + * Added support for enforcing multiple, but consistent name styles for + different name types inside a single module; based on a patch written + by morbo@google.com. + * Also warn about empty docstrings on overridden methods; contributed by sebastianu@google.com. diff --git a/checkers/base.py b/checkers/base.py index 7fe0759..279d5b1 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -58,6 +58,9 @@ if sys.version_info < (3, 0): BAD_FUNCTIONS.append('input') BAD_FUNCTIONS.append('file') +# Name categories that are always consistent with all naming conventions. +EXEMPT_NAME_CATEGORIES = {'exempt', 'ignore'} + del re def in_loop(node): @@ -904,8 +907,20 @@ class NameChecker(_BasicChecker): 'help' : 'Bad variable names which should always be refused, ' 'separated by a comma'} ), + ('name-group', + {'default' : (), + 'type' :'csv', 'metavar' : '<name1:name2>', + 'help' : ('Colon-delimited sets of names that determine each' + ' other\'s naming style when the name regexes' + ' allow several styles.')} + ), ) + def __init__(self, linter): + _BasicChecker.__init__(self, linter) + self._name_category = {} + self._name_group = {} + def open(self): self.stats = self.linter.add_stats(badname_module=0, badname_class=0, badname_function=0, @@ -915,6 +930,9 @@ class NameChecker(_BasicChecker): badname_inlinevar=0, badname_argument=0, badname_class_attribute=0) + for group in self.config.name_group: + for name_type in group.split(':'): + self._name_group[name_type] = 'group_%s' % (group,) @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): @@ -976,6 +994,14 @@ class NameChecker(_BasicChecker): else: self._recursive_check_names(arg.elts, node) + def _find_name_group(self, node_type): + return self._name_group.get(node_type, node_type) + + def _is_multi_naming_match(self, match): + return (match is not None and + match.lastgroup is not None and + match.lastgroup not in EXEMPT_NAME_CATEGORIES) + def _check_name(self, node_type, name, node): """check for a name using the type's regexp""" if is_inside_except(node): @@ -989,7 +1015,16 @@ class NameChecker(_BasicChecker): self.add_message('blacklisted-name', node=node, args=name) return regexp = getattr(self.config, node_type + '_rgx') - if regexp.match(name) is None: + match = regexp.match(name) + + if self._is_multi_naming_match(match): + name_group = self._find_name_group(node_type) + if name_group not in self._name_category: + self._name_category[name_group] = match.lastgroup + elif self._name_category[name_group] != match.lastgroup: + match = None + + if match is None: type_label = {'inlinedvar': 'inlined variable', 'const': 'constant', 'attr': 'attribute', diff --git a/doc/index.rst b/doc/index.rst index bc6f9d8..7b8725c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -13,6 +13,7 @@ https://bitbucket.org/logilab/pylint output message-control features + options extend ide-integration plugins diff --git a/doc/options.rst b/doc/options.rst new file mode 100644 index 0000000..ec17fb0 --- /dev/null +++ b/doc/options.rst @@ -0,0 +1,128 @@ +.. -*- coding: utf-8 -*- + +=============== + Configuration +=============== + +Naming Styles +------------- + +PyLint recognizes a number of different name types internally. With a few +exceptions, the type of the name is governed by the location the assignment to a +name is found in, and not the type of object assigned. + +``module`` + Module and package names, same as the file names. +``const`` + Module-level constants, any variable defined at module level that is not bound to a class object. +``class`` + Names in ``class`` statements, as well as names bound to class objects at module level. +``function`` + Functions, toplevel or nested in functions or methods. +``method`` + Methods, functions defined in class bodies. Includes static and class methods. +``attr`` + Attributes created on class instances inside methods. +``argument`` + Arguments to any function type, including lambdas. +``variable`` + Local variables in function scopes. +``class-attribute`` + Attributes defined in class bodies. +``inlinevar`` + Loop variables in list comprehensions and generator expressions. + +For each naming style, a separate regular expression matching valid names of +this type can be defined. By default, the regular expressions will enforce PEP8 +names. + +Regular expressions for the names are anchored at the beginning, any anchor for +the end must be supplied explicitly. Any name not matching the regular +expression will lead to an instance of ``invalid-name``. + + +.. option:: --module-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --const-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --class-rgx=<regex> + + Default value: ``'[A-Z_][a-zA-Z0-9]+$`` + +.. option:: --function-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --method-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --attr-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --argument-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --variable-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --class-attribute-rgx=<regex> + + Default value: ``([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$`` + +.. option:: --inlinevar-rgx=<regex> + + Default value: ``[A-Za-z_][A-Za-z0-9_]*$`` + +Multiple Naming Styles +^^^^^^^^^^^^^^^^^^^^^^ + +Large code bases that have been worked on for multiple years often exhibit an +evolution in style as well. In some cases, modules can be in the same package, +but still have different naming style based on the stratum they belong to. +However, intra-module consistency should still be required, to make changes +inside a single file easier. For this case, PyLint supports regular expression +with several named capturing group. + +The capturing group of the first valid match taints the module and enforces the +same group to be triggered on every subsequent occurrence of this name. + +Consider the following (simplified) example:: + + pylint --function-rgx='(?:(?P<snake>[a-z_]+)|(?P<camel>_?[A-Z]+))$' sample.py + +The regular expression defines two naming styles, ``snake`` for snake-case +names, and ``camel`` for camel-case names. + +In ``sample.py``, the function name on line 1 will taint the module and enforce +the match of named group ``snake`` for the remainder of the module:: + + def trigger_snake_case(arg): + ... + + def InvalidCamelCase(arg): + ... + + def valid_snake_case(arg): + ... + +Because of this, the name on line 4 will trigger an ``invalid-name`` warning, +even though the name matches the given regex. + +Matches named ``exempt`` or ``ignore`` can be used for non-tainting names, to +prevent built-in or interface-dictated names to trigger certain naming styles. + +.. option:: --name-group=<name1:name2:...,...> + + Default value: empty + + Format: comma-separated groups of colon-separated names. + + This option can be used to combine name styles. For example, ``function:method`` enforces that functions and methods use the same style, and a style triggered by either name type carries over to the other. This requires that the regular expression for the combined name types use the same group names. diff --git a/test/test_base.py b/test/test_base.py index e303bd7..617366c 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -130,6 +130,73 @@ class NameCheckerTest(CheckerTestCase): self.checker.visit_assname(assign.targets[0]) +class MultiNamingStyleTest(CheckerTestCase): + CHECKER_CLASS = base.NameChecker + + MULTI_STYLE_RE = re.compile('(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$') + + @set_config(class_rgx=MULTI_STYLE_RE) + def test_multi_name_detection_first(self): + classes = test_utils.extract_node(""" + class CLASSA(object): #@ + pass + class classb(object): #@ + pass + class CLASSC(object): #@ + pass + """) + with self.assertAddsMessages(Message('invalid-name', node=classes[1], args=('class', 'classb'))): + for cls in classes: + self.checker.visit_class(cls) + + @set_config(class_rgx=MULTI_STYLE_RE) + def test_multi_name_detection_first_invalid(self): + classes = test_utils.extract_node(""" + class class_a(object): #@ + pass + class classb(object): #@ + pass + class CLASSC(object): #@ + pass + """) + with self.assertAddsMessages(Message('invalid-name', node=classes[0], args=('class', 'class_a')), + Message('invalid-name', node=classes[2], args=('class', 'CLASSC'))): + for cls in classes: + self.checker.visit_class(cls) + + @set_config(method_rgx=MULTI_STYLE_RE, + function_rgx=MULTI_STYLE_RE, + name_group=('function:method',)) + def test_multi_name_detection_group(self): + function_defs = test_utils.extract_node(""" + class First(object): + def func(self): #@ + pass + + def FUNC(): #@ + pass + """, module_name='test') + with self.assertAddsMessages(Message('invalid-name', node=function_defs[1], args=('function', 'FUNC'))): + for func in function_defs: + self.checker.visit_function(func) + + @set_config(function_rgx=re.compile('(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$')) + def test_multi_name_detection_exempt(self): + function_defs = test_utils.extract_node(""" + def FOO(): #@ + pass + def lower(): #@ + pass + def FOO(): #@ + pass + def UPPER(): #@ + pass + """) + with self.assertAddsMessages(Message('invalid-name', node=function_defs[3], args=('function', 'UPPER'))): + for func in function_defs: + self.checker.visit_function(func) + + if __name__ == '__main__': from logilab.common.testlib import unittest_main unittest_main() |