Repository: pylint-dev/astroid

Issue:
Remove monkey-patching of methods onto classes
In [this comment](https://github.com/PyCQA/astroid/issues/170#issuecomment-163118573), @PCManticore stated that there was a plan to remove monkey-patching of methods onto classes. inference.py and protocols.py seem to suggest that this hasn't happened.

Is this still a design goal?

It looks to me like this would be a fairly mechanical change. I am assuming that since it hasn't happened yet, I am missing something. Is it simply a "haven't had time" issue?


Discussion / hints:
Thanks for creating the issue @degustaf 

Yes, this is still a design goal that we'd want to achieve with `astroid`. There are two reasons for why this didn't happen yet, the first one being the one that you identified about not having enough time to do it. The second one is that it's a rather tricky issue, since there are various cyclic dependencies between `inference.py`, `protocols.py` and the rest of `astroid` files. We attempted to do this in the so called "2.0" branch of astroid (https://github.com/PyCQA/astroid/tree/2.0) which was an effort to reengineer most of astroid's architecture. If you look in that branch, you can see that we managed to reduce that monkeypatching quite a bit, if not all, but at this point, that branch is quite behind everything else and has a lot more changes that can't come easily in `master` without additional work.

If you're interested in helping, cherry-picking the refactoring for dropping the monkeypatch would be definitely amazing.
Can someone provide some details on this please? 
astroid 2.0 branch is the default for a long time now, but Manticore's comment is quite recent.
Are there any examples before-after? 

But overall I just saw "contributor friendly" tag and wanted to see if I could be of any help.
Hey @gyermolenko ! This might not be contributor friendly after all, as it requires going through https://github.com/PyCQA/astroid/tree/2.0 and bringing back into `master` the removal of monkeypatching methods. The gist of the solution is the use of single dispatch to register inference functions: https://github.com/PyCQA/astroid/blob/2.0/astroid/inference.py#L37



Fix (patch):
diff --git a/ChangeLog b/ChangeLog
index d05a19c909..0a4414809d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -19,6 +19,13 @@ Release date: TBA
   Closes #1780
   Refs #2140
 
+* Remove the ``inference`` module. Node inference methods are now in the module
+  defining the node, rather than being associated to the node afterward.
+
+  Closes #679
+
+* Move ``LookupMixIn`` to ``astroid.nodes._base_nodes`` and make it private.
+
 * Reduce file system access in ``ast_from_file()``.
 
 * Reduce time to ``import astroid`` by delaying ``astroid_bootstrapping()`` until
diff --git a/astroid/__init__.py b/astroid/__init__.py
index f3c2c79018..29e052e1f7 100644
--- a/astroid/__init__.py
+++ b/astroid/__init__.py
@@ -40,7 +40,7 @@
 
 # isort: on
 
-from astroid import inference, raw_building
+from astroid import raw_building
 from astroid.__pkginfo__ import __version__, version
 from astroid.astroid_manager import MANAGER
 from astroid.bases import BaseInstance, BoundMethod, Instance, UnboundMethod
diff --git a/astroid/bases.py b/astroid/bases.py
index 2f756a615e..bd8f6bb958 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -10,9 +10,9 @@
 import collections
 import collections.abc
 from collections.abc import Iterable, Iterator
-from typing import TYPE_CHECKING, Any, ClassVar, Literal
+from typing import TYPE_CHECKING, Any, Literal
 
-from astroid import nodes
+from astroid import decorators, nodes
 from astroid.const import PY310_PLUS
 from astroid.context import (
     CallContext,
@@ -28,7 +28,6 @@
 )
 from astroid.interpreter import objectmodel
 from astroid.typing import (
-    InferBinaryOp,
     InferenceErrorInfo,
     InferenceResult,
     SuccessfulInferenceResult,
@@ -346,7 +345,16 @@ class Instance(BaseInstance):
     def __init__(self, proxied: nodes.ClassDef | None) -> None:
         super().__init__(proxied)
 
-    infer_binary_op: ClassVar[InferBinaryOp[Instance]]
+    @decorators.yes_if_nothing_inferred
+    def infer_binary_op(
+        self,
+        opnode: nodes.AugAssign | nodes.BinOp,
+        operator: str,
+        other: InferenceResult,
+        context: InferenceContext,
+        method: SuccessfulInferenceResult,
+    ) -> Generator[InferenceResult, None, None]:
+        return method.infer_call_result(self, context)
 
     def __repr__(self) -> str:
         return "<Instance of {}.{} at 0x{}>".format(
diff --git a/astroid/constraint.py b/astroid/constraint.py
index 6e23b592f1..08bb80e3c9 100644
--- a/astroid/constraint.py
+++ b/astroid/constraint.py
@@ -8,9 +8,9 @@
 import sys
 from abc import ABC, abstractmethod
 from collections.abc import Iterator
-from typing import Union
+from typing import TYPE_CHECKING, Union
 
-from astroid import bases, nodes, util
+from astroid import nodes, util
 from astroid.typing import InferenceResult
 
 if sys.version_info >= (3, 11):
@@ -18,6 +18,9 @@
 else:
     from typing_extensions import Self
 
+if TYPE_CHECKING:
+    from astroid import bases
+
 _NameNodes = Union[nodes.AssignAttr, nodes.Attribute, nodes.AssignName, nodes.Name]
 
 
diff --git a/astroid/filter_statements.py b/astroid/filter_statements.py
index 7f040dd4ed..acca676170 100644
--- a/astroid/filter_statements.py
+++ b/astroid/filter_statements.py
@@ -10,10 +10,14 @@
 
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 from astroid import nodes
-from astroid.nodes import node_classes
 from astroid.typing import SuccessfulInferenceResult
 
+if TYPE_CHECKING:
+    from astroid.nodes import _base_nodes
+
 
 def _get_filtered_node_statements(
     base_node: nodes.NodeNG, stmt_nodes: list[nodes.NodeNG]
@@ -44,7 +48,7 @@ def _get_if_statement_ancestor(node: nodes.NodeNG) -> nodes.If | None:
 
 
 def _filter_stmts(
-    base_node: node_classes.LookupMixIn,
+    base_node: _base_nodes.LookupMixIn,
     stmts: list[SuccessfulInferenceResult],
     frame: nodes.LocalsDictNodeNG,
     offset: int,
diff --git a/astroid/helpers.py b/astroid/helpers.py
index ab5ada3715..3e62fb99eb 100644
--- a/astroid/helpers.py
+++ b/astroid/helpers.py
@@ -324,3 +324,25 @@ def object_len(node, context: InferenceContext | None = None):
     raise AstroidTypeError(
         f"'{result_of_len}' object cannot be interpreted as an integer"
     )
+
+
+def _higher_function_scope(node: nodes.NodeNG) -> nodes.FunctionDef | None:
+    """Search for the first function which encloses the given
+    scope.
+
+    This can be used for looking up in that function's
+    scope, in case looking up in a lower scope for a particular
+    name fails.
+
+    :param node: A scope node.
+    :returns:
+        ``None``, if no parent function scope was found,
+        otherwise an instance of :class:`astroid.nodes.scoped_nodes.Function`,
+        which encloses the given node.
+    """
+    current = node
+    while current.parent and not isinstance(current.parent, nodes.FunctionDef):
+        current = current.parent
+    if current and current.parent:
+        return current.parent
+    return None
diff --git a/astroid/inference.py b/astroid/inference.py
deleted file mode 100644
index d7caddc575..0000000000
--- a/astroid/inference.py
+++ /dev/null
@@ -1,1293 +0,0 @@
-# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
-# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
-# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
-
-"""This module contains a set of functions to handle inference on astroid trees."""
-
-from __future__ import annotations
-
-import ast
-import functools
-import itertools
-import operator
-import typing
-from collections.abc import Callable, Generator, Iterable, Iterator
-from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
-
-from astroid import (
-    bases,
-    constraint,
-    decorators,
-    helpers,
-    nodes,
-    objects,
-    protocols,
-    util,
-)
-from astroid.const import PY310_PLUS
-from astroid.context import (
-    CallContext,
-    InferenceContext,
-    bind_context_to_node,
-    copy_context,
-)
-from astroid.exceptions import (
-    AstroidBuildingError,
-    AstroidError,
-    AstroidIndexError,
-    AstroidTypeError,
-    AstroidValueError,
-    AttributeInferenceError,
-    InferenceError,
-    NameInferenceError,
-    _NonDeducibleTypeHierarchy,
-)
-from astroid.interpreter import dunder_lookup
-from astroid.manager import AstroidManager
-from astroid.typing import (
-    InferenceErrorInfo,
-    InferenceResult,
-    SuccessfulInferenceResult,
-)
-
-if TYPE_CHECKING:
-    from astroid.objects import Property
-
-
-_T = TypeVar("_T")
-_BaseContainerT = TypeVar("_BaseContainerT", bound=nodes.BaseContainer)
-_FunctionDefT = TypeVar("_FunctionDefT", bound=nodes.FunctionDef)
-
-GetFlowFactory = typing.Callable[
-    [
-        InferenceResult,
-        Optional[InferenceResult],
-        Union[nodes.AugAssign, nodes.BinOp],
-        InferenceResult,
-        Optional[InferenceResult],
-        InferenceContext,
-        InferenceContext,
-    ],
-    "list[functools.partial[Generator[InferenceResult, None, None]]]",
-]
-
-# .infer method ###############################################################
-
-
-def infer_end(
-    self: _T, context: InferenceContext | None = None, **kwargs: Any
-) -> Iterator[_T]:
-    """Inference's end for nodes that yield themselves on inference.
-
-    These are objects for which inference does not have any semantic,
-    such as Module or Consts.
-    """
-    yield self
-
-
-# We add ignores to all assignments to methods
-# See https://github.com/python/mypy/issues/2427
-nodes.Module._infer = infer_end
-nodes.ClassDef._infer = infer_end
-nodes.Lambda._infer = infer_end  # type: ignore[assignment]
-nodes.Const._infer = infer_end  # type: ignore[assignment]
-nodes.Slice._infer = infer_end  # type: ignore[assignment]
-nodes.TypeAlias._infer = infer_end  # type: ignore[assignment]
-nodes.TypeVar._infer = infer_end  # type: ignore[assignment]
-nodes.ParamSpec._infer = infer_end  # type: ignore[assignment]
-nodes.TypeVarTuple._infer = infer_end  # type: ignore[assignment]
-
-
-def _infer_sequence_helper(
-    node: _BaseContainerT, context: InferenceContext | None = None
-) -> list[SuccessfulInferenceResult]:
-    """Infer all values based on _BaseContainer.elts."""
-    values = []
-
-    for elt in node.elts:
-        if isinstance(elt, nodes.Starred):
-            starred = helpers.safe_infer(elt.value, context)
-            if not starred:
-                raise InferenceError(node=node, context=context)
-            if not hasattr(starred, "elts"):
-                raise InferenceError(node=node, context=context)
-            values.extend(_infer_sequence_helper(starred))
-        elif isinstance(elt, nodes.NamedExpr):
-            value = helpers.safe_infer(elt.value, context)
-            if not value:
-                raise InferenceError(node=node, context=context)
-            values.append(value)
-        else:
-            values.append(elt)
-    return values
-
-
-@decorators.raise_if_nothing_inferred
-def infer_sequence(
-    self: _BaseContainerT,
-    context: InferenceContext | None = None,
-    **kwargs: Any,
-) -> Iterator[_BaseContainerT]:
-    has_starred_named_expr = any(
-        isinstance(e, (nodes.Starred, nodes.NamedExpr)) for e in self.elts
-    )
-    if has_starred_named_expr:
-        values = _infer_sequence_helper(self, context)
-        new_seq = type(self)(
-            lineno=self.lineno,
-            col_offset=self.col_offset,
-            parent=self.parent,
-            end_lineno=self.end_lineno,
-            end_col_offset=self.end_col_offset,
-        )
-        new_seq.postinit(values)
-
-        yield new_seq
-    else:
-        yield self
-
-
-nodes.List._infer = infer_sequence  # type: ignore[assignment]
-nodes.Tuple._infer = infer_sequence  # type: ignore[assignment]
-nodes.Set._infer = infer_sequence  # type: ignore[assignment]
-
-
-def infer_map(
-    self: nodes.Dict, context: InferenceContext | None = None
-) -> Iterator[nodes.Dict]:
-    if not any(isinstance(k, nodes.DictUnpack) for k, _ in self.items):
-        yield self
-    else:
-        items = _infer_map(self, context)
-        new_seq = type(self)(
-            self.lineno,
-            self.col_offset,
-            self.parent,
-            end_lineno=self.end_lineno,
-            end_col_offset=self.end_col_offset,
-        )
-        new_seq.postinit(list(items.items()))
-        yield new_seq
-
-
-def _update_with_replacement(
-    lhs_dict: dict[SuccessfulInferenceResult, SuccessfulInferenceResult],
-    rhs_dict: dict[SuccessfulInferenceResult, SuccessfulInferenceResult],
-) -> dict[SuccessfulInferenceResult, SuccessfulInferenceResult]:
-    """Delete nodes that equate to duplicate keys.
-
-    Since an astroid node doesn't 'equal' another node with the same value,
-    this function uses the as_string method to make sure duplicate keys
-    don't get through
-
-    Note that both the key and the value are astroid nodes
-
-    Fixes issue with DictUnpack causing duplicate keys
-    in inferred Dict items
-
-    :param lhs_dict: Dictionary to 'merge' nodes into
-    :param rhs_dict: Dictionary with nodes to pull from
-    :return : merged dictionary of nodes
-    """
-    combined_dict = itertools.chain(lhs_dict.items(), rhs_dict.items())
-    # Overwrite keys which have the same string values
-    string_map = {key.as_string(): (key, value) for key, value in combined_dict}
-    # Return to dictionary
-    return dict(string_map.values())
-
-
-def _infer_map(
-    node: nodes.Dict, context: InferenceContext | None
-) -> dict[SuccessfulInferenceResult, SuccessfulInferenceResult]:
-    """Infer all values based on Dict.items."""
-    values: dict[SuccessfulInferenceResult, SuccessfulInferenceResult] = {}
-    for name, value in node.items:
-        if isinstance(name, nodes.DictUnpack):
-            double_starred = helpers.safe_infer(value, context)
-            if not double_starred:
-                raise InferenceError
-            if not isinstance(double_starred, nodes.Dict):
-                raise InferenceError(node=node, context=context)
-            unpack_items = _infer_map(double_starred, context)
-            values = _update_with_replacement(values, unpack_items)
-        else:
-            key = helpers.safe_infer(name, context=context)
-            safe_value = helpers.safe_infer(value, context=context)
-            if any(not elem for elem in (key, safe_value)):
-                raise InferenceError(node=node, context=context)
-            # safe_value is SuccessfulInferenceResult as bool(Uninferable) == False
-            values = _update_with_replacement(values, {key: safe_value})
-    return values
-
-
-nodes.Dict._infer = infer_map  # type: ignore[assignment]
-
-
-def _higher_function_scope(node: nodes.NodeNG) -> nodes.FunctionDef | None:
-    """Search for the first function which encloses the given
-    scope. This can be used for looking up in that function's
-    scope, in case looking up in a lower scope for a particular
-    name fails.
-
-    :param node: A scope node.
-    :returns:
-        ``None``, if no parent function scope was found,
-        otherwise an instance of :class:`astroid.nodes.scoped_nodes.Function`,
-        which encloses the given node.
-    """
-    current = node
-    while current.parent and not isinstance(current.parent, nodes.FunctionDef):
-        current = current.parent
-    if current and current.parent:
-        return current.parent  # type: ignore[no-any-return]
-    return None
-
-
-def infer_name(
-    self: nodes.Name | nodes.AssignName,
-    context: InferenceContext | None = None,
-    **kwargs: Any,
-) -> Generator[InferenceResult, None, None]:
-    """Infer a Name: use name lookup rules."""
-    frame, stmts = self.lookup(self.name)
-    if not stmts:
-        # Try to see if the name is enclosed in a nested function
-        # and use the higher (first function) scope for searching.
-        parent_function = _higher_function_scope(self.scope())
-        if parent_function:
-            _, stmts = parent_function.lookup(self.name)
-
-        if not stmts:
-            raise NameInferenceError(
-                name=self.name, scope=self.scope(), context=context
-            )
-    context = copy_context(context)
-    context.lookupname = self.name
-    context.constraints[self.name] = constraint.get_constraints(self, frame)
-
-    return bases._infer_stmts(stmts, context, frame)
-
-
-# The order of the decorators here is important
-# See https://github.com/pylint-dev/astroid/commit/0a8a75db30da060a24922e05048bc270230f5
-nodes.Name._infer = decorators.raise_if_nothing_inferred(
-    decorators.path_wrapper(infer_name)
-)
-nodes.AssignName.infer_lhs = infer_name  # won't work with a path wrapper
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_call(
-    self: nodes.Call, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, InferenceErrorInfo]:
-    """Infer a Call node by trying to guess what the function returns."""
-    callcontext = copy_context(context)
-    callcontext.boundnode = None
-    if context is not None:
-        callcontext.extra_context = _populate_context_lookup(self, context.clone())
-
-    for callee in self.func.infer(context):
-        if isinstance(callee, util.UninferableBase):
-            yield callee
-            continue
-        try:
-            if hasattr(callee, "infer_call_result"):
-                callcontext.callcontext = CallContext(
-                    args=self.args, keywords=self.keywords, callee=callee
-                )
-                yield from callee.infer_call_result(caller=self, context=callcontext)
-        except InferenceError:
-            continue
-    return InferenceErrorInfo(node=self, context=context)
-
-
-nodes.Call._infer = infer_call  # type: ignore[assignment]
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_import(
-    self: nodes.Import,
-    context: InferenceContext | None = None,
-    asname: bool = True,
-    **kwargs: Any,
-) -> Generator[nodes.Module, None, None]:
-    """Infer an Import node: return the imported module/object."""
-    context = context or InferenceContext()
-    name = context.lookupname
-    if name is None:
-        raise InferenceError(node=self, context=context)
-
-    try:
-        if asname:
-            yield self.do_import_module(self.real_name(name))
-        else:
-            yield self.do_import_module(name)
-    except AstroidBuildingError as exc:
-        raise InferenceError(node=self, context=context) from exc
-
-
-nodes.Import._infer = infer_import
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_import_from(
-    self: nodes.ImportFrom,
-    context: InferenceContext | None = None,
-    asname: bool = True,
-    **kwargs: Any,
-) -> Generator[InferenceResult, None, None]:
-    """Infer a ImportFrom node: return the imported module/object."""
-    context = context or InferenceContext()
-    name = context.lookupname
-    if name is None:
-        raise InferenceError(node=self, context=context)
-    if asname:
-        try:
-            name = self.real_name(name)
-        except AttributeInferenceError as exc:
-            # See https://github.com/pylint-dev/pylint/issues/4692
-            raise InferenceError(node=self, context=context) from exc
-    try:
-        module = self.do_import_module()
-    except AstroidBuildingError as exc:
-        raise InferenceError(node=self, context=context) from exc
-
-    try:
-        context = copy_context(context)
-        context.lookupname = name
-        stmts = module.getattr(name, ignore_locals=module is self.root())
-        return bases._infer_stmts(stmts, context)
-    except AttributeInferenceError as error:
-        raise InferenceError(
-            str(error), target=self, attribute=name, context=context
-        ) from error
-
-
-nodes.ImportFrom._infer = infer_import_from  # type: ignore[assignment]
-
-
-def infer_attribute(
-    self: nodes.Attribute | nodes.AssignAttr,
-    context: InferenceContext | None = None,
-    **kwargs: Any,
-) -> Generator[InferenceResult, None, InferenceErrorInfo]:
-    """Infer an Attribute node by using getattr on the associated object."""
-    for owner in self.expr.infer(context):
-        if isinstance(owner, util.UninferableBase):
-            yield owner
-            continue
-
-        context = copy_context(context)
-        old_boundnode = context.boundnode
-        try:
-            context.boundnode = owner
-            if isinstance(owner, (nodes.ClassDef, bases.Instance)):
-                frame = owner if isinstance(owner, nodes.ClassDef) else owner._proxied
-                context.constraints[self.attrname] = constraint.get_constraints(
-                    self, frame=frame
-                )
-            yield from owner.igetattr(self.attrname, context)
-        except (
-            AttributeInferenceError,
-            InferenceError,
-            AttributeError,
-        ):
-            pass
-        finally:
-            context.boundnode = old_boundnode
-    return InferenceErrorInfo(node=self, context=context)
-
-
-# The order of the decorators here is important
-# See https://github.com/pylint-dev/astroid/commit/0a8a75db30da060a24922e05048bc270230f5
-nodes.Attribute._infer = decorators.raise_if_nothing_inferred(
-    decorators.path_wrapper(infer_attribute)
-)
-# won't work with a path wrapper
-nodes.AssignAttr.infer_lhs = decorators.raise_if_nothing_inferred(infer_attribute)
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_global(
-    self: nodes.Global, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, None]:
-    if context is None or context.lookupname is None:
-        raise InferenceError(node=self, context=context)
-    try:
-        return bases._infer_stmts(self.root().getattr(context.lookupname), context)
-    except AttributeInferenceError as error:
-        raise InferenceError(
-            str(error), target=self, attribute=context.lookupname, context=context
-        ) from error
-
-
-nodes.Global._infer = infer_global  # type: ignore[assignment]
-
-
-_SUBSCRIPT_SENTINEL = object()
-
-
-def infer_subscript(
-    self: nodes.Subscript, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
-    """Inference for subscripts.
-
-    We're understanding if the index is a Const
-    or a slice, passing the result of inference
-    to the value's `getitem` method, which should
-    handle each supported index type accordingly.
-    """
-
-    found_one = False
-    for value in self.value.infer(context):
-        if isinstance(value, util.UninferableBase):
-            yield util.Uninferable
-            return None
-        for index in self.slice.infer(context):
-            if isinstance(index, util.UninferableBase):
-                yield util.Uninferable
-                return None
-
-            # Try to deduce the index value.
-            index_value = _SUBSCRIPT_SENTINEL
-            if value.__class__ == bases.Instance:
-                index_value = index
-            elif index.__class__ == bases.Instance:
-                instance_as_index = helpers.class_instance_as_index(index)
-                if instance_as_index:
-                    index_value = instance_as_index
-            else:
-                index_value = index
-
-            if index_value is _SUBSCRIPT_SENTINEL:
-                raise InferenceError(node=self, context=context)
-
-            try:
-                assigned = value.getitem(index_value, context)
-            except (
-                AstroidTypeError,
-                AstroidIndexError,
-                AstroidValueError,
-                AttributeInferenceError,
-                AttributeError,
-            ) as exc:
-                raise InferenceError(node=self, context=context) from exc
-
-            # Prevent inferring if the inferred subscript
-            # is the same as the original subscripted object.
-            if self is assigned or isinstance(assigned, util.UninferableBase):
-                yield util.Uninferable
-                return None
-            yield from assigned.infer(context)
-            found_one = True
-
-    if found_one:
-        return InferenceErrorInfo(node=self, context=context)
-    return None
-
-
-# The order of the decorators here is important
-# See https://github.com/pylint-dev/astroid/commit/0a8a75db30da060a24922e05048bc270230f5
-nodes.Subscript._infer = decorators.raise_if_nothing_inferred(  # type: ignore[assignment]
-    decorators.path_wrapper(infer_subscript)
-)
-nodes.Subscript.infer_lhs = decorators.raise_if_nothing_inferred(infer_subscript)
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def _infer_boolop(
-    self: nodes.BoolOp, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
-    """Infer a boolean operation (and / or / not).
-
-    The function will calculate the boolean operation
-    for all pairs generated through inference for each component
-    node.
-    """
-    values = self.values
-    if self.op == "or":
-        predicate = operator.truth
-    else:
-        predicate = operator.not_
-
-    try:
-        inferred_values = [value.infer(context=context) for value in values]
-    except InferenceError:
-        yield util.Uninferable
-        return None
-
-    for pair in itertools.product(*inferred_values):
-        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(isinstance(item, util.UninferableBase) for item in bool_values):
-            # Can't infer the final result, just yield Uninferable.
-            yield util.Uninferable
-            continue
-
-        # Since the boolean operations are short circuited operations,
-        # this code yields the first value for which the predicate is True
-        # and if no value respected the predicate, then the last value will
-        # be returned (or Uninferable if there was no last value).
-        # This is conforming to the semantics of `and` and `or`:
-        #   1 and 0 -> 1
-        #   0 and 1 -> 0
-        #   1 or 0 -> 1
-        #   0 or 1 -> 1
-        value = util.Uninferable
-        for value, bool_value in zip(pair, bool_values):
-            if predicate(bool_value):
-                yield value
-                break
-        else:
-            yield value
-
-    return InferenceErrorInfo(node=self, context=context)
-
-
-nodes.BoolOp._infer = _infer_boolop
-
-
-# UnaryOp, BinOp and AugAssign inferences
-
-
-def _filter_operation_errors(
-    self: _T,
-    infer_callable: Callable[
-        [_T, InferenceContext | None],
-        Generator[InferenceResult | util.BadOperationMessage, None, None],
-    ],
-    context: InferenceContext | None,
-    error: type[util.BadOperationMessage],
-) -> Generator[InferenceResult, None, None]:
-    for result in infer_callable(self, context):
-        if isinstance(result, error):
-            # For the sake of .infer(), we don't care about operation
-            # errors, which is the job of pylint. So return something
-            # which shows that we can't infer the result.
-            yield util.Uninferable
-        else:
-            yield result
-
-
-def _infer_unaryop(
-    self: nodes.UnaryOp, context: InferenceContext | None = None
-) -> Generator[InferenceResult | util.BadUnaryOperationMessage, None, None]:
-    """Infer what an UnaryOp should return when evaluated."""
-    for operand in self.operand.infer(context):
-        try:
-            yield operand.infer_unary_op(self.op)
-        except TypeError as exc:
-            # The operand doesn't support this operation.
-            yield util.BadUnaryOperationMessage(operand, self.op, exc)
-        except AttributeError as exc:
-            meth = protocols.UNARY_OP_METHOD[self.op]
-            if meth is None:
-                # `not node`. Determine node's boolean
-                # value and negate its result, unless it is
-                # Uninferable, which will be returned as is.
-                bool_value = operand.bool_value()
-                if not isinstance(bool_value, util.UninferableBase):
-                    yield nodes.const_factory(not bool_value)
-                else:
-                    yield util.Uninferable
-            else:
-                if not isinstance(operand, (bases.Instance, nodes.ClassDef)):
-                    # The operation was used on something which
-                    # doesn't support it.
-                    yield util.BadUnaryOperationMessage(operand, self.op, exc)
-                    continue
-
-                try:
-                    try:
-                        methods = dunder_lookup.lookup(operand, meth)
-                    except AttributeInferenceError:
-                        yield util.BadUnaryOperationMessage(operand, self.op, exc)
-                        continue
-
-                    meth = methods[0]
-                    inferred = next(meth.infer(context=context), None)
-                    if (
-                        isinstance(inferred, util.UninferableBase)
-                        or not inferred.callable()
-                    ):
-                        continue
-
-                    context = copy_context(context)
-                    context.boundnode = operand
-                    context.callcontext = CallContext(args=[], callee=inferred)
-
-                    call_results = inferred.infer_call_result(self, context=context)
-                    result = next(call_results, None)
-                    if result is None:
-                        # Failed to infer, return the same type.
-                        yield operand
-                    else:
-                        yield result
-                except AttributeInferenceError as inner_exc:
-                    # The unary operation special method was not found.
-                    yield util.BadUnaryOperationMessage(operand, self.op, inner_exc)
-                except InferenceError:
-                    yield util.Uninferable
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_unaryop(
-    self: nodes.UnaryOp, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, InferenceErrorInfo]:
-    """Infer what an UnaryOp should return when evaluated."""
-    yield from _filter_operation_errors(
-        self, _infer_unaryop, context, util.BadUnaryOperationMessage
-    )
-    return InferenceErrorInfo(node=self, context=context)
-
-
-nodes.UnaryOp._infer_unaryop = _infer_unaryop
-nodes.UnaryOp._infer = infer_unaryop
-
-
-def _is_not_implemented(const) -> bool:
-    """Check if the given const node is NotImplemented."""
-    return isinstance(const, nodes.Const) and const.value is NotImplemented
-
-
-def _infer_old_style_string_formatting(
-    instance: nodes.Const, other: nodes.NodeNG, context: InferenceContext
-) -> tuple[util.UninferableBase | nodes.Const]:
-    """Infer the result of '"string" % ...'.
-
-    TODO: Instead of returning Uninferable we should rely
-    on the call to '%' to see if the result is actually uninferable.
-    """
-    if isinstance(other, nodes.Tuple):
-        if util.Uninferable in other.elts:
-            return (util.Uninferable,)
-        inferred_positional = [helpers.safe_infer(i, context) for i in other.elts]
-        if all(isinstance(i, nodes.Const) for i in inferred_positional):
-            values = tuple(i.value for i in inferred_positional)
-        else:
-            values = None
-    elif isinstance(other, nodes.Dict):
-        values: dict[Any, Any] = {}
-        for pair in other.items:
-            key = helpers.safe_infer(pair[0], context)
-            if not isinstance(key, nodes.Const):
-                return (util.Uninferable,)
-            value = helpers.safe_infer(pair[1], context)
-            if not isinstance(value, nodes.Const):
-                return (util.Uninferable,)
-            values[key.value] = value.value
-    elif isinstance(other, nodes.Const):
-        values = other.value
-    else:
-        return (util.Uninferable,)
-
-    try:
-        return (nodes.const_factory(instance.value % values),)
-    except (TypeError, KeyError, ValueError):
-        return (util.Uninferable,)
-
-
-def _invoke_binop_inference(
-    instance: InferenceResult,
-    opnode: nodes.AugAssign | nodes.BinOp,
-    op: str,
-    other: InferenceResult,
-    context: InferenceContext,
-    method_name: str,
-) -> Generator[InferenceResult, None, None]:
-    """Invoke binary operation inference on the given instance."""
-    methods = dunder_lookup.lookup(instance, method_name)
-    context = bind_context_to_node(context, instance)
-    method = methods[0]
-    context.callcontext.callee = method
-
-    if (
-        isinstance(instance, nodes.Const)
-        and isinstance(instance.value, str)
-        and op == "%"
-    ):
-        return iter(_infer_old_style_string_formatting(instance, other, context))
-
-    try:
-        inferred = next(method.infer(context=context))
-    except StopIteration as e:
-        raise InferenceError(node=method, context=context) from e
-    if isinstance(inferred, util.UninferableBase):
-        raise InferenceError
-    if not isinstance(
-        instance, (nodes.Const, nodes.Tuple, nodes.List, nodes.ClassDef, bases.Instance)
-    ):
-        raise InferenceError  # pragma: no cover # Used as a failsafe
-    return instance.infer_binary_op(opnode, op, other, context, inferred)
-
-
-def _aug_op(
-    instance: InferenceResult,
-    opnode: nodes.AugAssign,
-    op: str,
-    other: InferenceResult,
-    context: InferenceContext,
-    reverse: bool = False,
-) -> functools.partial[Generator[InferenceResult, None, None]]:
-    """Get an inference callable for an augmented binary operation."""
-    method_name = protocols.AUGMENTED_OP_METHOD[op]
-    return functools.partial(
-        _invoke_binop_inference,
-        instance=instance,
-        op=op,
-        opnode=opnode,
-        other=other,
-        context=context,
-        method_name=method_name,
-    )
-
-
-def _bin_op(
-    instance: InferenceResult,
-    opnode: nodes.AugAssign | nodes.BinOp,
-    op: str,
-    other: InferenceResult,
-    context: InferenceContext,
-    reverse: bool = False,
-) -> functools.partial[Generator[InferenceResult, None, None]]:
-    """Get an inference callable for a normal binary operation.
-
-    If *reverse* is True, then the reflected method will be used instead.
-    """
-    if reverse:
-        method_name = protocols.REFLECTED_BIN_OP_METHOD[op]
-    else:
-        method_name = protocols.BIN_OP_METHOD[op]
-    return functools.partial(
-        _invoke_binop_inference,
-        instance=instance,
-        op=op,
-        opnode=opnode,
-        other=other,
-        context=context,
-        method_name=method_name,
-    )
-
-
-def _bin_op_or_union_type(
-    left: bases.UnionType | nodes.ClassDef | nodes.Const,
-    right: bases.UnionType | nodes.ClassDef | nodes.Const,
-) -> Generator[InferenceResult, None, None]:
-    """Create a new UnionType instance for binary or, e.g. int | str."""
-    yield bases.UnionType(left, right)
-
-
-def _get_binop_contexts(context, left, right):
-    """Get contexts for binary operations.
-
-    This will return two inference contexts, the first one
-    for x.__op__(y), the other one for y.__rop__(x), where
-    only the arguments are inversed.
-    """
-    # The order is important, since the first one should be
-    # left.__op__(right).
-    for arg in (right, left):
-        new_context = context.clone()
-        new_context.callcontext = CallContext(args=[arg])
-        new_context.boundnode = None
-        yield new_context
-
-
-def _same_type(type1, type2) -> bool:
-    """Check if type1 is the same as type2."""
-    return type1.qname() == type2.qname()
-
-
-def _get_binop_flow(
-    left: InferenceResult,
-    left_type: InferenceResult | None,
-    binary_opnode: nodes.AugAssign | nodes.BinOp,
-    right: InferenceResult,
-    right_type: InferenceResult | None,
-    context: InferenceContext,
-    reverse_context: InferenceContext,
-) -> list[functools.partial[Generator[InferenceResult, None, None]]]:
-    """Get the flow for binary operations.
-
-    The rules are a bit messy:
-
-        * if left and right have the same type, then only one
-          method will be called, left.__op__(right)
-        * if left and right are unrelated typewise, then first
-          left.__op__(right) is tried and if this does not exist
-          or returns NotImplemented, then right.__rop__(left) is tried.
-        * if left is a subtype of right, then only left.__op__(right)
-          is tried.
-        * if left is a supertype of right, then right.__rop__(left)
-          is first tried and then left.__op__(right)
-    """
-    op = binary_opnode.op
-    if _same_type(left_type, right_type):
-        methods = [_bin_op(left, binary_opnode, op, right, context)]
-    elif helpers.is_subtype(left_type, right_type):
-        methods = [_bin_op(left, binary_opnode, op, right, context)]
-    elif helpers.is_supertype(left_type, right_type):
-        methods = [
-            _bin_op(right, binary_opnode, op, left, reverse_context, reverse=True),
-            _bin_op(left, binary_opnode, op, right, context),
-        ]
-    else:
-        methods = [
-            _bin_op(left, binary_opnode, op, right, context),
-            _bin_op(right, binary_opnode, op, left, reverse_context, reverse=True),
-        ]
-
-    if (
-        PY310_PLUS
-        and op == "|"
-        and (
-            isinstance(left, (bases.UnionType, nodes.ClassDef))
-            or isinstance(left, nodes.Const)
-            and left.value is None
-        )
-        and (
-            isinstance(right, (bases.UnionType, nodes.ClassDef))
-            or isinstance(right, nodes.Const)
-            and right.value is None
-        )
-    ):
-        methods.extend([functools.partial(_bin_op_or_union_type, left, right)])
-    return methods
-
-
-def _get_aug_flow(
-    left: InferenceResult,
-    left_type: InferenceResult | None,
-    aug_opnode: nodes.AugAssign,
-    right: InferenceResult,
-    right_type: InferenceResult | None,
-    context: InferenceContext,
-    reverse_context: InferenceContext,
-) -> list[functools.partial[Generator[InferenceResult, None, None]]]:
-    """Get the flow for augmented binary operations.
-
-    The rules are a bit messy:
-
-        * if left and right have the same type, then left.__augop__(right)
-          is first tried and then left.__op__(right).
-        * if left and right are unrelated typewise, then
-          left.__augop__(right) is tried, then left.__op__(right)
-          is tried and then right.__rop__(left) is tried.
-        * if left is a subtype of right, then left.__augop__(right)
-          is tried and then left.__op__(right).
-        * if left is a supertype of right, then left.__augop__(right)
-          is tried, then right.__rop__(left) and then
-          left.__op__(right)
-    """
-    bin_op = aug_opnode.op.strip("=")
-    aug_op = aug_opnode.op
-    if _same_type(left_type, right_type):
-        methods = [
-            _aug_op(left, aug_opnode, aug_op, right, context),
-            _bin_op(left, aug_opnode, bin_op, right, context),
-        ]
-    elif helpers.is_subtype(left_type, right_type):
-        methods = [
-            _aug_op(left, aug_opnode, aug_op, right, context),
-            _bin_op(left, aug_opnode, bin_op, right, context),
-        ]
-    elif helpers.is_supertype(left_type, right_type):
-        methods = [
-            _aug_op(left, aug_opnode, aug_op, right, context),
-            _bin_op(right, aug_opnode, bin_op, left, reverse_context, reverse=True),
-            _bin_op(left, aug_opnode, bin_op, right, context),
-        ]
-    else:
-        methods = [
-            _aug_op(left, aug_opnode, aug_op, right, context),
-            _bin_op(left, aug_opnode, bin_op, right, context),
-            _bin_op(right, aug_opnode, bin_op, left, reverse_context, reverse=True),
-        ]
-    return methods
-
-
-def _infer_binary_operation(
-    left: InferenceResult,
-    right: InferenceResult,
-    binary_opnode: nodes.AugAssign | nodes.BinOp,
-    context: InferenceContext,
-    flow_factory: GetFlowFactory,
-) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
-    """Infer a binary operation between a left operand and a right operand.
-
-    This is used by both normal binary operations and augmented binary
-    operations, the only difference is the flow factory used.
-    """
-
-    context, reverse_context = _get_binop_contexts(context, left, right)
-    left_type = helpers.object_type(left)
-    right_type = helpers.object_type(right)
-    methods = flow_factory(
-        left, left_type, binary_opnode, right, right_type, context, reverse_context
-    )
-    for method in methods:
-        try:
-            results = list(method())
-        except AttributeError:
-            continue
-        except AttributeInferenceError:
-            continue
-        except InferenceError:
-            yield util.Uninferable
-            return
-        else:
-            if any(isinstance(result, util.UninferableBase) for result in results):
-                yield util.Uninferable
-                return
-
-            if all(map(_is_not_implemented, results)):
-                continue
-            not_implemented = sum(
-                1 for result in results if _is_not_implemented(result)
-            )
-            if not_implemented and not_implemented != len(results):
-                # Can't infer yet what this is.
-                yield util.Uninferable
-                return
-
-            yield from results
-            return
-    # The operation doesn't seem to be supported so let the caller know about it
-    yield util.BadBinaryOperationMessage(left_type, binary_opnode.op, right_type)
-
-
-def _infer_binop(
-    self: nodes.BinOp, context: InferenceContext | None = None
-) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
-    """Binary operation inference logic."""
-    left = self.left
-    right = self.right
-
-    # we use two separate contexts for evaluating lhs and rhs because
-    # 1. evaluating lhs may leave some undesired entries in context.path
-    #    which may not let us infer right value of rhs
-    context = context or InferenceContext()
-    lhs_context = copy_context(context)
-    rhs_context = copy_context(context)
-    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(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
-            # Don't know how to process this.
-            yield util.Uninferable
-            return
-
-        try:
-            yield from _infer_binary_operation(lhs, rhs, self, context, _get_binop_flow)
-        except _NonDeducibleTypeHierarchy:
-            yield util.Uninferable
-
-
-@decorators.yes_if_nothing_inferred
-@decorators.path_wrapper
-def infer_binop(
-    self: nodes.BinOp, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, None]:
-    return _filter_operation_errors(
-        self, _infer_binop, context, util.BadBinaryOperationMessage
-    )
-
-
-nodes.BinOp._infer_binop = _infer_binop
-nodes.BinOp._infer = infer_binop
-
-COMPARE_OPS: dict[str, Callable[[Any, Any], bool]] = {
-    "==": operator.eq,
-    "!=": operator.ne,
-    "<": operator.lt,
-    "<=": operator.le,
-    ">": operator.gt,
-    ">=": operator.ge,
-    "in": lambda a, b: a in b,
-    "not in": lambda a, b: a not in b,
-}
-UNINFERABLE_OPS = {
-    "is",
-    "is not",
-}
-
-
-def _to_literal(node: SuccessfulInferenceResult) -> Any:
-    # Can raise SyntaxError or ValueError from ast.literal_eval
-    # Can raise AttributeError from node.as_string() as not all nodes have a visitor
-    # Is this the stupidest idea or the simplest idea?
-    return ast.literal_eval(node.as_string())
-
-
-def _do_compare(
-    left_iter: Iterable[InferenceResult], op: str, right_iter: Iterable[InferenceResult]
-) -> bool | util.UninferableBase:
-    """
-    If all possible combinations are either True or False, return that:
-    >>> _do_compare([1, 2], '<=', [3, 4])
-    True
-    >>> _do_compare([1, 2], '==', [3, 4])
-    False
-
-    If any item is uninferable, or if some combinations are True and some
-    are False, return Uninferable:
-    >>> _do_compare([1, 3], '<=', [2, 4])
-    util.Uninferable
-    """
-    retval: bool | None = None
-    if op in UNINFERABLE_OPS:
-        return util.Uninferable
-    op_func = COMPARE_OPS[op]
-
-    for left, right in itertools.product(left_iter, right_iter):
-        if isinstance(left, util.UninferableBase) or isinstance(
-            right, util.UninferableBase
-        ):
-            return util.Uninferable
-
-        try:
-            left, right = _to_literal(left), _to_literal(right)
-        except (SyntaxError, ValueError, AttributeError):
-            return util.Uninferable
-
-        try:
-            expr = op_func(left, right)
-        except TypeError as exc:
-            raise AstroidTypeError from exc
-
-        if retval is None:
-            retval = expr
-        elif retval != expr:
-            return util.Uninferable
-            # (or both, but "True | False" is basically the same)
-
-    assert retval is not None
-    return retval  # it was all the same value
-
-
-def _infer_compare(
-    self: nodes.Compare, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[nodes.Const | util.UninferableBase, None, None]:
-    """Chained comparison inference logic."""
-    retval: bool | util.UninferableBase = True
-
-    ops = self.ops
-    left_node = self.left
-    lhs = list(left_node.infer(context=context))
-    # should we break early if first element is uninferable?
-    for op, right_node in ops:
-        # eagerly evaluate rhs so that values can be re-used as lhs
-        rhs = list(right_node.infer(context=context))
-        try:
-            retval = _do_compare(lhs, op, rhs)
-        except AstroidTypeError:
-            retval = util.Uninferable
-            break
-        if retval is not True:
-            break  # short-circuit
-        lhs = rhs  # continue
-    if retval is util.Uninferable:
-        yield retval  # type: ignore[misc]
-    else:
-        yield nodes.Const(retval)
-
-
-nodes.Compare._infer = _infer_compare  # type: ignore[assignment]
-
-
-def _infer_augassign(
-    self: nodes.AugAssign, context: InferenceContext | None = None
-) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
-    """Inference logic for augmented binary operations."""
-    context = context or InferenceContext()
-
-    rhs_context = context.clone()
-
-    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(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
-            # Don't know how to process this.
-            yield util.Uninferable
-            return
-
-        try:
-            yield from _infer_binary_operation(
-                left=lhs,
-                right=rhs,
-                binary_opnode=self,
-                context=context,
-                flow_factory=_get_aug_flow,
-            )
-        except _NonDeducibleTypeHierarchy:
-            yield util.Uninferable
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_augassign(
-    self: nodes.AugAssign, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, None]:
-    return _filter_operation_errors(
-        self, _infer_augassign, context, util.BadBinaryOperationMessage
-    )
-
-
-nodes.AugAssign._infer_augassign = _infer_augassign
-nodes.AugAssign._infer = infer_augassign
-
-# End of binary operation inference.
-
-
-@decorators.raise_if_nothing_inferred
-def infer_arguments(
-    self: nodes.Arguments, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, None]:
-    if context is None or context.lookupname is None:
-        raise InferenceError(node=self, context=context)
-    return protocols._arguments_infer_argname(self, context.lookupname, context)
-
-
-nodes.Arguments._infer = infer_arguments  # type: ignore[assignment]
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_assign(
-    self: nodes.AssignName | nodes.AssignAttr,
-    context: InferenceContext | None = None,
-    **kwargs: Any,
-) -> Generator[InferenceResult, None, None]:
-    """Infer a AssignName/AssignAttr: need to inspect the RHS part of the
-    assign node.
-    """
-    if isinstance(self.parent, nodes.AugAssign):
-        return self.parent.infer(context)
-
-    stmts = list(self.assigned_stmts(context=context))
-    return bases._infer_stmts(stmts, context)
-
-
-nodes.AssignName._infer = infer_assign
-nodes.AssignAttr._infer = infer_assign
-
-
-@decorators.raise_if_nothing_inferred
-@decorators.path_wrapper
-def infer_empty_node(
-    self: nodes.EmptyNode, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, None]:
-    if not self.has_underlying_object():
-        yield util.Uninferable
-    else:
-        try:
-            yield from AstroidManager().infer_ast_from_something(
-                self.object, context=context
-            )
-        except AstroidError:
-            yield util.Uninferable
-
-
-nodes.EmptyNode._infer = infer_empty_node  # type: ignore[assignment]
-
-
-def _populate_context_lookup(call: nodes.Call, context: InferenceContext | None):
-    # Allows context to be saved for later
-    # for inference inside a function
-    context_lookup: dict[InferenceResult, InferenceContext] = {}
-    if context is None:
-        return context_lookup
-    for arg in call.args:
-        if isinstance(arg, nodes.Starred):
-            context_lookup[arg.value] = context
-        else:
-            context_lookup[arg] = context
-    keywords = call.keywords if call.keywords is not None else []
-    for keyword in keywords:
-        context_lookup[keyword.value] = context
-    return context_lookup
-
-
-@decorators.raise_if_nothing_inferred
-def infer_ifexp(
-    self: nodes.IfExp, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[InferenceResult, None, None]:
-    """Support IfExp inference.
-
-    If we can't infer the truthiness of the condition, we default
-    to inferring both branches. Otherwise, we infer either branch
-    depending on the condition.
-    """
-    both_branches = False
-    # We use two separate contexts for evaluating lhs and rhs because
-    # evaluating lhs may leave some undesired entries in context.path
-    # which may not let us infer right value of rhs.
-
-    context = context or InferenceContext()
-    lhs_context = copy_context(context)
-    rhs_context = copy_context(context)
-    try:
-        test = next(self.test.infer(context=context.clone()))
-    except (InferenceError, StopIteration):
-        both_branches = True
-    else:
-        if not isinstance(test, util.UninferableBase):
-            if test.bool_value():
-                yield from self.body.infer(context=lhs_context)
-            else:
-                yield from self.orelse.infer(context=rhs_context)
-        else:
-            both_branches = True
-    if both_branches:
-        yield from self.body.infer(context=lhs_context)
-        yield from self.orelse.infer(context=rhs_context)
-
-
-nodes.IfExp._infer = infer_ifexp  # type: ignore[assignment]
-
-
-def infer_functiondef(
-    self: _FunctionDefT, context: InferenceContext | None = None, **kwargs: Any
-) -> Generator[Property | _FunctionDefT, None, InferenceErrorInfo]:
-    if not self.decorators or not bases._is_property(self):
-        yield self
-        return InferenceErrorInfo(node=self, context=context)
-
-    # When inferring a property, we instantiate a new `objects.Property` object,
-    # which in turn, because it inherits from `FunctionDef`, sets itself in the locals
-    # of the wrapping frame. This means that every time we infer a property, the locals
-    # are mutated with a new instance of the property. To avoid this, we detect this
-    # scenario and avoid passing the `parent` argument to the constructor.
-    parent_frame = self.parent.frame()
-    property_already_in_parent_locals = self.name in parent_frame.locals and any(
-        isinstance(val, objects.Property) for val in parent_frame.locals[self.name]
-    )
-    # We also don't want to pass parent if the definition is within a Try node
-    if isinstance(self.parent, (nodes.TryExcept, nodes.TryFinally, nodes.If)):
-        property_already_in_parent_locals = True
-
-    prop_func = objects.Property(
-        function=self,
-        name=self.name,
-        lineno=self.lineno,
-        parent=self.parent if not property_already_in_parent_locals else None,
-        col_offset=self.col_offset,
-    )
-    if property_already_in_parent_locals:
-        prop_func.parent = self.parent
-    prop_func.postinit(body=[], args=self.args, doc_node=self.doc_node)
-    yield prop_func
-    return InferenceErrorInfo(node=self, context=context)
-
-
-nodes.FunctionDef._infer = infer_functiondef
diff --git a/astroid/manager.py b/astroid/manager.py
index 2df270f1ac..2d0903fe53 100644
--- a/astroid/manager.py
+++ b/astroid/manager.py
@@ -434,7 +434,7 @@ def clear_cache(self) -> None:
         # pylint: disable=import-outside-toplevel
         from astroid.inference_tip import clear_inference_tip_cache
         from astroid.interpreter.objectmodel import ObjectModel
-        from astroid.nodes.node_classes import LookupMixIn
+        from astroid.nodes._base_nodes import LookupMixIn
         from astroid.nodes.scoped_nodes import ClassDef
 
         clear_inference_tip_cache()
diff --git a/astroid/node_classes.py b/astroid/node_classes.py
index 980fa0a90b..7f3614e46b 100644
--- a/astroid/node_classes.py
+++ b/astroid/node_classes.py
@@ -48,7 +48,6 @@
     JoinedStr,
     Keyword,
     List,
-    LookupMixIn,
     Match,
     MatchAs,
     MatchCase,
diff --git a/astroid/nodes/__init__.py b/astroid/nodes/__init__.py
index 84fcb521f2..44712f1074 100644
--- a/astroid/nodes/__init__.py
+++ b/astroid/nodes/__init__.py
@@ -11,10 +11,6 @@
 """
 
 # Nodes not present in the builtin ast module:  DictUnpack, Unknown, and EvaluatedObject.
-
-# This is the only node we re-export from the private _base_nodes module. This
-# is because it was originally part of the public API and hasn't been deprecated.
-from astroid.nodes._base_nodes import Statement
 from astroid.nodes.node_classes import (
     CONST_CLS,
     AnnAssign,
@@ -115,10 +111,7 @@
 )
 from astroid.nodes.utils import Position
 
-_BaseContainer = BaseContainer  # TODO Remove for astroid 3.0
-
 ALL_NODE_CLASSES = (
-    _BaseContainer,
     BaseContainer,
     AnnAssign,
     Arguments,
@@ -223,6 +216,7 @@
     "Attribute",
     "AugAssign",
     "Await",
+    "BaseContainer",
     "BinOp",
     "BoolOp",
     "Break",
@@ -288,7 +282,6 @@
     "SetComp",
     "Slice",
     "Starred",
-    "Statement",
     "Subscript",
     "TryExcept",
     "TryFinally",
diff --git a/astroid/nodes/_base_nodes.py b/astroid/nodes/_base_nodes.py
index 15cc6a9ad1..d6d80986a9 100644
--- a/astroid/nodes/_base_nodes.py
+++ b/astroid/nodes/_base_nodes.py
@@ -10,15 +10,42 @@
 from __future__ import annotations
 
 import itertools
-from collections.abc import Iterator
-from functools import cached_property
-from typing import TYPE_CHECKING, ClassVar
-
-from astroid.exceptions import AttributeInferenceError
+from collections.abc import Generator, Iterator
+from functools import cached_property, lru_cache, partial
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union
+
+from astroid import bases, decorators, nodes, util
+from astroid.const import PY310_PLUS
+from astroid.context import (
+    CallContext,
+    InferenceContext,
+    bind_context_to_node,
+    copy_context,
+)
+from astroid.exceptions import (
+    AttributeInferenceError,
+    InferenceError,
+    NameInferenceError,
+)
+from astroid.interpreter import dunder_lookup
 from astroid.nodes.node_ng import NodeNG
+from astroid.typing import InferenceErrorInfo, InferenceResult
 
 if TYPE_CHECKING:
-    from astroid import nodes
+    from astroid.nodes.node_classes import AssignedStmtsPossibleNode, LocalsDictNodeNG
+
+    GetFlowFactory = Callable[
+        [
+            InferenceResult,
+            Optional[InferenceResult],
+            Union[nodes.AugAssign, nodes.BinOp],
+            InferenceResult,
+            Optional[InferenceResult],
+            InferenceContext,
+            InferenceContext,
+        ],
+        list[partial[Generator[InferenceResult, None, None]]],
+    ]
 
 
 class Statement(NodeNG):
@@ -223,3 +250,526 @@ def _elsed_block_range(
                 return lineno, orelse[-1].tolineno
             return lineno, orelse[0].fromlineno - 1
         return lineno, last or self.tolineno
+
+
+class LookupMixIn(NodeNG):
+    """Mixin to look up a name in the right scope."""
+
+    @lru_cache  # noqa
+    def lookup(self, name: str) -> tuple[LocalsDictNodeNG, list[NodeNG]]:
+        """Lookup where the given variable is assigned.
+
+        The lookup starts from self's scope. If self is not a frame itself
+        and the name is found in the inner frame locals, statements will be
+        filtered to remove ignorable statements according to self's location.
+
+        :param name: The name of the variable to find assignments for.
+
+        :returns: The scope node and the list of assignments associated to the
+            given name according to the scope where it has been found (locals,
+            globals or builtin).
+        """
+        return self.scope().scope_lookup(self, name)
+
+    def ilookup(self, name):
+        """Lookup the inferred values of the given variable.
+
+        :param name: The variable name to find values for.
+        :type name: str
+
+        :returns: The inferred values of the statements returned from
+            :meth:`lookup`.
+        :rtype: iterable
+        """
+        frame, stmts = self.lookup(name)
+        context = InferenceContext()
+        return bases._infer_stmts(stmts, context, frame)
+
+
+def _reflected_name(name) -> str:
+    return "__r" + name[2:]
+
+
+def _augmented_name(name) -> str:
+    return "__i" + name[2:]
+
+
+BIN_OP_METHOD = {
+    "+": "__add__",
+    "-": "__sub__",
+    "/": "__truediv__",
+    "//": "__floordiv__",
+    "*": "__mul__",
+    "**": "__pow__",
+    "%": "__mod__",
+    "&": "__and__",
+    "|": "__or__",
+    "^": "__xor__",
+    "<<": "__lshift__",
+    ">>": "__rshift__",
+    "@": "__matmul__",
+}
+
+REFLECTED_BIN_OP_METHOD = {
+    key: _reflected_name(value) for (key, value) in BIN_OP_METHOD.items()
+}
+AUGMENTED_OP_METHOD = {
+    key + "=": _augmented_name(value) for (key, value) in BIN_OP_METHOD.items()
+}
+
+
+class OperatorNode(NodeNG):
+    @staticmethod
+    def _filter_operation_errors(
+        infer_callable: Callable[
+            [InferenceContext | None],
+            Generator[InferenceResult | util.BadOperationMessage, None, None],
+        ],
+        context: InferenceContext | None,
+        error: type[util.BadOperationMessage],
+    ) -> Generator[InferenceResult, None, None]:
+        for result in infer_callable(context):
+            if isinstance(result, error):
+                # For the sake of .infer(), we don't care about operation
+                # errors, which is the job of pylint. So return something
+                # which shows that we can't infer the result.
+                yield util.Uninferable
+            else:
+                yield result
+
+    @staticmethod
+    def _is_not_implemented(const) -> bool:
+        """Check if the given const node is NotImplemented."""
+        return isinstance(const, nodes.Const) and const.value is NotImplemented
+
+    @staticmethod
+    def _infer_old_style_string_formatting(
+        instance: nodes.Const, other: nodes.NodeNG, context: InferenceContext
+    ) -> tuple[util.UninferableBase | nodes.Const]:
+        """Infer the result of '"string" % ...'.
+
+        TODO: Instead of returning Uninferable we should rely
+        on the call to '%' to see if the result is actually uninferable.
+        """
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        if isinstance(other, nodes.Tuple):
+            if util.Uninferable in other.elts:
+                return (util.Uninferable,)
+            inferred_positional = [helpers.safe_infer(i, context) for i in other.elts]
+            if all(isinstance(i, nodes.Const) for i in inferred_positional):
+                values = tuple(i.value for i in inferred_positional)
+            else:
+                values = None
+        elif isinstance(other, nodes.Dict):
+            values: dict[Any, Any] = {}
+            for pair in other.items:
+                key = helpers.safe_infer(pair[0], context)
+                if not isinstance(key, nodes.Const):
+                    return (util.Uninferable,)
+                value = helpers.safe_infer(pair[1], context)
+                if not isinstance(value, nodes.Const):
+                    return (util.Uninferable,)
+                values[key.value] = value.value
+        elif isinstance(other, nodes.Const):
+            values = other.value
+        else:
+            return (util.Uninferable,)
+
+        try:
+            return (nodes.const_factory(instance.value % values),)
+        except (TypeError, KeyError, ValueError):
+            return (util.Uninferable,)
+
+    @staticmethod
+    def _invoke_binop_inference(
+        instance: InferenceResult,
+        opnode: nodes.AugAssign | nodes.BinOp,
+        op: str,
+        other: InferenceResult,
+        context: InferenceContext,
+        method_name: str,
+    ) -> Generator[InferenceResult, None, None]:
+        """Invoke binary operation inference on the given instance."""
+        methods = dunder_lookup.lookup(instance, method_name)
+        context = bind_context_to_node(context, instance)
+        method = methods[0]
+        context.callcontext.callee = method
+
+        if (
+            isinstance(instance, nodes.Const)
+            and isinstance(instance.value, str)
+            and op == "%"
+        ):
+            return iter(
+                OperatorNode._infer_old_style_string_formatting(
+                    instance, other, context
+                )
+            )
+
+        try:
+            inferred = next(method.infer(context=context))
+        except StopIteration as e:
+            raise InferenceError(node=method, context=context) from e
+        if isinstance(inferred, util.UninferableBase):
+            raise InferenceError
+        if not isinstance(
+            instance,
+            (nodes.Const, nodes.Tuple, nodes.List, nodes.ClassDef, bases.Instance),
+        ):
+            raise InferenceError  # pragma: no cover # Used as a failsafe
+        return instance.infer_binary_op(opnode, op, other, context, inferred)
+
+    @staticmethod
+    def _aug_op(
+        instance: InferenceResult,
+        opnode: nodes.AugAssign,
+        op: str,
+        other: InferenceResult,
+        context: InferenceContext,
+        reverse: bool = False,
+    ) -> partial[Generator[InferenceResult, None, None]]:
+        """Get an inference callable for an augmented binary operation."""
+        method_name = AUGMENTED_OP_METHOD[op]
+        return partial(
+            OperatorNode._invoke_binop_inference,
+            instance=instance,
+            op=op,
+            opnode=opnode,
+            other=other,
+            context=context,
+            method_name=method_name,
+        )
+
+    @staticmethod
+    def _bin_op(
+        instance: InferenceResult,
+        opnode: nodes.AugAssign | nodes.BinOp,
+        op: str,
+        other: InferenceResult,
+        context: InferenceContext,
+        reverse: bool = False,
+    ) -> partial[Generator[InferenceResult, None, None]]:
+        """Get an inference callable for a normal binary operation.
+
+        If *reverse* is True, then the reflected method will be used instead.
+        """
+        if reverse:
+            method_name = REFLECTED_BIN_OP_METHOD[op]
+        else:
+            method_name = BIN_OP_METHOD[op]
+        return partial(
+            OperatorNode._invoke_binop_inference,
+            instance=instance,
+            op=op,
+            opnode=opnode,
+            other=other,
+            context=context,
+            method_name=method_name,
+        )
+
+    @staticmethod
+    def _bin_op_or_union_type(
+        left: bases.UnionType | nodes.ClassDef | nodes.Const,
+        right: bases.UnionType | nodes.ClassDef | nodes.Const,
+    ) -> Generator[InferenceResult, None, None]:
+        """Create a new UnionType instance for binary or, e.g. int | str."""
+        yield bases.UnionType(left, right)
+
+    @staticmethod
+    def _get_binop_contexts(context, left, right):
+        """Get contexts for binary operations.
+
+        This will return two inference contexts, the first one
+        for x.__op__(y), the other one for y.__rop__(x), where
+        only the arguments are inversed.
+        """
+        # The order is important, since the first one should be
+        # left.__op__(right).
+        for arg in (right, left):
+            new_context = context.clone()
+            new_context.callcontext = CallContext(args=[arg])
+            new_context.boundnode = None
+            yield new_context
+
+    @staticmethod
+    def _same_type(type1, type2) -> bool:
+        """Check if type1 is the same as type2."""
+        return type1.qname() == type2.qname()
+
+    @staticmethod
+    def _get_aug_flow(
+        left: InferenceResult,
+        left_type: InferenceResult | None,
+        aug_opnode: nodes.AugAssign,
+        right: InferenceResult,
+        right_type: InferenceResult | None,
+        context: InferenceContext,
+        reverse_context: InferenceContext,
+    ) -> list[partial[Generator[InferenceResult, None, None]]]:
+        """Get the flow for augmented binary operations.
+
+        The rules are a bit messy:
+
+            * if left and right have the same type, then left.__augop__(right)
+            is first tried and then left.__op__(right).
+            * if left and right are unrelated typewise, then
+            left.__augop__(right) is tried, then left.__op__(right)
+            is tried and then right.__rop__(left) is tried.
+            * if left is a subtype of right, then left.__augop__(right)
+            is tried and then left.__op__(right).
+            * if left is a supertype of right, then left.__augop__(right)
+            is tried, then right.__rop__(left) and then
+            left.__op__(right)
+        """
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        bin_op = aug_opnode.op.strip("=")
+        aug_op = aug_opnode.op
+        if OperatorNode._same_type(left_type, right_type):
+            methods = [
+                OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
+                OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
+            ]
+        elif helpers.is_subtype(left_type, right_type):
+            methods = [
+                OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
+                OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
+            ]
+        elif helpers.is_supertype(left_type, right_type):
+            methods = [
+                OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
+                OperatorNode._bin_op(
+                    right, aug_opnode, bin_op, left, reverse_context, reverse=True
+                ),
+                OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
+            ]
+        else:
+            methods = [
+                OperatorNode._aug_op(left, aug_opnode, aug_op, right, context),
+                OperatorNode._bin_op(left, aug_opnode, bin_op, right, context),
+                OperatorNode._bin_op(
+                    right, aug_opnode, bin_op, left, reverse_context, reverse=True
+                ),
+            ]
+        return methods
+
+    @staticmethod
+    def _get_binop_flow(
+        left: InferenceResult,
+        left_type: InferenceResult | None,
+        binary_opnode: nodes.AugAssign | nodes.BinOp,
+        right: InferenceResult,
+        right_type: InferenceResult | None,
+        context: InferenceContext,
+        reverse_context: InferenceContext,
+    ) -> list[partial[Generator[InferenceResult, None, None]]]:
+        """Get the flow for binary operations.
+
+        The rules are a bit messy:
+
+            * if left and right have the same type, then only one
+            method will be called, left.__op__(right)
+            * if left and right are unrelated typewise, then first
+            left.__op__(right) is tried and if this does not exist
+            or returns NotImplemented, then right.__rop__(left) is tried.
+            * if left is a subtype of right, then only left.__op__(right)
+            is tried.
+            * if left is a supertype of right, then right.__rop__(left)
+            is first tried and then left.__op__(right)
+        """
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        op = binary_opnode.op
+        if OperatorNode._same_type(left_type, right_type):
+            methods = [OperatorNode._bin_op(left, binary_opnode, op, right, context)]
+        elif helpers.is_subtype(left_type, right_type):
+            methods = [OperatorNode._bin_op(left, binary_opnode, op, right, context)]
+        elif helpers.is_supertype(left_type, right_type):
+            methods = [
+                OperatorNode._bin_op(
+                    right, binary_opnode, op, left, reverse_context, reverse=True
+                ),
+                OperatorNode._bin_op(left, binary_opnode, op, right, context),
+            ]
+        else:
+            methods = [
+                OperatorNode._bin_op(left, binary_opnode, op, right, context),
+                OperatorNode._bin_op(
+                    right, binary_opnode, op, left, reverse_context, reverse=True
+                ),
+            ]
+
+        if (
+            PY310_PLUS
+            and op == "|"
+            and (
+                isinstance(left, (bases.UnionType, nodes.ClassDef))
+                or isinstance(left, nodes.Const)
+                and left.value is None
+            )
+            and (
+                isinstance(right, (bases.UnionType, nodes.ClassDef))
+                or isinstance(right, nodes.Const)
+                and right.value is None
+            )
+        ):
+            methods.extend([partial(OperatorNode._bin_op_or_union_type, left, right)])
+        return methods
+
+    @staticmethod
+    def _infer_binary_operation(
+        left: InferenceResult,
+        right: InferenceResult,
+        binary_opnode: nodes.AugAssign | nodes.BinOp,
+        context: InferenceContext,
+        flow_factory: GetFlowFactory,
+    ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
+        """Infer a binary operation between a left operand and a right operand.
+
+        This is used by both normal binary operations and augmented binary
+        operations, the only difference is the flow factory used.
+        """
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        context, reverse_context = OperatorNode._get_binop_contexts(
+            context, left, right
+        )
+        left_type = helpers.object_type(left)
+        right_type = helpers.object_type(right)
+        methods = flow_factory(
+            left, left_type, binary_opnode, right, right_type, context, reverse_context
+        )
+        for method in methods:
+            try:
+                results = list(method())
+            except AttributeError:
+                continue
+            except AttributeInferenceError:
+                continue
+            except InferenceError:
+                yield util.Uninferable
+                return
+            else:
+                if any(isinstance(result, util.UninferableBase) for result in results):
+                    yield util.Uninferable
+                    return
+
+                if all(map(OperatorNode._is_not_implemented, results)):
+                    continue
+                not_implemented = sum(
+                    1 for result in results if OperatorNode._is_not_implemented(result)
+                )
+                if not_implemented and not_implemented != len(results):
+                    # Can't infer yet what this is.
+                    yield util.Uninferable
+                    return
+
+                yield from results
+                return
+
+        # The operation doesn't seem to be supported so let the caller know about it
+        yield util.BadBinaryOperationMessage(left_type, binary_opnode.op, right_type)
+
+
+class AttributeNode(NodeNG):
+    expr: NodeNG
+    """The name that this node represents."""
+    attrname: str
+
+    @decorators.raise_if_nothing_inferred
+    def _infer_attribute(
+        self,
+        context: InferenceContext | None = None,
+        **kwargs: Any,
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo]:
+        """Infer an Attribute node by using getattr on the associated object."""
+        # pylint: disable=import-outside-toplevel
+        from astroid.constraint import get_constraints
+
+        for owner in self.expr.infer(context):
+            if isinstance(owner, util.UninferableBase):
+                yield owner
+                continue
+
+            context = copy_context(context)
+            old_boundnode = context.boundnode
+            try:
+                context.boundnode = owner
+                if isinstance(owner, (nodes.ClassDef, bases.Instance)):
+                    frame = (
+                        owner if isinstance(owner, nodes.ClassDef) else owner._proxied
+                    )
+                    context.constraints[self.attrname] = get_constraints(
+                        self, frame=frame
+                    )
+                yield from owner.igetattr(self.attrname, context)
+            except (
+                AttributeInferenceError,
+                InferenceError,
+                AttributeError,
+            ):
+                pass
+            finally:
+                context.boundnode = old_boundnode
+        return InferenceErrorInfo(node=self, context=context)
+
+
+class AssignNode(NodeNG):
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer_assign(
+        self,
+        context: InferenceContext | None = None,
+        **kwargs: Any,
+    ) -> Generator[InferenceResult, None, None]:
+        """Infer a AssignName/AssignAttr: need to inspect the RHS part of the
+        assign node.
+        """
+        if isinstance(self.parent, nodes.AugAssign):
+            return self.parent.infer(context)
+
+        stmts = list(self.assigned_stmts(context=context))
+        return bases._infer_stmts(stmts, context)
+
+    def assigned_stmts(
+        self: nodes.AssignName | nodes.AssignAttr,
+        node: AssignedStmtsPossibleNode = None,
+        context: InferenceContext | None = None,
+        assign_path: list[int] | None = None,
+    ) -> Any:
+        """Returns the assigned statement (non inferred) according to the assignment type."""
+        return self.parent.assigned_stmts(node=self, context=context)
+
+
+class NameNode(LookupMixIn):
+    name: str
+
+    @decorators.raise_if_nothing_inferred
+    def _infer_name_node(
+        self,
+        context: InferenceContext | None = None,
+        **kwargs: Any,
+    ) -> Generator[InferenceResult, None, None]:
+        """Infer a Name: use name lookup rules."""
+        # pylint: disable=import-outside-toplevel
+        from astroid.constraint import get_constraints
+        from astroid.helpers import _higher_function_scope
+
+        frame, stmts = self.lookup(self.name)
+        if not stmts:
+            # Try to see if the name is enclosed in a nested function
+            # and use the higher (first function) scope for searching.
+            parent_function = _higher_function_scope(self.scope())
+            if parent_function:
+                _, stmts = parent_function.lookup(self.name)
+
+            if not stmts:
+                raise NameInferenceError(
+                    name=self.name, scope=self.scope(), context=context
+                )
+        context = copy_context(context)
+        context.lookupname = self.name
+        context.constraints[self.name] = get_constraints(self, frame)
+
+        return bases._infer_stmts(stmts, context, frame)
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
index 349385e976..4f2bc949fe 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -7,41 +7,46 @@
 from __future__ import annotations
 
 import abc
+import ast
 import itertools
+import operator
 import sys
 import typing
 import warnings
 from collections.abc import Generator, Iterable, Iterator, Mapping
-from functools import cached_property, lru_cache
+from functools import cached_property
 from typing import (
     TYPE_CHECKING,
     Any,
     Callable,
-    ClassVar,
     Literal,
     Optional,
     Union,
 )
 
-from astroid import decorators, util
+from astroid import decorators, protocols, util
 from astroid.bases import Instance, _infer_stmts
 from astroid.const import _EMPTY_OBJECT_MARKER, Context
-from astroid.context import InferenceContext
+from astroid.context import CallContext, InferenceContext, copy_context
 from astroid.exceptions import (
+    AstroidBuildingError,
+    AstroidError,
     AstroidIndexError,
     AstroidTypeError,
     AstroidValueError,
+    AttributeInferenceError,
     InferenceError,
     NoDefault,
     ParentMissingError,
+    _NonDeducibleTypeHierarchy,
 )
+from astroid.interpreter import dunder_lookup
 from astroid.manager import AstroidManager
 from astroid.nodes import _base_nodes
 from astroid.nodes.const import OP_PRECEDENCE
 from astroid.nodes.node_ng import NodeNG
 from astroid.typing import (
     ConstFactoryResult,
-    InferBinaryOp,
     InferenceErrorInfo,
     InferenceResult,
     SuccessfulInferenceResult,
@@ -52,7 +57,6 @@
 else:
     from typing_extensions import Self
 
-
 if TYPE_CHECKING:
     from astroid import nodes
     from astroid.nodes import LocalsDictNodeNG
@@ -337,46 +341,67 @@ def pytype(self) -> str:
     def get_children(self):
         yield from self.elts
 
+    @decorators.raise_if_nothing_inferred
+    def _infer(
+        self,
+        context: InferenceContext | None = None,
+        **kwargs: Any,
+    ) -> Iterator[Self]:
+        has_starred_named_expr = any(
+            isinstance(e, (Starred, NamedExpr)) for e in self.elts
+        )
+        if has_starred_named_expr:
+            values = self._infer_sequence_helper(context)
+            new_seq = type(self)(
+                lineno=self.lineno,
+                col_offset=self.col_offset,
+                parent=self.parent,
+                end_lineno=self.end_lineno,
+                end_col_offset=self.end_col_offset,
+            )
+            new_seq.postinit(values)
 
-# TODO: Move into _base_nodes. Blocked by import of _infer_stmts from bases.
-class LookupMixIn(NodeNG):
-    """Mixin to look up a name in the right scope."""
-
-    @lru_cache  # noqa
-    def lookup(self, name: str) -> tuple[LocalsDictNodeNG, list[NodeNG]]:
-        """Lookup where the given variable is assigned.
-
-        The lookup starts from self's scope. If self is not a frame itself
-        and the name is found in the inner frame locals, statements will be
-        filtered to remove ignorable statements according to self's location.
-
-        :param name: The name of the variable to find assignments for.
-
-        :returns: The scope node and the list of assignments associated to the
-            given name according to the scope where it has been found (locals,
-            globals or builtin).
-        """
-        return self.scope().scope_lookup(self, name)
-
-    def ilookup(self, name):
-        """Lookup the inferred values of the given variable.
-
-        :param name: The variable name to find values for.
-        :type name: str
-
-        :returns: The inferred values of the statements returned from
-            :meth:`lookup`.
-        :rtype: iterable
-        """
-        frame, stmts = self.lookup(name)
-        context = InferenceContext()
-        return _infer_stmts(stmts, context, frame)
+            yield new_seq
+        else:
+            yield self
+
+    def _infer_sequence_helper(
+        self, context: InferenceContext | None = None
+    ) -> list[SuccessfulInferenceResult]:
+        """Infer all values based on BaseContainer.elts."""
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        values = []
+
+        for elt in self.elts:
+            if isinstance(elt, Starred):
+                starred = helpers.safe_infer(elt.value, context)
+                if not starred:
+                    raise InferenceError(node=self, context=context)
+                if not hasattr(starred, "elts"):
+                    raise InferenceError(node=self, context=context)
+                # TODO: fresh context?
+                values.extend(starred._infer_sequence_helper(context))
+            elif isinstance(elt, NamedExpr):
+                value = helpers.safe_infer(elt.value, context)
+                if not value:
+                    raise InferenceError(node=self, context=context)
+                values.append(value)
+            else:
+                values.append(elt)
+        return values
 
 
 # Name classes
 
 
-class AssignName(_base_nodes.NoChildrenNode, LookupMixIn, _base_nodes.ParentAssignNode):
+class AssignName(
+    _base_nodes.NameNode,
+    _base_nodes.AssignNode,
+    _base_nodes.NoChildrenNode,
+    _base_nodes.LookupMixIn,
+    _base_nodes.ParentAssignNode,
+):
     """Variation of :class:`ast.Assign` representing assignment to a name.
 
     An :class:`AssignName` is the name of something that is assigned to.
@@ -394,8 +419,6 @@ class AssignName(_base_nodes.NoChildrenNode, LookupMixIn, _base_nodes.ParentAssi
 
     _other_fields = ("name",)
 
-    infer_lhs: ClassVar[InferLHS[AssignName]]
-
     def __init__(
         self,
         name: str,
@@ -417,13 +440,26 @@ def __init__(
             parent=parent,
         )
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[AssignName]]
+    assigned_stmts = protocols.assend_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        return self._infer_assign(context, **kwargs)
+
+    @decorators.raise_if_nothing_inferred
+    def infer_lhs(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        return self._infer_name_node(context, **kwargs)
+
 
-class DelName(_base_nodes.NoChildrenNode, LookupMixIn, _base_nodes.ParentAssignNode):
+class DelName(
+    _base_nodes.NoChildrenNode, _base_nodes.LookupMixIn, _base_nodes.ParentAssignNode
+):
     """Variation of :class:`ast.Delete` representing deletion of a name.
 
     A :class:`DelName` is the name of something that is deleted.
@@ -460,7 +496,7 @@ def __init__(
         )
 
 
-class Name(_base_nodes.NoChildrenNode, LookupMixIn):
+class Name(_base_nodes.NameNode, _base_nodes.NoChildrenNode):
     """Class representing an :class:`ast.Name` node.
 
     A :class:`Name` node is something that is named, but not covered by
@@ -505,6 +541,12 @@ def _get_name_nodes(self):
         for child_node in self.get_children():
             yield from child_node._get_name_nodes()
 
+    @decorators.path_wrapper
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        return self._infer_name_node(context, **kwargs)
+
 
 DEPRECATED_ARGUMENT_DEFAULT = object()
 
@@ -663,7 +705,7 @@ def postinit(
             type_comment_posonlyargs = []
         self.type_comment_posonlyargs = type_comment_posonlyargs
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[Arguments]]
+    assigned_stmts = protocols.arguments_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -894,6 +936,17 @@ def get_children(self):
             if elt is not None:
                 yield elt
 
+    @decorators.raise_if_nothing_inferred
+    def _infer(
+        self: nodes.Arguments, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        # pylint: disable-next=import-outside-toplevel
+        from astroid.protocols import _arguments_infer_argname
+
+        if context is None or context.lookupname is None:
+            raise InferenceError(node=self, context=context)
+        return _arguments_infer_argname(self, context.lookupname, context)
+
 
 def _find_arg(argname, args):
     for i, arg in enumerate(args):
@@ -934,7 +987,9 @@ def _format_args(
     return ", ".join(values)
 
 
-class AssignAttr(_base_nodes.ParentAssignNode):
+class AssignAttr(
+    _base_nodes.AttributeNode, _base_nodes.AssignNode, _base_nodes.ParentAssignNode
+):
     """Variation of :class:`ast.Assign` representing assignment to an attribute.
 
     >>> import astroid
@@ -950,11 +1005,6 @@ class AssignAttr(_base_nodes.ParentAssignNode):
     _astroid_fields = ("expr",)
     _other_fields = ("attrname",)
 
-    infer_lhs: ClassVar[InferLHS[AssignAttr]]
-
-    expr: NodeNG
-    """What has the attribute that is being assigned to."""
-
     def __init__(
         self,
         attrname: str,
@@ -979,7 +1029,7 @@ def __init__(
     def postinit(self, expr: NodeNG) -> None:
         self.expr = expr
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[AssignAttr]]
+    assigned_stmts = protocols.assend_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -987,6 +1037,16 @@ def postinit(self, expr: NodeNG) -> None:
     def get_children(self):
         yield self.expr
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        return self._infer_assign(context, **kwargs)
+
+    def infer_lhs(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        return self._infer_attribute(context, **kwargs)
+
 
 class Assert(_base_nodes.Statement):
     """Class representing an :class:`ast.Assert` node.
@@ -1052,7 +1112,7 @@ def postinit(
         self.value = value
         self.type_annotation = type_annotation
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[Assign]]
+    assigned_stmts = protocols.assign_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -1108,7 +1168,7 @@ def postinit(
         self.value = value
         self.simple = simple
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[AnnAssign]]
+    assigned_stmts = protocols.assign_annassigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -1121,7 +1181,9 @@ def get_children(self):
             yield self.value
 
 
-class AugAssign(_base_nodes.AssignTypeNode, _base_nodes.Statement):
+class AugAssign(
+    _base_nodes.AssignTypeNode, _base_nodes.OperatorNode, _base_nodes.Statement
+):
     """Class representing an :class:`ast.AugAssign` node.
 
     An :class:`AugAssign` is an assignment paired with an operator.
@@ -1169,16 +1231,11 @@ def postinit(self, target: Name | Attribute | Subscript, value: NodeNG) -> None:
         self.target = target
         self.value = value
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[AugAssign]]
+    assigned_stmts = protocols.assign_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
 
-    # This is set by inference.py
-    _infer_augassign: ClassVar[
-        InferBinaryOperation[AugAssign, util.BadBinaryOperationMessage]
-    ]
-
     def type_errors(self, context: InferenceContext | None = None):
         """Get a list of type errors which can occur during inference.
 
@@ -1207,8 +1264,45 @@ def _get_yield_nodes_skip_lambdas(self):
         yield from self.value._get_yield_nodes_skip_lambdas()
         yield from super()._get_yield_nodes_skip_lambdas()
 
+    def _infer_augassign(
+        self, context: InferenceContext | None = None
+    ) -> Generator[InferenceResult | util.BadBinaryOperationMessage, None, None]:
+        """Inference logic for augmented binary operations."""
+        context = context or InferenceContext()
+
+        rhs_context = context.clone()
+
+        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(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
+                # Don't know how to process this.
+                yield util.Uninferable
+                return
+
+            try:
+                yield from self._infer_binary_operation(
+                    left=lhs,
+                    right=rhs,
+                    binary_opnode=self,
+                    context=context,
+                    flow_factory=self._get_aug_flow,
+                )
+            except _NonDeducibleTypeHierarchy:
+                yield util.Uninferable
 
-class BinOp(NodeNG):
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self: nodes.AugAssign, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        return self._filter_operation_errors(
+            self._infer_augassign, context, util.BadBinaryOperationMessage
+        )
+
+
+class BinOp(_base_nodes.OperatorNode):
     """Class representing an :class:`ast.BinOp` node.
 
     A :class:`BinOp` node is an application of a binary operator.
@@ -1253,9 +1347,6 @@ def postinit(self, left: NodeNG, right: NodeNG) -> None:
         self.left = left
         self.right = right
 
-    # This is set by inference.py
-    _infer_binop: ClassVar[InferBinaryOperation[BinOp, util.BadBinaryOperationMessage]]
-
     def type_errors(self, context: InferenceContext | None = None):
         """Get a list of type errors which can occur during inference.
 
@@ -1286,6 +1377,43 @@ def op_left_associative(self) -> bool:
         # 2**3**4 == 2**(3**4)
         return self.op != "**"
 
+    def _infer_binop(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        """Binary operation inference logic."""
+        left = self.left
+        right = self.right
+
+        # we use two separate contexts for evaluating lhs and rhs because
+        # 1. evaluating lhs may leave some undesired entries in context.path
+        #    which may not let us infer right value of rhs
+        context = context or InferenceContext()
+        lhs_context = copy_context(context)
+        rhs_context = copy_context(context)
+        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(isinstance(value, util.UninferableBase) for value in (rhs, lhs)):
+                # Don't know how to process this.
+                yield util.Uninferable
+                return
+
+            try:
+                yield from self._infer_binary_operation(
+                    lhs, rhs, self, context, self._get_binop_flow
+                )
+            except _NonDeducibleTypeHierarchy:
+                yield util.Uninferable
+
+    @decorators.yes_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self: nodes.BinOp, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        return self._filter_operation_errors(
+            self._infer_binop, context, util.BadBinaryOperationMessage
+        )
+
 
 class BoolOp(NodeNG):
     """Class representing an :class:`ast.BoolOp` node.
@@ -1355,6 +1483,60 @@ def get_children(self):
     def op_precedence(self):
         return OP_PRECEDENCE[self.op]
 
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self: nodes.BoolOp, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        """Infer a boolean operation (and / or / not).
+
+        The function will calculate the boolean operation
+        for all pairs generated through inference for each component
+        node.
+        """
+        values = self.values
+        if self.op == "or":
+            predicate = operator.truth
+        else:
+            predicate = operator.not_
+
+        try:
+            inferred_values = [value.infer(context=context) for value in values]
+        except InferenceError:
+            yield util.Uninferable
+            return None
+
+        for pair in itertools.product(*inferred_values):
+            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(isinstance(item, util.UninferableBase) for item in bool_values):
+                # Can't infer the final result, just yield Uninferable.
+                yield util.Uninferable
+                continue
+
+            # Since the boolean operations are short circuited operations,
+            # this code yields the first value for which the predicate is True
+            # and if no value respected the predicate, then the last value will
+            # be returned (or Uninferable if there was no last value).
+            # This is conforming to the semantics of `and` and `or`:
+            #   1 and 0 -> 1
+            #   0 and 1 -> 0
+            #   1 or 0 -> 1
+            #   0 or 1 -> 1
+            value = util.Uninferable
+            for value, bool_value in zip(pair, bool_values):
+                if predicate(bool_value):
+                    yield value
+                    break
+            else:
+                yield value
+
+        return InferenceErrorInfo(node=self, context=context)
+
 
 class Break(_base_nodes.NoChildrenNode, _base_nodes.Statement):
     """Class representing an :class:`ast.Break` node.
@@ -1412,6 +1594,64 @@ def get_children(self):
 
         yield from self.keywords
 
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo]:
+        """Infer a Call node by trying to guess what the function returns."""
+        callcontext = copy_context(context)
+        callcontext.boundnode = None
+        if context is not None:
+            callcontext.extra_context = self._populate_context_lookup(context.clone())
+
+        for callee in self.func.infer(context):
+            if isinstance(callee, util.UninferableBase):
+                yield callee
+                continue
+            try:
+                if hasattr(callee, "infer_call_result"):
+                    callcontext.callcontext = CallContext(
+                        args=self.args, keywords=self.keywords, callee=callee
+                    )
+                    yield from callee.infer_call_result(
+                        caller=self, context=callcontext
+                    )
+            except InferenceError:
+                continue
+        return InferenceErrorInfo(node=self, context=context)
+
+    def _populate_context_lookup(self, context: InferenceContext | None):
+        """Allows context to be saved for later for inference inside a function."""
+        context_lookup: dict[InferenceResult, InferenceContext] = {}
+        if context is None:
+            return context_lookup
+        for arg in self.args:
+            if isinstance(arg, Starred):
+                context_lookup[arg.value] = context
+            else:
+                context_lookup[arg] = context
+        keywords = self.keywords if self.keywords is not None else []
+        for keyword in keywords:
+            context_lookup[keyword.value] = context
+        return context_lookup
+
+
+COMPARE_OPS: dict[str, Callable[[Any, Any], bool]] = {
+    "==": operator.eq,
+    "!=": operator.ne,
+    "<": operator.lt,
+    "<=": operator.le,
+    ">": operator.gt,
+    ">=": operator.ge,
+    "in": lambda a, b: a in b,
+    "not in": lambda a, b: a not in b,
+}
+UNINFERABLE_OPS = {
+    "is",
+    "is not",
+}
+
 
 class Compare(NodeNG):
     """Class representing an :class:`ast.Compare` node.
@@ -1461,6 +1701,88 @@ def last_child(self):
         return self.ops[-1][1]
         # return self.left
 
+    # TODO: move to util?
+    @staticmethod
+    def _to_literal(node: SuccessfulInferenceResult) -> Any:
+        # Can raise SyntaxError or ValueError from ast.literal_eval
+        # Can raise AttributeError from node.as_string() as not all nodes have a visitor
+        # Is this the stupidest idea or the simplest idea?
+        return ast.literal_eval(node.as_string())
+
+    def _do_compare(
+        self,
+        left_iter: Iterable[InferenceResult],
+        op: str,
+        right_iter: Iterable[InferenceResult],
+    ) -> bool | util.UninferableBase:
+        """
+        If all possible combinations are either True or False, return that:
+        >>> _do_compare([1, 2], '<=', [3, 4])
+        True
+        >>> _do_compare([1, 2], '==', [3, 4])
+        False
+
+        If any item is uninferable, or if some combinations are True and some
+        are False, return Uninferable:
+        >>> _do_compare([1, 3], '<=', [2, 4])
+        util.Uninferable
+        """
+        retval: bool | None = None
+        if op in UNINFERABLE_OPS:
+            return util.Uninferable
+        op_func = COMPARE_OPS[op]
+
+        for left, right in itertools.product(left_iter, right_iter):
+            if isinstance(left, util.UninferableBase) or isinstance(
+                right, util.UninferableBase
+            ):
+                return util.Uninferable
+
+            try:
+                left, right = self._to_literal(left), self._to_literal(right)
+            except (SyntaxError, ValueError, AttributeError):
+                return util.Uninferable
+
+            try:
+                expr = op_func(left, right)
+            except TypeError as exc:
+                raise AstroidTypeError from exc
+
+            if retval is None:
+                retval = expr
+            elif retval != expr:
+                return util.Uninferable
+                # (or both, but "True | False" is basically the same)
+
+        assert retval is not None
+        return retval  # it was all the same value
+
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[nodes.Const | util.UninferableBase, None, None]:
+        """Chained comparison inference logic."""
+        retval: bool | util.UninferableBase = True
+
+        ops = self.ops
+        left_node = self.left
+        lhs = list(left_node.infer(context=context))
+        # should we break early if first element is uninferable?
+        for op, right_node in ops:
+            # eagerly evaluate rhs so that values can be re-used as lhs
+            rhs = list(right_node.infer(context=context))
+            try:
+                retval = self._do_compare(lhs, op, rhs)
+            except AstroidTypeError:
+                retval = util.Uninferable
+                break
+            if retval is not True:
+                break  # short-circuit
+            lhs = rhs  # continue
+        if retval is util.Uninferable:
+            yield retval  # type: ignore[misc]
+        else:
+            yield Const(retval)
+
 
 class Comprehension(NodeNG):
     """Class representing an :class:`ast.comprehension` node.
@@ -1506,7 +1828,7 @@ def postinit(
         self.ifs = ifs
         self.is_async = is_async
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[Comprehension]]
+    assigned_stmts = protocols.for_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -1603,8 +1925,8 @@ def __init__(
 
         Instance.__init__(self, None)
 
-    infer_unary_op: ClassVar[InferUnaryOp[Const]]
-    infer_binary_op: ClassVar[InferBinaryOp[Const]]
+    infer_unary_op = protocols.const_infer_unary_op
+    infer_binary_op = protocols.const_infer_binary_op
 
     def __getattr__(self, name):
         # This is needed because of Proxy's __getattr__ method.
@@ -1692,6 +2014,11 @@ def bool_value(self, context: InferenceContext | None = None):
         """
         return bool(self.value)
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[Const]:
+        yield self
+
 
 class Continue(_base_nodes.NoChildrenNode, _base_nodes.Statement):
     """Class representing an :class:`ast.Continue` node.
@@ -1871,7 +2198,7 @@ def postinit(self, items: list[tuple[InferenceResult, InferenceResult]]) -> None
         """
         self.items = items
 
-    infer_unary_op: ClassVar[InferUnaryOp[Dict]]
+    infer_unary_op = protocols.dict_infer_unary_op
 
     def pytype(self) -> Literal["builtins.dict"]:
         """Get the name of the type that this node represents.
@@ -1923,13 +2250,12 @@ def getitem(
         :raises AstroidIndexError: If the given index does not exist in the
             dictionary.
         """
-        # pylint: disable-next=import-outside-toplevel; circular import
-        from astroid.helpers import safe_infer
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
 
         for key, value in self.items:
             # TODO(cpopa): no support for overriding yet, {1:2, **{1: 3}}.
             if isinstance(key, DictUnpack):
-                inferred_value = safe_infer(value, context)
+                inferred_value = helpers.safe_infer(value, context)
                 if not isinstance(inferred_value, Dict):
                     continue
 
@@ -1955,6 +2281,74 @@ def bool_value(self, context: InferenceContext | None = None):
         """
         return bool(self.items)
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[nodes.Dict]:
+        if not any(isinstance(k, DictUnpack) for k, _ in self.items):
+            yield self
+        else:
+            items = self._infer_map(context)
+            new_seq = type(self)(
+                lineno=self.lineno,
+                col_offset=self.col_offset,
+                parent=self.parent,
+                end_lineno=self.end_lineno,
+                end_col_offset=self.end_col_offset,
+            )
+            new_seq.postinit(list(items.items()))
+            yield new_seq
+
+    @staticmethod
+    def _update_with_replacement(
+        lhs_dict: dict[SuccessfulInferenceResult, SuccessfulInferenceResult],
+        rhs_dict: dict[SuccessfulInferenceResult, SuccessfulInferenceResult],
+    ) -> dict[SuccessfulInferenceResult, SuccessfulInferenceResult]:
+        """Delete nodes that equate to duplicate keys.
+
+        Since an astroid node doesn't 'equal' another node with the same value,
+        this function uses the as_string method to make sure duplicate keys
+        don't get through
+
+        Note that both the key and the value are astroid nodes
+
+        Fixes issue with DictUnpack causing duplicate keys
+        in inferred Dict items
+
+        :param lhs_dict: Dictionary to 'merge' nodes into
+        :param rhs_dict: Dictionary with nodes to pull from
+        :return : merged dictionary of nodes
+        """
+        combined_dict = itertools.chain(lhs_dict.items(), rhs_dict.items())
+        # Overwrite keys which have the same string values
+        string_map = {key.as_string(): (key, value) for key, value in combined_dict}
+        # Return to dictionary
+        return dict(string_map.values())
+
+    def _infer_map(
+        self, context: InferenceContext | None
+    ) -> dict[SuccessfulInferenceResult, SuccessfulInferenceResult]:
+        """Infer all values based on Dict.items."""
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        values: dict[SuccessfulInferenceResult, SuccessfulInferenceResult] = {}
+        for name, value in self.items:
+            if isinstance(name, DictUnpack):
+                double_starred = helpers.safe_infer(value, context)
+                if not double_starred:
+                    raise InferenceError
+                if not isinstance(double_starred, Dict):
+                    raise InferenceError(node=self, context=context)
+                unpack_items = double_starred._infer_map(context)
+                values = self._update_with_replacement(values, unpack_items)
+            else:
+                key = helpers.safe_infer(name, context=context)
+                safe_value = helpers.safe_infer(value, context=context)
+                if any(not elem for elem in (key, safe_value)):
+                    raise InferenceError(node=self, context=context)
+                # safe_value is SuccessfulInferenceResult as bool(Uninferable) == False
+                values = self._update_with_replacement(values, {key: safe_value})
+        return values
+
 
 class Expr(_base_nodes.Statement):
     """Class representing an :class:`ast.Expr` node.
@@ -2011,6 +2405,21 @@ def __init__(
     def has_underlying_object(self) -> bool:
         return self.object is not None and self.object is not _EMPTY_OBJECT_MARKER
 
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        if not self.has_underlying_object():
+            yield util.Uninferable
+        else:
+            try:
+                yield from AstroidManager().infer_ast_from_something(
+                    self.object, context=context
+                )
+            except AstroidError:
+                yield util.Uninferable
+
 
 class ExceptHandler(
     _base_nodes.MultiLineBlockNode, _base_nodes.AssignTypeNode, _base_nodes.Statement
@@ -2044,7 +2453,7 @@ class ExceptHandler(
     body: list[NodeNG]
     """The contents of the block."""
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[ExceptHandler]]
+    assigned_stmts = protocols.excepthandler_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -2142,7 +2551,7 @@ def postinit(
         self.orelse = orelse
         self.type_annotation = type_annotation
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[For]]
+    assigned_stmts = protocols.for_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -2283,16 +2692,47 @@ def __init__(
             parent=parent,
         )
 
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self,
+        context: InferenceContext | None = None,
+        asname: bool = True,
+        **kwargs: Any,
+    ) -> Generator[InferenceResult, None, None]:
+        """Infer a ImportFrom node: return the imported module/object."""
+        context = context or InferenceContext()
+        name = context.lookupname
+        if name is None:
+            raise InferenceError(node=self, context=context)
+        if asname:
+            try:
+                name = self.real_name(name)
+            except AttributeInferenceError as exc:
+                # See https://github.com/pylint-dev/pylint/issues/4692
+                raise InferenceError(node=self, context=context) from exc
+        try:
+            module = self.do_import_module()
+        except AstroidBuildingError as exc:
+            raise InferenceError(node=self, context=context) from exc
+
+        try:
+            context = copy_context(context)
+            context.lookupname = name
+            stmts = module.getattr(name, ignore_locals=module is self.root())
+            return _infer_stmts(stmts, context)
+        except AttributeInferenceError as error:
+            raise InferenceError(
+                str(error), target=self, attribute=name, context=context
+            ) from error
+
 
-class Attribute(NodeNG):
+class Attribute(_base_nodes.AttributeNode):
     """Class representing an :class:`ast.Attribute` node."""
 
     _astroid_fields = ("expr",)
     _other_fields = ("attrname",)
 
-    expr: NodeNG
-    """The name that this node represents."""
-
     def __init__(
         self,
         attrname: str,
@@ -2320,6 +2760,12 @@ def postinit(self, expr: NodeNG) -> None:
     def get_children(self):
         yield self.expr
 
+    @decorators.path_wrapper
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        return self._infer_attribute(context, **kwargs)
+
 
 class Global(_base_nodes.NoChildrenNode, _base_nodes.Statement):
     """Class representing an :class:`ast.Global` node.
@@ -2371,6 +2817,21 @@ def __init__(
     def _infer_name(self, frame, name):
         return name
 
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        if context is None or context.lookupname is None:
+            raise InferenceError(node=self, context=context)
+        try:
+            # pylint: disable-next=no-member
+            return _infer_stmts(self.root().getattr(context.lookupname), context)
+        except AttributeInferenceError as error:
+            raise InferenceError(
+                str(error), target=self, attribute=context.lookupname, context=context
+            ) from error
+
 
 class If(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
     """Class representing an :class:`ast.If` node.
@@ -2469,6 +2930,40 @@ def op_left_associative(self) -> Literal[False]:
         # `1 if True else (2 if False else 3)`
         return False
 
+    @decorators.raise_if_nothing_inferred
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, None]:
+        """Support IfExp inference.
+
+        If we can't infer the truthiness of the condition, we default
+        to inferring both branches. Otherwise, we infer either branch
+        depending on the condition.
+        """
+        both_branches = False
+        # We use two separate contexts for evaluating lhs and rhs because
+        # evaluating lhs may leave some undesired entries in context.path
+        # which may not let us infer right value of rhs.
+
+        context = context or InferenceContext()
+        lhs_context = copy_context(context)
+        rhs_context = copy_context(context)
+        try:
+            test = next(self.test.infer(context=context.clone()))
+        except (InferenceError, StopIteration):
+            both_branches = True
+        else:
+            if not isinstance(test, util.UninferableBase):
+                if test.bool_value():
+                    yield from self.body.infer(context=lhs_context)
+                else:
+                    yield from self.orelse.infer(context=rhs_context)
+            else:
+                both_branches = True
+        if both_branches:
+            yield from self.body.infer(context=lhs_context)
+            yield from self.orelse.infer(context=rhs_context)
+
 
 class Import(_base_nodes.ImportNode):
     """Class representing an :class:`ast.Import` node.
@@ -2521,6 +3016,28 @@ def __init__(
             parent=parent,
         )
 
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self,
+        context: InferenceContext | None = None,
+        asname: bool = True,
+        **kwargs: Any,
+    ) -> Generator[nodes.Module, None, None]:
+        """Infer an Import node: return the imported module/object."""
+        context = context or InferenceContext()
+        name = context.lookupname
+        if name is None:
+            raise InferenceError(node=self, context=context)
+
+        try:
+            if asname:
+                yield self.do_import_module(self.real_name(name))
+            else:
+                yield self.do_import_module(name)
+        except AstroidBuildingError as exc:
+            raise InferenceError(node=self, context=context) from exc
+
 
 class Keyword(NodeNG):
     """Class representing an :class:`ast.keyword` node.
@@ -2614,13 +3131,13 @@ def __init__(
             parent=parent,
         )
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[List]]
+    assigned_stmts = protocols.sequence_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
 
-    infer_unary_op: ClassVar[InferUnaryOp[List]]
-    infer_binary_op: ClassVar[InferBinaryOp[List]]
+    infer_unary_op = protocols.list_infer_unary_op
+    infer_binary_op = protocols.tl_infer_binary_op
 
     def pytype(self) -> Literal["builtins.list"]:
         """Get the name of the type that this node represents.
@@ -2727,6 +3244,11 @@ def __init__(
     def postinit(self, *, name: AssignName) -> None:
         self.name = name
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[ParamSpec]:
+        yield self
+
 
 class Pass(_base_nodes.NoChildrenNode, _base_nodes.Statement):
     """Class representing an :class:`ast.Pass` node.
@@ -2819,7 +3341,7 @@ class Set(BaseContainer):
     <Set.set l.1 at 0x7f23b2e71d68>
     """
 
-    infer_unary_op: ClassVar[InferUnaryOp[Set]]
+    infer_unary_op = protocols.set_infer_unary_op
 
     def pytype(self) -> Literal["builtins.set"]:
         """Get the name of the type that this node represents.
@@ -2912,6 +3434,11 @@ def get_children(self):
         if self.step is not None:
             yield self.step
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[Slice]:
+        yield self
+
 
 class Starred(_base_nodes.ParentAssignNode):
     """Class representing an :class:`ast.Starred` node.
@@ -2952,7 +3479,7 @@ def __init__(
     def postinit(self, value: NodeNG) -> None:
         self.value = value
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[Starred]]
+    assigned_stmts = protocols.starred_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -2970,11 +3497,10 @@ class Subscript(NodeNG):
     <Subscript l.1 at 0x7f23b2e71f60>
     """
 
+    _SUBSCRIPT_SENTINEL = object()
     _astroid_fields = ("value", "slice")
     _other_fields = ("ctx",)
 
-    infer_lhs: ClassVar[InferLHS[Subscript]]
-
     value: NodeNG
     """What is being indexed."""
 
@@ -3011,6 +3537,74 @@ def get_children(self):
         yield self.value
         yield self.slice
 
+    def _infer_subscript(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
+        """Inference for subscripts.
+
+        We're understanding if the index is a Const
+        or a slice, passing the result of inference
+        to the value's `getitem` method, which should
+        handle each supported index type accordingly.
+        """
+        from astroid import helpers  # pylint: disable=import-outside-toplevel
+
+        found_one = False
+        for value in self.value.infer(context):
+            if isinstance(value, util.UninferableBase):
+                yield util.Uninferable
+                return None
+            for index in self.slice.infer(context):
+                if isinstance(index, util.UninferableBase):
+                    yield util.Uninferable
+                    return None
+
+                # Try to deduce the index value.
+                index_value = self._SUBSCRIPT_SENTINEL
+                if value.__class__ == Instance:
+                    index_value = index
+                elif index.__class__ == Instance:
+                    instance_as_index = helpers.class_instance_as_index(index)
+                    if instance_as_index:
+                        index_value = instance_as_index
+                else:
+                    index_value = index
+
+                if index_value is self._SUBSCRIPT_SENTINEL:
+                    raise InferenceError(node=self, context=context)
+
+                try:
+                    assigned = value.getitem(index_value, context)
+                except (
+                    AstroidTypeError,
+                    AstroidIndexError,
+                    AstroidValueError,
+                    AttributeInferenceError,
+                    AttributeError,
+                ) as exc:
+                    raise InferenceError(node=self, context=context) from exc
+
+                # Prevent inferring if the inferred subscript
+                # is the same as the original subscripted object.
+                if self is assigned or isinstance(assigned, util.UninferableBase):
+                    yield util.Uninferable
+                    return None
+                yield from assigned.infer(context)
+                found_one = True
+
+        if found_one:
+            return InferenceErrorInfo(node=self, context=context)
+        return None
+
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(self, context: InferenceContext | None = None, **kwargs: Any):
+        return self._infer_subscript(context, **kwargs)
+
+    @decorators.raise_if_nothing_inferred
+    def infer_lhs(self, context: InferenceContext | None = None, **kwargs: Any):
+        return self._infer_subscript(context, **kwargs)
+
 
 class TryExcept(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
     """Class representing an :class:`ast.TryExcept` node.
@@ -3318,13 +3912,13 @@ def __init__(
             parent=parent,
         )
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[Tuple]]
+    assigned_stmts = protocols.sequence_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
 
-    infer_unary_op: ClassVar[InferUnaryOp[Tuple]]
-    infer_binary_op: ClassVar[InferBinaryOp[Tuple]]
+    infer_unary_op = protocols.tuple_infer_unary_op
+    infer_binary_op = protocols.tl_infer_binary_op
 
     def pytype(self) -> Literal["builtins.tuple"]:
         """Get the name of the type that this node represents.
@@ -3385,6 +3979,11 @@ def postinit(
         self.type_params = type_params
         self.value = value
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[TypeAlias]:
+        yield self
+
 
 class TypeVar(_base_nodes.AssignTypeNode):
     """Class representing a :class:`ast.TypeVar` node.
@@ -3421,6 +4020,11 @@ def postinit(self, *, name: AssignName, bound: NodeNG | None) -> None:
         self.name = name
         self.bound = bound
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[TypeVar]:
+        yield self
+
 
 class TypeVarTuple(_base_nodes.AssignTypeNode):
     """Class representing a :class:`ast.TypeVarTuple` node.
@@ -3455,8 +4059,21 @@ def __init__(
     def postinit(self, *, name: AssignName) -> None:
         self.name = name
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[TypeVarTuple]:
+        yield self
+
 
-class UnaryOp(NodeNG):
+UNARY_OP_METHOD = {
+    "+": "__pos__",
+    "-": "__neg__",
+    "~": "__invert__",
+    "not": None,  # XXX not '__nonzero__'
+}
+
+
+class UnaryOp(_base_nodes.OperatorNode):
     """Class representing an :class:`ast.UnaryOp` node.
 
     >>> import astroid
@@ -3495,19 +4112,14 @@ def __init__(
     def postinit(self, operand: NodeNG) -> None:
         self.operand = operand
 
-    # This is set by inference.py
-    _infer_unaryop: ClassVar[
-        InferBinaryOperation[UnaryOp, util.BadUnaryOperationMessage]
-    ]
-
     def type_errors(self, context: InferenceContext | None = None):
         """Get a list of type errors which can occur during inference.
 
-        Each TypeError is represented by a :class:`BadBinaryOperationMessage`,
+        Each TypeError is represented by a :class:`BadUnaryOperationMessage`,
         which holds the original exception.
 
         :returns: The list of possible type errors.
-        :rtype: list(BadBinaryOperationMessage)
+        :rtype: list(BadUnaryOperationMessage)
         """
         try:
             results = self._infer_unaryop(context=context)
@@ -3528,6 +4140,81 @@ def op_precedence(self):
 
         return super().op_precedence()
 
+    def _infer_unaryop(
+        self: nodes.UnaryOp, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[
+        InferenceResult | util.BadUnaryOperationMessage, None, InferenceErrorInfo
+    ]:
+        """Infer what an UnaryOp should return when evaluated."""
+        from astroid.nodes import ClassDef  # pylint: disable=import-outside-toplevel
+
+        for operand in self.operand.infer(context):
+            try:
+                yield operand.infer_unary_op(self.op)
+            except TypeError as exc:
+                # The operand doesn't support this operation.
+                yield util.BadUnaryOperationMessage(operand, self.op, exc)
+            except AttributeError as exc:
+                meth = UNARY_OP_METHOD[self.op]
+                if meth is None:
+                    # `not node`. Determine node's boolean
+                    # value and negate its result, unless it is
+                    # Uninferable, which will be returned as is.
+                    bool_value = operand.bool_value()
+                    if not isinstance(bool_value, util.UninferableBase):
+                        yield const_factory(not bool_value)
+                    else:
+                        yield util.Uninferable
+                else:
+                    if not isinstance(operand, (Instance, ClassDef)):
+                        # The operation was used on something which
+                        # doesn't support it.
+                        yield util.BadUnaryOperationMessage(operand, self.op, exc)
+                        continue
+
+                    try:
+                        try:
+                            methods = dunder_lookup.lookup(operand, meth)
+                        except AttributeInferenceError:
+                            yield util.BadUnaryOperationMessage(operand, self.op, exc)
+                            continue
+
+                        meth = methods[0]
+                        inferred = next(meth.infer(context=context), None)
+                        if (
+                            isinstance(inferred, util.UninferableBase)
+                            or not inferred.callable()
+                        ):
+                            continue
+
+                        context = copy_context(context)
+                        context.boundnode = operand
+                        context.callcontext = CallContext(args=[], callee=inferred)
+
+                        call_results = inferred.infer_call_result(self, context=context)
+                        result = next(call_results, None)
+                        if result is None:
+                            # Failed to infer, return the same type.
+                            yield operand
+                        else:
+                            yield result
+                    except AttributeInferenceError as inner_exc:
+                        # The unary operation special method was not found.
+                        yield util.BadUnaryOperationMessage(operand, self.op, inner_exc)
+                    except InferenceError:
+                        yield util.Uninferable
+
+    @decorators.raise_if_nothing_inferred
+    @decorators.path_wrapper
+    def _infer(
+        self: nodes.UnaryOp, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[InferenceResult, None, InferenceErrorInfo]:
+        """Infer what an UnaryOp should return when evaluated."""
+        yield from self._filter_operation_errors(
+            self._infer_unaryop, context, util.BadUnaryOperationMessage
+        )
+        return InferenceErrorInfo(node=self, context=context)
+
 
 class While(_base_nodes.MultiLineWithElseBlockNode, _base_nodes.Statement):
     """Class representing an :class:`ast.While` node.
@@ -3671,7 +4358,7 @@ def postinit(
             self.body = body
         self.type_annotation = type_annotation
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[With]]
+    assigned_stmts = protocols.with_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -3944,7 +4631,7 @@ def postinit(self, target: NodeNG, value: NodeNG) -> None:
         self.target = target
         self.value = value
 
-    assigned_stmts: ClassVar[AssignedStmtsCall[NamedExpr]]
+    assigned_stmts = protocols.named_expr_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -4345,17 +5032,7 @@ def postinit(
         self.patterns = patterns
         self.rest = rest
 
-    assigned_stmts: ClassVar[
-        Callable[
-            [
-                MatchMapping,
-                AssignName,
-                InferenceContext | None,
-                None,
-            ],
-            Generator[NodeNG, None, None],
-        ]
-    ]
+    assigned_stmts = protocols.match_mapping_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -4452,17 +5129,7 @@ def __init__(
     def postinit(self, *, name: AssignName | None) -> None:
         self.name = name
 
-    assigned_stmts: ClassVar[
-        Callable[
-            [
-                MatchStar,
-                AssignName,
-                InferenceContext | None,
-                None,
-            ],
-            Generator[NodeNG, None, None],
-        ]
-    ]
+    assigned_stmts = protocols.match_star_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
@@ -4523,17 +5190,7 @@ def postinit(
         self.pattern = pattern
         self.name = name
 
-    assigned_stmts: ClassVar[
-        Callable[
-            [
-                MatchAs,
-                AssignName,
-                InferenceContext | None,
-                None,
-            ],
-            Generator[NodeNG, None, None],
-        ]
-    ]
+    assigned_stmts = protocols.match_as_assigned_stmts
     """Returns the assigned statement (non inferred) according to the assignment type.
     See astroid/protocols.py for actual implementation.
     """
diff --git a/astroid/nodes/scoped_nodes/mixin.py b/astroid/nodes/scoped_nodes/mixin.py
index fa6aad412e..da03e06796 100644
--- a/astroid/nodes/scoped_nodes/mixin.py
+++ b/astroid/nodes/scoped_nodes/mixin.py
@@ -9,7 +9,7 @@
 from typing import TYPE_CHECKING, TypeVar, overload
 
 from astroid.filter_statements import _filter_stmts
-from astroid.nodes import node_classes, scoped_nodes
+from astroid.nodes import _base_nodes, node_classes, scoped_nodes
 from astroid.nodes.scoped_nodes.utils import builtin_lookup
 from astroid.typing import InferenceResult, SuccessfulInferenceResult
 
@@ -19,7 +19,7 @@
 _T = TypeVar("_T")
 
 
-class LocalsDictNodeNG(node_classes.LookupMixIn):
+class LocalsDictNodeNG(_base_nodes.LookupMixIn):
     """this class provides locals handling common to Module, FunctionDef
     and ClassDef nodes, including a dict like interface for direct access
     to locals information
@@ -52,7 +52,7 @@ def scope(self: _T) -> _T:
         return self
 
     def scope_lookup(
-        self, node: node_classes.LookupMixIn, name: str, offset: int = 0
+        self, node: _base_nodes.LookupMixIn, name: str, offset: int = 0
     ) -> tuple[LocalsDictNodeNG, list[nodes.NodeNG]]:
         """Lookup where the given variable is assigned.
 
@@ -70,7 +70,7 @@ def scope_lookup(
         raise NotImplementedError
 
     def _scope_lookup(
-        self, node: node_classes.LookupMixIn, name: str, offset: int = 0
+        self, node: _base_nodes.LookupMixIn, name: str, offset: int = 0
     ) -> tuple[LocalsDictNodeNG, list[nodes.NodeNG]]:
         """XXX method for interfacing the scope lookup"""
         try:
diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
index 94f4c53eeb..81a0c0e230 100644
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -16,9 +16,9 @@
 import warnings
 from collections.abc import Generator, Iterable, Iterator, Sequence
 from functools import cached_property, lru_cache
-from typing import TYPE_CHECKING, ClassVar, Literal, NoReturn, TypeVar
+from typing import TYPE_CHECKING, Any, ClassVar, Literal, NoReturn, TypeVar
 
-from astroid import bases, util
+from astroid import bases, protocols, util
 from astroid.const import IS_PYPY, PY38, PY39_PLUS, PYPY_7_3_11_PLUS
 from astroid.context import (
     CallContext,
@@ -44,10 +44,16 @@
 from astroid.nodes.scoped_nodes.mixin import ComprehensionScope, LocalsDictNodeNG
 from astroid.nodes.scoped_nodes.utils import builtin_lookup
 from astroid.nodes.utils import Position
-from astroid.typing import InferBinaryOp, InferenceResult, SuccessfulInferenceResult
+from astroid.typing import (
+    InferBinaryOp,
+    InferenceErrorInfo,
+    InferenceResult,
+    SuccessfulInferenceResult,
+)
 
 if TYPE_CHECKING:
-    from astroid import nodes
+    from astroid import nodes, objects
+    from astroid.nodes._base_nodes import LookupMixIn
 
 
 ITER_METHODS = ("__iter__", "__getitem__")
@@ -285,7 +291,7 @@ def block_range(self, lineno: int) -> tuple[int, int]:
         return self.fromlineno, self.tolineno
 
     def scope_lookup(
-        self, node: node_classes.LookupMixIn, name: str, offset: int = 0
+        self, node: LookupMixIn, name: str, offset: int = 0
     ) -> tuple[LocalsDictNodeNG, list[node_classes.NodeNG]]:
         """Lookup where the given variable is assigned.
 
@@ -578,6 +584,11 @@ def frame(self: _T, *, future: Literal[None, True] = None) -> _T:
         """
         return self
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[Module]:
+        yield self
+
 
 class GeneratorExp(ComprehensionScope):
     """Class representing an :class:`ast.GeneratorExp` node.
@@ -961,7 +972,7 @@ def infer_call_result(
         return self.body.infer(context)
 
     def scope_lookup(
-        self, node: node_classes.LookupMixIn, name: str, offset: int = 0
+        self, node: LookupMixIn, name: str, offset: int = 0
     ) -> tuple[LocalsDictNodeNG, list[NodeNG]]:
         """Lookup where the given names is assigned.
 
@@ -1025,6 +1036,11 @@ def getattr(
             return found_attrs
         raise AttributeInferenceError(target=self, attribute=name)
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[Lambda]:
+        yield self
+
 
 class FunctionDef(
     _base_nodes.MultiLineBlockNode,
@@ -1469,6 +1485,44 @@ def is_generator(self) -> bool:
         """
         return bool(next(self._get_yield_nodes_skip_lambdas(), False))
 
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Generator[objects.Property | FunctionDef, None, InferenceErrorInfo]:
+        from astroid import objects  # pylint: disable=import-outside-toplevel
+
+        if not self.decorators or not bases._is_property(self):
+            yield self
+            return InferenceErrorInfo(node=self, context=context)
+
+        # When inferring a property, we instantiate a new `objects.Property` object,
+        # which in turn, because it inherits from `FunctionDef`, sets itself in the locals
+        # of the wrapping frame. This means that every time we infer a property, the locals
+        # are mutated with a new instance of the property. To avoid this, we detect this
+        # scenario and avoid passing the `parent` argument to the constructor.
+        parent_frame = self.parent.frame()
+        property_already_in_parent_locals = self.name in parent_frame.locals and any(
+            isinstance(val, objects.Property) for val in parent_frame.locals[self.name]
+        )
+        # We also don't want to pass parent if the definition is within a Try node
+        if isinstance(
+            self.parent,
+            (node_classes.TryExcept, node_classes.TryFinally, node_classes.If),
+        ):
+            property_already_in_parent_locals = True
+
+        prop_func = objects.Property(
+            function=self,
+            name=self.name,
+            lineno=self.lineno,
+            parent=self.parent if not property_already_in_parent_locals else None,
+            col_offset=self.col_offset,
+        )
+        if property_already_in_parent_locals:
+            prop_func.parent = self.parent
+        prop_func.postinit(body=[], args=self.args, doc_node=self.doc_node)
+        yield prop_func
+        return InferenceErrorInfo(node=self, context=context)
+
     def infer_yield_result(self, context: InferenceContext | None = None):
         """Infer what the function yields when called
 
@@ -1600,7 +1654,7 @@ def get_children(self):
         yield from self.body
 
     def scope_lookup(
-        self, node: node_classes.LookupMixIn, name: str, offset: int = 0
+        self, node: LookupMixIn, name: str, offset: int = 0
     ) -> tuple[LocalsDictNodeNG, list[nodes.NodeNG]]:
         """Lookup where the given name is assigned."""
         if name == "__class__":
@@ -1848,7 +1902,9 @@ def __init__(
         for local_name, node in self.implicit_locals():
             self.add_local_node(node, local_name)
 
-    infer_binary_op: ClassVar[InferBinaryOp[ClassDef]]
+    infer_binary_op: ClassVar[
+        InferBinaryOp[ClassDef]
+    ] = protocols.instance_class_infer_binary_op
 
     def implicit_parameters(self) -> Literal[1]:
         return 1
@@ -2080,7 +2136,7 @@ def infer_call_result(
             yield self.instantiate_class()
 
     def scope_lookup(
-        self, node: node_classes.LookupMixIn, name: str, offset: int = 0
+        self, node: LookupMixIn, name: str, offset: int = 0
     ) -> tuple[LocalsDictNodeNG, list[nodes.NodeNG]]:
         """Lookup where the given name is assigned.
 
@@ -2875,3 +2931,8 @@ def frame(self: _T, *, future: Literal[None, True] = None) -> _T:
         :returns: The node itself.
         """
         return self
+
+    def _infer(
+        self, context: InferenceContext | None = None, **kwargs: Any
+    ) -> Iterator[ClassDef]:
+        yield self
diff --git a/astroid/protocols.py b/astroid/protocols.py
index e3b89b7ef7..f37c9c3256 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -12,9 +12,9 @@
 import itertools
 import operator as operator_mod
 from collections.abc import Callable, Generator, Iterator, Sequence
-from typing import Any, TypeVar
+from typing import TYPE_CHECKING, Any, TypeVar
 
-from astroid import arguments, bases, decorators, helpers, nodes, objects, util
+from astroid import bases, decorators, nodes, util
 from astroid.const import Context
 from astroid.context import InferenceContext, copy_context
 from astroid.exceptions import (
@@ -31,47 +31,11 @@
     SuccessfulInferenceResult,
 )
 
-_TupleListNodeT = TypeVar("_TupleListNodeT", nodes.Tuple, nodes.List)
-
-
-def _reflected_name(name) -> str:
-    return "__r" + name[2:]
-
-
-def _augmented_name(name) -> str:
-    return "__i" + name[2:]
-
+if TYPE_CHECKING:
+    _TupleListNodeT = TypeVar("_TupleListNodeT", nodes.Tuple, nodes.List)
 
 _CONTEXTLIB_MGR = "contextlib.contextmanager"
-BIN_OP_METHOD = {
-    "+": "__add__",
-    "-": "__sub__",
-    "/": "__truediv__",
-    "//": "__floordiv__",
-    "*": "__mul__",
-    "**": "__pow__",
-    "%": "__mod__",
-    "&": "__and__",
-    "|": "__or__",
-    "^": "__xor__",
-    "<<": "__lshift__",
-    ">>": "__rshift__",
-    "@": "__matmul__",
-}
-
-REFLECTED_BIN_OP_METHOD = {
-    key: _reflected_name(value) for (key, value) in BIN_OP_METHOD.items()
-}
-AUGMENTED_OP_METHOD = {
-    key + "=": _augmented_name(value) for (key, value) in BIN_OP_METHOD.items()
-}
 
-UNARY_OP_METHOD = {
-    "+": "__pos__",
-    "-": "__neg__",
-    "~": "__invert__",
-    "not": None,  # XXX not '__nonzero__'
-}
 _UNARY_OPERATORS: dict[str, Callable[[Any], Any]] = {
     "+": operator_mod.pos,
     "-": operator_mod.neg,
@@ -93,11 +57,25 @@ def _infer_unary_op(obj: Any, op: str) -> ConstFactoryResult:
     return nodes.const_factory(value)
 
 
-nodes.Tuple.infer_unary_op = lambda self, op: _infer_unary_op(tuple(self.elts), op)
-nodes.List.infer_unary_op = lambda self, op: _infer_unary_op(self.elts, op)
-nodes.Set.infer_unary_op = lambda self, op: _infer_unary_op(set(self.elts), op)
-nodes.Const.infer_unary_op = lambda self, op: _infer_unary_op(self.value, op)
-nodes.Dict.infer_unary_op = lambda self, op: _infer_unary_op(dict(self.items), op)
+def tuple_infer_unary_op(self, op):
+    return _infer_unary_op(tuple(self.elts), op)
+
+
+def list_infer_unary_op(self, op):
+    return _infer_unary_op(self.elts, op)
+
+
+def set_infer_unary_op(self, op):
+    return _infer_unary_op(set(self.elts), op)
+
+
+def const_infer_unary_op(self, op):
+    return _infer_unary_op(self.value, op)
+
+
+def dict_infer_unary_op(self, op):
+    return _infer_unary_op(dict(self.items), op)
+
 
 # Binary operations
 
@@ -157,15 +135,14 @@ def const_infer_binary_op(
         yield not_implemented
 
 
-nodes.Const.infer_binary_op = const_infer_binary_op
-
-
 def _multiply_seq_by_int(
     self: _TupleListNodeT,
     opnode: nodes.AugAssign | nodes.BinOp,
     other: nodes.Const,
     context: InferenceContext,
 ) -> _TupleListNodeT:
+    from astroid import helpers  # pylint: disable=import-outside-toplevel
+
     node = self.__class__(parent=opnode)
     filtered_elts = (
         helpers.safe_infer(elt, context) or util.Uninferable
@@ -205,6 +182,8 @@ def tl_infer_binary_op(
     or list. This refers to the left-hand side of the operation, so:
     'tuple() + 1' or '[] + A()'
     """
+    from astroid import helpers  # pylint: disable=import-outside-toplevel
+
     # For tuples and list the boundnode is no longer the tuple or list instance
     context.boundnode = None
     not_implemented = nodes.Const(NotImplemented)
@@ -233,13 +212,9 @@ def tl_infer_binary_op(
         yield not_implemented
 
 
-nodes.Tuple.infer_binary_op = tl_infer_binary_op
-nodes.List.infer_binary_op = tl_infer_binary_op
-
-
 @decorators.yes_if_nothing_inferred
 def instance_class_infer_binary_op(
-    self: bases.Instance | nodes.ClassDef,
+    self: nodes.ClassDef,
     opnode: nodes.AugAssign | nodes.BinOp,
     operator: str,
     other: InferenceResult,
@@ -249,12 +224,8 @@ def instance_class_infer_binary_op(
     return method.infer_call_result(self, context)
 
 
-bases.Instance.infer_binary_op = instance_class_infer_binary_op
-nodes.ClassDef.infer_binary_op = instance_class_infer_binary_op
-
-
 # assignment ##################################################################
-
+# pylint: disable-next=pointless-string-statement
 """The assigned_stmts method is responsible to return the assigned statement
 (e.g. not inferred) according to the assignment type.
 
@@ -337,10 +308,6 @@ def for_assigned_stmts(
     }
 
 
-nodes.For.assigned_stmts = for_assigned_stmts
-nodes.Comprehension.assigned_stmts = for_assigned_stmts
-
-
 def sequence_assigned_stmts(
     self: nodes.Tuple | nodes.List,
     node: node_classes.AssignedStmtsPossibleNode = None,
@@ -365,10 +332,6 @@ def sequence_assigned_stmts(
     )
 
 
-nodes.Tuple.assigned_stmts = sequence_assigned_stmts
-nodes.List.assigned_stmts = sequence_assigned_stmts
-
-
 def assend_assigned_stmts(
     self: nodes.AssignName | nodes.AssignAttr,
     node: node_classes.AssignedStmtsPossibleNode = None,
@@ -378,15 +341,13 @@ def assend_assigned_stmts(
     return self.parent.assigned_stmts(node=self, context=context)
 
 
-nodes.AssignName.assigned_stmts = assend_assigned_stmts
-nodes.AssignAttr.assigned_stmts = assend_assigned_stmts
-
-
 def _arguments_infer_argname(
     self, name: str | None, context: InferenceContext
 ) -> Generator[InferenceResult, None, None]:
     # arguments information may be missing, in which case we can't do anything
     # more
+    from astroid import arguments  # pylint: disable=import-outside-toplevel
+
     if not (self.arguments or self.vararg or self.kwarg):
         yield util.Uninferable
         return
@@ -449,6 +410,8 @@ def arguments_assigned_stmts(
     context: InferenceContext | None = None,
     assign_path: list[int] | None = None,
 ) -> Any:
+    from astroid import arguments  # pylint: disable=import-outside-toplevel
+
     try:
         node_name = node.name  # type: ignore[union-attr]
     except AttributeError:
@@ -472,9 +435,6 @@ def arguments_assigned_stmts(
     return _arguments_infer_argname(self, node_name, context)
 
 
-nodes.Arguments.assigned_stmts = arguments_assigned_stmts
-
-
 @decorators.raise_if_nothing_inferred
 def assign_assigned_stmts(
     self: nodes.AugAssign | nodes.Assign | nodes.AnnAssign,
@@ -510,11 +470,6 @@ def assign_annassigned_stmts(
             yield inferred
 
 
-nodes.Assign.assigned_stmts = assign_assigned_stmts
-nodes.AnnAssign.assigned_stmts = assign_annassigned_stmts
-nodes.AugAssign.assigned_stmts = assign_assigned_stmts
-
-
 def _resolve_assignment_parts(parts, assign_path, context):
     """Recursive function to resolve multiple assignments."""
     assign_path = assign_path[:]
@@ -562,6 +517,8 @@ def excepthandler_assigned_stmts(
     context: InferenceContext | None = None,
     assign_path: list[int] | None = None,
 ) -> Any:
+    from astroid import objects  # pylint: disable=import-outside-toplevel
+
     for assigned in node_classes.unpack_infer(self.type):
         if isinstance(assigned, nodes.ClassDef):
             assigned = objects.ExceptionInstance(assigned)
@@ -575,9 +532,6 @@ def excepthandler_assigned_stmts(
     }
 
 
-nodes.ExceptHandler.assigned_stmts = excepthandler_assigned_stmts
-
-
 def _infer_context_manager(self, mgr, context):
     try:
         inferred = next(mgr.infer(context=context))
@@ -696,9 +650,6 @@ def __enter__(self):
     }
 
 
-nodes.With.assigned_stmts = with_assigned_stmts
-
-
 @decorators.raise_if_nothing_inferred
 def named_expr_assigned_stmts(
     self: nodes.NamedExpr,
@@ -718,9 +669,6 @@ def named_expr_assigned_stmts(
         )
 
 
-nodes.NamedExpr.assigned_stmts = named_expr_assigned_stmts
-
-
 @decorators.yes_if_nothing_inferred
 def starred_assigned_stmts(  # noqa: C901
     self: nodes.Starred,
@@ -918,9 +866,6 @@ def _determine_starred_iteration_lookups(
         yield util.Uninferable
 
 
-nodes.Starred.assigned_stmts = starred_assigned_stmts
-
-
 @decorators.yes_if_nothing_inferred
 def match_mapping_assigned_stmts(
     self: nodes.MatchMapping,
@@ -935,9 +880,6 @@ def match_mapping_assigned_stmts(
     yield
 
 
-nodes.MatchMapping.assigned_stmts = match_mapping_assigned_stmts
-
-
 @decorators.yes_if_nothing_inferred
 def match_star_assigned_stmts(
     self: nodes.MatchStar,
@@ -952,9 +894,6 @@ def match_star_assigned_stmts(
     yield
 
 
-nodes.MatchStar.assigned_stmts = match_star_assigned_stmts
-
-
 @decorators.yes_if_nothing_inferred
 def match_as_assigned_stmts(
     self: nodes.MatchAs,
@@ -971,6 +910,3 @@ def match_as_assigned_stmts(
         and self.pattern is None
     ):
         yield self.parent.parent.subject
-
-
-nodes.MatchAs.assigned_stmts = match_as_assigned_stmts
diff --git a/doc/api/base_nodes.rst b/doc/api/base_nodes.rst
index 6253ce5ce5..14f7ab1071 100644
--- a/doc/api/base_nodes.rst
+++ b/doc/api/base_nodes.rst
@@ -12,7 +12,7 @@ These are abstract node classes that :ref:`other nodes <nodes>` inherit from.
    astroid.nodes._base_nodes.FilterStmtsBaseNode
    astroid.nodes._base_nodes.ImportNode
      astroid.nodes.LocalsDictNodeNG
-   astroid.nodes.node_classes.LookupMixIn
+   astroid.nodes._base_nodes.LookupMixIn
    astroid.nodes.NodeNG
    astroid.nodes._base_nodes.ParentAssignNode
    astroid.nodes.Statement
@@ -33,7 +33,7 @@ These are abstract node classes that :ref:`other nodes <nodes>` inherit from.
 
 .. autoclass:: astroid.nodes.LocalsDictNodeNG
 
-.. autoclass:: astroid.nodes.node_classes.LookupMixIn
+.. autoclass:: astroid.nodes._base_nodes.LookupMixIn
 
 .. autoclass:: astroid.nodes.NodeNG
 


Test changes:
diff --git a/tests/test_inference.py b/tests/test_inference.py
index 96778b89d3..03d0e2c744 100644
--- a/tests/test_inference.py
+++ b/tests/test_inference.py
@@ -41,7 +41,6 @@
     InferenceError,
     NotFoundError,
 )
-from astroid.inference import infer_end as inference_infer_end
 from astroid.objects import ExceptionInstance
 
 from . import resources
@@ -71,7 +70,7 @@ def infer_default(self: Any, *args: InferenceContext) -> None:
             raise InferenceError
 
         infer_default = decoratorsmod.path_wrapper(infer_default)
-        infer_end = decoratorsmod.path_wrapper(inference_infer_end)
+        infer_end = decoratorsmod.path_wrapper(Slice._infer)
         with self.assertRaises(InferenceError):
             next(infer_default(1))
         self.assertEqual(next(infer_end(1)), 1)
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 56b09945ba..6455a6e5d3 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -409,7 +409,7 @@ def test_borg(self) -> None:
 class ClearCacheTest(unittest.TestCase):
     def test_clear_cache_clears_other_lru_caches(self) -> None:
         lrus = (
-            astroid.nodes.node_classes.LookupMixIn.lookup,
+            astroid.nodes._base_nodes.LookupMixIn.lookup,
             astroid.modutils._cache_normalize_path_,
             util.is_namespace,
             astroid.interpreter.objectmodel.ObjectModel.attributes,
diff --git a/tests/test_nodes.py b/tests/test_nodes.py
index 7a4990cddb..41429fc5ab 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -1916,8 +1916,7 @@ def return_from_match(x):
     [
         node
         for node in astroid.nodes.ALL_NODE_CLASSES
-        if node.__name__
-        not in ["_BaseContainer", "BaseContainer", "NodeNG", "const_factory"]
+        if node.__name__ not in ["BaseContainer", "NodeNG", "const_factory"]
     ],
 )
 @pytest.mark.filterwarnings("error")