diff --git a/doc/data/messages/i/invalid-name/details.rst b/doc/data/messages/i/invalid-name/details.rst index ad155bc147..7e2090158a 100644 --- a/doc/data/messages/i/invalid-name/details.rst +++ b/doc/data/messages/i/invalid-name/details.rst @@ -88,11 +88,11 @@ The following type of names are checked with a predefined pattern: | ``typevar`` | ``T``, ``_CallableT``, ``_T_co``, ``AnyStr``, | ``DICT_T``, ``CALLABLE_T``, ``ENUM_T``, ``DeviceType``, | | | ``DeviceTypeT``, ``IPAddressT`` | ``_StrType`` | +--------------------+-------------------------------------------------------+------------------------------------------------------------+ -| ``typealias`` | ``GoodName``, ``_GoodName``, ``IPAddressType`` and | ``BadNameT``, ``badName``, ``TBadName``, ``TypeBadName`` | -| | other PascalCase variants that don't start with ``T``| | -| | or ``Type``. This is to distinguish them from | | -| | ``typevars``. Note that ``TopName`` is allowed but | | -| | ``TTopName`` isn't. | | +| ``typealias`` | ``GoodName``, ``_GoodName``, ``IPAddressType``, | ``BadNameT``, ``badName``, ``TBadName``, ``TypeBadName``, | +| | ``GoodName2`` and other PascalCase variants that | ``_1BadName`` | +| | don't start with ``T`` or ``Type``. This is to | | +| | distinguish them from ``typevars``. Note that | | +| | ``TopName`` is allowed but ``TTopName`` isn't. | | +--------------------+-------------------------------------------------------+------------------------------------------------------------+ Custom regular expressions diff --git a/doc/whatsnew/2/2.17/index.rst b/doc/whatsnew/2/2.17/index.rst index b3f4b61757..cd06bbc2e7 100644 --- a/doc/whatsnew/2/2.17/index.rst +++ b/doc/whatsnew/2/2.17/index.rst @@ -29,6 +29,42 @@ so we find problems before the actual release. .. towncrier release notes start +What's new in Pylint 2.17.2? +---------------------------- +Release date: 2023-04-03 + + +False Positives Fixed +--------------------- + +- ``invalid-name`` now allows for integers in ``typealias`` names: + - now valid: ``Good2Name``, ``GoodName2``. + - still invalid: ``_1BadName``. + + Closes #8485 (`#8485 `_) + +- No longer consider ``Union`` as type annotation as type alias for naming + checks. + + Closes #8487 (`#8487 `_) + +- ``unnecessary-lambda`` no longer warns on lambdas which use its parameters in + their body (other than the final arguments), e.g. + ``lambda foo: (bar if foo else baz)(foo)``. + + Closes #8496 (`#8496 `_) + + + +Other Bug Fixes +--------------- + +- Fix a crash in pyreverse when "/" characters are used in the output filename + e.g pyreverse -o png -p name/ path/to/project. + + Closes #8504 (`#8504 `_) + + What's new in Pylint 2.17.1? ---------------------------- Release date: 2023-03-22 diff --git a/doc/whatsnew/fragments/7506.false_positive b/doc/whatsnew/fragments/7506.false_positive new file mode 100644 index 0000000000..c6424e1f2e --- /dev/null +++ b/doc/whatsnew/fragments/7506.false_positive @@ -0,0 +1,3 @@ +Fix ``unused-import`` false positive for usage of ``six.with_metaclass``. + +Closes #7506 diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 897c373b9b..beebd2492a 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "2.17.1" +__version__ = "2.17.2" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/base/basic_checker.py b/pylint/checkers/base/basic_checker.py index 08d582b7fb..062e67a970 100644 --- a/pylint/checkers/base/basic_checker.py +++ b/pylint/checkers/base/basic_checker.py @@ -519,6 +519,7 @@ def _has_variadic_argument( ) @utils.only_required_for_messages("unnecessary-lambda") + # pylint: disable-next=too-many-return-statements def visit_lambda(self, node: nodes.Lambda) -> None: """Check whether the lambda is suspicious.""" # if the body of the lambda is a call expression with the same @@ -576,6 +577,13 @@ def visit_lambda(self, node: nodes.Lambda) -> None: if arg.name != passed_arg.name: return + # The lambda is necessary if it uses its parameter in the function it is + # calling in the lambda's body + # e.g. lambda foo: (func1 if foo else func2)(foo) + for name in call.func.nodes_of_class(nodes.Name): + if name.lookup(name.name)[0] is node: + return + self.add_message("unnecessary-lambda", line=node.fromlineno, node=node) @utils.only_required_for_messages("dangerous-default-value") diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py index 1341edc96b..c2b615a481 100644 --- a/pylint/checkers/base/name_checker/checker.py +++ b/pylint/checkers/base/name_checker/checker.py @@ -41,7 +41,9 @@ "typevar": re.compile( r"^_{0,2}(?!T[A-Z])(?:[A-Z]+|(?:[A-Z]+[a-z]+)+T?(? bool: inferred = utils.safe_infer(node) if isinstance(inferred, nodes.ClassDef): if inferred.qname() == ".Union": - return True + # Union is a special case because it can be used as a type alias + # or as a type annotation. We only want to check the former. + assert node is not None + return not ( + isinstance(node.parent, nodes.AnnAssign) + and node.parent.value is not None + ) elif isinstance(inferred, nodes.FunctionDef): if inferred.qname() == "typing.TypeAlias": return True diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 1ed70d9583..ef7fe50ccf 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -37,6 +37,12 @@ if TYPE_CHECKING: from pylint.lint import PyLinter +if sys.version_info >= (3, 8): + from functools import cached_property +else: + from astroid.decorators import cachedproperty as cached_property + + # The dictionary with Any should actually be a _ImportTree again # but mypy doesn't support recursive types yet _ImportTree = Dict[str, Union[List[Dict[str, Any]], List[str]]] @@ -997,7 +1003,7 @@ def _report_external_dependencies( self, sect: Section, _: LinterStats, _dummy: LinterStats | None ) -> None: """Return a verbatim layout for displaying dependencies.""" - dep_info = _make_tree_defs(self._external_dependencies_info().items()) + dep_info = _make_tree_defs(self._external_dependencies_info.items()) if not dep_info: raise EmptyReportError() tree_str = _repr_tree_defs(dep_info) @@ -1019,10 +1025,10 @@ def _report_dependencies_graph( _make_graph(filename, dep_info, sect, "") filename = self.linter.config.ext_import_graph if filename: - _make_graph(filename, self._external_dependencies_info(), sect, "external ") + _make_graph(filename, self._external_dependencies_info, sect, "external ") filename = self.linter.config.int_import_graph if filename: - _make_graph(filename, self._internal_dependencies_info(), sect, "internal ") + _make_graph(filename, self._internal_dependencies_info, sect, "internal ") def _filter_dependencies_graph(self, internal: bool) -> defaultdict[str, set[str]]: """Build the internal or the external dependency graph.""" @@ -1035,14 +1041,14 @@ def _filter_dependencies_graph(self, internal: bool) -> defaultdict[str, set[str graph[importee].add(importer) return graph - @astroid.decorators.cached + @cached_property def _external_dependencies_info(self) -> defaultdict[str, set[str]]: """Return cached external dependencies information or build and cache them. """ return self._filter_dependencies_graph(internal=False) - @astroid.decorators.cached + @cached_property def _internal_dependencies_info(self) -> defaultdict[str, set[str]]: """Return cached internal dependencies information or build and cache them. diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py index 3d0a4613ab..d92f4e2e56 100644 --- a/pylint/pyreverse/writer.py +++ b/pylint/pyreverse/writer.py @@ -41,7 +41,7 @@ def __init__(self, config: argparse.Namespace) -> None: def write(self, diadefs: Iterable[ClassDiagram | PackageDiagram]) -> None: """Write files for according to .""" for diagram in diadefs: - basename = diagram.title.strip().replace(" ", "_") + basename = diagram.title.strip().replace("/", "_").replace(" ", "_") file_name = f"{basename}.{self.config.output_format}" if os.path.exists(self.config.output_directory): file_name = os.path.join(self.config.output_directory, file_name) diff --git a/pyproject.toml b/pyproject.toml index 333d2856ed..559341011d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,8 +38,8 @@ dependencies = [ "platformdirs>=2.2.0", # Also upgrade requirements_test_min.txt. # Pinned to dev of second minor update to allow editable installs and fix primer issues, - # see https://github.com/PyCQA/astroid/issues/1341 - "astroid>=2.15.0,<=2.17.0-dev0", + # see https://github.com/pylint-dev/astroid/issues/1341 + "astroid>=2.15.2,<=2.17.0-dev0", "isort>=4.2.5,<6", "mccabe>=0.6,<0.8", "tomli>=1.1.0;python_version<'3.11'", diff --git a/requirements_test.txt b/requirements_test.txt index 3a9ad3f361..8a42362bf2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,6 +6,7 @@ contributors-txt>=1.0.0 pytest-cov~=4.0 pytest-profiling~=1.7 pytest-xdist~=3.2 +six # Type packages for mypy types-pkg_resources==0.1.3 tox>=3 diff --git a/requirements_test_min.txt b/requirements_test_min.txt index d3a76f096c..08321b54fd 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,6 +1,6 @@ -e .[testutils,spelling] # astroid dependency is also defined in pyproject.toml -astroid==2.15.0 # Pinned to a specific version for tests +astroid==2.15.2 # Pinned to a specific version for tests typing-extensions~=4.5 py~=1.11.0 pytest~=7.2 diff --git a/tbump.toml b/tbump.toml index 063a79c0ea..1f6594b2e7 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/pylint" [version] -current = "2.17.1" +current = "2.17.2" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/functional/t/typealias_naming_style_default.py b/tests/functional/t/typealias_naming_style_default.py index 45f801521d..6b27c14a01 100644 --- a/tests/functional/t/typealias_naming_style_default.py +++ b/tests/functional/t/typealias_naming_style_default.py @@ -9,6 +9,8 @@ GOODName: TypeAlias = int GOODNAMEType: TypeAlias = int TodoType: TypeAlias = int +Good2Name: TypeAlias = int +GoodName2: TypeAlias = int # Non-PascalCase names BadNAME: TypeAlias = int # [invalid-name] @@ -20,4 +22,9 @@ BAD_NAME = Union[int, str] # [invalid-name] _BAD_NAME = Union[int, str] # [invalid-name] __BAD_NAME = Union[int, str] # [invalid-name] +_1BadName = Union[int, str] # [invalid-name] ANOTHERBADNAME = Union[int, str] # [invalid-name] + +# Regression tests +# This is not a TypeAlias, and thus shouldn't flag the message +x: Union[str, int] = 42 diff --git a/tests/functional/t/typealias_naming_style_default.txt b/tests/functional/t/typealias_naming_style_default.txt index 237dac0762..455cbc2131 100644 --- a/tests/functional/t/typealias_naming_style_default.txt +++ b/tests/functional/t/typealias_naming_style_default.txt @@ -1,10 +1,11 @@ -invalid-name:14:0:14:7::"Type alias name ""BadNAME"" doesn't conform to predefined naming style":HIGH -invalid-name:15:0:15:7::"Type alias name ""badName"" doesn't conform to predefined naming style":HIGH -invalid-name:16:0:16:11::"Type alias name ""AlsoBADName"" doesn't conform to predefined naming style":HIGH -invalid-name:17:0:17:8::"Type alias name ""TBadName"" doesn't conform to predefined naming style":HIGH -invalid-name:18:0:18:8::"Type alias name ""TypeTodo"" doesn't conform to predefined naming style":HIGH -invalid-name:19:0:19:8::"Type alias name ""BadNameT"" doesn't conform to predefined naming style":HIGH -invalid-name:20:0:20:8::"Type alias name ""BAD_NAME"" doesn't conform to predefined naming style":HIGH -invalid-name:21:0:21:9::"Type alias name ""_BAD_NAME"" doesn't conform to predefined naming style":HIGH -invalid-name:22:0:22:10::"Type alias name ""__BAD_NAME"" doesn't conform to predefined naming style":HIGH -invalid-name:23:0:23:14::"Type alias name ""ANOTHERBADNAME"" doesn't conform to predefined naming style":HIGH +invalid-name:16:0:16:7::"Type alias name ""BadNAME"" doesn't conform to predefined naming style":HIGH +invalid-name:17:0:17:7::"Type alias name ""badName"" doesn't conform to predefined naming style":HIGH +invalid-name:18:0:18:11::"Type alias name ""AlsoBADName"" doesn't conform to predefined naming style":HIGH +invalid-name:19:0:19:8::"Type alias name ""TBadName"" doesn't conform to predefined naming style":HIGH +invalid-name:20:0:20:8::"Type alias name ""TypeTodo"" doesn't conform to predefined naming style":HIGH +invalid-name:21:0:21:8::"Type alias name ""BadNameT"" doesn't conform to predefined naming style":HIGH +invalid-name:22:0:22:8::"Type alias name ""BAD_NAME"" doesn't conform to predefined naming style":HIGH +invalid-name:23:0:23:9::"Type alias name ""_BAD_NAME"" doesn't conform to predefined naming style":HIGH +invalid-name:24:0:24:10::"Type alias name ""__BAD_NAME"" doesn't conform to predefined naming style":HIGH +invalid-name:25:0:25:9::"Type alias name ""_1BadName"" doesn't conform to predefined naming style":HIGH +invalid-name:26:0:26:14::"Type alias name ""ANOTHERBADNAME"" doesn't conform to predefined naming style":HIGH diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.py b/tests/functional/u/unnecessary/unnecessary_lambda.py index 3e5ece2b12..82571a4444 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.py +++ b/tests/functional/u/unnecessary/unnecessary_lambda.py @@ -24,6 +24,12 @@ # +1: [unnecessary-lambda] _ = lambda x, y, z, *args, **kwargs: _ANYARGS(x, y, z, *args, **kwargs) +# These don't use their parameters in their body +# +1: [unnecessary-lambda] +_ = lambda x: z(lambda x: x)(x) +# +1: [unnecessary-lambda] +_ = lambda x, y: z(lambda x, y: x + y)(x, y) + # Lambdas that are *not* unnecessary and should *not* trigger warnings. _ = lambda x: x _ = lambda x: x() @@ -50,3 +56,8 @@ _ = lambda: code().analysis() _ = lambda **kwargs: dict(bar=42, **kwargs) + +# These use the lambda parameters in their body +_ = lambda x: x(x) +_ = lambda x, y: x(x, y) +_ = lambda x: z(lambda y: x + y)(x) diff --git a/tests/functional/u/unnecessary/unnecessary_lambda.txt b/tests/functional/u/unnecessary/unnecessary_lambda.txt index 1cfb149dfd..87f80872cf 100644 --- a/tests/functional/u/unnecessary/unnecessary_lambda.txt +++ b/tests/functional/u/unnecessary/unnecessary_lambda.txt @@ -5,3 +5,5 @@ unnecessary-lambda:19:4:19:33::Lambda may not be necessary:UNDEFINED unnecessary-lambda:21:4:21:39::Lambda may not be necessary:UNDEFINED unnecessary-lambda:23:4:23:53::Lambda may not be necessary:UNDEFINED unnecessary-lambda:25:4:25:71::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:29:4:29:31::Lambda may not be necessary:UNDEFINED +unnecessary-lambda:31:4:31:44::Lambda may not be necessary:UNDEFINED diff --git a/tests/functional/u/unused/unused_import.py b/tests/functional/u/unused/unused_import.py index 3534cd0cf3..0abc7bf4dc 100644 --- a/tests/functional/u/unused/unused_import.py +++ b/tests/functional/u/unused/unused_import.py @@ -10,6 +10,7 @@ # +1:[unused-import,unused-import] from collections import deque, OrderedDict, Counter import re, html.parser # [unused-import] +import six DATA = Counter() # pylint: disable=self-assigning-variable @@ -98,8 +99,10 @@ def blop(self): import zoneinfo -class WithMetaclass(metaclass=ABCMeta): - pass +class WithMetaclass(six.with_metaclass(ABCMeta)): + """Regression test for https://github.com/PyCQA/pylint/issues/7506. + + Requires six.""" # Regression test for https://github.com/PyCQA/pylint/issues/3765 diff --git a/tests/functional/u/unused/unused_import.txt b/tests/functional/u/unused/unused_import.txt index f242bcb23d..f356843fa9 100644 --- a/tests/functional/u/unused/unused_import.txt +++ b/tests/functional/u/unused/unused_import.txt @@ -6,9 +6,9 @@ unused-import:8:0:8:21::Unused flags imported from sys:UNDEFINED unused-import:11:0:11:51::Unused OrderedDict imported from collections:UNDEFINED unused-import:11:0:11:51::Unused deque imported from collections:UNDEFINED unused-import:12:0:12:22::Unused import re:UNDEFINED -unused-import:16:0:16:40::Unused SomeOtherName imported from fake:UNDEFINED -unused-import:53:0:53:9::Unused import os:UNDEFINED -unused-import:88:4:88:19::Unused import unittest:UNDEFINED -unused-import:90:4:90:15::Unused import uuid:UNDEFINED -unused-import:92:4:92:19::Unused import warnings:UNDEFINED -unused-import:94:4:94:21::Unused import compileall:UNDEFINED +unused-import:17:0:17:40::Unused SomeOtherName imported from fake:UNDEFINED +unused-import:54:0:54:9::Unused import os:UNDEFINED +unused-import:89:4:89:19::Unused import unittest:UNDEFINED +unused-import:91:4:91:15::Unused import uuid:UNDEFINED +unused-import:93:4:93:19::Unused import warnings:UNDEFINED +unused-import:95:4:95:21::Unused import compileall:UNDEFINED diff --git a/tests/pyreverse/test_writer.py b/tests/pyreverse/test_writer.py index 805f8fab59..0bf92d9180 100644 --- a/tests/pyreverse/test_writer.py +++ b/tests/pyreverse/test_writer.py @@ -223,3 +223,20 @@ def test_color_for_stdlib_module(default_config: PyreverseConfig) -> None: obj.node = Mock() obj.node.qname.return_value = "collections" assert writer.get_shape_color(obj) == "grey" + + +def test_package_name_with_slash(default_config: PyreverseConfig) -> None: + """Test to check the names of the generated files are corrected + when using an incorrect character like "/" in the package name. + """ + writer = DiagramWriter(default_config) + obj = Mock() + + obj.objects = [] + obj.get_relationships.return_value = [] + obj.title = "test/package/name/with/slash/" + writer.write([obj]) + + assert os.path.exists("test_package_name_with_slash_.dot") + # remove the generated file + os.remove("test_package_name_with_slash_.dot") diff --git a/towncrier.toml b/towncrier.toml index b8c00a8ffb..35ce272433 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] -version = "2.17.1" +version = "2.17.2" directory = "doc/whatsnew/fragments" filename = "doc/whatsnew/2/2.17/index.rst" template = "doc/whatsnew/fragments/_template.rst"