summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Marek <shlomme@gmail.com>2014-04-09 09:04:32 +0200
committerTorsten Marek <shlomme@gmail.com>2014-04-09 09:04:32 +0200
commit4d9e83a9c8e04383da565bfa188c583e193750c1 (patch)
tree368e2344ab831e0c108ee1a179825604fd7344da
parent413a6b712da2afa80f74b7c740df5cfb41294cae (diff)
downloadpylint-4d9e83a9c8e04383da565bfa188c583e193750c1.tar.gz
Added support for enforcing multiple, but consistent name styles for different name types inside a single module.
-rw-r--r--ChangeLog4
-rw-r--r--checkers/base.py37
-rw-r--r--doc/index.rst1
-rw-r--r--doc/options.rst128
-rw-r--r--test/test_base.py67
5 files changed, 236 insertions, 1 deletions
diff --git a/ChangeLog b/ChangeLog
index b3e1b42..e7b416d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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()