10000 Autobalance by jb-leger · Pull Request #14081 · ipython/ipython · GitHub
[go: up one dir, main page]

Skip to content

Autobalance #14081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 99 additions & 17 deletions IPython/core/inputtransformer2.py
8000
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
import tokenize
from typing import List, Tuple, Optional, Any
import warnings
import io

# only for type checking, do not import this, it lead to a circular
# import
if False:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do

from typing import TYPE_CHECKING

...

if TYPE_CHECKING:

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 10f3017.

from IPython.core.interactiveshell import InteractiveShell

_indent_re = re.compile(r'^[ \t]+')

def leading_empty_lines(lines):
def leading_empty_lines(lines, **kwargs):
"""Remove leading empty lines

If the leading lines are empty or contain only whitespace, they will be
Expand All @@ -32,7 +38,8 @@ def leading_empty_lines(lines):
return lines[i:]
return lines

def leading_indent(lines):

def leading_indent(lines, **kwargs):
"""Remove leading indentation.

If the first line starts with a spaces or tabs, the same whitespace will be
Expand Down Expand Up @@ -77,7 +84,7 @@ def __init__(self, prompt_re, initial_re=None):
def _strip(self, lines):
return [self.prompt_re.sub('', l, count=1) for l in lines]

def __call__(self, lines):
def __call__(self, lines, **kwargs):
if not lines:
return lines
if self.initial_re.match(lines[0]) or \
Expand Down Expand Up @@ -115,7 +122,7 @@ def __call__(self, lines):
)


def cell_magic(lines):
def cell_magic(lines, **kwargs):
if not lines or not lines[0].startswith('%%'):
return lines
if re.match(r'%%\w+\?', lines[0]):
Expand Down Expand Up @@ -183,6 +190,74 @@ def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
+ [parts[-1].rstrip()]) # Strip newline from last line

_AUTOBALANCE_MARKERS = (("(", ")"), ("{", "}"), ("[", "]"))
_AUTOBALANCE_OPEN = {x: i for i, (x, _) in enumerate(_AUTOBALANCE_MARKERS)}
_AUTOBALANCE_CLOSE = {x: i for i, (_, x) in enumerate(_AUTOBALANCE_MARKERS)}


def _autobalance_line(inpt_code):
"""Add necessary [{( in the begin of the expr and )}] at the end to balance."""

tokens = tokenize.generate_tokens(io.StringIO(inpt_code).readline)
closed_without_open = []
opened_without_close = []
begin_expr = 0
while True:
try:
token = next(tokens)
except (StopIteration, tokenize.TokenError):
break
if token.type == tokenize.OP:
if token.string in _AUTOBALANCE_OPEN:
opened_without_close.append(_AUTOBALANCE_OPEN[token.string])
elif token.string in _AUTOBALANCE_CLOSE:
if opened_without_close:
last_opened = opened_without_close.pop(-1)
if last_opened != _AUTOBALANCE_CLOSE[token.string]:
# can not be balanced only adding in the begin and end
# of expr
return (False, inpt_code)
else:
closed_without_open.append(_AUTOBALANCE_CLOSE[token.string])
elif token.string == "=":
if not opened_without_close:
# this is a assigment
begin_expr = token.end[1]

if not opened_without_close and not closed_without_open:
# no needed changes
return (False, inpt_code)

new_code = (
inpt_code[:begin_expr]
+ (" " if begin_expr else "")
+ "".join(_AUTOBALANCE_MARKERS[k][0] for k in closed_without_open[::-1])
+ inpt_code[begin_expr:].strip()
+ "".join(_AUTOBALANCE_MARKERS[k][1] for k in opened_without_close)
)
return (True, new_code)


def autobalance(lines, **kwargs):
"""For single line cell, add necessary [{( and )}] to balance."""

shell = kwargs.get("shell")
display = kwargs.get("display", False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you are adding kwargs everywhere to get shell and display here, but that will break API of external transforms that don't take a kwargs. So we may wan to have a

class Autobalancer:

    def __init__(self, shell):
        ....

    def __call__(...):



Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good idea, thank you. This solution is a lot less intrusive. Done in 79a23bf.


if len(lines) != 1:
# apply only for single line cell
return lines

if shell is None or not shell.autobalance:
return lines

modified, new_line = _autobalance_line(lines[0])
if modified and display:
shell.auto_rewrite_input(new_line)
return [new_line]
return lines


class TokenTransformBase:
"""Base class for transformations which examine tokens.

Expand Down Expand Up @@ -248,7 +323,7 @@ def find(cls, tokens_by_line):
and (line[assign_ix+2].type == tokenize.NAME):
return cls(line[assign_ix+1].start)

def transform(self, lines: List[str]):
def transform(self, lines: List[str], **kwargs):
"""Transform a magic assignment found by the ``find()`` classmethod.
"""
start_line, start_col = self.start_line, self.start_col
Expand Down Expand Up @@ -287,7 +362,7 @@ def find(cls, tokens_by_line):
break
ix += 1

def transform(self, lines: List[str]):
def transform(self, lines: List[str], **kwargs):
"""Transform a system assignment found by the ``find()`` classmethod.
"""
start_line, start_col = self.start_line, self.start_col
Expand Down Expand Up @@ -404,7 +479,7 @@ def find(cls, tokens_by_line):
if line[ix].string in ESCAPE_SINGLES:
return cls(line[ix].start)

def transform(self, lines):
def transform(self, lines, **kwargs):
"""Transform an escaped line found by the ``find()`` classmethod.
"""
start_line, start_col = self.start_line, self.start_col
Expand Down Expand Up @@ -465,7 +540,7 @@ def find(cls, tokens_by_line):
ix += 1
return cls(line[ix].start, line[-2].start)

def transform(self, lines):
def transform(self, lines, **kwargs):
"""Transform a help command found by the ``find()`` classmethod.
"""

Expand Down Expand Up @@ -570,7 +645,11 @@ class TransformerManager:
The key methods for external use are ``transform_cell()``
and ``check_complete()``.
"""
def __init__(self):

shell: Optional["InteractiveShell"]

def __init__(self, shell=None):
self.shell = shell
self.cleanup_transforms = [
leading_empty_lines,
leading_indent,
Expand All @@ -579,6 +658,7 @@ def __init__(self):
]
self.line_transforms = [
cell_magic,
autobalance,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here you should be able to have Autobalancer(shell)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuation of last thread. Done in 79a23bf.

]
self.token_transformers = [
MagicAssign,
Expand All @@ -587,7 +667,7 @@ def __init__(self):
HelpEnd,
]

def do_one_token_transform(self, lines):
def do_one_token_transform(self, lines, display=False):
"""Find and run the transform earliest in the code.

Returns (changed, lines).
Expand All @@ -614,14 +694,16 @@ def do_one_token_transform(self, lines):
ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
for transformer in ordered_transformers:
try:
return True, transformer.transform(lines)
return True, transformer.transform(
lines, shell=self.shell, display=display
)
except SyntaxError:
pass
return False, lines

def do_token_transforms(self, lines):
def do_token_transforms(self, lines, display=False):
for _ in range(TRANSFORM_LOOP_LIMIT):
changed, lines = self.do_one_token_transform(lines)
changed, lines = self.do_one_token_transform(lines, display=display)
if not changed:
return lines

Expand All @@ -634,7 +716,7 @@ def transform_cell(self, cell: str) -> str:
cell += '\n' # Ensure the cell has a trailing newline
lines = cell.splitlines(keepends=True)
for transform in self.cleanup_transforms + self.line_transforms:
lines = transform(lines)
lines = transform(lines, shell=self.shell, display=True)

lines = self.do_token_transforms(lines)
return ''.join(lines)
Expand Down Expand Up @@ -684,7 +766,7 @@ def check_complete(self, cell: str):
try:
for transform in self.cleanup_transforms:
if not getattr(transform, 'has_side_effects', False):
lines = transform(lines)
lines = transform(lines, shell=self.shell, display=False)
except SyntaxError:
return 'invalid', None

Expand All @@ -698,8 +780,8 @@ def check_complete(self, cell: str):
try:
for transform in self.line_transforms:
if not getattr(transform, 'has_side_effects', False):
lines = transform(lines)
lines = self.do_token_transforms(lines)
lines = transform(lines, shell=self.shell, display=False)
lines = self.do_token_transforms(lines, display=False)
except SyntaxError:
return 'invalid', None

Expand Down
30 changes: 24 additions & 6 deletions IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
from IPython.core.extensions import ExtensionManager
from IPython.core.formatters import DisplayFormatter
from IPython.core.history import HistoryManager
from IPython.core.inputtransformer2 import ESC_MAGIC, ESC_MAGIC2
from IPython.core.inputtransformer2 import ESC_MAGIC, ESC_MAGIC2, TransformerManager
from IPython.core.logger import Logger
from IPython.core.macro import Macro
from IPython.core.payload import PayloadManager
Expand Down Expand Up @@ -306,6 +306,16 @@ class InteractiveShell(SingletonConfigurable):
"""
).tag(config=True)

autobalance = Bool(
False,
help="""
Add automatically opening and closing parenthesis, braces and brackets
at the begin matching respectively closing and opening ones. Opening
symbols are added in the begin of the expression, and closings symbols
at the end of the line. E.g. `1+2)/2]*(2+3` becomes `[(1+2)/2]*(2+3)`.
""",
).tag(config=True)
Copy link
Member

Choose a reason for hiding this comment

10000

The reason will be displayed to describe this comment to others. Learn more.

👍


autoindent = Bool(True, help=
"""
Autoindent IPython code entered interactively.
Expand Down Expand Up @@ -430,8 +440,9 @@ def _exiter_default(self):
ipython_dir= Unicode('').tag(config=True) # Set to get_ipython_dir() in __init__

# Used to transform cells before running them, and check whether code is complete
input_transformer_manager = Instance('IPython.core.inputtransformer2.TransformerManager',
())
input_transformer_manager = Instance(
"IPython.core.inputtransformer2.TransformerManager", allow_none=True
)

@property
def input_transformers_cleanup(self):
Expand Down Expand Up @@ -484,9 +495,8 @@ def input_splitter(self):
will be displayed as regular output instead."""
).tag(config=True)


show_rewritten_input = Bool(True,
help="Show rewritten input, e.g. for autocall."
show_rewritten_input = Bool(
True, help="Show rewritten input, e.g. for autocall and autobalance."
).tag(config=True)

quiet = Bool(False).tag(config=True)
Expand Down Expand Up @@ -593,6 +603,7 @@ def __init__(self, ipython_dir=None, profile_dir=None,
self.init_history()
self.init_encoding()
self.init_prefilter()
self.init_input_transformer_manager()

self.init_syntax_highlighting()
self.init_hooks()
Expand Down Expand Up @@ -2740,6 +2751,13 @@ def auto_rewrite_input(self, cmd):
print("------> " + cmd)

#-------------------------------------------------------------------------
# Things related to input_transformer_manager
# -------------------------------------------------------------------------

def init_input_transformer_manager(self):
self.input_transformer_manager = TransformerManager(shell=self)

# -------------------------------------------------------------------------
# Things related to extracting values/expressions from kernel and user_ns
#-------------------------------------------------------------------------

Expand Down
29 changes: 29 additions & 0 deletions IPython/core/magics/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,32 @@ def errorMessage() -> str:
self.shell.autocall = self._magic_state.autocall_save = 1

print("Automatic calling is:", list(valid_modes.values())[self.shell.autocall])

@line_magic
def autobalance(self, parameter_s=""):
"""Automatically balance parenthesis, braces and brackets.

On single line cell, this mode add automatically needed symbol at the
begin of the expression and the end of the line. If symbols need to
be added at other place, there exists a ambiguity, and this
transformation is not applied.

Without arguments toggles on/off. With arguments it sets the value, and
you can use any of (case insensitive):

- on, 1, True: to activate

- off, 0, False: to deactivate.

When symbols are added, the rewritten line is displayed. See
shell configuration `show_rewritten_input` to change this behavior.
"""

arg = parameter_s.lower()
if arg in ("on", "1", "true"):
self.shell.autobalance = True
elif arg in ("off", "0", "false"):
self.shell.autobalance = False
else:
self.shell.autobalance = not self.shell.autobalance
print("Autobalance " + ("enabled" if self.shell.autobalance else "disabled"))
2 changes: 1 addition & 1 deletion IPython/core/tests/test_inputtransformer2.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def test():
)


def null_cleanup_transformer(lines):
def null_cleanup_transformer(lines, **kwargs):
"""
A cleanup transform that returns an empty list.
"""
Expand Down
5 changes: 3 additions & 2 deletions IPython/core/tests/test_interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,12 +791,13 @@ class TestMiscTransform(unittest.TestCase):
def test_transform_only_once(self):
cleanup = 0
line_t = 0
def count_cleanup(lines):

def count_cleanup(lines, **kwargs):
nonlocal cleanup
cleanup += 1
return lines

def count_line_t(lines):
def count_line_t(lines, **kwargs):
nonlocal line_t
line_t += 1
return lines
Expand Down
0