code.py
755 lines
| 27.4 KiB
| text/x-python
|
PythonLexer
|
r6961 | """Implementation of code management magic functions. | ||
|
r6960 | """ | ||
#----------------------------------------------------------------------------- | ||||
# Copyright (c) 2012 The IPython Development Team. | ||||
# | ||||
# Distributed under the terms of the Modified BSD License. | ||||
# | ||||
# The full license is in the file COPYING.txt, distributed with this software. | ||||
#----------------------------------------------------------------------------- | ||||
#----------------------------------------------------------------------------- | ||||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
# Stdlib | ||||
import inspect | ||||
import io | ||||
import os | ||||
|
r8547 | import re | ||
|
r6960 | import sys | ||
|
r12841 | import ast | ||
|
r12835 | from itertools import chain | ||
|
r26397 | from urllib.request import Request, urlopen | ||
|
r24384 | from urllib.parse import urlencode | ||
|
r26001 | from pathlib import Path | ||
|
r6960 | |||
# Our own packages | ||||
|
r8278 | from IPython.core.error import TryNext, StdinNotImplementedError, UsageError | ||
|
r6960 | from IPython.core.macro import Macro | ||
|
r6973 | from IPython.core.magic import Magics, magics_class, line_magic | ||
|
r7462 | from IPython.core.oinspect import find_file, find_source_lines | ||
|
r26397 | from IPython.core.release import version | ||
|
r6960 | from IPython.testing.skipdoctest import skip_doctest | ||
|
r9023 | from IPython.utils.contexts import preserve_keys | ||
|
r22418 | from IPython.utils.path import get_py_filename | ||
|
r22092 | from warnings import warn | ||
from logging import error | ||||
|
r12920 | from IPython.utils.text import get_text_list | ||
|
r6960 | |||
#----------------------------------------------------------------------------- | ||||
# Magic implementation classes | ||||
#----------------------------------------------------------------------------- | ||||
# Used for exception handling in magic_edit | ||||
class MacroToEdit(ValueError): pass | ||||
|
r8547 | ipython_input_pat = re.compile(r"<ipython\-input\-(\d+)-[a-z\d]+>$") | ||
|
r12835 | # To match, e.g. 8-10 1:5 :10 3- | ||
range_re = re.compile(r""" | ||||
(?P<start>\d+)? | ||||
((?P<sep>[\-:]) | ||||
(?P<end>\d+)?)? | ||||
$""", re.VERBOSE) | ||||
def extract_code_ranges(ranges_str): | ||||
"""Turn a string of range for %%load into 2-tuples of (start, stop) | ||||
|
r24132 | ready to use as a slice of the content split by lines. | ||
|
r12835 | |||
Examples | ||||
-------- | ||||
list(extract_input_ranges("5-10 2")) | ||||
[(4, 10), (1, 2)] | ||||
""" | ||||
for range_str in ranges_str.split(): | ||||
rmatch = range_re.match(range_str) | ||||
if not rmatch: | ||||
continue | ||||
sep = rmatch.group("sep") | ||||
start = rmatch.group("start") | ||||
end = rmatch.group("end") | ||||
if sep == '-': | ||||
start = int(start) - 1 if start else None | ||||
end = int(end) if end else None | ||||
elif sep == ':': | ||||
start = int(start) - 1 if start else None | ||||
end = int(end) - 1 if end else None | ||||
else: | ||||
end = int(start) | ||||
start = int(start) - 1 | ||||
yield (start, end) | ||||
|
r12841 | def extract_symbols(code, symbols): | ||
""" | ||||
|
r12920 | Return a tuple (blocks, not_found) | ||
where ``blocks`` is a list of code fragments | ||||
for each symbol parsed from code, and ``not_found`` are | ||||
symbols not found in the code. | ||||
For example:: | ||||
|
r12841 | |||
|
r23634 | In [1]: code = '''a = 10 | ||
...: def b(): return 42 | ||||
...: class A: pass''' | ||||
|
r12841 | |||
|
r23634 | In [2]: extract_symbols(code, 'A,b,z') | ||
Out[2]: (['class A: pass\\n', 'def b(): return 42\\n'], ['z']) | ||||
|
r12841 | """ | ||
|
r12943 | symbols = symbols.split(',') | ||
|
r12945 | |||
# this will raise SyntaxError if code isn't valid Python | ||||
py_code = ast.parse(code) | ||||
|
r12844 | |||
|
r12841 | marks = [(getattr(s, 'name', None), s.lineno) for s in py_code.body] | ||
|
r12847 | code = code.split('\n') | ||
|
r12844 | |||
|
r12841 | symbols_lines = {} | ||
|
r12927 | |||
# we already know the start_lineno of each symbol (marks). | ||||
# To find each end_lineno, we traverse in reverse order until each | ||||
# non-blank line | ||||
end = len(code) | ||||
|
r12841 | for name, start in reversed(marks): | ||
|
r12847 | while not code[end - 1].strip(): | ||
end -= 1 | ||||
|
r12841 | if name: | ||
symbols_lines[name] = (start - 1, end) | ||||
end = start - 1 | ||||
|
r12844 | |||
|
r12927 | # Now symbols_lines is a map | ||
# {'symbol_name': (start_lineno, end_lineno), ...} | ||||
# fill a list with chunks of codes for each requested symbol | ||||
|
r12841 | blocks = [] | ||
|
r12920 | not_found = [] | ||
|
r12943 | for symbol in symbols: | ||
|
r12841 | if symbol in symbols_lines: | ||
|
r12844 | start, end = symbols_lines[symbol] | ||
|
r12847 | blocks.append('\n'.join(code[start:end]) + '\n') | ||
|
r12920 | else: | ||
not_found.append(symbol) | ||||
|
r12844 | |||
|
r12920 | return blocks, not_found | ||
|
r12841 | |||
|
r22702 | def strip_initial_indent(lines): | ||
"""For %load, strip indent from lines until finding an unindented line. | ||||
https://github.com/ipython/ipython/issues/9775 | ||||
""" | ||||
indent_re = re.compile(r'\s+') | ||||
it = iter(lines) | ||||
first_line = next(it) | ||||
indent_match = indent_re.match(first_line) | ||||
if indent_match: | ||||
# First line was indented | ||||
indent = indent_match.group() | ||||
yield first_line[len(indent):] | ||||
for line in it: | ||||
if line.startswith(indent): | ||||
yield line[len(indent):] | ||||
else: | ||||
# Less indented than the first line - stop dedenting | ||||
yield line | ||||
break | ||||
else: | ||||
yield first_line | ||||
# Pass the remaining lines through without dedenting | ||||
for line in it: | ||||
yield line | ||||
|
r12835 | |||
|
r8547 | class InteractivelyDefined(Exception): | ||
"""Exception for interactively defined variable in magic_edit""" | ||||
def __init__(self, index): | ||||
self.index = index | ||||
|
r6960 | |||
|
r6973 | @magics_class | ||
|
r6960 | class CodeMagics(Magics): | ||
"""Magics related to code management (loading, saving, editing, ...).""" | ||||
|
r21966 | def __init__(self, *args, **kwargs): | ||
self._knowntemps = set() | ||||
super(CodeMagics, self).__init__(*args, **kwargs) | ||||
|
r6960 | @line_magic | ||
def save(self, parameter_s=''): | ||||
"""Save a set of lines or a macro to a given filename. | ||||
Usage:\\ | ||||
|
r26930 | %save [options] filename [history] | ||
|
r6960 | |||
Options: | ||||
-r: use 'raw' input. By default, the 'processed' history is used, | ||||
so that magics are loaded in their transformed version to valid | ||||
Python. If this option is given, the raw input as typed as the | ||||
command line is used instead. | ||||
|
r7528 | |||
-f: force overwrite. If file exists, %save will prompt for overwrite | ||||
unless -f is given. | ||||
|
r6960 | |||
|
r7851 | -a: append to the file instead of overwriting it. | ||
|
r7841 | |||
|
r26930 | The history argument uses the same syntax as %history for input ranges, | ||
|
r6960 | then saves the lines to the filename you specify. | ||
|
r26635 | If no ranges are specified, saves history of the current session up to | ||
this point. | ||||
|
r6960 | It adds a '.py' extension to the file if you don't do so yourself, and | ||
|
r7255 | it asks for confirmation before overwriting existing files. | ||
If `-r` option is used, the default extension is `.ipy`. | ||||
""" | ||||
|
r6960 | |||
|
r7841 | opts,args = self.parse_options(parameter_s,'fra',mode='list') | ||
|
r8278 | if not args: | ||
raise UsageError('Missing filename.') | ||||
|
r7255 | raw = 'r' in opts | ||
|
r7528 | force = 'f' in opts | ||
|
r7841 | append = 'a' in opts | ||
mode = 'a' if append else 'w' | ||||
|
r25352 | ext = '.ipy' if raw else '.py' | ||
|
r22418 | fname, codefrom = args[0], " ".join(args[1:]) | ||
|
r25352 | if not fname.endswith(('.py','.ipy')): | ||
|
r7255 | fname += ext | ||
|
r27282 | fname = os.path.expanduser(fname) | ||
|
r7848 | file_exists = os.path.isfile(fname) | ||
if file_exists and not force and not append: | ||||
|
r7528 | try: | ||
overwrite = self.shell.ask_yes_no('File `%s` exists. Overwrite (y/[N])? ' % fname, default='n') | ||||
except StdinNotImplementedError: | ||||
|
r13348 | print("File `%s` exists. Use `%%save -f %s` to force overwrite" % (fname, parameter_s)) | ||
|
r7528 | return | ||
|
r7061 | if not overwrite : | ||
|
r13348 | print('Operation cancelled.') | ||
|
r6960 | return | ||
try: | ||||
|
r7255 | cmds = self.shell.find_user_code(codefrom,raw) | ||
|
r6960 | except (TypeError, ValueError) as e: | ||
|
r13348 | print(e.args[0]) | ||
|
r6960 | return | ||
|
r7841 | with io.open(fname, mode, encoding="utf-8") as f: | ||
|
r7848 | if not file_exists or not append: | ||
|
r25352 | f.write("# coding: utf-8\n") | ||
f.write(cmds) | ||||
|
r7842 | # make sure we end on a newline | ||
|
r25352 | if not cmds.endswith('\n'): | ||
f.write('\n') | ||||
|
r13348 | print('The following commands were written to file `%s`:' % fname) | ||
print(cmds) | ||||
|
r6960 | |||
@line_magic | ||||
def pastebin(self, parameter_s=''): | ||||
|
r26398 | """Upload code to dpaste.com, returning the URL. | ||
|
r6960 | |||
Usage:\\ | ||||
|
r26653 | %pastebin [-d "Custom description"][-e 24] 1-7 | ||
|
r6960 | |||
The argument can be an input history range, a filename, or the name of a | ||||
string or macro. | ||||
|
r26636 | If no arguments are given, uploads the history of this session up to | ||
this point. | ||||
|
r6960 | Options: | ||
|
r26397 | -d: Pass a custom description. The default will say | ||
|
r6960 | "Pasted from IPython". | ||
|
r26653 | -e: Pass number of days for the link to be expired. | ||
The default will be 7 days. | ||||
|
r6960 | """ | ||
|
r26655 | opts, args = self.parse_options(parameter_s, "d:e:") | ||
|
r6960 | |||
try: | ||||
code = self.shell.find_user_code(args) | ||||
except (ValueError, TypeError) as e: | ||||
|
r13348 | print(e.args[0]) | ||
|
r6960 | return | ||
|
r26653 | expiry_days = 7 | ||
|
r26658 | try: | ||
expiry_days = int(opts.get("e", 7)) | ||||
except ValueError as e: | ||||
print(e.args[0].capitalize()) | ||||
return | ||||
if expiry_days < 1 or expiry_days > 365: | ||||
print("Expiry days should be in range of 1 to 365") | ||||
return | ||||
|
r26653 | |||
|
r26399 | post_data = urlencode( | ||
{ | ||||
"title": opts.get("d", "Pasted from IPython"), | ||||
"syntax": "python", | ||||
"content": code, | ||||
|
r26655 | "expiry_days": expiry_days, | ||
|
r26399 | } | ||
).encode("utf-8") | ||||
request = Request( | ||||
|
r26654 | "https://dpaste.com/api/v2/", | ||
|
r26399 | headers={"User-Agent": "IPython v{}".format(version)}, | ||
) | ||||
|
r26397 | response = urlopen(request, post_data) | ||
|
r24384 | return response.headers.get('Location') | ||
|
r6960 | |||
@line_magic | ||||
def loadpy(self, arg_s): | ||||
|
r7061 | """Alias of `%load` | ||
|
r6960 | |||
|
r11444 | `%loadpy` has gained some flexibility and dropped the requirement of a `.py` | ||
|
r7061 | extension. So it has been renamed simply into %load. You can look at | ||
`%load`'s docstring for more info. | ||||
""" | ||||
|
r7104 | self.load(arg_s) | ||
|
r7061 | |||
@line_magic | ||||
def load(self, arg_s): | ||||
"""Load code into the current frontend. | ||||
Usage:\\ | ||||
%load [options] source | ||||
|
r6960 | |||
|
r16318 | where source can be a filename, URL, input history range, macro, or | ||
element in the user namespace | ||||
|
r7061 | |||
|
r26637 | If no arguments are given, loads the history of this session up to this | ||
point. | ||||
|
r7061 | Options: | ||
|
r13587 | |||
|
r12837 | -r <lines>: Specify lines or ranges of lines to load from the source. | ||
Ranges could be specified as x-y (x..y) or in python-style x:y | ||||
(x..(y-1)). Both limits x and y can be left blank (meaning the | ||||
beginning and end of the file, respectively). | ||||
|
r12846 | -s <symbols>: Specify function or classes to load from python source. | ||
|
r12841 | |||
|
r7061 | -y : Don't ask confirmation for loading source above 200 000 characters. | ||
|
r16318 | -n : Include the user's namespace when searching for source code. | ||
|
r7061 | This magic command can either take a local filename, a URL, an history | ||
range (see %history) or a macro as argument, it will prompt for | ||||
confirmation before loading source with more than 200 000 characters, unless | ||||
-y flag is passed or if the frontend does not support raw_input:: | ||||
|
r26637 | %load | ||
|
r7061 | %load myscript.py | ||
%load 7-27 | ||||
%load myMacro | ||||
%load http://www.example.com/myscript.py | ||||
|
r12835 | %load -r 5-10 myscript.py | ||
|
r12837 | %load -r 10-20,30,40: foo.py | ||
|
r12841 | %load -s MyClass,wonder_function myscript.py | ||
|
r16318 | %load -n MyClass | ||
%load -n my_module.wonder_function | ||||
|
r6960 | """ | ||
|
r16318 | opts,args = self.parse_options(arg_s,'yns:r:') | ||
search_ns = 'n' in opts | ||||
contents = self.shell.find_user_code(args, search_ns=search_ns) | ||||
|
r12835 | |||
|
r12841 | if 's' in opts: | ||
|
r12945 | try: | ||
blocks, not_found = extract_symbols(contents, opts['s']) | ||||
except SyntaxError: | ||||
# non python code | ||||
error("Unable to parse the input as valid Python code") | ||||
return | ||||
|
r12920 | if len(not_found) == 1: | ||
warn('The symbol `%s` was not found' % not_found[0]) | ||||
elif len(not_found) > 1: | ||||
warn('The symbols %s were not found' % get_text_list(not_found, | ||||
wrap_item_with='`') | ||||
) | ||||
contents = '\n'.join(blocks) | ||||
|
r12841 | |||
|
r12835 | if 'r' in opts: | ||
ranges = opts['r'].replace(',', ' ') | ||||
lines = contents.split('\n') | ||||
slices = extract_code_ranges(ranges) | ||||
contents = [lines[slice(*slc)] for slc in slices] | ||||
|
r22702 | contents = '\n'.join(strip_initial_indent(chain.from_iterable(contents))) | ||
|
r12835 | |||
|
r7061 | l = len(contents) | ||
|
r24084 | # 200 000 is ~ 2500 full 80 character lines | ||
|
r7061 | # so in average, more than 5000 lines | ||
if l > 200000 and 'y' not in opts: | ||||
try: | ||||
ans = self.shell.ask_yes_no(("The text you're trying to load seems pretty big"\ | ||||
" (%d characters). Continue (y/[N]) ?" % l), default='n' ) | ||||
except StdinNotImplementedError: | ||||
|
r24084 | #assume yes if raw input not implemented | ||
|
r7061 | ans = True | ||
if ans is False : | ||||
|
r13348 | print('Operation cancelled.') | ||
|
r7061 | return | ||
|
r6960 | |||
|
r19251 | contents = "# %load {}\n".format(arg_s) + contents | ||
self.shell.set_next_input(contents, replace=True) | ||||
|
r6960 | |||
|
r7077 | @staticmethod | ||
def _find_edit_target(shell, args, opts, last_call): | ||||
|
r6960 | """Utility method used by magic_edit to find what to edit.""" | ||
def make_filename(arg): | ||||
"Make a filename from the given args" | ||||
try: | ||||
filename = get_py_filename(arg) | ||||
except IOError: | ||||
# If it ends with .py but doesn't already exist, assume we want | ||||
# a new file. | ||||
if arg.endswith('.py'): | ||||
filename = arg | ||||
else: | ||||
filename = None | ||||
return filename | ||||
# Set a few locals from the options for convenience: | ||||
opts_prev = 'p' in opts | ||||
opts_raw = 'r' in opts | ||||
# custom exceptions | ||||
class DataIsObject(Exception): pass | ||||
# Default line number value | ||||
lineno = opts.get('n',None) | ||||
if opts_prev: | ||||
args = '_%s' % last_call[0] | ||||
|
r7859 | if args not in shell.user_ns: | ||
|
r6960 | args = last_call[1] | ||
# by default this is done with temp files, except when the given | ||||
# arg is a filename | ||||
use_temp = True | ||||
data = '' | ||||
# First, see if the arguments should be a filename. | ||||
filename = make_filename(args) | ||||
if filename: | ||||
use_temp = False | ||||
elif args: | ||||
# Mode where user specifies ranges of lines, like in %macro. | ||||
|
r7077 | data = shell.extract_input_lines(args, opts_raw) | ||
|
r6960 | if not data: | ||
try: | ||||
# Load the parameter given as a variable. If not a string, | ||||
# process it as an object instead (below) | ||||
|
r28756 | # print('*** args',args,'type',type(args)) # dbg | ||
|
r7077 | data = eval(args, shell.user_ns) | ||
|
r23037 | if not isinstance(data, str): | ||
|
r6960 | raise DataIsObject | ||
except (NameError,SyntaxError): | ||||
# given argument is not a variable, try as a filename | ||||
filename = make_filename(args) | ||||
if filename is None: | ||||
warn("Argument given (%s) can't be found as a variable " | ||||
"or as a filename." % args) | ||||
|
r9021 | return (None, None, None) | ||
|
r6960 | use_temp = False | ||
|
r25827 | except DataIsObject as e: | ||
|
r6960 | # macros have a special edit function | ||
if isinstance(data, Macro): | ||||
|
r25827 | raise MacroToEdit(data) from e | ||
|
r6960 | |||
# For objects, try to edit the file where they are defined | ||||
|
r7462 | filename = find_file(data) | ||
if filename: | ||||
|
r6960 | if 'fakemodule' in filename.lower() and \ | ||
inspect.isclass(data): | ||||
# class created by %edit? Try to find source | ||||
# by looking for method definitions instead, the | ||||
# __module__ in those classes is FakeModule. | ||||
attrs = [getattr(data, aname) for aname in dir(data)] | ||||
for attr in attrs: | ||||
if not inspect.ismethod(attr): | ||||
continue | ||||
|
r7462 | filename = find_file(attr) | ||
|
r6960 | if filename and \ | ||
'fakemodule' not in filename.lower(): | ||||
# change the attribute to be the edit | ||||
# target instead | ||||
data = attr | ||||
break | ||||
|
r8547 | |||
m = ipython_input_pat.match(os.path.basename(filename)) | ||||
if m: | ||||
|
r25827 | raise InteractivelyDefined(int(m.groups()[0])) from e | ||
|
r6960 | datafile = 1 | ||
|
r7462 | if filename is None: | ||
|
r6960 | filename = make_filename(args) | ||
datafile = 1 | ||||
|
r9021 | if filename is not None: | ||
# only warn about this if we get a real name | ||||
warn('Could not find file where `%s` is defined.\n' | ||||
|
r6986 | 'Opening a file named `%s`' % (args, filename)) | ||
|
r6960 | # Now, make sure we can actually read the source (if it was | ||
# in a temp file it's gone by now). | ||||
if datafile: | ||||
|
r7462 | if lineno is None: | ||
lineno = find_source_lines(data) | ||||
if lineno is None: | ||||
|
r6960 | filename = make_filename(args) | ||
if filename is None: | ||||
|
r9021 | warn('The file where `%s` was defined ' | ||
'cannot be read or found.' % data) | ||||
return (None, None, None) | ||||
|
r6960 | use_temp = False | ||
if use_temp: | ||||
|
r7077 | filename = shell.mktempfile(data) | ||
|
r13348 | print('IPython will make a temporary file named:',filename) | ||
|
r6960 | |||
|
r9189 | # use last_call to remember the state of the previous call, but don't | ||
# let it be clobbered by successive '-p' calls. | ||||
try: | ||||
last_call[0] = shell.displayhook.prompt_count | ||||
if not opts_prev: | ||||
last_call[1] = args | ||||
except: | ||||
pass | ||||
|
r6960 | return filename, lineno, use_temp | ||
def _edit_macro(self,mname,macro): | ||||
"""open an editor with the macro data in a file""" | ||||
filename = self.shell.mktempfile(macro.value) | ||||
self.shell.hooks.editor(filename) | ||||
# and make a new macro object, to replace the old one | ||||
|
r27495 | mvalue = Path(filename).read_text(encoding="utf-8") | ||
|
r6960 | self.shell.user_ns[mname] = Macro(mvalue) | ||
@skip_doctest | ||||
@line_magic | ||||
def edit(self, parameter_s='',last_call=['','']): | ||||
"""Bring up an editor and execute the resulting code. | ||||
Usage: | ||||
%edit [options] [args] | ||||
%edit runs IPython's editor hook. The default version of this hook is | ||||
set to call the editor specified by your $EDITOR environment variable. | ||||
If this isn't found, it will default to vi under Linux/Unix and to | ||||
notepad under Windows. See the end of this docstring for how to change | ||||
the editor hook. | ||||
You can also set the value of this editor via the | ||||
``TerminalInteractiveShell.editor`` option in your configuration file. | ||||
This is useful if you wish to use a different editor from your typical | ||||
default with IPython (and for Windows users who typically don't set | ||||
environment variables). | ||||
This command allows you to conveniently edit multi-line code right in | ||||
your IPython session. | ||||
If called without arguments, %edit opens up an empty editor with a | ||||
temporary file and will execute the contents of this file when you | ||||
close it (don't forget to save it!). | ||||
Options: | ||||
-n <number>: open the editor at a specified line number. By default, | ||||
the IPython editor hook uses the unix syntax 'editor +N filename', but | ||||
you can configure this by providing your own modified hook if your | ||||
favorite editor supports line-number specifications with a different | ||||
syntax. | ||||
-p: this will call the editor with the same data as the previous time | ||||
it was used, regardless of how long ago (in your current session) it | ||||
was. | ||||
-r: use 'raw' input. This option only applies to input taken from the | ||||
user's history. By default, the 'processed' history is used, so that | ||||
magics are loaded in their transformed version to valid Python. If | ||||
this option is given, the raw input as typed as the command line is | ||||
used instead. When you exit the editor, it will be executed by | ||||
IPython's own processor. | ||||
-x: do not execute the edited code immediately upon exit. This is | ||||
mainly useful if you are editing programs which need to be called with | ||||
command line arguments, which you can then do using %run. | ||||
Arguments: | ||||
If arguments are given, the following possibilities exist: | ||||
- If the argument is a filename, IPython will load that into the | ||||
editor. It will execute its contents with execfile() when you exit, | ||||
loading any code in the file into your interactive namespace. | ||||
- The arguments are ranges of input history, e.g. "7 ~1/4-6". | ||||
The syntax is the same as in the %history magic. | ||||
- If the argument is a string variable, its contents are loaded | ||||
into the editor. You can thus edit any string which contains | ||||
python code (including the result of previous edits). | ||||
- If the argument is the name of an object (other than a string), | ||||
IPython will try to locate the file where it was defined and open the | ||||
editor at the point where it is defined. You can use `%edit function` | ||||
to load an editor exactly at the point where 'function' is defined, | ||||
edit it and have the file be executed automatically. | ||||
- If the object is a macro (see %macro for details), this opens up your | ||||
specified editor with a temporary file containing the macro's data. | ||||
Upon exit, the macro is reloaded with the contents of the file. | ||||
Note: opening at an exact line is only supported under Unix, and some | ||||
editors (like kedit and gedit up to Gnome 2.8) do not understand the | ||||
'+NUMBER' parameter necessary for this feature. Good editors like | ||||
(X)Emacs, vi, jed, pico and joe all do. | ||||
After executing your code, %edit will return as output the code you | ||||
typed in the editor (except when it was an existing file). This way | ||||
you can reload the code in further invocations of %edit as a variable, | ||||
via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of | ||||
the output. | ||||
Note that %edit is also available through the alias %ed. | ||||
This is an example of creating a simple function inside the editor and | ||||
then modifying it. First, start up the editor:: | ||||
|
r7933 | In [1]: edit | ||
|
r6960 | Editing... done. Executing edited code... | ||
|
r28756 | Out[1]: 'def foo():\\n print("foo() was defined in an editing | ||
session")\\n' | ||||
|
r6960 | |||
We can then call the function foo():: | ||||
In [2]: foo() | ||||
foo() was defined in an editing session | ||||
Now we edit foo. IPython automatically loads the editor with the | ||||
(temporary) file where foo() was previously defined:: | ||||
|
r7933 | In [3]: edit foo | ||
|
r6960 | Editing... done. Executing edited code... | ||
And if we call foo() again we get the modified version:: | ||||
In [4]: foo() | ||||
foo() has now been changed! | ||||
Here is an example of how to edit a code snippet successive | ||||
times. First we call the editor:: | ||||
|
r7933 | In [5]: edit | ||
|
r6960 | Editing... done. Executing edited code... | ||
hello | ||||
|
r28756 | Out[5]: "print('hello')\\n" | ||
|
r6960 | |||
Now we call it again with the previous output (stored in _):: | ||||
|
r7933 | In [6]: edit _ | ||
|
r6960 | Editing... done. Executing edited code... | ||
hello world | ||||
|
r28756 | Out[6]: "print('hello world')\\n" | ||
|
r6960 | |||
Now we call it with the output #8 (stored in _8, also as Out[8]):: | ||||
|
r7933 | In [7]: edit _8 | ||
|
r6960 | Editing... done. Executing edited code... | ||
hello again | ||||
|
r28756 | Out[7]: "print('hello again')\\n" | ||
|
r6960 | |||
Changing the default editor hook: | ||||
If you wish to write your own editor hook, you can put it in a | ||||
configuration file which you load at startup time. The default hook | ||||
is defined in the IPython.core.hooks module, and you can use that as a | ||||
starting example for further modifications. That file also has | ||||
general instructions on how to set a new hook for use once you've | ||||
defined it.""" | ||||
opts,args = self.parse_options(parameter_s,'prxn:') | ||||
try: | ||||
|
r7282 | filename, lineno, is_temp = self._find_edit_target(self.shell, | ||
args, opts, last_call) | ||||
|
r6960 | except MacroToEdit as e: | ||
self._edit_macro(args, e.args[0]) | ||||
return | ||||
|
r8547 | except InteractivelyDefined as e: | ||
|
r13348 | print("Editing In[%i]" % e.index) | ||
|
r8547 | args = str(e.index) | ||
filename, lineno, is_temp = self._find_edit_target(self.shell, | ||||
args, opts, last_call) | ||||
|
r9021 | if filename is None: | ||
# nothing was found, warnings have already been issued, | ||||
# just give up. | ||||
return | ||||
|
r6960 | |||
|
r21966 | if is_temp: | ||
self._knowntemps.add(filename) | ||||
elif (filename in self._knowntemps): | ||||
is_temp = True | ||||
|
r6960 | # do actual editing here | ||
|
r13348 | print('Editing...', end=' ') | ||
|
r6960 | sys.stdout.flush() | ||
|
r26001 | filepath = Path(filename) | ||
|
r6960 | try: | ||
|
r26001 | # Quote filenames that may have spaces in them when opening | ||
# the editor | ||||
quoted = filename = str(filepath.absolute()) | ||||
|
r26011 | if " " in quoted: | ||
|
r26001 | quoted = "'%s'" % quoted | ||
|
r26011 | self.shell.hooks.editor(quoted, lineno) | ||
|
r6960 | except TryNext: | ||
warn('Could not open editor') | ||||
return | ||||
# XXX TODO: should this be generalized for all string vars? | ||||
# For now, this is special-cased to blocks created by cpaste | ||||
|
r26011 | if args.strip() == "pasted_block": | ||
|
r27495 | self.shell.user_ns["pasted_block"] = filepath.read_text(encoding="utf-8") | ||
|
r6960 | |||
if 'x' in opts: # -x prevents actual execution | ||||
|
r13348 | print() | ||
|
r6960 | else: | ||
|
r13348 | print('done. Executing edited code...') | ||
|
r9023 | with preserve_keys(self.shell.user_ns, '__file__'): | ||
if not is_temp: | ||||
|
r27495 | self.shell.user_ns["__file__"] = filename | ||
if "r" in opts: # Untranslated IPython code | ||||
source = filepath.read_text(encoding="utf-8") | ||||
|
r9475 | self.shell.run_cell(source, store_history=False) | ||
|
r9023 | else: | ||
self.shell.safe_execfile(filename, self.shell.user_ns, | ||||
self.shell.user_ns) | ||||
|
r6960 | |||
if is_temp: | ||||
try: | ||||
|
r27495 | return filepath.read_text(encoding="utf-8") | ||
|
r7787 | except IOError as msg: | ||
|
r26001 | if Path(msg.filename) == filepath: | ||
|
r6960 | warn('File not found. Did you forget to save?') | ||
return | ||||
else: | ||||
self.shell.showtraceback() | ||||