Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/IPython/core/magics/code.py: 16%
313 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
1"""Implementation of code management magic functions.
2"""
3#-----------------------------------------------------------------------------
4# Copyright (c) 2012 The IPython Development Team.
5#
6# Distributed under the terms of the Modified BSD License.
7#
8# The full license is in the file COPYING.txt, distributed with this software.
9#-----------------------------------------------------------------------------
11#-----------------------------------------------------------------------------
12# Imports
13#-----------------------------------------------------------------------------
15# Stdlib
16import inspect
17import io
18import os
19import re
20import sys
21import ast
22from itertools import chain
23from urllib.request import Request, urlopen
24from urllib.parse import urlencode
25from pathlib import Path
27# Our own packages
28from IPython.core.error import TryNext, StdinNotImplementedError, UsageError
29from IPython.core.macro import Macro
30from IPython.core.magic import Magics, magics_class, line_magic
31from IPython.core.oinspect import find_file, find_source_lines
32from IPython.core.release import version
33from IPython.testing.skipdoctest import skip_doctest
34from IPython.utils.contexts import preserve_keys
35from IPython.utils.path import get_py_filename
36from warnings import warn
37from logging import error
38from IPython.utils.text import get_text_list
40#-----------------------------------------------------------------------------
41# Magic implementation classes
42#-----------------------------------------------------------------------------
44# Used for exception handling in magic_edit
45class MacroToEdit(ValueError): pass
47ipython_input_pat = re.compile(r"<ipython\-input\-(\d+)-[a-z\d]+>$")
49# To match, e.g. 8-10 1:5 :10 3-
50range_re = re.compile(r"""
51(?P<start>\d+)?
52((?P<sep>[\-:])
53 (?P<end>\d+)?)?
54$""", re.VERBOSE)
57def extract_code_ranges(ranges_str):
58 """Turn a string of range for %%load into 2-tuples of (start, stop)
59 ready to use as a slice of the content split by lines.
61 Examples
62 --------
63 list(extract_input_ranges("5-10 2"))
64 [(4, 10), (1, 2)]
65 """
66 for range_str in ranges_str.split():
67 rmatch = range_re.match(range_str)
68 if not rmatch:
69 continue
70 sep = rmatch.group("sep")
71 start = rmatch.group("start")
72 end = rmatch.group("end")
74 if sep == '-':
75 start = int(start) - 1 if start else None
76 end = int(end) if end else None
77 elif sep == ':':
78 start = int(start) - 1 if start else None
79 end = int(end) - 1 if end else None
80 else:
81 end = int(start)
82 start = int(start) - 1
83 yield (start, end)
86def extract_symbols(code, symbols):
87 """
88 Return a tuple (blocks, not_found)
89 where ``blocks`` is a list of code fragments
90 for each symbol parsed from code, and ``not_found`` are
91 symbols not found in the code.
93 For example::
95 In [1]: code = '''a = 10
96 ...: def b(): return 42
97 ...: class A: pass'''
99 In [2]: extract_symbols(code, 'A,b,z')
100 Out[2]: (['class A: pass\\n', 'def b(): return 42\\n'], ['z'])
101 """
102 symbols = symbols.split(',')
104 # this will raise SyntaxError if code isn't valid Python
105 py_code = ast.parse(code)
107 marks = [(getattr(s, 'name', None), s.lineno) for s in py_code.body]
108 code = code.split('\n')
110 symbols_lines = {}
112 # we already know the start_lineno of each symbol (marks).
113 # To find each end_lineno, we traverse in reverse order until each
114 # non-blank line
115 end = len(code)
116 for name, start in reversed(marks):
117 while not code[end - 1].strip():
118 end -= 1
119 if name:
120 symbols_lines[name] = (start - 1, end)
121 end = start - 1
123 # Now symbols_lines is a map
124 # {'symbol_name': (start_lineno, end_lineno), ...}
126 # fill a list with chunks of codes for each requested symbol
127 blocks = []
128 not_found = []
129 for symbol in symbols:
130 if symbol in symbols_lines:
131 start, end = symbols_lines[symbol]
132 blocks.append('\n'.join(code[start:end]) + '\n')
133 else:
134 not_found.append(symbol)
136 return blocks, not_found
138def strip_initial_indent(lines):
139 """For %load, strip indent from lines until finding an unindented line.
141 https://github.com/ipython/ipython/issues/9775
142 """
143 indent_re = re.compile(r'\s+')
145 it = iter(lines)
146 first_line = next(it)
147 indent_match = indent_re.match(first_line)
149 if indent_match:
150 # First line was indented
151 indent = indent_match.group()
152 yield first_line[len(indent):]
154 for line in it:
155 if line.startswith(indent):
156 yield line[len(indent):]
157 else:
158 # Less indented than the first line - stop dedenting
159 yield line
160 break
161 else:
162 yield first_line
164 # Pass the remaining lines through without dedenting
165 for line in it:
166 yield line
169class InteractivelyDefined(Exception):
170 """Exception for interactively defined variable in magic_edit"""
171 def __init__(self, index):
172 self.index = index
175@magics_class
176class CodeMagics(Magics):
177 """Magics related to code management (loading, saving, editing, ...)."""
179 def __init__(self, *args, **kwargs):
180 self._knowntemps = set()
181 super(CodeMagics, self).__init__(*args, **kwargs)
183 @line_magic
184 def save(self, parameter_s=''):
185 """Save a set of lines or a macro to a given filename.
187 Usage:\\
188 %save [options] filename [history]
190 Options:
192 -r: use 'raw' input. By default, the 'processed' history is used,
193 so that magics are loaded in their transformed version to valid
194 Python. If this option is given, the raw input as typed as the
195 command line is used instead.
197 -f: force overwrite. If file exists, %save will prompt for overwrite
198 unless -f is given.
200 -a: append to the file instead of overwriting it.
202 The history argument uses the same syntax as %history for input ranges,
203 then saves the lines to the filename you specify.
205 If no ranges are specified, saves history of the current session up to
206 this point.
208 It adds a '.py' extension to the file if you don't do so yourself, and
209 it asks for confirmation before overwriting existing files.
211 If `-r` option is used, the default extension is `.ipy`.
212 """
214 opts,args = self.parse_options(parameter_s,'fra',mode='list')
215 if not args:
216 raise UsageError('Missing filename.')
217 raw = 'r' in opts
218 force = 'f' in opts
219 append = 'a' in opts
220 mode = 'a' if append else 'w'
221 ext = '.ipy' if raw else '.py'
222 fname, codefrom = args[0], " ".join(args[1:])
223 if not fname.endswith(('.py','.ipy')):
224 fname += ext
225 fname = os.path.expanduser(fname)
226 file_exists = os.path.isfile(fname)
227 if file_exists and not force and not append:
228 try:
229 overwrite = self.shell.ask_yes_no('File `%s` exists. Overwrite (y/[N])? ' % fname, default='n')
230 except StdinNotImplementedError:
231 print("File `%s` exists. Use `%%save -f %s` to force overwrite" % (fname, parameter_s))
232 return
233 if not overwrite :
234 print('Operation cancelled.')
235 return
236 try:
237 cmds = self.shell.find_user_code(codefrom,raw)
238 except (TypeError, ValueError) as e:
239 print(e.args[0])
240 return
241 with io.open(fname, mode, encoding="utf-8") as f:
242 if not file_exists or not append:
243 f.write("# coding: utf-8\n")
244 f.write(cmds)
245 # make sure we end on a newline
246 if not cmds.endswith('\n'):
247 f.write('\n')
248 print('The following commands were written to file `%s`:' % fname)
249 print(cmds)
251 @line_magic
252 def pastebin(self, parameter_s=''):
253 """Upload code to dpaste.com, returning the URL.
255 Usage:\\
256 %pastebin [-d "Custom description"][-e 24] 1-7
258 The argument can be an input history range, a filename, or the name of a
259 string or macro.
261 If no arguments are given, uploads the history of this session up to
262 this point.
264 Options:
266 -d: Pass a custom description. The default will say
267 "Pasted from IPython".
268 -e: Pass number of days for the link to be expired.
269 The default will be 7 days.
270 """
271 opts, args = self.parse_options(parameter_s, "d:e:")
273 try:
274 code = self.shell.find_user_code(args)
275 except (ValueError, TypeError) as e:
276 print(e.args[0])
277 return
279 expiry_days = 7
280 try:
281 expiry_days = int(opts.get("e", 7))
282 except ValueError as e:
283 print(e.args[0].capitalize())
284 return
285 if expiry_days < 1 or expiry_days > 365:
286 print("Expiry days should be in range of 1 to 365")
287 return
289 post_data = urlencode(
290 {
291 "title": opts.get("d", "Pasted from IPython"),
292 "syntax": "python",
293 "content": code,
294 "expiry_days": expiry_days,
295 }
296 ).encode("utf-8")
298 request = Request(
299 "https://dpaste.com/api/v2/",
300 headers={"User-Agent": "IPython v{}".format(version)},
301 )
302 response = urlopen(request, post_data)
303 return response.headers.get('Location')
305 @line_magic
306 def loadpy(self, arg_s):
307 """Alias of `%load`
309 `%loadpy` has gained some flexibility and dropped the requirement of a `.py`
310 extension. So it has been renamed simply into %load. You can look at
311 `%load`'s docstring for more info.
312 """
313 self.load(arg_s)
315 @line_magic
316 def load(self, arg_s):
317 """Load code into the current frontend.
319 Usage:\\
320 %load [options] source
322 where source can be a filename, URL, input history range, macro, or
323 element in the user namespace
325 If no arguments are given, loads the history of this session up to this
326 point.
328 Options:
330 -r <lines>: Specify lines or ranges of lines to load from the source.
331 Ranges could be specified as x-y (x..y) or in python-style x:y
332 (x..(y-1)). Both limits x and y can be left blank (meaning the
333 beginning and end of the file, respectively).
335 -s <symbols>: Specify function or classes to load from python source.
337 -y : Don't ask confirmation for loading source above 200 000 characters.
339 -n : Include the user's namespace when searching for source code.
341 This magic command can either take a local filename, a URL, an history
342 range (see %history) or a macro as argument, it will prompt for
343 confirmation before loading source with more than 200 000 characters, unless
344 -y flag is passed or if the frontend does not support raw_input::
346 %load
347 %load myscript.py
348 %load 7-27
349 %load myMacro
350 %load http://www.example.com/myscript.py
351 %load -r 5-10 myscript.py
352 %load -r 10-20,30,40: foo.py
353 %load -s MyClass,wonder_function myscript.py
354 %load -n MyClass
355 %load -n my_module.wonder_function
356 """
357 opts,args = self.parse_options(arg_s,'yns:r:')
358 search_ns = 'n' in opts
359 contents = self.shell.find_user_code(args, search_ns=search_ns)
361 if 's' in opts:
362 try:
363 blocks, not_found = extract_symbols(contents, opts['s'])
364 except SyntaxError:
365 # non python code
366 error("Unable to parse the input as valid Python code")
367 return
369 if len(not_found) == 1:
370 warn('The symbol `%s` was not found' % not_found[0])
371 elif len(not_found) > 1:
372 warn('The symbols %s were not found' % get_text_list(not_found,
373 wrap_item_with='`')
374 )
376 contents = '\n'.join(blocks)
378 if 'r' in opts:
379 ranges = opts['r'].replace(',', ' ')
380 lines = contents.split('\n')
381 slices = extract_code_ranges(ranges)
382 contents = [lines[slice(*slc)] for slc in slices]
383 contents = '\n'.join(strip_initial_indent(chain.from_iterable(contents)))
385 l = len(contents)
387 # 200 000 is ~ 2500 full 80 character lines
388 # so in average, more than 5000 lines
389 if l > 200000 and 'y' not in opts:
390 try:
391 ans = self.shell.ask_yes_no(("The text you're trying to load seems pretty big"\
392 " (%d characters). Continue (y/[N]) ?" % l), default='n' )
393 except StdinNotImplementedError:
394 #assume yes if raw input not implemented
395 ans = True
397 if ans is False :
398 print('Operation cancelled.')
399 return
401 contents = "# %load {}\n".format(arg_s) + contents
403 self.shell.set_next_input(contents, replace=True)
405 @staticmethod
406 def _find_edit_target(shell, args, opts, last_call):
407 """Utility method used by magic_edit to find what to edit."""
409 def make_filename(arg):
410 "Make a filename from the given args"
411 try:
412 filename = get_py_filename(arg)
413 except IOError:
414 # If it ends with .py but doesn't already exist, assume we want
415 # a new file.
416 if arg.endswith('.py'):
417 filename = arg
418 else:
419 filename = None
420 return filename
422 # Set a few locals from the options for convenience:
423 opts_prev = 'p' in opts
424 opts_raw = 'r' in opts
426 # custom exceptions
427 class DataIsObject(Exception): pass
429 # Default line number value
430 lineno = opts.get('n',None)
432 if opts_prev:
433 args = '_%s' % last_call[0]
434 if args not in shell.user_ns:
435 args = last_call[1]
437 # by default this is done with temp files, except when the given
438 # arg is a filename
439 use_temp = True
441 data = ''
443 # First, see if the arguments should be a filename.
444 filename = make_filename(args)
445 if filename:
446 use_temp = False
447 elif args:
448 # Mode where user specifies ranges of lines, like in %macro.
449 data = shell.extract_input_lines(args, opts_raw)
450 if not data:
451 try:
452 # Load the parameter given as a variable. If not a string,
453 # process it as an object instead (below)
455 #print '*** args',args,'type',type(args) # dbg
456 data = eval(args, shell.user_ns)
457 if not isinstance(data, str):
458 raise DataIsObject
460 except (NameError,SyntaxError):
461 # given argument is not a variable, try as a filename
462 filename = make_filename(args)
463 if filename is None:
464 warn("Argument given (%s) can't be found as a variable "
465 "or as a filename." % args)
466 return (None, None, None)
467 use_temp = False
469 except DataIsObject as e:
470 # macros have a special edit function
471 if isinstance(data, Macro):
472 raise MacroToEdit(data) from e
474 # For objects, try to edit the file where they are defined
475 filename = find_file(data)
476 if filename:
477 if 'fakemodule' in filename.lower() and \
478 inspect.isclass(data):
479 # class created by %edit? Try to find source
480 # by looking for method definitions instead, the
481 # __module__ in those classes is FakeModule.
482 attrs = [getattr(data, aname) for aname in dir(data)]
483 for attr in attrs:
484 if not inspect.ismethod(attr):
485 continue
486 filename = find_file(attr)
487 if filename and \
488 'fakemodule' not in filename.lower():
489 # change the attribute to be the edit
490 # target instead
491 data = attr
492 break
494 m = ipython_input_pat.match(os.path.basename(filename))
495 if m:
496 raise InteractivelyDefined(int(m.groups()[0])) from e
498 datafile = 1
499 if filename is None:
500 filename = make_filename(args)
501 datafile = 1
502 if filename is not None:
503 # only warn about this if we get a real name
504 warn('Could not find file where `%s` is defined.\n'
505 'Opening a file named `%s`' % (args, filename))
506 # Now, make sure we can actually read the source (if it was
507 # in a temp file it's gone by now).
508 if datafile:
509 if lineno is None:
510 lineno = find_source_lines(data)
511 if lineno is None:
512 filename = make_filename(args)
513 if filename is None:
514 warn('The file where `%s` was defined '
515 'cannot be read or found.' % data)
516 return (None, None, None)
517 use_temp = False
519 if use_temp:
520 filename = shell.mktempfile(data)
521 print('IPython will make a temporary file named:',filename)
523 # use last_call to remember the state of the previous call, but don't
524 # let it be clobbered by successive '-p' calls.
525 try:
526 last_call[0] = shell.displayhook.prompt_count
527 if not opts_prev:
528 last_call[1] = args
529 except:
530 pass
533 return filename, lineno, use_temp
535 def _edit_macro(self,mname,macro):
536 """open an editor with the macro data in a file"""
537 filename = self.shell.mktempfile(macro.value)
538 self.shell.hooks.editor(filename)
540 # and make a new macro object, to replace the old one
541 mvalue = Path(filename).read_text(encoding="utf-8")
542 self.shell.user_ns[mname] = Macro(mvalue)
544 @skip_doctest
545 @line_magic
546 def edit(self, parameter_s='',last_call=['','']):
547 """Bring up an editor and execute the resulting code.
549 Usage:
550 %edit [options] [args]
552 %edit runs IPython's editor hook. The default version of this hook is
553 set to call the editor specified by your $EDITOR environment variable.
554 If this isn't found, it will default to vi under Linux/Unix and to
555 notepad under Windows. See the end of this docstring for how to change
556 the editor hook.
558 You can also set the value of this editor via the
559 ``TerminalInteractiveShell.editor`` option in your configuration file.
560 This is useful if you wish to use a different editor from your typical
561 default with IPython (and for Windows users who typically don't set
562 environment variables).
564 This command allows you to conveniently edit multi-line code right in
565 your IPython session.
567 If called without arguments, %edit opens up an empty editor with a
568 temporary file and will execute the contents of this file when you
569 close it (don't forget to save it!).
572 Options:
574 -n <number>: open the editor at a specified line number. By default,
575 the IPython editor hook uses the unix syntax 'editor +N filename', but
576 you can configure this by providing your own modified hook if your
577 favorite editor supports line-number specifications with a different
578 syntax.
580 -p: this will call the editor with the same data as the previous time
581 it was used, regardless of how long ago (in your current session) it
582 was.
584 -r: use 'raw' input. This option only applies to input taken from the
585 user's history. By default, the 'processed' history is used, so that
586 magics are loaded in their transformed version to valid Python. If
587 this option is given, the raw input as typed as the command line is
588 used instead. When you exit the editor, it will be executed by
589 IPython's own processor.
591 -x: do not execute the edited code immediately upon exit. This is
592 mainly useful if you are editing programs which need to be called with
593 command line arguments, which you can then do using %run.
596 Arguments:
598 If arguments are given, the following possibilities exist:
600 - If the argument is a filename, IPython will load that into the
601 editor. It will execute its contents with execfile() when you exit,
602 loading any code in the file into your interactive namespace.
604 - The arguments are ranges of input history, e.g. "7 ~1/4-6".
605 The syntax is the same as in the %history magic.
607 - If the argument is a string variable, its contents are loaded
608 into the editor. You can thus edit any string which contains
609 python code (including the result of previous edits).
611 - If the argument is the name of an object (other than a string),
612 IPython will try to locate the file where it was defined and open the
613 editor at the point where it is defined. You can use `%edit function`
614 to load an editor exactly at the point where 'function' is defined,
615 edit it and have the file be executed automatically.
617 - If the object is a macro (see %macro for details), this opens up your
618 specified editor with a temporary file containing the macro's data.
619 Upon exit, the macro is reloaded with the contents of the file.
621 Note: opening at an exact line is only supported under Unix, and some
622 editors (like kedit and gedit up to Gnome 2.8) do not understand the
623 '+NUMBER' parameter necessary for this feature. Good editors like
624 (X)Emacs, vi, jed, pico and joe all do.
626 After executing your code, %edit will return as output the code you
627 typed in the editor (except when it was an existing file). This way
628 you can reload the code in further invocations of %edit as a variable,
629 via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
630 the output.
632 Note that %edit is also available through the alias %ed.
634 This is an example of creating a simple function inside the editor and
635 then modifying it. First, start up the editor::
637 In [1]: edit
638 Editing... done. Executing edited code...
639 Out[1]: 'def foo():\\n print "foo() was defined in an editing
640 session"\\n'
642 We can then call the function foo()::
644 In [2]: foo()
645 foo() was defined in an editing session
647 Now we edit foo. IPython automatically loads the editor with the
648 (temporary) file where foo() was previously defined::
650 In [3]: edit foo
651 Editing... done. Executing edited code...
653 And if we call foo() again we get the modified version::
655 In [4]: foo()
656 foo() has now been changed!
658 Here is an example of how to edit a code snippet successive
659 times. First we call the editor::
661 In [5]: edit
662 Editing... done. Executing edited code...
663 hello
664 Out[5]: "print 'hello'\\n"
666 Now we call it again with the previous output (stored in _)::
668 In [6]: edit _
669 Editing... done. Executing edited code...
670 hello world
671 Out[6]: "print 'hello world'\\n"
673 Now we call it with the output #8 (stored in _8, also as Out[8])::
675 In [7]: edit _8
676 Editing... done. Executing edited code...
677 hello again
678 Out[7]: "print 'hello again'\\n"
681 Changing the default editor hook:
683 If you wish to write your own editor hook, you can put it in a
684 configuration file which you load at startup time. The default hook
685 is defined in the IPython.core.hooks module, and you can use that as a
686 starting example for further modifications. That file also has
687 general instructions on how to set a new hook for use once you've
688 defined it."""
689 opts,args = self.parse_options(parameter_s,'prxn:')
691 try:
692 filename, lineno, is_temp = self._find_edit_target(self.shell,
693 args, opts, last_call)
694 except MacroToEdit as e:
695 self._edit_macro(args, e.args[0])
696 return
697 except InteractivelyDefined as e:
698 print("Editing In[%i]" % e.index)
699 args = str(e.index)
700 filename, lineno, is_temp = self._find_edit_target(self.shell,
701 args, opts, last_call)
702 if filename is None:
703 # nothing was found, warnings have already been issued,
704 # just give up.
705 return
707 if is_temp:
708 self._knowntemps.add(filename)
709 elif (filename in self._knowntemps):
710 is_temp = True
713 # do actual editing here
714 print('Editing...', end=' ')
715 sys.stdout.flush()
716 filepath = Path(filename)
717 try:
718 # Quote filenames that may have spaces in them when opening
719 # the editor
720 quoted = filename = str(filepath.absolute())
721 if " " in quoted:
722 quoted = "'%s'" % quoted
723 self.shell.hooks.editor(quoted, lineno)
724 except TryNext:
725 warn('Could not open editor')
726 return
728 # XXX TODO: should this be generalized for all string vars?
729 # For now, this is special-cased to blocks created by cpaste
730 if args.strip() == "pasted_block":
731 self.shell.user_ns["pasted_block"] = filepath.read_text(encoding="utf-8")
733 if 'x' in opts: # -x prevents actual execution
734 print()
735 else:
736 print('done. Executing edited code...')
737 with preserve_keys(self.shell.user_ns, '__file__'):
738 if not is_temp:
739 self.shell.user_ns["__file__"] = filename
740 if "r" in opts: # Untranslated IPython code
741 source = filepath.read_text(encoding="utf-8")
742 self.shell.run_cell(source, store_history=False)
743 else:
744 self.shell.safe_execfile(filename, self.shell.user_ns,
745 self.shell.user_ns)
747 if is_temp:
748 try:
749 return filepath.read_text(encoding="utf-8")
750 except IOError as msg:
751 if Path(msg.filename) == filepath:
752 warn('File not found. Did you forget to save?')
753 return
754 else:
755 self.shell.showtraceback()