New Upstream Release - astroid
Ready changes
Summary
Merged new upstream version: 2.15.5 (was: 2.14.2).
Diff
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 7e60642..c2c025e 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -13,6 +13,10 @@ env:
DEFAULT_PYTHON: "3.11"
PRE_COMMIT_CACHE: ~/.cache/pre-commit
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
jobs:
base-checks:
name: Checks
@@ -36,7 +40,7 @@ jobs:
'requirements_test_pre_commit.txt') }}" >> $GITHUB_OUTPUT
- name: Restore Python virtual environment
id: cache-venv
- uses: actions/cache@v3.2.4
+ uses: actions/cache@v3.2.6
with:
path: venv
key: >-
@@ -56,7 +60,7 @@ jobs:
hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Restore pre-commit environment
id: cache-precommit
- uses: actions/cache@v3.2.4
+ uses: actions/cache@v3.2.6
with:
path: ${{ env.PRE_COMMIT_CACHE }}
key: >-
@@ -104,7 +108,7 @@ jobs:
'requirements_test_pre_commit.txt') }}" >> $GITHUB_OUTPUT
- name: Restore Python virtual environment
id: cache-venv
- uses: actions/cache@v3.2.4
+ uses: actions/cache@v3.2.6
with:
path: venv
key: >-
@@ -158,7 +162,7 @@ jobs:
'requirements_test_brain.txt') }}" >> $env:GITHUB_OUTPUT
- name: Restore Python virtual environment
id: cache-venv
- uses: actions/cache@v3.2.4
+ uses: actions/cache@v3.2.6
with:
path: venv
key: >-
@@ -207,7 +211,7 @@ jobs:
}}" >> $GITHUB_OUTPUT
- name: Restore Python virtual environment
id: cache-venv
- uses: actions/cache@v3.2.4
+ uses: actions/cache@v3.2.6
with:
path: venv
key: >-
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 550ec97..51c5665 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -23,6 +23,10 @@ on:
permissions:
contents: read
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
jobs:
analyze:
name: Analyze
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f3be0c3..9b5cc55 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,7 +10,7 @@ repos:
- id: end-of-file-fixer
exclude: tests/testdata
- repo: https://github.com/PyCQA/autoflake
- rev: v2.0.0
+ rev: v2.0.1
hooks:
- id: autoflake
exclude: tests/testdata|astroid/__init__.py|astroid/scoped_nodes.py|astroid/node_classes.py
@@ -42,9 +42,9 @@ repos:
rev: v1.1.3
hooks:
- id: black-disable-checker
- exclude: tests/unittest_nodes_lineno.py
+ exclude: tests/test_nodes_lineno.py
- repo: https://github.com/psf/black
- rev: 22.12.0
+ rev: 23.1.0
hooks:
- id: black
args: [--safe, --quiet]
@@ -54,7 +54,7 @@ repos:
hooks:
- id: flake8
additional_dependencies:
- [flake8-bugbear==22.10.27, flake8-typing-imports==1.14.0]
+ [flake8-bugbear==23.2.13, flake8-typing-imports==1.14.0]
exclude: tests/testdata|doc/conf.py
- repo: local
hooks:
@@ -71,7 +71,7 @@ repos:
]
exclude: tests/testdata|conf.py
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.991
+ rev: v1.0.1
hooks:
- id: mypy
name: mypy
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 24a11da..685f8a0 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -41,9 +41,9 @@ Contributors
- Julien Jehannet <julien.jehannet@logilab.fr>
- Calen Pennington <calen.pennington@gmail.com>
- Tushar Sadhwani <86737547+tushar-deepsource@users.noreply.github.com>
+- Hugo van Kemenade <hugovk@users.noreply.github.com>
- Tim Martin <tim@asymptotic.co.uk>
- Phil Schaf <flying-sheep@web.de>
-- Hugo van Kemenade <hugovk@users.noreply.github.com>
- Alex Hall <alex.mojaki@gmail.com>
- Raphael Gaschignard <raphael@makeleaps.com>
- Radosław Ganczarek <radoslaw@ganczarek.in>
@@ -100,6 +100,8 @@ Contributors
- rr- <rr-@sakuya.pl>
- raylu <lurayl@gmail.com>
- plucury <plucury@gmail.com>
+- ostr00000 <ostr00000@gmail.com>
+- noah-weingarden <33741795+noah-weingarden@users.noreply.github.com>
- nathannaveen <42319948+nathannaveen@users.noreply.github.com>
- mathieui <mathieui@users.noreply.github.com>
- markmcclain <markmcclain@users.noreply.github.com>
@@ -175,6 +177,7 @@ Contributors
- Batuhan Taskaya <isidentical@gmail.com>
- BasPH <BasPH@users.noreply.github.com>
- Azeem Bande-Ali <A.BandeAli@gmail.com>
+- Avram Lubkin <aviso@rockhopper.net>
- Aru Sahni <arusahni@gmail.com>
- Artsiom Kaval <lezeroq@gmail.com>
- Anubhav <35621759+anubh-v@users.noreply.github.com>
diff --git a/ChangeLog b/ChangeLog
index e38393d..f29525c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,14 +2,114 @@
astroid's ChangeLog
===================
-What's New in astroid 2.15.0?
+What's New in astroid 2.16.0?
=============================
Release date: TBA
+What's New in astroid 2.15.6?
+=============================
+Release date: TBA
+
+
+
+What's New in astroid 2.15.5?
+=============================
+Release date: 2023-05-14
+
+* Handle ``objects.Super`` in ``helpers.object_type()``.
+
+ Refs pylint-dev/pylint#8554
+
+
+What's New in astroid 2.15.4?
+=============================
+Release date: 2023-04-24
+
+* Add visitor function for ``TryStar`` to ``AsStringVisitor`` and
+ add ``TryStar`` to ``astroid.nodes.ALL_NODE_CLASSES``.
+
+ Refs #2142
+
+
+What's New in astroid 2.15.3?
+=============================
+Release date: 2023-04-16
+
+* Fix ``infer_call_result()`` crash on methods called ``with_metaclass()``.
+
+ Closes #1735
+
+* Suppress ``UserWarning`` when finding module specs.
+
+ Closes pylint-dev/pylint#7906
+
+
+What's New in astroid 2.15.2?
+=============================
+Release date: 2023-04-02
+
+* Support more possible usages of ``attrs`` decorators.
+
+ Closes pylint-dev/pylint#7884
+
+
+What's New in astroid 2.15.1?
+=============================
+Release date: 2023-03-26
+
+* Restore behavior of setting a Call as a base for classes created using ``six.with_metaclass()``,
+ and harden support for using enums as metaclasses in this case.
+
+ Reverts #1622
+ Refs PyCQA/pylint#5935
+ Refs PyCQA/pylint#7506
+
+
+What's New in astroid 2.15.0?
+=============================
+Release date: 2023-03-06
+
+* astroid now supports ``TryStar`` nodes from python 3.11 and should be fully compatible with python 3.11.
+
+ Closes #2028
+
+* ``Formattedvalue.postinit`` is now keyword only. This is to allow correct typing of the
+ ``Formattedvalue`` class.
+
+ Refs #1516
+
+* ``Astroid`` now supports custom import hooks.
+
+ Refs PyCQA/pylint#7306
+
+* ``astroid`` now infers return values from match cases.
+
+ Refs PyCQA/pylint#5288
+
+* ``AstroidManager.clear_cache`` now also clears the inference context cache.
+
+ Refs #1780
+
+* ``Astroid`` now retrieves the default values of keyword only arguments and sets them on
+ ``Arguments.kw_defaults``.
+
+* ``Uninferable`` now has the type ``UninferableBase``. This is to facilitate correctly type annotating
+ code that uses this singleton.
+
+ Closes #1680
+
+* Deprecate ``modutils.is_standard_module()``. It will be removed in the next minor release.
+ Functionality has been replaced by two new functions,
+ ``modutils.is_stdlib_module()`` and ``modutils.module_in_path()``.
+
+ Closes #2012
+
+* Fix ``are_exclusive`` function when a walrus operator is used inside ``IfExp.test`` field.
+ Closes #2022
What's New in astroid 2.14.2?
=============================
diff --git a/astroid/__init__.py b/astroid/__init__.py
index 605a8b4..bcb0c2c 100644
--- a/astroid/__init__.py
+++ b/astroid/__init__.py
@@ -165,6 +165,7 @@ from astroid.nodes import ( # pylint: disable=redefined-builtin (Ellipsis)
Subscript,
TryExcept,
TryFinally,
+ TryStar,
Tuple,
UnaryOp,
Unknown,
diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py
index 9742a07..ab8f0f4 100644
--- a/astroid/__pkginfo__.py
+++ b/astroid/__pkginfo__.py
@@ -2,5 +2,5 @@
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
-__version__ = "2.14.2"
+__version__ = "2.15.5"
version = __version__
diff --git a/astroid/_backport_stdlib_names.py b/astroid/_backport_stdlib_names.py
new file mode 100644
index 0000000..51d6957
--- /dev/null
+++ b/astroid/_backport_stdlib_names.py
@@ -0,0 +1,356 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+"""
+Shim to support Python versions < 3.10 that don't have sys.stdlib_module_names
+
+These values were created by cherry-picking the commits from
+https://bugs.python.org/issue42955 into each version, but may be updated
+manually if changes are needed.
+"""
+
+import sys
+
+# TODO: Remove this file when Python 3.9 is no longer supported
+
+PY_3_7 = frozenset(
+ {
+ "__future__",
+ "_abc",
+ "_ast",
+ "_asyncio",
+ "_bisect",
+ "_blake2",
+ "_bootlocale",
+ "_bz2",
+ "_codecs",
+ "_codecs_cn",
+ "_codecs_hk",
+ "_codecs_iso2022",
+ "_codecs_jp",
+ "_codecs_kr",
+ "_codecs_tw",
+ "_collections",
+ "_collections_abc",
+ "_compat_pickle",
+ "_compression",
+ "_contextvars",
+ "_crypt",
+ "_csv",
+ "_ctypes",
+ "_curses",
+ "_curses_panel",
+ "_datetime",
+ "_dbm",
+ "_decimal",
+ "_dummy_thread",
+ "_elementtree",
+ "_functools",
+ "_gdbm",
+ "_hashlib",
+ "_heapq",
+ "_imp",
+ "_io",
+ "_json",
+ "_locale",
+ "_lsprof",
+ "_lzma",
+ "_markupbase",
+ "_md5",
+ "_msi",
+ "_multibytecodec",
+ "_multiprocessing",
+ "_opcode",
+ "_operator",
+ "_osx_support",
+ "_pickle",
+ "_posixsubprocess",
+ "_py_abc",
+ "_pydecimal",
+ "_pyio",
+ "_queue",
+ "_random",
+ "_sha1",
+ "_sha256",
+ "_sha3",
+ "_sha512",
+ "_signal",
+ "_sitebuiltins",
+ "_socket",
+ "_sqlite3",
+ "_sre",
+ "_ssl",
+ "_stat",
+ "_string",
+ "_strptime",
+ "_struct",
+ "_symtable",
+ "_thread",
+ "_threading_local",
+ "_tkinter",
+ "_tracemalloc",
+ "_uuid",
+ "_warnings",
+ "_weakref",
+ "_weakrefset",
+ "_winapi",
+ "abc",
+ "aifc",
+ "antigravity",
+ "argparse",
+ "array",
+ "ast",
+ "asynchat",
+ "asyncio",
+ "asyncore",
+ "atexit",
+ "audioop",
+ "base64",
+ "bdb",
+ "binascii",
+ "binhex",
+ "bisect",
+ "builtins",
+ "bz2",
+ "cProfile",
+ "calendar",
+ "cgi",
+ "cgitb",
+ "chunk",
+ "cmath",
+ "cmd",
+ "code",
+ "codecs",
+ "codeop",
+ "collections",
+ "colorsys",
+ "compileall",
+ "concurrent",
+ "configparser",
+ "contextlib",
+ "contextvars",
+ "copy",
+ "copyreg",
+ "crypt",
+ "csv",
+ "ctypes",
+ "curses",
+ "dataclasses",
+ "datetime",
+ "dbm",
+ "decimal",
+ "difflib",
+ "dis",
+ "distutils",
+ "doctest",
+ "dummy_threading",
+ "email",
+ "encodings",
+ "ensurepip",
+ "enum",
+ "errno",
+ "faulthandler",
+ "fcntl",
+ "filecmp",
+ "fileinput",
+ "fnmatch",
+ "formatter",
+ "fractions",
+ "ftplib",
+ "functools",
+ "gc",
+ "genericpath",
+ "getopt",
+ "getpass",
+ "gettext",
+ "glob",
+ "grp",
+ "gzip",
+ "hashlib",
+ "heapq",
+ "hmac",
+ "html",
+ "http",
+ "idlelib",
+ "imaplib",
+ "imghdr",
+ "imp",
+ "importlib",
+ "inspect",
+ "io",
+ "ipaddress",
+ "itertools",
+ "json",
+ "keyword",
+ "lib2to3",
+ "linecache",
+ "locale",
+ "logging",
+ "lzma",
+ "macpath",
+ "mailbox",
+ "mailcap",
+ "marshal",
+ "math",
+ "mimetypes",
+ "mmap",
+ "modulefinder",
+ "msilib",
+ "msvcrt",
+ "multiprocessing",
+ "netrc",
+ "nis",
+ "nntplib",
+ "nt",
+ "ntpath",
+ "nturl2path",
+ "numbers",
+ "opcode",
+ "operator",
+ "optparse",
+ "os",
+ "ossaudiodev",
+ "parser",
+ "pathlib",
+ "pdb",
+ "pickle",
+ "pickletools",
+ "pipes",
+ "pkgutil",
+ "platform",
+ "plistlib",
+ "poplib",
+ "posix",
+ "posixpath",
+ "pprint",
+ "profile",
+ "pstats",
+ "pty",
+ "pwd",
+ "py_compile",
+ "pyclbr",
+ "pydoc",
+ "pydoc_data",
+ "pyexpat",
+ "queue",
+ "quopri",
+ "random",
+ "re",
+ "readline",
+ "reprlib",
+ "resource",
+ "rlcompleter",
+ "runpy",
+ "sched",
+ "secrets",
+ "select",
+ "selectors",
+ "shelve",
+ "shlex",
+ "shutil",
+ "signal",
+ "site",
+ "smtpd",
+ "smtplib",
+ "sndhdr",
+ "socket",
+ "socketserver",
+ "spwd",
+ "sqlite3",
+ "sre_compile",
+ "sre_constants",
+ "sre_parse",
+ "ssl",
+ "stat",
+ "statistics",
+ "string",
+ "stringprep",
+ "struct",
+ "subprocess",
+ "sunau",
+ "symbol",
+ "symtable",
+ "sys",
+ "sysconfig",
+ "syslog",
+ "tabnanny",
+ "tarfile",
+ "telnetlib",
+ "tempfile",
+ "termios",
+ "textwrap",
+ "this",
+ "threading",
+ "time",
+ "timeit",
+ "tkinter",
+ "token",
+ "tokenize",
+ "trace",
+ "traceback",
+ "tracemalloc",
+ "tty",
+ "turtle",
+ "turtledemo",
+ "types",
+ "typing",
+ "unicodedata",
+ "unittest",
+ "urllib",
+ "uu",
+ "uuid",
+ "venv",
+ "warnings",
+ "wave",
+ "weakref",
+ "webbrowser",
+ "winreg",
+ "winsound",
+ "wsgiref",
+ "xdrlib",
+ "xml",
+ "xmlrpc",
+ "zipapp",
+ "zipfile",
+ "zipimport",
+ "zlib",
+ }
+)
+
+PY_3_8 = frozenset(
+ PY_3_7
+ - {
+ "macpath",
+ }
+ | {
+ "_posixshmem",
+ "_statistics",
+ "_xxsubinterpreters",
+ }
+)
+
+PY_3_9 = frozenset(
+ PY_3_8
+ - {
+ "_dummy_thread",
+ "dummy_threading",
+ }
+ | {
+ "_aix_support",
+ "_bootsubprocess",
+ "_peg_parser",
+ "_zoneinfo",
+ "graphlib",
+ "zoneinfo",
+ }
+)
+
+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):
+ 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/arguments.py b/astroid/arguments.py
index 8ac83dc..5936995 100644
--- a/astroid/arguments.py
+++ b/astroid/arguments.py
@@ -8,7 +8,7 @@ from astroid import nodes
from astroid.bases import Instance
from astroid.context import CallContext, InferenceContext
from astroid.exceptions import InferenceError, NoDefault
-from astroid.util import Uninferable
+from astroid.util import Uninferable, UninferableBase
class CallSite:
@@ -44,12 +44,12 @@ class CallSite:
self._unpacked_kwargs = self._unpack_keywords(keywords, context=context)
self.positional_arguments = [
- arg for arg in self._unpacked_args if arg is not Uninferable
+ arg for arg in self._unpacked_args if not isinstance(arg, UninferableBase)
]
self.keyword_arguments = {
key: value
for key, value in self._unpacked_kwargs.items()
- if value is not Uninferable
+ if not isinstance(value, UninferableBase)
}
@classmethod
@@ -142,7 +142,7 @@ class CallSite:
except StopIteration:
continue
- if inferred is Uninferable:
+ if isinstance(inferred, UninferableBase):
values.append(Uninferable)
continue
if not hasattr(inferred, "elts"):
diff --git a/astroid/bases.py b/astroid/bases.py
index e930328..d1972c1 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -28,7 +28,7 @@ from astroid.exceptions import (
NameInferenceError,
)
from astroid.typing import InferBinaryOp, InferenceErrorInfo, InferenceResult
-from astroid.util import Uninferable, lazy_descriptor, lazy_import
+from astroid.util import Uninferable, UninferableBase, lazy_descriptor, lazy_import
if sys.version_info >= (3, 8):
from typing import Literal
@@ -79,7 +79,9 @@ def _is_property(meth, context: InferenceContext | None = None) -> bool:
if PROPERTIES.intersection(decoratornames):
return True
stripped = {
- name.split(".")[-1] for name in decoratornames if name is not Uninferable
+ name.split(".")[-1]
+ for name in decoratornames
+ if not isinstance(name, UninferableBase)
}
if any(name in stripped for name in POSSIBLE_PROPERTIES):
return True
@@ -89,7 +91,7 @@ def _is_property(meth, context: InferenceContext | None = None) -> bool:
return False
for decorator in meth.decorators.nodes or ():
inferred = helpers.safe_infer(decorator, context=context)
- if inferred is None or inferred is Uninferable:
+ if inferred is None or isinstance(inferred, UninferableBase):
continue
if inferred.__class__.__name__ == "ClassDef":
for base_class in inferred.bases:
@@ -144,7 +146,7 @@ class Proxy:
def _infer_stmts(
- stmts: Sequence[nodes.NodeNG | type[Uninferable] | Instance],
+ stmts: Sequence[nodes.NodeNG | UninferableBase | Instance],
context: InferenceContext | None,
frame: nodes.NodeNG | Instance | None = None,
) -> collections.abc.Generator[InferenceResult, None, None]:
@@ -161,7 +163,7 @@ def _infer_stmts(
context = InferenceContext()
for stmt in stmts:
- if stmt is Uninferable:
+ if isinstance(stmt, UninferableBase):
yield stmt
inferred = True
continue
@@ -172,8 +174,7 @@ def _infer_stmts(
for constraint_stmt, potential_constraints in constraints.items():
if not constraint_stmt.parent_of(stmt):
stmt_constraints.update(potential_constraints)
- # Mypy doesn't recognize that 'stmt' can't be Uninferable
- for inf in stmt.infer(context=context): # type: ignore[union-attr]
+ for inf in stmt.infer(context=context):
if all(constraint.satisfied_by(inf) for constraint in stmt_constraints):
yield inf
inferred = True
@@ -206,7 +207,7 @@ def _infer_method_result_truth(instance, method_name, context):
try:
context.callcontext = CallContext(args=[], callee=meth)
for value in meth.infer_call_result(instance, context=context):
- if value is Uninferable:
+ if isinstance(value, UninferableBase):
return value
try:
inferred = next(value.infer(context=context))
@@ -316,7 +317,7 @@ class BaseInstance(Proxy):
# Otherwise we infer the call to the __call__ dunder normally
for node in self._proxied.igetattr("__call__", context):
- if node is Uninferable or not node.callable():
+ if isinstance(node, UninferableBase) or not node.callable():
continue
for res in node.infer_call_result(caller, context):
inferred = True
@@ -458,7 +459,7 @@ class UnboundMethod(Proxy):
caller: nodes.Call,
context: InferenceContext,
) -> collections.abc.Generator[
- nodes.Const | Instance | type[Uninferable], None, None
+ nodes.Const | Instance | UninferableBase, None, None
]:
if not caller.args:
return
@@ -477,7 +478,7 @@ class UnboundMethod(Proxy):
node_context = context.extra_context.get(caller.args[0])
for inferred in caller.args[0].infer(context=node_context):
- if inferred is Uninferable:
+ if isinstance(inferred, UninferableBase):
yield inferred
if isinstance(inferred, nodes.ClassDef):
yield Instance(inferred)
diff --git a/astroid/brain/brain_attrs.py b/astroid/brain/brain_attrs.py
index acb069e..7afcc8a 100644
--- a/astroid/brain/brain_attrs.py
+++ b/astroid/brain/brain_attrs.py
@@ -8,6 +8,7 @@ Astroid hook for the attrs library
Without this hook pylint reports unsupported-assignment-operation
for attrs classes
"""
+from astroid.helpers import safe_infer
from astroid.manager import AstroidManager
from astroid.nodes.node_classes import AnnAssign, Assign, AssignName, Call, Unknown
from astroid.nodes.scoped_nodes import ClassDef
@@ -40,6 +41,10 @@ def is_decorated_with_attrs(node, decorator_names=ATTRS_NAMES) -> bool:
decorator_attribute = decorator_attribute.func
if decorator_attribute.as_string() in decorator_names:
return True
+
+ inferred = safe_infer(decorator_attribute)
+ if inferred and inferred.root().name == "attr._next_gen":
+ return True
return False
diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py
index 02e0ecc..383621d 100644
--- a/astroid/brain/brain_builtin_inference.py
+++ b/astroid/brain/brain_builtin_inference.py
@@ -209,10 +209,10 @@ def _container_generic_inference(node, context, node_type, transform):
inferred = next(arg.infer(context=context))
except (InferenceError, StopIteration) as exc:
raise UseInferenceDefault from exc
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
raise UseInferenceDefault
transformed = transform(inferred)
- if not transformed or transformed is util.Uninferable:
+ if not transformed or isinstance(transformed, util.UninferableBase):
raise UseInferenceDefault
return transformed
@@ -423,7 +423,9 @@ def infer_super(node, context: InferenceContext | None = None):
except (InferenceError, StopIteration) as exc:
raise UseInferenceDefault from exc
- if mro_pointer is util.Uninferable or mro_type is util.Uninferable:
+ if isinstance(mro_pointer, util.UninferableBase) or isinstance(
+ mro_type, util.UninferableBase
+ ):
# No way we could understand this.
raise UseInferenceDefault
@@ -445,7 +447,7 @@ def _infer_getattr_args(node, context):
except (InferenceError, StopIteration) as exc:
raise UseInferenceDefault from exc
- if obj is util.Uninferable or attr is util.Uninferable:
+ if isinstance(obj, util.UninferableBase) or isinstance(attr, util.UninferableBase):
# If one of the arguments is something we can't infer,
# then also make the result of the getattr call something
# which is unknown.
@@ -467,8 +469,8 @@ def infer_getattr(node, context: InferenceContext | None = None):
"""
obj, attr = _infer_getattr_args(node, context)
if (
- obj is util.Uninferable
- or attr is util.Uninferable
+ isinstance(obj, util.UninferableBase)
+ or isinstance(attr, util.UninferableBase)
or not hasattr(obj, "igetattr")
):
return util.Uninferable
@@ -498,8 +500,8 @@ def infer_hasattr(node, context: InferenceContext | None = None):
try:
obj, attr = _infer_getattr_args(node, context)
if (
- obj is util.Uninferable
- or attr is util.Uninferable
+ isinstance(obj, util.UninferableBase)
+ or isinstance(attr, util.UninferableBase)
or not hasattr(obj, "getattr")
):
return util.Uninferable
@@ -530,7 +532,7 @@ def infer_callable(node, context: InferenceContext | None = None):
inferred = next(argument.infer(context=context))
except (InferenceError, StopIteration):
return util.Uninferable
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
return util.Uninferable
return nodes.Const(inferred.callable())
@@ -585,11 +587,11 @@ def infer_bool(node, context: InferenceContext | None = None):
inferred = next(argument.infer(context=context))
except (InferenceError, StopIteration):
return util.Uninferable
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
return util.Uninferable
bool_value = inferred.bool_value(context=context)
- if bool_value is util.Uninferable:
+ if isinstance(bool_value, util.UninferableBase):
return util.Uninferable
return nodes.Const(bool_value)
@@ -611,7 +613,7 @@ def infer_slice(node, context: InferenceContext | None = None):
infer_func = partial(helpers.safe_infer, context=context)
args = [infer_func(arg) for arg in args]
for arg in args:
- if not arg or arg is util.Uninferable:
+ if not arg or isinstance(arg, util.UninferableBase):
raise UseInferenceDefault
if not isinstance(arg, nodes.Const):
raise UseInferenceDefault
@@ -725,7 +727,7 @@ def infer_isinstance(callnode, context: InferenceContext | None = None):
raise UseInferenceDefault("TypeError: " + str(exc)) from exc
except MroError as exc:
raise UseInferenceDefault from exc
- if isinstance_bool is util.Uninferable:
+ if isinstance(isinstance_bool, util.UninferableBase):
raise UseInferenceDefault
return nodes.Const(isinstance_bool)
@@ -811,7 +813,7 @@ def infer_int(node, context: InferenceContext | None = None):
except (InferenceError, StopIteration) as exc:
raise UseInferenceDefault(str(exc)) from exc
- if first_value is util.Uninferable:
+ if isinstance(first_value, util.UninferableBase):
raise UseInferenceDefault
if isinstance(first_value, nodes.Const) and isinstance(
@@ -924,7 +926,7 @@ def _is_str_format_call(node: nodes.Call) -> bool:
def _infer_str_format_call(
node: nodes.Call, context: InferenceContext | None = None
-) -> Iterator[nodes.Const | type[util.Uninferable]]:
+) -> Iterator[nodes.Const | util.UninferableBase]:
"""Return a Const node based on the template and passed arguments."""
call = arguments.CallSite.from_call(node, context=context)
if isinstance(node.func.expr, nodes.Name):
diff --git a/astroid/brain/brain_dataclasses.py b/astroid/brain/brain_dataclasses.py
index a2e7adf..1397ed1 100644
--- a/astroid/brain/brain_dataclasses.py
+++ b/astroid/brain/brain_dataclasses.py
@@ -25,7 +25,7 @@ from astroid.exceptions import AstroidSyntaxError, InferenceError, UseInferenceD
from astroid.inference_tip import inference_tip
from astroid.manager import AstroidManager
from astroid.typing import InferenceResult
-from astroid.util import Uninferable
+from astroid.util import Uninferable, UninferableBase
if sys.version_info >= (3, 8):
from typing import Literal
@@ -446,7 +446,7 @@ def _looks_like_dataclass_decorator(
except (InferenceError, StopIteration):
inferred = Uninferable
- if inferred is Uninferable:
+ if isinstance(inferred, UninferableBase):
if isinstance(node, nodes.Name):
return node.name in decorator_names
if isinstance(node, nodes.Attribute):
@@ -594,7 +594,7 @@ _INFERABLE_TYPING_TYPES = frozenset(
def _infer_instance_from_annotation(
node: nodes.NodeNG, ctx: context.InferenceContext | None = None
-) -> Iterator[type[Uninferable] | bases.Instance]:
+) -> Iterator[UninferableBase | bases.Instance]:
"""Infer an instance corresponding to the type annotation represented by node.
Currently has limited support for the typing module.
diff --git a/astroid/brain/brain_fstrings.py b/astroid/brain/brain_fstrings.py
index db7dd95..c0df22e 100644
--- a/astroid/brain/brain_fstrings.py
+++ b/astroid/brain/brain_fstrings.py
@@ -2,13 +2,20 @@
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+from __future__ import annotations
+
import collections.abc
+from typing import TypeVar
+from astroid import nodes
from astroid.manager import AstroidManager
-from astroid.nodes.node_classes import FormattedValue
+
+_NodeT = TypeVar("_NodeT", bound=nodes.NodeNG)
-def _clone_node_with_lineno(node, parent, lineno):
+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
@@ -28,16 +35,22 @@ def _clone_node_with_lineno(node, parent, lineno):
return new_node
-def _transform_formatted_value(node): # pylint: disable=inconsistent-return-statements
+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 = FormattedValue(
+ new_node = nodes.FormattedValue(
lineno=node.lineno, col_offset=node.col_offset, parent=node.parent
)
new_value = _clone_node_with_lineno(
node=node.value, lineno=node.lineno, parent=new_node
)
- new_node.postinit(value=new_value, format_spec=node.format_spec)
+ new_node.postinit(
+ value=new_value,
+ conversion=node.conversion,
+ format_spec=node.format_spec,
+ )
return new_node
@@ -45,4 +58,4 @@ def _transform_formatted_value(node): # pylint: disable=inconsistent-return-sta
# 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.
-AstroidManager().register_transform(FormattedValue, _transform_formatted_value)
+AstroidManager().register_transform(nodes.FormattedValue, _transform_formatted_value)
diff --git a/astroid/brain/brain_functools.py b/astroid/brain/brain_functools.py
index ffdbc88..f6a9830 100644
--- a/astroid/brain/brain_functools.py
+++ b/astroid/brain/brain_functools.py
@@ -18,7 +18,7 @@ from astroid.interpreter import objectmodel
from astroid.manager import AstroidManager
from astroid.nodes.node_classes import AssignName, Attribute, Call, Name
from astroid.nodes.scoped_nodes import FunctionDef
-from astroid.util import Uninferable
+from astroid.util import UninferableBase
LRU_CACHE = "functools.lru_cache"
@@ -84,7 +84,7 @@ def _functools_partial_inference(
inferred_wrapped_function = next(partial_function.infer(context=context))
except (InferenceError, StopIteration) as exc:
raise UseInferenceDefault from exc
- if inferred_wrapped_function is Uninferable:
+ if isinstance(inferred_wrapped_function, UninferableBase):
raise UseInferenceDefault("Cannot infer the wrapped function")
if not isinstance(inferred_wrapped_function, FunctionDef):
raise UseInferenceDefault("The wrapped function is not a function")
diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py
index f914162..36b7036 100644
--- a/astroid/brain/brain_namedtuple_enum.py
+++ b/astroid/brain/brain_namedtuple_enum.py
@@ -52,13 +52,13 @@ TYPING_NAMEDTUPLE_BASENAMES: Final = {
def _infer_first(node, context):
- if node is util.Uninferable:
+ if isinstance(node, util.UninferableBase):
raise UseInferenceDefault
try:
value = next(node.infer(context=context))
except StopIteration as exc:
raise InferenceError from exc
- if value is util.Uninferable:
+ if isinstance(value, util.UninferableBase):
raise UseInferenceDefault()
return value
diff --git a/astroid/brain/brain_six.py b/astroid/brain/brain_six.py
index a35cfdd..0eb945d 100644
--- a/astroid/brain/brain_six.py
+++ b/astroid/brain/brain_six.py
@@ -219,7 +219,6 @@ def transform_six_with_metaclass(node):
"""
call = node.bases[0]
node._metaclass = call.args[0]
- node.bases = call.args[1:]
return node
diff --git a/astroid/brain/brain_typing.py b/astroid/brain/brain_typing.py
index b11bfa1..e0a9dfd 100644
--- a/astroid/brain/brain_typing.py
+++ b/astroid/brain/brain_typing.py
@@ -33,7 +33,6 @@ from astroid.nodes.node_classes import (
Tuple,
)
from astroid.nodes.scoped_nodes import ClassDef, FunctionDef
-from astroid.util import Uninferable
if sys.version_info >= (3, 8):
from typing import Final
@@ -297,7 +296,7 @@ def infer_typing_alias(
col_offset=assign_name.col_offset,
parent=node.parent,
)
- if res != Uninferable and isinstance(res, ClassDef):
+ if isinstance(res, ClassDef):
# Only add `res` as base if it's a `ClassDef`
# This isn't the case for `typing.Pattern` and `typing.Match`
class_def.postinit(bases=[res], body=[], decorators=None)
diff --git a/astroid/builder.py b/astroid/builder.py
index a03bd98..d115feb 100644
--- a/astroid/builder.py
+++ b/astroid/builder.py
@@ -238,7 +238,7 @@ class AstroidBuilder(raw_building.InspectBuilder):
try:
frame = node.frame(future=True)
for inferred in node.expr.infer():
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
continue
try:
# pylint: disable=unidiomatic-typecheck # We want a narrow check on the
@@ -255,10 +255,7 @@ class AstroidBuilder(raw_building.InspectBuilder):
# Const, Tuple or other containers that inherit from
# `Instance`
continue
- elif (
- isinstance(inferred, bases.Proxy)
- or inferred is util.Uninferable
- ):
+ elif isinstance(inferred, (bases.Proxy, util.UninferableBase)):
continue
elif inferred.is_function:
iattrs = inferred.instance_attrs
diff --git a/astroid/constraint.py b/astroid/constraint.py
index deed9ac..b6dc35c 100644
--- a/astroid/constraint.py
+++ b/astroid/constraint.py
@@ -74,7 +74,7 @@ class NoneConstraint(Constraint):
def satisfied_by(self, inferred: InferenceResult) -> bool:
"""Return True if this constraint is satisfied by the given inferred value."""
# Assume true if uninferable
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
return True
# Return the XOR of self.negate and matches(inferred, self.CONST_NONE)
diff --git a/astroid/decorators.py b/astroid/decorators.py
index b99803a..6bba37b 100644
--- a/astroid/decorators.py
+++ b/astroid/decorators.py
@@ -67,6 +67,7 @@ class cachedproperty:
"cachedproperty has been deprecated and will be removed in astroid 3.0 for Python 3.8+. "
"Use functools.cached_property instead.",
DeprecationWarning,
+ stacklevel=2,
)
try:
wrapped.__name__
@@ -214,6 +215,7 @@ if util.check_warnings_filter(): # noqa: C901
f" in astroid {astroid_version} "
f"('{arg}' should be of type: '{type_annotation}')",
DeprecationWarning,
+ stacklevel=2,
)
return func(*args, **kwargs)
@@ -251,6 +253,7 @@ if util.check_warnings_filter(): # noqa: C901
f"'{args[0].__class__.__qualname__}.{func.__name__}' is deprecated "
f"and will be removed in astroid {astroid_version} ({note})",
DeprecationWarning,
+ stacklevel=2,
)
return func(*args, **kwargs)
diff --git a/astroid/helpers.py b/astroid/helpers.py
index 8ab01b8..bb19bbf 100644
--- a/astroid/helpers.py
+++ b/astroid/helpers.py
@@ -8,7 +8,7 @@ from __future__ import annotations
from collections.abc import Generator
-from astroid import bases, manager, nodes, raw_building, util
+from astroid import bases, manager, nodes, objects, raw_building, util
from astroid.context import CallContext, InferenceContext
from astroid.exceptions import (
AstroidTypeError,
@@ -63,9 +63,9 @@ def _object_type(
yield _build_proxy_class("module", builtins)
elif isinstance(inferred, nodes.Unknown):
raise InferenceError
- elif inferred is util.Uninferable:
+ elif isinstance(inferred, util.UninferableBase):
yield inferred
- elif isinstance(inferred, (bases.Proxy, nodes.Slice)):
+ elif isinstance(inferred, (bases.Proxy, nodes.Slice, objects.Super)):
yield inferred._proxied
else: # pragma: no cover
raise AssertionError(f"We don't handle {type(inferred)} currently")
@@ -100,7 +100,7 @@ def _object_type_is_subclass(
else:
class_seq = class_or_seq
- if obj_type is util.Uninferable:
+ if isinstance(obj_type, util.UninferableBase):
return util.Uninferable
# Instances are not types
@@ -112,7 +112,7 @@ def _object_type_is_subclass(
# issubclass(type, (object, 1)) evaluates to true
# issubclass(object, (1, type)) raises TypeError
for klass in class_seq:
- if klass is util.Uninferable:
+ if isinstance(klass, util.UninferableBase):
raise AstroidTypeError("arg 2 must be a type or tuple of types")
for obj_subclass in obj_type.mro():
@@ -131,7 +131,7 @@ def object_isinstance(node, class_or_seq, context: InferenceContext | None = Non
:raises AstroidTypeError: if the given ``classes_or_seq`` are not types
"""
obj_type = object_type(node, context)
- if obj_type is util.Uninferable:
+ if isinstance(obj_type, util.UninferableBase):
return util.Uninferable
return _object_type_is_subclass(obj_type, class_or_seq, context=context)
@@ -275,7 +275,7 @@ def object_len(node, context: InferenceContext | None = None):
)
raise InferenceError(message)
- if inferred_node is None or inferred_node is util.Uninferable:
+ if inferred_node is None or isinstance(inferred_node, util.UninferableBase):
raise InferenceError(node=node)
if isinstance(inferred_node, nodes.Const) and isinstance(
inferred_node.value, (bytes, str)
@@ -300,7 +300,7 @@ def object_len(node, context: InferenceContext | None = None):
) from e
inferred = len_call.infer_call_result(node, context)
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
raise InferenceError(node=node, context=context)
result_of_len = next(inferred, None)
if (
diff --git a/astroid/inference.py b/astroid/inference.py
index dd0a487..65d03d3 100644
--- a/astroid/inference.py
+++ b/astroid/inference.py
@@ -268,7 +268,7 @@ def infer_call(
callcontext.extra_context = _populate_context_lookup(self, context.clone())
for callee in self.func.infer(context):
- if callee is util.Uninferable:
+ if isinstance(callee, util.UninferableBase):
yield callee
continue
try:
@@ -356,7 +356,7 @@ def infer_attribute(
) -> Generator[InferenceResult, None, InferenceErrorInfo]:
"""Infer an Attribute node by using getattr on the associated object."""
for owner in self.expr.infer(context):
- if owner is util.Uninferable:
+ if isinstance(owner, util.UninferableBase):
yield owner
continue
@@ -424,11 +424,11 @@ def infer_subscript(
found_one = False
for value in self.value.infer(context):
- if value is util.Uninferable:
+ if isinstance(value, util.UninferableBase):
yield util.Uninferable
return None
for index in self.slice.infer(context):
- if index is util.Uninferable:
+ if isinstance(index, util.UninferableBase):
yield util.Uninferable
return None
@@ -459,7 +459,7 @@ def infer_subscript(
# Prevent inferring if the inferred subscript
# is the same as the original subscripted object.
- if self is assigned or assigned is util.Uninferable:
+ if self is assigned or isinstance(assigned, util.UninferableBase):
yield util.Uninferable
return None
yield from assigned.infer(context)
@@ -502,13 +502,13 @@ def _infer_boolop(
return None
for pair in itertools.product(*inferred_values):
- if any(item is util.Uninferable for item in pair):
+ if any(isinstance(item, util.UninferableBase) for item in pair):
# Can't infer the final result, just yield Uninferable.
yield util.Uninferable
continue
bool_values = [item.bool_value() for item in pair]
- if any(item is util.Uninferable for item in bool_values):
+ if any(isinstance(item, util.UninferableBase) for item in bool_values):
# Can't infer the final result, just yield Uninferable.
yield util.Uninferable
continue
@@ -575,7 +575,7 @@ def _infer_unaryop(
# value and negate its result, unless it is
# Uninferable, which will be returned as is.
bool_value = operand.bool_value()
- if bool_value is not util.Uninferable:
+ if not isinstance(bool_value, util.UninferableBase):
yield nodes.const_factory(not bool_value)
else:
yield util.Uninferable
@@ -595,7 +595,10 @@ def _infer_unaryop(
meth = methods[0]
inferred = next(meth.infer(context=context), None)
- if inferred is util.Uninferable or not inferred.callable():
+ if (
+ isinstance(inferred, util.UninferableBase)
+ or not inferred.callable()
+ ):
continue
context = copy_context(context)
@@ -639,7 +642,7 @@ def _is_not_implemented(const) -> bool:
def _infer_old_style_string_formatting(
instance: nodes.Const, other: nodes.NodeNG, context: InferenceContext
-) -> tuple[type[util.Uninferable] | nodes.Const]:
+) -> tuple[util.UninferableBase | nodes.Const]:
"""Infer the result of '"string" % ...'.
TODO: Instead of returning Uninferable we should rely
@@ -699,7 +702,7 @@ def _invoke_binop_inference(
inferred = next(method.infer(context=context))
except StopIteration as e:
raise InferenceError(node=method, context=context) from e
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
raise InferenceError
if not isinstance(
instance, (nodes.Const, nodes.Tuple, nodes.List, nodes.ClassDef, bases.Instance)
@@ -923,7 +926,7 @@ def _infer_binary_operation(
yield util.Uninferable
return
else:
- if any(result is util.Uninferable for result in results):
+ if any(isinstance(result, util.UninferableBase) for result in results):
yield util.Uninferable
return
@@ -959,7 +962,7 @@ def _infer_binop(
lhs_iter = left.infer(context=lhs_context)
rhs_iter = right.infer(context=rhs_context)
for lhs, rhs in itertools.product(lhs_iter, rhs_iter):
- if any(value is util.Uninferable for value in (rhs, lhs)):
+ if any(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
# Don't know how to process this.
yield util.Uninferable
return
@@ -1008,7 +1011,7 @@ def _to_literal(node: nodes.NodeNG) -> Any:
def _do_compare(
left_iter: Iterable[nodes.NodeNG], op: str, right_iter: Iterable[nodes.NodeNG]
-) -> bool | type[util.Uninferable]:
+) -> bool | util.UninferableBase:
"""
If all possible combinations are either True or False, return that:
>>> _do_compare([1, 2], '<=', [3, 4])
@@ -1027,7 +1030,9 @@ def _do_compare(
op_func = COMPARE_OPS[op]
for left, right in itertools.product(left_iter, right_iter):
- if left is util.Uninferable or right is util.Uninferable:
+ if isinstance(left, util.UninferableBase) or isinstance(
+ right, util.UninferableBase
+ ):
return util.Uninferable
try:
@@ -1052,9 +1057,9 @@ def _do_compare(
def _infer_compare(
self: nodes.Compare, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[nodes.Const | type[util.Uninferable], None, None]:
+) -> Generator[nodes.Const | util.UninferableBase, None, None]:
"""Chained comparison inference logic."""
- retval: bool | type[util.Uninferable] = True
+ retval: bool | util.UninferableBase = True
ops = self.ops
left_node = self.left
@@ -1091,7 +1096,7 @@ def _infer_augassign(
lhs_iter = self.target.infer_lhs(context=context)
rhs_iter = self.value.infer(context=rhs_context)
for lhs, rhs in itertools.product(lhs_iter, rhs_iter):
- if any(value is util.Uninferable for value in (rhs, lhs)):
+ if any(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
# Don't know how to process this.
yield util.Uninferable
return
@@ -1216,7 +1221,7 @@ def infer_ifexp(
except (InferenceError, StopIteration):
both_branches = True
else:
- if test is not util.Uninferable:
+ if not isinstance(test, util.UninferableBase):
if test.bool_value():
yield from self.body.infer(context=lhs_context)
else:
diff --git a/astroid/inference_tip.py b/astroid/inference_tip.py
index 957cd04..9f315a5 100644
--- a/astroid/inference_tip.py
+++ b/astroid/inference_tip.py
@@ -11,16 +11,11 @@ from collections.abc import Iterator
import wrapt
-from astroid import bases, util
from astroid.exceptions import InferenceOverwriteError, UseInferenceDefault
from astroid.nodes import NodeNG
-from astroid.typing import InferFn
+from astroid.typing import InferenceResult, InferFn
-InferOptions = typing.Union[
- NodeNG, bases.Instance, bases.UnboundMethod, typing.Type[util.Uninferable]
-]
-
-_cache: dict[tuple[InferFn, NodeNG], list[InferOptions] | None] = {}
+_cache: dict[tuple[InferFn, NodeNG], list[InferenceResult] | None] = {}
def clear_inference_tip_cache() -> None:
@@ -31,7 +26,7 @@ def clear_inference_tip_cache() -> None:
@wrapt.decorator
def _inference_tip_cached(
func: InferFn, instance: None, args: typing.Any, kwargs: typing.Any
-) -> Iterator[InferOptions]:
+) -> Iterator[InferenceResult]:
"""Cache decorator used for inference tips."""
node = args[0]
try:
diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py
index ecf330b..5a7c4e8 100644
--- a/astroid/interpreter/_import/spec.py
+++ b/astroid/interpreter/_import/spec.py
@@ -12,6 +12,8 @@ import importlib.util
import os
import pathlib
import sys
+import types
+import warnings
import zipimport
from collections.abc import Iterator, Sequence
from pathlib import Path
@@ -23,9 +25,21 @@ from astroid.modutils import EXT_LIB_DIRS
from . import util
if sys.version_info >= (3, 8):
- from typing import Literal
+ from typing import Literal, Protocol
else:
- from typing_extensions import Literal
+ from typing_extensions import Literal, Protocol
+
+
+# The MetaPathFinder protocol comes from typeshed, which says:
+# Intentionally omits one deprecated and one optional method of `importlib.abc.MetaPathFinder`
+class _MetaPathFinder(Protocol):
+ def find_spec(
+ self,
+ fullname: str,
+ path: Sequence[str] | None,
+ target: types.ModuleType | None = ...,
+ ) -> importlib.machinery.ModuleSpec | None:
+ ... # pragma: no cover
class ModuleType(enum.Enum):
@@ -43,6 +57,20 @@ class ModuleType(enum.Enum):
PY_NAMESPACE = enum.auto()
+_MetaPathFinderModuleTypes: dict[str, ModuleType] = {
+ # Finders created by setuptools editable installs
+ "_EditableFinder": ModuleType.PY_SOURCE,
+ "_EditableNamespaceFinder": ModuleType.PY_NAMESPACE,
+ # Finders create by six
+ "_SixMetaPathImporter": ModuleType.PY_SOURCE,
+}
+
+_EditableFinderClasses: set[str] = {
+ "_EditableFinder",
+ "_EditableNamespaceFinder",
+}
+
+
class ModuleSpec(NamedTuple):
"""Defines a class similar to PEP 420's ModuleSpec.
@@ -120,10 +148,14 @@ class ImportlibFinder(Finder):
)
else:
try:
- spec = importlib.util.find_spec(modname)
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", category=UserWarning)
+ spec = importlib.util.find_spec(modname)
if (
- spec and spec.loader is importlib.machinery.FrozenImporter
- ): # noqa: E501 # type: ignore[comparison-overlap]
+ spec
+ and spec.loader # type: ignore[comparison-overlap] # noqa: E501
+ is importlib.machinery.FrozenImporter
+ ):
# No need for BuiltinImporter; builtins handled above
return ModuleSpec(
name=modname,
@@ -226,7 +258,6 @@ class ZipFinder(Finder):
super().__init__(path)
for entry_path in path:
if entry_path not in sys.path_importer_cache:
- # pylint: disable=no-member
try:
sys.path_importer_cache[entry_path] = zipimport.zipimporter( # type: ignore[assignment]
entry_path
@@ -310,7 +341,6 @@ 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():
- # pylint: disable-next=no-member
if isinstance(importer, zipimport.zipimporter):
yield filepath, importer
@@ -349,7 +379,7 @@ def _find_spec_with_path(
module_parts: list[str],
processed: list[str],
submodule_path: Sequence[str] | None,
-) -> tuple[Finder, ModuleSpec]:
+) -> tuple[Finder | _MetaPathFinder, ModuleSpec]:
for finder in _SPEC_FINDERS:
finder_instance = finder(search_path)
spec = finder_instance.find_module(
@@ -359,6 +389,43 @@ def _find_spec_with_path(
continue
return finder_instance, spec
+ # Support for custom finders
+ for meta_finder in sys.meta_path:
+ # See if we support the customer import hook of the meta_finder
+ meta_finder_name = meta_finder.__class__.__name__
+ if meta_finder_name not in _MetaPathFinderModuleTypes:
+ # Setuptools>62 creates its EditableFinders dynamically and have
+ # "type" as their __class__.__name__. We check __name__ as well
+ # to see if we can support the finder.
+ try:
+ meta_finder_name = meta_finder.__name__
+ except AttributeError:
+ continue
+ if meta_finder_name not in _MetaPathFinderModuleTypes:
+ continue
+
+ module_type = _MetaPathFinderModuleTypes[meta_finder_name]
+
+ # Meta path finders are supposed to have a find_spec method since
+ # Python 3.4. However, some third-party finders do not implement it.
+ # PEP302 does not refer to find_spec as well.
+ # See: https://github.com/PyCQA/astroid/pull/1752/
+ if not hasattr(meta_finder, "find_spec"):
+ continue
+
+ spec = meta_finder.find_spec(modname, submodule_path)
+ if spec:
+ return (
+ meta_finder,
+ ModuleSpec(
+ spec.name,
+ module_type,
+ spec.origin,
+ spec.origin,
+ spec.submodule_search_locations,
+ ),
+ )
+
raise ImportError(f"No module named {'.'.join(module_parts)}")
@@ -395,7 +462,12 @@ def find_spec(modpath: list[str], path: Sequence[str] | None = None) -> ModuleSp
)
processed.append(modname)
if modpath:
- submodule_path = finder.contribute_to_path(spec, processed)
+ if isinstance(finder, Finder):
+ submodule_path = finder.contribute_to_path(spec, processed)
+ # If modname is a package from an editable install, update submodule_path
+ # so that the next module in the path will be found inside of it using importlib.
+ elif finder.__name__ in _EditableFinderClasses:
+ submodule_path = spec.submodule_search_locations
if spec.type == ModuleType.PKG_DIRECTORY:
spec = spec._replace(submodule_search_locations=submodule_path)
diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py
index 491358a..b7dac7f 100644
--- a/astroid/interpreter/objectmodel.py
+++ b/astroid/interpreter/objectmodel.py
@@ -358,7 +358,7 @@ class FunctionModel(ObjectModel):
except StopIteration as e:
raise InferenceError(context=context, node=caller.args[0]) from e
- if cls is astroid.Uninferable:
+ if isinstance(cls, util.UninferableBase):
raise InferenceError(
"Invalid class inferred", target=self, context=context
)
diff --git a/astroid/manager.py b/astroid/manager.py
index 8a5b05c..965dd5a 100644
--- a/astroid/manager.py
+++ b/astroid/manager.py
@@ -20,7 +20,7 @@ from typing import Any, ClassVar
from astroid import nodes
from astroid._cache import CACHE_MANAGER
from astroid.const import BRAIN_MODULES_DIRECTORY
-from astroid.context import InferenceContext
+from astroid.context import InferenceContext, _invalidate_cache
from astroid.exceptions import AstroidBuildingError, AstroidImportError
from astroid.interpreter._import import spec, util
from astroid.modutils import (
@@ -30,7 +30,7 @@ from astroid.modutils import (
get_source_file,
is_module_name_part_of_extension_package_whitelist,
is_python_source,
- is_standard_module,
+ is_stdlib_module,
load_module_from_name,
modpath_from_file,
)
@@ -154,7 +154,7 @@ class AstroidManager:
def _can_load_extension(self, modname: str) -> bool:
if self.always_load_extensions:
return True
- if is_standard_module(modname):
+ if is_stdlib_module(modname):
return True
return is_module_name_part_of_extension_package_whitelist(
modname, self.extension_package_whitelist
@@ -407,7 +407,7 @@ class AstroidManager:
raw_building._astroid_bootstrapping()
def clear_cache(self) -> None:
- """Clear the underlying cache, bootstrap the builtins module and
+ """Clear the underlying caches, bootstrap the builtins module and
re-register transforms.
"""
# import here because of cyclic imports
@@ -418,6 +418,7 @@ class AstroidManager:
from astroid.nodes.scoped_nodes import ClassDef
clear_inference_tip_cache()
+ _invalidate_cache() # inference context cache
self.astroid_cache.clear()
# NB: not a new TransformVisitor()
diff --git a/astroid/mixins.py b/astroid/mixins.py
index d7fc1de..942e824 100644
--- a/astroid/mixins.py
+++ b/astroid/mixins.py
@@ -27,4 +27,5 @@ __all__ = (
warnings.warn(
"The 'astroid.mixins' module is deprecated and will become private in astroid 3.0.0",
DeprecationWarning,
+ stacklevel=2,
)
diff --git a/astroid/modutils.py b/astroid/modutils.py
index 1b5057f..f05b5f8 100644
--- a/astroid/modutils.py
+++ b/astroid/modutils.py
@@ -26,14 +26,20 @@ import os
import sys
import sysconfig
import types
+import warnings
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
+from astroid.const import IS_JYTHON, IS_PYPY, PY310_PLUS
from astroid.interpreter._import import spec, util
+if PY310_PLUS:
+ from sys import stdlib_module_names
+else:
+ from astroid._backport_stdlib_names import stdlib_module_names
+
logger = logging.getLogger(__name__)
@@ -510,6 +516,41 @@ def is_python_source(filename: str | None) -> bool:
return os.path.splitext(filename)[1][1:] in PY_SOURCE_EXTS
+def is_stdlib_module(modname: str) -> bool:
+ """Return: True if the modname is in the standard library"""
+ return modname.split(".")[0] in stdlib_module_names
+
+
+def module_in_path(modname: str, path: str | Iterable[str]) -> bool:
+ """Try to determine if a module is imported from one of the specified paths
+
+ :param modname: name of the module
+
+ :param path: paths to consider
+
+ :return:
+ true if the module:
+ - is located on the path listed in one of the directory in `paths`
+ """
+
+ modname = modname.split(".")[0]
+ try:
+ filename = file_from_modpath([modname])
+ except ImportError:
+ # Import failed, we can't check path if we don't know it
+ return False
+
+ if filename is None:
+ # No filename likely means it's compiled in, or potentially a namespace
+ return False
+ filename = _normalize_path(filename)
+
+ if isinstance(path, str):
+ return filename.startswith(_cache_normalize_path(path))
+
+ return any(filename.startswith(_cache_normalize_path(entry)) for entry in path)
+
+
def is_standard_module(modname: str, std_path: Iterable[str] | None = None) -> bool:
"""Try to guess if a module is a standard python module (by default,
see `std_path` parameter's description).
@@ -523,6 +564,12 @@ def is_standard_module(modname: str, std_path: Iterable[str] | None = None) -> b
- is located on the path listed in one of the directory in `std_path`
- is a built-in module
"""
+ warnings.warn(
+ "is_standard_module() is deprecated. Use, is_stdlib_module() or module_in_path() instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
modname = modname.split(".")[0]
try:
filename = file_from_modpath([modname])
diff --git a/astroid/node_classes.py b/astroid/node_classes.py
index 59bb010..6470963 100644
--- a/astroid/node_classes.py
+++ b/astroid/node_classes.py
@@ -76,6 +76,7 @@ from astroid.nodes.node_classes import ( # pylint: disable=redefined-builtin (E
Subscript,
TryExcept,
TryFinally,
+ TryStar,
Tuple,
UnaryOp,
Unknown,
@@ -95,4 +96,5 @@ warnings.warn(
"The 'astroid.node_classes' module is deprecated and will be replaced by "
"'astroid.nodes' in astroid 3.0.0",
DeprecationWarning,
+ stacklevel=2,
)
diff --git a/astroid/nodes/__init__.py b/astroid/nodes/__init__.py
index 68ddad7..157aa24 100644
--- a/astroid/nodes/__init__.py
+++ b/astroid/nodes/__init__.py
@@ -84,6 +84,7 @@ from astroid.nodes.node_classes import ( # pylint: disable=redefined-builtin (E
Subscript,
TryExcept,
TryFinally,
+ TryStar,
Tuple,
UnaryOp,
Unknown,
@@ -196,6 +197,7 @@ ALL_NODE_CLASSES = (
Subscript,
TryExcept,
TryFinally,
+ TryStar,
Tuple,
UnaryOp,
Unknown,
@@ -290,6 +292,7 @@ __all__ = (
"Subscript",
"TryExcept",
"TryFinally",
+ "TryStar",
"Tuple",
"UnaryOp",
"Unknown",
diff --git a/astroid/nodes/as_string.py b/astroid/nodes/as_string.py
index cbd5ee1..ae55ab8 100644
--- a/astroid/nodes/as_string.py
+++ b/astroid/nodes/as_string.py
@@ -9,6 +9,8 @@ from __future__ import annotations
from collections.abc import Iterator
from typing import TYPE_CHECKING
+from astroid import nodes
+
if TYPE_CHECKING:
from astroid.nodes import Const
from astroid.nodes.node_classes import (
@@ -254,13 +256,16 @@ class AsStringVisitor:
return ""
def visit_excepthandler(self, node) -> str:
+ n = "except"
+ if isinstance(getattr(node, "parent", None), nodes.TryStar):
+ n = "except*"
if node.type:
if node.name:
- excs = f"except {node.type.accept(self)} as {node.name.accept(self)}"
+ excs = f"{n} {node.type.accept(self)} as {node.name.accept(self)}"
else:
- excs = f"except {node.type.accept(self)}"
+ excs = f"{n} {node.type.accept(self)}"
else:
- excs = "except"
+ excs = f"{n}"
return f"{excs}:\n{self._stmt_list(node.body)}"
def visit_empty(self, node) -> str:
@@ -495,6 +500,17 @@ class AsStringVisitor:
self._stmt_list(node.body), self._stmt_list(node.finalbody)
)
+ def visit_trystar(self, node) -> str:
+ """return an astroid.TryStar node as string"""
+ trys = [f"try:\n{self._stmt_list(node.body)}"]
+ for handler in node.handlers:
+ trys.append(handler.accept(self))
+ if node.orelse:
+ trys.append(f"else:\n{self._stmt_list(node.orelse)}")
+ if node.finalbody:
+ trys.append(f"finally:\n{self._stmt_list(node.finalbody)}")
+ return "\n".join(trys)
+
def visit_tuple(self, node) -> str:
"""return an astroid.Tuple node as string"""
if len(node.elts) == 1:
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
index c8834d0..b7772c3 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -101,7 +101,7 @@ def unpack_infer(stmt, context: InferenceContext | None = None):
return {"node": stmt, "context": context}
# else, infer recursively, except Uninferable object that should be returned as is
for inferred in stmt.infer(context):
- if inferred is util.Uninferable:
+ if isinstance(inferred, util.UninferableBase):
yield inferred
else:
yield from unpack_infer(inferred, context)
@@ -137,10 +137,14 @@ def are_exclusive(stmt1, stmt2, exceptions: list[str] | None = None) -> bool:
# if the common parent is a If or TryExcept statement, look if
# nodes are in exclusive branches
if isinstance(node, If) and exceptions is None:
- if (
- node.locate_child(previous)[1]
- is not node.locate_child(children[node])[1]
- ):
+ c2attr, c2node = node.locate_child(previous)
+ c1attr, c1node = node.locate_child(children[node])
+ if "test" in (c1attr, c2attr):
+ # If any node is `If.test`, then it must be inclusive with
+ # the other node (`If.body` and `If.orelse`)
+ return False
+ if c1attr != c2attr:
+ # different `If` branches (`If.body` and `If.orelse`)
return True
elif isinstance(node, TryExcept):
c2attr, c2node = node.locate_child(previous)
@@ -650,8 +654,12 @@ class Arguments(_base_nodes.AssignTypeNode):
self.posonlyargs: list[AssignName] = []
"""The arguments that can only be passed positionally."""
- self.kw_defaults: list[NodeNG | None]
- """The default values for keyword arguments that cannot be passed positionally."""
+ self.kw_defaults: list[NodeNG | None] | None
+ """
+ The default values for keyword arguments that cannot be passed positionally.
+
+ See .args for why this can be None.
+ """
self.annotations: list[NodeNG | None]
"""The type annotations of arguments that can be passed positionally."""
@@ -695,7 +703,7 @@ class Arguments(_base_nodes.AssignTypeNode):
args: list[AssignName] | None,
defaults: list[NodeNG] | None,
kwonlyargs: list[AssignName],
- kw_defaults: list[NodeNG | None],
+ kw_defaults: list[NodeNG | None] | None,
annotations: list[NodeNG | None],
posonlyargs: list[AssignName] | None = None,
kwonlyargs_annotations: list[NodeNG | None] | None = None,
@@ -979,7 +987,7 @@ class Arguments(_base_nodes.AssignTypeNode):
yield from self.defaults
yield from self.kwonlyargs
- for elt in self.kw_defaults:
+ for elt in self.kw_defaults or ():
if elt is not None:
yield elt
@@ -2457,7 +2465,7 @@ class Dict(NodeNG, Instance):
continue
for inferredkey in key.infer(context):
- if inferredkey is util.Uninferable:
+ if isinstance(inferredkey, util.UninferableBase):
continue
if isinstance(inferredkey, Const) and isinstance(index, Const):
if inferredkey.value == index.value:
@@ -3205,6 +3213,7 @@ class If(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
"It has been moved to pylint and can be imported from 'pylint.checkers.utils' "
"starting with pylint 2.12",
DeprecationWarning,
+ stacklevel=2,
)
if isinstance(self.test, Compare):
value = self.test.left
@@ -3232,6 +3241,7 @@ class If(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
"It has been moved to pylint and can be imported from 'pylint.checkers.utils' "
"starting with pylint 2.12",
DeprecationWarning,
+ stacklevel=2,
)
return isinstance(
self.test, (Name, Attribute)
@@ -4206,6 +4216,107 @@ class TryFinally(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
yield from self.finalbody
+class TryStar(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
+ """Class representing an :class:`ast.TryStar` node."""
+
+ _astroid_fields = ("body", "handlers", "orelse", "finalbody")
+ _multi_line_block_fields = ("body", "handlers", "orelse", "finalbody")
+
+ def __init__(
+ self,
+ *,
+ lineno: int | None = None,
+ col_offset: int | None = None,
+ end_lineno: int | None = None,
+ end_col_offset: int | None = None,
+ parent: NodeNG | None = None,
+ ) -> None:
+ """
+ :param lineno: The line that this node appears on in the source code.
+ :param col_offset: The column that this node appears on in the
+ source code.
+ :param parent: The parent node in the syntax tree.
+ :param end_lineno: The last line this node appears on in the source code.
+ :param end_col_offset: The end column this node appears on in the
+ source code. Note: This is after the last symbol.
+ """
+ self.body: list[NodeNG] = []
+ """The contents of the block to catch exceptions from."""
+
+ self.handlers: list[ExceptHandler] = []
+ """The exception handlers."""
+
+ self.orelse: list[NodeNG] = []
+ """The contents of the ``else`` block."""
+
+ self.finalbody: list[NodeNG] = []
+ """The contents of the ``finally`` block."""
+
+ super().__init__(
+ lineno=lineno,
+ col_offset=col_offset,
+ end_lineno=end_lineno,
+ end_col_offset=end_col_offset,
+ parent=parent,
+ )
+
+ def postinit(
+ self,
+ *,
+ body: list[NodeNG] | None = None,
+ handlers: list[ExceptHandler] | None = None,
+ orelse: list[NodeNG] | None = None,
+ finalbody: list[NodeNG] | None = None,
+ ) -> None:
+ """Do some setup after initialisation.
+ :param body: The contents of the block to catch exceptions from.
+ :param handlers: The exception handlers.
+ :param orelse: The contents of the ``else`` block.
+ :param finalbody: The contents of the ``finally`` block.
+ """
+ if body:
+ self.body = body
+ if handlers:
+ self.handlers = handlers
+ if orelse:
+ self.orelse = orelse
+ if finalbody:
+ self.finalbody = finalbody
+
+ def _infer_name(self, frame, name):
+ return name
+
+ def block_range(self, lineno: int) -> tuple[int, int]:
+ """Get a range from a given line number to where this node ends."""
+ if lineno == self.fromlineno:
+ return lineno, lineno
+ if self.body and self.body[0].fromlineno <= lineno <= self.body[-1].tolineno:
+ # Inside try body - return from lineno till end of try body
+ return lineno, self.body[-1].tolineno
+ for exhandler in self.handlers:
+ if exhandler.type and lineno == exhandler.type.fromlineno:
+ return lineno, lineno
+ if exhandler.body[0].fromlineno <= lineno <= exhandler.body[-1].tolineno:
+ return lineno, exhandler.body[-1].tolineno
+ if self.orelse:
+ if self.orelse[0].fromlineno - 1 == lineno:
+ return lineno, lineno
+ if self.orelse[0].fromlineno <= lineno <= self.orelse[-1].tolineno:
+ return lineno, self.orelse[-1].tolineno
+ if self.finalbody:
+ if self.finalbody[0].fromlineno - 1 == lineno:
+ return lineno, lineno
+ if self.finalbody[0].fromlineno <= lineno <= self.finalbody[-1].tolineno:
+ return lineno, self.finalbody[-1].tolineno
+ return lineno, self.tolineno
+
+ def get_children(self):
+ yield from self.body
+ yield from self.handlers
+ yield from self.orelse
+ yield from self.finalbody
+
+
class Tuple(BaseContainer):
"""Class representing an :class:`ast.Tuple` node.
@@ -4692,20 +4803,18 @@ class FormattedValue(NodeNG):
self.value: NodeNG
"""The value to be formatted into the string."""
- self.conversion: int | None = None # can be None
+ self.conversion: int
"""The type of formatting to be applied to the value.
.. seealso::
:class:`ast.FormattedValue`
"""
- self.format_spec: NodeNG | None = None # can be None
+ self.format_spec: JoinedStr | None = None
"""The formatting to be applied to the value.
.. seealso::
:class:`ast.FormattedValue`
-
- :type: JoinedStr or None
"""
super().__init__(
@@ -4718,9 +4827,10 @@ class FormattedValue(NodeNG):
def postinit(
self,
+ *,
value: NodeNG,
- conversion: int | None = None,
- format_spec: NodeNG | None = None,
+ conversion: int,
+ format_spec: JoinedStr | None = None,
) -> None:
"""Do some setup after initialisation.
@@ -4951,13 +5061,11 @@ class EvaluatedObject(NodeNG):
_astroid_fields = ("original",)
_other_fields = ("value",)
- def __init__(
- self, original: NodeNG, value: NodeNG | type[util.Uninferable]
- ) -> None:
+ def __init__(self, original: NodeNG, value: NodeNG | util.UninferableBase) -> None:
self.original: NodeNG = original
"""The original node that has already been evaluated"""
- self.value: NodeNG | type[util.Uninferable] = value
+ self.value: NodeNG | util.UninferableBase = value
"""The inferred value"""
super().__init__(
@@ -4968,14 +5076,14 @@ class EvaluatedObject(NodeNG):
def _infer(
self, context: InferenceContext | None = None, **kwargs: Any
- ) -> Generator[NodeNG | type[util.Uninferable], None, None]:
+ ) -> Generator[NodeNG | util.UninferableBase, None, None]:
yield self.value
# Pattern matching #######################################################
-class Match(_base_nodes.Statement):
+class Match(_base_nodes.Statement, _base_nodes.MultiLineBlockNode):
"""Class representing a :class:`ast.Match` node.
>>> import astroid
@@ -4991,6 +5099,7 @@ class Match(_base_nodes.Statement):
"""
_astroid_fields = ("subject", "cases")
+ _multi_line_block_fields = ("cases",)
def __init__(
self,
diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py
index 38172f5..3f8222e 100644
--- a/astroid/nodes/node_ng.py
+++ b/astroid/nodes/node_ng.py
@@ -321,6 +321,7 @@ class NodeNG:
"This behaviour can already be triggered "
"by passing 'future=True' to a statement() call.",
DeprecationWarning,
+ stacklevel=2,
)
raise AttributeError(f"{self} object has no attribute 'parent'")
return self.parent.statement(future=future)
@@ -344,6 +345,7 @@ class NodeNG:
"This behaviour can already be triggered "
"by passing 'future=True' to a frame() call.",
DeprecationWarning,
+ stacklevel=2,
)
raise AttributeError(f"{self} object has no attribute 'parent'")
@@ -588,7 +590,7 @@ class NodeNG:
yield from ()
def _infer_name(self, frame, name):
- # overridden for ImportFrom, Import, Global, TryExcept and Arguments
+ # overridden for ImportFrom, Import, Global, TryExcept, TryStar and Arguments
pass
def _infer(
diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
index 411fe8b..5945f05 100644
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -315,6 +315,7 @@ class Module(LocalsDictNodeNG):
"The 'Module.doc' attribute is deprecated, "
"use 'Module.doc_node' instead.",
DeprecationWarning,
+ stacklevel=2,
)
return self._doc
@@ -324,6 +325,7 @@ class Module(LocalsDictNodeNG):
"Setting the 'Module.doc' attribute is deprecated, "
"use 'Module.doc_node' instead.",
DeprecationWarning,
+ stacklevel=2,
)
self._doc = value
@@ -474,6 +476,7 @@ class Module(LocalsDictNodeNG):
"considered a statement. This behaviour can already be triggered "
"by passing 'future=True' to a statement() call.",
DeprecationWarning,
+ stacklevel=2,
)
return self
@@ -1403,6 +1406,7 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda)
"The 'FunctionDef.doc' attribute is deprecated, "
"use 'FunctionDef.doc_node' instead.",
DeprecationWarning,
+ stacklevel=2,
)
return self._doc
@@ -1412,6 +1416,7 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda)
"Setting the 'FunctionDef.doc' attribute is deprecated, "
"use 'FunctionDef.doc_node' instead.",
DeprecationWarning,
+ stacklevel=2,
)
self._doc = value
@@ -1692,20 +1697,40 @@ class FunctionDef(_base_nodes.MultiLineBlockNode, _base_nodes.Statement, Lambda)
# generators, and filter it out later.
if (
self.name == "with_metaclass"
+ and caller is not None
and len(self.args.args) == 1
and self.args.vararg is not None
):
- metaclass = next(caller.args[0].infer(context), None)
+ if isinstance(caller.args, Arguments):
+ metaclass = next(caller.args.args[0].infer(context), None)
+ elif isinstance(caller.args, list):
+ metaclass = next(caller.args[0].infer(context), None)
+ else:
+ raise TypeError( # pragma: no cover
+ f"caller.args was neither Arguments nor list; got {type(caller.args)}"
+ )
if isinstance(metaclass, ClassDef):
try:
- class_bases = [next(arg.infer(context)) for arg in caller.args[1:]]
+ class_bases = [
+ # Find the first non-None inferred base value
+ next(
+ b
+ for b in arg.infer(context=context.clone())
+ if not (isinstance(b, Const) and b.value is None)
+ )
+ for arg in caller.args[1:]
+ ]
except StopIteration as e:
raise InferenceError(node=caller.args[1:], context=context) from e
new_class = ClassDef(name="temporary_class")
new_class.hide = True
new_class.parent = self
new_class.postinit(
- bases=[base for base in class_bases if base != util.Uninferable],
+ bases=[
+ base
+ for base in class_bases
+ if not isinstance(base, util.UninferableBase)
+ ],
body=[],
decorators=[],
metaclass=metaclass,
@@ -1826,8 +1851,6 @@ def _is_metaclass(klass, seen=None) -> bool:
if isinstance(baseobj, bases.Instance):
# not abstract
return False
- if baseobj is util.Uninferable:
- continue
if baseobj is klass:
continue
if not isinstance(baseobj, ClassDef):
@@ -2036,6 +2059,7 @@ class ClassDef(
"The 'ClassDef.doc' attribute is deprecated, "
"use 'ClassDef.doc_node' instead.",
DeprecationWarning,
+ stacklevel=2,
)
return self._doc
@@ -2045,6 +2069,7 @@ class ClassDef(
"Setting the 'ClassDef.doc' attribute is deprecated, "
"use 'ClassDef.doc_node.value' instead.",
DeprecationWarning,
+ stacklevel=2,
)
self._doc = value
@@ -2817,7 +2842,7 @@ class ClassDef(
return next(
node
for node in self._metaclass.infer(context=context)
- if node is not util.Uninferable
+ if not isinstance(node, util.UninferableBase)
)
except (InferenceError, StopIteration):
return None
@@ -2883,7 +2908,7 @@ class ClassDef(
values = [item[0] for item in slots.items]
else:
values = slots.itered()
- if values is util.Uninferable:
+ if isinstance(values, util.UninferableBase):
continue
if not values:
# Stop the iteration, because the class
@@ -2893,8 +2918,6 @@ class ClassDef(
for elt in values:
try:
for inferred in elt.infer():
- if inferred is util.Uninferable:
- continue
if not isinstance(
inferred, node_classes.Const
) or not isinstance(inferred.value, str):
diff --git a/astroid/protocols.py b/astroid/protocols.py
index 72549b7..dcc9e2b 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -132,7 +132,7 @@ def const_infer_binary_op(
other: InferenceResult,
context: InferenceContext,
_: SuccessfulInferenceResult,
-) -> Generator[ConstFactoryResult | type[util.Uninferable], None, None]:
+) -> Generator[ConstFactoryResult | util.UninferableBase, None, None]:
not_implemented = nodes.Const(NotImplemented)
if isinstance(other, nodes.Const):
if (
@@ -174,7 +174,7 @@ def _multiply_seq_by_int(
filtered_elts = (
helpers.safe_infer(elt, context) or util.Uninferable
for elt in self.elts
- if elt is not util.Uninferable
+ if not isinstance(elt, util.UninferableBase)
)
node.elts = list(filtered_elts) * other.value
return node
@@ -184,11 +184,11 @@ def _filter_uninferable_nodes(
elts: Sequence[InferenceResult], context: InferenceContext
) -> Iterator[SuccessfulInferenceResult]:
for elt in elts:
- if elt is util.Uninferable:
+ if isinstance(elt, util.UninferableBase):
yield nodes.Unknown()
else:
for inferred in elt.infer(context):
- if inferred is not util.Uninferable:
+ if not isinstance(inferred, util.UninferableBase):
yield inferred
else:
yield nodes.Unknown()
@@ -202,7 +202,7 @@ def tl_infer_binary_op(
other: InferenceResult,
context: InferenceContext,
method: SuccessfulInferenceResult,
-) -> Generator[_TupleListNodeT | nodes.Const | type[util.Uninferable], None, None]:
+) -> Generator[_TupleListNodeT | nodes.Const | util.UninferableBase, None, None]:
"""Infer a binary operation on a tuple or list.
The instance on which the binary operation is performed is a tuple
@@ -276,7 +276,7 @@ def _resolve_looppart(parts, assign_path, context):
assign_path = assign_path[:]
index = assign_path.pop(0)
for part in parts:
- if part is util.Uninferable:
+ if isinstance(part, util.UninferableBase):
continue
if not hasattr(part, "itered"):
continue
@@ -299,7 +299,7 @@ def _resolve_looppart(parts, assign_path, context):
# we achieved to resolved the assignment path,
# don't infer the last part
yield assigned
- elif assigned is util.Uninferable:
+ elif isinstance(assigned, util.UninferableBase):
break
else:
# we are not yet on the last part of the path
@@ -546,7 +546,7 @@ def _resolve_assignment_parts(parts, assign_path, context):
# we achieved to resolved the assignment path, don't infer the
# last part
yield assigned
- elif assigned is util.Uninferable:
+ elif isinstance(assigned, util.UninferableBase):
return
else:
# we are not yet on the last part of the path search on each
@@ -793,7 +793,7 @@ def starred_assigned_stmts( # noqa: C901
except (InferenceError, StopIteration):
yield util.Uninferable
return
- if rhs is util.Uninferable or not hasattr(rhs, "itered"):
+ if isinstance(rhs, util.UninferableBase) or not hasattr(rhs, "itered"):
yield util.Uninferable
return
@@ -841,7 +841,7 @@ def starred_assigned_stmts( # noqa: C901
except (InferenceError, StopIteration):
yield util.Uninferable
return
- if inferred_iterable is util.Uninferable or not hasattr(
+ if isinstance(inferred_iterable, util.UninferableBase) or not hasattr(
inferred_iterable, "itered"
):
yield util.Uninferable
diff --git a/astroid/raw_building.py b/astroid/raw_building.py
index 453ce8b..4409a63 100644
--- a/astroid/raw_building.py
+++ b/astroid/raw_building.py
@@ -117,6 +117,7 @@ def build_function(
defaults: list[Any] | None = None,
doc: str | None = None,
kwonlyargs: list[str] | None = None,
+ kwonlydefaults: list[Any] | None = None,
) -> nodes.FunctionDef:
"""create and initialize an astroid FunctionDef node"""
# first argument is now a list of decorators
@@ -140,13 +141,22 @@ def build_function(
else:
default_nodes = None
+ kwonlydefault_nodes: list[nodes.NodeNG | None] | None = []
+ if kwonlydefaults is not None:
+ for kwonlydefault in kwonlydefaults:
+ kwonlydefault_node = nodes.const_factory(kwonlydefault)
+ kwonlydefault_node.parent = argsnode
+ kwonlydefault_nodes.append(kwonlydefault_node)
+ else:
+ kwonlydefault_nodes = None
+
argsnode.postinit(
args=arguments,
defaults=default_nodes,
kwonlyargs=[
nodes.AssignName(name=arg, parent=argsnode) for arg in kwonlyargs or ()
],
- kw_defaults=[],
+ kw_defaults=kwonlydefault_nodes,
annotations=[],
posonlyargs=[
nodes.AssignName(name=arg, parent=argsnode) for arg in posonlyargs or ()
@@ -200,7 +210,7 @@ def object_build_class(
def _get_args_info_from_callable(
member: _FunctionTypes,
-) -> tuple[list[str], list[str], list[Any], list[str]]:
+) -> tuple[list[str], list[str], list[Any], list[str], list[Any]]:
"""Returns args, posonlyargs, defaults, kwonlyargs.
:note: currently ignores the return annotation.
@@ -210,6 +220,7 @@ def _get_args_info_from_callable(
defaults: list[Any] = []
posonlyargs: list[str] = []
kwonlyargs: list[str] = []
+ kwonlydefaults: list[Any] = []
for param_name, param in signature.parameters.items():
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
@@ -222,17 +233,26 @@ def _get_args_info_from_callable(
args.append(param_name)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwonlyargs.append(param_name)
- if param.default is not inspect._empty:
+ if param.default is not inspect.Parameter.empty:
+ kwonlydefaults.append(param.default)
+ continue
+ if param.default is not inspect.Parameter.empty:
defaults.append(param.default)
- return args, posonlyargs, defaults, kwonlyargs
+ return args, posonlyargs, defaults, kwonlyargs, kwonlydefaults
def object_build_function(
node: nodes.Module | nodes.ClassDef, member: _FunctionTypes, localname: str
) -> None:
"""create astroid for a living function object"""
- args, posonlyargs, defaults, kwonlyargs = _get_args_info_from_callable(member)
+ (
+ args,
+ posonlyargs,
+ defaults,
+ kwonlyargs,
+ kwonly_defaults,
+ ) = _get_args_info_from_callable(member)
func = build_function(
getattr(member, "__name__", None) or localname,
@@ -241,6 +261,7 @@ def object_build_function(
defaults,
member.__doc__,
kwonlyargs=kwonlyargs,
+ kwonlydefaults=kwonly_defaults,
)
node.add_local_node(func, localname)
diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py
index f0acac3..0d409c4 100644
--- a/astroid/rebuilder.py
+++ b/astroid/rebuilder.py
@@ -507,6 +507,12 @@ class TreeRebuilder:
) -> nodes.TryExcept | nodes.TryFinally:
...
+ if sys.version_info >= (3, 11):
+
+ @overload
+ def visit(self, node: ast.TryStar, parent: NodeNG) -> nodes.TryStar:
+ ...
+
@overload
def visit(self, node: ast.Tuple, parent: NodeNG) -> nodes.Tuple:
...
@@ -1442,9 +1448,9 @@ class TreeRebuilder:
parent=parent,
)
newnode.postinit(
- self.visit(node.value, newnode),
- node.conversion,
- self.visit(node.format_spec, newnode),
+ value=self.visit(node.value, newnode),
+ conversion=node.conversion,
+ format_spec=self.visit(node.format_spec, newnode),
)
return newnode
@@ -1822,6 +1828,22 @@ class TreeRebuilder:
return self.visit_tryexcept(node, parent)
return None
+ def visit_trystar(self, node: ast.TryStar, parent: NodeNG) -> nodes.TryStar:
+ newnode = nodes.TryStar(
+ lineno=node.lineno,
+ col_offset=node.col_offset,
+ end_lineno=getattr(node, "end_lineno", None),
+ end_col_offset=getattr(node, "end_col_offset", None),
+ parent=parent,
+ )
+ newnode.postinit(
+ body=[self.visit(n, newnode) for n in node.body],
+ handlers=[self.visit(n, newnode) for n in node.handlers],
+ orelse=[self.visit(n, newnode) for n in node.orelse],
+ finalbody=[self.visit(n, newnode) for n in node.finalbody],
+ )
+ return newnode
+
def visit_tuple(self, node: ast.Tuple, parent: NodeNG) -> nodes.Tuple:
"""Visit a Tuple node by returning a fresh instance of it."""
context = self._get_context(node)
diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py
index 1e3fbf3..0e5ef13 100644
--- a/astroid/scoped_nodes.py
+++ b/astroid/scoped_nodes.py
@@ -31,4 +31,5 @@ warnings.warn(
"The 'astroid.scoped_nodes' module is deprecated and will be replaced by "
"'astroid.nodes' in astroid 3.0.0",
DeprecationWarning,
+ stacklevel=2,
)
diff --git a/astroid/typing.py b/astroid/typing.py
index 990c988..62d8995 100644
--- a/astroid/typing.py
+++ b/astroid/typing.py
@@ -46,7 +46,7 @@ class AstroidManagerBrain(TypedDict):
_transform: transforms.TransformVisitor
-InferenceResult = Union["nodes.NodeNG", "type[util.Uninferable]", "bases.Proxy"]
+InferenceResult = Union["nodes.NodeNG", "util.UninferableBase", "bases.Proxy"]
SuccessfulInferenceResult = Union["nodes.NodeNG", "bases.Proxy"]
ConstFactoryResult = Union[
diff --git a/astroid/util.py b/astroid/util.py
index 39fba92..20ff810 100644
--- a/astroid/util.py
+++ b/astroid/util.py
@@ -2,6 +2,9 @@
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
import importlib
import sys
import warnings
@@ -10,9 +13,9 @@ from typing import Any
import lazy_object_proxy
if sys.version_info >= (3, 8):
- from typing import Literal
+ from typing import Final, Literal
else:
- from typing_extensions import Literal
+ from typing_extensions import Final, Literal
def lazy_descriptor(obj):
@@ -29,11 +32,13 @@ def lazy_import(module_name: str) -> lazy_object_proxy.Proxy:
)
-@object.__new__
-class Uninferable:
- """Special inference object, which is returned when inference fails."""
+class UninferableBase:
+ """Special inference object, which is returned when inference fails.
+
+ This is meant to be used as a singleton. Use astroid.util.Uninferable to access it.
+ """
- def __repr__(self) -> str:
+ def __repr__(self) -> Literal["Uninferable"]:
return "Uninferable"
__str__ = __repr__
@@ -47,7 +52,7 @@ class Uninferable:
return object.__getattribute__(self, name)
return self
- def __call__(self, *args, **kwargs):
+ def __call__(self, *args: Any, **kwargs: Any) -> UninferableBase:
return self
def __bool__(self) -> Literal[False]:
@@ -59,6 +64,9 @@ class Uninferable:
return visitor.visit_uninferable(self)
+Uninferable: Final = UninferableBase()
+
+
class BadOperationMessage:
"""Object which describes a TypeError occurred somewhere in the inference chain.
@@ -82,7 +90,7 @@ class BadUnaryOperationMessage(BadOperationMessage):
def _object_type(self, obj):
objtype = self._object_type_helper(obj)
- if objtype is Uninferable:
+ if isinstance(objtype, UninferableBase):
return None
return objtype
diff --git a/debian/changelog b/debian/changelog
index a71eec5..f92bbe0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+astroid (2.15.5-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 10 Jun 2023 02:13:12 -0000
+
astroid (2.14.2-1) unstable; urgency=medium
* New upstream release
diff --git a/doc/api/astroid.nodes.rst b/doc/api/astroid.nodes.rst
index 7372cdd..3e99e93 100644
--- a/doc/api/astroid.nodes.rst
+++ b/doc/api/astroid.nodes.rst
@@ -80,6 +80,7 @@ Nodes
astroid.nodes.Subscript
astroid.nodes.TryExcept
astroid.nodes.TryFinally
+ astroid.nodes.TryStar
astroid.nodes.Tuple
astroid.nodes.UnaryOp
astroid.nodes.Unknown
@@ -230,6 +231,8 @@ Nodes
.. autoclass:: astroid.nodes.TryFinally
+.. autoclass:: astroid.nodes.TryStar
+
.. autoclass:: astroid.nodes.Tuple
.. autoclass:: astroid.nodes.UnaryOp
diff --git a/doc/api/base_nodes.rst b/doc/api/base_nodes.rst
index 068c7bb..6253ce5 100644
--- a/doc/api/base_nodes.rst
+++ b/doc/api/base_nodes.rst
@@ -5,34 +5,31 @@ These are abstract node classes that :ref:`other nodes <nodes>` inherit from.
.. autosummary::
- astroid._base_nodes.AssignTypeNode
+ astroid.nodes._base_nodes.AssignTypeNode
astroid.nodes.BaseContainer
- astroid._base_nodes.MultiLineWithElseBlockNode
+ astroid.nodes._base_nodes.MultiLineWithElseBlockNode
astroid.nodes.ComprehensionScope
- astroid._base_nodes.FilterStmtsBaseNode
- astroid._base_nodes.ImportNode
- astroid.nodes.ListComp
- astroid.nodes.LocalsDictNodeNG
+ astroid.nodes._base_nodes.FilterStmtsBaseNode
+ astroid.nodes._base_nodes.ImportNode
+ astroid.nodes.LocalsDictNodeNG
astroid.nodes.node_classes.LookupMixIn
astroid.nodes.NodeNG
- astroid._base_nodes.ParentAssignNode
+ astroid.nodes._base_nodes.ParentAssignNode
astroid.nodes.Statement
astroid.nodes.Pattern
-.. autoclass:: astroid._base_nodes.AssignTypeNode
+.. autoclass:: astroid.nodes._base_nodes.AssignTypeNode
.. autoclass:: astroid.nodes.BaseContainer
-.. autoclass:: astroid._base_nodes.MultiLineWithElseBlockNode
+.. autoclass:: astroid.nodes._base_nodes.MultiLineWithElseBlockNode
.. autoclass:: astroid.nodes.ComprehensionScope
-.. autoclass:: astroid._base_nodes.FilterStmtsBaseNode
+.. autoclass:: astroid.nodes._base_nodes.FilterStmtsBaseNode
-.. autoclass:: astroid._base_nodes.ImportNode
-
-.. autoclass:: astroid.nodes.ListComp
+.. autoclass:: astroid.nodes._base_nodes.ImportNode
.. autoclass:: astroid.nodes.LocalsDictNodeNG
@@ -40,7 +37,7 @@ These are abstract node classes that :ref:`other nodes <nodes>` inherit from.
.. autoclass:: astroid.nodes.NodeNG
-.. autoclass:: astroid._base_nodes.ParentAssignNode
+.. autoclass:: astroid.nodes._base_nodes.ParentAssignNode
.. autoclass:: astroid.nodes.Statement
diff --git a/doc/conf.py b/doc/conf.py
index 8c5a03a..d9e4254 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -55,7 +55,8 @@ master_doc = "index"
# General information about the project.
project = "Astroid"
current_year = datetime.utcnow().year
-copyright = f"2003-{current_year}, Logilab, PyCQA and contributors"
+contributors = "Logilab, and astroid contributors"
+copyright = f"2003-{current_year}, {contributors}"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -202,7 +203,7 @@ latex_documents = [
"index",
"Astroid.tex",
"Astroid Documentation",
- "Logilab, PyCQA and contributors",
+ contributors,
"manual",
),
]
@@ -240,7 +241,7 @@ man_pages = [
"index",
"astroid",
"Astroid Documentation",
- ["Logilab, PyCQA and contributors"],
+ [contributors],
1,
)
]
diff --git a/doc/inference.rst b/doc/inference.rst
index 8d2c7e4..d66ea5e 100644
--- a/doc/inference.rst
+++ b/doc/inference.rst
@@ -22,10 +22,10 @@ In both cases the :meth:`infer` must return a *generator* which iterates
through the various *values* the node could take.
In some case the value yielded will not be a node found in the AST of the node
-but an instance of a special inference class such as :class:`Uninferable`,
+but an instance of a special inference class such as :obj:`Uninferable`,
or :class:`Instance`.
-Namely, the special singleton :obj:`Uninferable()` is yielded when the inference reaches
+Namely, the special singleton :obj:`Uninferable` is yielded when the inference reaches
a point where it can't follow the code and is so unable to guess a value; and
instances of the :class:`Instance` class are yielded when the current node is
inferred to be an instance of some known class.
diff --git a/pyproject.toml b/pyproject.toml
index 3fac032..b49ee2f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,5 @@
[build-system]
-requires = ["setuptools~=62.6", "wheel~=0.37.1"]
+requires = ["setuptools>=64.0", "wheel>=0.37.1"]
build-backend = "setuptools.build_meta"
[project]
@@ -7,9 +7,6 @@ name = "astroid"
license = {text = "LGPL-2.1-or-later"}
description = "An abstract syntax tree for Python with inference support."
readme = "README.rst"
-authors = [
- {name = "Python Code Quality Authority", email = "code-quality@python.org"}
-]
keywords = ["static code analysis", "python", "abstract syntax tree"]
classifiers = [
"Development Status :: 6 - Mature",
@@ -24,6 +21,7 @@ classifiers = [
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Libraries :: Python Modules",
@@ -41,7 +39,7 @@ dependencies = [
dynamic = ["version"]
[project.urls]
-"Docs" = "https://pylint.pycqa.org/projects/astroid/en/latest/"
+"Docs" = "https://pylint.readthedocs.io/projects/astroid/en/latest/"
"Source Code" = "https://github.com/PyCQA/astroid"
"Bug tracker" = "https://github.com/PyCQA/astroid/issues"
"Discord server" = "https://discord.gg/Egy6P8AMB5"
diff --git a/requirements_test.txt b/requirements_test.txt
index 48f5cc8..6e32914 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,7 +1,6 @@
-r requirements_test_min.txt
-r requirements_test_pre_commit.txt
contributors-txt>=0.7.4
-pre-commit~=2.21
tbump~=6.9.0
types-typed-ast; implementation_name=="cpython" and python_version<"3.8"
types-pkg_resources==0.1.3
diff --git a/requirements_test_brain.txt b/requirements_test_brain.txt
index b4778ba..2014e07 100644
--- a/requirements_test_brain.txt
+++ b/requirements_test_brain.txt
@@ -8,5 +8,5 @@ regex
types-python-dateutil
six
types-six
-urllib3
+urllib3>1,<2
typing_extensions>=4.4.0
diff --git a/requirements_test_min.txt b/requirements_test_min.txt
index 4a2be5e..6f4db81 100644
--- a/requirements_test_min.txt
+++ b/requirements_test_min.txt
@@ -1,3 +1,3 @@
-coverage~=7.1
+coverage~=7.2
pytest
pytest-cov~=4.0
diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt
index 829b327..371936b 100644
--- a/requirements_test_pre_commit.txt
+++ b/requirements_test_pre_commit.txt
@@ -1,7 +1,9 @@
-black==23.1a1
-pylint==2.15.10
+-r requirements_test_min.txt
+black==23.1.0
+pylint==2.16.2
isort==5.12.0;python_version>='3.8'
flake8==6.0.0;python_version>='3.8'
flake8-typing-imports==1.14.0;python_version>='3.8'
-flake8-bugbear==22.10.27;python_version>='3.8'
-mypy==0.991
+flake8-bugbear==23.2.13;python_version>='3.8'
+mypy==1.0.1
+pre-commit~=2.21
diff --git a/setup.cfg b/setup.cfg
index 19ab335..a0fa37c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,7 +13,10 @@ license_files =
# E203; Incompatible with black see https://github.com/psf/black/issues/315
# W503; Incompatible with black
# B901; Combine yield and return statements in one function
-extend-ignore = F401, E203, W503, B901
+# B905; We still support python 3.9
+# B906; Flake8 plugin specific check that is always going to be a false positive in pylint
+# B907; Not worth a refactor
+extend-ignore = F401, E203, W503, B901, B905, B906, B907
max-line-length = 110
select = B,C,E,F,W,T4,B9
# Required for flake8-typing-imports (v1.12.0)
diff --git a/tbump.toml b/tbump.toml
index 2fa235b..492ff3c 100644
--- a/tbump.toml
+++ b/tbump.toml
@@ -1,7 +1,7 @@
github_url = "https://github.com/PyCQA/astroid"
[version]
-current = "2.14.2"
+current = "2.15.5"
regex = '''
^(?P<major>0|[1-9]\d*)
\.
diff --git a/tests/brain/__init__.py b/tests/brain/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/brain/numpy/__init__.py b/tests/brain/numpy/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unittest_brain_numpy_core_einsumfunc.py b/tests/brain/numpy/test_core_einsumfunc.py
similarity index 100%
rename from tests/unittest_brain_numpy_core_einsumfunc.py
rename to tests/brain/numpy/test_core_einsumfunc.py
diff --git a/tests/unittest_brain_numpy_core_fromnumeric.py b/tests/brain/numpy/test_core_fromnumeric.py
similarity index 97%
rename from tests/unittest_brain_numpy_core_fromnumeric.py
rename to tests/brain/numpy/test_core_fromnumeric.py
index 09589f5..4fa2099 100644
--- a/tests/unittest_brain_numpy_core_fromnumeric.py
+++ b/tests/brain/numpy/test_core_fromnumeric.py
@@ -44,7 +44,3 @@ class BrainNumpyCoreFromNumericTest(unittest.TestCase):
inferred_values[-1].pytype() in licit_array_types,
msg=f"Illicit type for {func_[0]:s} ({inferred_values[-1].pytype()})",
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_core_function_base.py b/tests/brain/numpy/test_core_function_base.py
similarity index 97%
rename from tests/unittest_brain_numpy_core_function_base.py
rename to tests/brain/numpy/test_core_function_base.py
index f66fc94..1a59f4d 100644
--- a/tests/unittest_brain_numpy_core_function_base.py
+++ b/tests/brain/numpy/test_core_function_base.py
@@ -50,7 +50,3 @@ class BrainNumpyCoreFunctionBaseTest(unittest.TestCase):
func_[0], inferred_values[-1].pytype()
),
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_core_multiarray.py b/tests/brain/numpy/test_core_multiarray.py
similarity index 99%
rename from tests/unittest_brain_numpy_core_multiarray.py
rename to tests/brain/numpy/test_core_multiarray.py
index c4818a4..0e0f9f7 100644
--- a/tests/unittest_brain_numpy_core_multiarray.py
+++ b/tests/brain/numpy/test_core_multiarray.py
@@ -188,7 +188,3 @@ class BrainNumpyCoreMultiarrayTest(unittest.TestCase):
func_[0], inferred_values[-1].pytype()
),
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_core_numeric.py b/tests/brain/numpy/test_core_numeric.py
similarity index 98%
rename from tests/unittest_brain_numpy_core_numeric.py
rename to tests/brain/numpy/test_core_numeric.py
index e2b51fe..2481ece 100644
--- a/tests/unittest_brain_numpy_core_numeric.py
+++ b/tests/brain/numpy/test_core_numeric.py
@@ -76,7 +76,3 @@ def test_function_parameters(method: str, expected_args: list[str]) -> None:
)
actual_args = instance.inferred()[0].args.args
assert [arg.name for arg in actual_args] == expected_args
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_core_numerictypes.py b/tests/brain/numpy/test_core_numerictypes.py
similarity index 99%
rename from tests/unittest_brain_numpy_core_numerictypes.py
rename to tests/brain/numpy/test_core_numerictypes.py
index 40679fc..3cf053e 100644
--- a/tests/unittest_brain_numpy_core_numerictypes.py
+++ b/tests/brain/numpy/test_core_numerictypes.py
@@ -409,7 +409,3 @@ class NumpyBrainUtilsTest(unittest.TestCase):
node = builder.extract_node(src)
cls_node = node.inferred()[0]
self.assertIs(cls_node, Uninferable)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_core_umath.py b/tests/brain/numpy/test_core_umath.py
similarity index 99%
rename from tests/unittest_brain_numpy_core_umath.py
rename to tests/brain/numpy/test_core_umath.py
index 9a03119..d34c0f1 100644
--- a/tests/unittest_brain_numpy_core_umath.py
+++ b/tests/brain/numpy/test_core_umath.py
@@ -239,7 +239,3 @@ class NumpyBrainCoreUmathTest(unittest.TestCase):
f" as a ndarray and not as {effective_infer}"
),
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_ma.py b/tests/brain/numpy/test_ma.py
similarity index 100%
rename from tests/unittest_brain_numpy_ma.py
rename to tests/brain/numpy/test_ma.py
diff --git a/tests/unittest_brain_numpy_ndarray.py b/tests/brain/numpy/test_ndarray.py
similarity index 99%
rename from tests/unittest_brain_numpy_ndarray.py
rename to tests/brain/numpy/test_ndarray.py
index 1f778b0..1fe0f1e 100644
--- a/tests/unittest_brain_numpy_ndarray.py
+++ b/tests/brain/numpy/test_ndarray.py
@@ -168,7 +168,3 @@ class NumpyBrainNdarrayTest(unittest.TestCase):
cls_node = node.inferred()[0]
self.assertIsInstance(cls_node, nodes.ClassDef)
self.assertEqual(cls_node.name, "ndarray")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_numpy_random_mtrand.py b/tests/brain/numpy/test_random_mtrand.py
similarity index 98%
rename from tests/unittest_brain_numpy_random_mtrand.py
rename to tests/brain/numpy/test_random_mtrand.py
index 665d1de..ff3270f 100644
--- a/tests/unittest_brain_numpy_random_mtrand.py
+++ b/tests/brain/numpy/test_random_mtrand.py
@@ -99,7 +99,3 @@ class NumpyBrainRandomMtrandTest(unittest.TestCase):
default.value for default in inferred.args.defaults
]
self.assertEqual(default_args_values, exact_kwargs_default_values)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_argparse.py b/tests/brain/test_argparse.py
similarity index 100%
rename from tests/unittest_brain_argparse.py
rename to tests/brain/test_argparse.py
diff --git a/tests/brain/test_attr.py b/tests/brain/test_attr.py
new file mode 100644
index 0000000..0648109
--- /dev/null
+++ b/tests/brain/test_attr.py
@@ -0,0 +1,221 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+
+import astroid
+from astroid import nodes
+
+try:
+ import attr as attr_module # pylint: disable=unused-import
+
+ HAS_ATTR = True
+except ImportError:
+ HAS_ATTR = False
+
+
+@unittest.skipUnless(HAS_ATTR, "These tests require the attr library")
+class AttrsTest(unittest.TestCase):
+ def test_attr_transform(self) -> None:
+ module = astroid.parse(
+ """
+ import attr
+ from attr import attrs, attrib, field
+
+ @attr.s
+ class Foo:
+
+ d = attr.ib(attr.Factory(dict))
+
+ f = Foo()
+ f.d['answer'] = 42
+
+ @attr.s(slots=True)
+ class Bar:
+ d = attr.ib(attr.Factory(dict))
+
+ g = Bar()
+ g.d['answer'] = 42
+
+ @attrs
+ class Bah:
+ d = attrib(attr.Factory(dict))
+
+ h = Bah()
+ h.d['answer'] = 42
+
+ @attr.attrs
+ class Bai:
+ d = attr.attrib(attr.Factory(dict))
+
+ i = Bai()
+ i.d['answer'] = 42
+
+ @attr.define
+ class Spam:
+ d = field(default=attr.Factory(dict))
+
+ j = Spam(d=1)
+ j.d['answer'] = 42
+
+ @attr.mutable
+ class Eggs:
+ d = attr.field(default=attr.Factory(dict))
+
+ k = Eggs(d=1)
+ k.d['answer'] = 42
+
+ @attr.frozen
+ class Eggs:
+ d = attr.field(default=attr.Factory(dict))
+
+ l = Eggs(d=1)
+ l.d['answer'] = 42
+ """
+ )
+
+ for name in ("f", "g", "h", "i", "j", "k", "l"):
+ should_be_unknown = next(module.getattr(name)[0].infer()).getattr("d")[0]
+ self.assertIsInstance(should_be_unknown, astroid.Unknown)
+
+ def test_attrs_transform(self) -> None:
+ """Test brain for decorators of the 'attrs' package.
+
+ Package added support for 'attrs' a long side 'attr' in v21.3.0.
+ See: https://github.com/python-attrs/attrs/releases/tag/21.3.0
+ """
+ module = astroid.parse(
+ """
+ import attrs
+ from attrs import field, mutable, frozen, define
+ from attrs import mutable as my_mutable
+
+ @attrs.define
+ class Foo:
+
+ d = attrs.field(attrs.Factory(dict))
+
+ f = Foo()
+ f.d['answer'] = 42
+
+ @attrs.define(slots=True)
+ class Bar:
+ d = field(attrs.Factory(dict))
+
+ g = Bar()
+ g.d['answer'] = 42
+
+ @attrs.mutable
+ class Bah:
+ d = field(attrs.Factory(dict))
+
+ h = Bah()
+ h.d['answer'] = 42
+
+ @attrs.frozen
+ class Bai:
+ d = attrs.field(attrs.Factory(dict))
+
+ i = Bai()
+ i.d['answer'] = 42
+
+ @attrs.define
+ class Spam:
+ d = field(default=attrs.Factory(dict))
+
+ j = Spam(d=1)
+ j.d['answer'] = 42
+
+ @attrs.mutable
+ class Eggs:
+ d = attrs.field(default=attrs.Factory(dict))
+
+ k = Eggs(d=1)
+ k.d['answer'] = 42
+
+ @attrs.frozen
+ class Eggs:
+ d = attrs.field(default=attrs.Factory(dict))
+
+ l = Eggs(d=1)
+ l.d['answer'] = 42
+
+
+ @frozen
+ class Legs:
+ d = attrs.field(default=attrs.Factory(dict))
+
+ m = Legs(d=1)
+ m.d['answer'] = 42
+
+ @define
+ class FooBar:
+ d = attrs.field(default=attrs.Factory(dict))
+
+ n = FooBar(d=1)
+ n.d['answer'] = 42
+
+ @mutable
+ class BarFoo:
+ d = attrs.field(default=attrs.Factory(dict))
+
+ o = BarFoo(d=1)
+ o.d['answer'] = 42
+
+ @my_mutable
+ class FooFoo:
+ d = attrs.field(default=attrs.Factory(dict))
+
+ p = FooFoo(d=1)
+ p.d['answer'] = 42
+ """
+ )
+
+ for name in ("f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"):
+ should_be_unknown = next(module.getattr(name)[0].infer()).getattr("d")[0]
+ self.assertIsInstance(should_be_unknown, astroid.Unknown)
+
+ def test_special_attributes(self) -> None:
+ """Make sure special attrs attributes exist"""
+
+ code = """
+ import attr
+
+ @attr.s
+ class Foo:
+ pass
+ Foo()
+ """
+ foo_inst = next(astroid.extract_node(code).infer())
+ [attr_node] = foo_inst.getattr("__attrs_attrs__")
+ # Prevents https://github.com/PyCQA/pylint/issues/1884
+ assert isinstance(attr_node, nodes.Unknown)
+
+ def test_dont_consider_assignments_but_without_attrs(self) -> None:
+ code = """
+ import attr
+
+ class Cls: pass
+ @attr.s
+ class Foo:
+ temp = Cls()
+ temp.prop = 5
+ bar_thing = attr.ib(default=temp)
+ Foo()
+ """
+ next(astroid.extract_node(code).infer())
+
+ def test_attrs_with_annotation(self) -> None:
+ code = """
+ import attr
+
+ @attr.s
+ class Foo:
+ bar: int = attr.ib(default=5)
+ Foo()
+ """
+ should_be_unknown = next(astroid.extract_node(code).infer()).getattr("bar")[0]
+ self.assertIsInstance(should_be_unknown, astroid.Unknown)
diff --git a/tests/unittest_brain.py b/tests/brain/test_brain.py
similarity index 57%
rename from tests/unittest_brain.py
rename to tests/brain/test_brain.py
index dd929bd..dc12ea2 100644
--- a/tests/unittest_brain.py
+++ b/tests/brain/test_brain.py
@@ -2,1332 +2,102 @@
# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
-"""Tests for basic functionality in astroid.brain."""
-
-from __future__ import annotations
-
-import io
-import queue
-import re
-import sys
-import unittest
-import warnings
-from typing import Any
-
-import pytest
-
-import astroid
-from astroid import MANAGER, bases, builder, nodes, objects, test_utils, util
-from astroid.bases import Instance
-from astroid.brain.brain_namedtuple_enum import _get_namedtuple_fields
-from astroid.const import PY39_PLUS
-from astroid.exceptions import (
- AttributeInferenceError,
- InferenceError,
- UseInferenceDefault,
-)
-from astroid.nodes.node_classes import Const
-from astroid.nodes.scoped_nodes import ClassDef
-
-try:
- import multiprocessing # pylint: disable=unused-import
-
- HAS_MULTIPROCESSING = True
-except ImportError:
- HAS_MULTIPROCESSING = False
-
-
-try:
- with warnings.catch_warnings():
- warnings.simplefilter("ignore", DeprecationWarning)
- import nose # pylint: disable=unused-import
- HAS_NOSE = True
-except ImportError:
- HAS_NOSE = False
-
-try:
- import dateutil # pylint: disable=unused-import
-
- HAS_DATEUTIL = True
-except ImportError:
- HAS_DATEUTIL = False
-
-try:
- import attr as attr_module # pylint: disable=unused-import
-
- HAS_ATTR = True
-except ImportError:
- HAS_ATTR = False
-
-try:
- import six # pylint: disable=unused-import
-
- HAS_SIX = True
-except ImportError:
- HAS_SIX = False
-
-try:
- import typing_extensions # pylint: disable=unused-import
-
- HAS_TYPING_EXTENSIONS = True
- HAS_TYPING_EXTENSIONS_TYPEVAR = hasattr(typing_extensions, "TypeVar")
-except ImportError:
- HAS_TYPING_EXTENSIONS = False
- HAS_TYPING_EXTENSIONS_TYPEVAR = False
-
-
-def assertEqualMro(klass: ClassDef, expected_mro: list[str]) -> None:
- """Check mro names."""
- assert [member.qname() for member in klass.mro()] == expected_mro
-
-
-class HashlibTest(unittest.TestCase):
- def _assert_hashlib_class(self, class_obj: ClassDef) -> None:
- self.assertIn("update", class_obj)
- self.assertIn("digest", class_obj)
- self.assertIn("hexdigest", class_obj)
- 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["update"].args.args), 2)
-
- def test_hashlib(self) -> None:
- """Tests that brain extensions for hashlib work."""
- hashlib_module = MANAGER.ast_from_module_name("hashlib")
- for class_name in (
- "md5",
- "sha1",
- "sha224",
- "sha256",
- "sha384",
- "sha512",
- "sha3_224",
- "sha3_256",
- "sha3_384",
- "sha3_512",
- ):
- class_obj = hashlib_module[class_name]
- self._assert_hashlib_class(class_obj)
- self.assertEqual(len(class_obj["digest"].args.args), 1)
- self.assertEqual(len(class_obj["hexdigest"].args.args), 1)
-
- def test_shake(self) -> None:
- """Tests that the brain extensions for the hashlib shake algorithms work."""
- hashlib_module = MANAGER.ast_from_module_name("hashlib")
- for class_name in ("shake_128", "shake_256"):
- class_obj = hashlib_module[class_name]
- self._assert_hashlib_class(class_obj)
- self.assertEqual(len(class_obj["digest"].args.args), 2)
- self.assertEqual(len(class_obj["hexdigest"].args.args), 2)
-
- def test_blake2(self) -> None:
- """Tests that the brain extensions for the hashlib blake2 hash functions work."""
- hashlib_module = MANAGER.ast_from_module_name("hashlib")
- for class_name in ("blake2b", "blake2s"):
- class_obj = hashlib_module[class_name]
- self.assertEqual(len(class_obj["__init__"].args.args), 2)
- self.assertEqual(len(class_obj["digest"].args.args), 1)
- self.assertEqual(len(class_obj["hexdigest"].args.args), 1)
-
-
-class CollectionsDequeTests(unittest.TestCase):
- def _inferred_queue_instance(self) -> Instance:
- node = builder.extract_node(
- """
- import collections
- q = collections.deque([])
- q
- """
- )
- return next(node.infer())
-
- def test_deque(self) -> None:
- inferred = self._inferred_queue_instance()
- self.assertTrue(inferred.getattr("__len__"))
-
- def test_deque_py35methods(self) -> None:
- inferred = self._inferred_queue_instance()
- self.assertIn("copy", inferred.locals)
- 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__"))
-
-
-class OrderedDictTest(unittest.TestCase):
- def _inferred_ordered_dict_instance(self) -> Instance:
- node = builder.extract_node(
- """
- import collections
- d = collections.OrderedDict()
- d
- """
- )
- return next(node.infer())
-
- def test_ordered_dict_py34method(self) -> None:
- inferred = self._inferred_ordered_dict_instance()
- self.assertIn("move_to_end", inferred.locals)
-
-
-class NamedTupleTest(unittest.TestCase):
- def test_namedtuple_base(self) -> None:
- klass = builder.extract_node(
- """
- from collections import namedtuple
-
- class X(namedtuple("X", ["a", "b", "c"])):
- pass
- """
- )
- assert isinstance(klass, nodes.ClassDef)
- self.assertEqual(
- [anc.name for anc in klass.ancestors()], ["X", "tuple", "object"]
- )
- # See: https://github.com/PyCQA/pylint/issues/5982
- self.assertNotIn("X", klass.locals)
- for anc in klass.ancestors():
- self.assertFalse(anc.parent is None)
-
- def test_namedtuple_inference(self) -> None:
- klass = builder.extract_node(
- """
- from collections import namedtuple
-
- name = "X"
- fields = ["a", "b", "c"]
- class X(namedtuple(name, fields)):
- pass
- """
- )
- assert isinstance(klass, nodes.ClassDef)
- base = next(base for base in klass.ancestors() if base.name == "X")
- self.assertSetEqual({"a", "b", "c"}, set(base.instance_attrs))
-
- def test_namedtuple_inference_failure(self) -> None:
- klass = builder.extract_node(
- """
- from collections import namedtuple
-
- def foo(fields):
- return __(namedtuple("foo", fields))
- """
- )
- self.assertIs(util.Uninferable, next(klass.infer()))
-
- def test_namedtuple_advanced_inference(self) -> None:
- # urlparse return an object of class ParseResult, which has a
- # namedtuple call and a mixin as base classes
- result = builder.extract_node(
- """
- from urllib.parse import urlparse
-
- result = __(urlparse('gopher://'))
- """
- )
- instance = next(result.infer())
- self.assertGreaterEqual(len(instance.getattr("scheme")), 1)
- self.assertGreaterEqual(len(instance.getattr("port")), 1)
- with self.assertRaises(AttributeInferenceError):
- instance.getattr("foo")
- self.assertGreaterEqual(len(instance.getattr("geturl")), 1)
- self.assertEqual(instance.name, "ParseResult")
-
- def test_namedtuple_instance_attrs(self) -> None:
- result = builder.extract_node(
- """
- from collections import namedtuple
- namedtuple('a', 'a b c')(1, 2, 3) #@
- """
- )
- inferred = next(result.infer())
- for name, attr in inferred.instance_attrs.items():
- self.assertEqual(attr[0].attrname, name)
-
- def test_namedtuple_uninferable_fields(self) -> None:
- node = builder.extract_node(
- """
- x = [A] * 2
- from collections import namedtuple
- l = namedtuple('a', x)
- l(1)
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred)
-
- def test_namedtuple_access_class_fields(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "field other")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIn("field", inferred.locals)
- self.assertIn("other", inferred.locals)
-
- def test_namedtuple_rename_keywords(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "abc def", rename=True)
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIn("abc", inferred.locals)
- self.assertIn("_1", inferred.locals)
-
- def test_namedtuple_rename_duplicates(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "abc abc abc", rename=True)
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIn("abc", inferred.locals)
- self.assertIn("_1", inferred.locals)
- self.assertIn("_2", inferred.locals)
-
- def test_namedtuple_rename_uninferable(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "a b c", rename=UNINFERABLE)
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIn("a", inferred.locals)
- self.assertIn("b", inferred.locals)
- self.assertIn("c", inferred.locals)
-
- def test_namedtuple_func_form(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple(typename="Tuple", field_names="a b c", rename=UNINFERABLE)
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertEqual(inferred.name, "Tuple")
- self.assertIn("a", inferred.locals)
- self.assertIn("b", inferred.locals)
- self.assertIn("c", inferred.locals)
-
- def test_namedtuple_func_form_args_and_kwargs(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE)
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertEqual(inferred.name, "Tuple")
- self.assertIn("a", inferred.locals)
- self.assertIn("b", inferred.locals)
- self.assertIn("c", inferred.locals)
-
- def test_namedtuple_bases_are_actually_names_not_nodes(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE)
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIsInstance(inferred, astroid.ClassDef)
- self.assertIsInstance(inferred.bases[0], astroid.Name)
- self.assertEqual(inferred.bases[0].name, "tuple")
-
- def test_invalid_label_does_not_crash_inference(self) -> None:
- code = """
- import collections
- a = collections.namedtuple( 'a', ['b c'] )
- a
- """
- node = builder.extract_node(code)
- inferred = next(node.infer())
- assert isinstance(inferred, astroid.ClassDef)
- assert "b" not in inferred.locals
- assert "c" not in inferred.locals
-
- def test_no_rename_duplicates_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "abc abc")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred) # would raise ValueError
-
- def test_no_rename_keywords_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "abc def")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred) # would raise ValueError
-
- def test_no_rename_nonident_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "123 456")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred) # would raise ValueError
-
- def test_no_rename_underscore_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", "_1")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred) # would raise ValueError
-
- def test_invalid_typename_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("123", "abc")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred) # would raise ValueError
-
- def test_keyword_typename_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("while", "abc")
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred) # would raise ValueError
-
- def test_typeerror_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- Tuple = namedtuple("Tuple", [123, 456])
- Tuple #@
- """
- )
- inferred = next(node.infer())
- # namedtuple converts all arguments to strings so these should be too
- # and catch on the isidentifier() check
- self.assertIs(util.Uninferable, inferred)
-
- def test_pathological_str_does_not_crash_inference(self) -> None:
- node = builder.extract_node(
- """
- from collections import namedtuple
- class Invalid:
- def __str__(self):
- return 123 # will raise TypeError
- Tuple = namedtuple("Tuple", [Invalid()])
- Tuple #@
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred)
-
- def test_name_as_typename(self) -> None:
- """Reported in https://github.com/PyCQA/pylint/issues/7429 as a crash."""
- good_node, good_node_two, bad_node = builder.extract_node(
- """
- import collections
- collections.namedtuple(typename="MyTuple", field_names=["birth_date", "city"]) #@
- collections.namedtuple("MyTuple", field_names=["birth_date", "city"]) #@
- collections.namedtuple(["birth_date", "city"], typename="MyTuple") #@
- """
- )
- good_inferred = next(good_node.infer())
- assert isinstance(good_inferred, nodes.ClassDef)
- good_node_two_inferred = next(good_node_two.infer())
- assert isinstance(good_node_two_inferred, nodes.ClassDef)
- bad_node_inferred = next(bad_node.infer())
- assert bad_node_inferred == util.Uninferable
-
-
-class DefaultDictTest(unittest.TestCase):
- def test_1(self) -> None:
- node = builder.extract_node(
- """
- from collections import defaultdict
-
- X = defaultdict(int)
- X[0]
- """
- )
- inferred = next(node.infer())
- self.assertIs(util.Uninferable, inferred)
-
-
-class ModuleExtenderTest(unittest.TestCase):
- def test_extension_modules(self) -> None:
- transformer = MANAGER._transform
- for extender, _ in transformer.transforms[nodes.Module]:
- n = nodes.Module("__main__")
- extender(n)
-
-
-@unittest.skipUnless(HAS_NOSE, "This test requires nose library.")
-class NoseBrainTest(unittest.TestCase):
- def test_nose_tools(self):
- methods = builder.extract_node(
- """
- from nose.tools import assert_equal
- from nose.tools import assert_equals
- from nose.tools import assert_true
- assert_equal = assert_equal #@
- assert_true = assert_true #@
- assert_equals = assert_equals #@
- """
- )
- assert isinstance(methods, list)
- assert_equal = next(methods[0].value.infer())
- assert_true = next(methods[1].value.infer())
- assert_equals = next(methods[2].value.infer())
-
- self.assertIsInstance(assert_equal, astroid.BoundMethod)
- self.assertIsInstance(assert_true, astroid.BoundMethod)
- self.assertIsInstance(assert_equals, astroid.BoundMethod)
- self.assertEqual(assert_equal.qname(), "unittest.case.TestCase.assertEqual")
- self.assertEqual(assert_true.qname(), "unittest.case.TestCase.assertTrue")
- self.assertEqual(assert_equals.qname(), "unittest.case.TestCase.assertEqual")
-
-
-@unittest.skipUnless(HAS_SIX, "These tests require the six library")
-class SixBrainTest(unittest.TestCase):
- def test_attribute_access(self) -> None:
- ast_nodes = builder.extract_node(
- """
- import six
- six.moves.http_client #@
- six.moves.urllib_parse #@
- six.moves.urllib_error #@
- six.moves.urllib.request #@
- """
- )
- assert isinstance(ast_nodes, list)
- http_client = next(ast_nodes[0].infer())
- self.assertIsInstance(http_client, nodes.Module)
- self.assertEqual(http_client.name, "http.client")
-
- urllib_parse = next(ast_nodes[1].infer())
- self.assertIsInstance(urllib_parse, nodes.Module)
- self.assertEqual(urllib_parse.name, "urllib.parse")
- urljoin = next(urllib_parse.igetattr("urljoin"))
- urlencode = next(urllib_parse.igetattr("urlencode"))
- self.assertIsInstance(urljoin, nodes.FunctionDef)
- self.assertEqual(urljoin.qname(), "urllib.parse.urljoin")
- self.assertIsInstance(urlencode, nodes.FunctionDef)
- self.assertEqual(urlencode.qname(), "urllib.parse.urlencode")
-
- urllib_error = next(ast_nodes[2].infer())
- self.assertIsInstance(urllib_error, nodes.Module)
- self.assertEqual(urllib_error.name, "urllib.error")
- urlerror = next(urllib_error.igetattr("URLError"))
- self.assertIsInstance(urlerror, nodes.ClassDef)
- content_too_short = next(urllib_error.igetattr("ContentTooShortError"))
- self.assertIsInstance(content_too_short, nodes.ClassDef)
-
- urllib_request = next(ast_nodes[3].infer())
- self.assertIsInstance(urllib_request, nodes.Module)
- self.assertEqual(urllib_request.name, "urllib.request")
- urlopen = next(urllib_request.igetattr("urlopen"))
- urlretrieve = next(urllib_request.igetattr("urlretrieve"))
- self.assertIsInstance(urlopen, nodes.FunctionDef)
- self.assertEqual(urlopen.qname(), "urllib.request.urlopen")
- self.assertIsInstance(urlretrieve, nodes.FunctionDef)
- self.assertEqual(urlretrieve.qname(), "urllib.request.urlretrieve")
-
- def test_from_imports(self) -> None:
- ast_node = builder.extract_node(
- """
- from six.moves import http_client
- http_client.HTTPSConnection #@
- """
- )
- inferred = next(ast_node.infer())
- self.assertIsInstance(inferred, nodes.ClassDef)
- qname = "http.client.HTTPSConnection"
- self.assertEqual(inferred.qname(), qname)
-
- def test_from_submodule_imports(self) -> None:
- """Make sure ulrlib submodules can be imported from
-
- See PyCQA/pylint#1640 for relevant issue
- """
- ast_node = builder.extract_node(
- """
- from six.moves.urllib.parse import urlparse
- urlparse #@
- """
- )
- inferred = next(ast_node.infer())
- self.assertIsInstance(inferred, nodes.FunctionDef)
-
- def test_with_metaclass_subclasses_inheritance(self) -> None:
- ast_node = builder.extract_node(
- """
- class A(type):
- def test(cls):
- return cls
-
- class C:
- pass
-
- import six
- class B(six.with_metaclass(A, C)):
- pass
-
- B #@
- """
- )
- inferred = next(ast_node.infer())
- self.assertIsInstance(inferred, nodes.ClassDef)
- self.assertEqual(inferred.name, "B")
- self.assertIsInstance(inferred.bases[0], nodes.Name)
- self.assertEqual(inferred.bases[0].name, "C")
- ancestors = tuple(inferred.ancestors())
- self.assertIsInstance(ancestors[0], nodes.ClassDef)
- self.assertEqual(ancestors[0].name, "C")
- self.assertIsInstance(ancestors[1], nodes.ClassDef)
- self.assertEqual(ancestors[1].name, "object")
-
- @staticmethod
- def test_six_with_metaclass_enum_ancestor() -> None:
- code = """
- import six
- from enum import Enum, EnumMeta
-
- class FooMeta(EnumMeta):
- pass
-
- class Foo(six.with_metaclass(FooMeta, Enum)): #@
- bar = 1
- """
- klass = astroid.extract_node(code)
- assert list(klass.ancestors())[-1].name == "Enum"
-
- def test_six_with_metaclass_with_additional_transform(self) -> None:
- def transform_class(cls: Any) -> ClassDef:
- if cls.name == "A":
- cls._test_transform = 314
- return cls
-
- MANAGER.register_transform(nodes.ClassDef, transform_class)
- try:
- ast_node = builder.extract_node(
- """
- import six
- class A(six.with_metaclass(type, object)):
- pass
-
- A #@
- """
- )
- inferred = next(ast_node.infer())
- assert getattr(inferred, "_test_transform", None) == 314
- finally:
- MANAGER.unregister_transform(nodes.ClassDef, transform_class)
-
-
-@unittest.skipUnless(
- HAS_MULTIPROCESSING,
- "multiprocesing is required for this test, but "
- "on some platforms it is missing "
- "(Jython for instance)",
-)
-class MultiprocessingBrainTest(unittest.TestCase):
- def test_multiprocessing_module_attributes(self) -> None:
- # Test that module attributes are working,
- # especially on Python 3.4+, where they are obtained
- # from a context.
- module = builder.extract_node(
- """
- import multiprocessing
- """
- )
- assert isinstance(module, nodes.Import)
- module = module.do_import_module("multiprocessing")
- cpu_count = next(module.igetattr("cpu_count"))
- self.assertIsInstance(cpu_count, astroid.BoundMethod)
-
- def test_module_name(self) -> None:
- module = builder.extract_node(
- """
- import multiprocessing
- multiprocessing.SyncManager()
- """
- )
- inferred_sync_mgr = next(module.infer())
- module = inferred_sync_mgr.root()
- self.assertEqual(module.name, "multiprocessing.managers")
-
- def test_multiprocessing_manager(self) -> None:
- # Test that we have the proper attributes
- # for a multiprocessing.managers.SyncManager
- module = builder.parse(
- """
- import multiprocessing
- manager = multiprocessing.Manager()
- queue = manager.Queue()
- joinable_queue = manager.JoinableQueue()
- event = manager.Event()
- rlock = manager.RLock()
- lock = manager.Lock()
- bounded_semaphore = manager.BoundedSemaphore()
- condition = manager.Condition()
- barrier = manager.Barrier()
- pool = manager.Pool()
- list = manager.list()
- dict = manager.dict()
- value = manager.Value()
- array = manager.Array()
- namespace = manager.Namespace()
- """
- )
- ast_queue = next(module["queue"].infer())
- self.assertEqual(ast_queue.qname(), f"{queue.__name__}.Queue")
-
- joinable_queue = next(module["joinable_queue"].infer())
- self.assertEqual(joinable_queue.qname(), f"{queue.__name__}.Queue")
-
- event = next(module["event"].infer())
- event_name = "threading.Event"
- self.assertEqual(event.qname(), event_name)
-
- rlock = next(module["rlock"].infer())
- rlock_name = "threading._RLock"
- self.assertEqual(rlock.qname(), rlock_name)
-
- lock = next(module["lock"].infer())
- lock_name = "threading.lock"
- self.assertEqual(lock.qname(), lock_name)
-
- bounded_semaphore = next(module["bounded_semaphore"].infer())
- semaphore_name = "threading.BoundedSemaphore"
- self.assertEqual(bounded_semaphore.qname(), semaphore_name)
-
- pool = next(module["pool"].infer())
- pool_name = "multiprocessing.pool.Pool"
- self.assertEqual(pool.qname(), pool_name)
-
- for attr in ("list", "dict"):
- obj = next(module[attr].infer())
- self.assertEqual(obj.qname(), f"builtins.{attr}")
-
- # pypy's implementation of array.__spec__ return None. This causes problems for this inference.
- if not hasattr(sys, "pypy_version_info"):
- array = next(module["array"].infer())
- self.assertEqual(array.qname(), "array.array")
-
- manager = next(module["manager"].infer())
- # Verify that we have these attributes
- self.assertTrue(manager.getattr("start"))
- self.assertTrue(manager.getattr("shutdown"))
-
-
-class ThreadingBrainTest(unittest.TestCase):
- def test_lock(self) -> None:
- lock_instance = builder.extract_node(
- """
- import threading
- threading.Lock()
- """
- )
- inferred = next(lock_instance.infer())
- self.assert_is_valid_lock(inferred)
-
- acquire_method = inferred.getattr("acquire")[0]
- parameters = [param.name for param in acquire_method.args.args[1:]]
- assert parameters == ["blocking", "timeout"]
-
- assert inferred.getattr("locked")
-
- def test_rlock(self) -> None:
- self._test_lock_object("RLock")
-
- def test_semaphore(self) -> None:
- self._test_lock_object("Semaphore")
-
- def test_boundedsemaphore(self) -> None:
- self._test_lock_object("BoundedSemaphore")
-
- def _test_lock_object(self, object_name: str) -> None:
- lock_instance = builder.extract_node(
- f"""
- import threading
- threading.{object_name}()
- """
- )
- inferred = next(lock_instance.infer())
- self.assert_is_valid_lock(inferred)
-
- def assert_is_valid_lock(self, inferred: Instance) -> None:
- self.assertIsInstance(inferred, astroid.Instance)
- self.assertEqual(inferred.root().name, "threading")
- for method in ("acquire", "release", "__enter__", "__exit__"):
- self.assertIsInstance(next(inferred.igetattr(method)), astroid.BoundMethod)
-
-
-class EnumBrainTest(unittest.TestCase):
- def test_simple_enum(self) -> None:
- module = builder.parse(
- """
- import enum
-
- class MyEnum(enum.Enum):
- one = "one"
- two = "two"
-
- def mymethod(self, x):
- return 5
-
- """
- )
-
- enumeration = next(module["MyEnum"].infer())
- one = enumeration["one"]
- self.assertEqual(one.pytype(), ".MyEnum.one")
-
- for propname in ("name", "value"):
- prop = next(iter(one.getattr(propname)))
- self.assertIn("builtins.property", prop.decoratornames())
-
- meth = one.getattr("mymethod")[0]
- self.assertIsInstance(meth, astroid.FunctionDef)
-
- def test_looks_like_enum_false_positive(self) -> None:
- # Test that a class named Enumeration is not considered a builtin enum.
- module = builder.parse(
- """
- class Enumeration(object):
- def __init__(self, name, enum_list):
- pass
- test = 42
- """
- )
- enumeration = module["Enumeration"]
- test = next(enumeration.igetattr("test"))
- self.assertEqual(test.value, 42)
-
- def test_user_enum_false_positive(self) -> None:
- # Test that a user-defined class named Enum is not considered a builtin enum.
- ast_node = astroid.extract_node(
- """
- class Enum:
- pass
-
- class Color(Enum):
- red = 1
-
- Color.red #@
- """
- )
- assert isinstance(ast_node, nodes.NodeNG)
- inferred = ast_node.inferred()
- self.assertEqual(len(inferred), 1)
- self.assertIsInstance(inferred[0], astroid.Const)
- self.assertEqual(inferred[0].value, 1)
-
- def test_ignores_with_nodes_from_body_of_enum(self) -> None:
- code = """
- import enum
-
- class Error(enum.Enum):
- Foo = "foo"
- Bar = "bar"
- with "error" as err:
- pass
- """
- node = builder.extract_node(code)
- inferred = next(node.infer())
- assert "err" in inferred.locals
- assert len(inferred.locals["err"]) == 1
-
- def test_enum_multiple_base_classes(self) -> None:
- module = builder.parse(
- """
- import enum
-
- class Mixin:
- pass
-
- class MyEnum(Mixin, enum.Enum):
- one = 1
- """
- )
- enumeration = next(module["MyEnum"].infer())
- one = enumeration["one"]
-
- clazz = one.getattr("__class__")[0]
- self.assertTrue(
- clazz.is_subtype_of(".Mixin"),
- "Enum instance should share base classes with generating class",
- )
-
- def test_int_enum(self) -> None:
- module = builder.parse(
- """
- import enum
-
- class MyEnum(enum.IntEnum):
- one = 1
- """
- )
-
- enumeration = next(module["MyEnum"].infer())
- one = enumeration["one"]
-
- clazz = one.getattr("__class__")[0]
- self.assertTrue(
- clazz.is_subtype_of("builtins.int"),
- "IntEnum based enums should be a subtype of int",
- )
-
- def test_enum_func_form_is_class_not_instance(self) -> None:
- cls, instance = builder.extract_node(
- """
- from enum import Enum
- f = Enum('Audience', ['a', 'b', 'c'])
- f #@
- f(1) #@
- """
- )
- inferred_cls = next(cls.infer())
- self.assertIsInstance(inferred_cls, bases.Instance)
- inferred_instance = next(instance.infer())
- self.assertIsInstance(inferred_instance, bases.Instance)
- self.assertIsInstance(next(inferred_instance.igetattr("name")), nodes.Const)
- self.assertIsInstance(next(inferred_instance.igetattr("value")), nodes.Const)
-
- def test_enum_func_form_iterable(self) -> None:
- instance = builder.extract_node(
- """
- from enum import Enum
- Animal = Enum('Animal', 'ant bee cat dog')
- Animal
- """
- )
- inferred = next(instance.infer())
- self.assertIsInstance(inferred, astroid.Instance)
- self.assertTrue(inferred.getattr("__iter__"))
-
- def test_enum_func_form_subscriptable(self) -> None:
- instance, name = builder.extract_node(
- """
- from enum import Enum
- Animal = Enum('Animal', 'ant bee cat dog')
- Animal['ant'] #@
- Animal['ant'].name #@
- """
- )
- instance = next(instance.infer())
- self.assertIsInstance(instance, astroid.Instance)
+from __future__ import annotations
- inferred = next(name.infer())
- self.assertIsInstance(inferred, astroid.Const)
+import io
+import re
+import sys
+import unittest
- def test_enum_func_form_has_dunder_members(self) -> None:
- instance = builder.extract_node(
- """
- from enum import Enum
- Animal = Enum('Animal', 'ant bee cat dog')
- for i in Animal.__members__:
- i #@
- """
- )
- instance = next(instance.infer())
- self.assertIsInstance(instance, astroid.Const)
- self.assertIsInstance(instance.value, str)
+import pytest
- def test_infer_enum_value_as_the_right_type(self) -> None:
- string_value, int_value = builder.extract_node(
- """
- from enum import Enum
- class A(Enum):
- a = 'a'
- b = 1
- A.a.value #@
- A.b.value #@
- """
- )
- inferred_string = string_value.inferred()
- assert any(
- isinstance(elem, astroid.Const) and elem.value == "a"
- for elem in inferred_string
- )
+import astroid
+from astroid import MANAGER, builder, nodes, objects, test_utils, util
+from astroid.bases import Instance
+from astroid.brain.brain_namedtuple_enum import _get_namedtuple_fields
+from astroid.exceptions import (
+ AttributeInferenceError,
+ InferenceError,
+ UseInferenceDefault,
+)
+from astroid.nodes.node_classes import Const
+from astroid.nodes.scoped_nodes import ClassDef
- inferred_int = int_value.inferred()
- assert any(
- isinstance(elem, astroid.Const) and elem.value == 1 for elem in inferred_int
- )
- def test_mingled_single_and_double_quotes_does_not_crash(self) -> None:
- node = builder.extract_node(
- """
- from enum import Enum
- class A(Enum):
- a = 'x"y"'
- A.a.value #@
- """
- )
- inferred_string = next(node.infer())
- assert inferred_string.value == 'x"y"'
+def assertEqualMro(klass: ClassDef, expected_mro: list[str]) -> None:
+ """Check mro names."""
+ assert [member.qname() for member in klass.mro()] == expected_mro
- def test_special_characters_does_not_crash(self) -> None:
- node = builder.extract_node(
- """
- import enum
- class Example(enum.Enum):
- NULL = '\\N{NULL}'
- Example.NULL.value
- """
- )
- inferred_string = next(node.infer())
- assert inferred_string.value == "\N{NULL}"
- def test_dont_crash_on_for_loops_in_body(self) -> None:
+class CollectionsDequeTests(unittest.TestCase):
+ def _inferred_queue_instance(self) -> Instance:
node = builder.extract_node(
"""
-
- class Commands(IntEnum):
- _ignore_ = 'Commands index'
- _init_ = 'value string'
-
- BEL = 0x07, 'Bell'
- Commands = vars()
- for index in range(4):
- Commands[f'DC{index + 1}'] = 0x11 + index, f'Device Control {index + 1}'
-
- Commands
- """
- )
- inferred = next(node.infer())
- assert isinstance(inferred, astroid.ClassDef)
-
- def test_enum_tuple_list_values(self) -> None:
- tuple_node, list_node = builder.extract_node(
- """
- import enum
-
- class MyEnum(enum.Enum):
- a = (1, 2)
- b = [2, 4]
- MyEnum.a.value #@
- MyEnum.b.value #@
- """
- )
- inferred_tuple_node = next(tuple_node.infer())
- inferred_list_node = next(list_node.infer())
- assert isinstance(inferred_tuple_node, astroid.Tuple)
- assert isinstance(inferred_list_node, astroid.List)
- assert inferred_tuple_node.as_string() == "(1, 2)"
- assert inferred_list_node.as_string() == "[2, 4]"
-
- def test_enum_starred_is_skipped(self) -> None:
- code = """
- from enum import Enum
- class ContentType(Enum):
- TEXT, PHOTO, VIDEO, GIF, YOUTUBE, *_ = [1, 2, 3, 4, 5, 6]
- ContentType.TEXT #@
- """
- node = astroid.extract_node(code)
- next(node.infer())
-
- def test_enum_name_is_str_on_self(self) -> None:
- code = """
- from enum import Enum
- class TestEnum(Enum):
- def func(self):
- self.name #@
- self.value #@
- TestEnum.name #@
- TestEnum.value #@
- """
- i_name, i_value, c_name, c_value = astroid.extract_node(code)
-
- # <instance>.name should be a string, <class>.name should be a property (that
- # forwards the lookup to __getattr__)
- inferred = next(i_name.infer())
- assert isinstance(inferred, nodes.Const)
- assert inferred.pytype() == "builtins.str"
- inferred = next(c_name.infer())
- assert isinstance(inferred, objects.Property)
-
- # Inferring .value should not raise InferenceError. It is probably Uninferable
- # but we don't particularly care
- next(i_value.infer())
- next(c_value.infer())
-
- def test_enum_name_and_value_members_override_dynamicclassattr(self) -> None:
- code = """
- from enum import Enum
- class TrickyEnum(Enum):
- name = 1
- value = 2
-
- def func(self):
- self.name #@
- self.value #@
- TrickyEnum.name #@
- TrickyEnum.value #@
- """
- i_name, i_value, c_name, c_value = astroid.extract_node(code)
-
- # All of these cases should be inferred as enum members
- inferred = next(i_name.infer())
- assert isinstance(inferred, bases.Instance)
- assert inferred.pytype() == ".TrickyEnum.name"
- inferred = next(c_name.infer())
- assert isinstance(inferred, bases.Instance)
- assert inferred.pytype() == ".TrickyEnum.name"
- inferred = next(i_value.infer())
- assert isinstance(inferred, bases.Instance)
- assert inferred.pytype() == ".TrickyEnum.value"
- inferred = next(c_value.infer())
- assert isinstance(inferred, bases.Instance)
- assert inferred.pytype() == ".TrickyEnum.value"
-
- def test_enum_subclass_member_name(self) -> None:
- ast_node = astroid.extract_node(
- """
- from enum import Enum
-
- class EnumSubclass(Enum):
- pass
-
- class Color(EnumSubclass):
- red = 1
-
- Color.red.name #@
- """
- )
- assert isinstance(ast_node, nodes.NodeNG)
- inferred = ast_node.inferred()
- self.assertEqual(len(inferred), 1)
- self.assertIsInstance(inferred[0], astroid.Const)
- self.assertEqual(inferred[0].value, "red")
-
- def test_enum_subclass_member_value(self) -> None:
- ast_node = astroid.extract_node(
- """
- from enum import Enum
-
- class EnumSubclass(Enum):
- pass
-
- class Color(EnumSubclass):
- red = 1
-
- Color.red.value #@
- """
- )
- assert isinstance(ast_node, nodes.NodeNG)
- inferred = ast_node.inferred()
- self.assertEqual(len(inferred), 1)
- self.assertIsInstance(inferred[0], astroid.Const)
- self.assertEqual(inferred[0].value, 1)
-
- def test_enum_subclass_member_method(self) -> None:
- # See Pylint issue #2626
- ast_node = astroid.extract_node(
- """
- from enum import Enum
-
- class EnumSubclass(Enum):
- def hello_pylint(self) -> str:
- return self.name
-
- class Color(EnumSubclass):
- red = 1
-
- Color.red.hello_pylint() #@
+ import collections
+ q = collections.deque([])
+ q
"""
)
- assert isinstance(ast_node, nodes.NodeNG)
- inferred = ast_node.inferred()
- self.assertEqual(len(inferred), 1)
- self.assertIsInstance(inferred[0], astroid.Const)
- self.assertEqual(inferred[0].value, "red")
-
- def test_enum_subclass_different_modules(self) -> None:
- # See Pylint issue #2626
- astroid.extract_node(
- """
- from enum import Enum
+ return next(node.infer())
- class EnumSubclass(Enum):
- pass
- """,
- "a",
- )
- ast_node = astroid.extract_node(
- """
- from a import EnumSubclass
+ def test_deque(self) -> None:
+ inferred = self._inferred_queue_instance()
+ self.assertTrue(inferred.getattr("__len__"))
- class Color(EnumSubclass):
- red = 1
+ def test_deque_py35methods(self) -> None:
+ inferred = self._inferred_queue_instance()
+ self.assertIn("copy", inferred.locals)
+ self.assertIn("insert", inferred.locals)
+ self.assertIn("index", inferred.locals)
- Color.red.value #@
- """
- )
- assert isinstance(ast_node, nodes.NodeNG)
- inferred = ast_node.inferred()
- self.assertEqual(len(inferred), 1)
- self.assertIsInstance(inferred[0], astroid.Const)
- self.assertEqual(inferred[0].value, 1)
+ @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__")
- def test_members_member_ignored(self) -> None:
- ast_node = builder.extract_node(
- """
- from enum import Enum
- class Animal(Enum):
- a = 1
- __members__ = {}
- Animal.__members__ #@
- """
- )
+ @test_utils.require_version(minver="3.9")
+ def test_deque_py39methods(self):
+ inferred = self._inferred_queue_instance()
+ self.assertTrue(inferred.getattr("__class_getitem__"))
- inferred = next(ast_node.infer())
- self.assertIsInstance(inferred, astroid.Dict)
- self.assertTrue(inferred.locals)
- def test_enum_as_renamed_import(self) -> None:
- """Originally reported in https://github.com/PyCQA/pylint/issues/5776."""
- ast_node: nodes.Attribute = builder.extract_node(
+class OrderedDictTest(unittest.TestCase):
+ def _inferred_ordered_dict_instance(self) -> Instance:
+ node = builder.extract_node(
"""
- from enum import Enum as PyEnum
- class MyEnum(PyEnum):
- ENUM_KEY = "enum_value"
- MyEnum.ENUM_KEY
+ import collections
+ d = collections.OrderedDict()
+ d
"""
)
- inferred = next(ast_node.infer())
- assert isinstance(inferred, bases.Instance)
- assert inferred._proxied.name == "ENUM_KEY"
+ return next(node.infer())
- def test_class_named_enum(self) -> None:
- """Test that the user-defined class named `Enum` is not inferred as `enum.Enum`"""
- astroid.extract_node(
- """
- class Enum:
- def __init__(self, one, two):
- self.one = one
- self.two = two
- def pear(self):
- ...
- """,
- "module_with_class_named_enum",
- )
+ def test_ordered_dict_py34method(self) -> None:
+ inferred = self._inferred_ordered_dict_instance()
+ self.assertIn("move_to_end", inferred.locals)
- attribute_nodes = astroid.extract_node(
- """
- import module_with_class_named_enum
- module_with_class_named_enum.Enum("apple", "orange") #@
- typo_module_with_class_named_enum.Enum("apple", "orange") #@
- """
- )
- name_nodes = astroid.extract_node(
+class DefaultDictTest(unittest.TestCase):
+ def test_1(self) -> None:
+ node = builder.extract_node(
"""
- from module_with_class_named_enum import Enum
- Enum("apple", "orange") #@
- TypoEnum("apple", "orange") #@
- """
- )
-
- # Test that both of the successfully inferred `Name` & `Attribute`
- # nodes refer to the user-defined Enum class.
- for inferred in (attribute_nodes[0].inferred()[0], name_nodes[0].inferred()[0]):
- assert isinstance(inferred, astroid.Instance)
- assert inferred.name == "Enum"
- assert inferred.qname() == "module_with_class_named_enum.Enum"
- assert "pear" in inferred.locals
-
- # Test that an `InferenceError` is raised when an attempt is made to
- # infer a `Name` or `Attribute` node & they cannot be found.
- for node in (attribute_nodes[1], name_nodes[1]):
- with pytest.raises(InferenceError):
- node.inferred()
-
+ from collections import defaultdict
-@unittest.skipUnless(HAS_DATEUTIL, "This test requires the dateutil library.")
-class DateutilBrainTest(unittest.TestCase):
- def test_parser(self):
- module = builder.parse(
- """
- from dateutil.parser import parse
- d = parse('2000-01-01')
+ X = defaultdict(int)
+ X[0]
"""
)
- d_type = next(module["d"].infer())
- self.assertEqual(d_type.qname(), "datetime.datetime")
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred)
-class PytestBrainTest(unittest.TestCase):
- def test_pytest(self) -> None:
- ast_node = builder.extract_node(
- """
- import pytest
- pytest #@
- """
- )
- module = next(ast_node.infer())
- attrs = [
- "deprecated_call",
- "warns",
- "exit",
- "fail",
- "skip",
- "importorskip",
- "xfail",
- "mark",
- "raises",
- "freeze_includes",
- "set_trace",
- "fixture",
- "yield_fixture",
- ]
- for attr in attrs:
- self.assertIn(attr, module)
+class ModuleExtenderTest(unittest.TestCase):
+ def test_extension_modules(self) -> None:
+ transformer = MANAGER._transform
+ for extender, _ in transformer.transforms[nodes.Module]:
+ n = nodes.Module("__main__")
+ extender(n)
def streams_are_fine():
@@ -2187,29 +957,6 @@ class TypingBrain(unittest.TestCase):
assert i1.value == 2 # should be "Hello"!
-@pytest.mark.skipif(
- not HAS_TYPING_EXTENSIONS,
- reason="These tests require the typing_extensions library",
-)
-class TestTypingExtensions:
- @staticmethod
- @pytest.mark.skipif(
- not HAS_TYPING_EXTENSIONS_TYPEVAR,
- reason="Need typing_extensions>=4.4.0 to test TypeVar",
- )
- def test_typing_extensions_types() -> None:
- ast_nodes = builder.extract_node(
- """
- from typing_extensions import TypeVar
- TypeVar('MyTypeVar', int, float, complex) #@
- TypeVar('AnyStr', str, bytes) #@
- """
- )
- for node in ast_nodes:
- inferred = next(node.infer())
- assert isinstance(inferred, nodes.ClassDef)
-
-
class ReBrainTest(unittest.TestCase):
def test_regex_flags(self) -> None:
names = [name for name in dir(re) if name.isupper()]
@@ -2336,180 +1083,6 @@ class BrainUUIDTest(unittest.TestCase):
self.assertIsInstance(inferred, nodes.Const)
-@unittest.skipUnless(HAS_ATTR, "These tests require the attr library")
-class AttrsTest(unittest.TestCase):
- def test_attr_transform(self) -> None:
- module = astroid.parse(
- """
- import attr
- from attr import attrs, attrib, field
-
- @attr.s
- class Foo:
-
- d = attr.ib(attr.Factory(dict))
-
- f = Foo()
- f.d['answer'] = 42
-
- @attr.s(slots=True)
- class Bar:
- d = attr.ib(attr.Factory(dict))
-
- g = Bar()
- g.d['answer'] = 42
-
- @attrs
- class Bah:
- d = attrib(attr.Factory(dict))
-
- h = Bah()
- h.d['answer'] = 42
-
- @attr.attrs
- class Bai:
- d = attr.attrib(attr.Factory(dict))
-
- i = Bai()
- i.d['answer'] = 42
-
- @attr.define
- class Spam:
- d = field(default=attr.Factory(dict))
-
- j = Spam(d=1)
- j.d['answer'] = 42
-
- @attr.mutable
- class Eggs:
- d = attr.field(default=attr.Factory(dict))
-
- k = Eggs(d=1)
- k.d['answer'] = 42
-
- @attr.frozen
- class Eggs:
- d = attr.field(default=attr.Factory(dict))
-
- l = Eggs(d=1)
- l.d['answer'] = 42
- """
- )
-
- for name in ("f", "g", "h", "i", "j", "k", "l"):
- should_be_unknown = next(module.getattr(name)[0].infer()).getattr("d")[0]
- self.assertIsInstance(should_be_unknown, astroid.Unknown)
-
- def test_attrs_transform(self) -> None:
- """Test brain for decorators of the 'attrs' package.
-
- Package added support for 'attrs' a long side 'attr' in v21.3.0.
- See: https://github.com/python-attrs/attrs/releases/tag/21.3.0
- """
- module = astroid.parse(
- """
- import attrs
- from attrs import field, mutable, frozen
-
- @attrs.define
- class Foo:
-
- d = attrs.field(attrs.Factory(dict))
-
- f = Foo()
- f.d['answer'] = 42
-
- @attrs.define(slots=True)
- class Bar:
- d = field(attrs.Factory(dict))
-
- g = Bar()
- g.d['answer'] = 42
-
- @attrs.mutable
- class Bah:
- d = field(attrs.Factory(dict))
-
- h = Bah()
- h.d['answer'] = 42
-
- @attrs.frozen
- class Bai:
- d = attrs.field(attrs.Factory(dict))
-
- i = Bai()
- i.d['answer'] = 42
-
- @attrs.define
- class Spam:
- d = field(default=attrs.Factory(dict))
-
- j = Spam(d=1)
- j.d['answer'] = 42
-
- @attrs.mutable
- class Eggs:
- d = attrs.field(default=attrs.Factory(dict))
-
- k = Eggs(d=1)
- k.d['answer'] = 42
-
- @attrs.frozen
- class Eggs:
- d = attrs.field(default=attrs.Factory(dict))
-
- l = Eggs(d=1)
- l.d['answer'] = 42
- """
- )
-
- for name in ("f", "g", "h", "i", "j", "k", "l"):
- should_be_unknown = next(module.getattr(name)[0].infer()).getattr("d")[0]
- self.assertIsInstance(should_be_unknown, astroid.Unknown)
-
- def test_special_attributes(self) -> None:
- """Make sure special attrs attributes exist"""
-
- code = """
- import attr
-
- @attr.s
- class Foo:
- pass
- Foo()
- """
- foo_inst = next(astroid.extract_node(code).infer())
- [attr_node] = foo_inst.getattr("__attrs_attrs__")
- # Prevents https://github.com/PyCQA/pylint/issues/1884
- assert isinstance(attr_node, nodes.Unknown)
-
- def test_dont_consider_assignments_but_without_attrs(self) -> None:
- code = """
- import attr
-
- class Cls: pass
- @attr.s
- class Foo:
- temp = Cls()
- temp.prop = 5
- bar_thing = attr.ib(default=temp)
- Foo()
- """
- next(astroid.extract_node(code).infer())
-
- def test_attrs_with_annotation(self) -> None:
- code = """
- import attr
-
- @attr.s
- class Foo:
- bar: int = attr.ib(default=5)
- Foo()
- """
- should_be_unknown = next(astroid.extract_node(code).infer()).getattr("bar")[0]
- self.assertIsInstance(should_be_unknown, astroid.Unknown)
-
-
class RandomSampleTest(unittest.TestCase):
def test_inferred_successfully(self) -> None:
node = astroid.extract_node(
@@ -3489,7 +2062,3 @@ def test_no_attributeerror_on_self_referential_length_check() -> None:
)
assert isinstance(node, nodes.NodeNG)
node.inferred()
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_brain_builtin.py b/tests/brain/test_builtin.py
similarity index 100%
rename from tests/unittest_brain_builtin.py
rename to tests/brain/test_builtin.py
diff --git a/tests/unittest_brain_ctypes.py b/tests/brain/test_ctypes.py
similarity index 100%
rename from tests/unittest_brain_ctypes.py
rename to tests/brain/test_ctypes.py
diff --git a/tests/unittest_brain_dataclasses.py b/tests/brain/test_dataclasses.py
similarity index 100%
rename from tests/unittest_brain_dataclasses.py
rename to tests/brain/test_dataclasses.py
diff --git a/tests/brain/test_dateutil.py b/tests/brain/test_dateutil.py
new file mode 100644
index 0000000..d542e8b
--- /dev/null
+++ b/tests/brain/test_dateutil.py
@@ -0,0 +1,29 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+
+from astroid import builder
+
+try:
+ import dateutil # pylint: disable=unused-import
+
+ HAS_DATEUTIL = True
+except ImportError:
+ HAS_DATEUTIL = False
+
+
+@unittest.skipUnless(HAS_DATEUTIL, "This test requires the dateutil library.")
+class DateutilBrainTest(unittest.TestCase):
+ def test_parser(self):
+ module = builder.parse(
+ """
+ from dateutil.parser import parse
+ d = parse('2000-01-01')
+ """
+ )
+ d_type = next(module["d"].infer())
+ self.assertEqual(d_type.qname(), "datetime.datetime")
diff --git a/tests/brain/test_enum.py b/tests/brain/test_enum.py
new file mode 100644
index 0000000..9d95d2f
--- /dev/null
+++ b/tests/brain/test_enum.py
@@ -0,0 +1,495 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+
+import pytest
+
+import astroid
+from astroid import bases, builder, nodes, objects
+from astroid.exceptions import InferenceError
+
+
+class EnumBrainTest(unittest.TestCase):
+ def test_simple_enum(self) -> None:
+ module = builder.parse(
+ """
+ import enum
+
+ class MyEnum(enum.Enum):
+ one = "one"
+ two = "two"
+
+ def mymethod(self, x):
+ return 5
+
+ """
+ )
+
+ enumeration = next(module["MyEnum"].infer())
+ one = enumeration["one"]
+ self.assertEqual(one.pytype(), ".MyEnum.one")
+
+ for propname in ("name", "value"):
+ prop = next(iter(one.getattr(propname)))
+ self.assertIn("builtins.property", prop.decoratornames())
+
+ meth = one.getattr("mymethod")[0]
+ self.assertIsInstance(meth, astroid.FunctionDef)
+
+ def test_looks_like_enum_false_positive(self) -> None:
+ # Test that a class named Enumeration is not considered a builtin enum.
+ module = builder.parse(
+ """
+ class Enumeration(object):
+ def __init__(self, name, enum_list):
+ pass
+ test = 42
+ """
+ )
+ enumeration = module["Enumeration"]
+ test = next(enumeration.igetattr("test"))
+ self.assertEqual(test.value, 42)
+
+ def test_user_enum_false_positive(self) -> None:
+ # Test that a user-defined class named Enum is not considered a builtin enum.
+ ast_node = astroid.extract_node(
+ """
+ class Enum:
+ pass
+
+ class Color(Enum):
+ red = 1
+
+ Color.red #@
+ """
+ )
+ assert isinstance(ast_node, nodes.NodeNG)
+ inferred = ast_node.inferred()
+ self.assertEqual(len(inferred), 1)
+ self.assertIsInstance(inferred[0], astroid.Const)
+ self.assertEqual(inferred[0].value, 1)
+
+ def test_ignores_with_nodes_from_body_of_enum(self) -> None:
+ code = """
+ import enum
+
+ class Error(enum.Enum):
+ Foo = "foo"
+ Bar = "bar"
+ with "error" as err:
+ pass
+ """
+ node = builder.extract_node(code)
+ inferred = next(node.infer())
+ assert "err" in inferred.locals
+ assert len(inferred.locals["err"]) == 1
+
+ def test_enum_multiple_base_classes(self) -> None:
+ module = builder.parse(
+ """
+ import enum
+
+ class Mixin:
+ pass
+
+ class MyEnum(Mixin, enum.Enum):
+ one = 1
+ """
+ )
+ enumeration = next(module["MyEnum"].infer())
+ one = enumeration["one"]
+
+ clazz = one.getattr("__class__")[0]
+ self.assertTrue(
+ clazz.is_subtype_of(".Mixin"),
+ "Enum instance should share base classes with generating class",
+ )
+
+ def test_int_enum(self) -> None:
+ module = builder.parse(
+ """
+ import enum
+
+ class MyEnum(enum.IntEnum):
+ one = 1
+ """
+ )
+
+ enumeration = next(module["MyEnum"].infer())
+ one = enumeration["one"]
+
+ clazz = one.getattr("__class__")[0]
+ self.assertTrue(
+ clazz.is_subtype_of("builtins.int"),
+ "IntEnum based enums should be a subtype of int",
+ )
+
+ def test_enum_func_form_is_class_not_instance(self) -> None:
+ cls, instance = builder.extract_node(
+ """
+ from enum import Enum
+ f = Enum('Audience', ['a', 'b', 'c'])
+ f #@
+ f(1) #@
+ """
+ )
+ inferred_cls = next(cls.infer())
+ self.assertIsInstance(inferred_cls, bases.Instance)
+ inferred_instance = next(instance.infer())
+ self.assertIsInstance(inferred_instance, bases.Instance)
+ self.assertIsInstance(next(inferred_instance.igetattr("name")), nodes.Const)
+ self.assertIsInstance(next(inferred_instance.igetattr("value")), nodes.Const)
+
+ def test_enum_func_form_iterable(self) -> None:
+ instance = builder.extract_node(
+ """
+ from enum import Enum
+ Animal = Enum('Animal', 'ant bee cat dog')
+ Animal
+ """
+ )
+ inferred = next(instance.infer())
+ self.assertIsInstance(inferred, astroid.Instance)
+ self.assertTrue(inferred.getattr("__iter__"))
+
+ def test_enum_func_form_subscriptable(self) -> None:
+ instance, name = builder.extract_node(
+ """
+ from enum import Enum
+ Animal = Enum('Animal', 'ant bee cat dog')
+ Animal['ant'] #@
+ Animal['ant'].name #@
+ """
+ )
+ instance = next(instance.infer())
+ self.assertIsInstance(instance, astroid.Instance)
+
+ inferred = next(name.infer())
+ self.assertIsInstance(inferred, astroid.Const)
+
+ def test_enum_func_form_has_dunder_members(self) -> None:
+ instance = builder.extract_node(
+ """
+ from enum import Enum
+ Animal = Enum('Animal', 'ant bee cat dog')
+ for i in Animal.__members__:
+ i #@
+ """
+ )
+ instance = next(instance.infer())
+ self.assertIsInstance(instance, astroid.Const)
+ self.assertIsInstance(instance.value, str)
+
+ def test_infer_enum_value_as_the_right_type(self) -> None:
+ string_value, int_value = builder.extract_node(
+ """
+ from enum import Enum
+ class A(Enum):
+ a = 'a'
+ b = 1
+ A.a.value #@
+ A.b.value #@
+ """
+ )
+ inferred_string = string_value.inferred()
+ assert any(
+ isinstance(elem, astroid.Const) and elem.value == "a"
+ for elem in inferred_string
+ )
+
+ inferred_int = int_value.inferred()
+ assert any(
+ isinstance(elem, astroid.Const) and elem.value == 1 for elem in inferred_int
+ )
+
+ def test_mingled_single_and_double_quotes_does_not_crash(self) -> None:
+ node = builder.extract_node(
+ """
+ from enum import Enum
+ class A(Enum):
+ a = 'x"y"'
+ A.a.value #@
+ """
+ )
+ inferred_string = next(node.infer())
+ assert inferred_string.value == 'x"y"'
+
+ def test_special_characters_does_not_crash(self) -> None:
+ node = builder.extract_node(
+ """
+ import enum
+ class Example(enum.Enum):
+ NULL = '\\N{NULL}'
+ Example.NULL.value
+ """
+ )
+ inferred_string = next(node.infer())
+ assert inferred_string.value == "\N{NULL}"
+
+ def test_dont_crash_on_for_loops_in_body(self) -> None:
+ node = builder.extract_node(
+ """
+
+ class Commands(IntEnum):
+ _ignore_ = 'Commands index'
+ _init_ = 'value string'
+
+ BEL = 0x07, 'Bell'
+ Commands = vars()
+ for index in range(4):
+ Commands[f'DC{index + 1}'] = 0x11 + index, f'Device Control {index + 1}'
+
+ Commands
+ """
+ )
+ inferred = next(node.infer())
+ assert isinstance(inferred, astroid.ClassDef)
+
+ def test_enum_tuple_list_values(self) -> None:
+ tuple_node, list_node = builder.extract_node(
+ """
+ import enum
+
+ class MyEnum(enum.Enum):
+ a = (1, 2)
+ b = [2, 4]
+ MyEnum.a.value #@
+ MyEnum.b.value #@
+ """
+ )
+ inferred_tuple_node = next(tuple_node.infer())
+ inferred_list_node = next(list_node.infer())
+ assert isinstance(inferred_tuple_node, astroid.Tuple)
+ assert isinstance(inferred_list_node, astroid.List)
+ assert inferred_tuple_node.as_string() == "(1, 2)"
+ assert inferred_list_node.as_string() == "[2, 4]"
+
+ def test_enum_starred_is_skipped(self) -> None:
+ code = """
+ from enum import Enum
+ class ContentType(Enum):
+ TEXT, PHOTO, VIDEO, GIF, YOUTUBE, *_ = [1, 2, 3, 4, 5, 6]
+ ContentType.TEXT #@
+ """
+ node = astroid.extract_node(code)
+ next(node.infer())
+
+ def test_enum_name_is_str_on_self(self) -> None:
+ code = """
+ from enum import Enum
+ class TestEnum(Enum):
+ def func(self):
+ self.name #@
+ self.value #@
+ TestEnum.name #@
+ TestEnum.value #@
+ """
+ i_name, i_value, c_name, c_value = astroid.extract_node(code)
+
+ # <instance>.name should be a string, <class>.name should be a property (that
+ # forwards the lookup to __getattr__)
+ inferred = next(i_name.infer())
+ assert isinstance(inferred, nodes.Const)
+ assert inferred.pytype() == "builtins.str"
+ inferred = next(c_name.infer())
+ assert isinstance(inferred, objects.Property)
+
+ # Inferring .value should not raise InferenceError. It is probably Uninferable
+ # but we don't particularly care
+ next(i_value.infer())
+ next(c_value.infer())
+
+ def test_enum_name_and_value_members_override_dynamicclassattr(self) -> None:
+ code = """
+ from enum import Enum
+ class TrickyEnum(Enum):
+ name = 1
+ value = 2
+
+ def func(self):
+ self.name #@
+ self.value #@
+ TrickyEnum.name #@
+ TrickyEnum.value #@
+ """
+ i_name, i_value, c_name, c_value = astroid.extract_node(code)
+
+ # All of these cases should be inferred as enum members
+ inferred = next(i_name.infer())
+ assert isinstance(inferred, bases.Instance)
+ assert inferred.pytype() == ".TrickyEnum.name"
+ inferred = next(c_name.infer())
+ assert isinstance(inferred, bases.Instance)
+ assert inferred.pytype() == ".TrickyEnum.name"
+ inferred = next(i_value.infer())
+ assert isinstance(inferred, bases.Instance)
+ assert inferred.pytype() == ".TrickyEnum.value"
+ inferred = next(c_value.infer())
+ assert isinstance(inferred, bases.Instance)
+ assert inferred.pytype() == ".TrickyEnum.value"
+
+ def test_enum_subclass_member_name(self) -> None:
+ ast_node = astroid.extract_node(
+ """
+ from enum import Enum
+
+ class EnumSubclass(Enum):
+ pass
+
+ class Color(EnumSubclass):
+ red = 1
+
+ Color.red.name #@
+ """
+ )
+ assert isinstance(ast_node, nodes.NodeNG)
+ inferred = ast_node.inferred()
+ self.assertEqual(len(inferred), 1)
+ self.assertIsInstance(inferred[0], astroid.Const)
+ self.assertEqual(inferred[0].value, "red")
+
+ def test_enum_subclass_member_value(self) -> None:
+ ast_node = astroid.extract_node(
+ """
+ from enum import Enum
+
+ class EnumSubclass(Enum):
+ pass
+
+ class Color(EnumSubclass):
+ red = 1
+
+ Color.red.value #@
+ """
+ )
+ assert isinstance(ast_node, nodes.NodeNG)
+ inferred = ast_node.inferred()
+ self.assertEqual(len(inferred), 1)
+ self.assertIsInstance(inferred[0], astroid.Const)
+ self.assertEqual(inferred[0].value, 1)
+
+ def test_enum_subclass_member_method(self) -> None:
+ # See Pylint issue #2626
+ ast_node = astroid.extract_node(
+ """
+ from enum import Enum
+
+ class EnumSubclass(Enum):
+ def hello_pylint(self) -> str:
+ return self.name
+
+ class Color(EnumSubclass):
+ red = 1
+
+ Color.red.hello_pylint() #@
+ """
+ )
+ assert isinstance(ast_node, nodes.NodeNG)
+ inferred = ast_node.inferred()
+ self.assertEqual(len(inferred), 1)
+ self.assertIsInstance(inferred[0], astroid.Const)
+ self.assertEqual(inferred[0].value, "red")
+
+ def test_enum_subclass_different_modules(self) -> None:
+ # See Pylint issue #2626
+ astroid.extract_node(
+ """
+ from enum import Enum
+
+ class EnumSubclass(Enum):
+ pass
+ """,
+ "a",
+ )
+ ast_node = astroid.extract_node(
+ """
+ from a import EnumSubclass
+
+ class Color(EnumSubclass):
+ red = 1
+
+ Color.red.value #@
+ """
+ )
+ assert isinstance(ast_node, nodes.NodeNG)
+ inferred = ast_node.inferred()
+ self.assertEqual(len(inferred), 1)
+ self.assertIsInstance(inferred[0], astroid.Const)
+ self.assertEqual(inferred[0].value, 1)
+
+ def test_members_member_ignored(self) -> None:
+ ast_node = builder.extract_node(
+ """
+ from enum import Enum
+ class Animal(Enum):
+ a = 1
+ __members__ = {}
+ Animal.__members__ #@
+ """
+ )
+
+ inferred = next(ast_node.infer())
+ self.assertIsInstance(inferred, astroid.Dict)
+ self.assertTrue(inferred.locals)
+
+ def test_enum_as_renamed_import(self) -> None:
+ """Originally reported in https://github.com/PyCQA/pylint/issues/5776."""
+ ast_node: nodes.Attribute = builder.extract_node(
+ """
+ from enum import Enum as PyEnum
+ class MyEnum(PyEnum):
+ ENUM_KEY = "enum_value"
+ MyEnum.ENUM_KEY
+ """
+ )
+ inferred = next(ast_node.infer())
+ assert isinstance(inferred, bases.Instance)
+ assert inferred._proxied.name == "ENUM_KEY"
+
+ def test_class_named_enum(self) -> None:
+ """Test that the user-defined class named `Enum` is not inferred as `enum.Enum`"""
+ astroid.extract_node(
+ """
+ class Enum:
+ def __init__(self, one, two):
+ self.one = one
+ self.two = two
+ def pear(self):
+ ...
+ """,
+ "module_with_class_named_enum",
+ )
+
+ attribute_nodes = astroid.extract_node(
+ """
+ import module_with_class_named_enum
+ module_with_class_named_enum.Enum("apple", "orange") #@
+ typo_module_with_class_named_enum.Enum("apple", "orange") #@
+ """
+ )
+
+ name_nodes = astroid.extract_node(
+ """
+ from module_with_class_named_enum import Enum
+ Enum("apple", "orange") #@
+ TypoEnum("apple", "orange") #@
+ """
+ )
+
+ # Test that both of the successfully inferred `Name` & `Attribute`
+ # nodes refer to the user-defined Enum class.
+ for inferred in (attribute_nodes[0].inferred()[0], name_nodes[0].inferred()[0]):
+ assert isinstance(inferred, astroid.Instance)
+ assert inferred.name == "Enum"
+ assert inferred.qname() == "module_with_class_named_enum.Enum"
+ assert "pear" in inferred.locals
+
+ # Test that an `InferenceError` is raised when an attempt is made to
+ # infer a `Name` or `Attribute` node & they cannot be found.
+ for node in (attribute_nodes[1], name_nodes[1]):
+ with pytest.raises(InferenceError):
+ node.inferred()
diff --git a/tests/brain/test_hashlib.py b/tests/brain/test_hashlib.py
new file mode 100644
index 0000000..84c8b17
--- /dev/null
+++ b/tests/brain/test_hashlib.py
@@ -0,0 +1,64 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+
+from astroid import MANAGER
+from astroid.const import PY39_PLUS
+from astroid.nodes.scoped_nodes import ClassDef
+
+
+class HashlibTest(unittest.TestCase):
+ def _assert_hashlib_class(self, class_obj: ClassDef) -> None:
+ self.assertIn("update", class_obj)
+ self.assertIn("digest", class_obj)
+ self.assertIn("hexdigest", class_obj)
+ 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["update"].args.args), 2)
+
+ def test_hashlib(self) -> None:
+ """Tests that brain extensions for hashlib work."""
+ hashlib_module = MANAGER.ast_from_module_name("hashlib")
+ for class_name in (
+ "md5",
+ "sha1",
+ "sha224",
+ "sha256",
+ "sha384",
+ "sha512",
+ "sha3_224",
+ "sha3_256",
+ "sha3_384",
+ "sha3_512",
+ ):
+ class_obj = hashlib_module[class_name]
+ self._assert_hashlib_class(class_obj)
+ self.assertEqual(len(class_obj["digest"].args.args), 1)
+ self.assertEqual(len(class_obj["hexdigest"].args.args), 1)
+
+ def test_shake(self) -> None:
+ """Tests that the brain extensions for the hashlib shake algorithms work."""
+ hashlib_module = MANAGER.ast_from_module_name("hashlib")
+ for class_name in ("shake_128", "shake_256"):
+ class_obj = hashlib_module[class_name]
+ self._assert_hashlib_class(class_obj)
+ self.assertEqual(len(class_obj["digest"].args.args), 2)
+ self.assertEqual(len(class_obj["hexdigest"].args.args), 2)
+
+ def test_blake2(self) -> None:
+ """Tests that the brain extensions for the hashlib blake2 hash functions work."""
+ hashlib_module = MANAGER.ast_from_module_name("hashlib")
+ for class_name in ("blake2b", "blake2s"):
+ class_obj = hashlib_module[class_name]
+ self.assertEqual(len(class_obj["__init__"].args.args), 2)
+ self.assertEqual(len(class_obj["digest"].args.args), 1)
+ self.assertEqual(len(class_obj["hexdigest"].args.args), 1)
diff --git a/tests/brain/test_multiprocessing.py b/tests/brain/test_multiprocessing.py
new file mode 100644
index 0000000..ebcec7f
--- /dev/null
+++ b/tests/brain/test_multiprocessing.py
@@ -0,0 +1,115 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import queue
+import sys
+import unittest
+
+import astroid
+from astroid import builder, nodes
+
+try:
+ import multiprocessing # pylint: disable=unused-import
+
+ HAS_MULTIPROCESSING = True
+except ImportError:
+ HAS_MULTIPROCESSING = False
+
+
+@unittest.skipUnless(
+ HAS_MULTIPROCESSING,
+ "multiprocesing is required for this test, but "
+ "on some platforms it is missing "
+ "(Jython for instance)",
+)
+class MultiprocessingBrainTest(unittest.TestCase):
+ def test_multiprocessing_module_attributes(self) -> None:
+ # Test that module attributes are working,
+ # especially on Python 3.4+, where they are obtained
+ # from a context.
+ module = builder.extract_node(
+ """
+ import multiprocessing
+ """
+ )
+ assert isinstance(module, nodes.Import)
+ module = module.do_import_module("multiprocessing")
+ cpu_count = next(module.igetattr("cpu_count"))
+ self.assertIsInstance(cpu_count, astroid.BoundMethod)
+
+ def test_module_name(self) -> None:
+ module = builder.extract_node(
+ """
+ import multiprocessing
+ multiprocessing.SyncManager()
+ """
+ )
+ inferred_sync_mgr = next(module.infer())
+ module = inferred_sync_mgr.root()
+ self.assertEqual(module.name, "multiprocessing.managers")
+
+ def test_multiprocessing_manager(self) -> None:
+ # Test that we have the proper attributes
+ # for a multiprocessing.managers.SyncManager
+ module = builder.parse(
+ """
+ import multiprocessing
+ manager = multiprocessing.Manager()
+ queue = manager.Queue()
+ joinable_queue = manager.JoinableQueue()
+ event = manager.Event()
+ rlock = manager.RLock()
+ lock = manager.Lock()
+ bounded_semaphore = manager.BoundedSemaphore()
+ condition = manager.Condition()
+ barrier = manager.Barrier()
+ pool = manager.Pool()
+ list = manager.list()
+ dict = manager.dict()
+ value = manager.Value()
+ array = manager.Array()
+ namespace = manager.Namespace()
+ """
+ )
+ ast_queue = next(module["queue"].infer())
+ self.assertEqual(ast_queue.qname(), f"{queue.__name__}.Queue")
+
+ joinable_queue = next(module["joinable_queue"].infer())
+ self.assertEqual(joinable_queue.qname(), f"{queue.__name__}.Queue")
+
+ event = next(module["event"].infer())
+ event_name = "threading.Event"
+ self.assertEqual(event.qname(), event_name)
+
+ rlock = next(module["rlock"].infer())
+ rlock_name = "threading._RLock"
+ self.assertEqual(rlock.qname(), rlock_name)
+
+ lock = next(module["lock"].infer())
+ lock_name = "threading.lock"
+ self.assertEqual(lock.qname(), lock_name)
+
+ bounded_semaphore = next(module["bounded_semaphore"].infer())
+ semaphore_name = "threading.BoundedSemaphore"
+ self.assertEqual(bounded_semaphore.qname(), semaphore_name)
+
+ pool = next(module["pool"].infer())
+ pool_name = "multiprocessing.pool.Pool"
+ self.assertEqual(pool.qname(), pool_name)
+
+ for attr in ("list", "dict"):
+ obj = next(module[attr].infer())
+ self.assertEqual(obj.qname(), f"builtins.{attr}")
+
+ # pypy's implementation of array.__spec__ return None. This causes problems for this inference.
+ if not hasattr(sys, "pypy_version_info"):
+ array = next(module["array"].infer())
+ self.assertEqual(array.qname(), "array.array")
+
+ manager = next(module["manager"].infer())
+ # Verify that we have these attributes
+ self.assertTrue(manager.getattr("start"))
+ self.assertTrue(manager.getattr("shutdown"))
diff --git a/tests/brain/test_named_tuple.py b/tests/brain/test_named_tuple.py
new file mode 100644
index 0000000..dff042e
--- /dev/null
+++ b/tests/brain/test_named_tuple.py
@@ -0,0 +1,311 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+
+import astroid
+from astroid import builder, nodes, util
+from astroid.exceptions import AttributeInferenceError
+
+
+class NamedTupleTest(unittest.TestCase):
+ def test_namedtuple_base(self) -> None:
+ klass = builder.extract_node(
+ """
+ from collections import namedtuple
+
+ class X(namedtuple("X", ["a", "b", "c"])):
+ pass
+ """
+ )
+ assert isinstance(klass, nodes.ClassDef)
+ self.assertEqual(
+ [anc.name for anc in klass.ancestors()], ["X", "tuple", "object"]
+ )
+ # See: https://github.com/PyCQA/pylint/issues/5982
+ self.assertNotIn("X", klass.locals)
+ for anc in klass.ancestors():
+ self.assertFalse(anc.parent is None)
+
+ def test_namedtuple_inference(self) -> None:
+ klass = builder.extract_node(
+ """
+ from collections import namedtuple
+
+ name = "X"
+ fields = ["a", "b", "c"]
+ class X(namedtuple(name, fields)):
+ pass
+ """
+ )
+ assert isinstance(klass, nodes.ClassDef)
+ base = next(base for base in klass.ancestors() if base.name == "X")
+ self.assertSetEqual({"a", "b", "c"}, set(base.instance_attrs))
+
+ def test_namedtuple_inference_failure(self) -> None:
+ klass = builder.extract_node(
+ """
+ from collections import namedtuple
+
+ def foo(fields):
+ return __(namedtuple("foo", fields))
+ """
+ )
+ self.assertIs(util.Uninferable, next(klass.infer()))
+
+ def test_namedtuple_advanced_inference(self) -> None:
+ # urlparse return an object of class ParseResult, which has a
+ # namedtuple call and a mixin as base classes
+ result = builder.extract_node(
+ """
+ from urllib.parse import urlparse
+
+ result = __(urlparse('gopher://'))
+ """
+ )
+ instance = next(result.infer())
+ self.assertGreaterEqual(len(instance.getattr("scheme")), 1)
+ self.assertGreaterEqual(len(instance.getattr("port")), 1)
+ with self.assertRaises(AttributeInferenceError):
+ instance.getattr("foo")
+ self.assertGreaterEqual(len(instance.getattr("geturl")), 1)
+ self.assertEqual(instance.name, "ParseResult")
+
+ def test_namedtuple_instance_attrs(self) -> None:
+ result = builder.extract_node(
+ """
+ from collections import namedtuple
+ namedtuple('a', 'a b c')(1, 2, 3) #@
+ """
+ )
+ inferred = next(result.infer())
+ for name, attr in inferred.instance_attrs.items():
+ self.assertEqual(attr[0].attrname, name)
+
+ def test_namedtuple_uninferable_fields(self) -> None:
+ node = builder.extract_node(
+ """
+ x = [A] * 2
+ from collections import namedtuple
+ l = namedtuple('a', x)
+ l(1)
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred)
+
+ def test_namedtuple_access_class_fields(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "field other")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIn("field", inferred.locals)
+ self.assertIn("other", inferred.locals)
+
+ def test_namedtuple_rename_keywords(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "abc def", rename=True)
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIn("abc", inferred.locals)
+ self.assertIn("_1", inferred.locals)
+
+ def test_namedtuple_rename_duplicates(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "abc abc abc", rename=True)
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIn("abc", inferred.locals)
+ self.assertIn("_1", inferred.locals)
+ self.assertIn("_2", inferred.locals)
+
+ def test_namedtuple_rename_uninferable(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "a b c", rename=UNINFERABLE)
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIn("a", inferred.locals)
+ self.assertIn("b", inferred.locals)
+ self.assertIn("c", inferred.locals)
+
+ def test_namedtuple_func_form(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple(typename="Tuple", field_names="a b c", rename=UNINFERABLE)
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertEqual(inferred.name, "Tuple")
+ self.assertIn("a", inferred.locals)
+ self.assertIn("b", inferred.locals)
+ self.assertIn("c", inferred.locals)
+
+ def test_namedtuple_func_form_args_and_kwargs(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE)
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertEqual(inferred.name, "Tuple")
+ self.assertIn("a", inferred.locals)
+ self.assertIn("b", inferred.locals)
+ self.assertIn("c", inferred.locals)
+
+ def test_namedtuple_bases_are_actually_names_not_nodes(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE)
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIsInstance(inferred, astroid.ClassDef)
+ self.assertIsInstance(inferred.bases[0], astroid.Name)
+ self.assertEqual(inferred.bases[0].name, "tuple")
+
+ def test_invalid_label_does_not_crash_inference(self) -> None:
+ code = """
+ import collections
+ a = collections.namedtuple( 'a', ['b c'] )
+ a
+ """
+ node = builder.extract_node(code)
+ inferred = next(node.infer())
+ assert isinstance(inferred, astroid.ClassDef)
+ assert "b" not in inferred.locals
+ assert "c" not in inferred.locals
+
+ def test_no_rename_duplicates_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "abc abc")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred) # would raise ValueError
+
+ def test_no_rename_keywords_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "abc def")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred) # would raise ValueError
+
+ def test_no_rename_nonident_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "123 456")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred) # would raise ValueError
+
+ def test_no_rename_underscore_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", "_1")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred) # would raise ValueError
+
+ def test_invalid_typename_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("123", "abc")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred) # would raise ValueError
+
+ def test_keyword_typename_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("while", "abc")
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred) # would raise ValueError
+
+ def test_typeerror_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ Tuple = namedtuple("Tuple", [123, 456])
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ # namedtuple converts all arguments to strings so these should be too
+ # and catch on the isidentifier() check
+ self.assertIs(util.Uninferable, inferred)
+
+ def test_pathological_str_does_not_crash_inference(self) -> None:
+ node = builder.extract_node(
+ """
+ from collections import namedtuple
+ class Invalid:
+ def __str__(self):
+ return 123 # will raise TypeError
+ Tuple = namedtuple("Tuple", [Invalid()])
+ Tuple #@
+ """
+ )
+ inferred = next(node.infer())
+ self.assertIs(util.Uninferable, inferred)
+
+ def test_name_as_typename(self) -> None:
+ """Reported in https://github.com/PyCQA/pylint/issues/7429 as a crash."""
+ good_node, good_node_two, bad_node = builder.extract_node(
+ """
+ import collections
+ collections.namedtuple(typename="MyTuple", field_names=["birth_date", "city"]) #@
+ collections.namedtuple("MyTuple", field_names=["birth_date", "city"]) #@
+ collections.namedtuple(["birth_date", "city"], typename="MyTuple") #@
+ """
+ )
+ good_inferred = next(good_node.infer())
+ assert isinstance(good_inferred, nodes.ClassDef)
+ good_node_two_inferred = next(good_node_two.infer())
+ assert isinstance(good_node_two_inferred, nodes.ClassDef)
+ bad_node_inferred = next(bad_node.infer())
+ assert bad_node_inferred == util.Uninferable
diff --git a/tests/brain/test_nose.py b/tests/brain/test_nose.py
new file mode 100644
index 0000000..7b72f28
--- /dev/null
+++ b/tests/brain/test_nose.py
@@ -0,0 +1,45 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+import warnings
+
+import astroid
+from astroid import builder
+
+try:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ import nose # pylint: disable=unused-import
+ HAS_NOSE = True
+except ImportError:
+ HAS_NOSE = False
+
+
+@unittest.skipUnless(HAS_NOSE, "This test requires nose library.")
+class NoseBrainTest(unittest.TestCase):
+ def test_nose_tools(self):
+ methods = builder.extract_node(
+ """
+ from nose.tools import assert_equal
+ from nose.tools import assert_equals
+ from nose.tools import assert_true
+ assert_equal = assert_equal #@
+ assert_true = assert_true #@
+ assert_equals = assert_equals #@
+ """
+ )
+ assert isinstance(methods, list)
+ assert_equal = next(methods[0].value.infer())
+ assert_true = next(methods[1].value.infer())
+ assert_equals = next(methods[2].value.infer())
+
+ self.assertIsInstance(assert_equal, astroid.BoundMethod)
+ self.assertIsInstance(assert_true, astroid.BoundMethod)
+ self.assertIsInstance(assert_equals, astroid.BoundMethod)
+ self.assertEqual(assert_equal.qname(), "unittest.case.TestCase.assertEqual")
+ self.assertEqual(assert_true.qname(), "unittest.case.TestCase.assertTrue")
+ self.assertEqual(assert_equals.qname(), "unittest.case.TestCase.assertEqual")
diff --git a/tests/unittest_brain_pathlib.py b/tests/brain/test_pathlib.py
similarity index 100%
rename from tests/unittest_brain_pathlib.py
rename to tests/brain/test_pathlib.py
diff --git a/tests/brain/test_pytest.py b/tests/brain/test_pytest.py
new file mode 100644
index 0000000..55ecfb2
--- /dev/null
+++ b/tests/brain/test_pytest.py
@@ -0,0 +1,34 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+from astroid import builder
+
+
+def test_pytest() -> None:
+ ast_node = builder.extract_node(
+ """
+ import pytest
+ pytest #@
+ """
+ )
+ module = next(ast_node.infer())
+ attrs = [
+ "deprecated_call",
+ "warns",
+ "exit",
+ "fail",
+ "skip",
+ "importorskip",
+ "xfail",
+ "mark",
+ "raises",
+ "freeze_includes",
+ "set_trace",
+ "fixture",
+ "yield_fixture",
+ ]
+ for attr in attrs:
+ assert attr in module
diff --git a/tests/unittest_brain_qt.py b/tests/brain/test_qt.py
similarity index 100%
rename from tests/unittest_brain_qt.py
rename to tests/brain/test_qt.py
diff --git a/tests/test_brain_regex.py b/tests/brain/test_regex.py
similarity index 93%
rename from tests/test_brain_regex.py
rename to tests/brain/test_regex.py
index 1fbd59b..0d44074 100644
--- a/tests/test_brain_regex.py
+++ b/tests/brain/test_regex.py
@@ -24,6 +24,9 @@ class TestRegexBrain:
assert name in re_ast
assert next(re_ast[name].infer()).value == getattr(regex, name)
+ @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+."""
diff --git a/tests/unittest_brain_signal.py b/tests/brain/test_signal.py
similarity index 100%
rename from tests/unittest_brain_signal.py
rename to tests/brain/test_signal.py
diff --git a/tests/brain/test_six.py b/tests/brain/test_six.py
new file mode 100644
index 0000000..c9dac56
--- /dev/null
+++ b/tests/brain/test_six.py
@@ -0,0 +1,155 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+from typing import Any
+
+import astroid
+from astroid import MANAGER, builder, nodes
+from astroid.nodes.scoped_nodes import ClassDef
+
+try:
+ import six # pylint: disable=unused-import
+
+ HAS_SIX = True
+except ImportError:
+ HAS_SIX = False
+
+
+@unittest.skipUnless(HAS_SIX, "These tests require the six library")
+class SixBrainTest(unittest.TestCase):
+ def test_attribute_access(self) -> None:
+ ast_nodes = builder.extract_node(
+ """
+ import six
+ six.moves.http_client #@
+ six.moves.urllib_parse #@
+ six.moves.urllib_error #@
+ six.moves.urllib.request #@
+ """
+ )
+ assert isinstance(ast_nodes, list)
+ http_client = next(ast_nodes[0].infer())
+ self.assertIsInstance(http_client, nodes.Module)
+ self.assertEqual(http_client.name, "http.client")
+
+ urllib_parse = next(ast_nodes[1].infer())
+ self.assertIsInstance(urllib_parse, nodes.Module)
+ self.assertEqual(urllib_parse.name, "urllib.parse")
+ urljoin = next(urllib_parse.igetattr("urljoin"))
+ urlencode = next(urllib_parse.igetattr("urlencode"))
+ self.assertIsInstance(urljoin, nodes.FunctionDef)
+ self.assertEqual(urljoin.qname(), "urllib.parse.urljoin")
+ self.assertIsInstance(urlencode, nodes.FunctionDef)
+ self.assertEqual(urlencode.qname(), "urllib.parse.urlencode")
+
+ urllib_error = next(ast_nodes[2].infer())
+ self.assertIsInstance(urllib_error, nodes.Module)
+ self.assertEqual(urllib_error.name, "urllib.error")
+ urlerror = next(urllib_error.igetattr("URLError"))
+ self.assertIsInstance(urlerror, nodes.ClassDef)
+ content_too_short = next(urllib_error.igetattr("ContentTooShortError"))
+ self.assertIsInstance(content_too_short, nodes.ClassDef)
+
+ urllib_request = next(ast_nodes[3].infer())
+ self.assertIsInstance(urllib_request, nodes.Module)
+ self.assertEqual(urllib_request.name, "urllib.request")
+ urlopen = next(urllib_request.igetattr("urlopen"))
+ urlretrieve = next(urllib_request.igetattr("urlretrieve"))
+ self.assertIsInstance(urlopen, nodes.FunctionDef)
+ self.assertEqual(urlopen.qname(), "urllib.request.urlopen")
+ self.assertIsInstance(urlretrieve, nodes.FunctionDef)
+ self.assertEqual(urlretrieve.qname(), "urllib.request.urlretrieve")
+
+ def test_from_imports(self) -> None:
+ ast_node = builder.extract_node(
+ """
+ from six.moves import http_client
+ http_client.HTTPSConnection #@
+ """
+ )
+ inferred = next(ast_node.infer())
+ self.assertIsInstance(inferred, nodes.ClassDef)
+ qname = "http.client.HTTPSConnection"
+ self.assertEqual(inferred.qname(), qname)
+
+ def test_from_submodule_imports(self) -> None:
+ """Make sure ulrlib submodules can be imported from
+
+ See PyCQA/pylint#1640 for relevant issue
+ """
+ ast_node = builder.extract_node(
+ """
+ from six.moves.urllib.parse import urlparse
+ urlparse #@
+ """
+ )
+ inferred = next(ast_node.infer())
+ self.assertIsInstance(inferred, nodes.FunctionDef)
+
+ def test_with_metaclass_subclasses_inheritance(self) -> None:
+ ast_node = builder.extract_node(
+ """
+ class A(type):
+ def test(cls):
+ return cls
+
+ class C:
+ pass
+
+ import six
+ class B(six.with_metaclass(A, C)):
+ pass
+
+ B #@
+ """
+ )
+ inferred = next(ast_node.infer())
+ self.assertIsInstance(inferred, nodes.ClassDef)
+ self.assertEqual(inferred.name, "B")
+ self.assertIsInstance(inferred.bases[0], nodes.Call)
+ ancestors = tuple(inferred.ancestors())
+ self.assertIsInstance(ancestors[0], nodes.ClassDef)
+ self.assertEqual(ancestors[0].name, "C")
+ self.assertIsInstance(ancestors[1], nodes.ClassDef)
+ self.assertEqual(ancestors[1].name, "object")
+
+ @staticmethod
+ def test_six_with_metaclass_enum_ancestor() -> None:
+ code = """
+ import six
+ from enum import Enum, EnumMeta
+
+ class FooMeta(EnumMeta):
+ pass
+
+ class Foo(six.with_metaclass(FooMeta, Enum)): #@
+ bar = 1
+ """
+ klass = astroid.extract_node(code)
+ assert next(klass.ancestors()).name == "Enum"
+
+ def test_six_with_metaclass_with_additional_transform(self) -> None:
+ def transform_class(cls: Any) -> ClassDef:
+ if cls.name == "A":
+ cls._test_transform = 314
+ return cls
+
+ MANAGER.register_transform(nodes.ClassDef, transform_class)
+ try:
+ ast_node = builder.extract_node(
+ """
+ import six
+ class A(six.with_metaclass(type, object)):
+ pass
+
+ A #@
+ """
+ )
+ inferred = next(ast_node.infer())
+ assert getattr(inferred, "_test_transform", None) == 314
+ finally:
+ MANAGER.unregister_transform(nodes.ClassDef, transform_class)
diff --git a/tests/test_brain_ssl.py b/tests/brain/test_ssl.py
similarity index 100%
rename from tests/test_brain_ssl.py
rename to tests/brain/test_ssl.py
diff --git a/tests/brain/test_threading.py b/tests/brain/test_threading.py
new file mode 100644
index 0000000..f7da03d
--- /dev/null
+++ b/tests/brain/test_threading.py
@@ -0,0 +1,54 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import unittest
+
+import astroid
+from astroid import builder
+from astroid.bases import Instance
+
+
+class ThreadingBrainTest(unittest.TestCase):
+ def test_lock(self) -> None:
+ lock_instance = builder.extract_node(
+ """
+ import threading
+ threading.Lock()
+ """
+ )
+ inferred = next(lock_instance.infer())
+ self.assert_is_valid_lock(inferred)
+
+ acquire_method = inferred.getattr("acquire")[0]
+ parameters = [param.name for param in acquire_method.args.args[1:]]
+ assert parameters == ["blocking", "timeout"]
+
+ assert inferred.getattr("locked")
+
+ def test_rlock(self) -> None:
+ self._test_lock_object("RLock")
+
+ def test_semaphore(self) -> None:
+ self._test_lock_object("Semaphore")
+
+ def test_boundedsemaphore(self) -> None:
+ self._test_lock_object("BoundedSemaphore")
+
+ def _test_lock_object(self, object_name: str) -> None:
+ lock_instance = builder.extract_node(
+ f"""
+ import threading
+ threading.{object_name}()
+ """
+ )
+ inferred = next(lock_instance.infer())
+ self.assert_is_valid_lock(inferred)
+
+ def assert_is_valid_lock(self, inferred: Instance) -> None:
+ self.assertIsInstance(inferred, astroid.Instance)
+ self.assertEqual(inferred.root().name, "threading")
+ for method in ("acquire", "release", "__enter__", "__exit__"):
+ self.assertIsInstance(next(inferred.igetattr(method)), astroid.BoundMethod)
diff --git a/tests/brain/test_typing_extensions.py b/tests/brain/test_typing_extensions.py
new file mode 100644
index 0000000..27ee6ee
--- /dev/null
+++ b/tests/brain/test_typing_extensions.py
@@ -0,0 +1,41 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+
+from __future__ import annotations
+
+import pytest
+
+from astroid import builder, nodes
+
+try:
+ import typing_extensions # pylint: disable=unused-import
+
+ HAS_TYPING_EXTENSIONS = True
+ HAS_TYPING_EXTENSIONS_TYPEVAR = hasattr(typing_extensions, "TypeVar")
+except ImportError:
+ HAS_TYPING_EXTENSIONS = False
+ HAS_TYPING_EXTENSIONS_TYPEVAR = False
+
+
+@pytest.mark.skipif(
+ not HAS_TYPING_EXTENSIONS,
+ reason="These tests require the typing_extensions library",
+)
+class TestTypingExtensions:
+ @staticmethod
+ @pytest.mark.skipif(
+ not HAS_TYPING_EXTENSIONS_TYPEVAR,
+ reason="Need typing_extensions>=4.4.0 to test TypeVar",
+ )
+ def test_typing_extensions_types() -> None:
+ ast_nodes = builder.extract_node(
+ """
+ from typing_extensions import TypeVar
+ TypeVar('MyTypeVar', int, float, complex) #@
+ TypeVar('AnyStr', str, bytes) #@
+ """
+ )
+ for node in ast_nodes:
+ inferred = next(node.infer())
+ assert isinstance(inferred, nodes.ClassDef)
diff --git a/tests/unittest_brain_unittest.py b/tests/brain/test_unittest.py
similarity index 100%
rename from tests/unittest_brain_unittest.py
rename to tests/brain/test_unittest.py
diff --git a/tests/unittest_builder.py b/tests/test_builder.py
similarity index 99%
rename from tests/unittest_builder.py
rename to tests/test_builder.py
index 6c0c5fa..0da3f7f 100644
--- a/tests/unittest_builder.py
+++ b/tests/test_builder.py
@@ -1023,7 +1023,3 @@ class HermeticInterpreterTest(unittest.TestCase):
my_builder.module_build(
self.imported_module, modname=self.imported_module_path.stem
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_constraint.py b/tests/test_constraint.py
similarity index 100%
rename from tests/unittest_constraint.py
rename to tests/test_constraint.py
diff --git a/tests/unittest_decorators.py b/tests/test_decorators.py
similarity index 100%
rename from tests/unittest_decorators.py
rename to tests/test_decorators.py
diff --git a/tests/unittest_filter_statements.py b/tests/test_filter_statements.py
similarity index 100%
rename from tests/unittest_filter_statements.py
rename to tests/test_filter_statements.py
diff --git a/tests/test_group_exceptions.py b/tests/test_group_exceptions.py
new file mode 100644
index 0000000..11065aa
--- /dev/null
+++ b/tests/test_group_exceptions.py
@@ -0,0 +1,111 @@
+# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
+# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
+import textwrap
+
+import pytest
+
+from astroid import (
+ AssignName,
+ ExceptHandler,
+ For,
+ Name,
+ TryExcept,
+ Uninferable,
+ bases,
+ extract_node,
+)
+from astroid.const import PY311_PLUS
+from astroid.context import InferenceContext
+from astroid.nodes import Expr, Raise, TryStar
+
+
+@pytest.mark.skipif(not PY311_PLUS, reason="Requires Python 3.11 or higher")
+def test_group_exceptions() -> None:
+ node = extract_node(
+ textwrap.dedent(
+ """
+ try:
+ raise ExceptionGroup("group", [ValueError(654)])
+ except ExceptionGroup as eg:
+ for err in eg.exceptions:
+ if isinstance(err, ValueError):
+ print("Handling ValueError")
+ elif isinstance(err, TypeError):
+ print("Handling TypeError")"""
+ )
+ )
+ assert isinstance(node, TryExcept)
+ handler = node.handlers[0]
+ exception_group_block_range = (1, 4)
+ assert node.block_range(lineno=1) == exception_group_block_range
+ assert node.block_range(lineno=2) == (2, 2)
+ assert node.block_range(lineno=5) == (5, 9)
+ assert isinstance(handler, ExceptHandler)
+ assert handler.type.name == "ExceptionGroup"
+ children = list(handler.get_children())
+ assert len(children) == 3
+ exception_group, short_name, for_loop = children
+ assert isinstance(exception_group, Name)
+ assert exception_group.block_range(1) == exception_group_block_range
+ assert isinstance(short_name, AssignName)
+ assert isinstance(for_loop, For)
+
+
+@pytest.mark.skipif(not PY311_PLUS, reason="Requires Python 3.11 or higher")
+def test_star_exceptions() -> None:
+ code = textwrap.dedent(
+ """
+ try:
+ raise ExceptionGroup("group", [ValueError(654)])
+ except* ValueError:
+ print("Handling ValueError")
+ except* TypeError:
+ print("Handling TypeError")
+ else:
+ sys.exit(127)
+ finally:
+ sys.exit(0)"""
+ )
+ node = extract_node(code)
+ assert isinstance(node, TryStar)
+ assert node.as_string() == code.replace('"', "'").strip()
+ assert isinstance(node.body[0], Raise)
+ assert node.block_range(1) == (1, 11)
+ assert node.block_range(2) == (2, 2)
+ assert node.block_range(3) == (3, 3)
+ assert node.block_range(4) == (4, 4)
+ assert node.block_range(5) == (5, 5)
+ assert node.block_range(6) == (6, 6)
+ assert node.block_range(7) == (7, 7)
+ assert node.block_range(8) == (8, 8)
+ assert node.block_range(9) == (9, 9)
+ assert node.block_range(10) == (10, 10)
+ assert node.block_range(11) == (11, 11)
+ assert node.handlers
+ handler = node.handlers[0]
+ assert isinstance(handler, ExceptHandler)
+ assert handler.type.name == "ValueError"
+ orelse = node.orelse[0]
+ assert isinstance(orelse, Expr)
+ assert orelse.value.args[0].value == 127
+ final = node.finalbody[0]
+ assert isinstance(final, Expr)
+ assert final.value.args[0].value == 0
+
+
+@pytest.mark.skipif(not PY311_PLUS, reason="Requires Python 3.11 or higher")
+def test_star_exceptions_infer_name() -> None:
+ trystar = extract_node(
+ """
+try:
+ 1/0
+except* ValueError:
+ pass"""
+ )
+ name = "arbitraryName"
+ context = InferenceContext()
+ context.lookupname = name
+ stmts = bases._infer_stmts([trystar], context)
+ assert list(stmts) == [Uninferable]
+ assert context.lookupname == name
diff --git a/tests/unittest_helpers.py b/tests/test_helpers.py
similarity index 99%
rename from tests/unittest_helpers.py
rename to tests/test_helpers.py
index e0da009..90182a2 100644
--- a/tests/unittest_helpers.py
+++ b/tests/test_helpers.py
@@ -42,6 +42,7 @@ class TestHelpers(unittest.TestCase):
("type", self._extract("type")),
("object", self._extract("type")),
("object()", self._extract("object")),
+ ("super()", self._extract("super")),
("lambda: None", self._build_custom_builtin("function")),
("len", self._build_custom_builtin("builtin_function_or_method")),
("None", self._build_custom_builtin("NoneType")),
@@ -258,7 +259,3 @@ class TestHelpers(unittest.TestCase):
builtin_type = self._extract("type")
self.assertTrue(helpers.is_supertype(builtin_type, cls_a))
self.assertTrue(helpers.is_subtype(cls_a, builtin_type))
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_inference.py b/tests/test_inference.py
similarity index 99%
rename from tests/unittest_inference.py
rename to tests/test_inference.py
index 5aebd04..86fdbcf 100644
--- a/tests/unittest_inference.py
+++ b/tests/test_inference.py
@@ -4048,6 +4048,11 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase):
inferred = next(node.infer())
self.assertRaises(InferenceError, next, inferred.infer_call_result(node))
+ def test_infer_call_result_with_metaclass(self) -> None:
+ node = extract_node("def with_metaclass(meta, *bases): return 42")
+ inferred = next(node.infer_call_result(caller=node))
+ self.assertIsInstance(inferred, nodes.Const)
+
def test_context_call_for_context_managers(self) -> None:
ast_nodes = extract_node(
"""
@@ -7097,7 +7102,3 @@ class TestOldStyleStringFormatting:
inferred = next(node.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.value == "My name is Daniel, I'm 12.00"
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_inference_calls.py b/tests/test_inference_calls.py
similarity index 100%
rename from tests/unittest_inference_calls.py
rename to tests/test_inference_calls.py
diff --git a/tests/unittest_lookup.py b/tests/test_lookup.py
similarity index 99%
rename from tests/unittest_lookup.py
rename to tests/test_lookup.py
index 835b315..cc882e6 100644
--- a/tests/unittest_lookup.py
+++ b/tests/test_lookup.py
@@ -1008,7 +1008,3 @@ class LookupControlFlowTest(unittest.TestCase):
_, stmts = x_name.lookup("x")
self.assertEqual(len(stmts), 1)
self.assertEqual(stmts[0].lineno, 8)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_manager.py b/tests/test_manager.py
similarity index 97%
rename from tests/unittest_manager.py
rename to tests/test_manager.py
index f35b94e..19efc73 100644
--- a/tests/unittest_manager.py
+++ b/tests/test_manager.py
@@ -7,6 +7,7 @@ import site
import sys
import time
import unittest
+import warnings
from collections.abc import Iterator
from contextlib import contextmanager
from unittest import mock
@@ -22,7 +23,7 @@ from astroid.exceptions import (
AttributeInferenceError,
)
from astroid.interpreter._import import util
-from astroid.modutils import EXT_LIB_DIRS, is_standard_module
+from astroid.modutils import EXT_LIB_DIRS, module_in_path
from astroid.nodes import Const
from astroid.nodes.scoped_nodes import ClassDef
@@ -383,6 +384,15 @@ class AstroidManagerTest(
self.manager.ast_from_module_name(None)
+class IsolatedAstroidManagerTest(resources.AstroidCacheSetupMixin, unittest.TestCase):
+ def test_no_user_warning(self):
+ mgr = manager.AstroidManager()
+ with warnings.catch_warnings():
+ warnings.filterwarnings("error", category=UserWarning)
+ mgr.ast_from_module_name("setuptools")
+ mgr.ast_from_module_name("pip")
+
+
class BorgAstroidManagerTC(unittest.TestCase):
def test_borg(self) -> None:
"""Test that the AstroidManager is really a borg, i.e. that two different
@@ -411,7 +421,7 @@ class ClearCacheTest(unittest.TestCase):
# Generate some hits and misses
ClassDef().lookup("garbage")
- is_standard_module("unittest", std_path=["garbage_path"])
+ module_in_path("unittest", "garbage_path")
util.is_namespace("unittest")
astroid.interpreter.objectmodel.ObjectModel().attributes()
with pytest.raises(AttributeInferenceError):
@@ -430,6 +440,8 @@ class ClearCacheTest(unittest.TestCase):
astroid.MANAGER.clear_cache() # also calls bootstrap()
+ self.assertEqual(astroid.context._INFERENCE_CACHE, {})
+
# The cache sizes are now as low or lower than the original baseline
cleared_cache_infos = [lru.cache_info() for lru in lrus]
for cleared_cache, baseline_cache in zip(
@@ -457,7 +469,3 @@ class ClearCacheTest(unittest.TestCase):
isinstance_call = astroid.extract_node("isinstance(1, int)")
inferred = next(isinstance_call.infer())
self.assertIs(inferred.value, True)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_modutils.py b/tests/test_modutils.py
similarity index 70%
rename from tests/unittest_modutils.py
rename to tests/test_modutils.py
index ab1acaa..04f5eee 100644
--- a/tests/unittest_modutils.py
+++ b/tests/test_modutils.py
@@ -20,6 +20,7 @@ from pytest import CaptureFixture, LogCaptureFixture
import astroid
from astroid import modutils
+from astroid.const import PY310_PLUS
from astroid.interpreter._import import spec
from . import resources
@@ -27,9 +28,9 @@ from . import resources
try:
import urllib3 # pylint: disable=unused-import
- HAS_URLLIB3 = True
+ HAS_URLLIB3_V1 = urllib3.__version__.startswith("1")
except ImportError:
- HAS_URLLIB3 = False
+ HAS_URLLIB3_V1 = False
def _get_file_from_object(obj) -> str:
@@ -287,7 +288,7 @@ class GetSourceFileTest(unittest.TestCase):
self.assertRaises(modutils.NoSourceFile, modutils.get_source_file, "whatever")
-class StandardLibModuleTest(resources.SysPathSetup, unittest.TestCase):
+class IsStandardModuleTest(resources.SysPathSetup, unittest.TestCase):
"""
Return true if the module may be considered as a module from the standard
library.
@@ -296,50 +297,153 @@ class StandardLibModuleTest(resources.SysPathSetup, unittest.TestCase):
def test_datetime(self) -> None:
# This is an interesting example, since datetime, on pypy,
# is under lib_pypy, rather than the usual Lib directory.
- self.assertTrue(modutils.is_standard_module("datetime"))
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("datetime")
def test_builtins(self) -> None:
- self.assertFalse(modutils.is_standard_module("__builtin__"))
- self.assertTrue(modutils.is_standard_module("builtins"))
+ with pytest.warns(DeprecationWarning):
+ assert not modutils.is_standard_module("__builtin__")
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("builtins")
def test_builtin(self) -> None:
- self.assertTrue(modutils.is_standard_module("sys"))
- self.assertTrue(modutils.is_standard_module("marshal"))
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("sys")
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("marshal")
def test_nonstandard(self) -> None:
- self.assertFalse(modutils.is_standard_module("astroid"))
+ with pytest.warns(DeprecationWarning):
+ assert not modutils.is_standard_module("astroid")
def test_unknown(self) -> None:
- self.assertFalse(modutils.is_standard_module("unknown"))
+ with pytest.warns(DeprecationWarning):
+ assert not modutils.is_standard_module("unknown")
def test_4(self) -> None:
- self.assertTrue(modutils.is_standard_module("hashlib"))
- self.assertTrue(modutils.is_standard_module("pickle"))
- self.assertTrue(modutils.is_standard_module("email"))
- self.assertTrue(modutils.is_standard_module("io"))
- self.assertFalse(modutils.is_standard_module("StringIO"))
- self.assertTrue(modutils.is_standard_module("unicodedata"))
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("hashlib")
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("pickle")
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("email")
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("io")
+ with pytest.warns(DeprecationWarning):
+ assert not modutils.is_standard_module("StringIO")
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("unicodedata")
def test_custom_path(self) -> None:
datadir = resources.find("")
if any(datadir.startswith(p) for p in modutils.EXT_LIB_DIRS):
self.skipTest("known breakage of is_standard_module on installed package")
- self.assertTrue(modutils.is_standard_module("data.module", (datadir,)))
- self.assertTrue(
- modutils.is_standard_module("data.module", (os.path.abspath(datadir),))
- )
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("data.module", (datadir,))
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module(
+ "data.module", (os.path.abspath(datadir),)
+ )
# "" will evaluate to cwd
- self.assertTrue(modutils.is_standard_module("data.module", ("",)))
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("data.module", ("",))
def test_failing_edge_cases(self) -> None:
# using a subpackage/submodule path as std_path argument
- self.assertFalse(modutils.is_standard_module("xml.etree", etree.__path__))
+ with pytest.warns(DeprecationWarning):
+ assert not modutils.is_standard_module("xml.etree", etree.__path__)
+ # using a module + object name as modname argument
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("sys.path")
+ # this is because only the first package/module is considered
+ with pytest.warns(DeprecationWarning):
+ assert modutils.is_standard_module("sys.whatever")
+ with pytest.warns(DeprecationWarning):
+ assert not modutils.is_standard_module("xml.whatever", etree.__path__)
+
+
+class IsStdLibModuleTest(resources.SysPathSetup, unittest.TestCase):
+ """
+ Return true if the module is path of the standard library
+ """
+
+ def test_datetime(self) -> None:
+ # This is an interesting example, since datetime, on pypy,
+ # is under lib_pypy, rather than the usual Lib directory.
+ assert modutils.is_stdlib_module("datetime")
+
+ def test_builtins(self) -> None:
+ assert not modutils.is_stdlib_module("__builtin__")
+ assert modutils.is_stdlib_module("builtins")
+
+ def test_builtin(self) -> None:
+ assert modutils.is_stdlib_module("sys")
+ assert modutils.is_stdlib_module("marshal")
+
+ def test_nonstandard(self) -> None:
+ assert not modutils.is_stdlib_module("astroid")
+
+ def test_unknown(self) -> None:
+ assert not modutils.is_stdlib_module("unknown")
+
+ def test_4(self) -> None:
+ assert modutils.is_stdlib_module("hashlib")
+ assert modutils.is_stdlib_module("pickle")
+ assert modutils.is_stdlib_module("email")
+ assert modutils.is_stdlib_module("io")
+ assert not modutils.is_stdlib_module("StringIO")
+ assert modutils.is_stdlib_module("unicodedata")
+
+ def test_subpackages(self) -> None:
# using a module + object name as modname argument
- self.assertTrue(modutils.is_standard_module("sys.path"))
+ assert modutils.is_stdlib_module("sys.path")
# this is because only the first package/module is considered
- self.assertTrue(modutils.is_standard_module("sys.whatever"))
- self.assertFalse(modutils.is_standard_module("xml.whatever", etree.__path__))
+ assert modutils.is_stdlib_module("sys.whatever")
+
+ def test_platform_specific(self) -> None:
+ assert modutils.is_stdlib_module("_curses")
+ assert modutils.is_stdlib_module("msvcrt")
+ assert modutils.is_stdlib_module("termios")
+
+
+class ModuleInPathTest(resources.SysPathSetup, unittest.TestCase):
+ """
+ Return true if the module is imported from the specified path
+ """
+
+ def test_success(self) -> None:
+ datadir = resources.find("")
+ assert modutils.module_in_path("data.module", datadir)
+ assert modutils.module_in_path("data.module", (datadir,))
+ assert modutils.module_in_path("data.module", os.path.abspath(datadir))
+ # "" will evaluate to cwd
+ assert modutils.module_in_path("data.module", "")
+
+ def test_bad_import(self) -> None:
+ datadir = resources.find("")
+ assert not modutils.module_in_path("this_module_is_no_more", datadir)
+
+ def test_no_filename(self) -> None:
+ datadir = resources.find("")
+ assert not modutils.module_in_path("sys", datadir)
+
+ def test_failure(self) -> None:
+ datadir = resources.find("")
+ assert not modutils.module_in_path("etree", datadir)
+ assert not modutils.module_in_path("astroid", datadir)
+
+
+class BackportStdlibNamesTest(resources.SysPathSetup, unittest.TestCase):
+ """
+ Verify backport raises exception on newer versions
+ """
+
+ @pytest.mark.skipif(not PY310_PLUS, reason="Backport valid on <=3.9")
+ def test_import_error(self) -> None:
+ with pytest.raises(AssertionError):
+ # pylint: disable-next=import-outside-toplevel, unused-import
+ from astroid import _backport_stdlib_names # noqa
class IsRelativeTest(unittest.TestCase):
@@ -443,14 +547,19 @@ class ExtensionPackageWhitelistTest(unittest.TestCase):
)
-@pytest.mark.skipif(not HAS_URLLIB3, reason="This test requires urllib3.")
+@pytest.mark.skipif(not HAS_URLLIB3_V1, reason="This test requires urllib3 < 2.")
def test_file_info_from_modpath__SixMetaPathImporter() -> None:
- pytest.raises(
- ImportError,
- modutils.file_info_from_modpath,
- ["urllib3.packages.six.moves.http_client"],
- )
+ """Six is not backported anymore in urllib3 v2.0.0+"""
+ assert modutils.file_info_from_modpath(["urllib3.packages.six.moves.http_client"])
-if __name__ == "__main__":
- unittest.main()
+def test_find_setuptools_pep660_editable_install():
+ """Find the spec for a package installed via setuptools PEP 660 import hooks."""
+ # pylint: disable-next=import-outside-toplevel
+ from tests.testdata.python3.data.import_setuptools_pep660.__editable___example_0_1_0_finder import (
+ _EditableFinder,
+ )
+
+ with unittest.mock.patch.object(sys, "meta_path", new=[_EditableFinder]):
+ assert spec.find_spec(["example"])
+ assert spec.find_spec(["example", "subpackage"])
diff --git a/tests/unittest_nodes.py b/tests/test_nodes.py
similarity index 98%
rename from tests/unittest_nodes.py
rename to tests/test_nodes.py
index e0ef916..0633434 100644
--- a/tests/unittest_nodes.py
+++ b/tests/test_nodes.py
@@ -869,7 +869,22 @@ class ArgumentsNodeTC(unittest.TestCase):
"""
)
args = ast["func"].args
- self.assertTrue(args.is_argument("x"))
+ assert isinstance(args, nodes.Arguments)
+ assert args.is_argument("x")
+ assert args.kw_defaults == [None]
+
+ ast = builder.parse(
+ """
+ def func(*, x = "default"):
+ pass
+ """
+ )
+ args = ast["func"].args
+ assert isinstance(args, nodes.Arguments)
+ assert args.is_argument("x")
+ assert len(args.kw_defaults) == 1
+ 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):
@@ -1933,6 +1948,21 @@ class TestPatternMatching:
*case1.pattern.kwd_patterns,
]
-
-if __name__ == "__main__":
- unittest.main()
+ @staticmethod
+ def test_return_from_match():
+ code = textwrap.dedent(
+ """
+ def return_from_match(x):
+ match x:
+ case 10:
+ return 10
+ case _:
+ return -1
+
+ return_from_match(10) #@
+ """
+ ).strip()
+ node = builder.extract_node(code)
+ inferred = node.inferred()
+ assert len(inferred) == 2
+ assert [inf.value for inf in inferred] == [10, -1]
diff --git a/tests/unittest_nodes_lineno.py b/tests/test_nodes_lineno.py
similarity index 100%
rename from tests/unittest_nodes_lineno.py
rename to tests/test_nodes_lineno.py
diff --git a/tests/unittest_nodes_position.py b/tests/test_nodes_position.py
similarity index 100%
rename from tests/unittest_nodes_position.py
rename to tests/test_nodes_position.py
diff --git a/tests/unittest_object_model.py b/tests/test_object_model.py
similarity index 99%
rename from tests/unittest_object_model.py
rename to tests/test_object_model.py
index 732593d..8f41eda 100644
--- a/tests/unittest_object_model.py
+++ b/tests/test_object_model.py
@@ -832,7 +832,3 @@ class LruCacheModelTest(unittest.TestCase):
self.assertEqual(wrapped.name, "foo")
cache_info = next(ast_nodes[2].infer())
self.assertIsInstance(cache_info, astroid.Instance)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_objects.py b/tests/test_objects.py
similarity index 99%
rename from tests/unittest_objects.py
rename to tests/test_objects.py
index 6e62d7d..d5994cc 100644
--- a/tests/unittest_objects.py
+++ b/tests/test_objects.py
@@ -587,7 +587,3 @@ class SuperTests(unittest.TestCase):
assert isinstance(next(init_node[1].infer()), bases.BoundMethod)
assert isinstance(next(init_node[2].infer()), bases.BoundMethod)
assert isinstance(next(init_node[3].infer()), bases.BoundMethod)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_protocols.py b/tests/test_protocols.py
similarity index 99%
rename from tests/unittest_protocols.py
rename to tests/test_protocols.py
index cde7465..48351bc 100644
--- a/tests/unittest_protocols.py
+++ b/tests/test_protocols.py
@@ -16,7 +16,7 @@ from astroid import extract_node, nodes
from astroid.const import PY38_PLUS, PY310_PLUS
from astroid.exceptions import InferenceError
from astroid.manager import AstroidManager
-from astroid.util import Uninferable
+from astroid.util import Uninferable, UninferableBase
@contextlib.contextmanager
@@ -125,7 +125,7 @@ class ProtocolTests(unittest.TestCase):
assert isinstance(assigned, astroid.List)
assert assigned.as_string() == "[1, 2]"
- def _get_starred_stmts(self, code: str) -> list | Uninferable:
+ def _get_starred_stmts(self, code: str) -> list | UninferableBase:
assign_stmt = extract_node(f"{code} #@")
starred = next(assign_stmt.nodes_of_class(nodes.Starred))
return next(starred.assigned_stmts())
@@ -136,7 +136,7 @@ class ProtocolTests(unittest.TestCase):
stmts = stmts.elts
self.assertConstNodesEqual(expected, stmts)
- def _helper_starred_expected(self, code: str, expected: Uninferable) -> None:
+ def _helper_starred_expected(self, code: str, expected: UninferableBase) -> None:
stmts = self._get_starred_stmts(code)
self.assertEqual(expected, stmts)
@@ -409,7 +409,3 @@ class TestPatternMatching:
assert match_as.name
assigned_match_as = next(match_as.name.assigned_stmts())
assert assigned_match_as == subject
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_python3.py b/tests/test_python3.py
similarity index 99%
rename from tests/unittest_python3.py
rename to tests/test_python3.py
index bf0a0b3..07bccc5 100644
--- a/tests/unittest_python3.py
+++ b/tests/test_python3.py
@@ -399,7 +399,3 @@ class Python3TC(unittest.TestCase):
)
func = extract_node(code)
self.assertEqual(func.as_string().strip(), code.strip())
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_raw_building.py b/tests/test_raw_building.py
similarity index 99%
rename from tests/unittest_raw_building.py
rename to tests/test_raw_building.py
index ad0f0af..cfb5d1b 100644
--- a/tests/unittest_raw_building.py
+++ b/tests/test_raw_building.py
@@ -162,7 +162,3 @@ def test_build_module_getattr_catch_output(
assert expected_err in caplog.text
assert not out
assert not err
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_regrtest.py b/tests/test_regrtest.py
similarity index 99%
rename from tests/unittest_regrtest.py
rename to tests/test_regrtest.py
index 587255f..783f1cc 100644
--- a/tests/unittest_regrtest.py
+++ b/tests/test_regrtest.py
@@ -439,7 +439,3 @@ def test_recursion_during_inference(mocked) -> None:
with pytest.raises(InferenceError) as error:
next(node.infer())
assert error.value.message.startswith("RecursionError raised")
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_scoped_nodes.py b/tests/test_scoped_nodes.py
similarity index 99%
rename from tests/unittest_scoped_nodes.py
rename to tests/test_scoped_nodes.py
index c59bdc3..2722c56 100644
--- a/tests/unittest_scoped_nodes.py
+++ b/tests/test_scoped_nodes.py
@@ -1403,6 +1403,20 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase):
self.assertEqual(["object"], [base.name for base in klass.ancestors()])
self.assertEqual("type", klass.metaclass().name)
+ @unittest.skipUnless(HAS_SIX, "These tests require the six library")
+ def test_metaclass_generator_hack_enum_base(self):
+ """Regression test for https://github.com/PyCQA/pylint/issues/5935"""
+ klass = builder.extract_node(
+ """
+ import six
+ from enum import Enum, EnumMeta
+
+ class PetEnumPy2Metaclass(six.with_metaclass(EnumMeta, Enum)): #@
+ DOG = "dog"
+ """
+ )
+ self.assertEqual(list(klass.local_attr_ancestors("DOG")), [])
+
def test_add_metaclass(self) -> None:
klass = builder.extract_node(
"""
@@ -2891,7 +2905,3 @@ def test_deprecation_of_doc_attribute() -> None:
node_class = nodes.ClassDef(name="MyClass", doc="Docstring")
node_func = nodes.FunctionDef(name="MyFunction", doc="Docstring")
assert len(records) == 3
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_transforms.py b/tests/test_transforms.py
similarity index 99%
rename from tests/unittest_transforms.py
rename to tests/test_transforms.py
index ce44d23..0f5b9c6 100644
--- a/tests/unittest_transforms.py
+++ b/tests/test_transforms.py
@@ -238,7 +238,3 @@ class TestTransforms(unittest.TestCase):
import UserDict
"""
)
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/unittest_utils.py b/tests/test_utils.py
similarity index 54%
rename from tests/unittest_utils.py
rename to tests/test_utils.py
index 1b8d712..417b0dc 100644
--- a/tests/unittest_utils.py
+++ b/tests/test_utils.py
@@ -4,7 +4,10 @@
import unittest
-from astroid import Uninferable, builder, nodes
+import pytest
+
+from astroid import Uninferable, builder, extract_node, nodes
+from astroid.const import PY38_PLUS
from astroid.exceptions import InferenceError
@@ -30,6 +33,78 @@ class InferenceUtil(unittest.TestCase):
self.assertEqual(nodes.are_exclusive(xass1, xnames[1]), False)
self.assertEqual(nodes.are_exclusive(xass1, xnames[2]), False)
+ @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions")
+ def test_not_exclusive_walrus_operator(self) -> None:
+ node_if, node_body, node_or_else = extract_node(
+ """
+ if val := True: #@
+ print(val) #@
+ else:
+ print(val) #@
+ """
+ )
+ node_if: nodes.If
+ node_walrus = next(node_if.nodes_of_class(nodes.NamedExpr))
+
+ assert nodes.are_exclusive(node_walrus, node_if) is False
+ assert nodes.are_exclusive(node_walrus, node_body) is False
+ assert nodes.are_exclusive(node_walrus, node_or_else) is False
+
+ assert nodes.are_exclusive(node_if, node_body) is False
+ assert nodes.are_exclusive(node_if, node_or_else) is False
+ assert nodes.are_exclusive(node_body, node_or_else) is True
+
+ @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions")
+ def test_not_exclusive_walrus_multiple(self) -> None:
+ node_if, body_1, body_2, or_else_1, or_else_2 = extract_node(
+ """
+ if (val := True) or (val_2 := True): #@
+ print(val) #@
+ print(val_2) #@
+ else:
+ print(val) #@
+ print(val_2) #@
+ """
+ )
+ node_if: nodes.If
+ walruses = list(node_if.nodes_of_class(nodes.NamedExpr))
+
+ assert nodes.are_exclusive(node_if, walruses[0]) is False
+ assert nodes.are_exclusive(node_if, walruses[1]) is False
+
+ assert nodes.are_exclusive(walruses[0], walruses[1]) is False
+
+ assert nodes.are_exclusive(walruses[0], body_1) is False
+ assert nodes.are_exclusive(walruses[0], body_2) is False
+ assert nodes.are_exclusive(walruses[1], body_1) is False
+ assert nodes.are_exclusive(walruses[1], body_2) is False
+
+ assert nodes.are_exclusive(walruses[0], or_else_1) is False
+ assert nodes.are_exclusive(walruses[0], or_else_2) is False
+ assert nodes.are_exclusive(walruses[1], or_else_1) is False
+ assert nodes.are_exclusive(walruses[1], or_else_2) is False
+
+ @pytest.mark.skipif(not PY38_PLUS, reason="needs assignment expressions")
+ def test_not_exclusive_walrus_operator_nested(self) -> None:
+ node_if, node_body, node_or_else = extract_node(
+ """
+ if all((last_val := i) % 2 == 0 for i in range(10)): #@
+ print(last_val) #@
+ else:
+ print(last_val) #@
+ """
+ )
+ node_if: nodes.If
+ node_walrus = next(node_if.nodes_of_class(nodes.NamedExpr))
+
+ assert nodes.are_exclusive(node_walrus, node_if) is False
+ assert nodes.are_exclusive(node_walrus, node_body) is False
+ assert nodes.are_exclusive(node_walrus, node_or_else) is False
+
+ assert nodes.are_exclusive(node_if, node_body) is False
+ assert nodes.are_exclusive(node_if, node_or_else) is False
+ assert nodes.are_exclusive(node_body, node_or_else) is True
+
def test_if(self) -> None:
module = builder.parse(
"""
@@ -111,7 +186,3 @@ class InferenceUtil(unittest.TestCase):
inferred = next(node.infer())
with self.assertRaises(InferenceError):
list(nodes.unpack_infer(inferred))
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/tests/testdata/python3/data/import_setuptools_pep660/__editable___example_0_1_0_finder.py b/tests/testdata/python3/data/import_setuptools_pep660/__editable___example_0_1_0_finder.py
new file mode 100644
index 0000000..7e324f4
--- /dev/null
+++ b/tests/testdata/python3/data/import_setuptools_pep660/__editable___example_0_1_0_finder.py
@@ -0,0 +1,72 @@
+"""This file contains Finders automatically generated by setuptools for a package installed
+in editable mode via custom import hooks. It's generated here:
+https://github.com/pypa/setuptools/blob/c34b82735c1a9c8707bea00705ae2f621bf4c24d/setuptools/command/editable_wheel.py#L732-L801
+"""
+import sys
+from importlib.machinery import ModuleSpec
+from importlib.machinery import all_suffixes as module_suffixes
+from importlib.util import spec_from_file_location
+from itertools import chain
+from pathlib import Path
+
+MAPPING = {"example": Path(__file__).parent.resolve() / "example"}
+NAMESPACES = {}
+PATH_PLACEHOLDER = "__editable__.example-0.1.0.finder" + ".__path_hook__"
+
+
+class _EditableFinder: # MetaPathFinder
+ @classmethod
+ def find_spec(cls, fullname, path=None, target=None):
+ for pkg, pkg_path in reversed(list(MAPPING.items())):
+ if fullname == pkg or fullname.startswith(f"{pkg}."):
+ rest = fullname.replace(pkg, "", 1).strip(".").split(".")
+ return cls._find_spec(fullname, Path(pkg_path, *rest))
+
+ return None
+
+ @classmethod
+ def _find_spec(cls, fullname, candidate_path):
+ init = candidate_path / "__init__.py"
+ candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
+ for candidate in chain([init], candidates):
+ if candidate.exists():
+ return spec_from_file_location(fullname, candidate)
+
+
+class _EditableNamespaceFinder: # PathEntryFinder
+ @classmethod
+ def _path_hook(cls, path):
+ if path == PATH_PLACEHOLDER:
+ return cls
+ raise ImportError
+
+ @classmethod
+ def _paths(cls, fullname):
+ # Ensure __path__ is not empty for the spec to be considered a namespace.
+ return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]
+
+ @classmethod
+ def find_spec(cls, fullname, target=None):
+ if fullname in NAMESPACES:
+ spec = ModuleSpec(fullname, None, is_package=True)
+ spec.submodule_search_locations = cls._paths(fullname)
+ return spec
+ return None
+
+ @classmethod
+ def find_module(cls, fullname):
+ return None
+
+
+def install():
+ if not any(finder == _EditableFinder for finder in sys.meta_path):
+ sys.meta_path.append(_EditableFinder)
+
+ if not NAMESPACES:
+ return
+
+ if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):
+ # PathEntryFinder is needed to create NamespaceSpec without private APIS
+ sys.path_hooks.append(_EditableNamespaceFinder._path_hook)
+ if PATH_PLACEHOLDER not in sys.path:
+ sys.path.append(PATH_PLACEHOLDER) # Used just to trigger the path hook
diff --git a/tests/testdata/python3/data/import_setuptools_pep660/example/__init__.py b/tests/testdata/python3/data/import_setuptools_pep660/example/__init__.py
new file mode 100644
index 0000000..643085b
--- /dev/null
+++ b/tests/testdata/python3/data/import_setuptools_pep660/example/__init__.py
@@ -0,0 +1 @@
+from subpackage import hello
diff --git a/tests/testdata/python3/data/import_setuptools_pep660/example/subpackage/__init__.py b/tests/testdata/python3/data/import_setuptools_pep660/example/subpackage/__init__.py
new file mode 100644
index 0000000..d750169
--- /dev/null
+++ b/tests/testdata/python3/data/import_setuptools_pep660/example/subpackage/__init__.py
@@ -0,0 +1 @@
+hello = 1
diff --git a/tests/testdata/python3/data/path_pkg_resources_1/package/__init__.py b/tests/testdata/python3/data/path_pkg_resources_1/package/__init__.py
index b0d6433..f7bd573 100644
--- a/tests/testdata/python3/data/path_pkg_resources_1/package/__init__.py
+++ b/tests/testdata/python3/data/path_pkg_resources_1/package/__init__.py
@@ -1 +1,5 @@
-__import__('pkg_resources').declare_namespace(__name__)
\ No newline at end of file
+import warnings
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ __import__("pkg_resources").declare_namespace(__name__)
diff --git a/tox.ini b/tox.ini
index 6f77def..4729e48 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,67 +1,21 @@
[tox]
-envlist = py{37,38,39,310}
+envlist = py{37,38,39,310,311}
skip_missing_interpreters = true
isolated_build = true
-[testenv:pylint]
-deps =
- # We do not use the latest pylint version in CI tests as we want to choose when
- # we fix the warnings
- git+https://github.com/pycqa/pylint@main
- pre-commit~=2.13
- -r requirements_test_min.txt
-commands = pre-commit run pylint --all-files
-
[testenv]
deps =
- -r requirements_test_min.txt
+ -r requirements_test.txt
-r requirements_test_brain.txt
- coverage<5
-
-setenv =
- COVERAGE_FILE = {toxinidir}/.coverage.{envname}
-
commands =
- ; --pyargs is needed so the directory astroid doesn't shadow the tox
- ; installed astroid package
- ; This is important for tests' test data which create files
- ; inside the package
- python -Wi {envsitepackagesdir}/coverage run -m pytest --pyargs {posargs:tests}
+ pytest --cov {posargs}
[testenv:formatting]
-basepython = python3
deps =
- pytest
- git+https://github.com/pycqa/pylint@main
- pre-commit~=2.13
+ -r requirements_test_pre_commit.txt
commands =
pre-commit run --all-files
-[testenv:coveralls]
-setenv =
- COVERAGE_FILE = {toxinidir}/.coverage
-passenv =
- *
-deps =
- coverage<5
- coveralls
-skip_install = true
-commands =
- python {envsitepackagesdir}/coverage combine --append
- python {envsitepackagesdir}/coverage report --rcfile={toxinidir}/.coveragerc -m
- - coveralls --rcfile={toxinidir}/.coveragerc
-changedir = {toxinidir}
-
-[testenv:coverage-erase]
-setenv =
- COVERAGE_FILE = {toxinidir}/.coverage
-deps =
- coverage<5
-skip_install = true
-commands =
- python {envsitepackagesdir}/coverage erase
-changedir = {toxinidir}
-
[testenv:docs]
skipsdist = True
usedevelop = True
More details
Historical runs
- failed: FAILED tests/test_manager.py::IsolatedAstroidManagerTest::test_no_user_warning
- nothing-to-do: Last upstream version 2.12.13 already imported.
- nothing-to-do: Last upstream version 2.12.13 already imported.
- nothing-to-do: Last upstream version 2.12.13 already imported.
- nothing-to-do: Last upstream version 2.12.12 already imported.
- worker-timeout: No keepalives received in 1:00:53.126559.
- run-disappeared: Worker started processing new run rather than 07e21ac2-199a-4157-adef-6ec288e74993
- run-disappeared: Worker started processing new run rather than b264634c-c673-40a9-9582-94abb4aa4f6f
- run-disappeared: Jenkins job https://jenkins.debian.net/job/janitor-worker/744139/ has disappeared
- success: Merged new upstream version 2.11.3
- build-failed-stage-explain-bd-uninstallable: build failed stage explain-bd-uninstallable