Skip to content

Experimental: Unify NamingSchemes. #3704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ contributors:

* Jochen Preusche (iilei): contributor

* Buck Evan (bukzor, Google LLC): contributor

* Ram Rachum (cool-RR)

* Pieter Engelbrecht
Expand Down
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ Pylint's ChangeLog

Close #3834

* The `invalid-name` checker has been re-worked for increased consistency. Some
names that previously passed unnoticed will be flagged. You may solve these
new issues three (or more) ways:

1. increase the length of names to be more descriptive (recommended)
2. add strange-but-useful names to the good-names configuration
3. override the default regexen, via pylintrc (not recommended)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that not recommended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not recommended because even for those deeply familiar with regex (few) it is much too easy to make regex with unintended matches / exclusions.



What's New in Pylint 2.6.1?
===========================
Expand Down
118 changes: 52 additions & 66 deletions pylint/checkers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
import itertools
import re
import sys
from typing import Pattern

import astroid
import astroid.bases
Expand All @@ -81,83 +80,69 @@


class NamingStyle:
# It may seem counterintuitive that single naming style
# has multiple "accepted" forms of regular expressions,
# but we need to special-case stuff like dunder names
# in method names.
CLASS_NAME_RGX = None # type: Pattern[str]
MOD_NAME_RGX = None # type: Pattern[str]
CONST_NAME_RGX = None # type: Pattern[str]
COMP_VAR_RGX = None # type: Pattern[str]
DEFAULT_NAME_RGX = None # type: Pattern[str]
CLASS_ATTRIBUTE_RGX = None # type: Pattern[str]

@classmethod
def get_regex(cls, name_type):
return {
"module": cls.MOD_NAME_RGX,
"const": cls.CONST_NAME_RGX,
"class": cls.CLASS_NAME_RGX,
"function": cls.DEFAULT_NAME_RGX,
"method": cls.DEFAULT_NAME_RGX,
"attr": cls.DEFAULT_NAME_RGX,
"argument": cls.DEFAULT_NAME_RGX,
"variable": cls.DEFAULT_NAME_RGX,
"class_attribute": cls.CLASS_ATTRIBUTE_RGX,
"inlinevar": cls.COMP_VAR_RGX,
}[name_type]

name_template = r"[^\W\d_%s][^\W%s]%s"

def __init__(self, head_exclude: str, tail_exclude: str, min_length: int):
self.head_exclude = head_exclude
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it could be interesting to add comments explaining what do you call head and tail. From what i understand, head is only one character long whereas tail is all the rest. I'm i right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. As in head and tail of a snake -- the tail is everything but the head.

self.tail_exclude = tail_exclude
self.min_length = min_length

if min_length == 1:
tail_length = "*"
elif min_length == 2:
tail_length = "+"
elif min_length > 2:
tail_length = "{%i,}" % (min_length - 1)
else:
raise AssertionError(tail_length)

class SnakeCaseStyle(NamingStyle):
"""Regex rules for snake_case naming style."""
word = self.name_template % (head_exclude, tail_exclude, tail_length)
snake_word = self.name_template % ("A-Z", "A-Z", tail_length)

CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]+$")
MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$")
CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]*|__.*__)$")
COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$")
DEFAULT_NAME_RGX = re.compile(
r"([^\W\dA-Z][^\WA-Z]{2,}|_[^\WA-Z]*|__[^\WA-Z\d_][^\WA-Z]+__)$"
)
CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]{2,}|__.*__)$")
name = r"_{0,2}%s" % word
dunder = r"__%s__" % snake_word

self.NAME_RGX = re.compile("^(%s)$" % name)
self.ATTR_RGX = re.compile("^(%s|%s)$" % (name, dunder))

class CamelCaseStyle(NamingStyle):
"""Regex rules for camelCase naming style."""

CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]+$")
MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$")
CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]*|__.*__)$")
COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$")
DEFAULT_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$")
CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__.*__)$")
def get_regex(self, name_type):
# It may seem counterintuitive that single naming style
# has multiple "accepted" forms of regular expressions,
# but we need to special-case stuff like dunder names
# in method names.
return {
"module": self.NAME_RGX,
"const": self.ATTR_RGX,
"class": self.NAME_RGX,
"function": self.ATTR_RGX,
"method": self.ATTR_RGX,
"attr": self.ATTR_RGX,
"argument": self.ATTR_RGX,
"variable": self.ATTR_RGX,
"class_attribute": self.ATTR_RGX,
"inlinevar": self.NAME_RGX,
}[name_type]


class PascalCaseStyle(NamingStyle):
"""Regex rules for PascalCase naming style."""
# Regex rules for snake_case naming style.
SnakeCaseStyle = NamingStyle("A-Z", "A-Z", 3)

CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]*|__.*__)$")
COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$")
CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\W_]{2,}$")
# Regex rules for camelCase naming style.
CamelCaseStyle = NamingStyle("A-Z", "_", 3)

# Regex rules for PascalCase naming style.
PascalCaseStyle = NamingStyle("a-z", "_", 3)

class UpperCaseStyle(NamingStyle):
"""Regex rules for UPPER_CASE naming style."""
# Regex rules for UPPER_CASE naming style.
UpperCaseStyle = NamingStyle("a-z", "a-z", 3)

CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]*|__.*__)$")
COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]{2,}|__[^\W\dA-Z_]\w+__)$")
CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\Wa-z]{2,}$")
AnyStyle = NamingStyle("", "", 1)


class AnyStyle(NamingStyle):
@classmethod
def get_regex(cls, name_type):
return re.compile(".*")
class NoStyle(NamingStyle):
@staticmethod
def get_regex(name_type):
return re.compile(".+")


NAMING_STYLES = {
Expand All @@ -166,6 +151,7 @@ def get_regex(cls, name_type):
"PascalCase": PascalCaseStyle,
"UPPER_CASE": UpperCaseStyle,
"any": AnyStyle,
"none": NoStyle,
}

# do not require a doc string on private/system methods
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[pytest]
python_files=*test_*.py
addopts=-m "not acceptance"
addopts=-m "not acceptance" --tb=short
markers =
acceptance
10 changes: 9 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
from pylint.lint import PyLinter
from pylint.testutils import MinimalTestReporter

TOP = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PYLINTRC = os.path.join(TOP, "pylintrc")


@pytest.fixture(autouse=True)
def pylintrc():
os.environ["PYLINTRC"] = PYLINTRC


@pytest.fixture
def linter(checker, register, enable, disable, reporter):
Expand All @@ -24,7 +32,7 @@ def linter(checker, register, enable, disable, reporter):
if enable:
for msg in enable:
_linter.enable(msg)
os.environ.pop("PYLINTRC", None)
os.environ["PYLINTRC"] = PYLINTRC
return _linter


Expand Down
2 changes: 1 addition & 1 deletion tests/extensions/data/compare_to_zero.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable=literal-comparison,missing-docstring,misplaced-comparison-constant
# pylint: disable=literal-comparison,missing-docstring,misplaced-comparison-constant,invalid-name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find a little bit damageable if pylint is not compatible with constant name that are one letter long.
What do you think about it?


X = 123
Y = len('test')
Expand Down
2 changes: 1 addition & 1 deletion tests/extensions/data/empty_string_comparison.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable=literal-comparison,missing-docstring
# pylint: disable=literal-comparison,missing-docstring,invalid-name

X = ''
Y = 'test'
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/a/arguments_differ.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[basics]
good-names=___private3
2 changes: 1 addition & 1 deletion tests/functional/l/line_too_long.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
""" that one is too long tooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo loooooong"""

# The next line is exactly 80 characters long.
A = "--------------------------------------------------------------------------"
_ = "--------------------------------------------------------------------------"

# Do not trigger the line-too-long warning if the only token that makes the
# line longer than 80 characters is a trailing pylint disable.
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/m/method_hidden.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ class CustomProperty:
def __init__(self, _):
pass

def __get__(self, obj, __):
def __get__(self, obj, _):
if not obj:
return self
return 5

def __set__(self, _, __):
def __set__(self, _key, _val):
pass

class Ddef:
Expand Down
6 changes: 3 additions & 3 deletions tests/functional/n/namePresetCamelCase.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('([^\\W\\dA-Z][^\\W_]*|__.*__)$' pattern)"
invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to camelCase naming style ('[^\\W\\dA-Z][^\\W_]+$' pattern)"
invalid-name:22:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('([^\\W\\dA-Z][^\\W_]{2,}|__[^\\W\\dA-Z_]\\w+__)$' pattern)"
invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to camelCase naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\W_]{2,}|__[^\\W\\d_A-Z][^\\WA-Z]{2,}__)$' pattern)"
invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to camelCase naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\W_]{2,})$' pattern)"
invalid-name:22:say_hello:"Function name ""say_hello"" doesn't conform to camelCase naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\W_]{2,}|__[^\\W\\d_A-Z][^\\WA-Z]{2,}__)$' pattern)"
2 changes: 1 addition & 1 deletion tests/functional/n/name_good_bad_names_regex.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
blacklisted-name:5::"Black listed name ""explicit_bad_some_constant"""
invalid-name:7::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]*|__.*__)$' pattern)"
invalid-name:7::"Constant name ""snake_case_bad_SOME_CONSTANT"" doesn't conform to snake_case naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\WA-Z]{2,}|__[^\\W\\d_A-Z][^\\WA-Z]{2,}__)$' pattern)"
blacklisted-name:19:blacklisted_2_snake_case:"Black listed name ""blacklisted_2_snake_case"""
6 changes: 3 additions & 3 deletions tests/functional/n/name_preset_snake_case.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]*|__.*__)$' pattern)"
invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to snake_case naming style ('[^\\W\\dA-Z][^\\WA-Z]+$' pattern)"
invalid-name:22:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]{2,}|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)"
invalid-name:3::"Constant name ""SOME_CONSTANT"" doesn't conform to snake_case naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\WA-Z]{2,}|__[^\\W\\d_A-Z][^\\WA-Z]{2,}__)$' pattern)"
invalid-name:10:MyClass:"Class name ""MyClass"" doesn't conform to snake_case naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\WA-Z]{2,})$' pattern)"
invalid-name:22:sayHello:"Function name ""sayHello"" doesn't conform to snake_case naming style ('^(_{0,2}[^\\W\\d_A-Z][^\\WA-Z]{2,}|__[^\\W\\d_A-Z][^\\WA-Z]{2,}__)$' pattern)"
2 changes: 2 additions & 0 deletions tests/functional/r/regression_2443_duplicate_bases.rc
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
[testoptions]
min_pyver=3.6
[basics]
good-names=IN
2 changes: 2 additions & 0 deletions tests/functional/u/undefined_variable.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[basics]
good-names=i,j,k,ex,Run,_,_dt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again i think it is very risky to forbid using single character named variable...

4 changes: 2 additions & 2 deletions tests/functional/u/unnecessary_pass.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# pylint: disable=missing-docstring, too-few-public-methods

try:
A = 2
_ = 2
except ValueError:
A = 24
_ = 24
pass # [unnecessary-pass]

def docstring_only():
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/w/wrong_import_position11.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Checks import position rule"""
# pylint: disable=unused-import,pointless-string-statement
A = 1
_ = 1
import os # [wrong-import-position]
2 changes: 1 addition & 1 deletion tests/functional/w/wrong_import_position13.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Checks import position rule"""
# pylint: disable=unused-import,no-name-in-module
A = 1
_ = 1
from sys import x # [wrong-import-position]
2 changes: 1 addition & 1 deletion tests/input/func_excess_escapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
__revision__ = '$Id$'

# Bad escape sequences, which probably don't do what you expect.
A = "\[\]\\"
_ = "\[\]\\"
assert '\/' == '\\/'
ESCAPE_BACKSLASH = '\`'

Expand Down
10 changes: 5 additions & 5 deletions tests/input/func_typecheck_callfunc_assigment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ def func_no_return():
"""function without return"""
print('dougloup')

A = func_no_return()
_ = func_no_return()


def func_return_none():
"""function returning none"""
print('dougloup')
return None

A = func_return_none()
_ = func_return_none()


def func_implicit_return_none():
"""Function returning None from bare return statement."""
return

A = func_implicit_return_none()
_ = func_implicit_return_none()


def func_return_none_and_smth():
Expand All @@ -40,13 +40,13 @@ def func_return_none_and_smth():
return None
return 3

A = func_return_none_and_smth()
_ = func_return_none_and_smth()

def generator():
"""no problemo"""
yield 2

A = generator()
_ = generator()

class Abstract(object):
"""bla bla"""
Expand Down
3 changes: 2 additions & 1 deletion tests/input/func_w0801.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""test code similarities
by defaut docstring are not considered
by default docstring are not considered
"""
# pylint: disable=invalid-name
__revision__ = 'id'
A = 2
B = 3
Expand Down
3 changes: 2 additions & 1 deletion tests/input/w0801_same.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""test code similarities
by defaut docstring are not considered
by default docstring are not considered
"""
# pylint: disable=invalid-name
__revision__ = 'id'
A = 2
B = 3
Expand Down
4 changes: 2 additions & 2 deletions tests/messages/func_w0801.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
R: 1: Similar lines in 2 files
==input.func_w0801:3
==input.w0801_same:3
==input.func_w0801:4
==input.w0801_same:4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change needed?

__revision__ = 'id'
A = 2
B = 3
Expand Down