diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 81617987c8..438fe64fdf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,10 +24,10 @@ jobs: timeout-minutes: 20 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -39,7 +39,7 @@ jobs: 'requirements_full.txt', 'requirements_minimal.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -59,7 +59,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Restore pre-commit environment id: cache-precommit - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -86,10 +86,10 @@ jobs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -106,7 +106,7 @@ jobs: 'requirements_full.txt', 'requirements_minimal.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -145,10 +145,10 @@ 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@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -160,7 +160,7 @@ jobs: 'requirements_full.txt', 'requirements_minimal.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -195,10 +195,10 @@ jobs: python-version: ["pypy3.8", "pypy3.10"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -210,7 +210,7 @@ jobs: }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -241,17 +241,17 @@ jobs: needs: ["tests-linux", "tests-windows", "tests-pypy"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python 3.12 id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: "3.12" check-latest: true - name: Install dependencies run: pip install -U -r requirements_minimal.txt - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.2 + uses: actions/download-artifact@v4.1.4 - name: Combine Linux coverage results run: | coverage combine coverage-linux*/.coverage diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0708610253..b0c4782752 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release-tests.yml b/.github/workflows/release-tests.yml index 62a5902508..830ea56885 100644 --- a/.github/workflows/release-tests.yml +++ b/.github/workflows/release-tests.yml @@ -13,10 +13,10 @@ jobs: timeout-minutes: 5 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python 3.9 id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: # virtualenv 15.1.0 cannot be installed on Python 3.10+ python-version: 3.9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e209c427fd..e5a6b9ad97 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,10 +20,10 @@ jobs: url: https://pypi.org/project/astroid/ steps: - name: Check out code from Github - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1fa8cb710..af5c863daa 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/astral-sh/ruff-pre-commit - rev: "v0.2.1" + rev: "v0.3.4" hooks: - id: ruff exclude: tests/testdata @@ -23,7 +23,7 @@ repos: exclude: tests/testdata|setup.py types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.2 hooks: - id: pyupgrade exclude: tests/testdata @@ -34,7 +34,7 @@ repos: - id: black-disable-checker exclude: tests/test_nodes_lineno.py - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black args: [--safe, --quiet] @@ -54,7 +54,7 @@ repos: ] exclude: tests/testdata|conf.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.9.0 hooks: - id: mypy name: mypy diff --git a/ChangeLog b/ChangeLog index 019cdb8efa..101f51f47a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,10 +7,23 @@ What's New in astroid 3.2.0? ============================ Release date: TBA + * ``.pyi`` stub files are now preferred over ``.py`` files when resolving imports. Closes pylint-dev/#9185 +* ``igetattr()`` returns the last same-named function in a class (instead of + the first). This avoids false positives in pylint with ``@overload``. + + Closes #1015 + Refs pylint-dev/pylint#4696 + +* Adds ``module_denylist`` to ``AstroidManager`` for modules to be skipped during AST + generation. Modules in this list will cause an ``AstroidImportError`` to be raised + when an AST for them is requested. + + Refs pylint-dev/pylint#9442 + What's New in astroid 3.1.1? ============================ diff --git a/astroid/manager.py b/astroid/manager.py index c499fe5598..a7a51f19c5 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -59,6 +59,7 @@ class AstroidManager: "optimize_ast": False, "max_inferable_values": 100, "extension_package_whitelist": set(), + "module_denylist": set(), "_transform": TransformVisitor(), } @@ -70,6 +71,7 @@ def __init__(self) -> None: self.extension_package_whitelist = AstroidManager.brain[ "extension_package_whitelist" ] + self.module_denylist = AstroidManager.brain["module_denylist"] self._transform = AstroidManager.brain["_transform"] @property @@ -200,6 +202,8 @@ def ast_from_module_name( # noqa: C901 # 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 # module. + if modname in self.module_denylist: + raise AstroidImportError(f"Skipping ignored module {modname!r}") if modname in self.astroid_cache and use_cache: return self.astroid_cache[modname] if modname == "__main__": @@ -305,7 +309,6 @@ def file_from_module_name( modname.split("."), context_file=contextfile ) except ImportError as e: - # pylint: disable-next=redefined-variable-type value = AstroidImportError( "Failed to import module {modname} with error:\n{error}.", modname=modname, @@ -402,8 +405,7 @@ def infer_ast_from_something( # take care, on living object __module__ is regularly wrong :( modastroid = self.ast_from_module_name(modname) if klass is obj: - for inferred in modastroid.igetattr(name, context): - yield inferred + yield from modastroid.igetattr(name, context) else: for inferred in modastroid.igetattr(name, context): yield inferred.instantiate_class() diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 9cda4f1be0..79b7643e55 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -2508,12 +2508,21 @@ def igetattr( # to the attribute happening *after* the attribute's definition (e.g. AugAssigns on lists) if len(attributes) > 1: first_attr, attributes = attributes[0], attributes[1:] - first_scope = first_attr.scope() + first_scope = first_attr.parent.scope() attributes = [first_attr] + [ attr for attr in attributes if attr.parent and attr.parent.scope() == first_scope ] + functions = [attr for attr in attributes if isinstance(attr, FunctionDef)] + if functions: + # Prefer only the last function, unless a property is involved. + last_function = functions[-1] + attributes = [ + a + for a in attributes + if a not in functions or a is last_function or bases._is_property(a) + ] for inferred in bases._infer_stmts(attributes, context, frame=self): # yield Uninferable object instead of descriptors when necessary diff --git a/astroid/test_utils.py b/astroid/test_utils.py index 1119cd093f..afddb1a4ff 100644 --- a/astroid/test_utils.py +++ b/astroid/test_utils.py @@ -74,4 +74,5 @@ def brainless_manager(): m._mod_file_cache = {} m._transform = transforms.TransformVisitor() m.extension_package_whitelist = set() + m.module_denylist = set() return m diff --git a/tests/test_inference.py b/tests/test_inference.py index ffd78fe035..10fceb7b56 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -30,7 +30,7 @@ ) from astroid import decorators as decoratorsmod from astroid.arguments import CallSite -from astroid.bases import BoundMethod, Instance, UnboundMethod, UnionType +from astroid.bases import BoundMethod, Generator, Instance, UnboundMethod, UnionType from astroid.builder import AstroidBuilder, _extract_single_node, extract_node, parse from astroid.const import IS_PYPY, PY39_PLUS, PY310_PLUS, PY312_PLUS from astroid.context import CallContext, InferenceContext @@ -4321,6 +4321,53 @@ class Test(Outer.Inner): assert isinstance(inferred, nodes.Const) assert inferred.value == 123 + def test_infer_method_empty_body(self) -> None: + # https://github.com/PyCQA/astroid/issues/1015 + node = extract_node( + """ + class A: + def foo(self): ... + + A().foo() #@ + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value is None + + def test_infer_method_overload(self) -> None: + # https://github.com/PyCQA/astroid/issues/1015 + node = extract_node( + """ + class A: + def foo(self): ... + + def foo(self): + yield + + A().foo() #@ + """ + ) + inferred = list(node.infer()) + assert len(inferred) == 1 + assert isinstance(inferred[0], Generator) + + def test_infer_function_under_if(self) -> None: + node = extract_node( + """ + if 1 in [1]: + def func(): + return 42 + else: + def func(): + return False + + func() #@ + """ + ) + inferred = list(node.inferred()) + assert [const.value for const in inferred] == [42, False] + def test_delayed_attributes_without_slots(self) -> None: ast_node = extract_node( """ diff --git a/tests/test_manager.py b/tests/test_manager.py index a55fae1932..7861927930 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -383,6 +383,13 @@ def test_raises_exception_for_empty_modname(self) -> None: with pytest.raises(AstroidBuildingError): self.manager.ast_from_module_name(None) + def test_denied_modules_raise(self) -> None: + self.manager.module_denylist.add("random") + with pytest.raises(AstroidImportError, match="random"): + self.manager.ast_from_module_name("random") + # and module not in the deny list shouldn't raise + self.manager.ast_from_module_name("math") + class IsolatedAstroidManagerTest(resources.AstroidCacheSetupMixin, unittest.TestCase): def test_no_user_warning(self):