From 998ff34b5e7aa20eca7e3d9d438f15d500f9510b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Thu, 25 Aug 2022 11:28:59 +0200 Subject: [PATCH 01/93] Fix crash involving non-standard type comments (#1753) --- ChangeLog | 3 +++ astroid/rebuilder.py | 5 +++++ tests/unittest_builder.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/ChangeLog b/ChangeLog index 1841664176..60eab07f65 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,9 @@ What's New in astroid 2.12.4? ============================= Release date: TBA +* Fixed a crash involving non-standard type comments such as ``# type: # any comment``. + + Refs PyCQA/pylint#7347 What's New in astroid 2.12.3? diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index a658971352..070b9db84c 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -734,6 +734,11 @@ def check_type_comment( # Invalid type comment, just skip it. return None + # For '# type: # any comment' ast.parse returns a Module node, + # without any nodes in the body. + if not type_comment_ast.body: + return None + type_object = self.visit(type_comment_ast.body[0], parent=parent) if not isinstance(type_object, nodes.Expr): return None diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index 61ef1bba3a..cc1cb28bfd 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -737,6 +737,14 @@ def test_not_implemented(self) -> None: self.assertIsInstance(inferred, nodes.Const) self.assertEqual(inferred.value, NotImplemented) + def test_type_comments_without_content(self) -> None: + node = builder.parse( + """ + a = 1 # type: # any comment + """ + ) + assert node + class FileBuildTest(unittest.TestCase): def setUp(self) -> None: From d463bd2de2c4565e7e630bc61aa4685acc1f5183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Thu, 25 Aug 2022 11:28:59 +0200 Subject: [PATCH 02/93] Fix crash involving non-standard type comments (#1753) --- ChangeLog | 3 +++ astroid/rebuilder.py | 5 +++++ tests/unittest_builder.py | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/ChangeLog b/ChangeLog index b2b336170b..d25ecb25fe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,9 @@ What's New in astroid 2.12.4? ============================= Release date: TBA +* Fixed a crash involving non-standard type comments such as ``# type: # any comment``. + + Refs PyCQA/pylint#7347 What's New in astroid 2.12.3? diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index a658971352..070b9db84c 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -734,6 +734,11 @@ def check_type_comment( # Invalid type comment, just skip it. return None + # For '# type: # any comment' ast.parse returns a Module node, + # without any nodes in the body. + if not type_comment_ast.body: + return None + type_object = self.visit(type_comment_ast.body[0], parent=parent) if not isinstance(type_object, nodes.Expr): return None diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index 61ef1bba3a..cc1cb28bfd 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -737,6 +737,14 @@ def test_not_implemented(self) -> None: self.assertIsInstance(inferred, nodes.Const) self.assertEqual(inferred.value, NotImplemented) + def test_type_comments_without_content(self) -> None: + node = builder.parse( + """ + a = 1 # type: # any comment + """ + ) + assert node + class FileBuildTest(unittest.TestCase): def setUp(self) -> None: From 7fba17d69b033a5aace1d7b1aed7887a8ef2c4b4 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 25 Aug 2022 11:32:21 +0200 Subject: [PATCH 03/93] Bump astroid to 2.12.4, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index d25ecb25fe..285cf302d9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,10 +8,16 @@ Release date: TBA -What's New in astroid 2.12.4? +What's New in astroid 2.12.5? ============================= Release date: TBA + + +What's New in astroid 2.12.4? +============================= +Release date: 2022-08-25 + * Fixed a crash involving non-standard type comments such as ``# type: # any comment``. Refs PyCQA/pylint#7347 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 74eb08adbd..e2ffb145b4 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.3" +__version__ = "2.12.4" version = __version__ diff --git a/tbump.toml b/tbump.toml index aca5accc05..79a38ab1d4 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.3" +current = "2.12.4" regex = ''' ^(?P0|[1-9]\d*) \. From 4c3bf08947a9fa37e2463628d20c8316dc6dd71b Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 27 Aug 2022 16:51:33 -0400 Subject: [PATCH 04/93] Fix namespace package detection for frozen stdlib modules on PyPy (#1757) --- ChangeLog | 4 ++++ astroid/interpreter/_import/util.py | 4 +++- tests/unittest_manager.py | 13 ++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 3a7721f2b0..ff7540f018 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,10 @@ What's New in astroid 2.12.5? ============================= Release date: TBA +* Fix ``astroid.interpreter._import.util.is_namespace()`` incorrectly + returning ``True`` for frozen stdlib modules on PyPy. + + Closes #1755 What's New in astroid 2.12.4? diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index f082e9c4a7..3bdf0470f3 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -10,6 +10,8 @@ from importlib._bootstrap_external import _NamespacePath from importlib.util import _find_spec_from_path # type: ignore[attr-defined] +from astroid.const import IS_PYPY + @lru_cache(maxsize=4096) def is_namespace(modname: str) -> bool: @@ -38,7 +40,7 @@ def is_namespace(modname: str) -> bool: return False try: # .pth files will be on sys.modules - return sys.modules[modname].__spec__ is None + return sys.modules[modname].__spec__ is None and not IS_PYPY except KeyError: return False except AttributeError: diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 5bce29041e..fd5787d442 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -10,9 +10,11 @@ from collections.abc import Iterator from contextlib import contextmanager +import pytest + import astroid from astroid import manager, test_utils -from astroid.const import IS_JYTHON +from astroid.const import IS_JYTHON, IS_PYPY from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import util from astroid.modutils import is_standard_module @@ -128,6 +130,7 @@ def test_submodule_homonym_with_non_module(self) -> None: def test_module_is_not_namespace(self) -> None: self.assertFalse(util.is_namespace("tests.testdata.python3.data.all")) self.assertFalse(util.is_namespace("__main__")) + self.assertFalse(util.is_namespace("importlib._bootstrap")) def test_module_unexpectedly_missing_spec(self) -> None: astroid_module = sys.modules["astroid"] @@ -155,6 +158,10 @@ def test_implicit_namespace_package(self) -> None: for _ in range(2): sys.path.pop(0) + @pytest.mark.skipif( + IS_PYPY, + reason="PyPy provides no way to tell apart frozen stdlib from old-style namespace packages", + ) def test_namespace_package_pth_support(self) -> None: pth = "foogle_fax-0.12.5-py2.7-nspkg.pth" site.addpackage(resources.RESOURCE_PATH, pth, []) @@ -169,6 +176,10 @@ def test_namespace_package_pth_support(self) -> None: finally: sys.modules.pop("foogle") + @pytest.mark.skipif( + IS_PYPY, + reason="PyPy provides no way to tell apart frozen stdlib from old-style namespace packages", + ) def test_nested_namespace_import(self) -> None: pth = "foogle_fax-0.12.5-py2.7-nspkg.pth" site.addpackage(resources.RESOURCE_PATH, pth, []) From ad80ee22b2b94aed85fae769a1977dcd6970506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:57:27 +0200 Subject: [PATCH 05/93] Add a comment about missing ``__spec__`` on ``PyPy`` (#1758) --- astroid/interpreter/_import/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index 3bdf0470f3..0c29d670ef 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -40,6 +40,8 @@ def is_namespace(modname: str) -> bool: return False try: # .pth files will be on sys.modules + # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here + # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736 return sys.modules[modname].__spec__ is None and not IS_PYPY except KeyError: return False From 5dfc004afba87199371e061f283905c77eaad67d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 29 Aug 2022 02:38:51 -0400 Subject: [PATCH 06/93] Prevent first-party imports from being resolved to `site-packages` (#1756) Co-authored-by: Pierre Sassoulas --- ChangeLog | 5 +++++ astroid/interpreter/_import/util.py | 16 +++++++++++++++- tests/unittest_manager.py | 5 ++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index ff7540f018..793724ba74 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,11 @@ What's New in astroid 2.12.5? ============================= Release date: TBA + +* Prevent first-party imports from being resolved to `site-packages`. + + Refs PyCQA/pylint#7365 + * Fix ``astroid.interpreter._import.util.is_namespace()`` incorrectly returning ``True`` for frozen stdlib modules on PyPy. diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index 0c29d670ef..8cdb8ea19d 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -15,6 +15,13 @@ @lru_cache(maxsize=4096) def is_namespace(modname: str) -> bool: + from astroid.modutils import ( # pylint: disable=import-outside-toplevel + EXT_LIB_DIRS, + STD_LIB_DIRS, + ) + + STD_AND_EXT_LIB_DIRS = STD_LIB_DIRS.union(EXT_LIB_DIRS) + if modname in sys.builtin_module_names: return False @@ -72,8 +79,15 @@ def is_namespace(modname: str) -> bool: last_submodule_search_locations.append(str(assumed_location)) continue - # Update last_submodule_search_locations + # Update last_submodule_search_locations for next iteration if found_spec and found_spec.submodule_search_locations: + # But immediately return False if we can detect we are in stdlib + # or external lib (e.g site-packages) + if any( + any(location.startswith(lib_dir) for lib_dir in STD_AND_EXT_LIB_DIRS) + for location in found_spec.submodule_search_locations + ): + return False last_submodule_search_locations = found_spec.submodule_search_locations return ( diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index fd5787d442..678cbf2940 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -17,7 +17,7 @@ from astroid.const import IS_JYTHON, IS_PYPY from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import util -from astroid.modutils import is_standard_module +from astroid.modutils import EXT_LIB_DIRS, is_standard_module from astroid.nodes import Const from astroid.nodes.scoped_nodes import ClassDef @@ -130,6 +130,9 @@ def test_submodule_homonym_with_non_module(self) -> None: def test_module_is_not_namespace(self) -> None: self.assertFalse(util.is_namespace("tests.testdata.python3.data.all")) self.assertFalse(util.is_namespace("__main__")) + self.assertFalse( + util.is_namespace(list(EXT_LIB_DIRS)[0].rsplit("/", maxsplit=1)[-1]), + ) self.assertFalse(util.is_namespace("importlib._bootstrap")) def test_module_unexpectedly_missing_spec(self) -> None: From ef55fd3cd477ebe3c693b4b9eb15f52b56449b55 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 27 Aug 2022 16:51:33 -0400 Subject: [PATCH 07/93] Fix namespace package detection for frozen stdlib modules on PyPy (#1757) --- ChangeLog | 4 ++++ astroid/interpreter/_import/util.py | 4 +++- tests/unittest_manager.py | 13 ++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 285cf302d9..9dae0f4356 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,10 @@ What's New in astroid 2.12.5? ============================= Release date: TBA +* Fix ``astroid.interpreter._import.util.is_namespace()`` incorrectly + returning ``True`` for frozen stdlib modules on PyPy. + + Closes #1755 What's New in astroid 2.12.4? diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index f082e9c4a7..3bdf0470f3 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -10,6 +10,8 @@ from importlib._bootstrap_external import _NamespacePath from importlib.util import _find_spec_from_path # type: ignore[attr-defined] +from astroid.const import IS_PYPY + @lru_cache(maxsize=4096) def is_namespace(modname: str) -> bool: @@ -38,7 +40,7 @@ def is_namespace(modname: str) -> bool: return False try: # .pth files will be on sys.modules - return sys.modules[modname].__spec__ is None + return sys.modules[modname].__spec__ is None and not IS_PYPY except KeyError: return False except AttributeError: diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 9cf02aff86..79f21db138 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -10,9 +10,11 @@ from collections.abc import Iterator from contextlib import contextmanager +import pytest + import astroid from astroid import manager, test_utils -from astroid.const import IS_JYTHON +from astroid.const import IS_JYTHON, IS_PYPY from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import util from astroid.modutils import is_standard_module @@ -128,6 +130,7 @@ def test_submodule_homonym_with_non_module(self) -> None: def test_module_is_not_namespace(self) -> None: self.assertFalse(util.is_namespace("tests.testdata.python3.data.all")) self.assertFalse(util.is_namespace("__main__")) + self.assertFalse(util.is_namespace("importlib._bootstrap")) def test_module_unexpectedly_missing_spec(self) -> None: astroid_module = sys.modules["astroid"] @@ -155,6 +158,10 @@ def test_implicit_namespace_package(self) -> None: for _ in range(2): sys.path.pop(0) + @pytest.mark.skipif( + IS_PYPY, + reason="PyPy provides no way to tell apart frozen stdlib from old-style namespace packages", + ) def test_namespace_package_pth_support(self) -> None: pth = "foogle_fax-0.12.5-py2.7-nspkg.pth" site.addpackage(resources.RESOURCE_PATH, pth, []) @@ -169,6 +176,10 @@ def test_namespace_package_pth_support(self) -> None: finally: sys.modules.pop("foogle") + @pytest.mark.skipif( + IS_PYPY, + reason="PyPy provides no way to tell apart frozen stdlib from old-style namespace packages", + ) def test_nested_namespace_import(self) -> None: pth = "foogle_fax-0.12.5-py2.7-nspkg.pth" site.addpackage(resources.RESOURCE_PATH, pth, []) From 92529b5eafc42e92b9dc18377532fda2d9cdfc49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:57:27 +0200 Subject: [PATCH 08/93] Add a comment about missing ``__spec__`` on ``PyPy`` (#1758) --- astroid/interpreter/_import/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index 3bdf0470f3..0c29d670ef 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -40,6 +40,8 @@ def is_namespace(modname: str) -> bool: return False try: # .pth files will be on sys.modules + # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here + # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736 return sys.modules[modname].__spec__ is None and not IS_PYPY except KeyError: return False From 8852ecda407598636fcd760877e5a093148e8e67 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Mon, 29 Aug 2022 02:38:51 -0400 Subject: [PATCH 09/93] Prevent first-party imports from being resolved to `site-packages` (#1756) Co-authored-by: Pierre Sassoulas --- ChangeLog | 5 +++++ astroid/interpreter/_import/util.py | 16 +++++++++++++++- tests/unittest_manager.py | 5 ++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9dae0f4356..7c81884ff1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,11 @@ What's New in astroid 2.12.5? ============================= Release date: TBA + +* Prevent first-party imports from being resolved to `site-packages`. + + Refs PyCQA/pylint#7365 + * Fix ``astroid.interpreter._import.util.is_namespace()`` incorrectly returning ``True`` for frozen stdlib modules on PyPy. diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index 0c29d670ef..8cdb8ea19d 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -15,6 +15,13 @@ @lru_cache(maxsize=4096) def is_namespace(modname: str) -> bool: + from astroid.modutils import ( # pylint: disable=import-outside-toplevel + EXT_LIB_DIRS, + STD_LIB_DIRS, + ) + + STD_AND_EXT_LIB_DIRS = STD_LIB_DIRS.union(EXT_LIB_DIRS) + if modname in sys.builtin_module_names: return False @@ -72,8 +79,15 @@ def is_namespace(modname: str) -> bool: last_submodule_search_locations.append(str(assumed_location)) continue - # Update last_submodule_search_locations + # Update last_submodule_search_locations for next iteration if found_spec and found_spec.submodule_search_locations: + # But immediately return False if we can detect we are in stdlib + # or external lib (e.g site-packages) + if any( + any(location.startswith(lib_dir) for lib_dir in STD_AND_EXT_LIB_DIRS) + for location in found_spec.submodule_search_locations + ): + return False last_submodule_search_locations = found_spec.submodule_search_locations return ( diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 79f21db138..6c13da787d 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -17,7 +17,7 @@ from astroid.const import IS_JYTHON, IS_PYPY from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import util -from astroid.modutils import is_standard_module +from astroid.modutils import EXT_LIB_DIRS, is_standard_module from astroid.nodes import Const from astroid.nodes.scoped_nodes import ClassDef @@ -130,6 +130,9 @@ def test_submodule_homonym_with_non_module(self) -> None: def test_module_is_not_namespace(self) -> None: self.assertFalse(util.is_namespace("tests.testdata.python3.data.all")) self.assertFalse(util.is_namespace("__main__")) + self.assertFalse( + util.is_namespace(list(EXT_LIB_DIRS)[0].rsplit("/", maxsplit=1)[-1]), + ) self.assertFalse(util.is_namespace("importlib._bootstrap")) def test_module_unexpectedly_missing_spec(self) -> None: From c313631bca83f7b6eb7dd8990aa702b85eb22d64 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 28 Aug 2022 20:14:21 +0200 Subject: [PATCH 10/93] Bump astroid to 2.12.5, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7c81884ff1..5e1ce1749f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,11 +8,17 @@ Release date: TBA -What's New in astroid 2.12.5? +What's New in astroid 2.12.6? ============================= Release date: TBA + + +What's New in astroid 2.12.5? +============================= +Release date: 2022-08-29 + * Prevent first-party imports from being resolved to `site-packages`. Refs PyCQA/pylint#7365 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index e2ffb145b4..947ede2392 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.4" +__version__ = "2.12.5" version = __version__ diff --git a/tbump.toml b/tbump.toml index 79a38ab1d4..9849717172 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.4" +current = "2.12.5" regex = ''' ^(?P0|[1-9]\d*) \. From d4a45c89cc7ca397c7c4dfe3b9103293a9ecd4ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 19:34:41 +0200 Subject: [PATCH 11/93] Bump actions/cache from 3.0.7 to 3.0.8 (#1760) Bumps [actions/cache](https://github.com/actions/cache) from 3.0.7 to 3.0.8. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3.0.7...v3.0.8) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 530534475d..ddeb0d00f1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ jobs: 'requirements_test_brain.txt', 'requirements_test_pre_commit.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -56,7 +56,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -104,7 +104,7 @@ jobs: 'requirements_test_brain.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -150,7 +150,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: @@ -204,7 +204,7 @@ jobs: 'requirements_test_brain.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- @@ -248,7 +248,7 @@ jobs: hashFiles('setup.cfg', 'requirements_test_min.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.7 + uses: actions/cache@v3.0.8 with: path: venv key: >- From 68b1fd44b642df8f8a9a092967265f582d10c412 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Aug 2022 19:34:56 +0200 Subject: [PATCH 12/93] Bump pylint from 2.14.5 to 2.15.0 (#1761) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.14.5 to 2.15.0. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.14.5...v2.15.0) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 8aef8ec803..bfc963be62 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ black==22.6.0 -pylint==2.14.5 +pylint==2.15.0 isort==5.10.1 flake8==5.0.4 flake8-typing-imports==1.13.0 From 830a9a6fe621a6c6ac9218d2ba03cb0dfd0eae8c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Aug 2022 06:16:52 +0200 Subject: [PATCH 13/93] [pre-commit.ci] pre-commit autoupdate (#1762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/myint/autoflake: v1.4 → v1.5.1](https://github.com/myint/autoflake/compare/v1.4...v1.5.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cc2a69970..84c3e7ca3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: end-of-file-fixer exclude: tests/testdata - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v1.5.1 hooks: - id: autoflake exclude: tests/testdata|astroid/__init__.py|astroid/scoped_nodes.py|astroid/node_classes.py From daa6a44535b2a52f35e6f60b02719d31d69d264d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 4 Sep 2022 14:32:56 -0400 Subject: [PATCH 14/93] Fix a crash involving `Uninferable` args to `namedtuple` (#1763) --- ChangeLog | 2 ++ astroid/brain/brain_namedtuple_enum.py | 8 +++++++- tests/unittest_brain.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 531f448a83..4bfec0ec29 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,7 +17,9 @@ What's New in astroid 2.12.6? ============================= Release date: TBA +* Fix a crash involving ``Uninferable`` arguments to ``namedtuple()``. + Closes PyCQA/pylint#7375 What's New in astroid 2.12.5? diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index 736f9f9fc5..acc20c516d 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -538,7 +538,13 @@ def _get_namedtuple_fields(node: nodes.Call) -> str: extract a node from them later on. """ names = [] - for elt in next(node.args[1].infer()).elts: + try: + container = next(node.args[1].infer()) + except (InferenceError, StopIteration) as exc: + raise UseInferenceDefault from exc + if not isinstance(container, nodes.BaseContainer): + raise UseInferenceDefault + for elt in container.elts: if isinstance(elt, nodes.Const): names.append(elt.as_string()) continue diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 149100eb50..bcd92c02d5 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -18,6 +18,7 @@ import astroid from astroid import MANAGER, bases, builder, nodes, objects, test_utils, util from astroid.bases import Instance +from astroid.brain.brain_namedtuple_enum import _get_namedtuple_fields from astroid.const import PY39_PLUS from astroid.exceptions import ( AttributeInferenceError, @@ -1644,6 +1645,25 @@ def NamedTuple(): ) next(node.infer()) + def test_namedtuple_uninferable_member(self) -> None: + call = builder.extract_node( + """ + from typing import namedtuple + namedtuple('uninf', {x: x for x in range(0)}) #@""" + ) + with pytest.raises(UseInferenceDefault): + _get_namedtuple_fields(call) + + call = builder.extract_node( + """ + from typing import namedtuple + uninferable = {x: x for x in range(0)} + namedtuple('uninferable', uninferable) #@ + """ + ) + with pytest.raises(UseInferenceDefault): + _get_namedtuple_fields(call) + def test_typing_types(self) -> None: ast_nodes = builder.extract_node( """ From e0a2d6c35baa9eb391c8b5149436959956e6a3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:28:32 +0200 Subject: [PATCH 15/93] Handle ``dataclass`` ``kw_only`` keyword correctly (#1764) Co-authored-by: Jacob Walls --- ChangeLog | 4 ++ astroid/brain/brain_dataclasses.py | 101 ++++++++++++++++++---------- tests/unittest_brain_dataclasses.py | 81 +++++++++++++++++++++- 3 files changed, 150 insertions(+), 36 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4bfec0ec29..9a6582596d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -21,6 +21,10 @@ Release date: TBA Closes PyCQA/pylint#7375 +* The ``dataclass`` brain now understands the ``kw_only`` keyword in dataclass decorators. + + Closes PyCQA/pylint#7290 + What's New in astroid 2.12.5? ============================= diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index b458d70f9f..6549300cf1 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -22,12 +22,7 @@ from astroid import bases, context, helpers, inference_tip from astroid.builder import parse from astroid.const import PY39_PLUS, PY310_PLUS -from astroid.exceptions import ( - AstroidSyntaxError, - InferenceError, - MroError, - UseInferenceDefault, -) +from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault from astroid.manager import AstroidManager from astroid.nodes.node_classes import ( AnnAssign, @@ -89,21 +84,22 @@ def dataclass_transform(node: ClassDef) -> None: if not _check_generate_dataclass_init(node): return - try: - reversed_mro = list(reversed(node.mro())) - except MroError: - reversed_mro = [node] - - field_assigns = {} - field_order = [] - for klass in (k for k in reversed_mro if is_decorated_with_dataclass(k)): - for assign_node in _get_dataclass_attributes(klass, init=True): - name = assign_node.target.name - if name not in field_assigns: - field_order.append(name) - field_assigns[name] = assign_node - - init_str = _generate_dataclass_init([field_assigns[name] for name in field_order]) + kw_only_decorated = False + if PY310_PLUS and node.decorators.nodes: + for decorator in node.decorators.nodes: + if not isinstance(decorator, Call): + kw_only_decorated = False + break + for keyword in decorator.keywords: + if keyword.arg == "kw_only": + kw_only_decorated = keyword.value.bool_value() + + init_str = _generate_dataclass_init( + node, + list(_get_dataclass_attributes(node, init=True)), + kw_only_decorated, + ) + try: init_node = parse(init_str)["__init__"] except AstroidSyntaxError: @@ -179,22 +175,24 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: return True # Check for keyword arguments of the form init=False - return all( - keyword.arg != "init" - and keyword.value.bool_value() # type: ignore[union-attr] # value is never None + return not any( + keyword.arg == "init" + and not keyword.value.bool_value() # type: ignore[union-attr] # value is never None for keyword in found.keywords ) -def _generate_dataclass_init(assigns: list[AnnAssign]) -> str: +def _generate_dataclass_init( + node: ClassDef, assigns: list[AnnAssign], kw_only_decorated: bool +) -> str: """Return an init method for a dataclass given the targets.""" - target_names = [] - params = [] - assignments = [] + params: list[str] = [] + assignments: list[str] = [] + assign_names: list[str] = [] for assign in assigns: name, annotation, value = assign.target.name, assign.annotation, assign.value - target_names.append(name) + assign_names.append(name) if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None init_var = True @@ -208,10 +206,7 @@ def _generate_dataclass_init(assigns: list[AnnAssign]) -> str: init_var = False assignment_str = f"self.{name} = {name}" - if annotation: - param_str = f"{name}: {annotation.as_string()}" - else: - param_str = name + param_str = f"{name}: {annotation.as_string()}" if value: if isinstance(value, Call) and _looks_like_dataclass_field_call( @@ -235,7 +230,45 @@ def _generate_dataclass_init(assigns: list[AnnAssign]) -> str: if not init_var: assignments.append(assignment_str) - params_string = ", ".join(["self"] + params) + try: + base: ClassDef = next(next(iter(node.bases)).infer()) + base_init: FunctionDef | None = base.locals["__init__"][0] + except (StopIteration, InferenceError, KeyError): + base_init = None + + prev_pos_only = "" + prev_kw_only = "" + if base_init and base.is_dataclass: + # Skip the self argument and check for duplicate arguments + all_arguments = base_init.args.format_args()[6:].split(", ") + arguments = ", ".join( + i for i in all_arguments if i.split(":")[0] not in assign_names + ) + try: + prev_pos_only, prev_kw_only = arguments.split("*, ") + except ValueError: + prev_pos_only, prev_kw_only = arguments, "" + + if prev_pos_only and not prev_pos_only.endswith(", "): + prev_pos_only += ", " + + # Construct the new init method paramter string + params_string = "self, " + if prev_pos_only: + params_string += prev_pos_only + if not kw_only_decorated: + params_string += ", ".join(params) + + if not params_string.endswith(", "): + params_string += ", " + + if prev_kw_only: + params_string += "*, " + prev_kw_only + ", " + if kw_only_decorated: + params_string += ", ".join(params) + ", " + elif kw_only_decorated: + params_string += "*, " + ", ".join(params) + ", " + assignments_string = "\n ".join(assignments) if assignments else "pass" return f"def __init__({params_string}) -> None:\n {assignments_string}" diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 406c755775..ad2f648afa 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -625,12 +625,12 @@ class B(A): """ ) init = next(node.infer()) - assert [a.name for a in init.args.args] == ["self", "arg0", "arg2", "arg1"] + assert [a.name for a in init.args.args] == ["self", "arg0", "arg1", "arg2"] assert [a.as_string() if a else None for a in init.args.annotations] == [ None, "float", - "list", # not str "int", + "list", # not str ] @@ -747,3 +747,80 @@ class B: init = next(node_two.infer()) assert [a.name for a in init.args.args] == expected + + +def test_kw_only_decorator() -> None: + """Test that we update the signature correctly based on the keyword. + + kw_only was introduced in PY310. + """ + foodef, bardef, cee, dee = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass(kw_only=True) + class Foo: + a: int + e: str + + + @dataclass(kw_only=False) + class Bar(Foo): + c: int + + + @dataclass(kw_only=False) + class Cee(Bar): + d: int + + + @dataclass(kw_only=True) + class Dee(Cee): + ee: int + + + Foo.__init__ #@ + Bar.__init__ #@ + Cee.__init__ #@ + Dee.__init__ #@ + """ + ) + + foo_init: bases.UnboundMethod = next(foodef.infer()) + if PY310_PLUS: + assert [a.name for a in foo_init.args.args] == ["self"] + assert [a.name for a in foo_init.args.kwonlyargs] == ["a", "e"] + else: + assert [a.name for a in foo_init.args.args] == ["self", "a", "e"] + assert [a.name for a in foo_init.args.kwonlyargs] == [] + + bar_init: bases.UnboundMethod = next(bardef.infer()) + if PY310_PLUS: + assert [a.name for a in bar_init.args.args] == ["self", "c"] + assert [a.name for a in bar_init.args.kwonlyargs] == ["a", "e"] + else: + assert [a.name for a in bar_init.args.args] == ["self", "a", "e", "c"] + assert [a.name for a in bar_init.args.kwonlyargs] == [] + + cee_init: bases.UnboundMethod = next(cee.infer()) + if PY310_PLUS: + assert [a.name for a in cee_init.args.args] == ["self", "c", "d"] + assert [a.name for a in cee_init.args.kwonlyargs] == ["a", "e"] + else: + assert [a.name for a in cee_init.args.args] == ["self", "a", "e", "c", "d"] + assert [a.name for a in cee_init.args.kwonlyargs] == [] + + dee_init: bases.UnboundMethod = next(dee.infer()) + if PY310_PLUS: + assert [a.name for a in dee_init.args.args] == ["self", "c", "d"] + assert [a.name for a in dee_init.args.kwonlyargs] == ["a", "e", "ee"] + else: + assert [a.name for a in dee_init.args.args] == [ + "self", + "a", + "e", + "c", + "d", + "ee", + ] + assert [a.name for a in dee_init.args.kwonlyargs] == [] From 8f8448ee70d968784c3e2b9d622f2b1e8fe61f5d Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 4 Sep 2022 14:32:56 -0400 Subject: [PATCH 16/93] Fix a crash involving `Uninferable` args to `namedtuple` (#1763) --- ChangeLog | 2 ++ astroid/brain/brain_namedtuple_enum.py | 8 +++++++- tests/unittest_brain.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 5e1ce1749f..fdfc90cd95 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,7 +12,9 @@ What's New in astroid 2.12.6? ============================= Release date: TBA +* Fix a crash involving ``Uninferable`` arguments to ``namedtuple()``. + Closes PyCQA/pylint#7375 What's New in astroid 2.12.5? diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index 736f9f9fc5..acc20c516d 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -538,7 +538,13 @@ def _get_namedtuple_fields(node: nodes.Call) -> str: extract a node from them later on. """ names = [] - for elt in next(node.args[1].infer()).elts: + try: + container = next(node.args[1].infer()) + except (InferenceError, StopIteration) as exc: + raise UseInferenceDefault from exc + if not isinstance(container, nodes.BaseContainer): + raise UseInferenceDefault + for elt in container.elts: if isinstance(elt, nodes.Const): names.append(elt.as_string()) continue diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 149100eb50..bcd92c02d5 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -18,6 +18,7 @@ import astroid from astroid import MANAGER, bases, builder, nodes, objects, test_utils, util from astroid.bases import Instance +from astroid.brain.brain_namedtuple_enum import _get_namedtuple_fields from astroid.const import PY39_PLUS from astroid.exceptions import ( AttributeInferenceError, @@ -1644,6 +1645,25 @@ def NamedTuple(): ) next(node.infer()) + def test_namedtuple_uninferable_member(self) -> None: + call = builder.extract_node( + """ + from typing import namedtuple + namedtuple('uninf', {x: x for x in range(0)}) #@""" + ) + with pytest.raises(UseInferenceDefault): + _get_namedtuple_fields(call) + + call = builder.extract_node( + """ + from typing import namedtuple + uninferable = {x: x for x in range(0)} + namedtuple('uninferable', uninferable) #@ + """ + ) + with pytest.raises(UseInferenceDefault): + _get_namedtuple_fields(call) + def test_typing_types(self) -> None: ast_nodes = builder.extract_node( """ From 1f5dc457729d7219178ace9705d8445e89513472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Mon, 5 Sep 2022 16:28:32 +0200 Subject: [PATCH 17/93] Handle ``dataclass`` ``kw_only`` keyword correctly (#1764) Co-authored-by: Jacob Walls --- ChangeLog | 4 ++ astroid/brain/brain_dataclasses.py | 101 ++++++++++++++++++---------- tests/unittest_brain_dataclasses.py | 81 +++++++++++++++++++++- 3 files changed, 150 insertions(+), 36 deletions(-) diff --git a/ChangeLog b/ChangeLog index fdfc90cd95..1836e35efb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,10 @@ Release date: TBA Closes PyCQA/pylint#7375 +* The ``dataclass`` brain now understands the ``kw_only`` keyword in dataclass decorators. + + Closes PyCQA/pylint#7290 + What's New in astroid 2.12.5? ============================= diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index b458d70f9f..6549300cf1 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -22,12 +22,7 @@ from astroid import bases, context, helpers, inference_tip from astroid.builder import parse from astroid.const import PY39_PLUS, PY310_PLUS -from astroid.exceptions import ( - AstroidSyntaxError, - InferenceError, - MroError, - UseInferenceDefault, -) +from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault from astroid.manager import AstroidManager from astroid.nodes.node_classes import ( AnnAssign, @@ -89,21 +84,22 @@ def dataclass_transform(node: ClassDef) -> None: if not _check_generate_dataclass_init(node): return - try: - reversed_mro = list(reversed(node.mro())) - except MroError: - reversed_mro = [node] - - field_assigns = {} - field_order = [] - for klass in (k for k in reversed_mro if is_decorated_with_dataclass(k)): - for assign_node in _get_dataclass_attributes(klass, init=True): - name = assign_node.target.name - if name not in field_assigns: - field_order.append(name) - field_assigns[name] = assign_node - - init_str = _generate_dataclass_init([field_assigns[name] for name in field_order]) + kw_only_decorated = False + if PY310_PLUS and node.decorators.nodes: + for decorator in node.decorators.nodes: + if not isinstance(decorator, Call): + kw_only_decorated = False + break + for keyword in decorator.keywords: + if keyword.arg == "kw_only": + kw_only_decorated = keyword.value.bool_value() + + init_str = _generate_dataclass_init( + node, + list(_get_dataclass_attributes(node, init=True)), + kw_only_decorated, + ) + try: init_node = parse(init_str)["__init__"] except AstroidSyntaxError: @@ -179,22 +175,24 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: return True # Check for keyword arguments of the form init=False - return all( - keyword.arg != "init" - and keyword.value.bool_value() # type: ignore[union-attr] # value is never None + return not any( + keyword.arg == "init" + and not keyword.value.bool_value() # type: ignore[union-attr] # value is never None for keyword in found.keywords ) -def _generate_dataclass_init(assigns: list[AnnAssign]) -> str: +def _generate_dataclass_init( + node: ClassDef, assigns: list[AnnAssign], kw_only_decorated: bool +) -> str: """Return an init method for a dataclass given the targets.""" - target_names = [] - params = [] - assignments = [] + params: list[str] = [] + assignments: list[str] = [] + assign_names: list[str] = [] for assign in assigns: name, annotation, value = assign.target.name, assign.annotation, assign.value - target_names.append(name) + assign_names.append(name) if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None init_var = True @@ -208,10 +206,7 @@ def _generate_dataclass_init(assigns: list[AnnAssign]) -> str: init_var = False assignment_str = f"self.{name} = {name}" - if annotation: - param_str = f"{name}: {annotation.as_string()}" - else: - param_str = name + param_str = f"{name}: {annotation.as_string()}" if value: if isinstance(value, Call) and _looks_like_dataclass_field_call( @@ -235,7 +230,45 @@ def _generate_dataclass_init(assigns: list[AnnAssign]) -> str: if not init_var: assignments.append(assignment_str) - params_string = ", ".join(["self"] + params) + try: + base: ClassDef = next(next(iter(node.bases)).infer()) + base_init: FunctionDef | None = base.locals["__init__"][0] + except (StopIteration, InferenceError, KeyError): + base_init = None + + prev_pos_only = "" + prev_kw_only = "" + if base_init and base.is_dataclass: + # Skip the self argument and check for duplicate arguments + all_arguments = base_init.args.format_args()[6:].split(", ") + arguments = ", ".join( + i for i in all_arguments if i.split(":")[0] not in assign_names + ) + try: + prev_pos_only, prev_kw_only = arguments.split("*, ") + except ValueError: + prev_pos_only, prev_kw_only = arguments, "" + + if prev_pos_only and not prev_pos_only.endswith(", "): + prev_pos_only += ", " + + # Construct the new init method paramter string + params_string = "self, " + if prev_pos_only: + params_string += prev_pos_only + if not kw_only_decorated: + params_string += ", ".join(params) + + if not params_string.endswith(", "): + params_string += ", " + + if prev_kw_only: + params_string += "*, " + prev_kw_only + ", " + if kw_only_decorated: + params_string += ", ".join(params) + ", " + elif kw_only_decorated: + params_string += "*, " + ", ".join(params) + ", " + assignments_string = "\n ".join(assignments) if assignments else "pass" return f"def __init__({params_string}) -> None:\n {assignments_string}" diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 406c755775..ad2f648afa 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -625,12 +625,12 @@ class B(A): """ ) init = next(node.infer()) - assert [a.name for a in init.args.args] == ["self", "arg0", "arg2", "arg1"] + assert [a.name for a in init.args.args] == ["self", "arg0", "arg1", "arg2"] assert [a.as_string() if a else None for a in init.args.annotations] == [ None, "float", - "list", # not str "int", + "list", # not str ] @@ -747,3 +747,80 @@ class B: init = next(node_two.infer()) assert [a.name for a in init.args.args] == expected + + +def test_kw_only_decorator() -> None: + """Test that we update the signature correctly based on the keyword. + + kw_only was introduced in PY310. + """ + foodef, bardef, cee, dee = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass(kw_only=True) + class Foo: + a: int + e: str + + + @dataclass(kw_only=False) + class Bar(Foo): + c: int + + + @dataclass(kw_only=False) + class Cee(Bar): + d: int + + + @dataclass(kw_only=True) + class Dee(Cee): + ee: int + + + Foo.__init__ #@ + Bar.__init__ #@ + Cee.__init__ #@ + Dee.__init__ #@ + """ + ) + + foo_init: bases.UnboundMethod = next(foodef.infer()) + if PY310_PLUS: + assert [a.name for a in foo_init.args.args] == ["self"] + assert [a.name for a in foo_init.args.kwonlyargs] == ["a", "e"] + else: + assert [a.name for a in foo_init.args.args] == ["self", "a", "e"] + assert [a.name for a in foo_init.args.kwonlyargs] == [] + + bar_init: bases.UnboundMethod = next(bardef.infer()) + if PY310_PLUS: + assert [a.name for a in bar_init.args.args] == ["self", "c"] + assert [a.name for a in bar_init.args.kwonlyargs] == ["a", "e"] + else: + assert [a.name for a in bar_init.args.args] == ["self", "a", "e", "c"] + assert [a.name for a in bar_init.args.kwonlyargs] == [] + + cee_init: bases.UnboundMethod = next(cee.infer()) + if PY310_PLUS: + assert [a.name for a in cee_init.args.args] == ["self", "c", "d"] + assert [a.name for a in cee_init.args.kwonlyargs] == ["a", "e"] + else: + assert [a.name for a in cee_init.args.args] == ["self", "a", "e", "c", "d"] + assert [a.name for a in cee_init.args.kwonlyargs] == [] + + dee_init: bases.UnboundMethod = next(dee.infer()) + if PY310_PLUS: + assert [a.name for a in dee_init.args.args] == ["self", "c", "d"] + assert [a.name for a in dee_init.args.kwonlyargs] == ["a", "e", "ee"] + else: + assert [a.name for a in dee_init.args.args] == [ + "self", + "a", + "e", + "c", + "d", + "ee", + ] + assert [a.name for a in dee_init.args.kwonlyargs] == [] From e194631088aee587140c029a0404f8d40c6765b5 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 5 Sep 2022 17:18:56 +0200 Subject: [PATCH 18/93] Bump astroid to 2.12.6, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1836e35efb..37314dd4c6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,10 +8,16 @@ Release date: TBA -What's New in astroid 2.12.6? +What's New in astroid 2.12.7? ============================= Release date: TBA + + +What's New in astroid 2.12.6? +============================= +Release date: 2022-09-05 + * Fix a crash involving ``Uninferable`` arguments to ``namedtuple()``. Closes PyCQA/pylint#7375 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 947ede2392..b8e952e2ce 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.5" +__version__ = "2.12.6" version = __version__ diff --git a/tbump.toml b/tbump.toml index 9849717172..aa0e349808 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.5" +current = "2.12.6" regex = ''' ^(?P0|[1-9]\d*) \. From 3a0b104e3a2c29fa57c187675d497102149c2750 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:36:34 +0000 Subject: [PATCH 19/93] Bump black from 22.6.0 to 22.8.0 Bumps [black](https://github.com/psf/black) from 22.6.0 to 22.8.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.6.0...22.8.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index bfc963be62..2d1baeaa93 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,4 +1,4 @@ -black==22.6.0 +black==22.8.0 pylint==2.15.0 isort==5.10.1 flake8==5.0.4 From 58af36bb3ac8a93f30dc5bfd0fe90f8a2f61c32a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Sep 2022 01:16:29 +0000 Subject: [PATCH 20/93] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/myint/autoflake: v1.5.1 → v1.5.3](https://github.com/myint/autoflake/compare/v1.5.1...v1.5.3) - [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84c3e7ca3e..08dd220862 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: end-of-file-fixer exclude: tests/testdata - repo: https://github.com/myint/autoflake - rev: v1.5.1 + rev: v1.5.3 hooks: - id: autoflake exclude: tests/testdata|astroid/__init__.py|astroid/scoped_nodes.py|astroid/node_classes.py @@ -44,7 +44,7 @@ repos: - id: black-disable-checker exclude: tests/unittest_nodes_lineno.py - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: [--safe, --quiet] From da3e9fbb9ef2686e573a0dd3dcbed873d1ee7873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:21:11 +0200 Subject: [PATCH 21/93] Fix crash in ``dataclass`` brain (#1768) --- ChangeLog | 3 +++ astroid/brain/brain_dataclasses.py | 4 +++- tests/unittest_brain_dataclasses.py | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index bdf36d531c..f655c16e64 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,9 @@ What's New in astroid 2.12.7? ============================= Release date: TBA +* Fixed a crash in the ``dataclass`` brain for uninferable bases. + + Closes PyCQA/pylint#7418 What's New in astroid 2.12.6? diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 6549300cf1..3c83a25986 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -231,7 +231,9 @@ def _generate_dataclass_init( assignments.append(assignment_str) try: - base: ClassDef = next(next(iter(node.bases)).infer()) + base = next(next(iter(node.bases)).infer()) + if not isinstance(base, ClassDef): + raise InferenceError base_init: FunctionDef | None = base.locals["__init__"][0] except (StopIteration, InferenceError, KeyError): base_init = None diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index ad2f648afa..492d9ea555 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -824,3 +824,26 @@ class Dee(Cee): "ee", ] assert [a.name for a in dee_init.args.kwonlyargs] == [] + + +def test_dataclass_with_unknown_base() -> None: + """Regression test for dataclasses with unknown base classes. + + Reported in https://github.com/PyCQA/pylint/issues/7418 + """ + node = astroid.extract_node( + """ + import dataclasses + + from unknown import Unknown + + + @dataclasses.dataclass + class MyDataclass(Unknown): + pass + + MyDataclass() + """ + ) + + assert next(node.infer()) From fbe7859ec56ab64cc4d1b2604082878fdfdc8a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:21:11 +0200 Subject: [PATCH 22/93] Fix crash in ``dataclass`` brain (#1768) --- ChangeLog | 3 +++ astroid/brain/brain_dataclasses.py | 4 +++- tests/unittest_brain_dataclasses.py | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 37314dd4c6..1617711d80 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,9 @@ What's New in astroid 2.12.7? ============================= Release date: TBA +* Fixed a crash in the ``dataclass`` brain for uninferable bases. + + Closes PyCQA/pylint#7418 What's New in astroid 2.12.6? diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 6549300cf1..3c83a25986 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -231,7 +231,9 @@ def _generate_dataclass_init( assignments.append(assignment_str) try: - base: ClassDef = next(next(iter(node.bases)).infer()) + base = next(next(iter(node.bases)).infer()) + if not isinstance(base, ClassDef): + raise InferenceError base_init: FunctionDef | None = base.locals["__init__"][0] except (StopIteration, InferenceError, KeyError): base_init = None diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index ad2f648afa..492d9ea555 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -824,3 +824,26 @@ class Dee(Cee): "ee", ] assert [a.name for a in dee_init.args.kwonlyargs] == [] + + +def test_dataclass_with_unknown_base() -> None: + """Regression test for dataclasses with unknown base classes. + + Reported in https://github.com/PyCQA/pylint/issues/7418 + """ + node = astroid.extract_node( + """ + import dataclasses + + from unknown import Unknown + + + @dataclasses.dataclass + class MyDataclass(Unknown): + pass + + MyDataclass() + """ + ) + + assert next(node.infer()) From b2dbf7bdaa02436962c4c5ccbc6bbb8b8e0c3295 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 6 Sep 2022 12:23:08 +0200 Subject: [PATCH 23/93] Bump astroid to 2.12.7, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1617711d80..21476b4fd3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,10 +8,16 @@ Release date: TBA -What's New in astroid 2.12.7? +What's New in astroid 2.12.8? ============================= Release date: TBA + + +What's New in astroid 2.12.7? +============================= +Release date: 2022-09-06 + * Fixed a crash in the ``dataclass`` brain for uninferable bases. Closes PyCQA/pylint#7418 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index b8e952e2ce..db3d707674 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.6" +__version__ = "2.12.7" version = __version__ diff --git a/tbump.toml b/tbump.toml index aa0e349808..a876b7bc0a 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.6" +current = "2.12.7" regex = ''' ^(?P0|[1-9]\d*) \. From 3331d624eba4441ee1979d36b2c8277e576b27f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 17:47:33 +0200 Subject: [PATCH 24/93] Parse default values in ``dataclass`` attributes correctly (#1771) --- ChangeLog | 2 ++ astroid/brain/brain_dataclasses.py | 5 +--- astroid/nodes/node_classes.py | 17 +++++++++--- tests/unittest_brain_dataclasses.py | 41 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0736de4f9b..8b6db8c877 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,7 +17,9 @@ What's New in astroid 2.12.8? ============================= Release date: TBA +* Fixed parsing of default values in ``dataclass`` attributes. + Closes PyCQA/pylint#7425 What's New in astroid 2.12.7? ============================= diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 3c83a25986..bac8cda0e4 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -242,10 +242,7 @@ def _generate_dataclass_init( prev_kw_only = "" if base_init and base.is_dataclass: # Skip the self argument and check for duplicate arguments - all_arguments = base_init.args.format_args()[6:].split(", ") - arguments = ", ".join( - i for i in all_arguments if i.split(":")[0] not in assign_names - ) + arguments = base_init.args.format_args(skippable_names=assign_names)[6:] try: prev_pos_only, prev_kw_only = arguments.split("*, ") except ValueError: diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 0d23d209e0..caa79d093e 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -784,7 +784,7 @@ def arguments(self): """Get all the arguments for this node, including positional only and positional and keyword""" return list(itertools.chain((self.posonlyargs or ()), self.args or ())) - def format_args(self): + def format_args(self, *, skippable_names: set[str] | None = None) -> str: """Get the arguments formatted as string. :returns: The formatted arguments. @@ -804,6 +804,7 @@ def format_args(self): self.posonlyargs, positional_only_defaults, self.posonlyargs_annotations, + skippable_names=skippable_names, ) ) result.append("/") @@ -813,6 +814,7 @@ def format_args(self): self.args, positional_or_keyword_defaults, getattr(self, "annotations", None), + skippable_names=skippable_names, ) ) if self.vararg: @@ -822,7 +824,10 @@ def format_args(self): result.append("*") result.append( _format_args( - self.kwonlyargs, self.kw_defaults, self.kwonlyargs_annotations + self.kwonlyargs, + self.kw_defaults, + self.kwonlyargs_annotations, + skippable_names=skippable_names, ) ) if self.kwarg: @@ -929,7 +934,11 @@ def _find_arg(argname, args, rec=False): return None, None -def _format_args(args, defaults=None, annotations=None): +def _format_args( + args, defaults=None, annotations=None, skippable_names: set[str] | None = None +) -> str: + if skippable_names is None: + skippable_names = set() values = [] if args is None: return "" @@ -939,6 +948,8 @@ def _format_args(args, defaults=None, annotations=None): default_offset = len(args) - len(defaults) packed = itertools.zip_longest(args, annotations) for i, (arg, annotation) in enumerate(packed): + if arg.name in skippable_names: + continue if isinstance(arg, Tuple): values.append(f"({_format_args(arg.elts)})") else: diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 492d9ea555..3e71a409d1 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -847,3 +847,44 @@ class MyDataclass(Unknown): ) assert next(node.infer()) + + +def test_dataclass_with_default_factory() -> None: + """Regression test for dataclasses with default values. + + Reported in https://github.com/PyCQA/pylint/issues/7425 + """ + bad_node, good_node = astroid.extract_node( + """ + from dataclasses import dataclass + from typing import Union + + @dataclass + class BadExampleParentClass: + xyz: Union[str, int] + + @dataclass + class BadExampleClass(BadExampleParentClass): + xyz: str = "" + + BadExampleClass.__init__ #@ + + @dataclass + class GoodExampleParentClass: + xyz: str + + @dataclass + class GoodExampleClass(GoodExampleParentClass): + xyz: str = "" + + GoodExampleClass.__init__ #@ + """ + ) + + bad_init: bases.UnboundMethod = next(bad_node.infer()) + assert bad_init.args.defaults + assert [a.name for a in bad_init.args.args] == ["self", "xyz"] + + good_init: bases.UnboundMethod = next(good_node.infer()) + assert bad_init.args.defaults + assert [a.name for a in good_init.args.args] == ["self", "xyz"] From f3159a513a29954d752f08bd47da230fa6c9a04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:01:11 +0200 Subject: [PATCH 25/93] Fix crash in ``dataclass`` brain (#1770) --- ChangeLog | 4 + astroid/brain/brain_dataclasses.py | 132 ++++++++++++++-------------- tests/unittest_brain_dataclasses.py | 24 +++++ 3 files changed, 94 insertions(+), 66 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8b6db8c877..9b22486900 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,10 @@ What's New in astroid 2.12.8? ============================= Release date: TBA +* Fixed a crash in the ``dataclass`` brain for ``InitVars`` without subscript typing. + + Closes PyCQA/pylint#7422 + * Fixed parsing of default values in ``dataclass`` attributes. Closes PyCQA/pylint#7425 diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index bac8cda0e4..629024902c 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -16,26 +16,16 @@ from __future__ import annotations import sys -from collections.abc import Generator +from collections.abc import Iterator from typing import Tuple, Union -from astroid import bases, context, helpers, inference_tip +from astroid import bases, context, helpers, nodes from astroid.builder import parse from astroid.const import PY39_PLUS, PY310_PLUS from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault +from astroid.inference_tip import inference_tip from astroid.manager import AstroidManager -from astroid.nodes.node_classes import ( - AnnAssign, - Assign, - AssignName, - Attribute, - Call, - Name, - NodeNG, - Subscript, - Unknown, -) -from astroid.nodes.scoped_nodes import ClassDef, FunctionDef +from astroid.typing import InferenceResult from astroid.util import Uninferable if sys.version_info >= (3, 8): @@ -44,7 +34,9 @@ from typing_extensions import Literal _FieldDefaultReturn = Union[ - None, Tuple[Literal["default"], NodeNG], Tuple[Literal["default_factory"], Call] + None, + Tuple[Literal["default"], nodes.NodeNG], + Tuple[Literal["default_factory"], nodes.Call], ] DATACLASSES_DECORATORS = frozenset(("dataclass",)) @@ -55,9 +47,11 @@ DEFAULT_FACTORY = "_HAS_DEFAULT_FACTORY" # based on typing.py -def is_decorated_with_dataclass(node, decorator_names=DATACLASSES_DECORATORS): +def is_decorated_with_dataclass( + node: nodes.ClassDef, decorator_names: frozenset[str] = DATACLASSES_DECORATORS +) -> bool: """Return True if a decorated node has a `dataclass` decorator applied.""" - if not isinstance(node, ClassDef) or not node.decorators: + if not isinstance(node, nodes.ClassDef) or not node.decorators: return False return any( @@ -66,14 +60,14 @@ def is_decorated_with_dataclass(node, decorator_names=DATACLASSES_DECORATORS): ) -def dataclass_transform(node: ClassDef) -> None: +def dataclass_transform(node: nodes.ClassDef) -> None: """Rewrite a dataclass to be easily understood by pylint""" node.is_dataclass = True for assign_node in _get_dataclass_attributes(node): name = assign_node.target.name - rhs_node = Unknown( + rhs_node = nodes.Unknown( lineno=assign_node.lineno, col_offset=assign_node.col_offset, parent=assign_node, @@ -87,7 +81,7 @@ def dataclass_transform(node: ClassDef) -> None: kw_only_decorated = False if PY310_PLUS and node.decorators.nodes: for decorator in node.decorators.nodes: - if not isinstance(decorator, Call): + if not isinstance(decorator, nodes.Call): kw_only_decorated = False break for keyword in decorator.keywords: @@ -116,15 +110,17 @@ def dataclass_transform(node: ClassDef) -> None: root.locals[DEFAULT_FACTORY] = [new_assign.targets[0]] -def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: +def _get_dataclass_attributes( + node: nodes.ClassDef, init: bool = False +) -> Iterator[nodes.AnnAssign]: """Yield the AnnAssign nodes of dataclass attributes for the node. If init is True, also include InitVars, but exclude attributes from calls to field where init=False. """ for assign_node in node.body: - if not isinstance(assign_node, AnnAssign) or not isinstance( - assign_node.target, AssignName + if not isinstance(assign_node, nodes.AnnAssign) or not isinstance( + assign_node.target, nodes.AssignName ): continue @@ -137,11 +133,10 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: if init: value = assign_node.value if ( - isinstance(value, Call) + isinstance(value, nodes.Call) and _looks_like_dataclass_field_call(value, check_scope=False) and any( - keyword.arg == "init" - and not keyword.value.bool_value() # type: ignore[union-attr] # value is never None + keyword.arg == "init" and not keyword.value.bool_value() for keyword in value.keywords ) ): @@ -152,7 +147,7 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: yield assign_node -def _check_generate_dataclass_init(node: ClassDef) -> bool: +def _check_generate_dataclass_init(node: nodes.ClassDef) -> bool: """Return True if we should generate an __init__ method for node. This is True when: @@ -165,7 +160,7 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: found = None for decorator_attribute in node.decorators.nodes: - if not isinstance(decorator_attribute, Call): + if not isinstance(decorator_attribute, nodes.Call): continue if _looks_like_dataclass_decorator(decorator_attribute): @@ -183,7 +178,7 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: def _generate_dataclass_init( - node: ClassDef, assigns: list[AnnAssign], kw_only_decorated: bool + node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool ) -> str: """Return an init method for a dataclass given the targets.""" params: list[str] = [] @@ -196,7 +191,7 @@ def _generate_dataclass_init( if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None init_var = True - if isinstance(annotation, Subscript): + if isinstance(annotation, nodes.Subscript): annotation = annotation.slice else: # Cannot determine type annotation for parameter from InitVar @@ -206,10 +201,13 @@ def _generate_dataclass_init( init_var = False assignment_str = f"self.{name} = {name}" - param_str = f"{name}: {annotation.as_string()}" + if annotation is not None: + param_str = f"{name}: {annotation.as_string()}" + else: + param_str = name if value: - if isinstance(value, Call) and _looks_like_dataclass_field_call( + if isinstance(value, nodes.Call) and _looks_like_dataclass_field_call( value, check_scope=False ): result = _get_field_default(value) @@ -232,9 +230,9 @@ def _generate_dataclass_init( try: base = next(next(iter(node.bases)).infer()) - if not isinstance(base, ClassDef): + if not isinstance(base, nodes.ClassDef): raise InferenceError - base_init: FunctionDef | None = base.locals["__init__"][0] + base_init: nodes.FunctionDef | None = base.locals["__init__"][0] except (StopIteration, InferenceError, KeyError): base_init = None @@ -273,8 +271,8 @@ def _generate_dataclass_init( def infer_dataclass_attribute( - node: Unknown, ctx: context.InferenceContext | None = None -) -> Generator: + node: nodes.Unknown, ctx: context.InferenceContext | None = None +) -> Iterator[InferenceResult]: """Inference tip for an Unknown node that was dynamically generated to represent a dataclass attribute. @@ -282,7 +280,7 @@ def infer_dataclass_attribute( Then, an Instance of the annotated class is yielded. """ assign = node.parent - if not isinstance(assign, AnnAssign): + if not isinstance(assign, nodes.AnnAssign): yield Uninferable return @@ -296,10 +294,10 @@ def infer_dataclass_attribute( def infer_dataclass_field_call( - node: Call, ctx: context.InferenceContext | None = None -) -> Generator: + node: nodes.Call, ctx: context.InferenceContext | None = None +) -> Iterator[InferenceResult]: """Inference tip for dataclass field calls.""" - if not isinstance(node.parent, (AnnAssign, Assign)): + if not isinstance(node.parent, (nodes.AnnAssign, nodes.Assign)): raise UseInferenceDefault result = _get_field_default(node) if not result: @@ -315,14 +313,14 @@ def infer_dataclass_field_call( def _looks_like_dataclass_decorator( - node: NodeNG, decorator_names: frozenset[str] = DATACLASSES_DECORATORS + node: nodes.NodeNG, decorator_names: frozenset[str] = DATACLASSES_DECORATORS ) -> bool: """Return True if node looks like a dataclass decorator. Uses inference to lookup the value of the node, and if that fails, matches against specific names. """ - if isinstance(node, Call): # decorator with arguments + if isinstance(node, nodes.Call): # decorator with arguments node = node.func try: inferred = next(node.infer()) @@ -330,21 +328,21 @@ def _looks_like_dataclass_decorator( inferred = Uninferable if inferred is Uninferable: - if isinstance(node, Name): + if isinstance(node, nodes.Name): return node.name in decorator_names - if isinstance(node, Attribute): + if isinstance(node, nodes.Attribute): return node.attrname in decorator_names return False return ( - isinstance(inferred, FunctionDef) + isinstance(inferred, nodes.FunctionDef) and inferred.name in decorator_names and inferred.root().name in DATACLASS_MODULES ) -def _looks_like_dataclass_attribute(node: Unknown) -> bool: +def _looks_like_dataclass_attribute(node: nodes.Unknown) -> bool: """Return True if node was dynamically generated as the child of an AnnAssign statement. """ @@ -354,13 +352,15 @@ def _looks_like_dataclass_attribute(node: Unknown) -> bool: scope = parent.scope() return ( - isinstance(parent, AnnAssign) - and isinstance(scope, ClassDef) + isinstance(parent, nodes.AnnAssign) + and isinstance(scope, nodes.ClassDef) and is_decorated_with_dataclass(scope) ) -def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bool: +def _looks_like_dataclass_field_call( + node: nodes.Call, check_scope: bool = True +) -> bool: """Return True if node is calling dataclasses field or Field from an AnnAssign statement directly in the body of a ClassDef. @@ -370,9 +370,9 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo stmt = node.statement(future=True) scope = stmt.scope() if not ( - isinstance(stmt, AnnAssign) + isinstance(stmt, nodes.AnnAssign) and stmt.value is not None - and isinstance(scope, ClassDef) + and isinstance(scope, nodes.ClassDef) and is_decorated_with_dataclass(scope) ): return False @@ -382,13 +382,13 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo except (InferenceError, StopIteration): return False - if not isinstance(inferred, FunctionDef): + if not isinstance(inferred, nodes.FunctionDef): return False return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default(field_call: Call) -> _FieldDefaultReturn: +def _get_field_default(field_call: nodes.Call) -> _FieldDefaultReturn: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node @@ -408,7 +408,7 @@ def _get_field_default(field_call: Call) -> _FieldDefaultReturn: return "default", default if default is None and default_factory is not None: - new_call = Call( + new_call = nodes.Call( lineno=field_call.lineno, col_offset=field_call.col_offset, parent=field_call.parent, @@ -419,7 +419,7 @@ def _get_field_default(field_call: Call) -> _FieldDefaultReturn: return None -def _is_class_var(node: NodeNG) -> bool: +def _is_class_var(node: nodes.NodeNG) -> bool: """Return True if node is a ClassVar, with or without subscripting.""" if PY39_PLUS: try: @@ -431,15 +431,15 @@ def _is_class_var(node: NodeNG) -> bool: # Before Python 3.9, inference returns typing._SpecialForm instead of ClassVar. # Our backup is to inspect the node's structure. - return isinstance(node, Subscript) and ( - isinstance(node.value, Name) + return isinstance(node, nodes.Subscript) and ( + isinstance(node.value, nodes.Name) and node.value.name == "ClassVar" - or isinstance(node.value, Attribute) + or isinstance(node.value, nodes.Attribute) and node.value.attrname == "ClassVar" ) -def _is_keyword_only_sentinel(node: NodeNG) -> bool: +def _is_keyword_only_sentinel(node: nodes.NodeNG) -> bool: """Return True if node is the KW_ONLY sentinel.""" if not PY310_PLUS: return False @@ -450,7 +450,7 @@ def _is_keyword_only_sentinel(node: NodeNG) -> bool: ) -def _is_init_var(node: NodeNG) -> bool: +def _is_init_var(node: nodes.NodeNG) -> bool: """Return True if node is an InitVar, with or without subscripting.""" try: inferred = next(node.infer()) @@ -473,8 +473,8 @@ def _is_init_var(node: NodeNG) -> bool: def _infer_instance_from_annotation( - node: NodeNG, ctx: context.InferenceContext | None = None -) -> Generator: + node: nodes.NodeNG, ctx: context.InferenceContext | None = None +) -> Iterator[type[Uninferable] | bases.Instance]: """Infer an instance corresponding to the type annotation represented by node. Currently has limited support for the typing module. @@ -484,7 +484,7 @@ def _infer_instance_from_annotation( klass = next(node.infer(context=ctx)) except (InferenceError, StopIteration): yield Uninferable - if not isinstance(klass, ClassDef): + if not isinstance(klass, nodes.ClassDef): yield Uninferable elif klass.root().name in { "typing", @@ -500,17 +500,17 @@ def _infer_instance_from_annotation( AstroidManager().register_transform( - ClassDef, dataclass_transform, is_decorated_with_dataclass + nodes.ClassDef, dataclass_transform, is_decorated_with_dataclass ) AstroidManager().register_transform( - Call, + nodes.Call, inference_tip(infer_dataclass_field_call, raise_on_overwrite=True), _looks_like_dataclass_field_call, ) AstroidManager().register_transform( - Unknown, + nodes.Unknown, inference_tip(infer_dataclass_attribute, raise_on_overwrite=True), _looks_like_dataclass_attribute, ) diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 3e71a409d1..2f59d4c331 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -849,6 +849,30 @@ class MyDataclass(Unknown): assert next(node.infer()) +def test_dataclass_with_unknown_typing() -> None: + """Regression test for dataclasses with unknown base classes. + + Reported in https://github.com/PyCQA/pylint/issues/7422 + """ + node = astroid.extract_node( + """ + from dataclasses import dataclass, InitVar + + + @dataclass + class TestClass: + '''Test Class''' + + config: InitVar = None + + TestClass.__init__ #@ + """ + ) + + init_def: bases.UnboundMethod = next(node.infer()) + assert [a.name for a in init_def.args.args] == ["self", "config"] + + def test_dataclass_with_default_factory() -> None: """Regression test for dataclasses with default values. From 0720cbcd05a3938bdf8141328a1ceed1e2f38bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 17:47:33 +0200 Subject: [PATCH 26/93] Parse default values in ``dataclass`` attributes correctly (#1771) --- ChangeLog | 2 ++ astroid/brain/brain_dataclasses.py | 5 +--- astroid/nodes/node_classes.py | 17 +++++++++--- tests/unittest_brain_dataclasses.py | 41 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index 21476b4fd3..24ae8d0518 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,7 +12,9 @@ What's New in astroid 2.12.8? ============================= Release date: TBA +* Fixed parsing of default values in ``dataclass`` attributes. + Closes PyCQA/pylint#7425 What's New in astroid 2.12.7? ============================= diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 3c83a25986..bac8cda0e4 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -242,10 +242,7 @@ def _generate_dataclass_init( prev_kw_only = "" if base_init and base.is_dataclass: # Skip the self argument and check for duplicate arguments - all_arguments = base_init.args.format_args()[6:].split(", ") - arguments = ", ".join( - i for i in all_arguments if i.split(":")[0] not in assign_names - ) + arguments = base_init.args.format_args(skippable_names=assign_names)[6:] try: prev_pos_only, prev_kw_only = arguments.split("*, ") except ValueError: diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 0d23d209e0..caa79d093e 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -784,7 +784,7 @@ def arguments(self): """Get all the arguments for this node, including positional only and positional and keyword""" return list(itertools.chain((self.posonlyargs or ()), self.args or ())) - def format_args(self): + def format_args(self, *, skippable_names: set[str] | None = None) -> str: """Get the arguments formatted as string. :returns: The formatted arguments. @@ -804,6 +804,7 @@ def format_args(self): self.posonlyargs, positional_only_defaults, self.posonlyargs_annotations, + skippable_names=skippable_names, ) ) result.append("/") @@ -813,6 +814,7 @@ def format_args(self): self.args, positional_or_keyword_defaults, getattr(self, "annotations", None), + skippable_names=skippable_names, ) ) if self.vararg: @@ -822,7 +824,10 @@ def format_args(self): result.append("*") result.append( _format_args( - self.kwonlyargs, self.kw_defaults, self.kwonlyargs_annotations + self.kwonlyargs, + self.kw_defaults, + self.kwonlyargs_annotations, + skippable_names=skippable_names, ) ) if self.kwarg: @@ -929,7 +934,11 @@ def _find_arg(argname, args, rec=False): return None, None -def _format_args(args, defaults=None, annotations=None): +def _format_args( + args, defaults=None, annotations=None, skippable_names: set[str] | None = None +) -> str: + if skippable_names is None: + skippable_names = set() values = [] if args is None: return "" @@ -939,6 +948,8 @@ def _format_args(args, defaults=None, annotations=None): default_offset = len(args) - len(defaults) packed = itertools.zip_longest(args, annotations) for i, (arg, annotation) in enumerate(packed): + if arg.name in skippable_names: + continue if isinstance(arg, Tuple): values.append(f"({_format_args(arg.elts)})") else: diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 492d9ea555..3e71a409d1 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -847,3 +847,44 @@ class MyDataclass(Unknown): ) assert next(node.infer()) + + +def test_dataclass_with_default_factory() -> None: + """Regression test for dataclasses with default values. + + Reported in https://github.com/PyCQA/pylint/issues/7425 + """ + bad_node, good_node = astroid.extract_node( + """ + from dataclasses import dataclass + from typing import Union + + @dataclass + class BadExampleParentClass: + xyz: Union[str, int] + + @dataclass + class BadExampleClass(BadExampleParentClass): + xyz: str = "" + + BadExampleClass.__init__ #@ + + @dataclass + class GoodExampleParentClass: + xyz: str + + @dataclass + class GoodExampleClass(GoodExampleParentClass): + xyz: str = "" + + GoodExampleClass.__init__ #@ + """ + ) + + bad_init: bases.UnboundMethod = next(bad_node.infer()) + assert bad_init.args.defaults + assert [a.name for a in bad_init.args.args] == ["self", "xyz"] + + good_init: bases.UnboundMethod = next(good_node.infer()) + assert bad_init.args.defaults + assert [a.name for a in good_init.args.args] == ["self", "xyz"] From fab511c1477d13262e9e33b015906d4bca683953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 6 Sep 2022 18:01:11 +0200 Subject: [PATCH 27/93] Fix crash in ``dataclass`` brain (#1770) --- ChangeLog | 4 + astroid/brain/brain_dataclasses.py | 132 ++++++++++++++-------------- tests/unittest_brain_dataclasses.py | 24 +++++ 3 files changed, 94 insertions(+), 66 deletions(-) diff --git a/ChangeLog b/ChangeLog index 24ae8d0518..291be7498e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,10 @@ What's New in astroid 2.12.8? ============================= Release date: TBA +* Fixed a crash in the ``dataclass`` brain for ``InitVars`` without subscript typing. + + Closes PyCQA/pylint#7422 + * Fixed parsing of default values in ``dataclass`` attributes. Closes PyCQA/pylint#7425 diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index bac8cda0e4..629024902c 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -16,26 +16,16 @@ from __future__ import annotations import sys -from collections.abc import Generator +from collections.abc import Iterator from typing import Tuple, Union -from astroid import bases, context, helpers, inference_tip +from astroid import bases, context, helpers, nodes from astroid.builder import parse from astroid.const import PY39_PLUS, PY310_PLUS from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault +from astroid.inference_tip import inference_tip from astroid.manager import AstroidManager -from astroid.nodes.node_classes import ( - AnnAssign, - Assign, - AssignName, - Attribute, - Call, - Name, - NodeNG, - Subscript, - Unknown, -) -from astroid.nodes.scoped_nodes import ClassDef, FunctionDef +from astroid.typing import InferenceResult from astroid.util import Uninferable if sys.version_info >= (3, 8): @@ -44,7 +34,9 @@ from typing_extensions import Literal _FieldDefaultReturn = Union[ - None, Tuple[Literal["default"], NodeNG], Tuple[Literal["default_factory"], Call] + None, + Tuple[Literal["default"], nodes.NodeNG], + Tuple[Literal["default_factory"], nodes.Call], ] DATACLASSES_DECORATORS = frozenset(("dataclass",)) @@ -55,9 +47,11 @@ DEFAULT_FACTORY = "_HAS_DEFAULT_FACTORY" # based on typing.py -def is_decorated_with_dataclass(node, decorator_names=DATACLASSES_DECORATORS): +def is_decorated_with_dataclass( + node: nodes.ClassDef, decorator_names: frozenset[str] = DATACLASSES_DECORATORS +) -> bool: """Return True if a decorated node has a `dataclass` decorator applied.""" - if not isinstance(node, ClassDef) or not node.decorators: + if not isinstance(node, nodes.ClassDef) or not node.decorators: return False return any( @@ -66,14 +60,14 @@ def is_decorated_with_dataclass(node, decorator_names=DATACLASSES_DECORATORS): ) -def dataclass_transform(node: ClassDef) -> None: +def dataclass_transform(node: nodes.ClassDef) -> None: """Rewrite a dataclass to be easily understood by pylint""" node.is_dataclass = True for assign_node in _get_dataclass_attributes(node): name = assign_node.target.name - rhs_node = Unknown( + rhs_node = nodes.Unknown( lineno=assign_node.lineno, col_offset=assign_node.col_offset, parent=assign_node, @@ -87,7 +81,7 @@ def dataclass_transform(node: ClassDef) -> None: kw_only_decorated = False if PY310_PLUS and node.decorators.nodes: for decorator in node.decorators.nodes: - if not isinstance(decorator, Call): + if not isinstance(decorator, nodes.Call): kw_only_decorated = False break for keyword in decorator.keywords: @@ -116,15 +110,17 @@ def dataclass_transform(node: ClassDef) -> None: root.locals[DEFAULT_FACTORY] = [new_assign.targets[0]] -def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: +def _get_dataclass_attributes( + node: nodes.ClassDef, init: bool = False +) -> Iterator[nodes.AnnAssign]: """Yield the AnnAssign nodes of dataclass attributes for the node. If init is True, also include InitVars, but exclude attributes from calls to field where init=False. """ for assign_node in node.body: - if not isinstance(assign_node, AnnAssign) or not isinstance( - assign_node.target, AssignName + if not isinstance(assign_node, nodes.AnnAssign) or not isinstance( + assign_node.target, nodes.AssignName ): continue @@ -137,11 +133,10 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: if init: value = assign_node.value if ( - isinstance(value, Call) + isinstance(value, nodes.Call) and _looks_like_dataclass_field_call(value, check_scope=False) and any( - keyword.arg == "init" - and not keyword.value.bool_value() # type: ignore[union-attr] # value is never None + keyword.arg == "init" and not keyword.value.bool_value() for keyword in value.keywords ) ): @@ -152,7 +147,7 @@ def _get_dataclass_attributes(node: ClassDef, init: bool = False) -> Generator: yield assign_node -def _check_generate_dataclass_init(node: ClassDef) -> bool: +def _check_generate_dataclass_init(node: nodes.ClassDef) -> bool: """Return True if we should generate an __init__ method for node. This is True when: @@ -165,7 +160,7 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: found = None for decorator_attribute in node.decorators.nodes: - if not isinstance(decorator_attribute, Call): + if not isinstance(decorator_attribute, nodes.Call): continue if _looks_like_dataclass_decorator(decorator_attribute): @@ -183,7 +178,7 @@ def _check_generate_dataclass_init(node: ClassDef) -> bool: def _generate_dataclass_init( - node: ClassDef, assigns: list[AnnAssign], kw_only_decorated: bool + node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool ) -> str: """Return an init method for a dataclass given the targets.""" params: list[str] = [] @@ -196,7 +191,7 @@ def _generate_dataclass_init( if _is_init_var(annotation): # type: ignore[arg-type] # annotation is never None init_var = True - if isinstance(annotation, Subscript): + if isinstance(annotation, nodes.Subscript): annotation = annotation.slice else: # Cannot determine type annotation for parameter from InitVar @@ -206,10 +201,13 @@ def _generate_dataclass_init( init_var = False assignment_str = f"self.{name} = {name}" - param_str = f"{name}: {annotation.as_string()}" + if annotation is not None: + param_str = f"{name}: {annotation.as_string()}" + else: + param_str = name if value: - if isinstance(value, Call) and _looks_like_dataclass_field_call( + if isinstance(value, nodes.Call) and _looks_like_dataclass_field_call( value, check_scope=False ): result = _get_field_default(value) @@ -232,9 +230,9 @@ def _generate_dataclass_init( try: base = next(next(iter(node.bases)).infer()) - if not isinstance(base, ClassDef): + if not isinstance(base, nodes.ClassDef): raise InferenceError - base_init: FunctionDef | None = base.locals["__init__"][0] + base_init: nodes.FunctionDef | None = base.locals["__init__"][0] except (StopIteration, InferenceError, KeyError): base_init = None @@ -273,8 +271,8 @@ def _generate_dataclass_init( def infer_dataclass_attribute( - node: Unknown, ctx: context.InferenceContext | None = None -) -> Generator: + node: nodes.Unknown, ctx: context.InferenceContext | None = None +) -> Iterator[InferenceResult]: """Inference tip for an Unknown node that was dynamically generated to represent a dataclass attribute. @@ -282,7 +280,7 @@ def infer_dataclass_attribute( Then, an Instance of the annotated class is yielded. """ assign = node.parent - if not isinstance(assign, AnnAssign): + if not isinstance(assign, nodes.AnnAssign): yield Uninferable return @@ -296,10 +294,10 @@ def infer_dataclass_attribute( def infer_dataclass_field_call( - node: Call, ctx: context.InferenceContext | None = None -) -> Generator: + node: nodes.Call, ctx: context.InferenceContext | None = None +) -> Iterator[InferenceResult]: """Inference tip for dataclass field calls.""" - if not isinstance(node.parent, (AnnAssign, Assign)): + if not isinstance(node.parent, (nodes.AnnAssign, nodes.Assign)): raise UseInferenceDefault result = _get_field_default(node) if not result: @@ -315,14 +313,14 @@ def infer_dataclass_field_call( def _looks_like_dataclass_decorator( - node: NodeNG, decorator_names: frozenset[str] = DATACLASSES_DECORATORS + node: nodes.NodeNG, decorator_names: frozenset[str] = DATACLASSES_DECORATORS ) -> bool: """Return True if node looks like a dataclass decorator. Uses inference to lookup the value of the node, and if that fails, matches against specific names. """ - if isinstance(node, Call): # decorator with arguments + if isinstance(node, nodes.Call): # decorator with arguments node = node.func try: inferred = next(node.infer()) @@ -330,21 +328,21 @@ def _looks_like_dataclass_decorator( inferred = Uninferable if inferred is Uninferable: - if isinstance(node, Name): + if isinstance(node, nodes.Name): return node.name in decorator_names - if isinstance(node, Attribute): + if isinstance(node, nodes.Attribute): return node.attrname in decorator_names return False return ( - isinstance(inferred, FunctionDef) + isinstance(inferred, nodes.FunctionDef) and inferred.name in decorator_names and inferred.root().name in DATACLASS_MODULES ) -def _looks_like_dataclass_attribute(node: Unknown) -> bool: +def _looks_like_dataclass_attribute(node: nodes.Unknown) -> bool: """Return True if node was dynamically generated as the child of an AnnAssign statement. """ @@ -354,13 +352,15 @@ def _looks_like_dataclass_attribute(node: Unknown) -> bool: scope = parent.scope() return ( - isinstance(parent, AnnAssign) - and isinstance(scope, ClassDef) + isinstance(parent, nodes.AnnAssign) + and isinstance(scope, nodes.ClassDef) and is_decorated_with_dataclass(scope) ) -def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bool: +def _looks_like_dataclass_field_call( + node: nodes.Call, check_scope: bool = True +) -> bool: """Return True if node is calling dataclasses field or Field from an AnnAssign statement directly in the body of a ClassDef. @@ -370,9 +370,9 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo stmt = node.statement(future=True) scope = stmt.scope() if not ( - isinstance(stmt, AnnAssign) + isinstance(stmt, nodes.AnnAssign) and stmt.value is not None - and isinstance(scope, ClassDef) + and isinstance(scope, nodes.ClassDef) and is_decorated_with_dataclass(scope) ): return False @@ -382,13 +382,13 @@ def _looks_like_dataclass_field_call(node: Call, check_scope: bool = True) -> bo except (InferenceError, StopIteration): return False - if not isinstance(inferred, FunctionDef): + if not isinstance(inferred, nodes.FunctionDef): return False return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES -def _get_field_default(field_call: Call) -> _FieldDefaultReturn: +def _get_field_default(field_call: nodes.Call) -> _FieldDefaultReturn: """Return a the default value of a field call, and the corresponding keyword argument name. field(default=...) results in the ... node @@ -408,7 +408,7 @@ def _get_field_default(field_call: Call) -> _FieldDefaultReturn: return "default", default if default is None and default_factory is not None: - new_call = Call( + new_call = nodes.Call( lineno=field_call.lineno, col_offset=field_call.col_offset, parent=field_call.parent, @@ -419,7 +419,7 @@ def _get_field_default(field_call: Call) -> _FieldDefaultReturn: return None -def _is_class_var(node: NodeNG) -> bool: +def _is_class_var(node: nodes.NodeNG) -> bool: """Return True if node is a ClassVar, with or without subscripting.""" if PY39_PLUS: try: @@ -431,15 +431,15 @@ def _is_class_var(node: NodeNG) -> bool: # Before Python 3.9, inference returns typing._SpecialForm instead of ClassVar. # Our backup is to inspect the node's structure. - return isinstance(node, Subscript) and ( - isinstance(node.value, Name) + return isinstance(node, nodes.Subscript) and ( + isinstance(node.value, nodes.Name) and node.value.name == "ClassVar" - or isinstance(node.value, Attribute) + or isinstance(node.value, nodes.Attribute) and node.value.attrname == "ClassVar" ) -def _is_keyword_only_sentinel(node: NodeNG) -> bool: +def _is_keyword_only_sentinel(node: nodes.NodeNG) -> bool: """Return True if node is the KW_ONLY sentinel.""" if not PY310_PLUS: return False @@ -450,7 +450,7 @@ def _is_keyword_only_sentinel(node: NodeNG) -> bool: ) -def _is_init_var(node: NodeNG) -> bool: +def _is_init_var(node: nodes.NodeNG) -> bool: """Return True if node is an InitVar, with or without subscripting.""" try: inferred = next(node.infer()) @@ -473,8 +473,8 @@ def _is_init_var(node: NodeNG) -> bool: def _infer_instance_from_annotation( - node: NodeNG, ctx: context.InferenceContext | None = None -) -> Generator: + node: nodes.NodeNG, ctx: context.InferenceContext | None = None +) -> Iterator[type[Uninferable] | bases.Instance]: """Infer an instance corresponding to the type annotation represented by node. Currently has limited support for the typing module. @@ -484,7 +484,7 @@ def _infer_instance_from_annotation( klass = next(node.infer(context=ctx)) except (InferenceError, StopIteration): yield Uninferable - if not isinstance(klass, ClassDef): + if not isinstance(klass, nodes.ClassDef): yield Uninferable elif klass.root().name in { "typing", @@ -500,17 +500,17 @@ def _infer_instance_from_annotation( AstroidManager().register_transform( - ClassDef, dataclass_transform, is_decorated_with_dataclass + nodes.ClassDef, dataclass_transform, is_decorated_with_dataclass ) AstroidManager().register_transform( - Call, + nodes.Call, inference_tip(infer_dataclass_field_call, raise_on_overwrite=True), _looks_like_dataclass_field_call, ) AstroidManager().register_transform( - Unknown, + nodes.Unknown, inference_tip(infer_dataclass_attribute, raise_on_overwrite=True), _looks_like_dataclass_attribute, ) diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 3e71a409d1..2f59d4c331 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -849,6 +849,30 @@ class MyDataclass(Unknown): assert next(node.infer()) +def test_dataclass_with_unknown_typing() -> None: + """Regression test for dataclasses with unknown base classes. + + Reported in https://github.com/PyCQA/pylint/issues/7422 + """ + node = astroid.extract_node( + """ + from dataclasses import dataclass, InitVar + + + @dataclass + class TestClass: + '''Test Class''' + + config: InitVar = None + + TestClass.__init__ #@ + """ + ) + + init_def: bases.UnboundMethod = next(node.infer()) + assert [a.name for a in init_def.args.args] == ["self", "config"] + + def test_dataclass_with_default_factory() -> None: """Regression test for dataclasses with default values. From 65bca39bbf254bc760ac9d388e5a09333eaf5c87 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Tue, 6 Sep 2022 17:51:44 +0200 Subject: [PATCH 28/93] Bump astroid to 2.12.8, update changelog --- ChangeLog | 9 ++++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 291be7498e..70bb91cacd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,10 +8,17 @@ Release date: TBA -What's New in astroid 2.12.8? +What's New in astroid 2.12.9? ============================= Release date: TBA + + + +What's New in astroid 2.12.8? +============================= +Release date: 2022-09-06 + * Fixed a crash in the ``dataclass`` brain for ``InitVars`` without subscript typing. Closes PyCQA/pylint#7422 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index db3d707674..f5219d2035 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.7" +__version__ = "2.12.8" version = __version__ diff --git a/tbump.toml b/tbump.toml index a876b7bc0a..1c96e21c4a 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.7" +current = "2.12.8" regex = ''' ^(?P0|[1-9]\d*) \. From 7c36d112136180420fb78825f3244e02da80174e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 7 Sep 2022 09:43:22 +0200 Subject: [PATCH 29/93] Fix a crash on ``namedtuples`` that use ``typename`` (#1773) Co-authored-by: Pierre Sassoulas --- ChangeLog | 3 +++ astroid/brain/brain_namedtuple_enum.py | 12 ++++++++++++ tests/unittest_brain.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/ChangeLog b/ChangeLog index 7140d3e76b..f20ac1ac06 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,9 @@ What's New in astroid 2.12.9? ============================= Release date: TBA +* Fixed a crash on ``namedtuples`` that use ``typename`` to specify their name. + + Closes PyCQA/pylint#7429 What's New in astroid 2.12.8? diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index acc20c516d..aa2ff3cec7 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -538,10 +538,22 @@ def _get_namedtuple_fields(node: nodes.Call) -> str: extract a node from them later on. """ names = [] + container = None try: container = next(node.args[1].infer()) except (InferenceError, StopIteration) as exc: raise UseInferenceDefault from exc + # We pass on IndexError as we'll try to infer 'field_names' from the keywords + except IndexError: + pass + if not container: + for keyword_node in node.keywords: + if keyword_node.arg == "field_names": + try: + container = next(keyword_node.value.infer()) + except (InferenceError, StopIteration) as exc: + raise UseInferenceDefault from exc + break if not isinstance(container, nodes.BaseContainer): raise UseInferenceDefault for elt in container.elts: diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index bcd92c02d5..07d17c4ceb 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -434,6 +434,23 @@ def __str__(self): inferred = next(node.infer()) self.assertIs(util.Uninferable, inferred) + def test_name_as_typename(self) -> None: + """Reported in https://github.com/PyCQA/pylint/issues/7429 as a crash.""" + good_node, good_node_two, bad_node = builder.extract_node( + """ + import collections + collections.namedtuple(typename="MyTuple", field_names=["birth_date", "city"]) #@ + collections.namedtuple("MyTuple", field_names=["birth_date", "city"]) #@ + collections.namedtuple(["birth_date", "city"], typename="MyTuple") #@ + """ + ) + good_inferred = next(good_node.infer()) + assert isinstance(good_inferred, nodes.ClassDef) + good_node_two_inferred = next(good_node_two.infer()) + assert isinstance(good_node_two_inferred, nodes.ClassDef) + bad_node_inferred = next(bad_node.infer()) + assert bad_node_inferred == util.Uninferable + class DefaultDictTest(unittest.TestCase): def test_1(self) -> None: From b5812cc97bc7d63b72e2a99b7060a253d5ca3ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:30:19 +0200 Subject: [PATCH 30/93] Fixed the ``__init__`` of ``dataclassess`` with multiple inheritance (#1774) --- ChangeLog | 4 + astroid/brain/brain_dataclasses.py | 62 ++++++++----- tests/unittest_brain_dataclasses.py | 132 ++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 20 deletions(-) diff --git a/ChangeLog b/ChangeLog index f20ac1ac06..b4245d3d49 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,10 @@ What's New in astroid 2.12.9? ============================= Release date: TBA +* Fixed creation of the ``__init__`` of ``dataclassess`` with multiple inheritance. + + Closes PyCQA/pylint#7427 + * Fixed a crash on ``namedtuples`` that use ``typename`` to specify their name. Closes PyCQA/pylint#7429 diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 629024902c..264957e00c 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -177,6 +177,45 @@ def _check_generate_dataclass_init(node: nodes.ClassDef) -> bool: ) +def _find_arguments_from_base_classes( + node: nodes.ClassDef, skippable_names: set[str] +) -> tuple[str, str]: + """Iterate through all bases and add them to the list of arguments to add to the init.""" + prev_pos_only = "" + prev_kw_only = "" + for base in node.mro(): + if not base.is_dataclass: + continue + try: + base_init: nodes.FunctionDef = base.locals["__init__"][0] + except KeyError: + continue + + # Skip the self argument and check for duplicate arguments + arguments = base_init.args.format_args(skippable_names=skippable_names) + try: + new_prev_pos_only, new_prev_kw_only = arguments.split("*, ") + except ValueError: + new_prev_pos_only, new_prev_kw_only = arguments, "" + + if new_prev_pos_only: + # The split on '*, ' can crete a pos_only string that consists only of a comma + if new_prev_pos_only == ", ": + new_prev_pos_only = "" + elif not new_prev_pos_only.endswith(", "): + new_prev_pos_only += ", " + + # Dataclasses put last seen arguments at the front of the init + prev_pos_only = new_prev_pos_only + prev_pos_only + prev_kw_only = new_prev_kw_only + prev_kw_only + + # Add arguments to skippable arguments + skippable_names.update(arg.name for arg in base_init.args.args) + skippable_names.update(arg.name for arg in base_init.args.kwonlyargs) + + return prev_pos_only, prev_kw_only + + def _generate_dataclass_init( node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool ) -> str: @@ -228,26 +267,9 @@ def _generate_dataclass_init( if not init_var: assignments.append(assignment_str) - try: - base = next(next(iter(node.bases)).infer()) - if not isinstance(base, nodes.ClassDef): - raise InferenceError - base_init: nodes.FunctionDef | None = base.locals["__init__"][0] - except (StopIteration, InferenceError, KeyError): - base_init = None - - prev_pos_only = "" - prev_kw_only = "" - if base_init and base.is_dataclass: - # Skip the self argument and check for duplicate arguments - arguments = base_init.args.format_args(skippable_names=assign_names)[6:] - try: - prev_pos_only, prev_kw_only = arguments.split("*, ") - except ValueError: - prev_pos_only, prev_kw_only = arguments, "" - - if prev_pos_only and not prev_pos_only.endswith(", "): - prev_pos_only += ", " + prev_pos_only, prev_kw_only = _find_arguments_from_base_classes( + node, set(assign_names + ["self"]) + ) # Construct the new init method paramter string params_string = "self, " diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 2f59d4c331..7d69b35914 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -912,3 +912,135 @@ class GoodExampleClass(GoodExampleParentClass): good_init: bases.UnboundMethod = next(good_node.infer()) assert bad_init.args.defaults assert [a.name for a in good_init.args.args] == ["self", "xyz"] + + +def test_dataclass_with_multiple_inheritance() -> None: + """Regression test for dataclasses with multiple inheritance. + + Reported in https://github.com/PyCQA/pylint/issues/7427 + """ + first, second, overwritten, overwriting, mixed = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class BaseParent: + _abc: int = 1 + + @dataclass + class AnotherParent: + ef: int = 2 + + @dataclass + class FirstChild(BaseParent, AnotherParent): + ghi: int = 3 + + @dataclass + class ConvolutedParent(AnotherParent): + '''Convoluted Parent''' + + @dataclass + class SecondChild(BaseParent, ConvolutedParent): + jkl: int = 4 + + @dataclass + class OverwritingParent: + ef: str = "2" + + @dataclass + class OverwrittenChild(OverwritingParent, AnotherParent): + '''Overwritten Child''' + + @dataclass + class OverwritingChild(BaseParent, AnotherParent): + _abc: float = 1.0 + ef: float = 2.0 + + class NotADataclassParent: + ef: int = 2 + + @dataclass + class ChildWithMixedParents(BaseParent, NotADataclassParent): + ghi: int = 3 + + FirstChild.__init__ #@ + SecondChild.__init__ #@ + OverwrittenChild.__init__ #@ + OverwritingChild.__init__ #@ + ChildWithMixedParents.__init__ #@ + """ + ) + + first_init: bases.UnboundMethod = next(first.infer()) + assert [a.name for a in first_init.args.args] == ["self", "ef", "_abc", "ghi"] + assert [a.value for a in first_init.args.defaults] == [2, 1, 3] + + second_init: bases.UnboundMethod = next(second.infer()) + assert [a.name for a in second_init.args.args] == ["self", "ef", "_abc", "jkl"] + assert [a.value for a in second_init.args.defaults] == [2, 1, 4] + + overwritten_init: bases.UnboundMethod = next(overwritten.infer()) + assert [a.name for a in overwritten_init.args.args] == ["self", "ef"] + assert [a.value for a in overwritten_init.args.defaults] == ["2"] + + overwriting_init: bases.UnboundMethod = next(overwriting.infer()) + assert [a.name for a in overwriting_init.args.args] == ["self", "_abc", "ef"] + assert [a.value for a in overwriting_init.args.defaults] == [1.0, 2.0] + + mixed_init: bases.UnboundMethod = next(mixed.infer()) + assert [a.name for a in mixed_init.args.args] == ["self", "_abc", "ghi"] + assert [a.value for a in mixed_init.args.defaults] == [1, 3] + + +def test_dataclass_inits_of_non_dataclasses() -> None: + """Regression test for __init__ mangling for non dataclasses. + + Regression test against changes tested in test_dataclass_with_multiple_inheritance + """ + first, second, third = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class DataclassParent: + _abc: int = 1 + + + class NotADataclassParent: + ef: int = 2 + + + class FirstChild(DataclassParent, NotADataclassParent): + ghi: int = 3 + + + class SecondChild(DataclassParent, NotADataclassParent): + ghi: int = 3 + + def __init__(self, ef: int = 3): + self.ef = ef + + + class ThirdChild(NotADataclassParent, DataclassParent): + ghi: int = 3 + + def __init__(self, ef: int = 3): + self.ef = ef + + FirstChild.__init__ #@ + SecondChild.__init__ #@ + ThirdChild.__init__ #@ + """ + ) + + first_init: bases.UnboundMethod = next(first.infer()) + assert [a.name for a in first_init.args.args] == ["self", "_abc"] + assert [a.value for a in first_init.args.defaults] == [1] + + second_init: bases.UnboundMethod = next(second.infer()) + assert [a.name for a in second_init.args.args] == ["self", "ef"] + assert [a.value for a in second_init.args.defaults] == [3] + + third_init: bases.UnboundMethod = next(third.infer()) + assert [a.name for a in third_init.args.args] == ["self", "ef"] + assert [a.value for a in third_init.args.defaults] == [3] From d15466685643b47f3b8b42ae5c0ba14a429a5293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 7 Sep 2022 09:43:22 +0200 Subject: [PATCH 31/93] Fix a crash on ``namedtuples`` that use ``typename`` (#1773) Co-authored-by: Pierre Sassoulas --- ChangeLog | 3 +++ astroid/brain/brain_namedtuple_enum.py | 12 ++++++++++++ tests/unittest_brain.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/ChangeLog b/ChangeLog index 70bb91cacd..771e49491b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,9 @@ What's New in astroid 2.12.9? ============================= Release date: TBA +* Fixed a crash on ``namedtuples`` that use ``typename`` to specify their name. + + Closes PyCQA/pylint#7429 diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index acc20c516d..aa2ff3cec7 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -538,10 +538,22 @@ def _get_namedtuple_fields(node: nodes.Call) -> str: extract a node from them later on. """ names = [] + container = None try: container = next(node.args[1].infer()) except (InferenceError, StopIteration) as exc: raise UseInferenceDefault from exc + # We pass on IndexError as we'll try to infer 'field_names' from the keywords + except IndexError: + pass + if not container: + for keyword_node in node.keywords: + if keyword_node.arg == "field_names": + try: + container = next(keyword_node.value.infer()) + except (InferenceError, StopIteration) as exc: + raise UseInferenceDefault from exc + break if not isinstance(container, nodes.BaseContainer): raise UseInferenceDefault for elt in container.elts: diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index bcd92c02d5..07d17c4ceb 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -434,6 +434,23 @@ def __str__(self): inferred = next(node.infer()) self.assertIs(util.Uninferable, inferred) + def test_name_as_typename(self) -> None: + """Reported in https://github.com/PyCQA/pylint/issues/7429 as a crash.""" + good_node, good_node_two, bad_node = builder.extract_node( + """ + import collections + collections.namedtuple(typename="MyTuple", field_names=["birth_date", "city"]) #@ + collections.namedtuple("MyTuple", field_names=["birth_date", "city"]) #@ + collections.namedtuple(["birth_date", "city"], typename="MyTuple") #@ + """ + ) + good_inferred = next(good_node.infer()) + assert isinstance(good_inferred, nodes.ClassDef) + good_node_two_inferred = next(good_node_two.infer()) + assert isinstance(good_node_two_inferred, nodes.ClassDef) + bad_node_inferred = next(bad_node.infer()) + assert bad_node_inferred == util.Uninferable + class DefaultDictTest(unittest.TestCase): def test_1(self) -> None: From 449a95b08e39dd2d6f3a6c3cbf4ace3055340b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 7 Sep 2022 12:30:19 +0200 Subject: [PATCH 32/93] Fixed the ``__init__`` of ``dataclassess`` with multiple inheritance (#1774) --- ChangeLog | 4 + astroid/brain/brain_dataclasses.py | 62 ++++++++----- tests/unittest_brain_dataclasses.py | 132 ++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 20 deletions(-) diff --git a/ChangeLog b/ChangeLog index 771e49491b..b038fe7db0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,10 @@ What's New in astroid 2.12.9? ============================= Release date: TBA +* Fixed creation of the ``__init__`` of ``dataclassess`` with multiple inheritance. + + Closes PyCQA/pylint#7427 + * Fixed a crash on ``namedtuples`` that use ``typename`` to specify their name. Closes PyCQA/pylint#7429 diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 629024902c..264957e00c 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -177,6 +177,45 @@ def _check_generate_dataclass_init(node: nodes.ClassDef) -> bool: ) +def _find_arguments_from_base_classes( + node: nodes.ClassDef, skippable_names: set[str] +) -> tuple[str, str]: + """Iterate through all bases and add them to the list of arguments to add to the init.""" + prev_pos_only = "" + prev_kw_only = "" + for base in node.mro(): + if not base.is_dataclass: + continue + try: + base_init: nodes.FunctionDef = base.locals["__init__"][0] + except KeyError: + continue + + # Skip the self argument and check for duplicate arguments + arguments = base_init.args.format_args(skippable_names=skippable_names) + try: + new_prev_pos_only, new_prev_kw_only = arguments.split("*, ") + except ValueError: + new_prev_pos_only, new_prev_kw_only = arguments, "" + + if new_prev_pos_only: + # The split on '*, ' can crete a pos_only string that consists only of a comma + if new_prev_pos_only == ", ": + new_prev_pos_only = "" + elif not new_prev_pos_only.endswith(", "): + new_prev_pos_only += ", " + + # Dataclasses put last seen arguments at the front of the init + prev_pos_only = new_prev_pos_only + prev_pos_only + prev_kw_only = new_prev_kw_only + prev_kw_only + + # Add arguments to skippable arguments + skippable_names.update(arg.name for arg in base_init.args.args) + skippable_names.update(arg.name for arg in base_init.args.kwonlyargs) + + return prev_pos_only, prev_kw_only + + def _generate_dataclass_init( node: nodes.ClassDef, assigns: list[nodes.AnnAssign], kw_only_decorated: bool ) -> str: @@ -228,26 +267,9 @@ def _generate_dataclass_init( if not init_var: assignments.append(assignment_str) - try: - base = next(next(iter(node.bases)).infer()) - if not isinstance(base, nodes.ClassDef): - raise InferenceError - base_init: nodes.FunctionDef | None = base.locals["__init__"][0] - except (StopIteration, InferenceError, KeyError): - base_init = None - - prev_pos_only = "" - prev_kw_only = "" - if base_init and base.is_dataclass: - # Skip the self argument and check for duplicate arguments - arguments = base_init.args.format_args(skippable_names=assign_names)[6:] - try: - prev_pos_only, prev_kw_only = arguments.split("*, ") - except ValueError: - prev_pos_only, prev_kw_only = arguments, "" - - if prev_pos_only and not prev_pos_only.endswith(", "): - prev_pos_only += ", " + prev_pos_only, prev_kw_only = _find_arguments_from_base_classes( + node, set(assign_names + ["self"]) + ) # Construct the new init method paramter string params_string = "self, " diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 2f59d4c331..7d69b35914 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -912,3 +912,135 @@ class GoodExampleClass(GoodExampleParentClass): good_init: bases.UnboundMethod = next(good_node.infer()) assert bad_init.args.defaults assert [a.name for a in good_init.args.args] == ["self", "xyz"] + + +def test_dataclass_with_multiple_inheritance() -> None: + """Regression test for dataclasses with multiple inheritance. + + Reported in https://github.com/PyCQA/pylint/issues/7427 + """ + first, second, overwritten, overwriting, mixed = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class BaseParent: + _abc: int = 1 + + @dataclass + class AnotherParent: + ef: int = 2 + + @dataclass + class FirstChild(BaseParent, AnotherParent): + ghi: int = 3 + + @dataclass + class ConvolutedParent(AnotherParent): + '''Convoluted Parent''' + + @dataclass + class SecondChild(BaseParent, ConvolutedParent): + jkl: int = 4 + + @dataclass + class OverwritingParent: + ef: str = "2" + + @dataclass + class OverwrittenChild(OverwritingParent, AnotherParent): + '''Overwritten Child''' + + @dataclass + class OverwritingChild(BaseParent, AnotherParent): + _abc: float = 1.0 + ef: float = 2.0 + + class NotADataclassParent: + ef: int = 2 + + @dataclass + class ChildWithMixedParents(BaseParent, NotADataclassParent): + ghi: int = 3 + + FirstChild.__init__ #@ + SecondChild.__init__ #@ + OverwrittenChild.__init__ #@ + OverwritingChild.__init__ #@ + ChildWithMixedParents.__init__ #@ + """ + ) + + first_init: bases.UnboundMethod = next(first.infer()) + assert [a.name for a in first_init.args.args] == ["self", "ef", "_abc", "ghi"] + assert [a.value for a in first_init.args.defaults] == [2, 1, 3] + + second_init: bases.UnboundMethod = next(second.infer()) + assert [a.name for a in second_init.args.args] == ["self", "ef", "_abc", "jkl"] + assert [a.value for a in second_init.args.defaults] == [2, 1, 4] + + overwritten_init: bases.UnboundMethod = next(overwritten.infer()) + assert [a.name for a in overwritten_init.args.args] == ["self", "ef"] + assert [a.value for a in overwritten_init.args.defaults] == ["2"] + + overwriting_init: bases.UnboundMethod = next(overwriting.infer()) + assert [a.name for a in overwriting_init.args.args] == ["self", "_abc", "ef"] + assert [a.value for a in overwriting_init.args.defaults] == [1.0, 2.0] + + mixed_init: bases.UnboundMethod = next(mixed.infer()) + assert [a.name for a in mixed_init.args.args] == ["self", "_abc", "ghi"] + assert [a.value for a in mixed_init.args.defaults] == [1, 3] + + +def test_dataclass_inits_of_non_dataclasses() -> None: + """Regression test for __init__ mangling for non dataclasses. + + Regression test against changes tested in test_dataclass_with_multiple_inheritance + """ + first, second, third = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class DataclassParent: + _abc: int = 1 + + + class NotADataclassParent: + ef: int = 2 + + + class FirstChild(DataclassParent, NotADataclassParent): + ghi: int = 3 + + + class SecondChild(DataclassParent, NotADataclassParent): + ghi: int = 3 + + def __init__(self, ef: int = 3): + self.ef = ef + + + class ThirdChild(NotADataclassParent, DataclassParent): + ghi: int = 3 + + def __init__(self, ef: int = 3): + self.ef = ef + + FirstChild.__init__ #@ + SecondChild.__init__ #@ + ThirdChild.__init__ #@ + """ + ) + + first_init: bases.UnboundMethod = next(first.infer()) + assert [a.name for a in first_init.args.args] == ["self", "_abc"] + assert [a.value for a in first_init.args.defaults] == [1] + + second_init: bases.UnboundMethod = next(second.infer()) + assert [a.name for a in second_init.args.args] == ["self", "ef"] + assert [a.value for a in second_init.args.defaults] == [3] + + third_init: bases.UnboundMethod = next(third.infer()) + assert [a.name for a in third_init.args.args] == ["self", "ef"] + assert [a.value for a in third_init.args.defaults] == [3] From 7352e947bdf9b9c5ea51e601bbed7a063e98316d Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 7 Sep 2022 12:32:29 +0200 Subject: [PATCH 33/93] Bump astroid to 2.12.9, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index b038fe7db0..e38a3b6511 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,9 +8,15 @@ Release date: TBA +What's New in astroid 2.12.10? +============================== +Release date: TBA + + + What's New in astroid 2.12.9? ============================= -Release date: TBA +Release date: 2022-09-07 * Fixed creation of the ``__init__`` of ``dataclassess`` with multiple inheritance. diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index f5219d2035..9eaaeca92b 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.8" +__version__ = "2.12.9" version = __version__ diff --git a/tbump.toml b/tbump.toml index 1c96e21c4a..184902522c 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.8" +current = "2.12.9" regex = ''' ^(?P0|[1-9]\d*) \. From b4fdcf660e3718d1da2f9ce42345fd1bbbe0108d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Sep 2022 20:31:15 +0200 Subject: [PATCH 34/93] Bump pylint from 2.15.0 to 2.15.2 (#1778) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.0 to 2.15.2. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.0...v2.15.2) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 2d1baeaa93..727aa220d1 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ black==22.8.0 -pylint==2.15.0 +pylint==2.15.2 isort==5.10.1 flake8==5.0.4 flake8-typing-imports==1.13.0 From 4bf87a3751185028bcc3629ae98dd3c2a9218664 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:43:19 +0000 Subject: [PATCH 35/93] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - https://github.com/myint/autoflake → https://github.com/PyCQA/autoflake --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 08dd220862..8fc29143e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: exclude: .github/|tests/testdata - id: end-of-file-fixer exclude: tests/testdata - - repo: https://github.com/myint/autoflake + - repo: https://github.com/PyCQA/autoflake rev: v1.5.3 hooks: - id: autoflake From c3912574cc7c48792f7f50d16dfc7551ef4af8dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:51:31 +0200 Subject: [PATCH 36/93] Give a created Arguments node a parent --- astroid/interpreter/objectmodel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index 0c613fb26b..879ee7f256 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -795,7 +795,9 @@ class PropertyModel(ObjectModel): """Model for a builtin property""" def _init_function(self, name): - args = nodes.Arguments() + function = nodes.FunctionDef(name=name, parent=self._instance) + + args = nodes.Arguments(parent=function) args.postinit( args=[], defaults=[], @@ -807,8 +809,6 @@ def _init_function(self, name): kwonlyargs_annotations=[], ) - function = nodes.FunctionDef(name=name, parent=self._instance) - function.postinit(args=args, body=[]) return function From fbcd3721c942843275795b4b075c3fef0f69338a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 17 Sep 2022 15:05:04 +0200 Subject: [PATCH 37/93] Create a ``CacheManager`` class to manage storing and clearing caches (#1782) --- ChangeLog | 2 ++ astroid/_cache.py | 26 ++++++++++++++++++++++++++ astroid/decorators.py | 3 ++- astroid/manager.py | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 astroid/_cache.py diff --git a/ChangeLog b/ChangeLog index 142a9a7e0b..8c6803eb44 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,7 +17,9 @@ What's New in astroid 2.12.10? ============================== Release date: TBA +* ``decorators.cached`` now gets its cache cleared by calling ``AstroidManager.clear_cache``. + Refs #1780 What's New in astroid 2.12.9? ============================= diff --git a/astroid/_cache.py b/astroid/_cache.py new file mode 100644 index 0000000000..fc4ddc205b --- /dev/null +++ b/astroid/_cache.py @@ -0,0 +1,26 @@ +# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html +# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from typing import Any + + +class CacheManager: + """Manager of caches, to be used as a singleton.""" + + def __init__(self) -> None: + self.dict_caches: list[dict[Any, Any]] = [] + + def clear_all_caches(self) -> None: + """Clear all caches.""" + for dict_cache in self.dict_caches: + dict_cache.clear() + + def add_dict_cache(self, cache: dict[Any, Any]) -> None: + """Add a dictionary cache to the manager.""" + self.dict_caches.append(cache) + + +CACHE_MANAGER = CacheManager() diff --git a/astroid/decorators.py b/astroid/decorators.py index c4f44dcd27..e9cc3292c2 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -15,7 +15,7 @@ import wrapt -from astroid import util +from astroid import _cache, util from astroid.context import InferenceContext from astroid.exceptions import InferenceError @@ -34,6 +34,7 @@ def cached(func, instance, args, kwargs): cache = getattr(instance, "__cache", None) if cache is None: instance.__cache = cache = {} + _cache.CACHE_MANAGER.add_dict_cache(cache) try: return cache[func] except KeyError: diff --git a/astroid/manager.py b/astroid/manager.py index 77d22503cf..f0932d54de 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -16,6 +16,7 @@ from importlib.util import find_spec, module_from_spec from typing import TYPE_CHECKING, ClassVar +from astroid._cache import CACHE_MANAGER from astroid.const import BRAIN_MODULES_DIRECTORY from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import spec, util @@ -391,6 +392,8 @@ def clear_cache(self) -> None: # NB: not a new TransformVisitor() AstroidManager.brain["_transform"].transforms = collections.defaultdict(list) + CACHE_MANAGER.clear_all_caches() + for lru_cache in ( LookupMixIn.lookup, _cache_normalize_path_, From 36d22051b0af46e3954f63f1b09c7a2fb1fcc2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 17 Sep 2022 16:00:59 +0200 Subject: [PATCH 38/93] Add typing to ``metaclass`` methods (#1678) * Add typing to ``metaclass`` methods * Change to ``NodeNG`` --- astroid/nodes/scoped_nodes/scoped_nodes.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 3fec274a9e..b01c23f372 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -2811,7 +2811,9 @@ def implicit_metaclass(self): return builtin_lookup("type")[1][0] return None - def declared_metaclass(self, context=None): + def declared_metaclass( + self, context: InferenceContext | None = None + ) -> NodeNG | None: """Return the explicit declared metaclass for the current class. An explicit declared metaclass is defined @@ -2822,7 +2824,6 @@ def declared_metaclass(self, context=None): :returns: The metaclass of this class, or None if one could not be found. - :rtype: NodeNG or None """ for base in self.bases: try: @@ -2840,14 +2841,16 @@ def declared_metaclass(self, context=None): return next( node for node in self._metaclass.infer(context=context) - if node is not util.Uninferable + if isinstance(node, NodeNG) ) except (InferenceError, StopIteration): return None return None - def _find_metaclass(self, seen=None, context=None): + def _find_metaclass( + self, seen: set[ClassDef] | None = None, context: InferenceContext | None = None + ) -> NodeNG | None: if seen is None: seen = set() seen.add(self) @@ -2861,7 +2864,7 @@ def _find_metaclass(self, seen=None, context=None): break return klass - def metaclass(self, context=None): + def metaclass(self, context: InferenceContext | None = None) -> NodeNG | None: """Get the metaclass of this class. If this class does not define explicitly a metaclass, @@ -2869,7 +2872,6 @@ def metaclass(self, context=None): instead. :returns: The metaclass of this class. - :rtype: NodeNG or None """ return self._find_metaclass(context=context) From 18a303f2b4f11826de92cdf146520a33a0b89b84 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 17 Sep 2022 10:09:26 -0400 Subject: [PATCH 39/93] Stop detecting modules compiled by `cffi` as namespace packages (#1777) Co-authored-by: Pierre Sassoulas --- ChangeLog | 6 ++++++ astroid/interpreter/_import/util.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 8c6803eb44..8c9ad3e0d2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,12 @@ What's New in astroid 2.12.10? ============================== Release date: TBA + +* Fixed a crash when introspecting modules compiled by `cffi`. + + Closes #1776 + Closes PyCQA/pylint#7399 + * ``decorators.cached`` now gets its cache cleared by calling ``AstroidManager.clear_cache``. Refs #1780 diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index 8cdb8ea19d..c9466999ab 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -49,7 +49,13 @@ def is_namespace(modname: str) -> bool: # .pth files will be on sys.modules # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736 - return sys.modules[modname].__spec__ is None and not IS_PYPY + # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter" + # because of cffi's behavior + # See: https://github.com/PyCQA/astroid/issues/1776 + return ( + sys.modules[processed_components[0]].__spec__ is None + and not IS_PYPY + ) except KeyError: return False except AttributeError: From b14457f9b3d67d57bc594ab50e911746b18aae92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 17 Sep 2022 16:04:01 +0200 Subject: [PATCH 40/93] Type ``object_type`` --- astroid/helpers.py | 19 +++++++++++++++---- astroid/nodes/node_classes.py | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/astroid/helpers.py b/astroid/helpers.py index 928aeed6be..21f7ec97bf 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -8,6 +8,8 @@ from __future__ import annotations +from collections.abc import Generator + from astroid import bases, manager, nodes, raw_building, util from astroid.context import CallContext, InferenceContext from astroid.exceptions import ( @@ -18,7 +20,7 @@ _NonDeducibleTypeHierarchy, ) from astroid.nodes import scoped_nodes -from astroid.typing import InferenceResult +from astroid.typing import InferenceResult, SuccessfulInferenceResult def _build_proxy_class(cls_name: str, builtins: nodes.Module) -> nodes.ClassDef: @@ -42,7 +44,9 @@ def _function_type( return _build_proxy_class(cls_name, builtins) -def _object_type(node, context=None): +def _object_type( + node: SuccessfulInferenceResult, context: InferenceContext | None = None +) -> Generator[InferenceResult | None, None, None]: astroid_manager = manager.AstroidManager() builtins = astroid_manager.builtins_module context = context or InferenceContext() @@ -61,11 +65,18 @@ def _object_type(node, context=None): yield _build_proxy_class("module", builtins) elif isinstance(inferred, nodes.Unknown): raise InferenceError - else: + elif inferred is util.Uninferable: + yield inferred + elif isinstance(inferred, (bases.Proxy, nodes.Slice)): yield inferred._proxied + else: # pragma: no cover + # We don't handle other node types returned by infer currently + raise AssertionError() -def object_type(node, context=None): +def object_type( + node: SuccessfulInferenceResult, context: InferenceContext | None = None +) -> InferenceResult | None: """Obtain the type of the given node This is used to implement the ``type`` builtin, which means that it's diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index caa79d093e..a4686688b6 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -3738,7 +3738,7 @@ def _wrap_attribute(self, attr): return attr @cached_property - def _proxied(self): + def _proxied(self) -> nodes.ClassDef: builtins = AstroidManager().builtins_module return builtins.getattr("slice")[0] From 45530ad32d937ac6ce3112b2c7758527d06a67aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 17 Sep 2022 17:08:58 +0200 Subject: [PATCH 41/93] Add partial typing to ``BinOp`` and ``AugAssign`` inference paths --- astroid/helpers.py | 3 +- astroid/inference.py | 73 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/astroid/helpers.py b/astroid/helpers.py index 21f7ec97bf..82b719639b 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -70,8 +70,7 @@ def _object_type( elif isinstance(inferred, (bases.Proxy, nodes.Slice)): yield inferred._proxied else: # pragma: no cover - # We don't handle other node types returned by infer currently - raise AssertionError() + raise AssertionError(f"We don't handle {type(inferred)} currently") def object_type( diff --git a/astroid/inference.py b/astroid/inference.py index 942988b21c..a71005540b 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -11,8 +11,9 @@ import functools import itertools import operator +import typing from collections.abc import Callable, Generator, Iterable, Iterator -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union from astroid import bases, decorators, helpers, nodes, protocols, util from astroid.context import ( @@ -45,17 +46,26 @@ # Prevents circular imports objects = util.lazy_import("objects") - +_T = TypeVar("_T") +_BaseContainerT = TypeVar("_BaseContainerT", bound=nodes.BaseContainer) _FunctionDefT = TypeVar("_FunctionDefT", bound=nodes.FunctionDef) +GetFlowFactory = typing.Callable[ + [ + InferenceResult, + Optional[InferenceResult], + Union[nodes.AugAssign, nodes.BinOp], + InferenceResult, + Optional[InferenceResult], + InferenceContext, + InferenceContext, + ], + Any, +] # .infer method ############################################################### -_T = TypeVar("_T") -_BaseContainerT = TypeVar("_BaseContainerT", bound=nodes.BaseContainer) - - def infer_end( self: _T, context: InferenceContext | None = None, **kwargs: Any ) -> Iterator[_T]: @@ -652,7 +662,14 @@ def _infer_old_style_string_formatting( return (util.Uninferable,) -def _invoke_binop_inference(instance, opnode, op, other, context, method_name): +def _invoke_binop_inference( + instance: InferenceResult, + opnode: nodes.AugAssign | nodes.BinOp, + op: str, + other: InferenceResult, + context: InferenceContext, + method_name: str, +): """Invoke binary operation inference on the given instance.""" methods = dunder_lookup.lookup(instance, method_name) context = bind_context_to_node(context, instance) @@ -675,7 +692,14 @@ def _invoke_binop_inference(instance, opnode, op, other, context, method_name): return instance.infer_binary_op(opnode, op, other, context, inferred) -def _aug_op(instance, opnode, op, other, context, reverse=False): +def _aug_op( + instance: InferenceResult, + opnode: nodes.AugAssign, + op: str, + other: InferenceResult, + context: InferenceContext, + reverse: bool = False, +): """Get an inference callable for an augmented binary operation.""" method_name = protocols.AUGMENTED_OP_METHOD[op] return functools.partial( @@ -689,7 +713,14 @@ def _aug_op(instance, opnode, op, other, context, reverse=False): ) -def _bin_op(instance, opnode, op, other, context, reverse=False): +def _bin_op( + instance: InferenceResult, + opnode: nodes.AugAssign | nodes.BinOp, + op: str, + other: InferenceResult, + context: InferenceContext, + reverse: bool = False, +): """Get an inference callable for a normal binary operation. If *reverse* is True, then the reflected method will be used instead. @@ -731,7 +762,13 @@ def _same_type(type1, type2): def _get_binop_flow( - left, left_type, binary_opnode, right, right_type, context, reverse_context + left: InferenceResult, + left_type: InferenceResult | None, + binary_opnode: nodes.AugAssign | nodes.BinOp, + right: InferenceResult, + right_type: InferenceResult | None, + context: InferenceContext, + reverse_context: InferenceContext, ): """Get the flow for binary operations. @@ -766,7 +803,13 @@ def _get_binop_flow( def _get_aug_flow( - left, left_type, aug_opnode, right, right_type, context, reverse_context + left: InferenceResult, + left_type: InferenceResult | None, + aug_opnode: nodes.AugAssign, + right: InferenceResult, + right_type: InferenceResult | None, + context: InferenceContext, + reverse_context: InferenceContext, ): """Get the flow for augmented binary operations. @@ -810,7 +853,13 @@ def _get_aug_flow( return methods -def _infer_binary_operation(left, right, binary_opnode, context, flow_factory): +def _infer_binary_operation( + left: InferenceResult, + right: InferenceResult, + binary_opnode: nodes.AugAssign | nodes.BinOp, + context: InferenceContext, + flow_factory: GetFlowFactory, +): """Infer a binary operation between a left operand and a right operand This is used by both normal binary operations and augmented binary From df941c123f22e1cd061018230fdb831321b8aedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 17 Sep 2022 15:05:04 +0200 Subject: [PATCH 42/93] Create a ``CacheManager`` class to manage storing and clearing caches (#1782) --- ChangeLog | 2 ++ astroid/_cache.py | 26 ++++++++++++++++++++++++++ astroid/decorators.py | 3 ++- astroid/manager.py | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 astroid/_cache.py diff --git a/ChangeLog b/ChangeLog index e38a3b6511..7c8a7d4435 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,7 +12,9 @@ What's New in astroid 2.12.10? ============================== Release date: TBA +* ``decorators.cached`` now gets its cache cleared by calling ``AstroidManager.clear_cache``. + Refs #1780 What's New in astroid 2.12.9? ============================= diff --git a/astroid/_cache.py b/astroid/_cache.py new file mode 100644 index 0000000000..fc4ddc205b --- /dev/null +++ b/astroid/_cache.py @@ -0,0 +1,26 @@ +# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html +# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE +# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt + +from __future__ import annotations + +from typing import Any + + +class CacheManager: + """Manager of caches, to be used as a singleton.""" + + def __init__(self) -> None: + self.dict_caches: list[dict[Any, Any]] = [] + + def clear_all_caches(self) -> None: + """Clear all caches.""" + for dict_cache in self.dict_caches: + dict_cache.clear() + + def add_dict_cache(self, cache: dict[Any, Any]) -> None: + """Add a dictionary cache to the manager.""" + self.dict_caches.append(cache) + + +CACHE_MANAGER = CacheManager() diff --git a/astroid/decorators.py b/astroid/decorators.py index c4f44dcd27..e9cc3292c2 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -15,7 +15,7 @@ import wrapt -from astroid import util +from astroid import _cache, util from astroid.context import InferenceContext from astroid.exceptions import InferenceError @@ -34,6 +34,7 @@ def cached(func, instance, args, kwargs): cache = getattr(instance, "__cache", None) if cache is None: instance.__cache = cache = {} + _cache.CACHE_MANAGER.add_dict_cache(cache) try: return cache[func] except KeyError: diff --git a/astroid/manager.py b/astroid/manager.py index 25373a6b41..ff472256fa 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -16,6 +16,7 @@ from importlib.util import find_spec, module_from_spec from typing import TYPE_CHECKING, ClassVar +from astroid._cache import CACHE_MANAGER from astroid.const import BRAIN_MODULES_DIRECTORY from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import spec, util @@ -382,6 +383,8 @@ def clear_cache(self) -> None: # NB: not a new TransformVisitor() AstroidManager.brain["_transform"].transforms = collections.defaultdict(list) + CACHE_MANAGER.clear_all_caches() + for lru_cache in ( LookupMixIn.lookup, _cache_normalize_path_, From b3f0ea789ed2472aef377754957bcc1d15e35a92 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 17 Sep 2022 10:09:26 -0400 Subject: [PATCH 43/93] Stop detecting modules compiled by `cffi` as namespace packages (#1777) Co-authored-by: Pierre Sassoulas --- ChangeLog | 6 ++++++ astroid/interpreter/_import/util.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 7c8a7d4435..977d8ead59 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,12 @@ What's New in astroid 2.12.10? ============================== Release date: TBA + +* Fixed a crash when introspecting modules compiled by `cffi`. + + Closes #1776 + Closes PyCQA/pylint#7399 + * ``decorators.cached`` now gets its cache cleared by calling ``AstroidManager.clear_cache``. Refs #1780 diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index 8cdb8ea19d..c9466999ab 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -49,7 +49,13 @@ def is_namespace(modname: str) -> bool: # .pth files will be on sys.modules # __spec__ is set inconsistently on PyPy so we can't really on the heuristic here # See: https://foss.heptapod.net/pypy/pypy/-/issues/3736 - return sys.modules[modname].__spec__ is None and not IS_PYPY + # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter" + # because of cffi's behavior + # See: https://github.com/PyCQA/astroid/issues/1776 + return ( + sys.modules[processed_components[0]].__spec__ is None + and not IS_PYPY + ) except KeyError: return False except AttributeError: From 9c3b002f7825b7f5743a2acbb46010ab73e7e516 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sat, 17 Sep 2022 17:18:56 +0200 Subject: [PATCH 44/93] Bump astroid to 2.12.10, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- tbump.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 977d8ead59..88b4d4fadd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,11 +8,17 @@ Release date: TBA -What's New in astroid 2.12.10? +What's New in astroid 2.12.11? ============================== Release date: TBA + +What's New in astroid 2.12.10? +============================== +Release date: 2022-09-17 + + * Fixed a crash when introspecting modules compiled by `cffi`. Closes #1776 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 9eaaeca92b..550130af98 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.9" +__version__ = "2.12.10" version = __version__ diff --git a/tbump.toml b/tbump.toml index 184902522c..1f99bc259e 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.9" +current = "2.12.10" regex = ''' ^(?P0|[1-9]\d*) \. From e9162adef069e19147717d00075b811550ebd27c Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 18 Sep 2022 08:12:45 +0200 Subject: [PATCH 45/93] Fix a typo in astroid 2.10.0 release notes (#1788) --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 13a8a765c0..616ac1a2fe 100644 --- a/ChangeLog +++ b/ChangeLog @@ -418,7 +418,7 @@ Release date: 2022-02-27 Closes PyCQA/pylint#5679 -* Inlcude names of keyword-only arguments in ``astroid.scoped_nodes.Lambda.argnames``. +* Include names of keyword-only arguments in ``astroid.scoped_nodes.Lambda.argnames``. Closes PyCQA/pylint#5771 From bed16016ae465cd6d19aa01ad5772e9d5045bbdc Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 18 Sep 2022 08:28:17 +0200 Subject: [PATCH 46/93] Activate's flake8 max-complexity and max-line-length with a high enough value (#1790) --- setup.cfg | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index f5dca3637a..6223228efa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,15 +26,12 @@ skip_glob = tests/testdata [flake8] extend-ignore = - C901, # Function complexity checker F401, # Unused imports E203, # Incompatible with black see https://github.com/psf/black/issues/315 W503, # Incompatible with black - E501, # Line too long - B950, # Line too long B901 # Combine yield and return statements in one function -max-line-length=88 -max-complexity = 20 +max-complexity = 83 +max-line-length = 138 select = B,C,E,F,W,T4,B9 # Required for flake8-typing-imports (v1.12.0) # The plugin doesn't yet read the value from pyproject.toml From 354e4dfbeb140d66b88edfa681df2063d6ae1a9a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 18 Sep 2022 09:28:34 +0200 Subject: [PATCH 47/93] Migrate configs for isort, mypy, and pytest into pyproject.toml (#1789) Co-authored-by: Pierre Sassoulas --- pyproject.toml | 38 +++++++++++++++++++++++++++++++++++++ setup.cfg | 51 -------------------------------------------------- 2 files changed, 38 insertions(+), 51 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cc724ed419..66a52f5c7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,3 +54,41 @@ include = ["astroid*"] [tool.setuptools.dynamic] version = {attr = "astroid.__pkginfo__.__version__"} + +[tool.aliases] +test = "pytest" + +[tool.pytest.ini_options] +addopts = '-m "not acceptance"' +python_files = ["*test_*.py"] +testpaths = ["tests"] + +[tool.isort] +include_trailing_comma = true +known_first_party = ["astroid"] +known_third_party = ["attr", "nose", "numpy", "pytest", "six", "sphinx"] +line_length = 88 +multi_line_output = 3 +skip_glob = ["tests/testdata"] + +[tool.mypy] +enable_error_code = "ignore-without-code" +no_implicit_optional = true +scripts_are_modules = true +show_error_codes = true +warn_redundant_casts = true + +[[tool.mypy.overrides]] +# Importlib typeshed stubs do not include the private functions we use +module = [ + "_io.*", + "gi.*", + "importlib.*", + "lazy_object_proxy.*", + "nose.*", + "numpy.*", + "pytest", + "setuptools", + "wrapt.*", +] +ignore_missing_imports = true diff --git a/setup.cfg b/setup.cfg index 6223228efa..9fda04b000 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,22 +8,6 @@ license_files = LICENSE CONTRIBUTORS.txt -[aliases] -test = pytest - -[tool:pytest] -testpaths = tests -python_files = *test_*.py -addopts = -m "not acceptance" - -[isort] -multi_line_output = 3 -line_length = 88 -known_third_party = sphinx, pytest, six, nose, numpy, attr -known_first_party = astroid -include_trailing_comma = True -skip_glob = tests/testdata - [flake8] extend-ignore = F401, # Unused imports @@ -36,38 +20,3 @@ select = B,C,E,F,W,T4,B9 # Required for flake8-typing-imports (v1.12.0) # The plugin doesn't yet read the value from pyproject.toml min_python_version = 3.7.2 - -[mypy] -scripts_are_modules = True -no_implicit_optional = True -warn_redundant_casts = True -show_error_codes = True -enable_error_code = ignore-without-code - -[mypy-setuptools] -ignore_missing_imports = True - -[mypy-pytest] -ignore_missing_imports = True - -[mypy-nose.*] -ignore_missing_imports = True - -[mypy-numpy.*] -ignore_missing_imports = True - -[mypy-_io.*] -ignore_missing_imports = True - -[mypy-wrapt.*] -ignore_missing_imports = True - -[mypy-lazy_object_proxy.*] -ignore_missing_imports = True - -[mypy-gi.*] -ignore_missing_imports = True - -[mypy-importlib.*] -# Importlib typeshed stubs do not include the private functions we use -ignore_missing_imports = True From 731d0515fcbc3c95813270021ec422d3114e13d6 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 18 Sep 2022 08:59:25 +0200 Subject: [PATCH 48/93] Apply black on the mechanize brain --- astroid/brain/brain_mechanize.py | 50 ++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/astroid/brain/brain_mechanize.py b/astroid/brain/brain_mechanize.py index 4c86fd9ba3..6b08bc42f5 100644 --- a/astroid/brain/brain_mechanize.py +++ b/astroid/brain/brain_mechanize.py @@ -9,71 +9,111 @@ def mechanize_transform(): return AstroidBuilder(AstroidManager()).string_build( - """ - -class Browser(object): + """class Browser(object): def __getattr__(self, name): return None + def __getitem__(self, name): return None + def __setitem__(self, name, val): return None + def back(self, n=1): return None + def clear_history(self): return None + def click(self, *args, **kwds): return None + def click_link(self, link=None, **kwds): return None + def close(self): return None + def encoding(self): return None - def find_link(self, text=None, text_regex=None, name=None, name_regex=None, url=None, url_regex=None, tag=None, predicate=None, nr=0): + + def find_link( + self, + text=None, + text_regex=None, + name=None, + name_regex=None, + url=None, + url_regex=None, + tag=None, + predicate=None, + nr=0, + ): return None + def follow_link(self, link=None, **kwds): return None + def forms(self): return None + def geturl(self): return None + def global_form(self): return None + def links(self, **kwds): return None + def open_local_file(self, filename): return None + def open(self, url, data=None, timeout=None): return None + def open_novisit(self, url, data=None, timeout=None): return None + def open_local_file(self, filename): return None + def reload(self): return None + def response(self): return None + def select_form(self, name=None, predicate=None, nr=None, **attrs): return None + def set_cookie(self, cookie_string): return None + def set_handle_referer(self, handle): return None + def set_header(self, header, value=None): return None + def set_html(self, html, url="http://example.com/"): return None + def set_response(self, response): return None - def set_simple_cookie(self, name, value, domain, path='/'): + + def set_simple_cookie(self, name, value, domain, path="/"): return None + def submit(self, *args, **kwds): return None + def title(self): return None + def viewing_html(self): return None + def visit_response(self, response, request=None): return None """ From 8d6d31bb8f9b44c6147522c49fa099c110b067f0 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 18 Sep 2022 09:00:22 +0200 Subject: [PATCH 49/93] [flake8] Set the max line length to 110 instead of 138 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> Co-authored-by: Christian Clauss --- astroid/brain/brain_numpy_core_multiarray.py | 13 +++++++++---- astroid/brain/brain_typing.py | 7 ++++--- astroid/decorators.py | 3 ++- astroid/interpreter/_import/spec.py | 4 +++- astroid/node_classes.py | 3 ++- astroid/rebuilder.py | 3 ++- astroid/scoped_nodes.py | 3 ++- setup.cfg | 4 +++- tests/unittest_brain.py | 6 ++++-- tests/unittest_brain_ctypes.py | 8 +++++--- tests/unittest_modutils.py | 2 -- tests/unittest_nodes_lineno.py | 7 ++++--- 12 files changed, 40 insertions(+), 23 deletions(-) diff --git a/astroid/brain/brain_numpy_core_multiarray.py b/astroid/brain/brain_numpy_core_multiarray.py index 487ec471d0..dbdb24ea47 100644 --- a/astroid/brain/brain_numpy_core_multiarray.py +++ b/astroid/brain/brain_numpy_core_multiarray.py @@ -47,10 +47,15 @@ def vdot(a, b): return numpy.ndarray([0, 0])""", "bincount": """def bincount(x, weights=None, minlength=0): return numpy.ndarray([0, 0])""", - "busday_count": """def busday_count(begindates, enddates, weekmask='1111100', holidays=[], busdaycal=None, out=None): - return numpy.ndarray([0, 0])""", - "busday_offset": """def busday_offset(dates, offsets, roll='raise', weekmask='1111100', holidays=None, busdaycal=None, out=None): - return numpy.ndarray([0, 0])""", + "busday_count": """def busday_count( + begindates, enddates, weekmask='1111100', holidays=[], busdaycal=None, out=None + ): + return numpy.ndarray([0, 0])""", + "busday_offset": """def busday_offset( + dates, offsets, roll='raise', weekmask='1111100', holidays=None, + busdaycal=None, out=None + ): + return numpy.ndarray([0, 0])""", "can_cast": """def can_cast(from_, to, casting='safe'): return True""", "copyto": """def copyto(dst, src, casting='same_kind', where=True): diff --git a/astroid/brain/brain_typing.py b/astroid/brain/brain_typing.py index 807ba96e6e..b34b8bec50 100644 --- a/astroid/brain/brain_typing.py +++ b/astroid/brain/brain_typing.py @@ -240,7 +240,7 @@ def _forbid_class_getitem_access(node: ClassDef) -> None: def full_raiser(origin_func, attr, *args, **kwargs): """ Raises an AttributeInferenceError in case of access to __class_getitem__ method. - Otherwise just call origin_func. + Otherwise, just call origin_func. """ if attr == "__class_getitem__": raise AttributeInferenceError("__class_getitem__ access is not allowed") @@ -248,8 +248,9 @@ def full_raiser(origin_func, attr, *args, **kwargs): try: node.getattr("__class_getitem__") - # If we are here, then we are sure to modify object that do have __class_getitem__ method (which origin is one the - # protocol defined in collections module) whereas the typing module consider it should not + # If we are here, then we are sure to modify an object that does have + # __class_getitem__ method (which origin is the protocol defined in + # collections module) whereas the typing module considers it should not. # We do not want __class_getitem__ to be found in the classdef partial_raiser = partial(full_raiser, node.getattr) node.getattr = partial_raiser diff --git a/astroid/decorators.py b/astroid/decorators.py index e9cc3292c2..abdc127088 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -208,7 +208,8 @@ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: ): warnings.warn( f"'{arg}' will be a required argument for " - f"'{args[0].__class__.__qualname__}.{func.__name__}' in astroid {astroid_version} " + f"'{args[0].__class__.__qualname__}.{func.__name__}'" + f" in astroid {astroid_version} " f"('{arg}' should be of type: '{type_annotation}')", DeprecationWarning, ) diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 5350916b32..ce6d24166b 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -121,7 +121,9 @@ def find_module( else: try: spec = importlib.util.find_spec(modname) - if spec and spec.loader is importlib.machinery.FrozenImporter: # type: ignore[comparison-overlap] + if ( + spec and spec.loader is importlib.machinery.FrozenImporter + ): # noqa: E501 # type: ignore[comparison-overlap] # No need for BuiltinImporter; builtins handled above return ModuleSpec( name=modname, diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 3711309bbf..59bb0109eb 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -92,6 +92,7 @@ # Please remove astroid/scoped_nodes.py|astroid/node_classes.py in autoflake # exclude when removing this file. warnings.warn( - "The 'astroid.node_classes' module is deprecated and will be replaced by 'astroid.nodes' in astroid 3.0.0", + "The 'astroid.node_classes' module is deprecated and will be replaced by " + "'astroid.nodes' in astroid 3.0.0", DeprecationWarning, ) diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index 070b9db84c..757feaa434 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -1332,7 +1332,8 @@ def visit_attribute( ) # Prohibit a local save if we are in an ExceptHandler. if not isinstance(parent, nodes.ExceptHandler): - # mypy doesn't recognize that newnode has to be AssignAttr because it doesn't support ParamSpec + # mypy doesn't recognize that newnode has to be AssignAttr because it + # doesn't support ParamSpec # See https://github.com/python/mypy/issues/8645 self._delayed_assattr.append(newnode) # type: ignore[arg-type] else: diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index 677f892578..1e3fbf31e1 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -28,6 +28,7 @@ # Please remove astroid/scoped_nodes.py|astroid/node_classes.py in autoflake # exclude when removing this file. warnings.warn( - "The 'astroid.scoped_nodes' module is deprecated and will be replaced by 'astroid.nodes' in astroid 3.0.0", + "The 'astroid.scoped_nodes' module is deprecated and will be replaced by " + "'astroid.nodes' in astroid 3.0.0", DeprecationWarning, ) diff --git a/setup.cfg b/setup.cfg index 9fda04b000..74d9078e60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,9 @@ extend-ignore = W503, # Incompatible with black B901 # Combine yield and return statements in one function max-complexity = 83 -max-line-length = 138 +max-line-length = 120 +# per-file-ignores = +# astroid/brain/brain_numpy_core_multiarray.py: E501, B950 select = B,C,E,F,W,T4,B9 # Required for flake8-typing-imports (v1.12.0) # The plugin doesn't yet read the value from pyproject.toml diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 07d17c4ceb..2f88fc5422 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -1473,7 +1473,8 @@ def test_collections_object_not_yet_subscriptable_2(self): @test_utils.require_version(minver="3.9") def test_collections_object_subscriptable_3(self): - """With python39 ByteString class of the colletions module is subscritable (but not the same class from typing module)""" + """With Python 3.9 the ByteString class of the collections module is subscritable + (but not the same class from typing module)""" right_node = builder.extract_node( """ import collections.abc @@ -1984,7 +1985,8 @@ class Derived(typing.Hashable, typing.Iterator[int]): ) def test_typing_object_notsubscriptable_3(self): - """Until python39 ByteString class of the typing module is not subscritable (whereas it is in the collections module)""" + """Until python39 ByteString class of the typing module is not + subscriptable (whereas it is in the collections' module)""" right_node = builder.extract_node( """ import typing diff --git a/tests/unittest_brain_ctypes.py b/tests/unittest_brain_ctypes.py index cae95409f5..dbcf54d9b1 100644 --- a/tests/unittest_brain_ctypes.py +++ b/tests/unittest_brain_ctypes.py @@ -10,7 +10,8 @@ pytestmark = pytest.mark.skipif( hasattr(sys, "pypy_version_info"), - reason="pypy has its own implementation of _ctypes module which is different from the one of cpython", + reason="pypy has its own implementation of _ctypes module which is different " + "from the one of cpython", ) @@ -83,8 +84,9 @@ def test_ctypes_redefined_types_members(c_type, builtin_type, type_code): def test_cdata_member_access() -> None: """ - Test that the base members are still accessible. Each redefined ctypes type inherits from _SimpleCData which itself - inherits from _CData. Checks that _CData members are accessibles + Test that the base members are still accessible. Each redefined ctypes type + inherits from _SimpleCData which itself inherits from _CData. Checks that + _CData members are accessible. """ src = """ import ctypes diff --git a/tests/unittest_modutils.py b/tests/unittest_modutils.py index 82bb76602a..6f8d8033ec 100644 --- a/tests/unittest_modutils.py +++ b/tests/unittest_modutils.py @@ -411,7 +411,6 @@ def test_load_module_set_attribute(self) -> None: class ExtensionPackageWhitelistTest(unittest.TestCase): def test_is_module_name_part_of_extension_package_whitelist_true(self) -> None: - """Test that the is_module_name_part_of_extension_package_whitelist function returns True when needed""" self.assertTrue( modutils.is_module_name_part_of_extension_package_whitelist( "numpy", {"numpy"} @@ -429,7 +428,6 @@ def test_is_module_name_part_of_extension_package_whitelist_true(self) -> None: ) def test_is_module_name_part_of_extension_package_whitelist_success(self) -> None: - """Test that the is_module_name_part_of_extension_package_whitelist function returns False when needed""" self.assertFalse( modutils.is_module_name_part_of_extension_package_whitelist( "numpy", {"numpy.core"} diff --git a/tests/unittest_nodes_lineno.py b/tests/unittest_nodes_lineno.py index c1c089ac07..2cc8094d94 100644 --- a/tests/unittest_nodes_lineno.py +++ b/tests/unittest_nodes_lineno.py @@ -661,9 +661,10 @@ async def func(): #@ assert isinstance(f1.args.kwonlyargs[0], nodes.AssignName) assert (f1.args.kwonlyargs[0].lineno, f1.args.kwonlyargs[0].col_offset) == (4, 4) assert (f1.args.kwonlyargs[0].end_lineno, f1.args.kwonlyargs[0].end_col_offset) == (4, 16) - assert isinstance(f1.args.kwonlyargs_annotations[0], nodes.Name) - assert (f1.args.kwonlyargs_annotations[0].lineno, f1.args.kwonlyargs_annotations[0].col_offset) == (4, 13) - assert (f1.args.kwonlyargs_annotations[0].end_lineno, f1.args.kwonlyargs_annotations[0].end_col_offset) == (4, 16) + annotations = f1.args.kwonlyargs_annotations + assert isinstance(annotations[0], nodes.Name) + assert (annotations[0].lineno, annotations[0].col_offset) == (4, 13) + assert (annotations[0].end_lineno, annotations[0].end_col_offset) == (4, 16) assert isinstance(f1.args.kw_defaults[0], nodes.Const) assert (f1.args.kw_defaults[0].lineno, f1.args.kw_defaults[0].col_offset) == (4, 19) assert (f1.args.kw_defaults[0].end_lineno, f1.args.kw_defaults[0].end_col_offset) == (4, 20) From 056d8e5fab7a167f73115d524ab92170b3ed5f9f Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 18 Sep 2022 09:04:41 +0200 Subject: [PATCH 50/93] [flake8] Set the max complexity to the default instead of 83 --- astroid/arguments.py | 2 +- astroid/bases.py | 2 +- astroid/brain/brain_gi.py | 2 +- astroid/decorators.py | 2 +- astroid/manager.py | 2 +- astroid/nodes/scoped_nodes/scoped_nodes.py | 2 +- astroid/objects.py | 4 +++- astroid/protocols.py | 3 +-- astroid/rebuilder.py | 2 +- setup.cfg | 5 +---- 10 files changed, 12 insertions(+), 14 deletions(-) diff --git a/astroid/arguments.py b/astroid/arguments.py index fdbe7aac91..4108c0ddf0 100644 --- a/astroid/arguments.py +++ b/astroid/arguments.py @@ -150,7 +150,7 @@ def _unpack_args(self, args, context=None): values.append(arg) return values - def infer_argument(self, funcnode, name, context): + def infer_argument(self, funcnode, name, context): # noqa: C901 """infer a function argument value according to the call context Arguments: diff --git a/astroid/bases.py b/astroid/bases.py index 1f5072a8e8..25a8393dde 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -485,7 +485,7 @@ def implicit_parameters(self) -> Literal[0, 1]: def is_bound(self): return True - def _infer_type_new_call(self, caller, context): + def _infer_type_new_call(self, caller, context): # noqa: C901 """Try to infer what type.__new__(mcs, name, bases, attrs) returns. In order for such call to be valid, the metaclass needs to be diff --git a/astroid/brain/brain_gi.py b/astroid/brain/brain_gi.py index 53491d1400..248a60167b 100644 --- a/astroid/brain/brain_gi.py +++ b/astroid/brain/brain_gi.py @@ -54,7 +54,7 @@ ) -def _gi_build_stub(parent): +def _gi_build_stub(parent): # noqa: C901 """ Inspect the passed module recursively and build stubs for functions, classes, etc. diff --git a/astroid/decorators.py b/astroid/decorators.py index abdc127088..9def52cdc5 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -157,7 +157,7 @@ def raise_if_nothing_inferred(func, instance, args, kwargs): # Expensive decorators only used to emit Deprecation warnings. # If no other than the default DeprecationWarning are enabled, # fall back to passthrough implementations. -if util.check_warnings_filter(): +if util.check_warnings_filter(): # noqa: C901 def deprecate_default_argument_values( astroid_version: str = "3.0", **arguments: str diff --git a/astroid/manager.py b/astroid/manager.py index f0932d54de..737fcda361 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -148,7 +148,7 @@ def _can_load_extension(self, modname: str) -> bool: modname, self.extension_package_whitelist ) - def ast_from_module_name( + def ast_from_module_name( # noqa: C901 self, modname: str | None, context_file: str | None = None, diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index b01c23f372..833da66ca1 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -1475,7 +1475,7 @@ def extra_decorators(self) -> list[node_classes.Call]: return decorators @cached_property - def type(self) -> str: # pylint: disable=too-many-return-statements + def type(self) -> str: # pylint: disable=too-many-return-statements # noqa: C901 """The function type for this node. Possible values are: method, function, staticmethod, classmethod. diff --git a/astroid/objects.py b/astroid/objects.py index 1649674f8e..a1d886bb1f 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -138,7 +138,9 @@ def name(self): def qname(self) -> Literal["super"]: return "super" - def igetattr(self, name: str, context: InferenceContext | None = None): + def igetattr( # noqa: C901 + self, name: str, context: InferenceContext | None = None + ): """Retrieve the inferred values of the given attribute name.""" # '__class__' is a special attribute that should be taken directly # from the special attributes dict diff --git a/astroid/protocols.py b/astroid/protocols.py index 0d90d90bc3..1b2bf73de0 100644 --- a/astroid/protocols.py +++ b/astroid/protocols.py @@ -672,7 +672,7 @@ def named_expr_assigned_stmts( @decorators.yes_if_nothing_inferred -def starred_assigned_stmts( +def starred_assigned_stmts( # noqa: C901 self: nodes.Starred, node: node_classes.AssignedStmtsPossibleNode = None, context: InferenceContext | None = None, @@ -823,7 +823,6 @@ def _determine_starred_iteration_lookups( last_lookup = lookup_slice for element in itered: - # We probably want to infer the potential values *for each* element in an # iterable, but we can't infer a list of all values, when only a list of # step values are expected: diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index 757feaa434..2c868fd076 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -259,7 +259,7 @@ def visit_module( self._reset_end_lineno(newnode) return newnode - if TYPE_CHECKING: + if TYPE_CHECKING: # noqa: C901 @overload def visit(self, node: ast.arg, parent: NodeNG) -> nodes.AssignName: diff --git a/setup.cfg b/setup.cfg index 74d9078e60..c6be9f8f4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,10 +14,7 @@ extend-ignore = E203, # Incompatible with black see https://github.com/psf/black/issues/315 W503, # Incompatible with black B901 # Combine yield and return statements in one function -max-complexity = 83 -max-line-length = 120 -# per-file-ignores = -# astroid/brain/brain_numpy_core_multiarray.py: E501, B950 +max-line-length = 110 select = B,C,E,F,W,T4,B9 # Required for flake8-typing-imports (v1.12.0) # The plugin doesn't yet read the value from pyproject.toml From c5cfba825becac0b33bbbf7d7ed9c3b90bb379f7 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 19 Sep 2022 19:12:49 +0200 Subject: [PATCH 51/93] pylintrc: Remove duplicate plugin pylint.extensions.code_style (#1798) Keeping longer lists sorted makes it difficult to add duplicates. --- pylintrc | 1 - 1 file changed, 1 deletion(-) diff --git a/pylintrc b/pylintrc index ace5fb5728..0cc82e7a3d 100644 --- a/pylintrc +++ b/pylintrc @@ -22,7 +22,6 @@ load-plugins= pylint.extensions.code_style, pylint.extensions.overlapping_exceptions, pylint.extensions.typing, - pylint.extensions.code_style, pylint.extensions.set_membership, pylint.extensions.redefined_variable_type, pylint.extensions.for_any_all, From de15c45e4c9fc6aa5c759c37e904657de6c6c3d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 19:46:48 +0200 Subject: [PATCH 52/93] Bump pylint from 2.15.2 to 2.15.3 (#1799) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.2 to 2.15.3. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.2...v2.15.3) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 727aa220d1..41ce365097 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ black==22.8.0 -pylint==2.15.2 +pylint==2.15.3 isort==5.10.1 flake8==5.0.4 flake8-typing-imports==1.13.0 From 849d043f3e3cd63881ba919ccba4e6a726947d22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Sep 2022 05:57:01 +0200 Subject: [PATCH 53/93] [pre-commit.ci] pre-commit autoupdate (#1800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v1.5.3 → v1.6.0](https://github.com/PyCQA/autoflake/compare/v1.5.3...v1.6.0) - [github.com/asottile/pyupgrade: v2.37.3 → v2.38.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fc29143e2..d26a933b00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: end-of-file-fixer exclude: tests/testdata - repo: https://github.com/PyCQA/autoflake - rev: v1.5.3 + rev: v1.6.0 hooks: - id: autoflake exclude: tests/testdata|astroid/__init__.py|astroid/scoped_nodes.py|astroid/node_classes.py @@ -28,7 +28,7 @@ repos: exclude: tests/testdata|setup.py types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade exclude: tests/testdata From 4798d289b514669a6cf28d3385ef056b4dcbd59f Mon Sep 17 00:00:00 2001 From: Dani Alcala <112832187+clavedeluna@users.noreply.github.com> Date: Wed, 21 Sep 2022 15:15:14 -0300 Subject: [PATCH 54/93] Fix wrong pytest example in readme (#1805) Co-authored-by: Pierre Sassoulas --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b7c3c232e5..cbba168868 100644 --- a/README.rst +++ b/README.rst @@ -86,4 +86,4 @@ Tests are in the 'test' subdirectory. To launch the whole tests suite, you can u either `tox` or `pytest`:: tox - pytest astroid + pytest From 61dc314c9daf74ebe76767feeb1a3eb857f67771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 21 Sep 2022 20:20:57 +0200 Subject: [PATCH 55/93] Finish typing of ``AstroidManagerBrain`` (#1804) --- astroid/manager.py | 3 ++- astroid/typing.py | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/astroid/manager.py b/astroid/manager.py index 737fcda361..37c87005fc 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -13,6 +13,7 @@ import os import types import zipimport +from collections.abc import Callable from importlib.util import find_spec, module_from_spec from typing import TYPE_CHECKING, ClassVar @@ -353,7 +354,7 @@ def infer_ast_from_something(self, obj, context=None): for inferred in modastroid.igetattr(name, context): yield inferred.instantiate_class() - def register_failed_import_hook(self, hook): + def register_failed_import_hook(self, hook: Callable[[str], nodes.Module]) -> None: """Registers a hook to resolve imports that cannot be found otherwise. `hook` must be a function that accepts a single argument `modname` which diff --git a/astroid/typing.py b/astroid/typing.py index 6fd553ab04..e42cd716b1 100644 --- a/astroid/typing.py +++ b/astroid/typing.py @@ -8,8 +8,9 @@ from typing import TYPE_CHECKING, Any, Callable, Union if TYPE_CHECKING: - from astroid import bases, nodes, transforms, util + from astroid import bases, exceptions, nodes, transforms, util from astroid.context import InferenceContext + from astroid.interpreter._import import spec if sys.version_info >= (3, 8): from typing import TypedDict @@ -33,11 +34,13 @@ class AstroidManagerBrain(TypedDict): """Dictionary to store relevant information for a AstroidManager class.""" astroid_cache: dict[str, nodes.Module] - _mod_file_cache: dict - _failed_import_hooks: list + _mod_file_cache: dict[ + tuple[str, str | None], spec.ModuleSpec | exceptions.AstroidImportError + ] + _failed_import_hooks: list[Callable[[str], nodes.Module]] always_load_extensions: bool optimize_ast: bool - extension_package_whitelist: set + extension_package_whitelist: set[str] _transform: transforms.TransformVisitor From b867508d903719378e439ec48686565a497ec312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 21 Sep 2022 21:14:50 +0200 Subject: [PATCH 56/93] Finish typing of ``astroid.manager.py`` (#1806) --- astroid/builder.py | 6 ++-- astroid/exceptions.py | 2 +- astroid/inference_tip.py | 2 +- astroid/manager.py | 72 ++++++++++++++++++++++++++-------------- astroid/raw_building.py | 2 +- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/astroid/builder.py b/astroid/builder.py index 24caa0c6e0..0b8755a389 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -13,6 +13,7 @@ import os import textwrap import types +from collections.abc import Sequence from tokenize import detect_encoding from astroid import bases, modutils, nodes, raw_building, rebuilder, util @@ -261,8 +262,9 @@ def delayed_assattr(self, node): pass -def build_namespace_package_module(name: str, path: list[str]) -> nodes.Module: - return nodes.Module(name, path=path, package=True) +def build_namespace_package_module(name: str, path: Sequence[str]) -> nodes.Module: + # TODO: Typing: Remove the cast to list and just update typing to accept Sequence + return nodes.Module(name, path=list(path), package=True) def parse(code, module_name="", path=None, apply_transforms=True): diff --git a/astroid/exceptions.py b/astroid/exceptions.py index 0dac271dd7..87a8744ddd 100644 --- a/astroid/exceptions.py +++ b/astroid/exceptions.py @@ -87,7 +87,7 @@ def __init__( error: Exception | None = None, source: str | None = None, path: str | None = None, - cls: None = None, + cls: type | None = None, class_repr: str | None = None, **kws: Any, ) -> None: diff --git a/astroid/inference_tip.py b/astroid/inference_tip.py index 341efd631e..e4c54822e0 100644 --- a/astroid/inference_tip.py +++ b/astroid/inference_tip.py @@ -23,7 +23,7 @@ _cache: dict[tuple[InferFn, NodeNG], list[InferOptions] | None] = {} -def clear_inference_tip_cache(): +def clear_inference_tip_cache() -> None: """Clear the inference tips cache.""" _cache.clear() diff --git a/astroid/manager.py b/astroid/manager.py index 37c87005fc..e2f0d3fd91 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -13,12 +13,14 @@ import os import types import zipimport -from collections.abc import Callable +from collections.abc import Callable, Iterator, Sequence from importlib.util import find_spec, module_from_spec -from typing import TYPE_CHECKING, ClassVar +from typing import Any, ClassVar +from astroid import nodes from astroid._cache import CACHE_MANAGER from astroid.const import BRAIN_MODULES_DIRECTORY +from astroid.context import InferenceContext from astroid.exceptions import AstroidBuildingError, AstroidImportError from astroid.interpreter._import import spec, util from astroid.modutils import ( @@ -33,15 +35,12 @@ modpath_from_file, ) from astroid.transforms import TransformVisitor -from astroid.typing import AstroidManagerBrain - -if TYPE_CHECKING: - from astroid import nodes +from astroid.typing import AstroidManagerBrain, InferenceResult ZIP_IMPORT_EXTS = (".zip", ".egg", ".whl", ".pyz", ".pyzw") -def safe_repr(obj): +def safe_repr(obj: Any) -> str: try: return repr(obj) except Exception: # pylint: disable=broad-except @@ -91,11 +90,17 @@ def unregister_transform(self): def builtins_module(self) -> nodes.Module: return self.astroid_cache["builtins"] - def visit_transforms(self, node): + def visit_transforms(self, node: nodes.NodeNG) -> InferenceResult: """Visit the transforms and apply them to the given *node*.""" return self._transform.visit(node) - def ast_from_file(self, filepath, modname=None, fallback=True, source=False): + def ast_from_file( + self, + filepath: str, + modname: str | None = None, + fallback: bool = True, + source: bool = False, + ) -> nodes.Module: """given a module name, return the astroid object""" try: filepath = get_source_file(filepath, include_no_ext=True) @@ -121,20 +126,24 @@ def ast_from_file(self, filepath, modname=None, fallback=True, source=False): return self.ast_from_module_name(modname) raise AstroidBuildingError("Unable to build an AST for {path}.", path=filepath) - def ast_from_string(self, data, modname="", filepath=None): + def ast_from_string( + self, data: str, modname: str = "", filepath: str | None = None + ) -> nodes.Module: """Given some source code as a string, return its corresponding astroid object""" # pylint: disable=import-outside-toplevel; circular import from astroid.builder import AstroidBuilder return AstroidBuilder(self).string_build(data, modname, filepath) - def _build_stub_module(self, modname): + def _build_stub_module(self, modname: str) -> nodes.Module: # pylint: disable=import-outside-toplevel; circular import from astroid.builder import AstroidBuilder return AstroidBuilder(self).string_build("", modname) - def _build_namespace_module(self, modname: str, path: list[str]) -> nodes.Module: + def _build_namespace_module( + self, modname: str, path: Sequence[str] + ) -> nodes.Module: # pylint: disable=import-outside-toplevel; circular import from astroid.builder import build_namespace_package_module @@ -184,14 +193,14 @@ def ast_from_module_name( # noqa: C901 ): return self._build_stub_module(modname) try: - module = load_module_from_name(modname) + named_module = load_module_from_name(modname) except Exception as e: raise AstroidImportError( "Loading {modname} failed with:\n{error}", modname=modname, path=found_spec.location, ) from e - return self.ast_from_module(module, modname) + return self.ast_from_module(named_module, modname) elif found_spec.type == spec.ModuleType.PY_COMPILED: raise AstroidImportError( @@ -202,7 +211,7 @@ def ast_from_module_name( # noqa: C901 elif found_spec.type == spec.ModuleType.PY_NAMESPACE: return self._build_namespace_module( - modname, found_spec.submodule_search_locations + modname, found_spec.submodule_search_locations or [] ) elif found_spec.type == spec.ModuleType.PY_FROZEN: if found_spec.location is None: @@ -228,7 +237,7 @@ def ast_from_module_name( # noqa: C901 if context_file: os.chdir(old_cwd) - def zip_import_data(self, filepath): + def zip_import_data(self, filepath: str) -> nodes.Module | None: if zipimport is None: return None @@ -255,7 +264,9 @@ def zip_import_data(self, filepath): continue return None - def file_from_module_name(self, modname, contextfile): + def file_from_module_name( + self, modname: str, contextfile: str | None + ) -> spec.ModuleSpec: try: value = self._mod_file_cache[(modname, contextfile)] except KeyError: @@ -277,7 +288,9 @@ def file_from_module_name(self, modname, contextfile): raise value.with_traceback(None) # pylint: disable=no-member return value - def ast_from_module(self, module: types.ModuleType, modname: str | None = None): + def ast_from_module( + self, module: types.ModuleType, modname: str | None = None + ) -> nodes.Module: """given an imported module, return the astroid object""" modname = modname or module.__name__ if modname in self.astroid_cache: @@ -286,7 +299,8 @@ def ast_from_module(self, module: types.ModuleType, modname: str | None = None): # some builtin modules don't have __file__ attribute filepath = module.__file__ if is_python_source(filepath): - return self.ast_from_file(filepath, modname) + # Type is checked in is_python_source + return self.ast_from_file(filepath, modname) # type: ignore[arg-type] except AttributeError: pass @@ -295,7 +309,7 @@ def ast_from_module(self, module: types.ModuleType, modname: str | None = None): return AstroidBuilder(self).module_build(module, modname) - def ast_from_class(self, klass, modname=None): + def ast_from_class(self, klass: type, modname: str | None = None) -> nodes.ClassDef: """get astroid for the given class""" if modname is None: try: @@ -308,14 +322,24 @@ def ast_from_class(self, klass, modname=None): modname=modname, ) from exc modastroid = self.ast_from_module_name(modname) - return modastroid.getattr(klass.__name__)[0] # XXX + ret = modastroid.getattr(klass.__name__)[0] + assert isinstance(ret, nodes.ClassDef) + return ret - def infer_ast_from_something(self, obj, context=None): + def infer_ast_from_something( + self, obj: object, context: InferenceContext | None = None + ) -> Iterator[InferenceResult]: """infer astroid for the given class""" if hasattr(obj, "__class__") and not isinstance(obj, type): klass = obj.__class__ - else: + elif isinstance(obj, type): klass = obj + else: + raise AstroidBuildingError( # pragma: no cover + "Unable to get type for {class_repr}.", + cls=None, + class_repr=safe_repr(obj), + ) try: modname = klass.__module__ except AttributeError as exc: @@ -364,7 +388,7 @@ def register_failed_import_hook(self, hook: Callable[[str], nodes.Module]) -> No """ self._failed_import_hooks.append(hook) - def cache_module(self, module): + def cache_module(self, module: nodes.Module) -> None: """Cache a module if no module with the same name is known yet.""" self.astroid_cache.setdefault(module.name, module) diff --git a/astroid/raw_building.py b/astroid/raw_building.py index 8cff41d33d..a18e71888a 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -486,7 +486,7 @@ def _set_proxied(const): return _CONST_PROXY[const.value.__class__] -def _astroid_bootstrapping(): +def _astroid_bootstrapping() -> None: """astroid bootstrapping the builtins module""" # this boot strapping is necessary since we need the Const nodes to # inspect_build builtins, and then we can proxy Const From e5f7488f9370b8f69c3605aa1be8650c7a3d76e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Thu, 22 Sep 2022 22:23:44 +0200 Subject: [PATCH 57/93] Finish typing of ``astroid.builder.py`` (#1807) --- astroid/builder.py | 90 +++++++++++++++++++++++++++-------------- astroid/exceptions.py | 2 +- astroid/raw_building.py | 2 +- 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/astroid/builder.py b/astroid/builder.py index 0b8755a389..a3b87faafe 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -10,19 +10,25 @@ from __future__ import annotations +import ast import os import textwrap import types -from collections.abc import Sequence +from collections.abc import Iterator, Sequence +from io import TextIOWrapper from tokenize import detect_encoding +from typing import TYPE_CHECKING from astroid import bases, modutils, nodes, raw_building, rebuilder, util -from astroid._ast import get_parser_module +from astroid._ast import ParserModule, get_parser_module from astroid.exceptions import AstroidBuildingError, AstroidSyntaxError, InferenceError from astroid.manager import AstroidManager -from astroid.nodes.node_classes import NodeNG -objects = util.lazy_import("objects") +if TYPE_CHECKING: + from astroid import objects +else: + objects = util.lazy_import("objects") + # The name of the transient function that is used to # wrap expressions to be extracted when calling @@ -35,7 +41,7 @@ MISPLACED_TYPE_ANNOTATION_ERROR = "misplaced type annotation" -def open_source_file(filename): +def open_source_file(filename: str) -> tuple[TextIOWrapper, str, str]: # pylint: disable=consider-using-with with open(filename, "rb") as byte_stream: encoding = detect_encoding(byte_stream.readline)[0] @@ -44,7 +50,7 @@ def open_source_file(filename): return stream, encoding, data -def _can_assign_attr(node, attrname): +def _can_assign_attr(node: nodes.ClassDef, attrname: str | None) -> bool: try: slots = node.slots() except NotImplementedError: @@ -65,7 +71,9 @@ class AstroidBuilder(raw_building.InspectBuilder): by default being True. """ - def __init__(self, manager=None, apply_transforms=True): + def __init__( + self, manager: AstroidManager | None = None, apply_transforms: bool = True + ) -> None: super().__init__(manager) self._apply_transforms = apply_transforms @@ -95,9 +103,10 @@ def module_build( # We have to handle transformation by ourselves since the # rebuilder isn't called for builtin nodes node = self._manager.visit_transforms(node) + assert isinstance(node, nodes.Module) return node - def file_build(self, path, modname=None): + def file_build(self, path: str, modname: str | None = None) -> nodes.Module: """Build astroid from a source code file (i.e. from an ast) *path* is expected to be a python source file @@ -135,7 +144,9 @@ def file_build(self, path, modname=None): module, builder = self._data_build(data, modname, path) return self._post_build(module, builder, encoding) - def string_build(self, data, modname="", path=None): + def string_build( + self, data: str, modname: str = "", path: str | None = None + ) -> nodes.Module: """Build astroid from source code string.""" module, builder = self._data_build(data, modname, path) module.file_bytes = data.encode("utf-8") @@ -163,7 +174,7 @@ def _post_build( return module def _data_build( - self, data: str, modname, path + self, data: str, modname: str, path: str | None ) -> tuple[nodes.Module, rebuilder.TreeRebuilder]: """Build tree node from data and add some informations""" try: @@ -193,18 +204,19 @@ def _data_build( module = builder.visit_module(node, modname, node_file, package) return module, builder - def add_from_names_to_locals(self, node): + def add_from_names_to_locals(self, node: nodes.ImportFrom) -> None: """Store imported names to the locals Resort the locals if coming from a delayed node """ - def _key_func(node): - return node.fromlineno + def _key_func(node: nodes.NodeNG) -> int: + return node.fromlineno or 0 - def sort_locals(my_list): + def sort_locals(my_list: list[nodes.NodeNG]) -> None: my_list.sort(key=_key_func) + assert node.parent # It should always default to the module for (name, asname) in node.names: if name == "*": try: @@ -218,7 +230,7 @@ def sort_locals(my_list): node.parent.set_local(asname or name, node) sort_locals(node.parent.scope().locals[asname or name]) - def delayed_assattr(self, node): + def delayed_assattr(self, node: nodes.AssignAttr) -> None: """Visit a AssAttr node This adds name to locals and handle members definition. @@ -229,8 +241,12 @@ def delayed_assattr(self, node): if inferred is util.Uninferable: continue try: - cls = inferred.__class__ - if cls is bases.Instance or cls is objects.ExceptionInstance: + # pylint: disable=unidiomatic-typecheck # We want a narrow check on the + # parent type, not all of its subclasses + if ( + type(inferred) == bases.Instance + or type(inferred) == objects.ExceptionInstance + ): inferred = inferred._proxied iattrs = inferred.instance_attrs if not _can_assign_attr(inferred, node.attrname): @@ -239,6 +255,11 @@ def delayed_assattr(self, node): # Const, Tuple or other containers that inherit from # `Instance` continue + elif ( + isinstance(inferred, bases.Proxy) + or inferred is util.Uninferable + ): + continue elif inferred.is_function: iattrs = inferred.instance_attrs else: @@ -267,7 +288,12 @@ def build_namespace_package_module(name: str, path: Sequence[str]) -> nodes.Modu return nodes.Module(name, path=list(path), package=True) -def parse(code, module_name="", path=None, apply_transforms=True): +def parse( + code: str, + module_name: str = "", + path: str | None = None, + apply_transforms: bool = True, +) -> nodes.Module: """Parses a source string in order to obtain an astroid AST from it :param str code: The code for the module. @@ -284,7 +310,7 @@ def parse(code, module_name="", path=None, apply_transforms=True): return builder.string_build(code, modname=module_name, path=path) -def _extract_expressions(node): +def _extract_expressions(node: nodes.NodeNG) -> Iterator[nodes.NodeNG]: """Find expressions in a call to _TRANSIENT_FUNCTION and extract them. The function walks the AST recursively to search for expressions that @@ -303,6 +329,7 @@ def _extract_expressions(node): and node.func.name == _TRANSIENT_FUNCTION ): real_expr = node.args[0] + assert node.parent real_expr.parent = node.parent # Search for node in all _astng_fields (the fields checked when # get_children is called) of its parent. Some of those fields may @@ -311,7 +338,7 @@ def _extract_expressions(node): # like no call to _TRANSIENT_FUNCTION ever took place. for name in node.parent._astroid_fields: child = getattr(node.parent, name) - if isinstance(child, (list, tuple)): + if isinstance(child, list): for idx, compound_child in enumerate(child): if compound_child is node: child[idx] = real_expr @@ -323,7 +350,7 @@ def _extract_expressions(node): yield from _extract_expressions(child) -def _find_statement_by_line(node, line): +def _find_statement_by_line(node: nodes.NodeNG, line: int) -> nodes.NodeNG | None: """Extracts the statement on a specific line from an AST. If the line number of node matches line, it will be returned; @@ -358,7 +385,7 @@ def _find_statement_by_line(node, line): return None -def extract_node(code: str, module_name: str = "") -> NodeNG | list[NodeNG]: +def extract_node(code: str, module_name: str = "") -> nodes.NodeNG | list[nodes.NodeNG]: """Parses some Python code as a module and extracts a designated AST node. Statements: @@ -412,13 +439,13 @@ def extract_node(code: str, module_name: str = "") -> NodeNG | list[NodeNG]: :returns: The designated node from the parse tree, or a list of nodes. """ - def _extract(node): + def _extract(node: nodes.NodeNG | None) -> nodes.NodeNG | None: if isinstance(node, nodes.Expr): return node.value return node - requested_lines = [] + requested_lines: list[int] = [] for idx, line in enumerate(code.splitlines()): if line.strip().endswith(_STATEMENT_SELECTOR): requested_lines.append(idx + 1) @@ -427,7 +454,7 @@ def _extract(node): if not tree.body: raise ValueError("Empty tree, cannot extract from it") - extracted = [] + extracted: list[nodes.NodeNG | None] = [] if requested_lines: extracted = [_find_statement_by_line(tree, line) for line in requested_lines] @@ -438,12 +465,13 @@ def _extract(node): extracted.append(tree.body[-1]) extracted = [_extract(node) for node in extracted] - if len(extracted) == 1: - return extracted[0] - return extracted + extracted_without_none = [node for node in extracted if node is not None] + if len(extracted_without_none) == 1: + return extracted_without_none[0] + return extracted_without_none -def _extract_single_node(code: str, module_name: str = "") -> NodeNG: +def _extract_single_node(code: str, module_name: str = "") -> nodes.NodeNG: """Call extract_node while making sure that only one value is returned.""" ret = extract_node(code, module_name) if isinstance(ret, list): @@ -451,7 +479,9 @@ def _extract_single_node(code: str, module_name: str = "") -> NodeNG: return ret -def _parse_string(data, type_comments=True): +def _parse_string( + data: str, type_comments: bool = True +) -> tuple[ast.Module, ParserModule]: parser_module = get_parser_module(type_comments=type_comments) try: parsed = parser_module.parse(data + "\n", type_comments=type_comments) diff --git a/astroid/exceptions.py b/astroid/exceptions.py index 87a8744ddd..412b0ac703 100644 --- a/astroid/exceptions.py +++ b/astroid/exceptions.py @@ -131,7 +131,7 @@ class AstroidSyntaxError(AstroidBuildingError): def __init__( self, message: str, - modname: str, + modname: str | None, error: Exception, path: str | None, source: str | None = None, diff --git a/astroid/raw_building.py b/astroid/raw_building.py index a18e71888a..212939c226 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -338,7 +338,7 @@ class InspectBuilder: FunctionDef and ClassDef nodes and some others as guessed. """ - def __init__(self, manager_instance=None): + def __init__(self, manager_instance: AstroidManager | None = None) -> None: self._manager = manager_instance or AstroidManager() self._done: dict[types.ModuleType | type, nodes.Module | nodes.ClassDef] = {} self._module: types.ModuleType From 8559936fd1d2309b27514853bb1c344e04fd7579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saugat=20Pachhai=20=28=E0=A4=B8=E0=A5=8C=E0=A4=97=E0=A4=BE?= =?UTF-8?q?=E0=A4=A4=29?= Date: Wed, 21 Sep 2022 17:53:50 +0545 Subject: [PATCH 58/93] improve is_namespace check See https://stackoverflow.com/a/42962529. Let's take the following contents as an example: ```python import celery.result ``` From #1777, astroid started to use `processed_components` for namespace check. In the above case, the `modname` is `celery.result`, it first checks for `celery` and then `celery.result`. Before that PR, it'd always check for `celery.result`. `celery` is recreating module to make it lazily load. See https://github.com/celery/celery/blob/34533ab44d2a6492004bc3df44dc04ad5c6611e7/celery/__init__.py#L150. This module does not have `__spec__` set. Reading through Python's docs, it seems that `__spec__` can be set to None, so it seems like it's not a thing that we can depend upon for namespace checks. See https://docs.python.org/3/reference/import.html#spec__. --- The `celery.result` gets imported for me when pylint-pytest plugin tries to load fixtures, but this could happen anytime if any plugin imports packages. In that case, `importlib.util._find_spec_from_path("celery")` will raise ValueError since it's already in `sys.modules` and does not have a spec. Fixes https://github.com/PyCQA/pylint/issues/7488. --- ChangeLog | 4 ++++ astroid/interpreter/_import/util.py | 5 ++++- tests/unittest_manager.py | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 616ac1a2fe..e5bc5dc9f2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,10 @@ Release date: TBA Refs PyCQA/pylint#5151 +* Improve detection of namespace packages for the modules with ``__spec__`` set to None. + + Closes PyCQA/pylint#7488. + What's New in astroid 2.12.11? ============================== diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index c9466999ab..b5b089331e 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -52,8 +52,11 @@ def is_namespace(modname: str) -> bool: # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter" # because of cffi's behavior # See: https://github.com/PyCQA/astroid/issues/1776 + mod = sys.modules[processed_components[0]] return ( - sys.modules[processed_components[0]].__spec__ is None + mod.__spec__ is None + and getattr(mod, "__file__", None) is None + and hasattr(mod, "__path__") and not IS_PYPY ) except KeyError: diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 678cbf2940..ba773e4e35 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -144,6 +144,15 @@ def test_module_unexpectedly_missing_spec(self) -> None: finally: astroid_module.__spec__ = original_spec + def test_module_unexpectedly_spec_is_none(self) -> None: + astroid_module = sys.modules["astroid"] + original_spec = astroid_module.__spec__ + astroid_module.__spec__ = None + try: + self.assertFalse(util.is_namespace("astroid")) + finally: + astroid_module.__spec__ = original_spec + def test_implicit_namespace_package(self) -> None: data_dir = os.path.dirname(resources.find("data/namespace_pep_420")) contribute = os.path.join(data_dir, "contribute_to_namespace") From 6eb73d02c3af28a03a349bbb3d9bfbf888d09629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Fri, 23 Sep 2022 21:31:10 +0200 Subject: [PATCH 59/93] Handle empty ``modname`` in ``ast_from_module_name`` (#1801) --- astroid/manager.py | 2 ++ tests/unittest_manager.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/astroid/manager.py b/astroid/manager.py index e2f0d3fd91..9f88c699fa 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -165,6 +165,8 @@ def ast_from_module_name( # noqa: C901 use_cache: bool = True, ) -> nodes.Module: """Given a module name, return the astroid object.""" + if modname is None: + raise AstroidBuildingError("No module name given.") # Sometimes we don't want to use the cache. For example, when we're # importing a module with the same name as the file that is importing # we want to fallback on the import system to make sure we get the correct diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index ba773e4e35..1c8a3fdc5f 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -365,6 +365,10 @@ def test_same_name_import_module(self) -> None: stdlib_math = next(module.body[1].value.args[0].infer()) assert self.manager.astroid_cache["math"] != stdlib_math + def test_raises_exception_for_empty_modname(self) -> None: + with pytest.raises(AstroidBuildingError): + self.manager.ast_from_module_name(None) + class BorgAstroidManagerTC(unittest.TestCase): def test_borg(self) -> None: From 89547ea228de8596acd668dc9e7ee597cbcd0bd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 19:38:39 +0200 Subject: [PATCH 60/93] Update sphinx requirement from ~=5.1 to ~=5.2 (#1808) Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.1.0...v5.2.1) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 90795c2603..36d03c8341 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,2 +1,2 @@ -e . -sphinx~=5.1 +sphinx~=5.2 From c0c60747666607a6be9bd2084217831c10e692e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Sep 2022 07:54:37 +0200 Subject: [PATCH 61/93] [pre-commit.ci] pre-commit autoupdate (#1809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v1.6.0 → v1.6.1](https://github.com/PyCQA/autoflake/compare/v1.6.0...v1.6.1) - [github.com/asottile/pyupgrade: v2.38.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.38.0...v2.38.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d26a933b00..e8a9047117 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: end-of-file-fixer exclude: tests/testdata - repo: https://github.com/PyCQA/autoflake - rev: v1.6.0 + rev: v1.6.1 hooks: - id: autoflake exclude: tests/testdata|astroid/__init__.py|astroid/scoped_nodes.py|astroid/node_classes.py @@ -28,7 +28,7 @@ repos: exclude: tests/testdata|setup.py types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.0 + rev: v2.38.2 hooks: - id: pyupgrade exclude: tests/testdata From ac1d9a14eadcc84e701d302e5e8dd625eb05f9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Thu, 29 Sep 2022 13:54:08 +0200 Subject: [PATCH 62/93] Create ``ContextManagerModel`` and let ``GeneratorModel`` inherit --- ChangeLog | 4 +++ astroid/interpreter/objectmodel.py | 44 +++++++++++++++++++++++++++- tests/unittest_object_model.py | 47 ++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index e5bc5dc9f2..c10ae3152c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,10 @@ Release date: TBA Closes PyCQA/pylint#7488. +* Create ``ContextManagerModel`` and let ``GeneratorModel`` inherit from it. + + Refs PyCQA/pylint#2567 + What's New in astroid 2.12.11? ============================== diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index 879ee7f256..1f41a11122 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -588,6 +588,48 @@ def attr___self__(self): attr_im_self = attr___self__ +class ContextManagerModel(ObjectModel): + """Model for context managers. + + Based on 3.3.9 of the Data Model documentation: + https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers + """ + + @property + def attr___enter__(self) -> bases.BoundMethod: + """Representation of the base implementation of __enter__. + + As per Python documentation: + Enter the runtime context related to this object. The with statement + will bind this method's return value to the target(s) specified in the + as clause of the statement, if any. + """ + node: nodes.FunctionDef = builder.extract_node("""def __enter__(self): ...""") + # We set the parent as being the ClassDef of 'object' as that + # is where this method originally comes from + node.parent = AstroidManager().builtins_module["object"] + + return bases.BoundMethod(proxy=node, bound=_get_bound_node(self)) + + @property + def attr___exit__(self) -> bases.BoundMethod: + """Representation of the base implementation of __exit__. + + As per Python documentation: + Exit the runtime context related to this object. The parameters describe the + exception that caused the context to be exited. If the context was exited + without an exception, all three arguments will be None. + """ + node: nodes.FunctionDef = builder.extract_node( + """def __exit__(self, exc_type, exc_value, traceback): ...""" + ) + # We set the parent as being the ClassDef of 'object' as that + # is where this method originally comes from + node.parent = AstroidManager().builtins_module["object"] + + return bases.BoundMethod(proxy=node, bound=_get_bound_node(self)) + + class BoundMethodModel(FunctionModel): @property def attr___func__(self): @@ -598,7 +640,7 @@ def attr___self__(self): return self._instance.bound -class GeneratorModel(FunctionModel): +class GeneratorModel(FunctionModel, ContextManagerModel): def __new__(cls, *args, **kwargs): # Append the values from the GeneratorType unto this object. ret = super().__new__(cls, *args, **kwargs) diff --git a/tests/unittest_object_model.py b/tests/unittest_object_model.py index 3dbe5026b9..9d412b7865 100644 --- a/tests/unittest_object_model.py +++ b/tests/unittest_object_model.py @@ -571,6 +571,45 @@ def test(a: 1, b: 2, /, c: 3): pass self.assertEqual(annotations.getitem(astroid.Const("c")).value, 3) +class TestContextManagerModel: + def test_model(self) -> None: + """We use a generator to test this model.""" + ast_nodes = builder.extract_node( + """ + def test(): + "a" + yield + + gen = test() + gen.__enter__ #@ + gen.__exit__ #@ + """ + ) + assert isinstance(ast_nodes, list) + + enter = next(ast_nodes[0].infer()) + assert isinstance(enter, astroid.BoundMethod) + # Test that the method is correctly bound + assert isinstance(enter.bound, bases.Generator) + assert enter.bound._proxied.qname() == "builtins.generator" + # Test that thet FunctionDef accepts no arguments except self + # NOTE: This probably shouldn't be double proxied, but this is a + # quirck of the current model implementations. + assert isinstance(enter._proxied._proxied, nodes.FunctionDef) + assert len(enter._proxied._proxied.args.args) == 1 + assert enter._proxied._proxied.args.args[0].name == "self" + + exit_node = next(ast_nodes[1].infer()) + assert isinstance(exit_node, astroid.BoundMethod) + # Test that the FunctionDef accepts the arguments as defiend in the ObjectModel + assert isinstance(exit_node._proxied._proxied, nodes.FunctionDef) + assert len(exit_node._proxied._proxied.args.args) == 4 + assert exit_node._proxied._proxied.args.args[0].name == "self" + assert exit_node._proxied._proxied.args.args[1].name == "exc_type" + assert exit_node._proxied._proxied.args.args[2].name == "exc_value" + assert exit_node._proxied._proxied.args.args[3].name == "traceback" + + class GeneratorModelTest(unittest.TestCase): def test_model(self) -> None: ast_nodes = builder.extract_node( @@ -585,6 +624,8 @@ def test(): gen.gi_code #@ gen.gi_frame #@ gen.send #@ + gen.__enter__ #@ + gen.__exit__ #@ """ ) assert isinstance(ast_nodes, list) @@ -605,6 +646,12 @@ def test(): send = next(ast_nodes[4].infer()) self.assertIsInstance(send, astroid.BoundMethod) + enter = next(ast_nodes[5].infer()) + assert isinstance(enter, astroid.BoundMethod) + + exit_node = next(ast_nodes[6].infer()) + assert isinstance(exit_node, astroid.BoundMethod) + class ExceptionModelTest(unittest.TestCase): @staticmethod From 5298ac3771aaa5ae7d4df87c619585da4fdb03b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Sat, 1 Oct 2022 21:46:44 +0200 Subject: [PATCH 63/93] Remove unnecessary ``xfail`` from tests (#1813) --- tests/unittest_inference.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index 767d2190e8..8ca10ca60f 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -3374,7 +3374,6 @@ def __radd__(self, other): return NotImplemented self.assertIsInstance(inferred, Instance) self.assertEqual(inferred.name, "B") - @pytest.mark.xfail(reason="String interpolation is incorrect for modulo formatting") def test_string_interpolation(self): ast_nodes = extract_node( """ From 1fd21c75691b876801311df3199a7f2b49824aab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 20:20:48 +0200 Subject: [PATCH 64/93] Update pytest-cov requirement from ~=3.0 to ~=4.0 (#1817) Updates the requirements on [pytest-cov](https://github.com/pytest-dev/pytest-cov) to permit the latest version. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v3.0.0...v4.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 4cd6433e0e..0313b858f6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ contributors-txt>=0.7.4 coveralls~=3.3 coverage~=6.4 pre-commit~=2.20 -pytest-cov~=3.0 +pytest-cov~=4.0 tbump~=6.9.0 types-typed-ast; implementation_name=="cpython" and python_version<"3.8" types-pkg_resources==0.1.3 From 8282563910941b1d8cda4ce3770030d611e76eb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 20:21:00 +0200 Subject: [PATCH 65/93] Bump mypy from 0.971 to 0.982 (#1816) Bumps [mypy](https://github.com/python/mypy) from 0.971 to 0.982. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.971...v0.982) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 41ce365097..154a121535 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,4 +3,4 @@ pylint==2.15.3 isort==5.10.1 flake8==5.0.4 flake8-typing-imports==1.13.0 -mypy==0.971 +mypy==0.982 From 73be77cd82a9b6e0723437348b81661dd463b79b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 20:21:13 +0200 Subject: [PATCH 66/93] Update coverage requirement from ~=6.4 to ~=6.5 (#1815) Updates the requirements on [coverage](https://github.com/nedbat/coveragepy) to permit the latest version. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.4...6.5.0) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0313b858f6..ec539a07ec 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ -r requirements_test_pre_commit.txt contributors-txt>=0.7.4 coveralls~=3.3 -coverage~=6.4 +coverage~=6.5 pre-commit~=2.20 pytest-cov~=4.0 tbump~=6.9.0 From 2413385d7c2c7b6541e099cf27f3b4e862cdb194 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 20:22:01 +0200 Subject: [PATCH 67/93] Bump actions/cache from 3.0.8 to 3.0.10 (#1814) Bumps [actions/cache](https://github.com/actions/cache) from 3.0.8 to 3.0.10. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3.0.8...v3.0.10) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ddeb0d00f1..e168fb1607 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,7 @@ jobs: 'requirements_test_brain.txt', 'requirements_test_pre_commit.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -56,7 +56,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.10 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -104,7 +104,7 @@ jobs: 'requirements_test_brain.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -150,7 +150,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.10 with: path: venv key: @@ -204,7 +204,7 @@ jobs: 'requirements_test_brain.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.10 with: path: venv key: >- @@ -248,7 +248,7 @@ jobs: hashFiles('setup.cfg', 'requirements_test_min.txt') }}" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.8 + uses: actions/cache@v3.0.10 with: path: venv key: >- From 2a1b0d346ab0a2d2c3ec6753d21eb03f83504550 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Oct 2022 09:38:43 +0200 Subject: [PATCH 68/93] [pre-commit.ci] pre-commit autoupdate (#1819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v0.971 → v0.981](https://github.com/pre-commit/mirrors-mypy/compare/v0.971...v0.981) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8a9047117..946e2d91a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: ] exclude: tests/testdata|conf.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.971 + rev: v0.981 hooks: - id: mypy name: mypy From 1ffe4001633488c8c0cf1160d98793bbe3f79da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 4 Oct 2022 11:58:19 +0200 Subject: [PATCH 69/93] Fix regression in the creation of the ``__init__`` of dataclasses (#1812) Co-authored-by: Jacob Walls --- ChangeLog | 3 ++ astroid/brain/brain_dataclasses.py | 69 +++++++++++++++++----------- astroid/nodes/node_classes.py | 69 ++++++++++++++++++++++++++++ tests/unittest_brain_dataclasses.py | 70 +++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 27 deletions(-) diff --git a/ChangeLog b/ChangeLog index c10ae3152c..1210e850c4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,7 +25,10 @@ What's New in astroid 2.12.11? ============================== Release date: TBA +* Fixed a regression in the creation of the ``__init__`` of dataclasses with + multiple inheritance. + Closes PyCQA/pylint#7434 What's New in astroid 2.12.10? ============================== diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 264957e00c..5d3c346101 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -181,9 +181,12 @@ def _find_arguments_from_base_classes( node: nodes.ClassDef, skippable_names: set[str] ) -> tuple[str, str]: """Iterate through all bases and add them to the list of arguments to add to the init.""" - prev_pos_only = "" - prev_kw_only = "" - for base in node.mro(): + pos_only_store: dict[str, tuple[str | None, str | None]] = {} + kw_only_store: dict[str, tuple[str | None, str | None]] = {} + # See TODO down below + # all_have_defaults = True + + for base in reversed(node.mro()): if not base.is_dataclass: continue try: @@ -191,29 +194,41 @@ def _find_arguments_from_base_classes( except KeyError: continue - # Skip the self argument and check for duplicate arguments - arguments = base_init.args.format_args(skippable_names=skippable_names) - try: - new_prev_pos_only, new_prev_kw_only = arguments.split("*, ") - except ValueError: - new_prev_pos_only, new_prev_kw_only = arguments, "" - - if new_prev_pos_only: - # The split on '*, ' can crete a pos_only string that consists only of a comma - if new_prev_pos_only == ", ": - new_prev_pos_only = "" - elif not new_prev_pos_only.endswith(", "): - new_prev_pos_only += ", " - - # Dataclasses put last seen arguments at the front of the init - prev_pos_only = new_prev_pos_only + prev_pos_only - prev_kw_only = new_prev_kw_only + prev_kw_only - - # Add arguments to skippable arguments - skippable_names.update(arg.name for arg in base_init.args.args) - skippable_names.update(arg.name for arg in base_init.args.kwonlyargs) - - return prev_pos_only, prev_kw_only + pos_only, kw_only = base_init.args._get_arguments_data() + for posarg, data in pos_only.items(): + if posarg in skippable_names: + continue + # if data[1] is None: + # if all_have_defaults and pos_only_store: + # # TODO: This should return an Uninferable as this would raise + # # a TypeError at runtime. However, transforms can't return + # # Uninferables currently. + # pass + # all_have_defaults = False + pos_only_store[posarg] = data + + for kwarg, data in kw_only.items(): + if kwarg in skippable_names: + continue + kw_only_store[kwarg] = data + + pos_only, kw_only = "", "" + for pos_arg, data in pos_only_store.items(): + pos_only += pos_arg + if data[0]: + pos_only += ": " + data[0] + if data[1]: + pos_only += " = " + data[1] + pos_only += ", " + for kw_arg, data in kw_only_store.items(): + kw_only += kw_arg + if data[0]: + kw_only += ": " + data[0] + if data[1]: + kw_only += " = " + data[1] + kw_only += ", " + + return pos_only, kw_only def _generate_dataclass_init( @@ -282,7 +297,7 @@ def _generate_dataclass_init( params_string += ", " if prev_kw_only: - params_string += "*, " + prev_kw_only + ", " + params_string += "*, " + prev_kw_only if kw_only_decorated: params_string += ", ".join(params) + ", " elif kw_only_decorated: diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index a4686688b6..2f515dbe90 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -834,6 +834,75 @@ def format_args(self, *, skippable_names: set[str] | None = None) -> str: result.append(f"**{self.kwarg}") return ", ".join(result) + def _get_arguments_data( + self, + ) -> tuple[ + dict[str, tuple[str | None, str | None]], + dict[str, tuple[str | None, str | None]], + ]: + """Get the arguments as dictionary with information about typing and defaults. + + The return tuple contains a dictionary for positional and keyword arguments with their typing + and their default value, if any. + The method follows a similar order as format_args but instead of formatting into a string it + returns the data that is used to do so. + """ + pos_only: dict[str, tuple[str | None, str | None]] = {} + kw_only: dict[str, tuple[str | None, str | None]] = {} + + # Setup and match defaults with arguments + positional_only_defaults = [] + positional_or_keyword_defaults = self.defaults + if self.defaults: + args = self.args or [] + positional_or_keyword_defaults = self.defaults[-len(args) :] + positional_only_defaults = self.defaults[: len(self.defaults) - len(args)] + + for index, posonly in enumerate(self.posonlyargs): + annotation, default = self.posonlyargs_annotations[index], None + if annotation is not None: + annotation = annotation.as_string() + if positional_only_defaults: + default = positional_only_defaults[index].as_string() + pos_only[posonly.name] = (annotation, default) + + for index, arg in enumerate(self.args): + annotation, default = self.annotations[index], None + if annotation is not None: + annotation = annotation.as_string() + if positional_or_keyword_defaults: + defaults_offset = len(self.args) - len(positional_or_keyword_defaults) + default_index = index - defaults_offset + if ( + default_index > -1 + and positional_or_keyword_defaults[default_index] is not None + ): + default = positional_or_keyword_defaults[default_index].as_string() + pos_only[arg.name] = (annotation, default) + + if self.vararg: + annotation = self.varargannotation + if annotation is not None: + annotation = annotation.as_string() + pos_only[self.vararg] = (annotation, None) + + for index, kwarg in enumerate(self.kwonlyargs): + annotation = self.kwonlyargs_annotations[index] + if annotation is not None: + annotation = annotation.as_string() + default = self.kw_defaults[index] + if default is not None: + default = default.as_string() + kw_only[kwarg.name] = (annotation, default) + + if self.kwarg: + annotation = self.kwargannotation + if annotation is not None: + annotation = annotation.as_string() + kw_only[self.kwarg] = (annotation, None) + + return pos_only, kw_only + def default_value(self, argname): """Get the default value for an argument. diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 7d69b35914..a65a8dec0e 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -918,6 +918,7 @@ def test_dataclass_with_multiple_inheritance() -> None: """Regression test for dataclasses with multiple inheritance. Reported in https://github.com/PyCQA/pylint/issues/7427 + Reported in https://github.com/PyCQA/pylint/issues/7434 """ first, second, overwritten, overwriting, mixed = astroid.extract_node( """ @@ -991,6 +992,75 @@ class ChildWithMixedParents(BaseParent, NotADataclassParent): assert [a.name for a in mixed_init.args.args] == ["self", "_abc", "ghi"] assert [a.value for a in mixed_init.args.defaults] == [1, 3] + first = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class BaseParent: + required: bool + + @dataclass + class FirstChild(BaseParent): + ... + + @dataclass + class SecondChild(BaseParent): + optional: bool = False + + @dataclass + class GrandChild(FirstChild, SecondChild): + ... + + GrandChild.__init__ #@ + """ + ) + + first_init: bases.UnboundMethod = next(first.infer()) + assert [a.name for a in first_init.args.args] == ["self", "required", "optional"] + assert [a.value for a in first_init.args.defaults] == [False] + + +@pytest.mark.xfail(reason="Transforms returning Uninferable isn't supported.") +def test_dataclass_non_default_argument_after_default() -> None: + """Test that a non-default argument after a default argument is not allowed. + + This should succeed, but the dataclass brain is a transform + which currently can't return an Uninferable correctly. Therefore, we can't + set the dataclass ClassDef node to be Uninferable currently. + Eventually it can be merged into test_dataclass_with_multiple_inheritance. + """ + + impossible = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class BaseParent: + required: bool + + @dataclass + class FirstChild(BaseParent): + ... + + @dataclass + class SecondChild(BaseParent): + optional: bool = False + + @dataclass + class ThirdChild: + other: bool = False + + @dataclass + class ImpossibleGrandChild(FirstChild, SecondChild, ThirdChild): + ... + + ImpossibleGrandChild() #@ + """ + ) + + assert next(impossible.infer()) is Uninferable + def test_dataclass_inits_of_non_dataclasses() -> None: """Regression test for __init__ mangling for non dataclasses. From 3d4dc50253a8cb0bf0ee8fda54c2bbea83bf9a92 Mon Sep 17 00:00:00 2001 From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> Date: Sat, 8 Oct 2022 10:30:13 +0200 Subject: [PATCH 70/93] Add ``_value2member_map_`` member to the ``enum`` brain. (#1820) Refs PyCQA/pylint#3941 --- ChangeLog | 4 ++++ astroid/brain/brain_namedtuple_enum.py | 4 ++++ tests/unittest_scoped_nodes.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/ChangeLog b/ChangeLog index 1210e850c4..e76a920774 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,10 @@ Release date: TBA Refs PyCQA/pylint#2567 +* Add ``_value2member_map_`` member to the ``enum`` brain. + + Refs PyCQA/pylint#3941 + What's New in astroid 2.12.11? ============================== diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index aa2ff3cec7..dfc9bf6833 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -425,6 +425,10 @@ def name(self): new_targets.append(fake.instantiate_class()) dunder_members[local] = fake node.locals[local] = new_targets + + # The undocumented `_value2member_map_` member: + node.locals["_value2member_map_"] = [nodes.Dict(parent=node)] + members = nodes.Dict(parent=node) members.postinit( [ diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index a11a3b9630..9e33de6ec0 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -2534,6 +2534,21 @@ class Veg(Enum): assert inferred_member_value.value is None +def test_enums_value2member_map_() -> None: + """Check the `_value2member_map_` member is present in an Enum class""" + node = builder.extract_node( + """ + from enum import Enum + class Veg(Enum): + TOMATO: 1 + + Veg + """ + ) + inferred_class = node.inferred()[0] + assert "_value2member_map_" in inferred_class.locals + + @pytest.mark.parametrize("annotation, value", [("int", 42), ("bytes", b"")]) def test_enums_type_annotation_non_str_member(annotation, value) -> None: """A type-annotated member of an Enum class where: From eafc0ff093fdd55d892d023887984617742f4daf Mon Sep 17 00:00:00 2001 From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> Date: Sat, 8 Oct 2022 10:30:13 +0200 Subject: [PATCH 71/93] Add ``_value2member_map_`` member to the ``enum`` brain. (#1820) Refs PyCQA/pylint#3941 --- ChangeLog | 4 ++++ astroid/brain/brain_namedtuple_enum.py | 4 ++++ tests/unittest_scoped_nodes.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/ChangeLog b/ChangeLog index 88b4d4fadd..fad64dc079 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ What's New in astroid 2.13.0? Release date: TBA +* Add ``_value2member_map_`` member to the ``enum`` brain. + + Refs PyCQA/pylint#3941 + What's New in astroid 2.12.11? ============================== diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index aa2ff3cec7..dfc9bf6833 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -425,6 +425,10 @@ def name(self): new_targets.append(fake.instantiate_class()) dunder_members[local] = fake node.locals[local] = new_targets + + # The undocumented `_value2member_map_` member: + node.locals["_value2member_map_"] = [nodes.Dict(parent=node)] + members = nodes.Dict(parent=node) members.postinit( [ diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index a11a3b9630..9e33de6ec0 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -2534,6 +2534,21 @@ class Veg(Enum): assert inferred_member_value.value is None +def test_enums_value2member_map_() -> None: + """Check the `_value2member_map_` member is present in an Enum class""" + node = builder.extract_node( + """ + from enum import Enum + class Veg(Enum): + TOMATO: 1 + + Veg + """ + ) + inferred_class = node.inferred()[0] + assert "_value2member_map_" in inferred_class.locals + + @pytest.mark.parametrize("annotation, value", [("int", 42), ("bytes", b"")]) def test_enums_type_annotation_non_str_member(annotation, value) -> None: """A type-annotated member of an Enum class where: From b05a51f77e399fd7849f825a545e375ebde02fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saugat=20Pachhai=20=28=E0=A4=B8=E0=A5=8C=E0=A4=97=E0=A4=BE?= =?UTF-8?q?=E0=A4=A4=29?= Date: Wed, 21 Sep 2022 17:53:50 +0545 Subject: [PATCH 72/93] improve is_namespace check See https://stackoverflow.com/a/42962529. Let's take the following contents as an example: ```python import celery.result ``` From #1777, astroid started to use `processed_components` for namespace check. In the above case, the `modname` is `celery.result`, it first checks for `celery` and then `celery.result`. Before that PR, it'd always check for `celery.result`. `celery` is recreating module to make it lazily load. See https://github.com/celery/celery/blob/34533ab44d2a6492004bc3df44dc04ad5c6611e7/celery/__init__.py#L150. This module does not have `__spec__` set. Reading through Python's docs, it seems that `__spec__` can be set to None, so it seems like it's not a thing that we can depend upon for namespace checks. See https://docs.python.org/3/reference/import.html#spec__. --- The `celery.result` gets imported for me when pylint-pytest plugin tries to load fixtures, but this could happen anytime if any plugin imports packages. In that case, `importlib.util._find_spec_from_path("celery")` will raise ValueError since it's already in `sys.modules` and does not have a spec. Fixes https://github.com/PyCQA/pylint/issues/7488. --- ChangeLog | 4 ++++ astroid/interpreter/_import/util.py | 5 ++++- tests/unittest_manager.py | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index fad64dc079..15b5d5172e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,10 @@ Release date: TBA Refs PyCQA/pylint#3941 +* Improve detection of namespace packages for the modules with ``__spec__`` set to None. + + Closes PyCQA/pylint#7488. + What's New in astroid 2.12.11? ============================== diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index c9466999ab..b5b089331e 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -52,8 +52,11 @@ def is_namespace(modname: str) -> bool: # Check first fragment of modname, e.g. "astroid", not "astroid.interpreter" # because of cffi's behavior # See: https://github.com/PyCQA/astroid/issues/1776 + mod = sys.modules[processed_components[0]] return ( - sys.modules[processed_components[0]].__spec__ is None + mod.__spec__ is None + and getattr(mod, "__file__", None) is None + and hasattr(mod, "__path__") and not IS_PYPY ) except KeyError: diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 6c13da787d..5a4e943e19 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -144,6 +144,15 @@ def test_module_unexpectedly_missing_spec(self) -> None: finally: astroid_module.__spec__ = original_spec + def test_module_unexpectedly_spec_is_none(self) -> None: + astroid_module = sys.modules["astroid"] + original_spec = astroid_module.__spec__ + astroid_module.__spec__ = None + try: + self.assertFalse(util.is_namespace("astroid")) + finally: + astroid_module.__spec__ = original_spec + def test_implicit_namespace_package(self) -> None: data_dir = os.path.dirname(resources.find("data/namespace_pep_420")) contribute = os.path.join(data_dir, "contribute_to_namespace") From f2308bc173172426ebd91f05b72dc5ca8a974fa6 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Mon, 10 Oct 2022 12:12:53 +0200 Subject: [PATCH 73/93] Bump astroid to 2.12.11, update changelog --- CONTRIBUTORS.txt | 3 ++- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- script/.contributors_aliases.json | 5 +++++ tbump.toml | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 6ade90bbad..51fae0fae0 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -23,6 +23,7 @@ Maintainers - Łukasz Rogalski - Florian Bruhin - Ashley Whetter +- Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> - Dimitri Prybysh - Areveny @@ -39,7 +40,6 @@ Contributors - David Gilman - Julien Jehannet - Calen Pennington -- Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> - Tim Martin - Phil Schaf - Hugo van Kemenade @@ -111,6 +111,7 @@ Contributors - Stanislav Levin - Simon Hewitt - Serhiy Storchaka +- Saugat Pachhai (सौगात) - Roy Wright - Robin Jarry - René Fritze <47802+renefritze@users.noreply.github.com> diff --git a/ChangeLog b/ChangeLog index 15b5d5172e..a9634a3052 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,12 +16,18 @@ Release date: TBA Closes PyCQA/pylint#7488. -What's New in astroid 2.12.11? +What's New in astroid 2.12.12? ============================== Release date: TBA +What's New in astroid 2.12.11? +============================== +Release date: 2022-10-10 + + + What's New in astroid 2.12.10? ============================== Release date: 2022-09-17 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 550130af98..a309ce859f 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.10" +__version__ = "2.12.11" version = __version__ diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 7076260223..976e2f04da 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -62,6 +62,11 @@ "name": "Dimitri Prybysh", "team": "Maintainers" }, + "31762852+mbyrnepr2@users.noreply.github.com": { + "mails": ["31762852+mbyrnepr2@users.noreply.github.com", "mbyrnepr2@gmail.com"], + "name": "Mark Byrne", + "team": "Maintainers" + }, "github@euresti.com": { "mails": ["david@dropbox.com", "github@euresti.com"], "name": "David Euresti" diff --git a/tbump.toml b/tbump.toml index 1f99bc259e..6be48869e8 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.10" +current = "2.12.11" regex = ''' ^(?P0|[1-9]\d*) \. From 76e8117545c0efb7a6247686963daa1bdab72581 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:30:36 +0200 Subject: [PATCH 74/93] Bump actions/checkout from 3.0.2 to 3.1.0 (#1824) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.0.2 to 3.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.0.2...v3.1.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/release-tests.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e168fb1607..9d786d3743 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 20 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.2.0 @@ -86,7 +86,7 @@ jobs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.2.0 @@ -142,7 +142,7 @@ jobs: COVERAGERC_FILE: .coveragerc steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.2.0 @@ -190,7 +190,7 @@ jobs: # Workaround to set correct temp directory on Windows # https://github.com/actions/virtual-environments/issues/712 - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.2.0 @@ -235,7 +235,7 @@ jobs: python-version: ["pypy3.7", "pypy3.8", "pypy3.9"] steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python uses: actions/setup-python@v4.2.0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index da5a1c9466..7f5c8f1343 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release-tests.yml b/.github/workflows/release-tests.yml index 0937cd118a..031deada83 100644 --- a/.github/workflows/release-tests.yml +++ b/.github/workflows/release-tests.yml @@ -16,7 +16,7 @@ jobs: timeout-minutes: 5 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python id: python uses: actions/setup-python@v4.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab98005f28..218a23d783 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from Github - uses: actions/checkout@v3.0.2 + uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v4.2.0 From 409e2187ed2bdcebfa8e1e1f22db1e40861f4afb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:31:06 +0200 Subject: [PATCH 75/93] Bump actions/setup-python from 4.2.0 to 4.3.0 (#1823) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4.2.0...v4.3.0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 10 +++++----- .github/workflows/release-tests.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d786d3743..3b12360795 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -89,7 +89,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Install Qt @@ -145,7 +145,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Restore Python virtual environment @@ -193,7 +193,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Generate partial Python venv restore key @@ -238,7 +238,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ matrix.python-version }} - name: Generate partial Python venv restore key diff --git a/.github/workflows/release-tests.yml b/.github/workflows/release-tests.yml index 031deada83..2ea78ff27b 100644 --- a/.github/workflows/release-tests.yml +++ b/.github/workflows/release-tests.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Create Python virtual environment with virtualenv==15.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 218a23d783..61eb5ff6f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v3.1.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.2.0 + uses: actions/setup-python@v4.3.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Install requirements From e3828870a1aee46dac137990ae152ea4639e1cc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:32:56 +0200 Subject: [PATCH 76/93] Bump pylint from 2.15.3 to 2.15.4 (#1826) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.3 to 2.15.4. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Commits](https://github.com/PyCQA/pylint/compare/v2.15.3...v2.15.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 154a121535..be31e4a9aa 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ black==22.8.0 -pylint==2.15.3 +pylint==2.15.4 isort==5.10.1 flake8==5.0.4 flake8-typing-imports==1.13.0 From b0220bdd11d3fe0a18f6d8bed2402cba6fd5dfc2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Oct 2022 19:52:52 +0200 Subject: [PATCH 77/93] Bump black from 22.8.0 to 22.10.0 (#1825) Bumps [black](https://github.com/psf/black) from 22.8.0 to 22.10.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.8.0...22.10.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Pierre Sassoulas --- requirements_test_pre_commit.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index be31e4a9aa..333de33115 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,4 +1,4 @@ -black==22.8.0 +black==22.10.0 pylint==2.15.4 isort==5.10.1 flake8==5.0.4 From c15278d7b6ed2ed2847ddbdd679615d707d8fb6a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 07:09:07 +0200 Subject: [PATCH 78/93] [pre-commit.ci] pre-commit autoupdate (#1829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/PyCQA/autoflake: v1.6.1 → v1.7.2](https://github.com/PyCQA/autoflake/compare/v1.6.1...v1.7.2) - [github.com/asottile/pyupgrade: v2.38.2 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v2.38.2...v3.1.0) - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) - [github.com/pre-commit/mirrors-mypy: v0.981 → v0.982](https://github.com/pre-commit/mirrors-mypy/compare/v0.981...v0.982) - [github.com/pre-commit/mirrors-prettier: v3.0.0-alpha.0 → v3.0.0-alpha.1](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0-alpha.0...v3.0.0-alpha.1) * Update .pre-commit-config.yaml Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pierre Sassoulas --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 946e2d91a2..a8b1176dcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: exclude: tests/testdata|setup.py types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.1.0 hooks: - id: pyupgrade exclude: tests/testdata @@ -44,7 +44,7 @@ repos: - id: black-disable-checker exclude: tests/unittest_nodes_lineno.py - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: [--safe, --quiet] @@ -71,7 +71,7 @@ repos: ] exclude: tests/testdata|conf.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.981 + rev: v0.982 hooks: - id: mypy name: mypy @@ -90,7 +90,7 @@ repos: ] exclude: tests/testdata| # exclude everything, we're not ready - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.0 + rev: v3.0.0-alpha.1 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] From 9c06593226519a690a22cf3482a5a5c72665a27a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 13 Oct 2022 22:57:42 +0200 Subject: [PATCH 79/93] Replace deprecated set-output commands [ci] (#1831) --- .github/workflows/ci.yaml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3b12360795..366e93129b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,9 +28,10 @@ jobs: - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ + echo "key=base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt', - 'requirements_test_brain.txt', 'requirements_test_pre_commit.txt') }}" + 'requirements_test_brain.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -52,8 +53,8 @@ jobs: - name: Generate pre-commit restore key id: generate-pre-commit-key run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" + echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Restore pre-commit environment id: cache-precommit uses: actions/cache@v3.0.10 @@ -99,9 +100,9 @@ jobs: - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + echo "key=venv-${{ env.CACHE_VERSION }}-${{ hashFiles('setup.cfg', 'requirements_test.txt', 'requirements_test_min.txt', - 'requirements_test_brain.txt') }}" + 'requirements_test_brain.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -199,9 +200,9 @@ jobs: - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + echo "key=venv-${{ env.CACHE_VERSION }}-${{ hashFiles('setup.cfg', 'requirements_test_min.txt', - 'requirements_test_brain.txt') }}" + 'requirements_test_brain.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 @@ -244,8 +245,8 @@ jobs: - name: Generate partial Python venv restore key id: generate-python-key run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test_min.txt') }}" + echo "key=venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('setup.cfg', 'requirements_test_min.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v3.0.10 From 2bdcd08522f5088cc565995af4e083c38e60ae6c Mon Sep 17 00:00:00 2001 From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> Date: Sat, 15 Oct 2022 10:33:02 +0200 Subject: [PATCH 80/93] Update the ``hashlib`` brain ``hash.digest`` & ``hash.hexdigest`` methods. (#1827) * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. * Move content in Changelog to a different section. Refs PyCQA/pylint#4039 --- ChangeLog | 4 ++ astroid/brain/brain_hashlib.py | 104 ++++++++++++++++++++++----------- tests/unittest_brain.py | 30 ++++++++-- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/ChangeLog b/ChangeLog index 55a6eb783b..6d4ed25a1e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,10 @@ What's New in astroid 2.12.12? ============================== Release date: TBA +* Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. + + Refs PyCQA/pylint#4039 + diff --git a/astroid/brain/brain_hashlib.py b/astroid/brain/brain_hashlib.py index b628361d8d..e321af6dc8 100644 --- a/astroid/brain/brain_hashlib.py +++ b/astroid/brain/brain_hashlib.py @@ -10,48 +10,86 @@ def _hashlib_transform(): maybe_usedforsecurity = ", usedforsecurity=True" if PY39_PLUS else "" - signature = f"value=''{maybe_usedforsecurity}" + init_signature = f"value=''{maybe_usedforsecurity}" + digest_signature = "self" + shake_digest_signature = "self, length" + template = """ - class %(name)s(object): - def __init__(self, %(signature)s): pass - def digest(self): - return %(digest)s - def copy(self): - return self - def update(self, value): pass - def hexdigest(self): - return '' - @property - def name(self): - return %(name)r - @property - def block_size(self): - return 1 - @property - def digest_size(self): - return 1 + class %(name)s: + def __init__(self, %(init_signature)s): pass + def digest(%(digest_signature)s): + return %(digest)s + def copy(self): + return self + def update(self, value): pass + def hexdigest(%(digest_signature)s): + return '' + @property + def name(self): + return %(name)r + @property + def block_size(self): + return 1 + @property + def digest_size(self): + return 1 """ + algorithms_with_signature = dict.fromkeys( - ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"], signature + [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + ], + (init_signature, digest_signature), + ) + + blake2b_signature = ( + "data=b'', *, digest_size=64, key=b'', salt=b'', " + "person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, " + f"node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" + ) + + blake2s_signature = ( + "data=b'', *, digest_size=32, key=b'', salt=b'', " + "person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, " + f"node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" ) - blake2b_signature = f"data=b'', *, digest_size=64, key=b'', salt=b'', \ - person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, \ - node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" - blake2s_signature = f"data=b'', *, digest_size=32, key=b'', salt=b'', \ - person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, \ - node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" - new_algorithms = dict.fromkeys( - ["sha3_224", "sha3_256", "sha3_384", "sha3_512", "shake_128", "shake_256"], - signature, + + shake_algorithms = dict.fromkeys( + ["shake_128", "shake_256"], + (init_signature, shake_digest_signature), ) - algorithms_with_signature.update(new_algorithms) + algorithms_with_signature.update(shake_algorithms) + algorithms_with_signature.update( - {"blake2b": blake2b_signature, "blake2s": blake2s_signature} + { + "blake2b": (blake2b_signature, digest_signature), + "blake2s": (blake2s_signature, digest_signature), + } ) + classes = "".join( - template % {"name": hashfunc, "digest": 'b""', "signature": signature} - for hashfunc, signature in algorithms_with_signature.items() + template + % { + "name": hashfunc, + "digest": 'b""', + "init_signature": init_signature, + "digest_signature": digest_signature, + } + for hashfunc, ( + init_signature, + digest_signature, + ) in algorithms_with_signature.items() ) + return parse(classes) diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 2f88fc5422..114751c8d9 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -83,24 +83,44 @@ def _assert_hashlib_class(self, class_obj: ClassDef) -> None: len(class_obj["__init__"].args.defaults), 2 if PY39_PLUS else 1 ) self.assertEqual(len(class_obj["update"].args.args), 2) - self.assertEqual(len(class_obj["digest"].args.args), 1) - self.assertEqual(len(class_obj["hexdigest"].args.args), 1) def test_hashlib(self) -> None: """Tests that brain extensions for hashlib work.""" hashlib_module = MANAGER.ast_from_module_name("hashlib") - for class_name in ("md5", "sha1"): + for class_name in ( + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + ): class_obj = hashlib_module[class_name] self._assert_hashlib_class(class_obj) + self.assertEqual(len(class_obj["digest"].args.args), 1) + self.assertEqual(len(class_obj["hexdigest"].args.args), 1) - def test_hashlib_py36(self) -> None: + def test_shake(self) -> None: + """Tests that the brain extensions for the hashlib shake algorithms work.""" hashlib_module = MANAGER.ast_from_module_name("hashlib") - for class_name in ("sha3_224", "sha3_512", "shake_128"): + for class_name in ("shake_128", "shake_256"): class_obj = hashlib_module[class_name] self._assert_hashlib_class(class_obj) + self.assertEqual(len(class_obj["digest"].args.args), 2) + self.assertEqual(len(class_obj["hexdigest"].args.args), 2) + + def test_blake2(self) -> None: + """Tests that the brain extensions for the hashlib blake2 hash functions work.""" + hashlib_module = MANAGER.ast_from_module_name("hashlib") for class_name in ("blake2b", "blake2s"): class_obj = hashlib_module[class_name] self.assertEqual(len(class_obj["__init__"].args.args), 2) + self.assertEqual(len(class_obj["digest"].args.args), 1) + self.assertEqual(len(class_obj["hexdigest"].args.args), 1) class CollectionsDequeTests(unittest.TestCase): From 05b8e8baec9f81463f6fba7df286ddcadab614aa Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 15 Oct 2022 12:33:23 -0400 Subject: [PATCH 81/93] Prevent a crash when a module's ``__path__`` is missing --- ChangeLog | 2 ++ astroid/interpreter/_import/util.py | 2 ++ tests/unittest_manager.py | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/ChangeLog b/ChangeLog index 6d4ed25a1e..618fb4ab1b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -29,7 +29,9 @@ Release date: TBA Refs PyCQA/pylint#4039 +* Prevent a crash when a module's ``__path__`` attribute is unexpectedly missing. + Refs PyCQA/pylint#7592 What's New in astroid 2.12.11? diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index b5b089331e..6cc15b5d3c 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -42,6 +42,8 @@ def is_namespace(modname: str) -> bool: found_spec = _find_spec_from_path( working_modname, path=last_submodule_search_locations ) + except AttributeError: + return False except ValueError: if modname == "__main__": return False diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 1c8a3fdc5f..2266e32ab0 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -9,6 +9,7 @@ import unittest from collections.abc import Iterator from contextlib import contextmanager +from unittest import mock import pytest @@ -144,6 +145,14 @@ def test_module_unexpectedly_missing_spec(self) -> None: finally: astroid_module.__spec__ = original_spec + @mock.patch( + "astroid.interpreter._import.util._find_spec_from_path", + side_effect=AttributeError, + ) + def test_module_unexpectedly_missing_path(self, mocked) -> None: + """https://github.com/PyCQA/pylint/issues/7592""" + self.assertFalse(util.is_namespace("astroid")) + def test_module_unexpectedly_spec_is_none(self) -> None: astroid_module = sys.modules["astroid"] original_spec = astroid_module.__spec__ From 85fe271902c8e7c11e05bc88c062142b0e6f0e2b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Oct 2022 09:17:40 +0200 Subject: [PATCH 82/93] Fix detecting invalid metaclasses (#1836) Regression from #1678 --- astroid/nodes/scoped_nodes/scoped_nodes.py | 2 +- tests/unittest_scoped_nodes.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 833da66ca1..579c58267f 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -2841,7 +2841,7 @@ def declared_metaclass( return next( node for node in self._metaclass.infer(context=context) - if isinstance(node, NodeNG) + if node is not util.Uninferable ) except (InferenceError, StopIteration): return None diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index 9e33de6ec0..2db193ccde 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -1415,6 +1415,20 @@ class Invalid(object): inferred = next(klass.infer()) self.assertIsNone(inferred.metaclass()) + @staticmethod + def test_with_invalid_metaclass(): + klass = extract_node( + """ + class InvalidAsMetaclass: ... + + class Invalid(metaclass=InvalidAsMetaclass()): #@ + pass + """ + ) + inferred = next(klass.infer()) + metaclass = inferred.metaclass() + assert isinstance(metaclass, Instance) + def test_nonregr_infer_callresult(self) -> None: astroid = builder.parse( """ From a29ee0f3fbbe3aa580280c979d3f01b0146dd00f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Oct 2022 09:17:57 +0200 Subject: [PATCH 83/93] Move changelog entry for dataclasses regression (#1835) --- ChangeLog | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 618fb4ab1b..b24bbffa54 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,11 @@ What's New in astroid 2.12.12? ============================== Release date: TBA +* Fixed a regression in the creation of the ``__init__`` of dataclasses with + multiple inheritance. + + Closes PyCQA/pylint#7434 + * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. Refs PyCQA/pylint#4039 @@ -42,10 +47,6 @@ Release date: 2022-10-10 Closes PyCQA/pylint#7488. -* Fixed a regression in the creation of the ``__init__`` of dataclasses with - multiple inheritance. - - Closes PyCQA/pylint#7434 What's New in astroid 2.12.10? ============================== From dabffad7e84c93f9d6dd8f0eb67e63a30e5e098b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:47:26 +0200 Subject: [PATCH 84/93] Fix getattr inference with empty annotation assignments (#1834) --- ChangeLog | 5 ++++ astroid/nodes/scoped_nodes/scoped_nodes.py | 35 ++++++++++++---------- tests/unittest_scoped_nodes.py | 16 ++++++++++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/ChangeLog b/ChangeLog index b24bbffa54..bbe609ec61 100644 --- a/ChangeLog +++ b/ChangeLog @@ -38,6 +38,11 @@ Release date: TBA Refs PyCQA/pylint#7592 +* Fix inferring attributes with empty annotation assignments if parent + class contains valid assignment. + + Refs PyCQA/pylint#7631 + What's New in astroid 2.12.11? ============================== diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 579c58267f..e3632d6d3e 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -46,6 +46,7 @@ from astroid.nodes.scoped_nodes.mixin import ComprehensionScope, LocalsDictNodeNG from astroid.nodes.scoped_nodes.utils import builtin_lookup from astroid.nodes.utils import Position +from astroid.typing import InferenceResult if sys.version_info >= (3, 8): from functools import cached_property @@ -2525,7 +2526,12 @@ def instantiate_class(self) -> bases.Instance: pass return bases.Instance(self) - def getattr(self, name, context=None, class_context=True): + def getattr( + self, + name: str, + context: InferenceContext | None = None, + class_context: bool = True, + ) -> list[NodeNG]: """Get an attribute from this class, using Python's attribute semantic. This method doesn't look in the :attr:`instance_attrs` dictionary @@ -2541,13 +2547,10 @@ def getattr(self, name, context=None, class_context=True): metaclass will be done. :param name: The attribute to look for. - :type name: str :param class_context: Whether the attribute can be accessed statically. - :type class_context: bool :returns: The attribute. - :rtype: list(NodeNG) :raises AttributeInferenceError: If the attribute cannot be inferred. """ @@ -2570,17 +2573,16 @@ def getattr(self, name, context=None, class_context=True): if class_context: values += self._metaclass_lookup_attribute(name, context) - if not values: - raise AttributeInferenceError(target=self, attribute=name, context=context) - - # Look for AnnAssigns, which are not attributes in the purest sense. - for value in values: + # Remove AnnAssigns without value, which are not attributes in the purest sense. + for value in values.copy(): if isinstance(value, node_classes.AssignName): stmt = value.statement(future=True) if isinstance(stmt, node_classes.AnnAssign) and stmt.value is None: - raise AttributeInferenceError( - target=self, attribute=name, context=context - ) + values.pop(values.index(value)) + + if not values: + raise AttributeInferenceError(target=self, attribute=name, context=context) + return values def _metaclass_lookup_attribute(self, name, context): @@ -2622,14 +2624,17 @@ def _get_attribute_from_metaclass(self, cls, name, context): else: yield bases.BoundMethod(attr, self) - def igetattr(self, name, context=None, class_context=True): + def igetattr( + self, + name: str, + context: InferenceContext | None = None, + class_context: bool = True, + ) -> Iterator[InferenceResult]: """Infer the possible values of the given variable. :param name: The name of the variable to infer. - :type name: str :returns: The inferred possible values. - :rtype: iterable(NodeNG or Uninferable) """ # set lookup name since this is necessary to infer on import nodes for # instance diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index 2db193ccde..2a37a8ad7e 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -1268,6 +1268,22 @@ class Past(Present): self.assertIsInstance(attr1, nodes.AssignName) self.assertEqual(attr1.name, "attr") + @staticmethod + def test_getattr_with_enpty_annassign() -> None: + code = """ + class Parent: + attr: int = 2 + + class Child(Parent): #@ + attr: int + """ + child = extract_node(code) + attr = child.getattr("attr") + assert len(attr) == 1 + assert isinstance(attr[0], nodes.AssignName) + assert attr[0].name == "attr" + assert attr[0].lineno == 3 + def test_function_with_decorator_lineno(self) -> None: data = """ @f(a=2, From 3ee1ee565a493bf1a7952d8ee25ed9777c524ca3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 19:40:09 +0200 Subject: [PATCH 85/93] Update sphinx requirement from ~=5.2 to ~=5.3 (#1838) Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v5.2.0...v5.3.0) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 36d03c8341..3033b17ba7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,2 +1,2 @@ -e . -sphinx~=5.2 +sphinx~=5.3 From aae90e6480c3fb9da95447587e639af6ad297689 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 19:40:51 +0200 Subject: [PATCH 86/93] Bump actions/cache from 3.0.10 to 3.0.11 (#1839) Bumps [actions/cache](https://github.com/actions/cache) from 3.0.10 to 3.0.11. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3.0.10...v3.0.11) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 366e93129b..74f4ecc6d4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -57,7 +57,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -105,7 +105,7 @@ jobs: 'requirements_test_brain.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -151,7 +151,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: @@ -205,7 +205,7 @@ jobs: 'requirements_test_brain.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- @@ -249,7 +249,7 @@ jobs: hashFiles('setup.cfg', 'requirements_test_min.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.0.10 + uses: actions/cache@v3.0.11 with: path: venv key: >- From 7ed8c6db48faaab02eb57c94659aa54e5c9c3aa1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 08:52:23 +0200 Subject: [PATCH 87/93] [pre-commit.ci] pre-commit autoupdate (#1840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/PyCQA/autoflake: v1.6.1 → v1.7.6](https://github.com/PyCQA/autoflake/compare/v1.6.1...v1.7.6) - [github.com/pre-commit/mirrors-prettier: v3.0.0-alpha.1 → v3.0.0-alpha.2](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0-alpha.1...v3.0.0-alpha.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8b1176dcf..b5f67fb354 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: end-of-file-fixer exclude: tests/testdata - repo: https://github.com/PyCQA/autoflake - rev: v1.6.1 + rev: v1.7.6 hooks: - id: autoflake exclude: tests/testdata|astroid/__init__.py|astroid/scoped_nodes.py|astroid/node_classes.py @@ -90,7 +90,7 @@ repos: ] exclude: tests/testdata| # exclude everything, we're not ready - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.0-alpha.1 + rev: v3.0.0-alpha.2 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] From db46a700d253b620404ca46500ab0196c61bc486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Tue, 4 Oct 2022 11:58:19 +0200 Subject: [PATCH 88/93] Fix regression in the creation of the ``__init__`` of dataclasses (#1812) Co-authored-by: Jacob Walls --- ChangeLog | 3 ++ astroid/brain/brain_dataclasses.py | 69 +++++++++++++++++----------- astroid/nodes/node_classes.py | 69 ++++++++++++++++++++++++++++ tests/unittest_brain_dataclasses.py | 70 +++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 27 deletions(-) diff --git a/ChangeLog b/ChangeLog index a9634a3052..6306670ccb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -26,7 +26,10 @@ What's New in astroid 2.12.11? ============================== Release date: 2022-10-10 +* Fixed a regression in the creation of the ``__init__`` of dataclasses with + multiple inheritance. + Closes PyCQA/pylint#7434 What's New in astroid 2.12.10? ============================== diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 264957e00c..5d3c346101 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -181,9 +181,12 @@ def _find_arguments_from_base_classes( node: nodes.ClassDef, skippable_names: set[str] ) -> tuple[str, str]: """Iterate through all bases and add them to the list of arguments to add to the init.""" - prev_pos_only = "" - prev_kw_only = "" - for base in node.mro(): + pos_only_store: dict[str, tuple[str | None, str | None]] = {} + kw_only_store: dict[str, tuple[str | None, str | None]] = {} + # See TODO down below + # all_have_defaults = True + + for base in reversed(node.mro()): if not base.is_dataclass: continue try: @@ -191,29 +194,41 @@ def _find_arguments_from_base_classes( except KeyError: continue - # Skip the self argument and check for duplicate arguments - arguments = base_init.args.format_args(skippable_names=skippable_names) - try: - new_prev_pos_only, new_prev_kw_only = arguments.split("*, ") - except ValueError: - new_prev_pos_only, new_prev_kw_only = arguments, "" - - if new_prev_pos_only: - # The split on '*, ' can crete a pos_only string that consists only of a comma - if new_prev_pos_only == ", ": - new_prev_pos_only = "" - elif not new_prev_pos_only.endswith(", "): - new_prev_pos_only += ", " - - # Dataclasses put last seen arguments at the front of the init - prev_pos_only = new_prev_pos_only + prev_pos_only - prev_kw_only = new_prev_kw_only + prev_kw_only - - # Add arguments to skippable arguments - skippable_names.update(arg.name for arg in base_init.args.args) - skippable_names.update(arg.name for arg in base_init.args.kwonlyargs) - - return prev_pos_only, prev_kw_only + pos_only, kw_only = base_init.args._get_arguments_data() + for posarg, data in pos_only.items(): + if posarg in skippable_names: + continue + # if data[1] is None: + # if all_have_defaults and pos_only_store: + # # TODO: This should return an Uninferable as this would raise + # # a TypeError at runtime. However, transforms can't return + # # Uninferables currently. + # pass + # all_have_defaults = False + pos_only_store[posarg] = data + + for kwarg, data in kw_only.items(): + if kwarg in skippable_names: + continue + kw_only_store[kwarg] = data + + pos_only, kw_only = "", "" + for pos_arg, data in pos_only_store.items(): + pos_only += pos_arg + if data[0]: + pos_only += ": " + data[0] + if data[1]: + pos_only += " = " + data[1] + pos_only += ", " + for kw_arg, data in kw_only_store.items(): + kw_only += kw_arg + if data[0]: + kw_only += ": " + data[0] + if data[1]: + kw_only += " = " + data[1] + kw_only += ", " + + return pos_only, kw_only def _generate_dataclass_init( @@ -282,7 +297,7 @@ def _generate_dataclass_init( params_string += ", " if prev_kw_only: - params_string += "*, " + prev_kw_only + ", " + params_string += "*, " + prev_kw_only if kw_only_decorated: params_string += ", ".join(params) + ", " elif kw_only_decorated: diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index caa79d093e..cccfa14cb4 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -834,6 +834,75 @@ def format_args(self, *, skippable_names: set[str] | None = None) -> str: result.append(f"**{self.kwarg}") return ", ".join(result) + def _get_arguments_data( + self, + ) -> tuple[ + dict[str, tuple[str | None, str | None]], + dict[str, tuple[str | None, str | None]], + ]: + """Get the arguments as dictionary with information about typing and defaults. + + The return tuple contains a dictionary for positional and keyword arguments with their typing + and their default value, if any. + The method follows a similar order as format_args but instead of formatting into a string it + returns the data that is used to do so. + """ + pos_only: dict[str, tuple[str | None, str | None]] = {} + kw_only: dict[str, tuple[str | None, str | None]] = {} + + # Setup and match defaults with arguments + positional_only_defaults = [] + positional_or_keyword_defaults = self.defaults + if self.defaults: + args = self.args or [] + positional_or_keyword_defaults = self.defaults[-len(args) :] + positional_only_defaults = self.defaults[: len(self.defaults) - len(args)] + + for index, posonly in enumerate(self.posonlyargs): + annotation, default = self.posonlyargs_annotations[index], None + if annotation is not None: + annotation = annotation.as_string() + if positional_only_defaults: + default = positional_only_defaults[index].as_string() + pos_only[posonly.name] = (annotation, default) + + for index, arg in enumerate(self.args): + annotation, default = self.annotations[index], None + if annotation is not None: + annotation = annotation.as_string() + if positional_or_keyword_defaults: + defaults_offset = len(self.args) - len(positional_or_keyword_defaults) + default_index = index - defaults_offset + if ( + default_index > -1 + and positional_or_keyword_defaults[default_index] is not None + ): + default = positional_or_keyword_defaults[default_index].as_string() + pos_only[arg.name] = (annotation, default) + + if self.vararg: + annotation = self.varargannotation + if annotation is not None: + annotation = annotation.as_string() + pos_only[self.vararg] = (annotation, None) + + for index, kwarg in enumerate(self.kwonlyargs): + annotation = self.kwonlyargs_annotations[index] + if annotation is not None: + annotation = annotation.as_string() + default = self.kw_defaults[index] + if default is not None: + default = default.as_string() + kw_only[kwarg.name] = (annotation, default) + + if self.kwarg: + annotation = self.kwargannotation + if annotation is not None: + annotation = annotation.as_string() + kw_only[self.kwarg] = (annotation, None) + + return pos_only, kw_only + def default_value(self, argname): """Get the default value for an argument. diff --git a/tests/unittest_brain_dataclasses.py b/tests/unittest_brain_dataclasses.py index 7d69b35914..a65a8dec0e 100644 --- a/tests/unittest_brain_dataclasses.py +++ b/tests/unittest_brain_dataclasses.py @@ -918,6 +918,7 @@ def test_dataclass_with_multiple_inheritance() -> None: """Regression test for dataclasses with multiple inheritance. Reported in https://github.com/PyCQA/pylint/issues/7427 + Reported in https://github.com/PyCQA/pylint/issues/7434 """ first, second, overwritten, overwriting, mixed = astroid.extract_node( """ @@ -991,6 +992,75 @@ class ChildWithMixedParents(BaseParent, NotADataclassParent): assert [a.name for a in mixed_init.args.args] == ["self", "_abc", "ghi"] assert [a.value for a in mixed_init.args.defaults] == [1, 3] + first = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class BaseParent: + required: bool + + @dataclass + class FirstChild(BaseParent): + ... + + @dataclass + class SecondChild(BaseParent): + optional: bool = False + + @dataclass + class GrandChild(FirstChild, SecondChild): + ... + + GrandChild.__init__ #@ + """ + ) + + first_init: bases.UnboundMethod = next(first.infer()) + assert [a.name for a in first_init.args.args] == ["self", "required", "optional"] + assert [a.value for a in first_init.args.defaults] == [False] + + +@pytest.mark.xfail(reason="Transforms returning Uninferable isn't supported.") +def test_dataclass_non_default_argument_after_default() -> None: + """Test that a non-default argument after a default argument is not allowed. + + This should succeed, but the dataclass brain is a transform + which currently can't return an Uninferable correctly. Therefore, we can't + set the dataclass ClassDef node to be Uninferable currently. + Eventually it can be merged into test_dataclass_with_multiple_inheritance. + """ + + impossible = astroid.extract_node( + """ + from dataclasses import dataclass + + @dataclass + class BaseParent: + required: bool + + @dataclass + class FirstChild(BaseParent): + ... + + @dataclass + class SecondChild(BaseParent): + optional: bool = False + + @dataclass + class ThirdChild: + other: bool = False + + @dataclass + class ImpossibleGrandChild(FirstChild, SecondChild, ThirdChild): + ... + + ImpossibleGrandChild() #@ + """ + ) + + assert next(impossible.infer()) is Uninferable + def test_dataclass_inits_of_non_dataclasses() -> None: """Regression test for __init__ mangling for non dataclasses. From fccbc9705433f83826c896049e05b9deae62208f Mon Sep 17 00:00:00 2001 From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> Date: Sat, 15 Oct 2022 10:33:02 +0200 Subject: [PATCH 89/93] Update the ``hashlib`` brain ``hash.digest`` & ``hash.hexdigest`` methods. (#1827) * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. * Move content in Changelog to a different section. Refs PyCQA/pylint#4039 --- ChangeLog | 4 ++ astroid/brain/brain_hashlib.py | 104 ++++++++++++++++++++++----------- tests/unittest_brain.py | 30 ++++++++-- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6306670ccb..31f12cc201 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,10 @@ What's New in astroid 2.12.12? ============================== Release date: TBA +* Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. + + Refs PyCQA/pylint#4039 + What's New in astroid 2.12.11? diff --git a/astroid/brain/brain_hashlib.py b/astroid/brain/brain_hashlib.py index b628361d8d..e321af6dc8 100644 --- a/astroid/brain/brain_hashlib.py +++ b/astroid/brain/brain_hashlib.py @@ -10,48 +10,86 @@ def _hashlib_transform(): maybe_usedforsecurity = ", usedforsecurity=True" if PY39_PLUS else "" - signature = f"value=''{maybe_usedforsecurity}" + init_signature = f"value=''{maybe_usedforsecurity}" + digest_signature = "self" + shake_digest_signature = "self, length" + template = """ - class %(name)s(object): - def __init__(self, %(signature)s): pass - def digest(self): - return %(digest)s - def copy(self): - return self - def update(self, value): pass - def hexdigest(self): - return '' - @property - def name(self): - return %(name)r - @property - def block_size(self): - return 1 - @property - def digest_size(self): - return 1 + class %(name)s: + def __init__(self, %(init_signature)s): pass + def digest(%(digest_signature)s): + return %(digest)s + def copy(self): + return self + def update(self, value): pass + def hexdigest(%(digest_signature)s): + return '' + @property + def name(self): + return %(name)r + @property + def block_size(self): + return 1 + @property + def digest_size(self): + return 1 """ + algorithms_with_signature = dict.fromkeys( - ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"], signature + [ + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + ], + (init_signature, digest_signature), + ) + + blake2b_signature = ( + "data=b'', *, digest_size=64, key=b'', salt=b'', " + "person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, " + f"node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" + ) + + blake2s_signature = ( + "data=b'', *, digest_size=32, key=b'', salt=b'', " + "person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, " + f"node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" ) - blake2b_signature = f"data=b'', *, digest_size=64, key=b'', salt=b'', \ - person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, \ - node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" - blake2s_signature = f"data=b'', *, digest_size=32, key=b'', salt=b'', \ - person=b'', fanout=1, depth=1, leaf_size=0, node_offset=0, \ - node_depth=0, inner_size=0, last_node=False{maybe_usedforsecurity}" - new_algorithms = dict.fromkeys( - ["sha3_224", "sha3_256", "sha3_384", "sha3_512", "shake_128", "shake_256"], - signature, + + shake_algorithms = dict.fromkeys( + ["shake_128", "shake_256"], + (init_signature, shake_digest_signature), ) - algorithms_with_signature.update(new_algorithms) + algorithms_with_signature.update(shake_algorithms) + algorithms_with_signature.update( - {"blake2b": blake2b_signature, "blake2s": blake2s_signature} + { + "blake2b": (blake2b_signature, digest_signature), + "blake2s": (blake2s_signature, digest_signature), + } ) + classes = "".join( - template % {"name": hashfunc, "digest": 'b""', "signature": signature} - for hashfunc, signature in algorithms_with_signature.items() + template + % { + "name": hashfunc, + "digest": 'b""', + "init_signature": init_signature, + "digest_signature": digest_signature, + } + for hashfunc, ( + init_signature, + digest_signature, + ) in algorithms_with_signature.items() ) + return parse(classes) diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 07d17c4ceb..d6d4ef5aad 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -83,24 +83,44 @@ def _assert_hashlib_class(self, class_obj: ClassDef) -> None: len(class_obj["__init__"].args.defaults), 2 if PY39_PLUS else 1 ) self.assertEqual(len(class_obj["update"].args.args), 2) - self.assertEqual(len(class_obj["digest"].args.args), 1) - self.assertEqual(len(class_obj["hexdigest"].args.args), 1) def test_hashlib(self) -> None: """Tests that brain extensions for hashlib work.""" hashlib_module = MANAGER.ast_from_module_name("hashlib") - for class_name in ("md5", "sha1"): + for class_name in ( + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + ): class_obj = hashlib_module[class_name] self._assert_hashlib_class(class_obj) + self.assertEqual(len(class_obj["digest"].args.args), 1) + self.assertEqual(len(class_obj["hexdigest"].args.args), 1) - def test_hashlib_py36(self) -> None: + def test_shake(self) -> None: + """Tests that the brain extensions for the hashlib shake algorithms work.""" hashlib_module = MANAGER.ast_from_module_name("hashlib") - for class_name in ("sha3_224", "sha3_512", "shake_128"): + for class_name in ("shake_128", "shake_256"): class_obj = hashlib_module[class_name] self._assert_hashlib_class(class_obj) + self.assertEqual(len(class_obj["digest"].args.args), 2) + self.assertEqual(len(class_obj["hexdigest"].args.args), 2) + + def test_blake2(self) -> None: + """Tests that the brain extensions for the hashlib blake2 hash functions work.""" + hashlib_module = MANAGER.ast_from_module_name("hashlib") for class_name in ("blake2b", "blake2s"): class_obj = hashlib_module[class_name] self.assertEqual(len(class_obj["__init__"].args.args), 2) + self.assertEqual(len(class_obj["digest"].args.args), 1) + self.assertEqual(len(class_obj["hexdigest"].args.args), 1) class CollectionsDequeTests(unittest.TestCase): From a97d958ee71a78b1f963f8693620d9b4fee2c9ac Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sat, 15 Oct 2022 12:33:23 -0400 Subject: [PATCH 90/93] Prevent a crash when a module's ``__path__`` is missing --- ChangeLog | 2 ++ astroid/interpreter/_import/util.py | 2 ++ tests/unittest_manager.py | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/ChangeLog b/ChangeLog index 31f12cc201..052b1397ed 100644 --- a/ChangeLog +++ b/ChangeLog @@ -24,7 +24,9 @@ Release date: TBA Refs PyCQA/pylint#4039 +* Prevent a crash when a module's ``__path__`` attribute is unexpectedly missing. + Refs PyCQA/pylint#7592 What's New in astroid 2.12.11? ============================== diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py index b5b089331e..6cc15b5d3c 100644 --- a/astroid/interpreter/_import/util.py +++ b/astroid/interpreter/_import/util.py @@ -42,6 +42,8 @@ def is_namespace(modname: str) -> bool: found_spec = _find_spec_from_path( working_modname, path=last_submodule_search_locations ) + except AttributeError: + return False except ValueError: if modname == "__main__": return False diff --git a/tests/unittest_manager.py b/tests/unittest_manager.py index 5a4e943e19..a2466923c2 100644 --- a/tests/unittest_manager.py +++ b/tests/unittest_manager.py @@ -9,6 +9,7 @@ import unittest from collections.abc import Iterator from contextlib import contextmanager +from unittest import mock import pytest @@ -144,6 +145,14 @@ def test_module_unexpectedly_missing_spec(self) -> None: finally: astroid_module.__spec__ = original_spec + @mock.patch( + "astroid.interpreter._import.util._find_spec_from_path", + side_effect=AttributeError, + ) + def test_module_unexpectedly_missing_path(self, mocked) -> None: + """https://github.com/PyCQA/pylint/issues/7592""" + self.assertFalse(util.is_namespace("astroid")) + def test_module_unexpectedly_spec_is_none(self) -> None: astroid_module = sys.modules["astroid"] original_spec = astroid_module.__spec__ From a0ee56ca03f2f4fff277ed8899d2520932975892 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Oct 2022 09:17:57 +0200 Subject: [PATCH 91/93] Move changelog entry for dataclasses regression (#1835) --- ChangeLog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ChangeLog b/ChangeLog index 052b1397ed..d057957a55 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,8 @@ What's New in astroid 2.12.12? ============================== Release date: TBA + + * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. Refs PyCQA/pylint#4039 @@ -32,11 +34,16 @@ What's New in astroid 2.12.11? ============================== Release date: 2022-10-10 +* Improve detection of namespace packages for the modules with ``__spec__`` set to None. + + Closes PyCQA/pylint#7488. + * Fixed a regression in the creation of the ``__init__`` of dataclasses with multiple inheritance. Closes PyCQA/pylint#7434 + What's New in astroid 2.12.10? ============================== Release date: 2022-09-17 From 19623e71f4a64c86499ffa052c7b2a67a8cdab77 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Oct 2022 11:47:26 +0200 Subject: [PATCH 92/93] Fix getattr inference with empty annotation assignments (#1834) --- ChangeLog | 8 +++-- astroid/nodes/scoped_nodes/scoped_nodes.py | 35 ++++++++++++---------- tests/unittest_scoped_nodes.py | 16 ++++++++++ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/ChangeLog b/ChangeLog index d057957a55..4f63f145e4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,8 +20,6 @@ What's New in astroid 2.12.12? ============================== Release date: TBA - - * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. Refs PyCQA/pylint#4039 @@ -30,6 +28,12 @@ Release date: TBA Refs PyCQA/pylint#7592 +* Fix inferring attributes with empty annotation assignments if parent + class contains valid assignment. + + Refs PyCQA/pylint#7631 + + What's New in astroid 2.12.11? ============================== Release date: 2022-10-10 diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index f3fc3c100f..475ae46b00 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -46,6 +46,7 @@ from astroid.nodes.scoped_nodes.mixin import ComprehensionScope, LocalsDictNodeNG from astroid.nodes.scoped_nodes.utils import builtin_lookup from astroid.nodes.utils import Position +from astroid.typing import InferenceResult if sys.version_info >= (3, 8): from functools import cached_property @@ -2519,7 +2520,12 @@ def instantiate_class(self) -> bases.Instance: pass return bases.Instance(self) - def getattr(self, name, context=None, class_context=True): + def getattr( + self, + name: str, + context: InferenceContext | None = None, + class_context: bool = True, + ) -> list[NodeNG]: """Get an attribute from this class, using Python's attribute semantic. This method doesn't look in the :attr:`instance_attrs` dictionary @@ -2535,13 +2541,10 @@ def getattr(self, name, context=None, class_context=True): metaclass will be done. :param name: The attribute to look for. - :type name: str :param class_context: Whether the attribute can be accessed statically. - :type class_context: bool :returns: The attribute. - :rtype: list(NodeNG) :raises AttributeInferenceError: If the attribute cannot be inferred. """ @@ -2564,17 +2567,16 @@ def getattr(self, name, context=None, class_context=True): if class_context: values += self._metaclass_lookup_attribute(name, context) - if not values: - raise AttributeInferenceError(target=self, attribute=name, context=context) - - # Look for AnnAssigns, which are not attributes in the purest sense. - for value in values: + # Remove AnnAssigns without value, which are not attributes in the purest sense. + for value in values.copy(): if isinstance(value, node_classes.AssignName): stmt = value.statement(future=True) if isinstance(stmt, node_classes.AnnAssign) and stmt.value is None: - raise AttributeInferenceError( - target=self, attribute=name, context=context - ) + values.pop(values.index(value)) + + if not values: + raise AttributeInferenceError(target=self, attribute=name, context=context) + return values def _metaclass_lookup_attribute(self, name, context): @@ -2616,14 +2618,17 @@ def _get_attribute_from_metaclass(self, cls, name, context): else: yield bases.BoundMethod(attr, self) - def igetattr(self, name, context=None, class_context=True): + def igetattr( + self, + name: str, + context: InferenceContext | None = None, + class_context: bool = True, + ) -> Iterator[InferenceResult]: """Infer the possible values of the given variable. :param name: The name of the variable to infer. - :type name: str :returns: The inferred possible values. - :rtype: iterable(NodeNG or Uninferable) """ # set lookup name since this is necessary to infer on import nodes for # instance diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index 9e33de6ec0..f3b4288ef7 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -1268,6 +1268,22 @@ class Past(Present): self.assertIsInstance(attr1, nodes.AssignName) self.assertEqual(attr1.name, "attr") + @staticmethod + def test_getattr_with_enpty_annassign() -> None: + code = """ + class Parent: + attr: int = 2 + + class Child(Parent): #@ + attr: int + """ + child = extract_node(code) + attr = child.getattr("attr") + assert len(attr) == 1 + assert isinstance(attr[0], nodes.AssignName) + assert attr[0].name == "attr" + assert attr[0].lineno == 3 + def test_function_with_decorator_lineno(self) -> None: data = """ @f(a=2, From 52f6d2d7722db383af035be929f18af5e9fe8cd5 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Wed, 19 Oct 2022 10:35:39 +0200 Subject: [PATCH 93/93] Bump astroid to 2.12.12, update changelog --- ChangeLog | 8 +++++++- astroid/__pkginfo__.py | 2 +- script/.contributors_aliases.json | 10 +++++----- tbump.toml | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 4f63f145e4..faccd14564 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,10 +16,16 @@ Release date: TBA Closes PyCQA/pylint#7488. -What's New in astroid 2.12.12? +What's New in astroid 2.12.13? ============================== Release date: TBA + + +What's New in astroid 2.12.12? +============================== +Release date: 2022-10-19 + * Add the ``length`` parameter to ``hash.digest`` & ``hash.hexdigest`` in the ``hashlib`` brain. Refs PyCQA/pylint#4039 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index a309ce859f..0d925a50eb 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "2.12.11" +__version__ = "2.12.12" version = __version__ diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 976e2f04da..061e77e5de 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -13,6 +13,11 @@ "name": "Marc Mueller", "team": "Maintainers" }, + "31762852+mbyrnepr2@users.noreply.github.com": { + "mails": ["31762852+mbyrnepr2@users.noreply.github.com", "mbyrnepr2@gmail.com"], + "name": "Mark Byrne", + "team": "Maintainers" + }, "adam.grant.hendry@gmail.com": { "mails": ["adam.grant.hendry@gmail.com"], "name": "Adam Hendry" @@ -62,11 +67,6 @@ "name": "Dimitri Prybysh", "team": "Maintainers" }, - "31762852+mbyrnepr2@users.noreply.github.com": { - "mails": ["31762852+mbyrnepr2@users.noreply.github.com", "mbyrnepr2@gmail.com"], - "name": "Mark Byrne", - "team": "Maintainers" - }, "github@euresti.com": { "mails": ["david@dropbox.com", "github@euresti.com"], "name": "David Euresti" diff --git a/tbump.toml b/tbump.toml index 6be48869e8..65cf8b8c27 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/PyCQA/astroid" [version] -current = "2.12.11" +current = "2.12.12" regex = ''' ^(?P0|[1-9]\d*) \.