diff --git a/doc/whatsnew/3/3.3/index.rst b/doc/whatsnew/3/3.3/index.rst index 0faf140c77..179f6e8d9c 100644 --- a/doc/whatsnew/3/3.3/index.rst +++ b/doc/whatsnew/3/3.3/index.rst @@ -14,6 +14,30 @@ Summary -- Release highlights .. towncrier release notes start +What's new in Pylint 3.3.2? +--------------------------- +Release date: 2024-12-01 + + +False Positives Fixed +--------------------- + +- Fix a false positive for `potential-index-error` when an indexed iterable + contains a starred element that evaluates to more than one item. + + Closes #10076 (`#10076 `_) + + + +Other Bug Fixes +--------------- + +- Fixes the issue with --source-root option not working when the source files are in a subdirectory of the source root (e.g. when using a /src layout). + + Closes #10026 (`#10026 `_) + + + What's new in Pylint 3.3.1? --------------------------- Release date: 2024-09-24 @@ -66,7 +90,7 @@ New Checks Closes #9099 (`#9099 `_) -- Add `using-exception-group-in-unsupported-version` and +- Add `using-exception-groups-in-unsupported-version` and `using-generic-type-syntax-in-unsupported-version` for uses of Python 3.11+ or 3.12+ features on lower supported versions provided with `--py-version`. diff --git a/examples/pylintrc b/examples/pylintrc index a26254c199..97064b3e95 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -93,7 +93,7 @@ prefer-stubs=no # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.10 +py-version=3.12 # Discover python modules and packages in the file system subtree. recursive=no diff --git a/examples/pyproject.toml b/examples/pyproject.toml index a647cbb577..ec4790b26f 100644 --- a/examples/pyproject.toml +++ b/examples/pyproject.toml @@ -83,7 +83,7 @@ persistent = true # Minimum Python version to use for version dependent checks. Will default to the # version used to run pylint. -py-version = "3.10" +py-version = "3.12" # Discover python modules and packages in the file system subtree. # recursive = diff --git a/pylint/__pkginfo__.py b/pylint/__pkginfo__.py index 1e68b60ea4..9ed79f3d4a 100644 --- a/pylint/__pkginfo__.py +++ b/pylint/__pkginfo__.py @@ -9,7 +9,7 @@ from __future__ import annotations -__version__ = "3.3.1" +__version__ = "3.3.2" def get_numversion_from_version(v: str) -> tuple[int, int, int]: diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py index 47f6b92d6e..d293e77de7 100644 --- a/pylint/checkers/variables.py +++ b/pylint/checkers/variables.py @@ -3356,6 +3356,19 @@ def visit_subscript(self, node: nodes.Subscript) -> None: self._check_potential_index_error(node, inferred_slice) + def _inferred_iterable_length(self, iterable: nodes.Tuple | nodes.List) -> int: + length = 0 + for elt in iterable.elts: + if not isinstance(elt, nodes.Starred): + length += 1 + continue + unpacked = utils.safe_infer(elt.value) + if isinstance(unpacked, nodes.BaseContainer): + length += len(unpacked.elts) + else: + length += 1 + return length + def _check_potential_index_error( self, node: nodes.Subscript, inferred_slice: nodes.NodeNG | None ) -> None: @@ -3369,7 +3382,7 @@ def _check_potential_index_error( # If the node.value is a Tuple or List without inference it is defined in place if isinstance(node.value, (nodes.Tuple, nodes.List)): # Add 1 because iterables are 0-indexed - if len(node.value.elts) < inferred_slice.value + 1: + if self._inferred_iterable_length(node.value) < inferred_slice.value + 1: self.add_message( "potential-index-error", node=node, confidence=INFERENCE ) diff --git a/pylint/lint/expand_modules.py b/pylint/lint/expand_modules.py index f40bdeea5b..a7d31dea6b 100644 --- a/pylint/lint/expand_modules.py +++ b/pylint/lint/expand_modules.py @@ -33,7 +33,7 @@ def discover_package_path(modulepath: str, source_roots: Sequence[str]) -> str: # Look for a source root that contains the module directory for source_root in source_roots: source_root = os.path.realpath(os.path.expanduser(source_root)) - if os.path.commonpath([source_root, dirname]) == source_root: + if os.path.commonpath([source_root, dirname]) in [dirname, source_root]: return source_root # Fall back to legacy discovery by looking for __init__.py upwards as diff --git a/pyproject.toml b/pyproject.toml index f4dc647e18..4dfbd99dfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ # 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/pylint-dev/astroid/issues/1341 - "astroid>=3.3.4,<=3.4.0-dev0", + "astroid>=3.3.5,<=3.4.0-dev0", "isort>=4.2.5,<6,!=5.13.0", "mccabe>=0.6,<0.8", "tomli>=1.1.0;python_version<'3.11'", diff --git a/requirements_test_min.txt b/requirements_test_min.txt index 06e60ed7b7..c14b34b837 100644 --- a/requirements_test_min.txt +++ b/requirements_test_min.txt @@ -1,6 +1,6 @@ .[testutils,spelling] # astroid dependency is also defined in pyproject.toml -astroid==3.3.4 # Pinned to a specific version for tests +astroid==3.3.5 # Pinned to a specific version for tests typing-extensions~=4.12 py~=1.11.0 pytest~=8.3 diff --git a/tbump.toml b/tbump.toml index 35de0dbe91..a07de7ba04 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/pylint" [version] -current = "3.3.1" +current = "3.3.2" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/functional/p/potential_index_error.py b/tests/functional/p/potential_index_error.py index 4d3c48d75d..71c025a767 100644 --- a/tests/functional/p/potential_index_error.py +++ b/tests/functional/p/potential_index_error.py @@ -23,3 +23,26 @@ def my_func(): # Test that we don't crash on more complicated indices/slices # We do not raise here (currently) print([1, 2, 3][2:3]) + + +# Test for cases with unpacking operation +my_list = ["foo", "bar"] +my_set = {"foo", "bar"} +my_tuple = ("foo", "bar") +my_iterable = (*my_list, *my_set, *my_tuple, *("foo", "bar")) +my_non_iterable = None + +print([*my_list][1]) +print([*my_list][2]) # [potential-index-error] + +print([*my_set][1]) +print([*my_set][2]) # [potential-index-error] + +print((*my_tuple,)[1]) +print((*my_tuple,)[2]) # [potential-index-error] + +print((*my_iterable,)[7]) +print((*my_iterable,)[8]) # [potential-index-error] + +print((*my_non_iterable,)[0]) +print((*my_non_iterable,)[1]) # [potential-index-error] diff --git a/tests/functional/p/potential_index_error.txt b/tests/functional/p/potential_index_error.txt index 2340f81737..fc99227ec9 100644 --- a/tests/functional/p/potential_index_error.txt +++ b/tests/functional/p/potential_index_error.txt @@ -1,3 +1,8 @@ potential-index-error:6:6:6:18::Invalid index for iterable length:INFERENCE potential-index-error:7:6:7:18::Invalid index for iterable length:INFERENCE potential-index-error:8:6:8:22::Invalid index for iterable length:INFERENCE +potential-index-error:36:6:36:19::Invalid index for iterable length:INFERENCE +potential-index-error:39:6:39:18::Invalid index for iterable length:INFERENCE +potential-index-error:42:6:42:21::Invalid index for iterable length:INFERENCE +potential-index-error:45:6:45:24::Invalid index for iterable length:INFERENCE +potential-index-error:48:6:48:28::Invalid index for iterable length:INFERENCE diff --git a/tests/pyreverse/test_main.py b/tests/pyreverse/test_main.py index e8e46df2c1..59fcab16f4 100644 --- a/tests/pyreverse/test_main.py +++ b/tests/pyreverse/test_main.py @@ -65,6 +65,42 @@ def test_project_root_in_sys_path() -> None: assert sys.path == [PROJECT_ROOT_DIR] +def test_discover_package_path_source_root_as_parent(tmp_path: Any) -> None: + """Test discover_package_path when source root is a parent of the module.""" + # Create this temporary structure: + # /tmp_path/ + # └── project/ + # └── my-package/ + # └── __init__.py + project_dir = tmp_path / "project" + package_dir = project_dir / "mypackage" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").touch() + + # Test with project_dir as source root (parent of package) + result = discover_package_path(str(package_dir), [str(project_dir)]) + assert result == str(project_dir) + + +def test_discover_package_path_source_root_as_child(tmp_path: Any) -> None: + """Test discover_package_path when source root is a child of the module.""" + # Create this temporary structure: + # /tmp_path/ + # └── project/ + # └── src/ + # └── my-package/ + # └── __init__.py + project_dir = tmp_path / "project" + src_dir = project_dir / "src" + package_dir = src_dir / "mypackage" + package_dir.mkdir(parents=True) + (package_dir / "__init__.py").touch() + + # Test with src_dir as source root (child of project) + result = discover_package_path(str(project_dir), [str(src_dir)]) + assert result == str(src_dir) + + @mock.patch("pylint.pyreverse.main.Linker", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.DiadefsHandler", new=mock.MagicMock()) @mock.patch("pylint.pyreverse.main.writer") diff --git a/towncrier.toml b/towncrier.toml index 9908fbeb0c..e289bc8ffc 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,5 +1,5 @@ [tool.towncrier] -version = "3.3.1" +version = "3.3.2" directory = "doc/whatsnew/fragments" filename = "doc/whatsnew/3/3.3/index.rst" template = "doc/whatsnew/fragments/_template.rst"