diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ae694e6ba..7c4e1b57c8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ on: env: CACHE_VERSION: 3 KEY_PREFIX: venv - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.13" PRE_COMMIT_CACHE: ~/.cache/pre-commit concurrency: @@ -24,10 +24,10 @@ jobs: timeout-minutes: 20 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.4.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.2 + uses: actions/cache@v4.2.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.2 + uses: actions/cache@v4.2.2 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -81,15 +81,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9, "3.10", "3.11", "3.12", "3.13-dev"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.4.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.2 + uses: actions/cache@v4.2.2 with: path: venv key: >- @@ -117,7 +117,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - python -m pip install -U pip setuptools wheel + python -m pip install -U pip wheel pip install -U -r requirements_full.txt pip install -e . - name: Run pytest @@ -125,10 +125,11 @@ jobs: . venv/bin/activate pytest --cov - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.6.1 with: name: coverage-linux-${{ matrix.python-version }} path: .coverage + include-hidden-files: true tests-windows: name: tests / run / ${{ matrix.python-version }} / Windows @@ -138,17 +139,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9, "3.10", "3.11", "3.12", "3.13-dev"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] steps: - name: Set temp directory run: echo "TEMP=$env:USERPROFILE\AppData\Local\Temp" >> $env:GITHUB_ENV # 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.7 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.4.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -160,7 +161,7 @@ jobs: 'requirements_full.txt', 'requirements_minimal.txt') }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.2.2 with: path: venv key: >- @@ -179,10 +180,11 @@ jobs: . venv\\Scripts\\activate pytest --cov - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.6.1 with: name: coverage-windows-${{ matrix.python-version }} path: .coverage + include-hidden-files: true tests-pypy: name: tests / run / ${{ matrix.python-version }} / Linux @@ -195,10 +197,10 @@ jobs: python-version: ["pypy3.9", "pypy3.10"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.4.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -210,7 +212,7 @@ jobs: }}" >> $GITHUB_OUTPUT - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.2.2 with: path: venv key: >- @@ -229,10 +231,11 @@ jobs: . venv/bin/activate pytest --cov - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.6.1 with: name: coverage-pypy-${{ matrix.python-version }} path: .coverage + include-hidden-files: true coverage: name: tests / process / coverage @@ -241,22 +244,22 @@ jobs: needs: ["tests-linux", "tests-windows", "tests-pypy"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.7 - - name: Set up Python 3.12 + uses: actions/checkout@v4.2.2 + - name: Set up Python 3.13 id: python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.4.0 with: - python-version: "3.12" + python-version: "3.13" 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.8 + uses: actions/download-artifact@v4.1.9 - name: Combine Linux coverage results run: | coverage combine coverage-linux*/.coverage coverage xml -o coverage-linux.xml - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -267,7 +270,7 @@ jobs: run: | coverage combine coverage-windows*/.coverage coverage xml -o coverage-windows.xml - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -278,7 +281,7 @@ jobs: run: | coverage combine coverage-pypy*/.coverage coverage xml -o coverage-pypy.xml - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d527a2f221..9164b9b92a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,18 +6,16 @@ on: - published env: - DEFAULT_PYTHON: "3.12" + DEFAULT_PYTHON: "3.13" permissions: contents: read jobs: - release-pypi: - name: Upload release to PyPI + build: + name: Build release assets runs-on: ubuntu-latest - environment: - name: PyPI - url: https://pypi.org/project/astroid/ + if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags') steps: - name: Check out code from Github uses: actions/checkout@v4.1.7 @@ -31,15 +29,52 @@ jobs: run: | # Remove dist, build, and astroid.egg-info # when building locally for testing! - python -m pip install twine build + python -m pip install build - name: Build distributions run: | python -m build + - name: Upload release assets + uses: actions/upload-artifact@v4.6.1 + with: + name: release-assets + path: dist/ + + release-pypi: + name: Upload release to PyPI + runs-on: ubuntu-latest + needs: ["build"] + environment: + name: PyPI + url: https://pypi.org/project/astroid/ + permissions: + id-token: write + steps: + - name: Download release assets + uses: actions/download-artifact@v4.1.9 + with: + name: release-assets + path: dist/ - name: Upload to PyPI if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags') - env: - TWINE_REPOSITORY: pypi - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload --verbose dist/* + uses: pypa/gh-action-pypi-publish@release/v1 + + release-github: + name: Upload assets to Github release + runs-on: ubuntu-latest + needs: ["build"] + permissions: + contents: write + id-token: write + steps: + - name: Download release assets + uses: actions/download-artifact@v4.1.9 + with: + name: release-assets + path: dist/ + - name: Sign the dists with Sigstore and upload assets to Github release + if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags') + uses: sigstore/gh-action-sigstore-python@v3.0.0 + with: + inputs: | + ./dist/*.tar.gz + ./dist/*.whl diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 57270668c0..322d5d29d2 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -15,9 +15,9 @@ Maintainers ----------- - Pierre Sassoulas - Daniël van Noord <13665637+DanielNoord@users.noreply.github.com> -- Hippo91 -- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Jacob Walls +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> +- Hippo91 - Bryce Guinta - Ceridwen - Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com> @@ -41,6 +41,7 @@ Contributors - Tushar Sadhwani - Julien Jehannet - Calen Pennington +- Antonio - Hugo van Kemenade - Tim Martin - Phil Schaf @@ -53,6 +54,7 @@ Contributors - David Shea - Daniel Harding - Christian Clauss +- correctmost <134317971+correctmost@users.noreply.github.com> - Ville Skyttä - Rene Zhang - Philip Lorenz @@ -65,7 +67,6 @@ Contributors - FELD Boris - Enji Cooper - Dani Alcala <112832187+clavedeluna@users.noreply.github.com> -- Antonio - Adrien Di Mascio - tristanlatr <19967168+tristanlatr@users.noreply.github.com> - emile@crater.logilab.fr @@ -129,6 +130,7 @@ Contributors - Peter de Blanc - Peter Talley - Ovidiu Sabou +- Oleh Prypin - Nicolas Noirbent - Neil Girdhar - Miro Hrončok @@ -140,6 +142,8 @@ Contributors - Kian Meng, Ang - Kai Mueller <15907922+kasium@users.noreply.github.com> - Jörg Thalheim +- Jérome Perrin +- JulianJvn <128477611+JulianJvn@users.noreply.github.com> - Josef Kemetmüller - Jonathan Striebel - John Belmonte @@ -147,11 +151,14 @@ Contributors - Jeff Quast - Jarrad Hope - Jared Garst +- Jamie Scott - Jakub Wilk - Iva Miholic - Ionel Maries Cristian - HoverHell +- Hashem Nasarat - HQupgradeHQ <18361586+HQupgradeHQ@users.noreply.github.com> +- Gwyn Ciesla - Grygorii Iermolenko - Gregory P. Smith - Giuseppe Scrivano @@ -159,6 +166,7 @@ Contributors - Francis Charette Migneault - Felix Mölder - Federico Bond +- Eric Vergnaud - DudeNr33 <3929834+DudeNr33@users.noreply.github.com> - Dmitry Shachnev - Denis Laxalde @@ -191,8 +199,7 @@ Contributors - Alexander Scheel - Alexander Presnyakov - Ahmed Azzaoui -- JulianJvn <128477611+JulianJvn@users.noreply.github.com> -- Gwyn Ciesla + Co-Author --------- @@ -204,9 +211,3 @@ under this name, or we did not manage to find their commits in the history. - carl - alain lefroy - Mark Gius -- Jérome Perrin -- Jamie Scott -- correctmost <134317971+correctmost@users.noreply.github.com> -- Oleh Prypin -- Eric Vergnaud -- Hashem Nasarat diff --git a/ChangeLog b/ChangeLog index 5de60c6fbd..1bb530414a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,10 +9,47 @@ Release date: TBA +What's New in astroid 3.3.10? +============================= +Release date: TBA + + + +What's New in astroid 3.3.9? +============================ +Release date: 2025-03-09 + + +* Fix crash when `sys.modules` contains lazy loader objects during checking. + + Closes #2686 + Closes pylint-dev/pylint#8589 + +* Upload release assets to PyPI via Trusted Publishing. + + Refs pylint-dev/pylint#10256 + + +What's New in astroid 3.3.8? +============================ +Release date: 2024-12-23 + +* Fix inability to import `collections.abc` in python 3.13.1. The reported fixes in astroid 3.3.6 + and 3.3.7 did not actually fix this issue. + + Closes pylint-dev/pylint#10112 + + What's New in astroid 3.3.7? ============================ -Release date: TBA +Release date: 2024-12-20 + +This release was yanked. +* Fix inability to import `collections.abc` in python 3.13.1. The reported fix in astroid 3.3.6 + did not actually fix this issue. + + Closes pylint-dev/pylint#10112 What's New in astroid 3.3.6? @@ -20,6 +57,7 @@ What's New in astroid 3.3.6? Release date: 2024-12-08 * Fix inability to import `collections.abc` in python 3.13.1. + _It was later found that this did not resolve the linked issue. It was fixed in astroid 3.3.7_ Closes pylint-dev/pylint#10112 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 60f4185308..331a37ff2f 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -2,5 +2,5 @@ # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt -__version__ = "3.3.6" +__version__ = "3.3.9" version = __version__ diff --git a/astroid/brain/brain_collections.py b/astroid/brain/brain_collections.py index 462c85add2..94944e67ad 100644 --- a/astroid/brain/brain_collections.py +++ b/astroid/brain/brain_collections.py @@ -8,7 +8,7 @@ from astroid.brain.helpers import register_module_extender from astroid.builder import AstroidBuilder, extract_node, parse -from astroid.const import PY313_0, PY313_PLUS +from astroid.const import PY313_PLUS from astroid.context import InferenceContext from astroid.exceptions import AttributeInferenceError from astroid.manager import AstroidManager @@ -20,8 +20,7 @@ def _collections_transform(): return parse( - (" import _collections_abc as abc" if PY313_PLUS and not PY313_0 else "") - + """ + """ class defaultdict(dict): default_factory = None def __missing__(self, key): pass @@ -33,7 +32,7 @@ def __getitem__(self, key): return default_factory ) -def _collections_abc_313_0_transform() -> nodes.Module: +def _collections_abc_313_transform() -> nodes.Module: """See https://github.com/python/cpython/pull/124735""" return AstroidBuilder(AstroidManager()).string_build( "from _collections_abc import *" @@ -133,7 +132,7 @@ def register(manager: AstroidManager) -> None: ClassDef, easy_class_getitem_inference, _looks_like_subscriptable ) - if PY313_0: + if PY313_PLUS: register_module_extender( - manager, "collections.abc", _collections_abc_313_0_transform + manager, "collections.abc", _collections_abc_313_transform ) diff --git a/astroid/const.py b/astroid/const.py index a10c0f4a2b..c010818063 100644 --- a/astroid/const.py +++ b/astroid/const.py @@ -9,7 +9,6 @@ PY311_PLUS = sys.version_info >= (3, 11) PY312_PLUS = sys.version_info >= (3, 12) PY313_PLUS = sys.version_info >= (3, 13) -PY313_0 = sys.version_info[:3] == (3, 13, 0) WIN32 = sys.platform == "win32" diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 09e98c888b..fd53da56ea 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -133,36 +133,22 @@ def find_module( processed: list[str], submodule_path: Sequence[str] | None, ) -> ModuleSpec | None: - if submodule_path is not None: - submodule_path = list(submodule_path) - elif modname in sys.builtin_module_names: + # Although we should be able to use `find_spec` this doesn't work on PyPy for builtins. + # Therefore, we use the `builtin_module_nams` heuristic for these. + if submodule_path is None and modname in sys.builtin_module_names: return ModuleSpec( name=modname, location=None, type=ModuleType.C_BUILTIN, ) + + if submodule_path is not None: + search_paths = list(submodule_path) else: - try: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - spec = importlib.util.find_spec(modname) - if ( - spec - and spec.loader # type: ignore[comparison-overlap] # noqa: E501 - is importlib.machinery.FrozenImporter - ): - # No need for BuiltinImporter; builtins handled above - return ModuleSpec( - name=modname, - location=getattr(spec.loader_state, "filename", None), - type=ModuleType.PY_FROZEN, - ) - except ValueError: - pass - submodule_path = sys.path + search_paths = sys.path suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0]) - for entry in submodule_path: + for entry in search_paths: package_directory = os.path.join(entry, modname) for suffix in suffixes: package_file_name = "__init__" + suffix @@ -178,6 +164,58 @@ def find_module( file_path = os.path.join(entry, file_name) if os.path.isfile(file_path): return ModuleSpec(name=modname, location=file_path, type=type_) + + # sys.stdlib_module_names was added in Python 3.10 + if PY310_PLUS: + # If the module name matches a stdlib module name, check whether this is a frozen + # module. Note that `find_spec` actually imports parent modules, so we want to make + # sure we only run this code for stuff that can be expected to be frozen. For now + # this is only stdlib. + if modname in sys.stdlib_module_names or ( + processed and processed[0] in sys.stdlib_module_names + ): + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=Warning) + spec = importlib.util.find_spec(".".join((*processed, modname))) + except ValueError: + spec = None + + if ( + spec + and spec.loader # type: ignore[comparison-overlap] # noqa: E501 + is importlib.machinery.FrozenImporter + ): + return ModuleSpec( + name=modname, + location=getattr(spec.loader_state, "filename", None), + type=ModuleType.PY_FROZEN, + ) + else: + # NOTE: This is broken code. It doesn't work on Python 3.13+ where submodules can also + # be frozen. However, we don't want to worry about this and we don't want to break + # support for older versions of Python. This is just copy-pasted from the old non + # working version to at least have no functional behaviour change on <=3.10. + # It can be removed after 3.10 is no longer supported in favour of the logic above. + if submodule_path is None: # pylint: disable=else-if-used + try: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + spec = importlib.util.find_spec(modname) + if ( + spec + and spec.loader # type: ignore[comparison-overlap] # noqa: E501 + is importlib.machinery.FrozenImporter + ): + # No need for BuiltinImporter; builtins handled above + return ModuleSpec( + name=modname, + location=getattr(spec.loader_state, "filename", None), + type=ModuleType.PY_FROZEN, + ) + except ValueError: + pass + return None def contribute_to_path( @@ -231,13 +269,12 @@ def find_module( if processed: modname = ".".join([*processed, modname]) if util.is_namespace(modname) and modname in sys.modules: - submodule_path = sys.modules[modname].__path__ return ModuleSpec( name=modname, location="", origin="namespace", type=ModuleType.PY_NAMESPACE, - submodule_search_locations=submodule_path, + submodule_search_locations=sys.modules[modname].__path__, ) return None @@ -353,13 +390,15 @@ def _search_zip( if PY310_PLUS: if not importer.find_spec(os.path.sep.join(modpath)): raise ImportError( - "No module named %s in %s/%s" - % (".".join(modpath[1:]), filepath, modpath) + "No module named {} in {}/{}".format( + ".".join(modpath[1:]), filepath, modpath + ) ) elif not importer.find_module(os.path.sep.join(modpath)): raise ImportError( - "No module named %s in %s/%s" - % (".".join(modpath[1:]), filepath, modpath) + "No module named {} in {}/{}".format( + ".".join(modpath[1:]), filepath, modpath + ) ) return ( ModuleType.PY_ZIPMODULE, diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index 6845ca99c1..db78f8ce00 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -5488,6 +5488,7 @@ def _create_basic_elements( """Create a list of nodes to function as the elements of a new node.""" elements: list[NodeNG] = [] for element in value: + # NOTE: avoid accessing any attributes of element in the loop. element_node = const_factory(element) element_node.parent = node elements.append(element_node) @@ -5500,6 +5501,7 @@ def _create_dict_items( """Create a list of node pairs to function as the items of a new dict node.""" elements: list[tuple[SuccessfulInferenceResult, SuccessfulInferenceResult]] = [] for key, value in values.items(): + # NOTE: avoid accessing any attributes of both key and value in the loop. key_node = const_factory(key) key_node.parent = node value_node = const_factory(value) @@ -5510,18 +5512,23 @@ def _create_dict_items( def const_factory(value: Any) -> ConstFactoryResult: """Return an astroid node for a python value.""" - assert not isinstance(value, NodeNG) + # NOTE: avoid accessing any attributes of value until it is known that value + # is of a const type, to avoid possibly triggering code for a live object. + # Accesses include value.__class__ and isinstance(value, ...), but not type(value). + # See: https://github.com/pylint-dev/astroid/issues/2686 + value_type = type(value) + assert not issubclass(value_type, NodeNG) # This only handles instances of the CONST types. Any # subclasses get inferred as EmptyNode. # TODO: See if we should revisit these with the normal builder. - if value.__class__ not in CONST_CLS: + if value_type not in CONST_CLS: node = EmptyNode() node.object = value return node instance: List | Set | Tuple | Dict - initializer_cls = CONST_CLS[value.__class__] + initializer_cls = CONST_CLS[value_type] if issubclass(initializer_cls, (List, Set, Tuple)): instance = initializer_cls( lineno=None, diff --git a/astroid/raw_building.py b/astroid/raw_building.py index a89a87b571..b7aafb00e5 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -370,7 +370,11 @@ def _base_class_object_build( # this at least resolves common case such as Exception.args, # OSError.errno if issubclass(member, Exception): - instdict = member().__dict__ + member_object = member() + if hasattr(member_object, "__dict__"): + instdict = member_object.__dict__ + else: + raise TypeError else: raise TypeError except TypeError: diff --git a/requirements_dev.txt b/requirements_dev.txt index 4949455f52..77a6fd9f64 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,6 +3,5 @@ # Tools used during development, prefer running these with pre-commit black pre-commit -pylint>=3.2.0 -mypy +pylint>=3.2.7 ruff diff --git a/requirements_minimal.txt b/requirements_minimal.txt index 71170ce8bd..04cf4991ac 100644 --- a/requirements_minimal.txt +++ b/requirements_minimal.txt @@ -6,3 +6,5 @@ tbump~=6.11 coverage~=7.6 pytest pytest-cov~=5.0 +mypy +setuptools diff --git a/script/.contributors_aliases.json b/script/.contributors_aliases.json index 1dc38a4870..20f67fc00f 100644 --- a/script/.contributors_aliases.json +++ b/script/.contributors_aliases.json @@ -1,4 +1,8 @@ { + "134317971+correctmost@users.noreply.github.com": { + "mails": ["134317971+correctmost@users.noreply.github.com"], + "name": "correctmost" + }, "13665637+DanielNoord@users.noreply.github.com": { "mails": ["13665637+DanielNoord@users.noreply.github.com"], "name": "Daniël van Noord", @@ -98,7 +102,7 @@ "name": "Jacob Bogdanov" }, "jacobtylerwalls@gmail.com": { - "mails": ["jacobtylerwalls@gmail.com"], + "mails": ["jacobtylerwalls@gmail.com", "jwalls@fargeo.com"], "name": "Jacob Walls", "team": "Maintainers" }, diff --git a/tbump.toml b/tbump.toml index 96381c8fee..a86677bdfb 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/astroid" [version] -current = "3.3.6" +current = "3.3.9" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 447c4cde26..bec2cf2e5c 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -192,6 +192,22 @@ def check_metaclass_is_abc(node: nodes.ClassDef): class CollectionsBrain(unittest.TestCase): + def test_collections_abc_is_importable(self) -> None: + """ + Test that we can import `collections.abc`. + + The collections.abc has gone through various formats of being frozen. Therefore, we ensure + that we can still import it (correctly). + """ + import_node = builder.extract_node("import collections.abc") + assert isinstance(import_node, nodes.Import) + imported_module = import_node.do_import_module(import_node.names[0][0]) + # Make sure that the file we have imported is actually the submodule of collections and + # not the `abc` module. (Which would happen if you call `importlib.util.find_spec("abc")` + # instead of `importlib.util.find_spec("collections.abc")`) + assert isinstance(imported_module.file, str) + assert "collections" in imported_module.file + def test_collections_object_not_subscriptable(self) -> None: """ Test that unsubscriptable types are detected diff --git a/tests/test_raw_building.py b/tests/test_raw_building.py index 951bf09d90..f9536ebd20 100644 --- a/tests/test_raw_building.py +++ b/tests/test_raw_building.py @@ -19,9 +19,11 @@ from typing import Any from unittest import mock +import mypy.build import pytest import tests.testdata.python3.data.fake_module_with_broken_getattr as fm_getattr +import tests.testdata.python3.data.fake_module_with_collection_getattribute as fm_collection import tests.testdata.python3.data.fake_module_with_warnings as fm from astroid.builder import AstroidBuilder from astroid.const import IS_PYPY, PY312_PLUS @@ -31,8 +33,11 @@ build_from_import, build_function, build_module, + object_build_class, ) +DUMMY_MOD = build_module("DUMMY") + class RawBuildingTC(unittest.TestCase): def test_attach_dummy_node(self) -> None: @@ -118,6 +123,14 @@ def test_module_object_with_broken_getattr(self) -> None: # This should not raise an exception AstroidBuilder().inspect_build(fm_getattr, "test") + def test_module_collection_with_object_getattribute(self) -> None: + # Tests https://github.com/pylint-dev/astroid/issues/2686 + # When astroid live inspection of module's collection raises + # error when element __getattribute__ causes collection to change size. + + # This should not raise an exception + AstroidBuilder().inspect_build(fm_collection, "test") + @pytest.mark.skipif( "posix" not in sys.builtin_module_names, reason="Platform doesn't support posix" @@ -157,3 +170,8 @@ def mocked_sys_modules_getitem(name: str) -> types.ModuleType | CustomGetattr: assert expected_err in caplog.text assert not out assert not err + + +def test_missing__dict__(): + # This shouldn't raise an exception. + object_build_class(DUMMY_MOD, mypy.build.ModuleNotFound, "arbitrary_name") diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 460868e02b..f4875ca5f2 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -265,6 +265,9 @@ def transform_class(cls): """ ) + @pytest.mark.skipif( + IS_PYPY, reason="Could not find a useful recursion limit on all versions" + ) def test_transform_aborted_if_recursion_limited(self): def transform_call(node: Call) -> Const: return node @@ -274,7 +277,7 @@ def transform_call(node: Call) -> Const: ) original_limit = sys.getrecursionlimit() - sys.setrecursionlimit(500 if IS_PYPY else 1000) + sys.setrecursionlimit(1000) try: with pytest.warns(UserWarning) as records: diff --git a/tests/testdata/python3/data/fake_module_with_collection_getattribute.py b/tests/testdata/python3/data/fake_module_with_collection_getattribute.py new file mode 100644 index 0000000000..e6ab45089f --- /dev/null +++ b/tests/testdata/python3/data/fake_module_with_collection_getattribute.py @@ -0,0 +1,11 @@ +class Changer: + def __getattribute__(self, name): + list_collection.append(self) + set_collection.add(self) + dict_collection[self] = self + return object.__getattribute__(self, name) + + +list_collection = [Changer()] +set_collection = {Changer()} +dict_collection = {Changer(): Changer()}