diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bdc8ad36c3..9ae694e6ba 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.5 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -81,15 +81,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13-dev"] outputs: python-key: ${{ steps.generate-python-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -125,7 +125,7 @@ jobs: . venv/bin/activate pytest --cov - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.4 with: name: coverage-linux-${{ matrix.python-version }} path: .coverage @@ -138,17 +138,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13-dev"] 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.5 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -179,7 +179,7 @@ jobs: . venv\\Scripts\\activate pytest --cov - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.4 with: name: coverage-windows-${{ matrix.python-version }} path: .coverage @@ -192,13 +192,13 @@ jobs: fail-fast: false matrix: # We only test on the lowest and highest supported PyPy versions - python-version: ["pypy3.8", "pypy3.10"] + python-version: ["pypy3.9", "pypy3.10"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -229,7 +229,7 @@ jobs: . venv/bin/activate pytest --cov - name: Upload coverage artifact - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.3.4 with: name: coverage-pypy-${{ matrix.python-version }} path: .coverage @@ -241,17 +241,17 @@ jobs: needs: ["tests-linux", "tests-windows", "tests-pypy"] steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.7 - name: Set up Python 3.12 id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 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.7 + uses: actions/download-artifact@v4.1.8 - 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 2ef77a4529..fc639bf325 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.5 + uses: actions/checkout@v4.1.7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release-tests.yml b/.github/workflows/release-tests.yml index cea29641c2..11a6e2b384 100644 --- a/.github/workflows/release-tests.yml +++ b/.github/workflows/release-tests.yml @@ -13,14 +13,18 @@ jobs: timeout-minutes: 5 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.7 - name: Set up Python 3.9 id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 with: # virtualenv 15.1.0 cannot be installed on Python 3.10+ python-version: 3.9 + env: + PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org" - name: Create Python virtual environment with virtualenv==15.1.0 + env: + PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org" run: | python -m pip install virtualenv==15.1.0 python -m virtualenv venv2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ebc589970..d527a2f221 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.5 + uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.1.1 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 977fd91612..91a2ddb2d4 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.4.3" + rev: "v0.5.5" hooks: - id: ruff exclude: tests/testdata @@ -23,11 +23,11 @@ repos: exclude: tests/testdata|setup.py types: [python] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.17.0 hooks: - id: pyupgrade exclude: tests/testdata - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/Pierre-Sassoulas/black-disable-checker/ rev: v1.1.3 hooks: @@ -50,11 +50,12 @@ repos: "-rn", "-sn", "--rcfile=pylintrc", + "--output-format=github", # "--load-plugins=pylint.extensions.docparams", We're not ready for that ] exclude: tests/testdata|conf.py - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.11.0 hooks: - id: mypy name: mypy diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 8ec45caf50..04345d7cb5 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -206,3 +206,6 @@ under this name, or we did not manage to find their commits in the history. - Mark Gius - Jérome Perrin - Jamie Scott +- correctmost <134317971+correctmost@users.noreply.github.com> +- Oleh Prypin +- Eric Vergnaud diff --git a/ChangeLog b/ChangeLog index a591a93ccd..d4b2ca2450 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,18 +3,57 @@ astroid's ChangeLog =================== -What's New in astroid 3.3.0? +What's New in astroid 3.4.0? ============================ Release date: TBA -What's New in astroid 3.2.5? +What's New in astroid 3.3.3? ============================ Release date: TBA +What's New in astroid 3.3.2? +============================ +Release date: 2024-08-11 + +* Restore support for soft-deprecated members of the ``typing`` module with python 3.13. + + Refs pylint-dev/pylint#9852 + + +What's New in astroid 3.3.1? +============================ +Release date: 2024-08-06 + +* Fix a crash introduced in 3.3.0 involving invalid format strings. + + Closes #2492 + + +What's New in astroid 3.3.0? +============================ +Release date: 2024-08-04 + +* Add support for Python 3.13. + +* Remove support for Python 3.8 (and constants `PY38`, `PY39_PLUS`, and `PYPY_7_3_11_PLUS`). + + Refs #2443 + +* Add the ``__annotations__`` attribute to the ``ClassDef`` object model. + + Closes pylint-dev/pylint#7126 + +* Implement inference for JoinedStr and FormattedValue + +* Add support for ``ssl.OP_LEGACY_SERVER_CONNECT`` (new in Python 3.12). + + Closes pylint-dev/pylint#9849 + + What's New in astroid 3.2.4? ============================ Release date: 2024-07-20 @@ -24,7 +63,6 @@ Release date: 2024-07-20 Closes #2467 - What's New in astroid 3.2.3? ============================ Release date: 2024-07-11 diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 660b15a268..99c1a03511 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.2.4" +__version__ = "3.3.2" version = __version__ diff --git a/astroid/_backport_stdlib_names.py b/astroid/_backport_stdlib_names.py index 39c5f65bac..901f90b90d 100644 --- a/astroid/_backport_stdlib_names.py +++ b/astroid/_backport_stdlib_names.py @@ -346,11 +346,7 @@ } ) -if sys.version_info[:2] == (3, 7): - stdlib_module_names = PY_3_7 -elif sys.version_info[:2] == (3, 8): - stdlib_module_names = PY_3_8 -elif sys.version_info[:2] == (3, 9): +if sys.version_info[:2] == (3, 9): stdlib_module_names = PY_3_9 else: raise AssertionError("This module is only intended as a backport for Python <= 3.9") diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index d53520dc46..e9d00e2e1a 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -7,9 +7,9 @@ from __future__ import annotations import itertools -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Iterator from functools import partial -from typing import TYPE_CHECKING, Any, Iterator, NoReturn, Type, Union, cast +from typing import TYPE_CHECKING, Any, NoReturn, Union, cast from astroid import arguments, helpers, inference_tip, nodes, objects, util from astroid.builder import AstroidBuilder @@ -40,10 +40,10 @@ ] BuiltContainers = Union[ - Type[tuple], - Type[list], - Type[set], - Type[frozenset], + type[tuple], + type[list], + type[set], + type[frozenset], ] CopyResult = Union[ diff --git a/astroid/brain/brain_collections.py b/astroid/brain/brain_collections.py index 8f1fd6c306..22017786ac 100644 --- a/astroid/brain/brain_collections.py +++ b/astroid/brain/brain_collections.py @@ -6,7 +6,6 @@ from astroid.brain.helpers import register_module_extender from astroid.builder import extract_node, parse -from astroid.const import PY39_PLUS from astroid.context import InferenceContext from astroid.exceptions import AttributeInferenceError from astroid.manager import AstroidManager @@ -61,9 +60,7 @@ def __add__(self, other): pass def __iadd__(self, other): pass def __mul__(self, other): pass def __imul__(self, other): pass - def __rmul__(self, other): pass""" - if PY39_PLUS: - base_deque_class += """ + def __rmul__(self, other): pass @classmethod def __class_getitem__(self, item): return cls""" return base_deque_class @@ -73,9 +70,7 @@ def _ordered_dict_mock(): base_ordered_dict_class = """ class OrderedDict(dict): def __reversed__(self): return self[::-1] - def move_to_end(self, key, last=False): pass""" - if PY39_PLUS: - base_ordered_dict_class += """ + def move_to_end(self, key, last=False): pass @classmethod def __class_getitem__(cls, item): return cls""" return base_ordered_dict_class @@ -116,11 +111,10 @@ def easy_class_getitem_inference(node, context: InferenceContext | None = None): def register(manager: AstroidManager) -> None: register_module_extender(manager, "collections", _collections_transform) - if PY39_PLUS: - # Starting with Python39 some objects of the collection module are subscriptable - # thanks to the __class_getitem__ method but the way it is implemented in - # _collection_abc makes it difficult to infer. (We would have to handle AssignName inference in the - # getitem method of the ClassDef class) Instead we put here a mock of the __class_getitem__ method - manager.register_transform( - ClassDef, easy_class_getitem_inference, _looks_like_subscriptable - ) + # Starting with Python39 some objects of the collection module are subscriptable + # thanks to the __class_getitem__ method but the way it is implemented in + # _collection_abc makes it difficult to infer. (We would have to handle AssignName inference in the + # getitem method of the ClassDef class) Instead we put here a mock of the __class_getitem__ method + manager.register_transform( + ClassDef, easy_class_getitem_inference, _looks_like_subscriptable + ) diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py index 88a4385fda..845295bf9b 100644 --- a/astroid/brain/brain_dataclasses.py +++ b/astroid/brain/brain_dataclasses.py @@ -15,11 +15,11 @@ from __future__ import annotations from collections.abc import Iterator -from typing import Literal, Tuple, Union +from typing import Literal, Union from astroid import bases, context, nodes from astroid.builder import parse -from astroid.const import PY39_PLUS, PY310_PLUS +from astroid.const import PY310_PLUS, PY313_PLUS from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceDefault from astroid.inference_tip import inference_tip from astroid.manager import AstroidManager @@ -28,8 +28,8 @@ _FieldDefaultReturn = Union[ None, - Tuple[Literal["default"], nodes.NodeNG], - Tuple[Literal["default_factory"], nodes.Call], + tuple[Literal["default"], nodes.NodeNG], + tuple[Literal["default_factory"], nodes.Call], ] DATACLASSES_DECORATORS = frozenset(("dataclass",)) @@ -503,6 +503,17 @@ def _looks_like_dataclass_field_call( return inferred.name == FIELD_NAME and inferred.root().name in DATACLASS_MODULES +def _looks_like_dataclasses(node: nodes.Module) -> bool: + return node.qname() == "dataclasses" + + +def _resolve_private_replace_to_public(node: nodes.Module) -> None: + """In python/cpython@6f3c138, a _replace() method was extracted from + replace(), and this indirection made replace() uninferable.""" + if "_replace" in node.locals: + node.locals["replace"] = node.locals["_replace"] + + def _get_field_default(field_call: nodes.Call) -> _FieldDefaultReturn: """Return a the default value of a field call, and the corresponding keyword argument name. @@ -539,22 +550,12 @@ def _get_field_default(field_call: nodes.Call) -> _FieldDefaultReturn: def _is_class_var(node: nodes.NodeNG) -> bool: """Return True if node is a ClassVar, with or without subscripting.""" - if PY39_PLUS: - try: - inferred = next(node.infer()) - except (InferenceError, StopIteration): - return False - - return getattr(inferred, "name", "") == "ClassVar" + try: + inferred = next(node.infer()) + except (InferenceError, StopIteration): + return False - # Before Python 3.9, inference returns typing._SpecialForm instead of ClassVar. - # Our backup is to inspect the node's structure. - return isinstance(node, nodes.Subscript) and ( - isinstance(node.value, nodes.Name) - and node.value.name == "ClassVar" - or isinstance(node.value, nodes.Attribute) - and node.value.attrname == "ClassVar" - ) + return getattr(inferred, "name", "") == "ClassVar" def _is_keyword_only_sentinel(node: nodes.NodeNG) -> bool: @@ -618,6 +619,13 @@ def _infer_instance_from_annotation( def register(manager: AstroidManager) -> None: + if PY313_PLUS: + manager.register_transform( + nodes.Module, + _resolve_private_replace_to_public, + _looks_like_dataclasses, + ) + manager.register_transform( nodes.ClassDef, dataclass_transform, is_decorated_with_dataclass ) diff --git a/astroid/brain/brain_fstrings.py b/astroid/brain/brain_fstrings.py deleted file mode 100644 index 262a27d259..0000000000 --- a/astroid/brain/brain_fstrings.py +++ /dev/null @@ -1,72 +0,0 @@ -# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html -# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE -# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt - -from __future__ import annotations - -import collections.abc -from typing import TypeVar - -from astroid import nodes -from astroid.manager import AstroidManager - -_NodeT = TypeVar("_NodeT", bound=nodes.NodeNG) - - -def _clone_node_with_lineno( - node: _NodeT, parent: nodes.NodeNG, lineno: int | None -) -> _NodeT: - cls = node.__class__ - other_fields = node._other_fields - _astroid_fields = node._astroid_fields - init_params = { - "lineno": lineno, - "col_offset": node.col_offset, - "parent": parent, - "end_lineno": node.end_lineno, - "end_col_offset": node.end_col_offset, - } - postinit_params = {param: getattr(node, param) for param in _astroid_fields} - if other_fields: - init_params.update({param: getattr(node, param) for param in other_fields}) - new_node = cls(**init_params) - if hasattr(node, "postinit") and _astroid_fields: - for param, child in postinit_params.items(): - if child and not isinstance(child, collections.abc.Sequence): - cloned_child = _clone_node_with_lineno( - node=child, lineno=new_node.lineno, parent=new_node - ) - postinit_params[param] = cloned_child - new_node.postinit(**postinit_params) - return new_node - - -def _transform_formatted_value( # pylint: disable=inconsistent-return-statements - node: nodes.FormattedValue, -) -> nodes.FormattedValue | None: - if node.value and node.value.lineno == 1: - if node.lineno != node.value.lineno: - new_node = nodes.FormattedValue( - lineno=node.lineno, - col_offset=node.col_offset, - parent=node.parent, - end_lineno=node.end_lineno, - end_col_offset=node.end_col_offset, - ) - new_value = _clone_node_with_lineno( - node=node.value, lineno=node.lineno, parent=new_node - ) - new_node.postinit( - value=new_value, - conversion=node.conversion, - format_spec=node.format_spec, - ) - return new_node - - -# TODO: this fix tries to *patch* http://bugs.python.org/issue29051 -# The problem is that FormattedValue.value, which is a Name node, -# has wrong line numbers, usually 1. This creates problems for pylint, -# which expects correct line numbers for things such as message control. -def register(manager: AstroidManager) -> None: - manager.register_transform(nodes.FormattedValue, _transform_formatted_value) diff --git a/astroid/brain/brain_hashlib.py b/astroid/brain/brain_hashlib.py index ae0632a901..91aa4c4277 100644 --- a/astroid/brain/brain_hashlib.py +++ b/astroid/brain/brain_hashlib.py @@ -4,13 +4,11 @@ from astroid.brain.helpers import register_module_extender from astroid.builder import parse -from astroid.const import PY39_PLUS from astroid.manager import AstroidManager def _hashlib_transform(): - maybe_usedforsecurity = ", usedforsecurity=True" if PY39_PLUS else "" - init_signature = f"value=''{maybe_usedforsecurity}" + init_signature = "value='', usedforsecurity=True" digest_signature = "self" shake_digest_signature = "self, length" @@ -54,13 +52,13 @@ def digest_size(self): 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}" + "node_depth=0, inner_size=0, last_node=False, usedforsecurity=True" ) 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}" + "node_depth=0, inner_size=0, last_node=False, usedforsecurity=True" ) shake_algorithms = dict.fromkeys( diff --git a/astroid/brain/brain_pathlib.py b/astroid/brain/brain_pathlib.py index 116cd2eef9..d0f531324b 100644 --- a/astroid/brain/brain_pathlib.py +++ b/astroid/brain/brain_pathlib.py @@ -8,6 +8,7 @@ from astroid import bases, context, inference_tip, nodes from astroid.builder import _extract_single_node +from astroid.const import PY313_PLUS from astroid.exceptions import InferenceError, UseInferenceDefault from astroid.manager import AstroidManager @@ -27,10 +28,11 @@ def _looks_like_parents_subscript(node: nodes.Subscript) -> bool: value = next(node.value.infer()) except (InferenceError, StopIteration): return False + parents = "builtins.tuple" if PY313_PLUS else "pathlib._PathParents" return ( isinstance(value, bases.Instance) and isinstance(value._proxied, nodes.ClassDef) - and value.qname() == "pathlib._PathParents" + and value.qname() == parents ) diff --git a/astroid/brain/brain_re.py b/astroid/brain/brain_re.py index e675f66112..19f2a5b39c 100644 --- a/astroid/brain/brain_re.py +++ b/astroid/brain/brain_re.py @@ -7,7 +7,7 @@ from astroid import context, inference_tip, nodes from astroid.brain.helpers import register_module_extender from astroid.builder import _extract_single_node, parse -from astroid.const import PY39_PLUS, PY311_PLUS +from astroid.const import PY311_PLUS from astroid.manager import AstroidManager @@ -84,9 +84,8 @@ def infer_pattern_match(node: nodes.Call, ctx: context.InferenceContext | None = end_lineno=node.end_lineno, end_col_offset=node.end_col_offset, ) - if PY39_PLUS: - func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) - class_def.locals["__class_getitem__"] = [func_to_add] + func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) + class_def.locals["__class_getitem__"] = [func_to_add] return iter([class_def]) diff --git a/astroid/brain/brain_regex.py b/astroid/brain/brain_regex.py index aff0610cb4..5a2d81e809 100644 --- a/astroid/brain/brain_regex.py +++ b/astroid/brain/brain_regex.py @@ -7,7 +7,6 @@ from astroid import context, inference_tip, nodes from astroid.brain.helpers import register_module_extender from astroid.builder import _extract_single_node, parse -from astroid.const import PY39_PLUS from astroid.manager import AstroidManager @@ -83,9 +82,8 @@ def infer_pattern_match(node: nodes.Call, ctx: context.InferenceContext | None = end_lineno=node.end_lineno, end_col_offset=node.end_col_offset, ) - if PY39_PLUS: - func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) - class_def.locals["__class_getitem__"] = [func_to_add] + func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) + class_def.locals["__class_getitem__"] = [func_to_add] return iter([class_def]) diff --git a/astroid/brain/brain_ssl.py b/astroid/brain/brain_ssl.py index 42018b5bfa..23d7ee4f73 100644 --- a/astroid/brain/brain_ssl.py +++ b/astroid/brain/brain_ssl.py @@ -6,7 +6,7 @@ from astroid import parse from astroid.brain.helpers import register_module_extender -from astroid.const import PY310_PLUS +from astroid.const import PY310_PLUS, PY312_PLUS from astroid.manager import AstroidManager @@ -42,13 +42,16 @@ class Options(_IntFlag): OP_NO_COMPRESSION = 11 OP_NO_TICKET = 12 OP_NO_RENEGOTIATION = 13 - OP_ENABLE_MIDDLEBOX_COMPAT = 14""" + OP_ENABLE_MIDDLEBOX_COMPAT = 14 + """ + if PY312_PLUS: + enum += "OP_LEGACY_SERVER_CONNECT = 15" return enum def ssl_transform(): return parse( - """ + f""" # Import necessary for conversion of objects defined in C into enums from enum import IntEnum as _IntEnum, IntFlag as _IntFlag @@ -71,6 +74,8 @@ def ssl_transform(): OP_NO_TLSv1, OP_NO_TLSv1_1, OP_NO_TLSv1_2, OP_SINGLE_DH_USE, OP_SINGLE_ECDH_USE) + {"from _ssl import OP_LEGACY_SERVER_CONNECT" if PY312_PLUS else ""} + from _ssl import (ALERT_DESCRIPTION_ACCESS_DENIED, ALERT_DESCRIPTION_BAD_CERTIFICATE, ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE, ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE, diff --git a/astroid/brain/brain_subprocess.py b/astroid/brain/brain_subprocess.py index e7e1034bb8..fbc088a680 100644 --- a/astroid/brain/brain_subprocess.py +++ b/astroid/brain/brain_subprocess.py @@ -6,7 +6,7 @@ from astroid.brain.helpers import register_module_extender from astroid.builder import parse -from astroid.const import PY39_PLUS, PY310_PLUS, PY311_PLUS +from astroid.const import PY310_PLUS, PY311_PLUS from astroid.manager import AstroidManager @@ -17,10 +17,9 @@ def _subprocess_transform(): self, args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=None, startupinfo=None, creationflags=0, restore_signals=True, - start_new_session=False, pass_fds=(), *, encoding=None, errors=None, text=None""" + start_new_session=False, pass_fds=(), *, encoding=None, errors=None, text=None, + user=None, group=None, extra_groups=None, umask=-1""" - if PY39_PLUS: - args += ", user=None, group=None, extra_groups=None, umask=-1" if PY310_PLUS: args += ", pipesize=-1" if PY311_PLUS: @@ -87,14 +86,11 @@ def terminate(self): def kill(self): pass {ctx_manager} - """ - ) - if PY39_PLUS: - code += """ - @classmethod - def __class_getitem__(cls, item): - pass + @classmethod + def __class_getitem__(cls, item): + pass """ + ) init_lines = textwrap.dedent(init).splitlines() indented_init = "\n".join(" " * 4 + line for line in init_lines) diff --git a/astroid/brain/brain_type.py b/astroid/brain/brain_type.py index 02322ef026..d3461e68d4 100644 --- a/astroid/brain/brain_type.py +++ b/astroid/brain/brain_type.py @@ -23,7 +23,6 @@ from __future__ import annotations from astroid import extract_node, inference_tip, nodes -from astroid.const import PY39_PLUS from astroid.context import InferenceContext from astroid.exceptions import UseInferenceDefault from astroid.manager import AstroidManager @@ -64,7 +63,6 @@ def __class_getitem__(cls, key): def register(manager: AstroidManager) -> None: - if PY39_PLUS: - manager.register_transform( - nodes.Name, inference_tip(infer_type_sub), _looks_like_type_subscript - ) + manager.register_transform( + nodes.Name, inference_tip(infer_type_sub), _looks_like_type_subscript + ) diff --git a/astroid/brain/brain_typing.py b/astroid/brain/brain_typing.py index 9965abc25c..38b01778b1 100644 --- a/astroid/brain/brain_typing.py +++ b/astroid/brain/brain_typing.py @@ -15,7 +15,7 @@ from astroid import context, extract_node, inference_tip from astroid.brain.helpers import register_module_extender from astroid.builder import AstroidBuilder, _extract_single_node -from astroid.const import PY39_PLUS, PY312_PLUS +from astroid.const import PY312_PLUS, PY313_PLUS from astroid.exceptions import ( AstroidSyntaxError, AttributeInferenceError, @@ -33,7 +33,6 @@ Name, NodeNG, Subscript, - Tuple, ) from astroid.nodes.scoped_nodes import ClassDef, FunctionDef @@ -168,6 +167,15 @@ def infer_typing_attr( # If typing subscript belongs to an alias handle it separately. raise UseInferenceDefault + if ( + PY313_PLUS + and isinstance(value, FunctionDef) + and value.qname() == "typing.Annotated" + ): + # typing.Annotated is a FunctionDef on 3.13+ + node._explicit_inference = lambda node, context: iter([value]) + return iter([value]) + if isinstance(value, ClassDef) and value.qname() in { "typing.Generic", "typing.Annotated", @@ -217,14 +225,6 @@ def _looks_like_typedDict( # pylint: disable=invalid-name return node.qname() in TYPING_TYPEDDICT_QUALIFIED -def infer_old_typedDict( # pylint: disable=invalid-name - node: ClassDef, ctx: context.InferenceContext | None = None -) -> Iterator[ClassDef]: - func_to_add = _extract_single_node("dict") - node.locals["__call__"] = [func_to_add] - return iter([node]) - - def infer_typedDict( # pylint: disable=invalid-name node: FunctionDef, ctx: context.InferenceContext | None = None ) -> Iterator[ClassDef]: @@ -328,13 +328,7 @@ def infer_typing_alias( class_def.postinit(bases=[res], body=[], decorators=None) maybe_type_var = node.args[1] - if ( - not PY39_PLUS - and not (isinstance(maybe_type_var, Tuple) and not maybe_type_var.elts) - or PY39_PLUS - and isinstance(maybe_type_var, Const) - and maybe_type_var.value > 0 - ): + if isinstance(maybe_type_var, Const) and maybe_type_var.value > 0: # If typing alias is subscriptable, add `__class_getitem__` to ClassDef func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) class_def.locals["__class_getitem__"] = [func_to_add] @@ -362,23 +356,12 @@ def _looks_like_special_alias(node: Call) -> bool: PY39: Callable = _CallableType(collections.abc.Callable, 2) """ return isinstance(node.func, Name) and ( - not PY39_PLUS - and node.func.name == "_VariadicGenericAlias" - and ( - isinstance(node.args[0], Name) - and node.args[0].name == "tuple" - or isinstance(node.args[0], Attribute) - and node.args[0].as_string() == "collections.abc.Callable" - ) - or PY39_PLUS - and ( - node.func.name == "_TupleType" - and isinstance(node.args[0], Name) - and node.args[0].name == "tuple" - or node.func.name == "_CallableType" - and isinstance(node.args[0], Attribute) - and node.args[0].as_string() == "collections.abc.Callable" - ) + node.func.name == "_TupleType" + and isinstance(node.args[0], Name) + and node.args[0].name == "tuple" + or node.func.name == "_CallableType" + and isinstance(node.args[0], Attribute) + and node.args[0].as_string() == "collections.abc.Callable" ) @@ -468,6 +451,18 @@ class TypeVar: @classmethod def __class_getitem__(cls, item): return cls class TypeVarTuple: ... + class ContextManager: + @classmethod + def __class_getitem__(cls, item): return cls + class AsyncContextManager: + @classmethod + def __class_getitem__(cls, item): return cls + class Pattern: + @classmethod + def __class_getitem__(cls, item): return cls + class Match: + @classmethod + def __class_getitem__(cls, item): return cls """ ) ) @@ -486,14 +481,9 @@ def register(manager: AstroidManager) -> None: Call, inference_tip(infer_typing_cast), _looks_like_typing_cast ) - if PY39_PLUS: - manager.register_transform( - FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict - ) - else: - manager.register_transform( - ClassDef, inference_tip(infer_old_typedDict), _looks_like_typedDict - ) + manager.register_transform( + FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict + ) manager.register_transform( Call, inference_tip(infer_typing_alias), _looks_like_typing_alias diff --git a/astroid/brain/helpers.py b/astroid/brain/helpers.py index baf6c5c854..79d778b5a3 100644 --- a/astroid/brain/helpers.py +++ b/astroid/brain/helpers.py @@ -38,7 +38,6 @@ def register_all_brains(manager: AstroidManager) -> None: brain_dataclasses, brain_datetime, brain_dateutil, - brain_fstrings, brain_functools, brain_gi, brain_hashlib, @@ -91,7 +90,6 @@ def register_all_brains(manager: AstroidManager) -> None: brain_dataclasses.register(manager) brain_datetime.register(manager) brain_dateutil.register(manager) - brain_fstrings.register(manager) brain_functools.register(manager) brain_gi.register(manager) brain_hashlib.register(manager) diff --git a/astroid/builder.py b/astroid/builder.py index cff859124e..932b461fa5 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -246,10 +246,7 @@ def delayed_assattr(self, node: nodes.AssignAttr) -> None: try: # 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 - ): + if type(inferred) in {bases.Instance, objects.ExceptionInstance}: inferred = inferred._proxied iattrs = inferred.instance_attrs if not _can_assign_attr(inferred, node.attrname): diff --git a/astroid/const.py b/astroid/const.py index b57959be7b..c010818063 100644 --- a/astroid/const.py +++ b/astroid/const.py @@ -5,8 +5,6 @@ import enum import sys -PY38 = sys.version_info[:2] == (3, 8) -PY39_PLUS = sys.version_info >= (3, 9) PY310_PLUS = sys.version_info >= (3, 10) PY311_PLUS = sys.version_info >= (3, 11) PY312_PLUS = sys.version_info >= (3, 12) @@ -17,9 +15,6 @@ IS_PYPY = sys.implementation.name == "pypy" IS_JYTHON = sys.implementation.name == "jython" -# pylint: disable-next=no-member -PYPY_7_3_11_PLUS = IS_PYPY and sys.pypy_version_info >= (7, 3, 11) # type: ignore[attr-defined] - class Context(enum.Enum): Load = 1 diff --git a/astroid/context.py b/astroid/context.py index cccc81c077..0b8c259fc6 100644 --- a/astroid/context.py +++ b/astroid/context.py @@ -8,8 +8,8 @@ import contextlib import pprint -from collections.abc import Iterator -from typing import TYPE_CHECKING, Dict, Optional, Sequence, Tuple +from collections.abc import Iterator, Sequence +from typing import TYPE_CHECKING, Optional from astroid.typing import InferenceResult, SuccessfulInferenceResult @@ -17,8 +17,8 @@ from astroid import constraint, nodes from astroid.nodes.node_classes import Keyword, NodeNG -_InferenceCache = Dict[ - Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"] +_InferenceCache = dict[ + tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"] ] _INFERENCE_CACHE: _InferenceCache = {} diff --git a/astroid/decorators.py b/astroid/decorators.py index 8baca60b67..6c8b1bac32 100644 --- a/astroid/decorators.py +++ b/astroid/decorators.py @@ -60,11 +60,9 @@ def wrapped( def yes_if_nothing_inferred( - func: Callable[_P, Generator[InferenceResult, None, None]] -) -> Callable[_P, Generator[InferenceResult, None, None]]: - def inner( - *args: _P.args, **kwargs: _P.kwargs - ) -> Generator[InferenceResult, None, None]: + func: Callable[_P, Generator[InferenceResult]] +) -> Callable[_P, Generator[InferenceResult]]: + def inner(*args: _P.args, **kwargs: _P.kwargs) -> Generator[InferenceResult]: generator = func(*args, **kwargs) try: @@ -80,11 +78,9 @@ def inner( def raise_if_nothing_inferred( - func: Callable[_P, Generator[InferenceResult, None, None]], -) -> Callable[_P, Generator[InferenceResult, None, None]]: - def inner( - *args: _P.args, **kwargs: _P.kwargs - ) -> Generator[InferenceResult, None, None]: + func: Callable[_P, Generator[InferenceResult]], +) -> Callable[_P, Generator[InferenceResult]]: + def inner(*args: _P.args, **kwargs: _P.kwargs) -> Generator[InferenceResult]: generator = func(*args, **kwargs) try: yield next(generator) diff --git a/astroid/helpers.py b/astroid/helpers.py index 244612146f..fe57b16bbc 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -60,7 +60,7 @@ def _function_type( def _object_type( node: InferenceResult, context: InferenceContext | None = None -) -> Generator[InferenceResult | None, None, None]: +) -> Generator[InferenceResult | None]: astroid_manager = manager.AstroidManager() builtins = astroid_manager.builtins_module context = context or InferenceContext() diff --git a/astroid/inference_tip.py b/astroid/inference_tip.py index cb1fb37909..c3187c0670 100644 --- a/astroid/inference_tip.py +++ b/astroid/inference_tip.py @@ -40,7 +40,7 @@ def inner( node: _NodesT, context: InferenceContext | None = None, **kwargs: Any, - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: partial_cache_key = (func, node) if partial_cache_key in _CURRENTLY_INFERRING: # If through recursion we end up trying to infer the same diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index c178e20e1b..09e98c888b 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -337,7 +337,7 @@ def _is_setuptools_namespace(location: pathlib.Path) -> bool: def _get_zipimporters() -> Iterator[tuple[str, zipimport.zipimporter]]: for filepath, importer in sys.path_importer_cache.items(): - if isinstance(importer, zipimport.zipimporter): + if importer is not None and isinstance(importer, zipimport.zipimporter): yield filepath, importer diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index 199e8285cc..0f553ab084 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -492,6 +492,10 @@ def __init__(self): super().__init__() + @property + def attr___annotations__(self) -> node_classes.Unkown: + return node_classes.Unknown() + @property def attr___module__(self): return node_classes.Const(self._instance.root().qname()) diff --git a/astroid/manager.py b/astroid/manager.py index ade31c0e91..e7c2c806f7 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -23,6 +23,7 @@ from astroid.modutils import ( NoSourceFile, _cache_normalize_path_, + _has_init, file_info_from_modpath, get_source_file, is_module_name_part_of_extension_package_whitelist, @@ -469,6 +470,7 @@ def clear_cache(self) -> None: for lru_cache in ( LookupMixIn.lookup, _cache_normalize_path_, + _has_init, util.is_namespace, ObjectModel.attributes, ClassDef._metaclass_lookup_attribute, diff --git a/astroid/modutils.py b/astroid/modutils.py index bcb602c6b8..8f7d0d3fe9 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -30,9 +30,8 @@ from collections.abc import Callable, Iterable, Sequence from contextlib import redirect_stderr, redirect_stdout from functools import lru_cache -from pathlib import Path -from astroid.const import IS_JYTHON, IS_PYPY, PY310_PLUS +from astroid.const import IS_JYTHON, PY310_PLUS from astroid.interpreter._import import spec, util if PY310_PLUS: @@ -74,20 +73,6 @@ except AttributeError: pass -if IS_PYPY and sys.version_info < (3, 8): - # PyPy stores the stdlib in two places: sys.prefix/lib_pypy and sys.prefix/lib-python/3 - # sysconfig.get_path on PyPy returns the first, but without an underscore so we patch this manually. - # Beginning with 3.8 the stdlib is only stored in: sys.prefix/pypy{py_version_short} - STD_LIB_DIRS.add(str(Path(sysconfig.get_path("stdlib")).parent / "lib_pypy")) - STD_LIB_DIRS.add(str(Path(sysconfig.get_path("stdlib")).parent / "lib-python/3")) - - # TODO: This is a fix for a workaround in virtualenv. At some point we should revisit - # whether this is still necessary. See https://github.com/pylint-dev/astroid/pull/1324. - STD_LIB_DIRS.add(str(Path(sysconfig.get_path("platstdlib")).parent / "lib_pypy")) - STD_LIB_DIRS.add( - str(Path(sysconfig.get_path("platstdlib")).parent / "lib-python/3") - ) - if os.name == "posix": # Need the real prefix if we're in a virtualenv, otherwise # the usual one will do. @@ -190,9 +175,10 @@ def load_module_from_name(dotted_name: str) -> types.ModuleType: # Capture and log anything emitted during import to avoid # contaminating JSON reports in pylint - with redirect_stderr(io.StringIO()) as stderr, redirect_stdout( - io.StringIO() - ) as stdout: + with ( + redirect_stderr(io.StringIO()) as stderr, + redirect_stdout(io.StringIO()) as stdout, + ): module = importlib.import_module(dotted_name) stderr_value = stderr.getvalue() @@ -670,6 +656,7 @@ def _is_python_file(filename: str) -> bool: return filename.endswith((".py", ".pyi", ".so", ".pyd", ".pyw")) +@lru_cache(maxsize=1024) def _has_init(directory: str) -> str | None: """If the given directory has a valid __init__ file, return its path, else return None. diff --git a/astroid/nodes/_base_nodes.py b/astroid/nodes/_base_nodes.py index ddcac994c6..96c3c1c06d 100644 --- a/astroid/nodes/_base_nodes.py +++ b/astroid/nodes/_base_nodes.py @@ -10,9 +10,9 @@ from __future__ import annotations import itertools -from collections.abc import Generator, Iterator +from collections.abc import Callable, Generator, Iterator from functools import cached_property, lru_cache, partial -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union +from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union from astroid import bases, nodes, util from astroid.const import PY310_PLUS @@ -328,11 +328,11 @@ class OperatorNode(NodeNG): def _filter_operation_errors( infer_callable: Callable[ [InferenceContext | None], - Generator[InferenceResult | util.BadOperationMessage, None, None], + Generator[InferenceResult | util.BadOperationMessage], ], context: InferenceContext | None, error: type[util.BadOperationMessage], - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: for result in infer_callable(context): if isinstance(result, error): # For the sake of .infer(), we don't care about operation @@ -392,7 +392,7 @@ def _invoke_binop_inference( other: InferenceResult, context: InferenceContext, method_name: str, - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: """Invoke binary operation inference on the given instance.""" methods = dunder_lookup.lookup(instance, method_name) context = bind_context_to_node(context, instance) @@ -431,7 +431,7 @@ def _aug_op( other: InferenceResult, context: InferenceContext, reverse: bool = False, - ) -> partial[Generator[InferenceResult, None, None]]: + ) -> partial[Generator[InferenceResult]]: """Get an inference callable for an augmented binary operation.""" method_name = AUGMENTED_OP_METHOD[op] return partial( @@ -452,7 +452,7 @@ def _bin_op( other: InferenceResult, context: InferenceContext, reverse: bool = False, - ) -> partial[Generator[InferenceResult, None, None]]: + ) -> partial[Generator[InferenceResult]]: """Get an inference callable for a normal binary operation. If *reverse* is True, then the reflected method will be used instead. @@ -475,7 +475,7 @@ def _bin_op( def _bin_op_or_union_type( left: bases.UnionType | nodes.ClassDef | nodes.Const, right: bases.UnionType | nodes.ClassDef | nodes.Const, - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: """Create a new UnionType instance for binary or, e.g. int | str.""" yield bases.UnionType(left, right) @@ -509,7 +509,7 @@ def _get_aug_flow( right_type: InferenceResult | None, context: InferenceContext, reverse_context: InferenceContext, - ) -> list[partial[Generator[InferenceResult, None, None]]]: + ) -> list[partial[Generator[InferenceResult]]]: """Get the flow for augmented binary operations. The rules are a bit messy: @@ -566,7 +566,7 @@ def _get_binop_flow( right_type: InferenceResult | None, context: InferenceContext, reverse_context: InferenceContext, - ) -> list[partial[Generator[InferenceResult, None, None]]]: + ) -> list[partial[Generator[InferenceResult]]]: """Get the flow for binary operations. The rules are a bit messy: @@ -627,7 +627,7 @@ def _infer_binary_operation( binary_opnode: nodes.AugAssign | nodes.BinOp, context: InferenceContext, flow_factory: GetFlowFactory, - ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]: + ) -> Generator[InferenceResult | util.BadBinaryOperationMessage]: """Infer a binary operation between a left operand and a right operand. This is used by both normal binary operations and augmented binary diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py index bfb5e3c231..1924c78eba 100644 --- a/astroid/nodes/node_classes.py +++ b/astroid/nodes/node_classes.py @@ -13,12 +13,11 @@ import sys import typing import warnings -from collections.abc import Generator, Iterable, Iterator, Mapping +from collections.abc import Callable, Generator, Iterable, Iterator, Mapping from functools import cached_property from typing import ( TYPE_CHECKING, Any, - Callable, ClassVar, Literal, Optional, @@ -77,17 +76,17 @@ def _is_const(value) -> bool: _NodesT, AssignedStmtsPossibleNode, Optional[InferenceContext], - Optional[typing.List[int]], + Optional[list[int]], ], Any, ] InferBinaryOperation = Callable[ [_NodesT, Optional[InferenceContext]], - typing.Generator[Union[InferenceResult, _BadOpMessageT], None, None], + Generator[Union[InferenceResult, _BadOpMessageT], None, None], ] InferLHS = Callable[ [_NodesT, Optional[InferenceContext]], - typing.Generator[InferenceResult, None, Optional[InferenceErrorInfo]], + Generator[InferenceResult, None, Optional[InferenceErrorInfo]], ] InferUnaryOp = Callable[[_NodesT, str], ConstFactoryResult] @@ -1029,7 +1028,7 @@ def get_children(self): @decorators.raise_if_nothing_inferred def _infer( self: nodes.Arguments, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: # pylint: disable-next=import-outside-toplevel from astroid.protocols import _arguments_infer_argname @@ -1418,7 +1417,7 @@ def _get_yield_nodes_skip_lambdas(self): def _infer_augassign( self, context: InferenceContext | None = None - ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]: + ) -> Generator[InferenceResult | util.BadBinaryOperationMessage]: """Inference logic for augmented binary operations.""" context = context or InferenceContext() @@ -1448,7 +1447,7 @@ def _infer_augassign( @decorators.path_wrapper def _infer( self: nodes.AugAssign, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: return self._filter_operation_errors( self._infer_augassign, context, util.BadBinaryOperationMessage ) @@ -1533,7 +1532,7 @@ def op_left_associative(self) -> bool: def _infer_binop( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: """Binary operation inference logic.""" left = self.left right = self.right @@ -1563,7 +1562,7 @@ def _infer_binop( @decorators.path_wrapper def _infer( self: nodes.BinOp, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: return self._filter_operation_errors( self._infer_binop, context, util.BadBinaryOperationMessage ) @@ -1912,7 +1911,7 @@ def _do_compare( def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[nodes.Const | util.UninferableBase, None, None]: + ) -> Generator[nodes.Const | util.UninferableBase]: """Chained comparison inference logic.""" retval: bool | util.UninferableBase = True @@ -2562,7 +2561,7 @@ def has_underlying_object(self) -> bool: @decorators.path_wrapper def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: if not self.has_underlying_object(): yield util.Uninferable else: @@ -2852,7 +2851,7 @@ def _infer( context: InferenceContext | None = None, asname: bool = True, **kwargs: Any, - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: """Infer a ImportFrom node: return the imported module/object.""" context = context or InferenceContext() name = context.lookupname @@ -2977,7 +2976,7 @@ def _infer_name(self, frame, name): @decorators.path_wrapper def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: if context is None or context.lookupname is None: raise InferenceError(node=self, context=context) try: @@ -3094,7 +3093,7 @@ def op_left_associative(self) -> Literal[False]: @decorators.raise_if_nothing_inferred def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: """Support IfExp inference. If we can't infer the truthiness of the condition, we default @@ -3183,7 +3182,7 @@ def _infer( context: InferenceContext | None = None, asname: bool = True, **kwargs: Any, - ) -> Generator[nodes.Module, None, None]: + ) -> Generator[nodes.Module]: """Infer an Import node: return the imported module/object.""" context = context or InferenceContext() name = context.lookupname @@ -4124,7 +4123,7 @@ def _infer( InferenceContext | None, None, ], - Generator[NodeNG, None, None], + Generator[NodeNG], ] ] = protocols.assign_assigned_stmts @@ -4674,6 +4673,42 @@ def get_children(self): if self.format_spec is not None: yield self.format_spec + def _infer( + self, context: InferenceContext | None = None, **kwargs: Any + ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]: + if self.format_spec is None: + yield from self.value.infer(context, **kwargs) + return + uninferable_already_generated = False + for format_spec in self.format_spec.infer(context, **kwargs): + if not isinstance(format_spec, Const): + if not uninferable_already_generated: + yield util.Uninferable + uninferable_already_generated = True + continue + for value in self.value.infer(context, **kwargs): + if isinstance(value, Const): + try: + formatted = format(value.value, format_spec.value) + yield Const( + formatted, + lineno=self.lineno, + col_offset=self.col_offset, + end_lineno=self.end_lineno, + end_col_offset=self.end_col_offset, + ) + continue + except (ValueError, TypeError): + # happens when format_spec.value is invalid + pass # fall through + if not uninferable_already_generated: + yield util.Uninferable + uninferable_already_generated = True + continue + + +MISSING_VALUE = "{MISSING_VALUE}" + class JoinedStr(NodeNG): """Represents a list of string expressions to be joined. @@ -4735,6 +4770,34 @@ def postinit(self, values: list[NodeNG] | None = None) -> None: def get_children(self): yield from self.values + def _infer( + self, context: InferenceContext | None = None, **kwargs: Any + ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]: + yield from self._infer_from_values(self.values, context) + + @classmethod + def _infer_from_values( + cls, nodes: list[NodeNG], context: InferenceContext | None = None, **kwargs: Any + ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]: + if len(nodes) == 1: + yield from nodes[0]._infer(context, **kwargs) + return + uninferable_already_generated = False + for prefix in nodes[0]._infer(context, **kwargs): + for suffix in cls._infer_from_values(nodes[1:], context, **kwargs): + result = "" + for node in (prefix, suffix): + if isinstance(node, Const): + result += str(node.value) + continue + result += MISSING_VALUE + if MISSING_VALUE in result: + if not uninferable_already_generated: + uninferable_already_generated = True + yield util.Uninferable + else: + yield Const(result) + class NamedExpr(_base_nodes.AssignTypeNode): """Represents the assignment from the assignment expression @@ -4926,7 +4989,7 @@ def __init__( def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[NodeNG | util.UninferableBase, None, None]: + ) -> Generator[NodeNG | util.UninferableBase]: yield self.value diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py index 1c4eb9a90b..3a482f3cc9 100644 --- a/astroid/nodes/node_ng.py +++ b/astroid/nodes/node_ng.py @@ -15,8 +15,6 @@ Any, ClassVar, Literal, - Tuple, - Type, TypeVar, Union, cast, @@ -53,7 +51,7 @@ _NodesT = TypeVar("_NodesT", bound="NodeNG") _NodesT2 = TypeVar("_NodesT2", bound="NodeNG") _NodesT3 = TypeVar("_NodesT3", bound="NodeNG") -SkipKlassT = Union[None, Type["NodeNG"], Tuple[Type["NodeNG"], ...]] +SkipKlassT = Union[None, type["NodeNG"], tuple[type["NodeNG"], ...]] class NodeNG: @@ -126,7 +124,7 @@ def __init__( def infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[InferenceResult, None, None]: + ) -> Generator[InferenceResult]: """Get a generator of the inferred values. This is the main entry point to the inference system. diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index dc48a43c71..af3b9d39a8 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, TypeVar from astroid import bases, protocols, util -from astroid.const import IS_PYPY, PY38, PY39_PLUS, PYPY_7_3_11_PLUS from astroid.context import ( CallContext, InferenceContext, @@ -603,7 +602,7 @@ def frame(self: _T, *, future: Literal[None, True] = None) -> _T: def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[Module, None, None]: + ) -> Generator[Module]: yield self @@ -1053,7 +1052,7 @@ def getattr( def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[Lambda, None, None]: + ) -> Generator[Lambda]: yield self def _get_yield_nodes_skip_functions(self): @@ -1736,7 +1735,11 @@ async def func(things): """ -def _is_metaclass(klass, seen=None, context: InferenceContext | None = None) -> bool: +def _is_metaclass( + klass: ClassDef, + seen: set[str] | None = None, + context: InferenceContext | None = None, +) -> bool: """Return if the given class can be used as a metaclass. """ @@ -1768,7 +1771,11 @@ def _is_metaclass(klass, seen=None, context: InferenceContext | None = None) -> return False -def _class_type(klass, ancestors=None, context: InferenceContext | None = None): +def _class_type( + klass: ClassDef, + ancestors: set[str] | None = None, + context: InferenceContext | None = None, +) -> Literal["class", "exception", "metaclass"]: """return a ClassDef node type to differ metaclass and exception from 'regular' classes """ @@ -1791,7 +1798,7 @@ def _class_type(klass, ancestors=None, context: InferenceContext | None = None): for base in klass.ancestors(recurs=False): name = _class_type(base, ancestors) if name != "class": - if name == "metaclass" and not _is_metaclass(klass): + if name == "metaclass" and klass._type != "metaclass": # don't propagate it if the current class # can't be a metaclass continue @@ -1860,7 +1867,7 @@ def my_meth(self, arg): :type: objectmodel.ClassModel """ - _type = None + _type: Literal["class", "exception", "metaclass"] | None = None _metaclass: NodeNG | None = None _metaclass_hack = False hide = False @@ -1947,7 +1954,10 @@ def implicit_locals(self): """ locals_ = (("__module__", self.special_attributes.attr___module__),) # __qualname__ is defined in PEP3155 - locals_ += (("__qualname__", self.special_attributes.attr___qualname__),) + locals_ += ( + ("__qualname__", self.special_attributes.attr___qualname__), + ("__annotations__", self.special_attributes.attr___annotations__), + ) return locals_ # pylint: disable=redefined-outer-name @@ -2001,26 +2011,6 @@ def _newstyle_impl(self, context: InferenceContext | None = None): doc=("Whether this is a new style class or not\n\n" ":type: bool or None"), ) - @cached_property - def fromlineno(self) -> int: - """The first line that this node appears on in the source code. - - Can also return 0 if the line can not be determined. - """ - if IS_PYPY and PY38 and not PYPY_7_3_11_PLUS: - # For Python < 3.8 the lineno is the line number of the first decorator. - # We want the class statement lineno. Similar to 'FunctionDef.fromlineno' - # PyPy (3.8): Fixed with version v7.3.11 - lineno = self.lineno or 0 - if self.decorators is not None: - lineno += sum( - node.tolineno - (node.lineno or 0) + 1 - for node in self.decorators.nodes - ) - - return lineno or 0 - return super().fromlineno - @cached_property def blockstart_tolineno(self): """The line on which the beginning of this block ends. @@ -2232,7 +2222,7 @@ def basenames(self): def ancestors( self, recurs: bool = True, context: InferenceContext | None = None - ) -> Generator[ClassDef, None, None]: + ) -> Generator[ClassDef]: """Iterate over the base classes in prefixed depth first order. :param recurs: Whether to recurse or return direct ancestors only. @@ -2642,7 +2632,6 @@ def getitem(self, index, context: InferenceContext | None = None): if ( isinstance(method, node_classes.EmptyNode) and self.pytype() == "builtins.type" - and PY39_PLUS ): return self raise @@ -2986,5 +2975,5 @@ def frame(self: _T, *, future: Literal[None, True] = None) -> _T: def _infer( self, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[ClassDef, None, None]: + ) -> Generator[ClassDef]: yield self diff --git a/astroid/objects.py b/astroid/objects.py index f94d1f16ff..2d12f59805 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -362,5 +362,5 @@ def infer_call_result( def _infer( self: _T, context: InferenceContext | None = None, **kwargs: Any - ) -> Generator[_T, None, None]: + ) -> Generator[_T]: yield self diff --git a/astroid/protocols.py b/astroid/protocols.py index 8e90ddab58..bacb786a99 100644 --- a/astroid/protocols.py +++ b/astroid/protocols.py @@ -106,7 +106,7 @@ def const_infer_binary_op( other: InferenceResult, context: InferenceContext, _: SuccessfulInferenceResult, -) -> Generator[ConstFactoryResult | util.UninferableBase, None, None]: +) -> Generator[ConstFactoryResult | util.UninferableBase]: not_implemented = nodes.Const(NotImplemented) if isinstance(other, nodes.Const): if ( @@ -176,7 +176,7 @@ def tl_infer_binary_op( other: InferenceResult, context: InferenceContext, method: SuccessfulInferenceResult, -) -> Generator[_TupleListNodeT | nodes.Const | util.UninferableBase, None, None]: +) -> Generator[_TupleListNodeT | nodes.Const | util.UninferableBase]: """Infer a binary operation on a tuple or list. The instance on which the binary operation is performed is a tuple @@ -224,7 +224,7 @@ def instance_class_infer_binary_op( other: InferenceResult, context: InferenceContext, method: SuccessfulInferenceResult, -) -> Generator[InferenceResult, None, None]: +) -> Generator[InferenceResult]: return method.infer_call_result(self, context) @@ -347,7 +347,7 @@ def assend_assigned_stmts( def _arguments_infer_argname( self, name: str | None, context: InferenceContext -) -> Generator[InferenceResult, None, None]: +) -> Generator[InferenceResult]: # arguments information may be missing, in which case we can't do anything # more from astroid import arguments # pylint: disable=import-outside-toplevel @@ -877,7 +877,7 @@ def match_mapping_assigned_stmts( node: nodes.AssignName, context: InferenceContext | None = None, assign_path: None = None, -) -> Generator[nodes.NodeNG, None, None]: +) -> Generator[nodes.NodeNG]: """Return empty generator (return -> raises StopIteration) so inferred value is Uninferable. """ @@ -891,7 +891,7 @@ def match_star_assigned_stmts( node: nodes.AssignName, context: InferenceContext | None = None, assign_path: None = None, -) -> Generator[nodes.NodeNG, None, None]: +) -> Generator[nodes.NodeNG]: """Return empty generator (return -> raises StopIteration) so inferred value is Uninferable. """ @@ -905,7 +905,7 @@ def match_as_assigned_stmts( node: nodes.AssignName, context: InferenceContext | None = None, assign_path: None = None, -) -> Generator[nodes.NodeNG, None, None]: +) -> Generator[nodes.NodeNG]: """Infer MatchAs as the Match subject if it's the only MatchCase pattern else raise StopIteration to yield Uninferable. """ @@ -923,7 +923,7 @@ def generic_type_assigned_stmts( node: nodes.AssignName, context: InferenceContext | None = None, assign_path: None = None, -) -> Generator[nodes.NodeNG, None, None]: +) -> Generator[nodes.NodeNG]: """Hack. Return any Node so inference doesn't fail when evaluating __class_getitem__. Revert if it's causing issues. """ diff --git a/astroid/raw_building.py b/astroid/raw_building.py index ba7a60712a..a89a87b571 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -557,9 +557,10 @@ def imported_member(self, node, member, name: str) -> bool: # check if it sounds valid and then add an import node, else use a # dummy node try: - with redirect_stderr(io.StringIO()) as stderr, redirect_stdout( - io.StringIO() - ) as stdout: + with ( + redirect_stderr(io.StringIO()) as stderr, + redirect_stdout(io.StringIO()) as stdout, + ): getattr(sys.modules[modname], name) stderr_value = stderr.getvalue() if stderr_value: diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index c1d547dd90..b783885019 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -18,7 +18,7 @@ from astroid import nodes from astroid._ast import ParserModule, get_parser_module, parse_function_type_comment -from astroid.const import IS_PYPY, PY38, PY39_PLUS, PY312_PLUS, Context +from astroid.const import PY312_PLUS, Context from astroid.manager import AstroidManager from astroid.nodes import NodeNG from astroid.nodes.node_classes import AssignName @@ -80,10 +80,6 @@ def _get_doc(self, node: T_Doc) -> tuple[T_Doc, ast.Constant | ast.Str | None]: ): doc_ast_node = first_value node.body = node.body[1:] - # The ast parser of python < 3.8 sets col_offset of multi-line strings to -1 - # as it is unable to determine the value correctly. We reset this to None. - if doc_ast_node.col_offset == -1: - doc_ast_node.col_offset = None return node, doc_ast_node except IndexError: pass # ast built from scratch @@ -157,25 +153,6 @@ def _get_position_info( end_col_offset=t.end[1], ) - def _reset_end_lineno(self, newnode: nodes.NodeNG) -> None: - """Reset end_lineno and end_col_offset attributes for PyPy 3.8. - - For some nodes, these are either set to -1 or only partially assigned. - To keep consistency across astroid and pylint, reset all. - - This has been fixed in PyPy 3.9. - For reference, an (incomplete) list of nodes with issues: - - ClassDef - For - - FunctionDef - While - - Call - If - - Decorators - Try - - With - Assign - """ - newnode.end_lineno = None - newnode.end_col_offset = None - for child_node in newnode.get_children(): - self._reset_end_lineno(child_node) - def visit_module( self, node: ast.Module, modname: str, modpath: str, package: bool ) -> nodes.Module: @@ -194,8 +171,6 @@ def visit_module( [self.visit(child, newnode) for child in node.body], doc_node=self.visit(doc_ast_node, newnode), ) - if IS_PYPY and PY38: - self._reset_end_lineno(newnode) return newnode if TYPE_CHECKING: # noqa: C901 @@ -315,16 +290,6 @@ def visit( @overload def visit(self, node: ast.NamedExpr, parent: NodeNG) -> nodes.NamedExpr: ... - if sys.version_info < (3, 9): - # Not used in Python 3.9+ - @overload - def visit( - self, node: ast.ExtSlice, parent: nodes.Subscript - ) -> nodes.Tuple: ... - - @overload - def visit(self, node: ast.Index, parent: nodes.Subscript) -> NodeNG: ... - @overload def visit(self, node: ast.keyword, parent: NodeNG) -> nodes.Keyword: ... @@ -542,14 +507,6 @@ def visit_arguments(self, node: ast.arguments, parent: NodeNG) -> nodes.Argument kwarg = node.kwarg.arg kwargannotation = self.visit(node.kwarg.annotation, newnode) - if PY38: - # In Python 3.8 'end_lineno' and 'end_col_offset' - # for 'kwonlyargs' don't include the annotation. - for arg in node.kwonlyargs: - if arg.annotation is not None: - arg.end_lineno = arg.annotation.end_lineno - arg.end_col_offset = arg.annotation.end_col_offset - kwonlyargs = [self.visit(child, newnode) for child in node.kwonlyargs] kw_defaults = [self.visit(child, newnode) for child in node.kw_defaults] annotations = [self.visit(arg.annotation, newnode) for arg in node.args] @@ -955,7 +912,7 @@ def visit_delete(self, node: ast.Delete, parent: NodeNG) -> nodes.Delete: def _visit_dict_items( self, node: ast.Dict, parent: NodeNG, newnode: nodes.Dict - ) -> Generator[tuple[NodeNG, NodeNG], None, None]: + ) -> Generator[tuple[NodeNG, NodeNG]]: for key, value in zip(node.keys, node.values): rebuilt_key: NodeNG rebuilt_value = self.visit(value, newnode) @@ -1047,14 +1004,9 @@ def _visit_for( self, cls: type[_ForT], node: ast.For | ast.AsyncFor, parent: NodeNG ) -> _ForT: """Visit a For node by returning a fresh instance of it.""" - col_offset = node.col_offset - if IS_PYPY and not PY39_PLUS and isinstance(node, ast.AsyncFor) and self._data: - # pylint: disable-next=unsubscriptable-object - col_offset = self._data[node.lineno - 1].index("async") - newnode = cls( lineno=node.lineno, - col_offset=col_offset, + col_offset=node.col_offset, end_lineno=node.end_lineno, end_col_offset=node.end_col_offset, parent=parent, @@ -1333,21 +1285,6 @@ def visit_namedexpr(self, node: ast.NamedExpr, parent: NodeNG) -> nodes.NamedExp ) return newnode - if sys.version_info < (3, 9): - # Not used in Python 3.9+. - def visit_extslice( - self, node: ast.ExtSlice, parent: nodes.Subscript - ) -> nodes.Tuple: - """Visit an ExtSlice node by returning a fresh instance of Tuple.""" - # ExtSlice doesn't have lineno or col_offset information - newnode = nodes.Tuple(ctx=Context.Load, parent=parent) - newnode.postinit([self.visit(dim, newnode) for dim in node.dims]) - return newnode - - def visit_index(self, node: ast.Index, parent: nodes.Subscript) -> NodeNG: - """Visit a Index node by returning a fresh instance of NodeNG.""" - return self.visit(node.value, parent) - def visit_keyword(self, node: ast.keyword, parent: NodeNG) -> nodes.Keyword: """Visit a Keyword node by returning a fresh instance of it.""" newnode = nodes.Keyword( @@ -1732,14 +1669,9 @@ def _visit_with( node: ast.With | ast.AsyncWith, parent: NodeNG, ) -> _WithT: - col_offset = node.col_offset - if IS_PYPY and not PY39_PLUS and isinstance(node, ast.AsyncWith) and self._data: - # pylint: disable-next=unsubscriptable-object - col_offset = self._data[node.lineno - 1].index("async") - newnode = cls( lineno=node.lineno, - col_offset=col_offset, + col_offset=node.col_offset, end_lineno=node.end_lineno, end_col_offset=node.end_col_offset, parent=parent, diff --git a/astroid/transforms.py b/astroid/transforms.py index 0d9c22e966..5f0e533136 100644 --- a/astroid/transforms.py +++ b/astroid/transforms.py @@ -7,7 +7,7 @@ import warnings from collections import defaultdict from collections.abc import Callable -from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar, Union, cast, overload +from typing import TYPE_CHECKING, Optional, TypeVar, Union, cast, overload from astroid.context import _invalidate_cache from astroid.typing import SuccessfulInferenceResult, TransformFn @@ -21,12 +21,12 @@ _Predicate = Optional[Callable[[_SuccessfulInferenceResultT], bool]] _Vistables = Union[ - "nodes.NodeNG", List["nodes.NodeNG"], Tuple["nodes.NodeNG", ...], str, None + "nodes.NodeNG", list["nodes.NodeNG"], tuple["nodes.NodeNG", ...], str, None ] _VisitReturns = Union[ SuccessfulInferenceResult, - List[SuccessfulInferenceResult], - Tuple[SuccessfulInferenceResult, ...], + list[SuccessfulInferenceResult], + tuple[SuccessfulInferenceResult, ...], str, None, ] diff --git a/astroid/typing.py b/astroid/typing.py index 7e3fbec184..27d95c21fb 100644 --- a/astroid/typing.py +++ b/astroid/typing.py @@ -4,11 +4,10 @@ from __future__ import annotations +from collections.abc import Callable, Generator from typing import ( TYPE_CHECKING, Any, - Callable, - Generator, Generic, Protocol, TypedDict, diff --git a/doc/requirements.txt b/doc/requirements.txt index e29754b80a..6f446f6323 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,3 +1,3 @@ -e . -sphinx~=7.3 -furo==2024.4.27 +sphinx~=7.4 +furo==2024.7.18 diff --git a/pylintrc b/pylintrc index ee22082248..76aa73716c 100644 --- a/pylintrc +++ b/pylintrc @@ -39,7 +39,7 @@ unsafe-load-any-extension=no extension-pkg-whitelist= # Minimum supported python version -py-version = 3.8.0 +py-version = 3.9.0 [REPORTS] @@ -348,6 +348,9 @@ ext-import-graph= # not be disabled) int-import-graph= +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library=_io [EXCEPTIONS] diff --git a/pyproject.toml b/pyproject.toml index c5d4c15fd8..b0078e813e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,18 +17,18 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Quality Assurance", "Topic :: Software Development :: Testing", ] -requires-python = ">=3.8.0" +requires-python = ">=3.9.0" dependencies = [ "typing-extensions>=4.0.0;python_version<'3.11'", ] @@ -83,11 +83,13 @@ ignore_missing_imports = true [tool.ruff] +target-version = "py39" # ruff is less lenient than pylint and does not make any exceptions # (for docstrings, strings and comments in particular). line-length = 110 +[tool.ruff.lint] select = [ "E", # pycodestyle "F", # pyflakes @@ -112,8 +114,7 @@ fixable = [ "RUF", # ruff ] unfixable = ["RUF001"] -target-version = "py38" -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # Ruff is autofixing a tests with a voluntarily sneaky unicode "tests/test_regrtest.py" = ["RUF001"] diff --git a/requirements_full.txt b/requirements_full.txt index 1780bc89d3..99cd84a6ee 100644 --- a/requirements_full.txt +++ b/requirements_full.txt @@ -8,6 +8,7 @@ numpy>=1.17.0,<2; python_version<"3.12" python-dateutil PyQt6 regex +setuptools; python_version<"3.12" six urllib3>1,<2 typing_extensions>=4.4.0 diff --git a/requirements_minimal.txt b/requirements_minimal.txt index 3d0518caf7..71170ce8bd 100644 --- a/requirements_minimal.txt +++ b/requirements_minimal.txt @@ -3,6 +3,6 @@ contributors-txt>=0.7.4 tbump~=6.11 # Tools used to run tests -coverage~=7.5 +coverage~=7.6 pytest pytest-cov~=5.0 diff --git a/tbump.toml b/tbump.toml index 2024435017..83d0b32940 100644 --- a/tbump.toml +++ b/tbump.toml @@ -1,7 +1,7 @@ github_url = "https://github.com/pylint-dev/astroid" [version] -current = "3.2.4" +current = "3.3.2" regex = ''' ^(?P0|[1-9]\d*) \. diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index b8bc84e31f..447c4cde26 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -51,13 +51,6 @@ def test_deque_py35methods(self) -> None: self.assertIn("insert", inferred.locals) self.assertIn("index", inferred.locals) - @test_utils.require_version(maxver="3.8") - def test_deque_not_py39methods(self): - inferred = self._inferred_queue_instance() - with self.assertRaises(AttributeInferenceError): - inferred.getattr("__class_getitem__") - - @test_utils.require_version(minver="3.9") def test_deque_py39methods(self): inferred = self._inferred_queue_instance() self.assertTrue(inferred.getattr("__class_getitem__")) @@ -172,7 +165,6 @@ def test_invalid_type_subscript(self): # noinspection PyStatementEffect val_inf.getattr("__class_getitem__")[0] - @test_utils.require_version(minver="3.9") def test_builtin_subscriptable(self): """Starting with python3.9 builtin types such as list are subscriptable. Any builtin class such as "enumerate" or "staticmethod" also works.""" @@ -231,7 +223,6 @@ def test_collections_object_not_subscriptable(self) -> None: with self.assertRaises(AttributeInferenceError): inferred.getattr("__class_getitem__") - @test_utils.require_version(minver="3.9") def test_collections_object_subscriptable(self): """Starting with python39 some object of collections module are subscriptable. Test one of them""" right_node = builder.extract_node( @@ -295,7 +286,6 @@ def test_collections_object_not_yet_subscriptable(self): with self.assertRaises(AttributeInferenceError): inferred.getattr("__class_getitem__") - @test_utils.require_version(minver="3.9") def test_collections_object_subscriptable_2(self): """Starting with python39 Iterator in the collection.abc module is subscriptable""" node = builder.extract_node( @@ -329,7 +319,6 @@ def test_collections_object_not_yet_subscriptable_2(self): with self.assertRaises(InferenceError): next(node.infer()) - @test_utils.require_version(minver="3.9") def test_collections_object_subscriptable_3(self): """With Python 3.9 the ByteString class of the collections module is subscriptable (but not the same class from typing module)""" @@ -345,7 +334,6 @@ def test_collections_object_subscriptable_3(self): inferred.getattr("__class_getitem__")[0], nodes.FunctionDef ) - @test_utils.require_version(minver="3.9") def test_collections_object_subscriptable_4(self): """Multiple inheritance with subscriptable collection class""" node = builder.extract_node( @@ -645,9 +633,8 @@ class Bar[T](Foo[T]): ... assert ancestors[0].name == "Foo" assert ancestors[1].name == "object" - @test_utils.require_version(minver="3.9") def test_typing_annotated_subscriptable(self): - """Test typing.Annotated is subscriptable with __class_getitem__""" + """typing.Annotated is subscriptable with __class_getitem__ below 3.13.""" node = builder.extract_node( """ import typing @@ -655,8 +642,13 @@ def test_typing_annotated_subscriptable(self): """ ) inferred = next(node.infer()) - assert isinstance(inferred, nodes.ClassDef) - assert isinstance(inferred.getattr("__class_getitem__")[0], nodes.FunctionDef) + if PY313_PLUS: + assert isinstance(inferred, nodes.FunctionDef) + else: + assert isinstance(inferred, nodes.ClassDef) + assert isinstance( + inferred.getattr("__class_getitem__")[0], nodes.FunctionDef + ) def test_typing_generic_slots(self): """Test slots for Generic subclass.""" @@ -676,7 +668,6 @@ def __init__(self, value): assert isinstance(slots[0], nodes.Const) assert slots[0].value == "value" - @test_utils.require_version(minver="3.9") def test_typing_no_duplicates(self): node = builder.extract_node( """ @@ -686,7 +677,6 @@ def test_typing_no_duplicates(self): ) assert len(node.inferred()) == 1 - @test_utils.require_version(minver="3.9") def test_typing_no_duplicates_2(self): node = builder.extract_node( """ @@ -753,7 +743,6 @@ def test_typing_namedtuple_dont_crash_on_no_fields(self) -> None: inferred = next(node.infer()) self.assertIsInstance(inferred, astroid.Instance) - @test_utils.require_version("3.8") def test_typed_dict(self): code = builder.extract_node( """ @@ -929,7 +918,6 @@ def test_typing_object_notsubscriptable_3(self): inferred.getattr("__class_getitem__")[0], nodes.FunctionDef ) - @test_utils.require_version(minver="3.9") def test_typing_object_builtin_subscriptable(self): """ Test that builtins alias, such as typing.List, are subscriptable @@ -945,7 +933,6 @@ def test_typing_object_builtin_subscriptable(self): self.assertIsInstance(inferred.getattr("__iter__")[0], nodes.FunctionDef) @staticmethod - @test_utils.require_version(minver="3.9") def test_typing_type_subscriptable(): node = builder.extract_node( """ @@ -1067,7 +1054,6 @@ def test_re_pattern_unsubscriptable(self): with self.assertRaises(InferenceError): next(wrong_node2.infer()) - @test_utils.require_version(minver="3.9") def test_re_pattern_subscriptable(self): """Test re.Pattern and re.Match are subscriptable in PY39+""" node1 = builder.extract_node( @@ -1091,23 +1077,6 @@ def test_re_pattern_subscriptable(self): assert isinstance(inferred2.getattr("__class_getitem__")[0], nodes.FunctionDef) -class BrainFStrings(unittest.TestCase): - def test_no_crash_on_const_reconstruction(self) -> None: - node = builder.extract_node( - """ - max_width = 10 - - test1 = f'{" ":{max_width+4}}' - print(f'"{test1}"') - - test2 = f'[{"7":>{max_width}}:0]' - test2 - """ - ) - inferred = next(node.infer()) - self.assertIs(inferred, util.Uninferable) - - class BrainNamedtupleAnnAssignTest(unittest.TestCase): def test_no_crash_on_ann_assign_in_namedtuple(self) -> None: node = builder.extract_node( diff --git a/tests/brain/test_hashlib.py b/tests/brain/test_hashlib.py index 01177862f4..390dcc8469 100644 --- a/tests/brain/test_hashlib.py +++ b/tests/brain/test_hashlib.py @@ -7,7 +7,6 @@ import unittest from astroid import MANAGER -from astroid.const import PY39_PLUS from astroid.nodes.scoped_nodes import ClassDef @@ -19,10 +18,8 @@ def _assert_hashlib_class(self, class_obj: ClassDef) -> None: self.assertIn("block_size", class_obj) self.assertIn("digest_size", class_obj) # usedforsecurity was added in Python 3.9, see 8e7174a9 - self.assertEqual(len(class_obj["__init__"].args.args), 3 if PY39_PLUS else 2) - self.assertEqual( - len(class_obj["__init__"].args.defaults), 2 if PY39_PLUS else 1 - ) + self.assertEqual(len(class_obj["__init__"].args.args), 3) + self.assertEqual(len(class_obj["__init__"].args.defaults), 2) self.assertEqual(len(class_obj["update"].args.args), 2) def test_hashlib(self) -> None: diff --git a/tests/brain/test_pathlib.py b/tests/brain/test_pathlib.py index d935d964ab..5aea8d3769 100644 --- a/tests/brain/test_pathlib.py +++ b/tests/brain/test_pathlib.py @@ -5,7 +5,7 @@ import astroid from astroid import bases -from astroid.const import PY310_PLUS +from astroid.const import PY310_PLUS, PY313_PLUS from astroid.util import Uninferable @@ -23,7 +23,10 @@ def test_inference_parents() -> None: inferred = name_node.inferred() assert len(inferred) == 1 assert isinstance(inferred[0], bases.Instance) - assert inferred[0].qname() == "pathlib._PathParents" + if PY313_PLUS: + assert inferred[0].qname() == "builtins.tuple" + else: + assert inferred[0].qname() == "pathlib._PathParents" def test_inference_parents_subscript_index() -> None: @@ -40,7 +43,10 @@ def test_inference_parents_subscript_index() -> None: inferred = path.inferred() assert len(inferred) == 1 assert isinstance(inferred[0], bases.Instance) - assert inferred[0].qname() == "pathlib.Path" + if PY313_PLUS: + assert inferred[0].qname() == "pathlib._local.Path" + else: + assert inferred[0].qname() == "pathlib.Path" def test_inference_parents_subscript_slice() -> None: diff --git a/tests/brain/test_regex.py b/tests/brain/test_regex.py index c3e0bbe7ef..1313ea40f2 100644 --- a/tests/brain/test_regex.py +++ b/tests/brain/test_regex.py @@ -11,7 +11,7 @@ import pytest -from astroid import MANAGER, builder, nodes, test_utils +from astroid import MANAGER, builder, nodes @pytest.mark.skipif(not HAS_REGEX, reason="This test requires the regex library.") @@ -27,7 +27,6 @@ def test_regex_flags(self) -> None: @pytest.mark.xfail( reason="Started failing on main, but no one reproduced locally yet" ) - @test_utils.require_version(minver="3.9") def test_regex_pattern_and_match_subscriptable(self): """Test regex.Pattern and regex.Match are subscriptable in PY39+.""" node1 = builder.extract_node( diff --git a/tests/brain/test_ssl.py b/tests/brain/test_ssl.py index 798bebfb72..418c589953 100644 --- a/tests/brain/test_ssl.py +++ b/tests/brain/test_ssl.py @@ -4,7 +4,10 @@ """Tests for the ssl brain.""" +import pytest + from astroid import bases, nodes, parse +from astroid.const import PY312_PLUS def test_ssl_brain() -> None: @@ -41,3 +44,21 @@ def test_ssl_brain() -> None: inferred_cert_required = next(module.body[4].value.infer()) assert isinstance(inferred_cert_required, bases.Instance) assert inferred_cert_required._proxied.name == "CERT_REQUIRED" + + +@pytest.mark.skipif(not PY312_PLUS, reason="Uses new 3.12 constant") +def test_ssl_brain_py312() -> None: + """Test ssl brain transform.""" + module = parse( + """ + import ssl + ssl.OP_LEGACY_SERVER_CONNECT + ssl.Options.OP_LEGACY_SERVER_CONNECT + """ + ) + + inferred_constant = next(module.body[1].value.infer()) + assert isinstance(inferred_constant, nodes.Const) + + inferred_instance = next(module.body[2].value.infer()) + assert isinstance(inferred_instance, bases.Instance) diff --git a/tests/brain/test_unittest.py b/tests/brain/test_unittest.py index aed05f7645..53f7d9f771 100644 --- a/tests/brain/test_unittest.py +++ b/tests/brain/test_unittest.py @@ -5,13 +5,11 @@ import unittest from astroid import builder -from astroid.test_utils import require_version class UnittestTest(unittest.TestCase): """A class that tests the brain_unittest module.""" - @require_version(minver="3.8.0") def test_isolatedasynciotestcase(self): """ Tests that the IsolatedAsyncioTestCase class is statically imported diff --git a/tests/test_builder.py b/tests/test_builder.py index 23a5a83836..f9dac6169e 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -19,7 +19,7 @@ import pytest from astroid import Instance, builder, nodes, test_utils, util -from astroid.const import IS_PYPY, PY38, PY39_PLUS, PYPY_7_3_11_PLUS +from astroid.const import IS_PYPY from astroid.exceptions import ( AstroidBuildingError, AstroidSyntaxError, @@ -57,10 +57,7 @@ def test_callfunc_lineno(self) -> None: self.assertIsInstance(strarg, nodes.Const) if IS_PYPY: self.assertEqual(strarg.fromlineno, 4) - if not PY39_PLUS: - self.assertEqual(strarg.tolineno, 4) - else: - self.assertEqual(strarg.tolineno, 5) + self.assertEqual(strarg.tolineno, 5) else: self.assertEqual(strarg.fromlineno, 4) self.assertEqual(strarg.tolineno, 5) @@ -157,12 +154,7 @@ class C: # L13 c = ast_module.body[2] assert isinstance(c, nodes.ClassDef) - if IS_PYPY and PY38 and not PYPY_7_3_11_PLUS: - # Not perfect, but best we can do for PyPy 3.8 (< v7.3.11). - # Can't detect closing bracket on new line. - assert c.fromlineno == 12 - else: - assert c.fromlineno == 13 + assert c.fromlineno == 13 assert c.tolineno == 14 @staticmethod @@ -849,12 +841,13 @@ def test_class_locals(self) -> None: klass1 = module["YO"] locals1 = klass1.locals keys = sorted(locals1.keys()) - assert_keys = ["__init__", "__module__", "__qualname__", "a"] + assert_keys = ["__annotations__", "__init__", "__module__", "__qualname__", "a"] self.assertEqual(keys, assert_keys) klass2 = module["YOUPI"] locals2 = klass2.locals keys = locals2.keys() assert_keys = [ + "__annotations__", "__init__", "__module__", "__qualname__", diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 1e57ac0777..2dd94a6ae3 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -8,6 +8,7 @@ import pytest from astroid import builder, helpers, manager, nodes, raw_building, util +from astroid.builder import AstroidBuilder from astroid.const import IS_PYPY from astroid.exceptions import _NonDeducibleTypeHierarchy from astroid.nodes.scoped_nodes import ClassDef @@ -17,6 +18,7 @@ class TestHelpers(unittest.TestCase): def setUp(self) -> None: builtins_name = builtins.__name__ astroid_manager = manager.AstroidManager() + AstroidBuilder(astroid_manager) # Only to ensure boostrap self.builtins = astroid_manager.astroid_cache[builtins_name] self.manager = manager.AstroidManager() diff --git a/tests/test_inference.py b/tests/test_inference.py index 65e662097b..a8b11b1614 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -19,6 +19,7 @@ import pytest from astroid import ( + Const, Slice, Uninferable, arguments, @@ -32,7 +33,7 @@ from astroid.arguments import CallSite 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.const import IS_PYPY, PY310_PLUS, PY312_PLUS from astroid.context import CallContext, InferenceContext from astroid.exceptions import ( AstroidTypeError, @@ -652,6 +653,19 @@ def process_line(word_pos): ) ) + def test_fstring_inference(self) -> None: + code = """ + name = "John" + result = f"Hello {name}!" + """ + ast = parse(code, __name__) + node = ast["result"] + inferred = node.inferred() + self.assertEqual(len(inferred), 1) + value_node = inferred[0] + self.assertIsInstance(value_node, Const) + self.assertEqual(value_node.value, "Hello John!") + def test_float_complex_ambiguity(self) -> None: code = ''' def no_conjugate_member(magic_flag): #@ @@ -2732,11 +2746,6 @@ def __radd__(self, other): msg.format(op="+=", lhs="int", rhs="list"), ] - # PEP-584 supports | for dictionary union - if not PY39_PLUS: - ast_nodes.append(extract_node("{} | {} #@")) - expected.append(msg.format(op="|", lhs="dict", rhs="dict")) - for node, expected_value in zip(ast_nodes, expected): errors = node.type_errors() self.assertEqual(len(errors), 1) @@ -4485,8 +4494,7 @@ def test_getitem_of_class_raised_type_error(self) -> None: # and reraise it as a TypeError in Class.getitem node = extract_node( """ - def test(): - yield + def test(): ... test() """ ) @@ -4516,7 +4524,6 @@ def func(object): inferred = next(node.infer()) assert inferred is util.Uninferable - @test_utils.require_version(minver="3.9") def test_infer_arg_called_type_when_used_as_index_is_uninferable(self): # https://github.com/pylint-dev/astroid/pull/958 node = extract_node( @@ -4531,7 +4538,6 @@ def func(type): assert not isinstance(inferred, nodes.ClassDef) # was inferred as builtins.type assert inferred is util.Uninferable - @test_utils.require_version(minver="3.9") def test_infer_arg_called_type_when_used_as_subscript_is_uninferable(self): # https://github.com/pylint-dev/astroid/pull/958 node = extract_node( @@ -4544,7 +4550,6 @@ def func(type): assert not isinstance(inferred, nodes.ClassDef) # was inferred as builtins.type assert inferred is util.Uninferable - @test_utils.require_version(minver="3.9") def test_infer_arg_called_type_defined_in_outer_scope_is_uninferable(self): # https://github.com/pylint-dev/astroid/pull/958 node = extract_node( @@ -5497,6 +5502,51 @@ class instance(object): self.assertIsInstance(inferred, Instance) +@pytest.mark.parametrize( + "code, result", + [ + # regular f-string + ( + """width = 10 +precision = 4 +value = 12.34567 +result = f"result: {value:{width}.{precision}}!" +""", + "result: 12.35!", + ), + # unsupported format + ( + """width = None +precision = 4 +value = 12.34567 +result = f"result: {value:{width}.{precision}}!" +""", + None, + ), + # unsupported value + ( + """width = 10 +precision = 4 +value = None +result = f"result: {value:{width}.{precision}}!" +""", + None, + ), + ], +) +def test_formatted_fstring_inference(code, result) -> None: + ast = parse(code, __name__) + node = ast["result"] + inferred = node.inferred() + assert len(inferred) == 1 + value_node = inferred[0] + if result is None: + assert value_node is util.Uninferable + else: + assert isinstance(value_node, Const) + assert value_node.value == result + + def test_augassign_recursion() -> None: """Make sure inference doesn't throw a RecursionError. @@ -6381,7 +6431,6 @@ def check_equal(a, b): assert inferred.value is None -@test_utils.require_version(minver="3.8") def test_posonlyargs_inference() -> None: code = """ class A: @@ -6769,34 +6818,6 @@ def test_custom_decorators_for_classmethod_and_staticmethods(code, obj, obj_type assert inferred.type == obj_type -@pytest.mark.skipif( - PY39_PLUS, - reason="Exact inference with dataclasses (replace function) in python3.9", -) -def test_dataclasses_subscript_inference_recursion_error(): - code = """ - from dataclasses import dataclass, replace - - @dataclass - class ProxyConfig: - auth: str = "/auth" - - - a = ProxyConfig("") - test_dict = {"proxy" : {"auth" : "", "bla" : "f"}} - - foo = test_dict['proxy'] - replace(a, **test_dict['proxy']) # This fails - """ - node = extract_node(code) - # Reproduces only with safe_infer() - assert util.safe_infer(node) is None - - -@pytest.mark.skipif( - not PY39_PLUS, - reason="Exact inference with dataclasses (replace function) in python3.9", -) def test_dataclasses_subscript_inference_recursion_error_39(): code = """ from dataclasses import dataclass, replace diff --git a/tests/test_lookup.py b/tests/test_lookup.py index a19f287637..b452d62894 100644 --- a/tests/test_lookup.py +++ b/tests/test_lookup.py @@ -6,7 +6,7 @@ import functools import unittest -from astroid import builder, nodes, test_utils +from astroid import builder, nodes from astroid.exceptions import ( AttributeInferenceError, InferenceError, @@ -789,7 +789,6 @@ def f2(*, x): self.assertEqual(len(stmts2), 1) self.assertEqual(stmts2[0].lineno, 7) - @test_utils.require_version(minver="3.8") def test_assign_after_posonly_param(self): """When an assignment statement overwrites a function positional-only parameter, only the assignment is returned, even when the variable and assignment do diff --git a/tests/test_manager.py b/tests/test_manager.py index c91ec0a4cf..34ddd06d4a 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -16,7 +16,7 @@ import astroid from astroid import manager, test_utils -from astroid.const import IS_JYTHON, IS_PYPY +from astroid.const import IS_JYTHON, IS_PYPY, PY312_PLUS from astroid.exceptions import ( AstroidBuildingError, AstroidImportError, @@ -391,13 +391,18 @@ def test_denied_modules_raise(self) -> None: class IsolatedAstroidManagerTest(unittest.TestCase): + @pytest.mark.skipif(PY312_PLUS, reason="distutils was removed in python 3.12") def test_no_user_warning(self): + """When Python 3.12 is minimum, this test will no longer provide value.""" mgr = manager.AstroidManager() self.addCleanup(mgr.clear_cache) with warnings.catch_warnings(): warnings.filterwarnings("error", category=UserWarning) mgr.ast_from_module_name("setuptools") - mgr.ast_from_module_name("pip") + try: + mgr.ast_from_module_name("pip") + except astroid.AstroidImportError: + pytest.skip("pip is not installed") class BorgAstroidManagerTC(unittest.TestCase): diff --git a/tests/test_nodes.py b/tests/test_nodes.py index c5605a9328..64cae2f676 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -25,7 +25,6 @@ extract_node, nodes, parse, - test_utils, transforms, util, ) @@ -892,7 +891,6 @@ def func(*, x = "default"): assert isinstance(args.kw_defaults[0], nodes.Const) assert args.kw_defaults[0].value == "default" - @test_utils.require_version(minver="3.8") def test_positional_only(self): ast = builder.parse( """ @@ -1630,7 +1628,6 @@ def func(): assert node.doc_node is None -@test_utils.require_version(minver="3.8") def test_parse_fstring_debug_mode() -> None: node = astroid.extract_node('f"{3=}"') assert isinstance(node, nodes.JoinedStr) diff --git a/tests/test_nodes_lineno.py b/tests/test_nodes_lineno.py index b0cdb9850b..c8a8839e21 100644 --- a/tests/test_nodes_lineno.py +++ b/tests/test_nodes_lineno.py @@ -8,44 +8,9 @@ import astroid from astroid import builder, nodes -from astroid.const import IS_PYPY, PY38, PY39_PLUS, PY310_PLUS, PY312_PLUS +from astroid.const import PY310_PLUS, PY312_PLUS -@pytest.mark.skipif( - not (PY38 and IS_PYPY), - reason="end_lineno and end_col_offset were added in PY38", -) -class TestEndLinenoNotSet: - """Test 'end_lineno' and 'end_col_offset' are initialized as 'None' for Python < - 3.8. - """ - - @staticmethod - def test_end_lineno_not_set() -> None: - code = textwrap.dedent( - """ - [1, 2, 3] #@ - var #@ - """ - ).strip() - ast_nodes = builder.extract_node(code) - assert isinstance(ast_nodes, list) and len(ast_nodes) == 2 - - n1 = ast_nodes[0] - assert isinstance(n1, nodes.List) - assert (n1.lineno, n1.col_offset) == (1, 0) - assert (n1.end_lineno, n1.end_col_offset) == (None, None) - - n2 = ast_nodes[1] - assert isinstance(n2, nodes.Name) - assert (n2.lineno, n2.col_offset) == (2, 0) - assert (n2.end_lineno, n2.end_col_offset) == (None, None) - - -@pytest.mark.skipif( - PY38 and IS_PYPY, - reason="end_lineno and end_col_offset were added in PY38", -) class TestLinenoColOffset: """Test 'lineno', 'col_offset', 'end_lineno', and 'end_col_offset' for all nodes. @@ -200,13 +165,8 @@ def test_end_lineno_call() -> None: assert (c1.args[0].end_lineno, c1.args[0].end_col_offset) == (1, 9) # fmt: off - if PY39_PLUS: - # 'lineno' and 'col_offset' information only added in Python 3.9 - assert (c1.keywords[0].lineno, c1.keywords[0].col_offset) == (1, 11) - assert (c1.keywords[0].end_lineno, c1.keywords[0].end_col_offset) == (1, 21) - else: - assert (c1.keywords[0].lineno, c1.keywords[0].col_offset) == (None, None) - assert (c1.keywords[0].end_lineno, c1.keywords[0].end_col_offset) == (None, None) + assert (c1.keywords[0].lineno, c1.keywords[0].col_offset) == (1, 11) + assert (c1.keywords[0].end_lineno, c1.keywords[0].end_col_offset) == (1, 21) assert (c1.keywords[0].value.lineno, c1.keywords[0].value.col_offset) == (1, 16) assert (c1.keywords[0].value.end_lineno, c1.keywords[0].value.end_col_offset) == (1, 21) # fmt: on @@ -842,13 +802,8 @@ def test_end_lineno_subscript() -> None: assert isinstance(s3.slice, nodes.Tuple) assert (s3.lineno, s3.col_offset) == (3, 0) assert (s3.end_lineno, s3.end_col_offset) == (3, 11) - if PY39_PLUS: - # 'lineno' and 'col_offset' information only added in Python 3.9 - assert (s3.slice.lineno, s3.slice.col_offset) == (3, 4) - assert (s3.slice.end_lineno, s3.slice.end_col_offset) == (3, 10) - else: - assert (s3.slice.lineno, s3.slice.col_offset) == (None, None) - assert (s3.slice.end_lineno, s3.slice.end_col_offset) == (None, None) + assert (s3.slice.lineno, s3.slice.col_offset) == (3, 4) + assert (s3.slice.end_lineno, s3.slice.end_col_offset) == (3, 10) @staticmethod def test_end_lineno_import() -> None: @@ -995,14 +950,8 @@ def test_end_lineno_string() -> None: assert (s2.end_lineno, s2.end_col_offset) == (1, 29) assert isinstance(s2.value, nodes.Const) # 42.1234 - if PY39_PLUS: - assert (s2.value.lineno, s2.value.col_offset) == (1, 16) - assert (s2.value.end_lineno, s2.value.end_col_offset) == (1, 23) - else: - # Bug in Python 3.8 - # https://bugs.python.org/issue44885 - assert (s2.value.lineno, s2.value.col_offset) == (1, 1) - assert (s2.value.end_lineno, s2.value.end_col_offset) == (1, 8) + assert (s2.value.lineno, s2.value.col_offset) == (1, 16) + assert (s2.value.end_lineno, s2.value.end_col_offset) == (1, 23) assert isinstance(s2.format_spec, nodes.JoinedStr) # ':02d' if PY312_PLUS: assert (s2.format_spec.lineno, s2.format_spec.col_offset) == (1, 23) @@ -1033,14 +982,8 @@ def test_end_lineno_string() -> None: assert (s4.end_lineno, s4.end_col_offset) == (2, 17) assert isinstance(s4.value, nodes.Name) # 'name' - if PY39_PLUS: - assert (s4.value.lineno, s4.value.col_offset) == (2, 10) - assert (s4.value.end_lineno, s4.value.end_col_offset) == (2, 14) - else: - # Bug in Python 3.8 - # https://bugs.python.org/issue44885 - assert (s4.value.lineno, s4.value.col_offset) == (2, 1) - assert (s4.value.end_lineno, s4.value.end_col_offset) == (2, 5) + assert (s4.value.lineno, s4.value.col_offset) == (2, 10) + assert (s4.value.end_lineno, s4.value.end_col_offset) == (2, 14) @staticmethod @pytest.mark.skipif(not PY310_PLUS, reason="pattern matching was added in PY310") @@ -1246,13 +1189,8 @@ class X(Parent, var=42): assert (c1.decorators.end_lineno, c1.decorators.end_col_offset) == (2, 11) assert (c1.bases[0].lineno, c1.bases[0].col_offset) == (3, 8) assert (c1.bases[0].end_lineno, c1.bases[0].end_col_offset) == (3, 14) - if PY39_PLUS: - # 'lineno' and 'col_offset' information only added in Python 3.9 - assert (c1.keywords[0].lineno, c1.keywords[0].col_offset) == (3, 16) - assert (c1.keywords[0].end_lineno, c1.keywords[0].end_col_offset) == (3, 22) - else: - assert (c1.keywords[0].lineno, c1.keywords[0].col_offset) == (None, None) - assert (c1.keywords[0].end_lineno, c1.keywords[0].end_col_offset) == (None, None) + assert (c1.keywords[0].lineno, c1.keywords[0].col_offset) == (3, 16) + assert (c1.keywords[0].end_lineno, c1.keywords[0].end_col_offset) == (3, 22) assert (c1.body[0].lineno, c1.body[0].col_offset) == (4, 4) assert (c1.body[0].end_lineno, c1.body[0].end_col_offset) == (4, 8) # fmt: on diff --git a/tests/test_object_model.py b/tests/test_object_model.py index b4b648a150..9ad4d39a90 100644 --- a/tests/test_object_model.py +++ b/tests/test_object_model.py @@ -8,7 +8,7 @@ import pytest import astroid -from astroid import bases, builder, nodes, objects, test_utils, util +from astroid import bases, builder, nodes, objects, util from astroid.const import PY311_PLUS from astroid.exceptions import InferenceError @@ -362,7 +362,6 @@ def test(self): return 42 self.assertEqual(len(args), 2) self.assertEqual([arg.name for arg in args], ["self", "type"]) - @test_utils.require_version(minver="3.8") def test__get__and_positional_only_args(self): node = builder.extract_node( """ @@ -573,7 +572,6 @@ def test(a: 1, *args: 2, f:4='lala', **kwarg:3)->2: pass self.assertIsInstance(kwdefaults, astroid.Dict) # self.assertEqual(kwdefaults.getitem('f').value, 'lala') - @test_utils.require_version(minver="3.8") def test_annotation_positional_only(self): ast_node = builder.extract_node( """ @@ -857,3 +855,47 @@ def foo(): assert wrapped.name == "foo" cache_info = next(ast_nodes[2].infer()) assert isinstance(cache_info, astroid.Instance) + + +def test_class_annotations() -> None: + """Test that the `__annotations__` attribute is avaiable in the class scope""" + annotations, klass_attribute = builder.extract_node( + """ + class Test: + __annotations__ #@ + Test.__annotations__ #@ + """ + ) + # Test that `__annotations__` attribute is available in the class scope: + assert isinstance(annotations, nodes.Name) + # The `__annotations__` attribute is `Uninferable`: + assert next(annotations.infer()) is astroid.Uninferable + + # Test that we can access the class annotations: + assert isinstance(klass_attribute, nodes.Attribute) + + +def test_class_annotations_typed_dict() -> None: + """Test that we can access class annotations on various TypedDicts""" + apple, pear = builder.extract_node( + """ + from typing import TypedDict + + + class Apple(TypedDict): + a: int + b: str + + + Pear = TypedDict('OtherTypedDict', {'a': int, 'b': str}) + + + Apple.__annotations__ #@ + Pear.__annotations__ #@ + """ + ) + + assert isinstance(apple, nodes.Attribute) + assert next(apple.infer()) is astroid.Uninferable + assert isinstance(pear, nodes.Attribute) + assert next(pear.infer()) is astroid.Uninferable diff --git a/tests/test_python3.py b/tests/test_python3.py index 7593a2ada9..8c3bc16950 100644 --- a/tests/test_python3.py +++ b/tests/test_python3.py @@ -9,7 +9,6 @@ from astroid import exceptions, nodes from astroid.builder import AstroidBuilder, extract_node -from astroid.test_utils import require_version class Python3TC(unittest.TestCase): @@ -351,7 +350,6 @@ def test_async_comprehensions(self) -> None: for comp in non_async_comprehensions: self.assertFalse(comp.generators[0].is_async) - @require_version("3.7") def test_async_comprehensions_outside_coroutine(self): # When async and await will become keywords, async comprehensions # will be allowed outside of coroutines body diff --git a/tests/test_raw_building.py b/tests/test_raw_building.py index d206022b8f..951bf09d90 100644 --- a/tests/test_raw_building.py +++ b/tests/test_raw_building.py @@ -10,6 +10,7 @@ from __future__ import annotations +import _io import logging import os import sys @@ -18,7 +19,6 @@ from typing import Any from unittest import mock -import _io import pytest import tests.testdata.python3.data.fake_module_with_broken_getattr as fm_getattr diff --git a/tests/test_regrtest.py b/tests/test_regrtest.py index 101e1d4417..f7383d25fb 100644 --- a/tests/test_regrtest.py +++ b/tests/test_regrtest.py @@ -11,6 +11,7 @@ from astroid import MANAGER, Instance, bases, manager, nodes, parse, test_utils from astroid.builder import AstroidBuilder, _extract_single_node, extract_node +from astroid.const import PY312_PLUS from astroid.context import InferenceContext from astroid.exceptions import InferenceError from astroid.raw_building import build_module @@ -100,7 +101,7 @@ def test_numpy_crash(self): inferred = callfunc.inferred() self.assertEqual(len(inferred), 1) - @unittest.skipUnless(HAS_NUMPY, "Needs numpy") + @unittest.skipUnless(HAS_NUMPY and not PY312_PLUS, "Needs numpy and < Python 3.12") def test_numpy_distutils(self): """Special handling of virtualenv's patching of distutils shouldn't interfere with numpy.distutils. diff --git a/tests/test_scoped_nodes.py b/tests/test_scoped_nodes.py index 995f0428d9..209710b86a 100644 --- a/tests/test_scoped_nodes.py +++ b/tests/test_scoped_nodes.py @@ -26,11 +26,10 @@ nodes, objects, parse, - test_utils, util, ) from astroid.bases import BoundMethod, Generator, Instance, UnboundMethod -from astroid.const import IS_PYPY, PY38, WIN32 +from astroid.const import WIN32 from astroid.exceptions import ( AstroidBuildingError, AttributeInferenceError, @@ -1210,6 +1209,7 @@ def registered(cls, application): astroid = builder.parse(data, __name__) cls = astroid["WebAppObject"] assert_keys = [ + "__annotations__", "__module__", "__qualname__", "appli", @@ -1349,10 +1349,7 @@ def g2(): astroid = builder.parse(data) self.assertEqual(astroid["g1"].fromlineno, 4) self.assertEqual(astroid["g1"].tolineno, 5) - if PY38 and IS_PYPY: - self.assertEqual(astroid["g2"].fromlineno, 9) - else: - self.assertEqual(astroid["g2"].fromlineno, 10) + self.assertEqual(astroid["g2"].fromlineno, 10) self.assertEqual(astroid["g2"].tolineno, 11) def test_metaclass_error(self) -> None: @@ -2750,13 +2747,11 @@ def __init__(self: int, other: float, /, **kw): ), ], ) -@test_utils.require_version("3.8") def test_posonlyargs_python_38(func): ast_node = builder.extract_node(func) assert ast_node.as_string().strip() == func.strip() -@test_utils.require_version("3.8") def test_posonlyargs_default_value() -> None: ast_node = builder.extract_node( """