diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index f91449cb03..adc2e10c8e 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -8,8 +8,7 @@ on: pull_request: ~ env: - # Also change CACHE_VERSION in the other workflows - CACHE_VERSION: 8 + CACHE_VERSION: 1 DEFAULT_PYTHON: "3.10" PRE_COMMIT_CACHE: ~/.cache/pre-commit diff --git a/.github/workflows/primer-test.yaml b/.github/workflows/primer-test.yaml index a20f6a3e08..bda9f51f11 100644 --- a/.github/workflows/primer-test.yaml +++ b/.github/workflows/primer-test.yaml @@ -13,8 +13,7 @@ on: - ".github/workflows/primer-test.yaml" env: - # Also change CACHE_VERSION in the CI workflow - CACHE_VERSION: 6 + CACHE_VERSION: 1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/primer_comment.yaml b/.github/workflows/primer_comment.yaml index 88bada7267..ebef77e77b 100644 --- a/.github/workflows/primer_comment.yaml +++ b/.github/workflows/primer_comment.yaml @@ -13,7 +13,7 @@ on: - completed env: - CACHE_VERSION: 2 + CACHE_VERSION: 1 permissions: contents: read diff --git a/.github/workflows/primer_run_main.yaml b/.github/workflows/primer_run_main.yaml index 3debf50929..c231e1ff2b 100644 --- a/.github/workflows/primer_run_main.yaml +++ b/.github/workflows/primer_run_main.yaml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VERSION: 2 + CACHE_VERSION: 1 jobs: run-primer: diff --git a/.github/workflows/primer_run_pr.yaml b/.github/workflows/primer_run_pr.yaml index 96ada3cdd1..efe88c05cd 100644 --- a/.github/workflows/primer_run_pr.yaml +++ b/.github/workflows/primer_run_pr.yaml @@ -24,7 +24,7 @@ concurrency: cancel-in-progress: true env: - CACHE_VERSION: 2 + CACHE_VERSION: 1 jobs: run-primer: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 051c8dab08..dfae0e72ce 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -10,8 +10,7 @@ on: - doc/data/messages/** env: - # Also change CACHE_VERSION in the other workflows - CACHE_VERSION: 7 + CACHE_VERSION: 1 jobs: tests-linux: diff --git a/.pyenchant_pylint_custom_dict.txt b/.pyenchant_pylint_custom_dict.txt index 742acceed3..c827856b53 100644 --- a/.pyenchant_pylint_custom_dict.txt +++ b/.pyenchant_pylint_custom_dict.txt @@ -244,6 +244,7 @@ pragma's pragmas pre preorder +prepended proc py pyenchant diff --git a/doc/user_guide/usage/run.rst b/doc/user_guide/usage/run.rst index 9a15d04a38..84e1a8e2f7 100644 --- a/doc/user_guide/usage/run.rst +++ b/doc/user_guide/usage/run.rst @@ -9,22 +9,24 @@ Pylint is meant to be called from the command line. The usage is :: pylint [options] modules_or_packages -By default the ``pylint`` command only accepts a list of python modules and packages. Using a -directory which is not a package results in an error:: +By default the ``pylint`` command only accepts a list of python modules and packages. +On versions below 2.15, specifying a directory that is not an explicit package +(with ``__init__.py``) results in an error:: pylint mydir ************* Module mydir mydir/__init__.py:1:0: F0010: error while code parsing: Unable to load file mydir/__init__.py: [Errno 2] No such file or directory: 'mydir/__init__.py' (parse-error) -When ``--recursive=y`` option is used, modules and packages are also accepted as parameters:: +Thus, on versions before 2.15, or when dealing with certain edge cases that have not yet been solved, +using the ``--recursive=y`` option allows for linting a namespace package:: pylint --recursive=y mydir mymodule mypackage This option makes ``pylint`` attempt to discover all modules (files ending with ``.py`` extension) -and all packages (all directories containing a ``__init__.py`` file). +and all explicit packages (all directories containing a ``__init__.py`` file). -Pylint **will not import** this package or module, though uses Python internals +Pylint **will not import** this package or module, but it does use Python internals to locate them and as such is subject to the same rules and configuration. You should pay attention to your ``PYTHONPATH``, since it is a common error to analyze an installed version of a module instead of the development version. diff --git a/doc/whatsnew/2/2.15/index.rst b/doc/whatsnew/2/2.15/index.rst index ff67eeae6a..3f3e34ba88 100644 --- a/doc/whatsnew/2/2.15/index.rst +++ b/doc/whatsnew/2/2.15/index.rst @@ -11,6 +11,9 @@ Summary -- Release highlights ============================= +* We improved ``pylint``'s handling of namespace packages. More packages should be + linted without resorting to using the ``-recursive=y`` option. + New checkers ============ @@ -86,6 +89,8 @@ Other Changes Closes #6953 +* Update ``astroid`` to 2.12. + Internal changes ================ diff --git a/pylint/checkers/imports.py b/pylint/checkers/imports.py index 97d9d51f6c..a283de9b12 100644 --- a/pylint/checkers/imports.py +++ b/pylint/checkers/imports.py @@ -767,7 +767,9 @@ def _get_imported_module(self, importnode, modname): return None self.add_message("relative-beyond-top-level", node=importnode) except astroid.AstroidSyntaxError as exc: - message = f"Cannot import {modname!r} due to syntax error {str(exc.error)!r}" # pylint: disable=no-member; false positive + message = ( + f"Cannot import {modname!r} due to syntax error {str(exc.error)!r}" + ) self.add_message("syntax-error", line=importnode.lineno, args=message) except astroid.AstroidBuildingError: diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 4dc469495e..fa7d96d99d 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -1137,9 +1137,24 @@ def visit_classdef(self, node: nodes.ClassDef) -> None: """Visit class: update consumption analysis variable.""" self._to_consume.append(NamesConsumer(node, "class")) - def leave_classdef(self, _: nodes.ClassDef) -> None: + def leave_classdef(self, node: nodes.ClassDef) -> None: """Leave class: update consumption analysis variable.""" - # do not check for not used locals here (no sense) + # Check for hidden ancestor names + # e.g. "six" in: Class X(six.with_metaclass(ABCMeta, object)): + for name_node in node.nodes_of_class(nodes.Name): + if ( + isinstance(name_node.parent, nodes.Call) + and isinstance(name_node.parent.func, nodes.Attribute) + and isinstance(name_node.parent.func.expr, nodes.Name) + ): + hidden_name_node = name_node.parent.func.expr + for consumer in self._to_consume: + if hidden_name_node.name in consumer.to_consume: + consumer.mark_as_consumed( + hidden_name_node.name, + consumer.to_consume[hidden_name_node.name], + ) + break self._to_consume.pop() def visit_lambda(self, node: nodes.Lambda) -> None: diff --git a/pylint/lint/pylinter.py b/pylint/lint/pylinter.py index 96e7cb9232..bacce60e11 100644 --- a/pylint/lint/pylinter.py +++ b/pylint/lint/pylinter.py @@ -914,7 +914,6 @@ def get_ast( data, modname, filepath ) except astroid.AstroidSyntaxError as ex: - # pylint: disable=no-member self.add_message( "syntax-error", line=getattr(ex.error, "lineno", 0), diff --git a/pyproject.toml b/pyproject.toml index c2f041cf30..b5ed6a13bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,9 @@ dependencies = [ "dill>=0.2", "platformdirs>=2.2.0", # Also upgrade requirements_test_min.txt if you are bumping astroid. - # Pinned to dev of next minor update to allow editable installs, + # 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.11.6,<=2.12.0-dev0", + "astroid>=2.12.2,<=2.14.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 4689a05755..6ebb575cbe 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ coveralls~=3.3 coverage~=6.4 pre-commit~=2.20 tbump~=6.9.0 -contributors-txt>=0.7.3 +contributors-txt>=0.9.0 pytest-cov~=3.0 pytest-profiling~=1.7 pytest-xdist~=2.5 diff --git a/requirements_test_min.txt b/requirements_test_min.txt index d905879aa1..18f27d5eda 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,6 +1,6 @@ -e .[testutils,spelling] -# astroid dependency is also defined in setup.cfg -astroid==2.11.6 # Pinned to a specific version for tests +# astroid dependency is also defined in pyproject.toml +astroid==2.12.2 # Pinned to a specific version for tests typing-extensions~=4.3 pytest~=7.1 pytest-benchmark~=3.4 diff --git a/tests/functional/u/unused/unused_import.py b/tests/functional/u/unused/unused_import.py index 143ee7cf3a..46907b47c5 100644 --- a/tests/functional/u/unused/unused_import.py +++ b/tests/functional/u/unused/unused_import.py @@ -3,6 +3,7 @@ import xml.etree # [unused-import] import xml.sax # [unused-import] import os.path as test # [unused-import] +from abc import ABCMeta from sys import argv as test2 # [unused-import] from sys import flags # [unused-import] # +1:[unused-import,unused-import] @@ -10,6 +11,7 @@ import re, html.parser # [unused-import] DATA = Counter() # pylint: disable=self-assigning-variable +import six from fake import SomeName, SomeOtherName # [unused-import] class SomeClass(object): SomeName = SomeName # https://bitbucket.org/logilab/pylint/issue/475 @@ -87,3 +89,6 @@ def blop(self): TYPE_CHECKING = False if TYPE_CHECKING: import zoneinfo + +class WithMetaclass(six.with_metaclass(ABCMeta, object)): + pass diff --git a/tests/functional/u/unused/unused_import.txt b/tests/functional/u/unused/unused_import.txt index 059405388e..cad621a599 100644 --- a/tests/functional/u/unused/unused_import.txt +++ b/tests/functional/u/unused/unused_import.txt @@ -1,14 +1,14 @@ unused-import:3:0:3:16::Unused import xml.etree:UNDEFINED unused-import:4:0:4:14::Unused import xml.sax:UNDEFINED unused-import:5:0:5:22::Unused os.path imported as test:UNDEFINED -unused-import:6:0:6:29::Unused argv imported from sys as test2:UNDEFINED -unused-import:7:0:7:21::Unused flags imported from sys:UNDEFINED -unused-import:9:0:9:51::Unused OrderedDict imported from collections:UNDEFINED -unused-import:9:0:9:51::Unused deque imported from collections:UNDEFINED -unused-import:10:0:10:22::Unused import re:UNDEFINED -unused-import:13:0:13:40::Unused SomeOtherName imported from fake:UNDEFINED -unused-import:48:0:48:9::Unused import os:UNDEFINED -unused-import:79:4:79:19::Unused import unittest:UNDEFINED -unused-import:81:4:81:15::Unused import uuid:UNDEFINED -unused-import:83:4:83:19::Unused import warnings:UNDEFINED -unused-import:85:4:85:21::Unused import compileall:UNDEFINED +unused-import:7:0:7:29::Unused argv imported from sys as test2:UNDEFINED +unused-import:8:0:8:21::Unused flags imported from sys:UNDEFINED +unused-import:10:0:10:51::Unused OrderedDict imported from collections:UNDEFINED +unused-import:10:0:10:51::Unused deque imported from collections:UNDEFINED +unused-import:11:0:11:22::Unused import re:UNDEFINED +unused-import:15:0:15:40::Unused SomeOtherName imported from fake:UNDEFINED +unused-import:50:0:50:9::Unused import os:UNDEFINED +unused-import:81:4:81:19::Unused import unittest:UNDEFINED +unused-import:83:4:83:15::Unused import uuid:UNDEFINED +unused-import:85:4:85:19::Unused import warnings:UNDEFINED +unused-import:87:4:87:21::Unused import compileall:UNDEFINED diff --git a/tests/pyreverse/test_diadefs.py b/tests/pyreverse/test_diadefs.py index 01457802c4..ab8d65bbd1 100644 --- a/tests/pyreverse/test_diadefs.py +++ b/tests/pyreverse/test_diadefs.py @@ -24,6 +24,10 @@ from pylint.pyreverse.diagrams import DiagramEntity, Relationship from pylint.pyreverse.inspector import Linker, Project from pylint.testutils.pyreverse import PyreverseConfig +from pylint.testutils.utils import _test_cwd + +HERE = Path(__file__) +TESTS = HERE.parent.parent def _process_classes(classes: list[DiagramEntity]) -> list[tuple[bool, str]]: @@ -50,7 +54,8 @@ def HANDLER(default_config: PyreverseConfig) -> DiadefsHandler: @pytest.fixture(scope="module") def PROJECT(get_project): - return get_project("data") + with _test_cwd(TESTS): + yield get_project("data") def test_option_values( @@ -100,15 +105,15 @@ class TestDefaultDiadefGenerator: ("specialization", "Specialization", "Ancestor"), ] - def test_exctract_relations( - self, HANDLER: DiadefsHandler, PROJECT: Project - ) -> None: + @pytest.mark.xfail + def test_extract_relations(self, HANDLER: DiadefsHandler, PROJECT: Project) -> None: """Test extract_relations between classes.""" cd = DefaultDiadefGenerator(Linker(PROJECT), HANDLER).visit(PROJECT)[1] cd.extract_relationships() relations = _process_relations(cd.relationships) assert relations == self._should_rels + @pytest.mark.xfail def test_functional_relation_extraction( self, default_config: PyreverseConfig, get_project: Callable ) -> None: diff --git a/tests/pyreverse/test_inspector.py b/tests/pyreverse/test_inspector.py index 0fd54e8f81..15f9d305a7 100644 --- a/tests/pyreverse/test_inspector.py +++ b/tests/pyreverse/test_inspector.py @@ -9,7 +9,8 @@ from __future__ import annotations import os -from collections.abc import Callable +from collections.abc import Callable, Generator +from pathlib import Path import astroid import pytest @@ -17,14 +18,19 @@ from pylint.pyreverse import inspector from pylint.pyreverse.inspector import Project +from pylint.testutils.utils import _test_cwd + +HERE = Path(__file__) +TESTS = HERE.parent.parent @pytest.fixture -def project(get_project: Callable) -> Project: - project = get_project("data", "data") - linker = inspector.Linker(project) - linker.visit(project) - return project +def project(get_project: Callable) -> Generator[Project, None, None]: + with _test_cwd(TESTS): + project = get_project("data", "data") + linker = inspector.Linker(project) + linker.visit(project) + yield project def test_class_implements(project: Project) -> None: diff --git a/tests/test_func.py b/tests/test_func.py index 4769af4260..23f5ff1024 100644 --- a/tests/test_func.py +++ b/tests/test_func.py @@ -14,9 +14,11 @@ from pylint.testutils import UPDATE_FILE, UPDATE_OPTION, _get_tests_info, linter from pylint.testutils.reporter_for_tests import GenericTestReporter +from pylint.testutils.utils import _test_cwd -INPUT_DIR = join(dirname(abspath(__file__)), "input") -MSG_DIR = join(dirname(abspath(__file__)), "messages") +TESTS_DIR = dirname(abspath(__file__)) +INPUT_DIR = join(TESTS_DIR, "input") +MSG_DIR = join(TESTS_DIR, "messages") FILTER_RGX = None @@ -46,7 +48,11 @@ def _test_functionality(self) -> None: tocheck += [ self.package + f".{name.replace('.py', '')}" for name, _ in self.depends ] - self._test(tocheck) + # given that TESTS_DIR could be treated as a namespace package + # when under the current directory, cd to it so that "tests." is not + # prepended to module names in the output of cyclic-import + with _test_cwd(TESTS_DIR): + self._test(tocheck) def _check_result(self, got: str) -> None: error_msg = ( diff --git a/tests/test_self.py b/tests/test_self.py index 99af35af9a..23942c0189 100644 --- a/tests/test_self.py +++ b/tests/test_self.py @@ -921,7 +921,7 @@ def test_regression_parallel_mode_without_filepath(self) -> None: path = join( HERE, "regrtest_data", "regression_missing_init_3564", "subdirectory/" ) - self._test_output([path, "-j2"], expected_output="No such file or directory") + self._test_output([path, "-j2"], expected_output="") def test_output_file_valid_path(self, tmpdir: LocalPath) -> None: path = join(HERE, "regrtest_data", "unused_variable.py") @@ -1083,13 +1083,6 @@ def test_max_inferred_for_complicated_class_hierarchy() -> None: # Error code should not include bit-value 1 for crash assert not ex.value.code % 2 - def test_regression_recursive(self): - """Tests if error is raised when linter is executed over directory not using --recursive=y""" - self._test_output( - [join(HERE, "regrtest_data", "directory", "subdirectory"), "--recursive=n"], - expected_output="No such file or directory", - ) - def test_recursive(self): """Tests if running linter over directory using --recursive=y""" self._runtest( @@ -1169,22 +1162,6 @@ def test_ignore_path_recursive_current_dir(self) -> None: code=0, ) - def test_regression_recursive_current_dir(self): - with _test_sys_path(): - # pytest is including directory HERE/regrtest_data to sys.path which causes - # astroid to believe that directory is a package. - sys.path = [ - path - for path in sys.path - if not os.path.basename(path) == "regrtest_data" - ] - with _test_cwd(): - os.chdir(join(HERE, "regrtest_data", "directory")) - self._test_output( - ["."], - expected_output="No such file or directory", - ) - class TestCallbackOptions: """Test for all callback options we support."""