Skip to content

Commit 6245869

Browse files
committed
Move the AST operator tables out of guarded_eval
`IPython.terminal.shortcuts.filters` evaluates the filter expressions of the built-in key bindings at import time, and needed three names from `IPython.core.guarded_eval` to do it: `BINARY_OP_DUNDERS`, `UNARY_OP_DUNDERS` and `_find_dunder`. Importing `guarded_eval` for them cost ~5ms and pulled in `typing_extensions` on every terminal start, for two dicts and a six-line helper that only need `ast`. Move those (and `COMP_OP_DUNDERS`, which belongs with them) to `IPython.core._dunder_ops`, and re-export them from `guarded_eval`, which stays their documented home.
1 parent ebc8f60 commit 6245869

3 files changed

Lines changed: 72 additions & 44 deletions

File tree

IPython/core/_dunder_ops.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Mapping from AST operator nodes to the dunder methods that implement them.
2+
3+
This lives in its own module, rather than in `IPython.core.guarded_eval` where
4+
it is mostly used, so that the terminal shortcut filters can resolve operators
5+
in a filter expression without importing the whole of `guarded_eval` -- and
6+
with it `typing_extensions`, `dataclasses` and `inspect` -- on every startup.
7+
8+
The names are re-exported from `IPython.core.guarded_eval`, which remains
9+
their documented home.
10+
"""
11+
12+
import ast
13+
14+
__all__ = [
15+
"BINARY_OP_DUNDERS",
16+
"COMP_OP_DUNDERS",
17+
"UNARY_OP_DUNDERS",
18+
]
19+
20+
BINARY_OP_DUNDERS: dict[type[ast.operator], tuple[str]] = {
21+
ast.Add: ("__add__",),
22+
ast.Sub: ("__sub__",),
23+
ast.Mult: ("__mul__",),
24+
ast.Div: ("__truediv__",),
25+
ast.FloorDiv: ("__floordiv__",),
26+
ast.Mod: ("__mod__",),
27+
ast.Pow: ("__pow__",),
28+
ast.LShift: ("__lshift__",),
29+
ast.RShift: ("__rshift__",),
30+
ast.BitOr: ("__or__",),
31+
ast.BitXor: ("__xor__",),
32+
ast.BitAnd: ("__and__",),
33+
ast.MatMult: ("__matmul__",),
34+
}
35+
36+
COMP_OP_DUNDERS: dict[type[ast.cmpop], tuple[str, ...]] = {
37+
ast.Eq: ("__eq__",),
38+
ast.NotEq: ("__ne__", "__eq__"),
39+
ast.Lt: ("__lt__", "__gt__"),
40+
ast.LtE: ("__le__", "__ge__"),
41+
ast.Gt: ("__gt__", "__lt__"),
42+
ast.GtE: ("__ge__", "__le__"),
43+
ast.In: ("__contains__",),
44+
# Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
45+
}
46+
47+
UNARY_OP_DUNDERS: dict[type[ast.unaryop], tuple[str, ...]] = {
48+
ast.USub: ("__neg__",),
49+
ast.UAdd: ("__pos__",),
50+
# we have to check both __inv__ and __invert__!
51+
ast.Invert: ("__invert__", "__inv__"),
52+
ast.Not: ("__not__",),
53+
}
54+
55+
56+
def _find_dunder(node_op, dunders) -> tuple[str, ...] | None:
57+
dunder = None
58+
for op, candidate_dunder in dunders.items():
59+
if isinstance(node_op, op):
60+
dunder = candidate_dunder
61+
return dunder

IPython/core/guarded_eval.py

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
from dataclasses import dataclass, field
2727
from types import MethodDescriptorType, ModuleType, MethodType
2828

29+
from IPython.core._dunder_ops import (
30+
BINARY_OP_DUNDERS,
31+
COMP_OP_DUNDERS,
32+
UNARY_OP_DUNDERS,
33+
_find_dunder,
34+
)
2935
from IPython.utils.decorators import undoc
3036

3137
import types
@@ -465,41 +471,6 @@ def guarded_eval(code: str, context: EvaluationContext):
465471
return eval_node(node, context)
466472

467473

468-
BINARY_OP_DUNDERS: dict[type[ast.operator], tuple[str]] = {
469-
ast.Add: ("__add__",),
470-
ast.Sub: ("__sub__",),
471-
ast.Mult: ("__mul__",),
472-
ast.Div: ("__truediv__",),
473-
ast.FloorDiv: ("__floordiv__",),
474-
ast.Mod: ("__mod__",),
475-
ast.Pow: ("__pow__",),
476-
ast.LShift: ("__lshift__",),
477-
ast.RShift: ("__rshift__",),
478-
ast.BitOr: ("__or__",),
479-
ast.BitXor: ("__xor__",),
480-
ast.BitAnd: ("__and__",),
481-
ast.MatMult: ("__matmul__",),
482-
}
483-
484-
COMP_OP_DUNDERS: dict[type[ast.cmpop], tuple[str, ...]] = {
485-
ast.Eq: ("__eq__",),
486-
ast.NotEq: ("__ne__", "__eq__"),
487-
ast.Lt: ("__lt__", "__gt__"),
488-
ast.LtE: ("__le__", "__ge__"),
489-
ast.Gt: ("__gt__", "__lt__"),
490-
ast.GtE: ("__ge__", "__le__"),
491-
ast.In: ("__contains__",),
492-
# Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
493-
}
494-
495-
UNARY_OP_DUNDERS: dict[type[ast.unaryop], tuple[str, ...]] = {
496-
ast.USub: ("__neg__",),
497-
ast.UAdd: ("__pos__",),
498-
# we have to check both __inv__ and __invert__!
499-
ast.Invert: ("__invert__", "__inv__"),
500-
ast.Not: ("__not__",),
501-
}
502-
503474
GENERIC_CONTAINER_TYPES = (dict, list, set, tuple, frozenset)
504475

505476

@@ -535,14 +506,6 @@ def _ipython_key_completions_(self):
535506
return self.items.keys()
536507

537508

538-
def _find_dunder(node_op, dunders) -> tuple[str, ...] | None:
539-
dunder = None
540-
for op, candidate_dunder in dunders.items():
541-
if isinstance(node_op, op):
542-
dunder = candidate_dunder
543-
return dunder
544-
545-
546509
def get_policy(context: EvaluationContext) -> EvaluationPolicy:
547510
policy = copy(EVALUATION_POLICIES[context.evaluation])
548511

IPython/terminal/shortcuts/filters.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626
)
2727
from prompt_toolkit.layout.layout import FocusableElement
2828

29+
from IPython.core._dunder_ops import (
30+
BINARY_OP_DUNDERS,
31+
UNARY_OP_DUNDERS,
32+
_find_dunder,
33+
)
2934
from IPython.core.getipython import get_ipython
30-
from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
3135
from IPython.terminal.shortcuts import auto_suggest
3236
from IPython.utils.decorators import undoc
3337

0 commit comments

Comments
 (0)