diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/__init__.py b/packages/python/yap_kernel/yap_ipython/core/magics/__init__.py new file mode 100644 index 000000000..ef88eaca2 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/__init__.py @@ -0,0 +1,41 @@ +"""Implementation of all the magic functions built into yap_ipython. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 +#----------------------------------------------------------------------------- + +from ..magic import Magics, magics_class +from .auto import AutoMagics +from .basic import BasicMagics +from .code import CodeMagics, MacroToEdit +from .config import ConfigMagics +from .display import DisplayMagics +from .execution import ExecutionMagics +from .extension import ExtensionMagics +from .history import HistoryMagics +from .logging import LoggingMagics +from .namespace import NamespaceMagics +from .osm import OSMagics +from .pylab import PylabMagics +from .script import ScriptMagics + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +@magics_class +class UserMagics(Magics): + """Placeholder for user-defined magics to be added at runtime. + + All magics are eventually merged into a single namespace at runtime, but we + use this class to isolate the magics defined dynamically by the user into + their own class. + """ diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/auto.py b/packages/python/yap_kernel/yap_ipython/core/magics/auto.py new file mode 100644 index 000000000..ef1c00c1b --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/auto.py @@ -0,0 +1,128 @@ +"""Implementation of magic functions that control various automatic behaviors. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 +#----------------------------------------------------------------------------- + +# Our own packages +from yap_ipython.core.magic import Bunch, Magics, magics_class, line_magic +from yap_ipython.testing.skipdoctest import skip_doctest +from logging import error + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +@magics_class +class AutoMagics(Magics): + """Magics that control various autoX behaviors.""" + + def __init__(self, shell): + super(AutoMagics, self).__init__(shell) + # namespace for holding state we may need + self._magic_state = Bunch() + + @line_magic + def automagic(self, parameter_s=''): + """Make magic functions callable without having to type the initial %. + + Without arguments toggles on/off (when off, you must call it as + %automagic, of course). With arguments it sets the value, and you can + use any of (case insensitive): + + - on, 1, True: to activate + + - off, 0, False: to deactivate. + + Note that magic functions have lowest priority, so if there's a + variable whose name collides with that of a magic fn, automagic won't + work for that function (you get the variable instead). However, if you + delete the variable (del var), the previously shadowed magic function + becomes visible to automagic again.""" + + arg = parameter_s.lower() + mman = self.shell.magics_manager + if arg in ('on', '1', 'true'): + val = True + elif arg in ('off', '0', 'false'): + val = False + else: + val = not mman.auto_magic + mman.auto_magic = val + print('\n' + self.shell.magics_manager.auto_status()) + + @skip_doctest + @line_magic + def autocall(self, parameter_s=''): + """Make functions callable without having to type parentheses. + + Usage: + + %autocall [mode] + + The mode can be one of: 0->Off, 1->Smart, 2->Full. If not given, the + value is toggled on and off (remembering the previous state). + + In more detail, these values mean: + + 0 -> fully disabled + + 1 -> active, but do not apply if there are no arguments on the line. + + In this mode, you get:: + + In [1]: callable + Out[1]: + + In [2]: callable 'hello' + ------> callable('hello') + Out[2]: False + + 2 -> Active always. Even if no arguments are present, the callable + object is called:: + + In [2]: float + ------> float() + Out[2]: 0.0 + + Note that even with autocall off, you can still use '/' at the start of + a line to treat the first argument on the command line as a function + and add parentheses to it:: + + In [8]: /str 43 + ------> str(43) + Out[8]: '43' + + # all-random (note for auto-testing) + """ + + if parameter_s: + arg = int(parameter_s) + else: + arg = 'toggle' + + if not arg in (0, 1, 2, 'toggle'): + error('Valid modes: (0->Off, 1->Smart, 2->Full') + return + + if arg in (0, 1, 2): + self.shell.autocall = arg + else: # toggle + if self.shell.autocall: + self._magic_state.autocall_save = self.shell.autocall + self.shell.autocall = 0 + else: + try: + self.shell.autocall = self._magic_state.autocall_save + except AttributeError: + self.shell.autocall = self._magic_state.autocall_save = 1 + + print("Automatic calling is:",['OFF','Smart','Full'][self.shell.autocall]) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/basic.py b/packages/python/yap_kernel/yap_ipython/core/magics/basic.py new file mode 100644 index 000000000..42d005aae --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/basic.py @@ -0,0 +1,614 @@ +"""Implementation of basic magic functions.""" + + +import argparse +import textwrap +import io +import sys +from pprint import pformat + +from yap_ipython.core import magic_arguments, page +from yap_ipython.core.error import UsageError +from yap_ipython.core.magic import Magics, magics_class, line_magic, magic_escapes +from yap_ipython.utils.text import format_screen, dedent, indent +from yap_ipython.testing.skipdoctest import skip_doctest +from yap_ipython.utils.ipstruct import Struct +from warnings import warn +from logging import error + + +class MagicsDisplay(object): + def __init__(self, magics_manager, ignore=None): + self.ignore = ignore if ignore else [] + self.magics_manager = magics_manager + + def _lsmagic(self): + """The main implementation of the %lsmagic""" + mesc = magic_escapes['line'] + cesc = magic_escapes['cell'] + mman = self.magics_manager + magics = mman.lsmagic() + out = ['Available line magics:', + mesc + (' '+mesc).join(sorted([m for m,v in magics['line'].items() if (v not in self.ignore)])), + '', + 'Available cell magics:', + cesc + (' '+cesc).join(sorted([m for m,v in magics['cell'].items() if (v not in self.ignore)])), + '', + mman.auto_status()] + return '\n'.join(out) + + def _repr_pretty_(self, p, cycle): + p.text(self._lsmagic()) + + def __str__(self): + return self._lsmagic() + + def _jsonable(self): + """turn magics dict into jsonable dict of the same structure + + replaces object instances with their class names as strings + """ + magic_dict = {} + mman = self.magics_manager + magics = mman.lsmagic() + for key, subdict in magics.items(): + d = {} + magic_dict[key] = d + for name, obj in subdict.items(): + try: + classname = obj.__self__.__class__.__name__ + except AttributeError: + classname = 'Other' + + d[name] = classname + return magic_dict + + def _repr_json_(self): + return self._jsonable() + + +@magics_class +class BasicMagics(Magics): + """Magics that provide central yap_ipython functionality. + + These are various magics that don't fit into specific categories but that + are all part of the base 'yap_ipython experience'.""" + + @magic_arguments.magic_arguments() + @magic_arguments.argument( + '-l', '--line', action='store_true', + help="""Create a line magic alias.""" + ) + @magic_arguments.argument( + '-c', '--cell', action='store_true', + help="""Create a cell magic alias.""" + ) + @magic_arguments.argument( + 'name', + help="""Name of the magic to be created.""" + ) + @magic_arguments.argument( + 'target', + help="""Name of the existing line or cell magic.""" + ) + @magic_arguments.argument( + '-p', '--params', default=None, + help="""Parameters passed to the magic function.""" + ) + @line_magic + def alias_magic(self, line=''): + """Create an alias for an existing line or cell magic. + + Examples + -------- + :: + + In [1]: %alias_magic t timeit + Created `%t` as an alias for `%timeit`. + Created `%%t` as an alias for `%%timeit`. + + In [2]: %t -n1 pass + 1 loops, best of 3: 954 ns per loop + + In [3]: %%t -n1 + ...: pass + ...: + 1 loops, best of 3: 954 ns per loop + + In [4]: %alias_magic --cell whereami pwd + UsageError: Cell magic function `%%pwd` not found. + In [5]: %alias_magic --line whereami pwd + Created `%whereami` as an alias for `%pwd`. + + In [6]: %whereami + Out[6]: u'/home/testuser' + + In [7]: %alias_magic h history -p "-l 30" --line + Created `%h` as an alias for `%history -l 30`. + """ + + args = magic_arguments.parse_argstring(self.alias_magic, line) + shell = self.shell + mman = self.shell.magics_manager + escs = ''.join(magic_escapes.values()) + + target = args.target.lstrip(escs) + name = args.name.lstrip(escs) + + params = args.params + if (params and + ((params.startswith('"') and params.endswith('"')) + or (params.startswith("'") and params.endswith("'")))): + params = params[1:-1] + + # Find the requested magics. + m_line = shell.find_magic(target, 'line') + m_cell = shell.find_magic(target, 'cell') + if args.line and m_line is None: + raise UsageError('Line magic function `%s%s` not found.' % + (magic_escapes['line'], target)) + if args.cell and m_cell is None: + raise UsageError('Cell magic function `%s%s` not found.' % + (magic_escapes['cell'], target)) + + # If --line and --cell are not specified, default to the ones + # that are available. + if not args.line and not args.cell: + if not m_line and not m_cell: + raise UsageError( + 'No line or cell magic with name `%s` found.' % target + ) + args.line = bool(m_line) + args.cell = bool(m_cell) + + params_str = "" if params is None else " " + params + + if args.line: + mman.register_alias(name, target, 'line', params) + print('Created `%s%s` as an alias for `%s%s%s`.' % ( + magic_escapes['line'], name, + magic_escapes['line'], target, params_str)) + + if args.cell: + mman.register_alias(name, target, 'cell', params) + print('Created `%s%s` as an alias for `%s%s%s`.' % ( + magic_escapes['cell'], name, + magic_escapes['cell'], target, params_str)) + + @line_magic + def lsmagic(self, parameter_s=''): + """List currently available magic functions.""" + return MagicsDisplay(self.shell.magics_manager, ignore=[self.pip]) + + def _magic_docs(self, brief=False, rest=False): + """Return docstrings from magic functions.""" + mman = self.shell.magics_manager + docs = mman.lsmagic_docs(brief, missing='No documentation') + + if rest: + format_string = '**%s%s**::\n\n%s\n\n' + else: + format_string = '%s%s:\n%s\n' + + return ''.join( + [format_string % (magic_escapes['line'], fname, + indent(dedent(fndoc))) + for fname, fndoc in sorted(docs['line'].items())] + + + [format_string % (magic_escapes['cell'], fname, + indent(dedent(fndoc))) + for fname, fndoc in sorted(docs['cell'].items())] + ) + + @line_magic + def magic(self, parameter_s=''): + """Print information about the magic function system. + + Supported formats: -latex, -brief, -rest + """ + + mode = '' + try: + mode = parameter_s.split()[0][1:] + except IndexError: + pass + + brief = (mode == 'brief') + rest = (mode == 'rest') + magic_docs = self._magic_docs(brief, rest) + + if mode == 'latex': + print(self.format_latex(magic_docs)) + return + else: + magic_docs = format_screen(magic_docs) + + out = [""" +yap_ipython's 'magic' functions +=========================== + +The magic function system provides a series of functions which allow you to +control the behavior of yap_ipython itself, plus a lot of system-type +features. There are two kinds of magics, line-oriented and cell-oriented. + +Line magics are prefixed with the % character and work much like OS +command-line calls: they get as an argument the rest of the line, where +arguments are passed without parentheses or quotes. For example, this will +time the given statement:: + + %timeit range(1000) + +Cell magics are prefixed with a double %%, and they are functions that get as +an argument not only the rest of the line, but also the lines below it in a +separate argument. These magics are called with two arguments: the rest of the +call line and the body of the cell, consisting of the lines below the first. +For example:: + + %%timeit x = numpy.random.randn((100, 100)) + numpy.linalg.svd(x) + +will time the execution of the numpy svd routine, running the assignment of x +as part of the setup phase, which is not timed. + +In a line-oriented client (the terminal or Qt console yap_ipython), starting a new +input with %% will automatically enter cell mode, and yap_ipython will continue +reading input until a blank line is given. In the notebook, simply type the +whole cell as one entity, but keep in mind that the %% escape can only be at +the very start of the cell. + +NOTE: If you have 'automagic' enabled (via the command line option or with the +%automagic function), you don't need to type in the % explicitly for line +magics; cell magics always require an explicit '%%' escape. By default, +yap_ipython ships with automagic on, so you should only rarely need the % escape. + +Example: typing '%cd mydir' (without the quotes) changes your working directory +to 'mydir', if it exists. + +For a list of the available magic functions, use %lsmagic. For a description +of any of them, type %magic_name?, e.g. '%cd?'. + +Currently the magic system has the following functions:""", + magic_docs, + "Summary of magic functions (from %slsmagic):" % magic_escapes['line'], + str(self.lsmagic()), + ] + page.page('\n'.join(out)) + + + @line_magic + def page(self, parameter_s=''): + """Pretty print the object and display it through a pager. + + %page [options] OBJECT + + If no object is given, use _ (last output). + + Options: + + -r: page str(object), don't pretty-print it.""" + + # After a function contributed by Olivier Aubert, slightly modified. + + # Process options/args + opts, args = self.parse_options(parameter_s, 'r') + raw = 'r' in opts + + oname = args and args or '_' + info = self.shell._ofind(oname) + if info['found']: + txt = (raw and str or pformat)( info['obj'] ) + page.page(txt) + else: + print('Object `%s` not found' % oname) + + @line_magic + def profile(self, parameter_s=''): + """DEPRECATED since yap_ipython 2.0. + + Raise `UsageError`. To profile code use the :magic:`prun` magic. + + + See Also + -------- + prun : run code using the Python profiler (:magic:`prun`) + """ + raise UsageError("The `%profile` magic has been deprecated since yap_ipython 2.0. " + "and removed in yap_ipython 6.0. Please use the value of `get_ipython().profile` instead " + "to see current profile in use. Perhaps you meant to use `%prun` to profile code?") + + @line_magic + def pprint(self, parameter_s=''): + """Toggle pretty printing on/off.""" + ptformatter = self.shell.display_formatter.formatters['text/plain'] + ptformatter.pprint = bool(1 - ptformatter.pprint) + print('Pretty printing has been turned', + ['OFF','ON'][ptformatter.pprint]) + + @line_magic + def colors(self, parameter_s=''): + """Switch color scheme for prompts, info system and exception handlers. + + Currently implemented schemes: NoColor, Linux, LightBG. + + Color scheme names are not case-sensitive. + + Examples + -------- + To get a plain black and white terminal:: + + %colors nocolor + """ + def color_switch_err(name): + warn('Error changing %s color schemes.\n%s' % + (name, sys.exc_info()[1]), stacklevel=2) + + + new_scheme = parameter_s.strip() + if not new_scheme: + raise UsageError( + "%colors: you must specify a color scheme. See '%colors?'") + # local shortcut + shell = self.shell + + # Set shell colour scheme + try: + shell.colors = new_scheme + shell.refresh_style() + except: + color_switch_err('shell') + + # Set exception colors + try: + shell.InteractiveTB.set_colors(scheme = new_scheme) + shell.SyntaxTB.set_colors(scheme = new_scheme) + except: + color_switch_err('exception') + + # Set info (for 'object?') colors + if shell.color_info: + try: + shell.inspector.set_active_scheme(new_scheme) + except: + color_switch_err('object inspector') + else: + shell.inspector.set_active_scheme('NoColor') + + @line_magic + def xmode(self, parameter_s=''): + """Switch modes for the exception handlers. + + Valid modes: Plain, Context and Verbose. + + If called without arguments, acts as a toggle.""" + + def xmode_switch_err(name): + warn('Error changing %s exception modes.\n%s' % + (name,sys.exc_info()[1])) + + shell = self.shell + new_mode = parameter_s.strip().capitalize() + try: + shell.InteractiveTB.set_mode(mode=new_mode) + print('Exception reporting mode:',shell.InteractiveTB.mode) + except: + xmode_switch_err('user') + + @line_magic + def pip(self, args=''): + """ + Intercept usage of ``pip`` in yap_ipython and direct user to run command outside of yap_ipython. + """ + print(textwrap.dedent(''' + The following command must be run outside of the yap_ipython shell: + + $ pip {args} + + The Python package manager (pip) can only be used from outside of yap_ipython. + Please reissue the `pip` command in a separate terminal or command prompt. + + See the Python documentation for more information on how to install packages: + + https://docs.python.org/3/installing/'''.format(args=args))) + + @line_magic + def quickref(self, arg): + """ Show a quick reference sheet """ + from yap_ipython.core.usage import quick_reference + qr = quick_reference + self._magic_docs(brief=True) + page.page(qr) + + @line_magic + def doctest_mode(self, parameter_s=''): + """Toggle doctest mode on and off. + + This mode is intended to make yap_ipython behave as much as possible like a + plain Python shell, from the perspective of how its prompts, exceptions + and output look. This makes it easy to copy and paste parts of a + session into doctests. It does so by: + + - Changing the prompts to the classic ``>>>`` ones. + - Changing the exception reporting mode to 'Plain'. + - Disabling pretty-printing of output. + + Note that yap_ipython also supports the pasting of code snippets that have + leading '>>>' and '...' prompts in them. This means that you can paste + doctests from files or docstrings (even if they have leading + whitespace), and the code will execute correctly. You can then use + '%history -t' to see the translated history; this will give you the + input after removal of all the leading prompts and whitespace, which + can be pasted back into an editor. + + With these features, you can switch into this mode easily whenever you + need to do testing and changes to doctests, without having to leave + your existing yap_ipython session. + """ + + # Shorthands + shell = self.shell + meta = shell.meta + disp_formatter = self.shell.display_formatter + ptformatter = disp_formatter.formatters['text/plain'] + # dstore is a data store kept in the instance metadata bag to track any + # changes we make, so we can undo them later. + dstore = meta.setdefault('doctest_mode',Struct()) + save_dstore = dstore.setdefault + + # save a few values we'll need to recover later + mode = save_dstore('mode',False) + save_dstore('rc_pprint',ptformatter.pprint) + save_dstore('xmode',shell.InteractiveTB.mode) + save_dstore('rc_separate_out',shell.separate_out) + save_dstore('rc_separate_out2',shell.separate_out2) + save_dstore('rc_separate_in',shell.separate_in) + save_dstore('rc_active_types',disp_formatter.active_types) + + if not mode: + # turn on + + # Prompt separators like plain python + shell.separate_in = '' + shell.separate_out = '' + shell.separate_out2 = '' + + + ptformatter.pprint = False + disp_formatter.active_types = ['text/plain'] + + shell.magic('xmode Plain') + else: + # turn off + shell.separate_in = dstore.rc_separate_in + + shell.separate_out = dstore.rc_separate_out + shell.separate_out2 = dstore.rc_separate_out2 + + ptformatter.pprint = dstore.rc_pprint + disp_formatter.active_types = dstore.rc_active_types + + shell.magic('xmode ' + dstore.xmode) + + # mode here is the state before we switch; switch_doctest_mode takes + # the mode we're switching to. + shell.switch_doctest_mode(not mode) + + # Store new mode and inform + dstore.mode = bool(not mode) + mode_label = ['OFF','ON'][dstore.mode] + print('Doctest mode is:', mode_label) + + @line_magic + def gui(self, parameter_s=''): + """Enable or disable yap_ipython GUI event loop integration. + + %gui [GUINAME] + + This magic replaces yap_ipython's threaded shells that were activated + using the (pylab/wthread/etc.) command line flags. GUI toolkits + can now be enabled at runtime and keyboard + interrupts should work without any problems. The following toolkits + are supported: wxPython, PyQt4, PyGTK, Tk and Cocoa (OSX):: + + %gui wx # enable wxPython event loop integration + %gui qt4|qt # enable PyQt4 event loop integration + %gui qt5 # enable PyQt5 event loop integration + %gui gtk # enable PyGTK event loop integration + %gui gtk3 # enable Gtk3 event loop integration + %gui tk # enable Tk event loop integration + %gui osx # enable Cocoa event loop integration + # (requires %matplotlib 1.1) + %gui # disable all event loop integration + + WARNING: after any of these has been called you can simply create + an application object, but DO NOT start the event loop yourself, as + we have already handled that. + """ + opts, arg = self.parse_options(parameter_s, '') + if arg=='': arg = None + try: + return self.shell.enable_gui(arg) + except Exception as e: + # print simple error message, rather than traceback if we can't + # hook up the GUI + error(str(e)) + + @skip_doctest + @line_magic + def precision(self, s=''): + """Set floating point precision for pretty printing. + + Can set either integer precision or a format string. + + If numpy has been imported and precision is an int, + numpy display precision will also be set, via ``numpy.set_printoptions``. + + If no argument is given, defaults will be restored. + + Examples + -------- + :: + + In [1]: from math import pi + + In [2]: %precision 3 + Out[2]: u'%.3f' + + In [3]: pi + Out[3]: 3.142 + + In [4]: %precision %i + Out[4]: u'%i' + + In [5]: pi + Out[5]: 3 + + In [6]: %precision %e + Out[6]: u'%e' + + In [7]: pi**10 + Out[7]: 9.364805e+04 + + In [8]: %precision + Out[8]: u'%r' + + In [9]: pi**10 + Out[9]: 93648.047476082982 + """ + ptformatter = self.shell.display_formatter.formatters['text/plain'] + ptformatter.float_precision = s + return ptformatter.float_format + + @magic_arguments.magic_arguments() + @magic_arguments.argument( + '-e', '--export', action='store_true', default=False, + help=argparse.SUPPRESS + ) + @magic_arguments.argument( + 'filename', type=str, + help='Notebook name or filename' + ) + @line_magic + def notebook(self, s): + """Export and convert yap_ipython notebooks. + + This function can export the current yap_ipython history to a notebook file. + For example, to export the history to "foo.ipynb" do "%notebook foo.ipynb". + + The -e or --export flag is deprecated in yap_ipython 5.2, and will be + removed in the future. + """ + args = magic_arguments.parse_argstring(self.notebook, s) + + from nbformat import write, v4 + + cells = [] + hist = list(self.shell.history_manager.get_range()) + if(len(hist)<=1): + raise ValueError('History is empty, cannot export') + for session, execution_count, source in hist[:-1]: + cells.append(v4.new_code_cell( + execution_count=execution_count, + source=source + )) + nb = v4.new_notebook(cells=cells) + with io.open(args.filename, 'w', encoding='utf-8') as f: + write(nb, f, version=4) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/code.py b/packages/python/yap_kernel/yap_ipython/core/magics/code.py new file mode 100644 index 000000000..37dd8400a --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/code.py @@ -0,0 +1,740 @@ +"""Implementation of code management magic functions. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 +import re +import sys +import ast +from itertools import chain + +# Our own packages +from yap_ipython.core.error import TryNext, StdinNotImplementedError, UsageError +from yap_ipython.core.macro import Macro +from yap_ipython.core.magic import Magics, magics_class, line_magic +from yap_ipython.core.oinspect import find_file, find_source_lines +from yap_ipython.testing.skipdoctest import skip_doctest +from yap_ipython.utils import py3compat +from yap_ipython.utils.contexts import preserve_keys +from yap_ipython.utils.path import get_py_filename +from warnings import warn +from logging import error +from yap_ipython.utils.text import get_text_list + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +# Used for exception handling in magic_edit +class MacroToEdit(ValueError): pass + +ipython_input_pat = re.compile(r"$") + +# To match, e.g. 8-10 1:5 :10 3- +range_re = re.compile(r""" +(?P\d+)? +((?P[\-:]) + (?P\d+)?)? +$""", re.VERBOSE) + + +def extract_code_ranges(ranges_str): + """Turn a string of range for %%load into 2-tuples of (start, stop) + ready to use as a slice of the content splitted by lines. + + 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) + + +def extract_symbols(code, symbols): + """ + 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:: + + In [1]: code = '''a = 10 + ...: def b(): return 42 + ...: class A: pass''' + + In [2]: extract_symbols(code, 'A,b,z') + Out[2]: (['class A: pass\\n', 'def b(): return 42\\n'], ['z']) + """ + symbols = symbols.split(',') + + # this will raise SyntaxError if code isn't valid Python + py_code = ast.parse(code) + + marks = [(getattr(s, 'name', None), s.lineno) for s in py_code.body] + code = code.split('\n') + + symbols_lines = {} + + # 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) + for name, start in reversed(marks): + while not code[end - 1].strip(): + end -= 1 + if name: + symbols_lines[name] = (start - 1, end) + end = start - 1 + + # Now symbols_lines is a map + # {'symbol_name': (start_lineno, end_lineno), ...} + + # fill a list with chunks of codes for each requested symbol + blocks = [] + not_found = [] + for symbol in symbols: + if symbol in symbols_lines: + start, end = symbols_lines[symbol] + blocks.append('\n'.join(code[start:end]) + '\n') + else: + not_found.append(symbol) + + return blocks, not_found + +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 + + +class InteractivelyDefined(Exception): + """Exception for interactively defined variable in magic_edit""" + def __init__(self, index): + self.index = index + + +@magics_class +class CodeMagics(Magics): + """Magics related to code management (loading, saving, editing, ...).""" + + def __init__(self, *args, **kwargs): + self._knowntemps = set() + super(CodeMagics, self).__init__(*args, **kwargs) + + @line_magic + def save(self, parameter_s=''): + """Save a set of lines or a macro to a given filename. + + Usage:\\ + %save [options] filename n1-n2 n3-n4 ... n5 .. n6 ... + + 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. + + -f: force overwrite. If file exists, %save will prompt for overwrite + unless -f is given. + + -a: append to the file instead of overwriting it. + + This function uses the same syntax as %history for input ranges, + then saves the lines to the filename you specify. + + It adds a '.py' extension to the file if you don't do so yourself, and + it asks for confirmation before overwriting existing files. + + If `-r` option is used, the default extension is `.ipy`. + """ + + opts,args = self.parse_options(parameter_s,'fra',mode='list') + if not args: + raise UsageError('Missing filename.') + raw = 'r' in opts + force = 'f' in opts + append = 'a' in opts + mode = 'a' if append else 'w' + ext = u'.ipy' if raw else u'.py' + fname, codefrom = args[0], " ".join(args[1:]) + if not fname.endswith((u'.py',u'.ipy')): + fname += ext + file_exists = os.path.isfile(fname) + if file_exists and not force and not append: + try: + overwrite = self.shell.ask_yes_no('File `%s` exists. Overwrite (y/[N])? ' % fname, default='n') + except StdinNotImplementedError: + print("File `%s` exists. Use `%%save -f %s` to force overwrite" % (fname, parameter_s)) + return + if not overwrite : + print('Operation cancelled.') + return + try: + cmds = self.shell.find_user_code(codefrom,raw) + except (TypeError, ValueError) as e: + print(e.args[0]) + return + out = py3compat.cast_unicode(cmds) + with io.open(fname, mode, encoding="utf-8") as f: + if not file_exists or not append: + f.write(u"# coding: utf-8\n") + f.write(out) + # make sure we end on a newline + if not out.endswith(u'\n'): + f.write(u'\n') + print('The following commands were written to file `%s`:' % fname) + print(cmds) + + @line_magic + def pastebin(self, parameter_s=''): + """Upload code to Github's Gist paste bin, returning the URL. + + Usage:\\ + %pastebin [-d "Custom description"] 1-7 + + The argument can be an input history range, a filename, or the name of a + string or macro. + + Options: + + -d: Pass a custom description for the gist. The default will say + "Pasted from yap_ipython". + """ + opts, args = self.parse_options(parameter_s, 'd:') + + try: + code = self.shell.find_user_code(args) + except (ValueError, TypeError) as e: + print(e.args[0]) + return + + # Deferred import + try: + from urllib.request import urlopen # Py 3 + except ImportError: + from urllib2 import urlopen + import json + post_data = json.dumps({ + "description": opts.get('d', "Pasted from yap_ipython"), + "public": True, + "files": { + "file1.py": { + "content": code + } + } + }).encode('utf-8') + + response = urlopen("https://api.github.com/gists", post_data) + response_data = json.loads(response.read().decode('utf-8')) + return response_data['html_url'] + + @line_magic + def loadpy(self, arg_s): + """Alias of `%load` + + `%loadpy` has gained some flexibility and dropped the requirement of a `.py` + extension. So it has been renamed simply into %load. You can look at + `%load`'s docstring for more info. + """ + self.load(arg_s) + + @line_magic + def load(self, arg_s): + """Load code into the current frontend. + + Usage:\\ + %load [options] source + + where source can be a filename, URL, input history range, macro, or + element in the user namespace + + Options: + + -r : 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). + + -s : Specify function or classes to load from python source. + + -y : Don't ask confirmation for loading source above 200 000 characters. + + -n : Include the user's namespace when searching for source code. + + 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:: + + %load myscript.py + %load 7-27 + %load myMacro + %load http://www.example.com/myscript.py + %load -r 5-10 myscript.py + %load -r 10-20,30,40: foo.py + %load -s MyClass,wonder_function myscript.py + %load -n MyClass + %load -n my_module.wonder_function + """ + opts,args = self.parse_options(arg_s,'yns:r:') + + if not args: + raise UsageError('Missing filename, URL, input history range, ' + 'macro, or element in the user namespace.') + + search_ns = 'n' in opts + + contents = self.shell.find_user_code(args, search_ns=search_ns) + + if 's' in opts: + 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 + + 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) + + 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] + contents = '\n'.join(strip_initial_indent(chain.from_iterable(contents))) + + l = len(contents) + + # 200 000 is ~ 2500 full 80 character lines + # 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: + #assume yes if raw input not implemented + ans = True + + if ans is False : + print('Operation cancelled.') + return + + contents = "# %load {}\n".format(arg_s) + contents + + self.shell.set_next_input(contents, replace=True) + + @staticmethod + def _find_edit_target(shell, args, opts, last_call): + """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] + if args not in shell.user_ns: + 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. + data = shell.extract_input_lines(args, opts_raw) + if not data: + try: + # Load the parameter given as a variable. If not a string, + # process it as an object instead (below) + + #print '*** args',args,'type',type(args) # dbg + data = eval(args, shell.user_ns) + if not isinstance(data, str): + 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) + return (None, None, None) + use_temp = False + + except DataIsObject: + # macros have a special edit function + if isinstance(data, Macro): + raise MacroToEdit(data) + + # For objects, try to edit the file where they are defined + filename = find_file(data) + if filename: + 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 + filename = find_file(attr) + if filename and \ + 'fakemodule' not in filename.lower(): + # change the attribute to be the edit + # target instead + data = attr + break + + m = ipython_input_pat.match(os.path.basename(filename)) + if m: + raise InteractivelyDefined(int(m.groups()[0])) + + datafile = 1 + if filename is None: + filename = make_filename(args) + datafile = 1 + 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' + 'Opening a file named `%s`' % (args, filename)) + # Now, make sure we can actually read the source (if it was + # in a temp file it's gone by now). + if datafile: + if lineno is None: + lineno = find_source_lines(data) + if lineno is None: + filename = make_filename(args) + if filename is None: + warn('The file where `%s` was defined ' + 'cannot be read or found.' % data) + return (None, None, None) + use_temp = False + + if use_temp: + filename = shell.mktempfile(data) + print('yap_ipython will make a temporary file named:',filename) + + # 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 + + + 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 + with open(filename) as mfile: + mvalue = mfile.read() + 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 yap_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 yap_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 yap_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 : open the editor at a specified line number. By default, + the yap_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 + yap_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, yap_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), + yap_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 _ or Out[], where 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:: + + In [1]: edit + Editing... done. Executing edited code... + Out[1]: 'def foo():\\n print "foo() was defined in an editing + session"\\n' + + We can then call the function foo():: + + In [2]: foo() + foo() was defined in an editing session + + Now we edit foo. yap_ipython automatically loads the editor with the + (temporary) file where foo() was previously defined:: + + In [3]: edit foo + 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:: + + In [5]: edit + Editing... done. Executing edited code... + hello + Out[5]: "print 'hello'\\n" + + Now we call it again with the previous output (stored in _):: + + In [6]: edit _ + Editing... done. Executing edited code... + hello world + Out[6]: "print 'hello world'\\n" + + Now we call it with the output #8 (stored in _8, also as Out[8]):: + + In [7]: edit _8 + Editing... done. Executing edited code... + hello again + Out[7]: "print 'hello again'\\n" + + + 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 yap_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: + filename, lineno, is_temp = self._find_edit_target(self.shell, + args, opts, last_call) + except MacroToEdit as e: + self._edit_macro(args, e.args[0]) + return + except InteractivelyDefined as e: + print("Editing In[%i]" % e.index) + args = str(e.index) + filename, lineno, is_temp = self._find_edit_target(self.shell, + args, opts, last_call) + if filename is None: + # nothing was found, warnings have already been issued, + # just give up. + return + + if is_temp: + self._knowntemps.add(filename) + elif (filename in self._knowntemps): + is_temp = True + + + # do actual editing here + print('Editing...', end=' ') + sys.stdout.flush() + try: + # Quote filenames that may have spaces in them + if ' ' in filename: + filename = "'%s'" % filename + self.shell.hooks.editor(filename,lineno) + 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 + if args.strip() == 'pasted_block': + with open(filename, 'r') as f: + self.shell.user_ns['pasted_block'] = f.read() + + if 'x' in opts: # -x prevents actual execution + print() + else: + print('done. Executing edited code...') + with preserve_keys(self.shell.user_ns, '__file__'): + if not is_temp: + self.shell.user_ns['__file__'] = filename + if 'r' in opts: # Untranslated yap_ipython code + with open(filename, 'r') as f: + source = f.read() + self.shell.run_cell(source, store_history=False) + else: + self.shell.safe_execfile(filename, self.shell.user_ns, + self.shell.user_ns) + + if is_temp: + try: + return open(filename).read() + except IOError as msg: + if msg.filename == filename: + warn('File not found. Did you forget to save?') + return + else: + self.shell.showtraceback() diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/config.py b/packages/python/yap_kernel/yap_ipython/core/magics/config.py new file mode 100644 index 000000000..e8eb1f5df --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/config.py @@ -0,0 +1,158 @@ +"""Implementation of configuration-related magic functions. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 re + +# Our own packages +from yap_ipython.core.error import UsageError +from yap_ipython.core.magic import Magics, magics_class, line_magic +from logging import error + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +reg = re.compile('^\w+\.\w+$') +@magics_class +class ConfigMagics(Magics): + + def __init__(self, shell): + super(ConfigMagics, self).__init__(shell) + self.configurables = [] + + @line_magic + def config(self, s): + """configure yap_ipython + + %config Class[.trait=value] + + This magic exposes most of the yap_ipython config system. Any + Configurable class should be able to be configured with the simple + line:: + + %config Class.trait=value + + Where `value` will be resolved in the user's namespace, if it is an + expression or variable name. + + Examples + -------- + + To see what classes are available for config, pass no arguments:: + + In [1]: %config + Available objects for config: + TerminalInteractiveShell + HistoryManager + PrefilterManager + AliasManager + IPCompleter + DisplayFormatter + + To view what is configurable on a given class, just pass the class + name:: + + In [2]: %config IPCompleter + IPCompleter options + ----------------- + IPCompleter.omit__names= + Current: 2 + Choices: (0, 1, 2) + Instruct the completer to omit private method names + Specifically, when completing on ``object.``. + When 2 [default]: all names that start with '_' will be excluded. + When 1: all 'magic' names (``__foo__``) will be excluded. + When 0: nothing will be excluded. + IPCompleter.merge_completions= + Current: True + Whether to merge completion results into a single list + If False, only the completion results from the first non-empty + completer will be returned. + IPCompleter.limit_to__all__= + Current: False + Instruct the completer to use __all__ for the completion + Specifically, when completing on ``object.``. + When True: only those names in obj.__all__ will be included. + When False [default]: the __all__ attribute is ignored + IPCompleter.greedy= + Current: False + Activate greedy completion + This will enable completion on elements of lists, results of + function calls, etc., but can be unsafe because the code is + actually evaluated on TAB. + + but the real use is in setting values:: + + In [3]: %config IPCompleter.greedy = True + + and these values are read from the user_ns if they are variables:: + + In [4]: feeling_greedy=False + + In [5]: %config IPCompleter.greedy = feeling_greedy + + """ + from traitlets.config.loader import Config + # some yap_ipython objects are Configurable, but do not yet have + # any configurable traits. Exclude them from the effects of + # this magic, as their presence is just noise: + configurables = sorted(set([ c for c in self.shell.configurables + if c.__class__.class_traits(config=True) + ]), key=lambda x: x.__class__.__name__) + classnames = [ c.__class__.__name__ for c in configurables ] + + line = s.strip() + if not line: + # print available configurable names + print("Available objects for config:") + for name in classnames: + print(" ", name) + return + elif line in classnames: + # `%config TerminalInteractiveShell` will print trait info for + # TerminalInteractiveShell + c = configurables[classnames.index(line)] + cls = c.__class__ + help = cls.class_get_help(c) + # strip leading '--' from cl-args: + help = re.sub(re.compile(r'^--', re.MULTILINE), '', help) + print(help) + return + elif reg.match(line): + cls, attr = line.split('.') + return getattr(configurables[classnames.index(cls)],attr) + elif '=' not in line: + msg = "Invalid config statement: %r, "\ + "should be `Class.trait = value`." + + ll = line.lower() + for classname in classnames: + if ll == classname.lower(): + msg = msg + '\nDid you mean %s (note the case)?' % classname + break + + raise UsageError( msg % line) + + # otherwise, assume we are setting configurables. + # leave quotes on args when splitting, because we want + # unquoted args to eval in user_ns + cfg = Config() + exec("cfg."+line, locals(), self.shell.user_ns) + + for configurable in configurables: + try: + configurable.update_config(cfg) + except Exception as e: + error(e) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/display.py b/packages/python/yap_kernel/yap_ipython/core/magics/display.py new file mode 100644 index 000000000..63a5f1507 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/display.py @@ -0,0 +1,70 @@ +"""Simple magics for display formats""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 +#----------------------------------------------------------------------------- + +# Our own packages +from yap_ipython.core.display import display, Javascript, Latex, SVG, HTML, Markdown +from yap_ipython.core.magic import ( + Magics, magics_class, cell_magic +) + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + + +@magics_class +class DisplayMagics(Magics): + """Magics for displaying various output types with literals + + Defines javascript/latex/svg/html cell magics for writing + blocks in those languages, to be rendered in the frontend. + """ + + @cell_magic + def js(self, line, cell): + """Run the cell block of Javascript code + + Alias of `%%javascript` + """ + self.javascript(line, cell) + + @cell_magic + def javascript(self, line, cell): + """Run the cell block of Javascript code""" + display(Javascript(cell)) + + + @cell_magic + def latex(self, line, cell): + """Render the cell as a block of latex + + The subset of latex which is support depends on the implementation in + the client. In the Jupyter Notebook, this magic only renders the subset + of latex defined by MathJax + [here](https://docs.mathjax.org/en/v2.5-latest/tex.html).""" + display(Latex(cell)) + + @cell_magic + def svg(self, line, cell): + """Render the cell as an SVG literal""" + display(SVG(cell)) + + @cell_magic + def html(self, line, cell): + """Render the cell as a block of HTML""" + display(HTML(cell)) + + @cell_magic + def markdown(self, line, cell): + """Render the cell as Markdown text block""" + display(Markdown(cell)) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/execution.py b/packages/python/yap_kernel/yap_ipython/core/magics/execution.py new file mode 100644 index 000000000..64ca193d7 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/execution.py @@ -0,0 +1,1424 @@ +# -*- coding: utf-8 -*- +"""Implementation of execution-related magic functions.""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + + +import ast +import bdb +import builtins as builtin_mod +import gc +import itertools +import os +import shlex +import sys +import time +import timeit +import math +from pdb import Restart + +# cProfile was added in Python2.5 +try: + import cProfile as profile + import pstats +except ImportError: + # profile isn't bundled by default in Debian for license reasons + try: + import profile, pstats + except ImportError: + profile = pstats = None + +from yap_ipython.core import oinspect +from yap_ipython.core import magic_arguments +from yap_ipython.core import page +from yap_ipython.core.error import UsageError +from yap_ipython.core.macro import Macro +from yap_ipython.core.magic import (Magics, magics_class, line_magic, cell_magic, + line_cell_magic, on_off, needs_local_scope) +from yap_ipython.testing.skipdoctest import skip_doctest +from yap_ipython.utils.contexts import preserve_keys +from yap_ipython.utils.capture import capture_output +from yap_ipython.utils.ipstruct import Struct +from yap_ipython.utils.module_paths import find_mod +from yap_ipython.utils.path import get_py_filename, shellglob +from yap_ipython.utils.timing import clock, clock2 +from warnings import warn +from logging import error +from io import StringIO + + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + + +class TimeitResult(object): + """ + Object returned by the timeit magic with info about the run. + + Contains the following attributes : + + loops: (int) number of loops done per measurement + repeat: (int) number of times the measurement has been repeated + best: (float) best execution time / number + all_runs: (list of float) execution time of each run (in s) + compile_time: (float) time of statement compilation (s) + + """ + def __init__(self, loops, repeat, best, worst, all_runs, compile_time, precision): + self.loops = loops + self.repeat = repeat + self.best = best + self.worst = worst + self.all_runs = all_runs + self.compile_time = compile_time + self._precision = precision + self.timings = [ dt / self.loops for dt in all_runs] + + @property + def average(self): + return math.fsum(self.timings) / len(self.timings) + + @property + def stdev(self): + mean = self.average + return (math.fsum([(x - mean) ** 2 for x in self.timings]) / len(self.timings)) ** 0.5 + + def __str__(self): + pm = '+-' + if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding: + try: + u'\xb1'.encode(sys.stdout.encoding) + pm = u'\xb1' + except: + pass + return ( + u"{mean} {pm} {std} per loop (mean {pm} std. dev. of {runs} run{run_plural}, {loops} loop{loop_plural} each)" + .format( + pm = pm, + runs = self.repeat, + loops = self.loops, + loop_plural = "" if self.loops == 1 else "s", + run_plural = "" if self.repeat == 1 else "s", + mean = _format_time(self.average, self._precision), + std = _format_time(self.stdev, self._precision)) + ) + + def _repr_pretty_(self, p , cycle): + unic = self.__str__() + p.text(u'') + + + +class TimeitTemplateFiller(ast.NodeTransformer): + """Fill in the AST template for timing execution. + + This is quite closely tied to the template definition, which is in + :meth:`ExecutionMagics.timeit`. + """ + def __init__(self, ast_setup, ast_stmt): + self.ast_setup = ast_setup + self.ast_stmt = ast_stmt + + def visit_FunctionDef(self, node): + "Fill in the setup statement" + self.generic_visit(node) + if node.name == "inner": + node.body[:1] = self.ast_setup.body + + return node + + def visit_For(self, node): + "Fill in the statement to be timed" + if getattr(getattr(node.body[0], 'value', None), 'id', None) == 'stmt': + node.body = self.ast_stmt.body + return node + + +class Timer(timeit.Timer): + """Timer class that explicitly uses self.inner + + which is an undocumented implementation detail of CPython, + not shared by PyPy. + """ + # Timer.timeit copied from CPython 3.4.2 + def timeit(self, number=timeit.default_number): + """Time 'number' executions of the main statement. + + To be precise, this executes the setup statement once, and + then returns the time it takes to execute the main statement + a number of times, as a float measured in seconds. The + argument is the number of times through the loop, defaulting + to one million. The main statement, the setup statement and + the timer function to be used are passed to the constructor. + """ + it = itertools.repeat(None, number) + gcold = gc.isenabled() + gc.disable() + try: + timing = self.inner(it, self.timer) + finally: + if gcold: + gc.enable() + return timing + + +@magics_class +class ExecutionMagics(Magics): + """Magics related to code execution, debugging, profiling, etc. + + """ + + def __init__(self, shell): + super(ExecutionMagics, self).__init__(shell) + if profile is None: + self.prun = self.profile_missing_notice + # Default execution function used to actually run user code. + self.default_runner = None + + def profile_missing_notice(self, *args, **kwargs): + error("""\ +The profile module could not be found. It has been removed from the standard +python packages because of its non-free license. To use profiling, install the +python-profiler package from non-free.""") + + @skip_doctest + @line_cell_magic + def prun(self, parameter_s='', cell=None): + + """Run a statement through the python code profiler. + + Usage, in line mode: + %prun [options] statement + + Usage, in cell mode: + %%prun [options] [statement] + code... + code... + + In cell mode, the additional code lines are appended to the (possibly + empty) statement in the first line. Cell mode allows you to easily + profile multiline blocks without having to put them in a separate + function. + + The given statement (which doesn't require quote marks) is run via the + python profiler in a manner similar to the profile.run() function. + Namespaces are internally managed to work correctly; profile.run + cannot be used in yap_ipython because it makes certain assumptions about + namespaces which do not hold under yap_ipython. + + Options: + + -l + you can place restrictions on what or how much of the + profile gets printed. The limit value can be: + + * A string: only information for function names containing this string + is printed. + + * An integer: only these many lines are printed. + + * A float (between 0 and 1): this fraction of the report is printed + (for example, use a limit of 0.4 to see the topmost 40% only). + + You can combine several limits with repeated use of the option. For + example, ``-l __init__ -l 5`` will print only the topmost 5 lines of + information about class constructors. + + -r + return the pstats.Stats object generated by the profiling. This + object has all the information about the profile in it, and you can + later use it for further analysis or in other functions. + + -s + sort profile by given key. You can provide more than one key + by using the option several times: '-s key1 -s key2 -s key3...'. The + default sorting key is 'time'. + + The following is copied verbatim from the profile documentation + referenced below: + + When more than one key is provided, additional keys are used as + secondary criteria when the there is equality in all keys selected + before them. + + Abbreviations can be used for any key names, as long as the + abbreviation is unambiguous. The following are the keys currently + defined: + + ============ ===================== + Valid Arg Meaning + ============ ===================== + "calls" call count + "cumulative" cumulative time + "file" file name + "module" file name + "pcalls" primitive call count + "line" line number + "name" function name + "nfl" name/file/line + "stdname" standard name + "time" internal time + ============ ===================== + + Note that all sorts on statistics are in descending order (placing + most time consuming items first), where as name, file, and line number + searches are in ascending order (i.e., alphabetical). The subtle + distinction between "nfl" and "stdname" is that the standard name is a + sort of the name as printed, which means that the embedded line + numbers get compared in an odd way. For example, lines 3, 20, and 40 + would (if the file names were the same) appear in the string order + "20" "3" and "40". In contrast, "nfl" does a numeric compare of the + line numbers. In fact, sort_stats("nfl") is the same as + sort_stats("name", "file", "line"). + + -T + save profile results as shown on screen to a text + file. The profile is still shown on screen. + + -D + save (via dump_stats) profile statistics to given + filename. This data is in a format understood by the pstats module, and + is generated by a call to the dump_stats() method of profile + objects. The profile is still shown on screen. + + -q + suppress output to the pager. Best used with -T and/or -D above. + + If you want to run complete programs under the profiler's control, use + ``%run -p [prof_opts] filename.py [args to program]`` where prof_opts + contains profiler specific options as described here. + + You can read the complete documentation for the profile module with:: + + In [1]: import profile; profile.help() + """ + opts, arg_str = self.parse_options(parameter_s, 'D:l:rs:T:q', + list_all=True, posix=False) + if cell is not None: + arg_str += '\n' + cell + arg_str = self.shell.input_splitter.transform_cell(arg_str) + return self._run_with_profiler(arg_str, opts, self.shell.user_ns) + + def _run_with_profiler(self, code, opts, namespace): + """ + Run `code` with profiler. Used by ``%prun`` and ``%run -p``. + + Parameters + ---------- + code : str + Code to be executed. + opts : Struct + Options parsed by `self.parse_options`. + namespace : dict + A dictionary for Python namespace (e.g., `self.shell.user_ns`). + + """ + + # Fill default values for unspecified options: + opts.merge(Struct(D=[''], l=[], s=['time'], T=[''])) + + prof = profile.Profile() + try: + prof = prof.runctx(code, namespace, namespace) + sys_exit = '' + except SystemExit: + sys_exit = """*** SystemExit exception caught in code being profiled.""" + + stats = pstats.Stats(prof).strip_dirs().sort_stats(*opts.s) + + lims = opts.l + if lims: + lims = [] # rebuild lims with ints/floats/strings + for lim in opts.l: + try: + lims.append(int(lim)) + except ValueError: + try: + lims.append(float(lim)) + except ValueError: + lims.append(lim) + + # Trap output. + stdout_trap = StringIO() + stats_stream = stats.stream + try: + stats.stream = stdout_trap + stats.print_stats(*lims) + finally: + stats.stream = stats_stream + + output = stdout_trap.getvalue() + output = output.rstrip() + + if 'q' not in opts: + page.page(output) + print(sys_exit, end=' ') + + dump_file = opts.D[0] + text_file = opts.T[0] + if dump_file: + prof.dump_stats(dump_file) + print('\n*** Profile stats marshalled to file',\ + repr(dump_file)+'.',sys_exit) + if text_file: + pfile = open(text_file,'w') + pfile.write(output) + pfile.close() + print('\n*** Profile printout saved to text file',\ + repr(text_file)+'.',sys_exit) + + if 'r' in opts: + return stats + else: + return None + + @line_magic + def pdb(self, parameter_s=''): + """Control the automatic calling of the pdb interactive debugger. + + Call as '%pdb on', '%pdb 1', '%pdb off' or '%pdb 0'. If called without + argument it works as a toggle. + + When an exception is triggered, yap_ipython can optionally call the + interactive pdb debugger after the traceback printout. %pdb toggles + this feature on and off. + + The initial state of this feature is set in your configuration + file (the option is ``InteractiveShell.pdb``). + + If you want to just activate the debugger AFTER an exception has fired, + without having to type '%pdb on' and rerunning your code, you can use + the %debug magic.""" + + par = parameter_s.strip().lower() + + if par: + try: + new_pdb = {'off':0,'0':0,'on':1,'1':1}[par] + except KeyError: + print ('Incorrect argument. Use on/1, off/0, ' + 'or nothing for a toggle.') + return + else: + # toggle + new_pdb = not self.shell.call_pdb + + # set on the shell + self.shell.call_pdb = new_pdb + print('Automatic pdb calling has been turned',on_off(new_pdb)) + + @skip_doctest + @magic_arguments.magic_arguments() + @magic_arguments.argument('--breakpoint', '-b', metavar='FILE:LINE', + help=""" + Set break point at LINE in FILE. + """ + ) + @magic_arguments.argument('statement', nargs='*', + help=""" + Code to run in debugger. + You can omit this in cell magic mode. + """ + ) + @line_cell_magic + def debug(self, line='', cell=None): + """Activate the interactive debugger. + + This magic command support two ways of activating debugger. + One is to activate debugger before executing code. This way, you + can set a break point, to step through the code from the point. + You can use this mode by giving statements to execute and optionally + a breakpoint. + + The other one is to activate debugger in post-mortem mode. You can + activate this mode simply running %debug without any argument. + If an exception has just occurred, this lets you inspect its stack + frames interactively. Note that this will always work only on the last + traceback that occurred, so you must call this quickly after an + exception that you wish to inspect has fired, because if another one + occurs, it clobbers the previous one. + + If you want yap_ipython to automatically do this on every exception, see + the %pdb magic for more details. + """ + args = magic_arguments.parse_argstring(self.debug, line) + + if not (args.breakpoint or args.statement or cell): + self._debug_post_mortem() + else: + code = "\n".join(args.statement) + if cell: + code += "\n" + cell + self._debug_exec(code, args.breakpoint) + + def _debug_post_mortem(self): + self.shell.debugger(force=True) + + def _debug_exec(self, code, breakpoint): + if breakpoint: + (filename, bp_line) = breakpoint.rsplit(':', 1) + bp_line = int(bp_line) + else: + (filename, bp_line) = (None, None) + self._run_with_debugger(code, self.shell.user_ns, filename, bp_line) + + @line_magic + def tb(self, s): + """Print the last traceback with the currently active exception mode. + + See %xmode for changing exception reporting modes.""" + self.shell.showtraceback() + + @skip_doctest + @line_magic + def run(self, parameter_s='', runner=None, + file_finder=get_py_filename): + """Run the named file inside yap_ipython as a program. + + Usage:: + + %run [-n -i -e -G] + [( -t [-N] | -d [-b] | -p [profile options] )] + ( -m mod | file ) [args] + + Parameters after the filename are passed as command-line arguments to + the program (put in sys.argv). Then, control returns to yap_ipython's + prompt. + + This is similar to running at a system prompt ``python file args``, + but with the advantage of giving you yap_ipython's tracebacks, and of + loading all variables into your interactive namespace for further use + (unless -p is used, see below). + + The file is executed in a namespace initially consisting only of + ``__name__=='__main__'`` and sys.argv constructed as indicated. It thus + sees its environment as if it were being run as a stand-alone program + (except for sharing global objects such as previously imported + modules). But after execution, the yap_ipython interactive namespace gets + updated with all variables defined in the program (except for __name__ + and sys.argv). This allows for very convenient loading of code for + interactive work, while giving each program a 'clean sheet' to run in. + + Arguments are expanded using shell-like glob match. Patterns + '*', '?', '[seq]' and '[!seq]' can be used. Additionally, + tilde '~' will be expanded into user's home directory. Unlike + real shells, quotation does not suppress expansions. Use + *two* back slashes (e.g. ``\\\\*``) to suppress expansions. + To completely disable these expansions, you can use -G flag. + + Options: + + -n + __name__ is NOT set to '__main__', but to the running file's name + without extension (as python does under import). This allows running + scripts and reloading the definitions in them without calling code + protected by an ``if __name__ == "__main__"`` clause. + + -i + run the file in yap_ipython's namespace instead of an empty one. This + is useful if you are experimenting with code written in a text editor + which depends on variables defined interactively. + + -e + ignore sys.exit() calls or SystemExit exceptions in the script + being run. This is particularly useful if yap_ipython is being used to + run unittests, which always exit with a sys.exit() call. In such + cases you are interested in the output of the test results, not in + seeing a traceback of the unittest module. + + -t + print timing information at the end of the run. yap_ipython will give + you an estimated CPU time consumption for your script, which under + Unix uses the resource module to avoid the wraparound problems of + time.clock(). Under Unix, an estimate of time spent on system tasks + is also given (for Windows platforms this is reported as 0.0). + + If -t is given, an additional ``-N`` option can be given, where + must be an integer indicating how many times you want the script to + run. The final timing report will include total and per run results. + + For example (testing the script uniq_stable.py):: + + In [1]: run -t uniq_stable + + yap_ipython CPU timings (estimated): + User : 0.19597 s. + System: 0.0 s. + + In [2]: run -t -N5 uniq_stable + + yap_ipython CPU timings (estimated): + Total runs performed: 5 + Times : Total Per run + User : 0.910862 s, 0.1821724 s. + System: 0.0 s, 0.0 s. + + -d + run your program under the control of pdb, the Python debugger. + This allows you to execute your program step by step, watch variables, + etc. Internally, what yap_ipython does is similar to calling:: + + pdb.run('execfile("YOURFILENAME")') + + with a breakpoint set on line 1 of your file. You can change the line + number for this automatic breakpoint to be by using the -bN option + (where N must be an integer). For example:: + + %run -d -b40 myscript + + will set the first breakpoint at line 40 in myscript.py. Note that + the first breakpoint must be set on a line which actually does + something (not a comment or docstring) for it to stop execution. + + Or you can specify a breakpoint in a different file:: + + %run -d -b myotherfile.py:20 myscript + + When the pdb debugger starts, you will see a (Pdb) prompt. You must + first enter 'c' (without quotes) to start execution up to the first + breakpoint. + + Entering 'help' gives information about the use of the debugger. You + can easily see pdb's full documentation with "import pdb;pdb.help()" + at a prompt. + + -p + run program under the control of the Python profiler module (which + prints a detailed report of execution times, function calls, etc). + + You can pass other options after -p which affect the behavior of the + profiler itself. See the docs for %prun for details. + + In this mode, the program's variables do NOT propagate back to the + yap_ipython interactive namespace (because they remain in the namespace + where the profiler executes them). + + Internally this triggers a call to %prun, see its documentation for + details on the options available specifically for profiling. + + There is one special usage for which the text above doesn't apply: + if the filename ends with .ipy[nb], the file is run as ipython script, + just as if the commands were written on yap_ipython prompt. + + -m + specify module name to load instead of script path. Similar to + the -m option for the python interpreter. Use this option last if you + want to combine with other %run options. Unlike the python interpreter + only source modules are allowed no .pyc or .pyo files. + For example:: + + %run -m example + + will run the example module. + + -G + disable shell-like glob expansion of arguments. + + """ + + # Logic to handle issue #3664 + # Add '--' after '-m ' to ignore additional args passed to a module. + if '-m' in parameter_s and '--' not in parameter_s: + argv = shlex.split(parameter_s, posix=(os.name == 'posix')) + for idx, arg in enumerate(argv): + if arg and arg.startswith('-') and arg != '-': + if arg == '-m': + argv.insert(idx + 2, '--') + break + else: + # Positional arg, break + break + parameter_s = ' '.join(shlex.quote(arg) for arg in argv) + + # get arguments and set sys.argv for program to be run. + opts, arg_lst = self.parse_options(parameter_s, + 'nidtN:b:pD:l:rs:T:em:G', + mode='list', list_all=1) + if "m" in opts: + modulename = opts["m"][0] + modpath = find_mod(modulename) + if modpath is None: + warn('%r is not a valid modulename on sys.path'%modulename) + return + arg_lst = [modpath] + arg_lst + try: + filename = file_finder(arg_lst[0]) + except IndexError: + warn('you must provide at least a filename.') + print('\n%run:\n', oinspect.getdoc(self.run)) + return + except IOError as e: + try: + msg = str(e) + except UnicodeError: + msg = e.message + error(msg) + return + + if filename.lower().endswith(('.ipy', '.ipynb')): + with preserve_keys(self.shell.user_ns, '__file__'): + self.shell.user_ns['__file__'] = filename + self.shell.safe_execfile_ipy(filename) + return + + # Control the response to exit() calls made by the script being run + exit_ignore = 'e' in opts + + # Make sure that the running script gets a proper sys.argv as if it + # were run from a system shell. + save_argv = sys.argv # save it for later restoring + + if 'G' in opts: + args = arg_lst[1:] + else: + # tilde and glob expansion + args = shellglob(map(os.path.expanduser, arg_lst[1:])) + + sys.argv = [filename] + args # put in the proper filename + + if 'n' in opts: + name = os.path.splitext(os.path.basename(filename))[0] + else: + name = '__main__' + + if 'i' in opts: + # Run in user's interactive namespace + prog_ns = self.shell.user_ns + __name__save = self.shell.user_ns['__name__'] + prog_ns['__name__'] = name + main_mod = self.shell.user_module + + # Since '%run foo' emulates 'python foo.py' at the cmd line, we must + # set the __file__ global in the script's namespace + # TK: Is this necessary in interactive mode? + prog_ns['__file__'] = filename + else: + # Run in a fresh, empty namespace + + # The shell MUST hold a reference to prog_ns so after %run + # exits, the python deletion mechanism doesn't zero it out + # (leaving dangling references). See interactiveshell for details + main_mod = self.shell.new_main_mod(filename, name) + prog_ns = main_mod.__dict__ + + # pickle fix. See interactiveshell for an explanation. But we need to + # make sure that, if we overwrite __main__, we replace it at the end + main_mod_name = prog_ns['__name__'] + + if main_mod_name == '__main__': + restore_main = sys.modules['__main__'] + else: + restore_main = False + + # This needs to be undone at the end to prevent holding references to + # every single object ever created. + sys.modules[main_mod_name] = main_mod + + if 'p' in opts or 'd' in opts: + if 'm' in opts: + code = 'run_module(modulename, prog_ns)' + code_ns = { + 'run_module': self.shell.safe_run_module, + 'prog_ns': prog_ns, + 'modulename': modulename, + } + else: + if 'd' in opts: + # allow exceptions to raise in debug mode + code = 'execfile(filename, prog_ns, raise_exceptions=True)' + else: + code = 'execfile(filename, prog_ns)' + code_ns = { + 'execfile': self.shell.safe_execfile, + 'prog_ns': prog_ns, + 'filename': get_py_filename(filename), + } + + try: + stats = None + if 'p' in opts: + stats = self._run_with_profiler(code, opts, code_ns) + else: + if 'd' in opts: + bp_file, bp_line = parse_breakpoint( + opts.get('b', ['1'])[0], filename) + self._run_with_debugger( + code, code_ns, filename, bp_line, bp_file) + else: + if 'm' in opts: + def run(): + self.shell.safe_run_module(modulename, prog_ns) + else: + if runner is None: + runner = self.default_runner + if runner is None: + runner = self.shell.safe_execfile + + def run(): + runner(filename, prog_ns, prog_ns, + exit_ignore=exit_ignore) + + if 't' in opts: + # timed execution + try: + nruns = int(opts['N'][0]) + if nruns < 1: + error('Number of runs must be >=1') + return + except (KeyError): + nruns = 1 + self._run_with_timing(run, nruns) + else: + # regular execution + run() + + if 'i' in opts: + self.shell.user_ns['__name__'] = __name__save + else: + # update yap_ipython interactive namespace + + # Some forms of read errors on the file may mean the + # __name__ key was never set; using pop we don't have to + # worry about a possible KeyError. + prog_ns.pop('__name__', None) + + with preserve_keys(self.shell.user_ns, '__file__'): + self.shell.user_ns.update(prog_ns) + finally: + # It's a bit of a mystery why, but __builtins__ can change from + # being a module to becoming a dict missing some key data after + # %run. As best I can see, this is NOT something yap_ipython is doing + # at all, and similar problems have been reported before: + # http://coding.derkeiler.com/Archive/Python/comp.lang.python/2004-10/0188.html + # Since this seems to be done by the interpreter itself, the best + # we can do is to at least restore __builtins__ for the user on + # exit. + self.shell.user_ns['__builtins__'] = builtin_mod + + # Ensure key global structures are restored + sys.argv = save_argv + if restore_main: + sys.modules['__main__'] = restore_main + else: + # Remove from sys.modules the reference to main_mod we'd + # added. Otherwise it will trap references to objects + # contained therein. + del sys.modules[main_mod_name] + + return stats + + def _run_with_debugger(self, code, code_ns, filename=None, + bp_line=None, bp_file=None): + """ + Run `code` in debugger with a break point. + + Parameters + ---------- + code : str + Code to execute. + code_ns : dict + A namespace in which `code` is executed. + filename : str + `code` is ran as if it is in `filename`. + bp_line : int, optional + Line number of the break point. + bp_file : str, optional + Path to the file in which break point is specified. + `filename` is used if not given. + + Raises + ------ + UsageError + If the break point given by `bp_line` is not valid. + + """ + deb = self.shell.InteractiveTB.pdb + if not deb: + self.shell.InteractiveTB.pdb = self.shell.InteractiveTB.debugger_cls() + deb = self.shell.InteractiveTB.pdb + + # deb.checkline() fails if deb.curframe exists but is None; it can + # handle it not existing. https://github.com/ipython/ipython/issues/10028 + if hasattr(deb, 'curframe'): + del deb.curframe + + # reset Breakpoint state, which is moronically kept + # in a class + bdb.Breakpoint.next = 1 + bdb.Breakpoint.bplist = {} + bdb.Breakpoint.bpbynumber = [None] + deb.clear_all_breaks() + if bp_line is not None: + # Set an initial breakpoint to stop execution + maxtries = 10 + bp_file = bp_file or filename + checkline = deb.checkline(bp_file, bp_line) + if not checkline: + for bp in range(bp_line + 1, bp_line + maxtries + 1): + if deb.checkline(bp_file, bp): + break + else: + msg = ("\nI failed to find a valid line to set " + "a breakpoint\n" + "after trying up to line: %s.\n" + "Please set a valid breakpoint manually " + "with the -b option." % bp) + raise UsageError(msg) + # if we find a good linenumber, set the breakpoint + deb.do_break('%s:%s' % (bp_file, bp_line)) + + if filename: + # Mimic Pdb._runscript(...) + deb._wait_for_mainpyfile = True + deb.mainpyfile = deb.canonic(filename) + + # Start file run + print("NOTE: Enter 'c' at the %s prompt to continue execution." % deb.prompt) + try: + if filename: + # save filename so it can be used by methods on the deb object + deb._exec_filename = filename + while True: + try: + deb.run(code, code_ns) + except Restart: + print("Restarting") + if filename: + deb._wait_for_mainpyfile = True + deb.mainpyfile = deb.canonic(filename) + continue + else: + break + + + except: + etype, value, tb = sys.exc_info() + # Skip three frames in the traceback: the %run one, + # one inside bdb.py, and the command-line typed by the + # user (run by exec in pdb itself). + self.shell.InteractiveTB(etype, value, tb, tb_offset=3) + + @staticmethod + def _run_with_timing(run, nruns): + """ + Run function `run` and print timing information. + + Parameters + ---------- + run : callable + Any callable object which takes no argument. + nruns : int + Number of times to execute `run`. + + """ + twall0 = time.time() + if nruns == 1: + t0 = clock2() + run() + t1 = clock2() + t_usr = t1[0] - t0[0] + t_sys = t1[1] - t0[1] + print("\nIPython CPU timings (estimated):") + print(" User : %10.2f s." % t_usr) + print(" System : %10.2f s." % t_sys) + else: + runs = range(nruns) + t0 = clock2() + for nr in runs: + run() + t1 = clock2() + t_usr = t1[0] - t0[0] + t_sys = t1[1] - t0[1] + print("\nIPython CPU timings (estimated):") + print("Total runs performed:", nruns) + print(" Times : %10s %10s" % ('Total', 'Per run')) + print(" User : %10.2f s, %10.2f s." % (t_usr, t_usr / nruns)) + print(" System : %10.2f s, %10.2f s." % (t_sys, t_sys / nruns)) + twall1 = time.time() + print("Wall time: %10.2f s." % (twall1 - twall0)) + + @skip_doctest + @line_cell_magic + @needs_local_scope + def timeit(self, line='', cell=None, local_ns=None): + """Time execution of a Python statement or expression + + Usage, in line mode: + %timeit [-n -r [-t|-c] -q -p

-o] statement + or in cell mode: + %%timeit [-n -r [-t|-c] -q -p

-o] setup_code + code + code... + + Time execution of a Python statement or expression using the timeit + module. This function can be used both as a line and cell magic: + + - In line mode you can time a single-line statement (though multiple + ones can be chained with using semicolons). + + - In cell mode, the statement in the first line is used as setup code + (executed but not timed) and the body of the cell is timed. The cell + body has access to any variables created in the setup code. + + Options: + -n: execute the given statement times in a loop. If this value + is not given, a fitting value is chosen. + + -r: repeat the loop iteration times and take the best result. + Default: 3 + + -t: use time.time to measure the time, which is the default on Unix. + This function measures wall time. + + -c: use time.clock to measure the time, which is the default on + Windows and measures wall time. On Unix, resource.getrusage is used + instead and returns the CPU user time. + + -p

: use a precision of

digits to display the timing result. + Default: 3 + + -q: Quiet, do not print result. + + -o: return a TimeitResult that can be stored in a variable to inspect + the result in more details. + + + Examples + -------- + :: + + In [1]: %timeit pass + 8.26 ns ± 0.12 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each) + + In [2]: u = None + + In [3]: %timeit u is None + 29.9 ns ± 0.643 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) + + In [4]: %timeit -r 4 u == None + + In [5]: import time + + In [6]: %timeit -n1 time.sleep(2) + + + The times reported by %timeit will be slightly higher than those + reported by the timeit.py script when variables are accessed. This is + due to the fact that %timeit executes the statement in the namespace + of the shell, compared with timeit.py, which uses a single setup + statement to import function or create variables. Generally, the bias + does not matter as long as results from timeit.py are not mixed with + those from %timeit.""" + + opts, stmt = self.parse_options(line,'n:r:tcp:qo', + posix=False, strict=False) + if stmt == "" and cell is None: + return + + timefunc = timeit.default_timer + number = int(getattr(opts, "n", 0)) + default_repeat = 7 if timeit.default_repeat < 7 else timeit.default_repeat + repeat = int(getattr(opts, "r", default_repeat)) + precision = int(getattr(opts, "p", 3)) + quiet = 'q' in opts + return_result = 'o' in opts + if hasattr(opts, "t"): + timefunc = time.time + if hasattr(opts, "c"): + timefunc = clock + + timer = Timer(timer=timefunc) + # this code has tight coupling to the inner workings of timeit.Timer, + # but is there a better way to achieve that the code stmt has access + # to the shell namespace? + transform = self.shell.input_splitter.transform_cell + + if cell is None: + # called as line magic + ast_setup = self.shell.compile.ast_parse("pass") + ast_stmt = self.shell.compile.ast_parse(transform(stmt)) + else: + ast_setup = self.shell.compile.ast_parse(transform(stmt)) + ast_stmt = self.shell.compile.ast_parse(transform(cell)) + + ast_setup = self.shell.transform_ast(ast_setup) + ast_stmt = self.shell.transform_ast(ast_stmt) + + # Check that these compile to valid Python code *outside* the timer func + # Invalid code may become valid when put inside the function & loop, + # which messes up error messages. + # https://github.com/ipython/ipython/issues/10636 + self.shell.compile(ast_setup, "", "exec") + self.shell.compile(ast_stmt, "", "exec") + + # This codestring is taken from timeit.template - we fill it in as an + # AST, so that we can apply our AST transformations to the user code + # without affecting the timing code. + timeit_ast_template = ast.parse('def inner(_it, _timer):\n' + ' setup\n' + ' _t0 = _timer()\n' + ' for _i in _it:\n' + ' stmt\n' + ' _t1 = _timer()\n' + ' return _t1 - _t0\n') + + timeit_ast = TimeitTemplateFiller(ast_setup, ast_stmt).visit(timeit_ast_template) + timeit_ast = ast.fix_missing_locations(timeit_ast) + + # Track compilation time so it can be reported if too long + # Minimum time above which compilation time will be reported + tc_min = 0.1 + + t0 = clock() + code = self.shell.compile(timeit_ast, "", "exec") + tc = clock()-t0 + + ns = {} + glob = self.shell.user_ns + # handles global vars with same name as local vars. We store them in conflict_globs. + if local_ns is not None: + conflict_globs = {} + for var_name, var_val in glob.items(): + if var_name in local_ns: + conflict_globs[var_name] = var_val + glob.update(local_ns) + + exec(code, glob, ns) + timer.inner = ns["inner"] + + # This is used to check if there is a huge difference between the + # best and worst timings. + # Issue: https://github.com/ipython/ipython/issues/6471 + if number == 0: + # determine number so that 0.2 <= total time < 2.0 + for index in range(0, 10): + number = 10 ** index + time_number = timer.timeit(number) + if time_number >= 0.2: + break + + all_runs = timer.repeat(repeat, number) + best = min(all_runs) / number + worst = max(all_runs) / number + timeit_result = TimeitResult(number, repeat, best, worst, all_runs, tc, precision) + + # Restore global vars from conflict_globs + if local_ns is not None: + if len(conflict_globs) > 0: + glob.update(conflict_globs) + + if not quiet : + # Check best timing is greater than zero to avoid a + # ZeroDivisionError. + # In cases where the slowest timing is lesser than a micosecond + # we assume that it does not really matter if the fastest + # timing is 4 times faster than the slowest timing or not. + if worst > 4 * best and best > 0 and worst > 1e-6: + print("The slowest run took %0.2f times longer than the " + "fastest. This could mean that an intermediate result " + "is being cached." % (worst / best)) + + print( timeit_result ) + + if tc > tc_min: + print("Compiler time: %.2f s" % tc) + if return_result: + return timeit_result + + @skip_doctest + @needs_local_scope + @line_cell_magic + def time(self,line='', cell=None, local_ns=None): + """Time execution of a Python statement or expression. + + The CPU and wall clock times are printed, and the value of the + expression (if any) is returned. Note that under Win32, system time + is always reported as 0, since it can not be measured. + + This function can be used both as a line and cell magic: + + - In line mode you can time a single-line statement (though multiple + ones can be chained with using semicolons). + + - In cell mode, you can time the cell body (a directly + following statement raises an error). + + This function provides very basic timing functionality. Use the timeit + magic for more control over the measurement. + + Examples + -------- + :: + + In [1]: %time 2**128 + CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s + Wall time: 0.00 + Out[1]: 340282366920938463463374607431768211456L + + In [2]: n = 1000000 + + In [3]: %time sum(range(n)) + CPU times: user 1.20 s, sys: 0.05 s, total: 1.25 s + Wall time: 1.37 + Out[3]: 499999500000L + + In [4]: %time print 'hello world' + hello world + CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s + Wall time: 0.00 + + Note that the time needed by Python to compile the given expression + will be reported if it is more than 0.1s. In this example, the + actual exponentiation is done by Python at compilation time, so while + the expression can take a noticeable amount of time to compute, that + time is purely due to the compilation: + + In [5]: %time 3**9999; + CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s + Wall time: 0.00 s + + In [6]: %time 3**999999; + CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s + Wall time: 0.00 s + Compiler : 0.78 s + """ + + # fail immediately if the given expression can't be compiled + + if line and cell: + raise UsageError("Can't use statement directly after '%%time'!") + + if cell: + expr = self.shell.input_transformer_manager.transform_cell(cell) + else: + expr = self.shell.input_transformer_manager.transform_cell(line) + + # Minimum time above which parse time will be reported + tp_min = 0.1 + + t0 = clock() + expr_ast = self.shell.compile.ast_parse(expr) + tp = clock()-t0 + + # Apply AST transformations + expr_ast = self.shell.transform_ast(expr_ast) + + # Minimum time above which compilation time will be reported + tc_min = 0.1 + + if len(expr_ast.body)==1 and isinstance(expr_ast.body[0], ast.Expr): + mode = 'eval' + source = '' + expr_ast = ast.Expression(expr_ast.body[0].value) + else: + mode = 'exec' + source = '' + t0 = clock() + code = self.shell.compile(expr_ast, source, mode) + tc = clock()-t0 + + # skew measurement as little as possible + glob = self.shell.user_ns + wtime = time.time + # time execution + wall_st = wtime() + if mode=='eval': + st = clock2() + try: + out = eval(code, glob, local_ns) + except: + self.shell.showtraceback() + return + end = clock2() + else: + st = clock2() + try: + exec(code, glob, local_ns) + except: + self.shell.showtraceback() + return + end = clock2() + out = None + wall_end = wtime() + # Compute actual times and report + wall_time = wall_end-wall_st + cpu_user = end[0]-st[0] + cpu_sys = end[1]-st[1] + cpu_tot = cpu_user+cpu_sys + # On windows cpu_sys is always zero, so no new information to the next print + if sys.platform != 'win32': + print("CPU times: user %s, sys: %s, total: %s" % \ + (_format_time(cpu_user),_format_time(cpu_sys),_format_time(cpu_tot))) + print("Wall time: %s" % _format_time(wall_time)) + if tc > tc_min: + print("Compiler : %s" % _format_time(tc)) + if tp > tp_min: + print("Parser : %s" % _format_time(tp)) + return out + + @skip_doctest + @line_magic + def macro(self, parameter_s=''): + """Define a macro for future re-execution. It accepts ranges of history, + filenames or string objects. + + Usage:\\ + %macro [options] name n1-n2 n3-n4 ... n5 .. n6 ... + + 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 at the + command line is used instead. + + -q: quiet macro definition. By default, a tag line is printed + to indicate the macro has been created, and then the contents of + the macro are printed. If this option is given, then no printout + is produced once the macro is created. + + This will define a global variable called `name` which is a string + made of joining the slices and lines you specify (n1,n2,... numbers + above) from your input history into a single string. This variable + acts like an automatic function which re-executes those lines as if + you had typed them. You just type 'name' at the prompt and the code + executes. + + The syntax for indicating input ranges is described in %history. + + Note: as a 'hidden' feature, you can also use traditional python slice + notation, where N:M means numbers N through M-1. + + For example, if your history contains (print using %hist -n ):: + + 44: x=1 + 45: y=3 + 46: z=x+y + 47: print x + 48: a=5 + 49: print 'x',x,'y',y + + you can create a macro with lines 44 through 47 (included) and line 49 + called my_macro with:: + + In [55]: %macro my_macro 44-47 49 + + Now, typing `my_macro` (without quotes) will re-execute all this code + in one pass. + + You don't need to give the line-numbers in order, and any given line + number can appear multiple times. You can assemble macros with any + lines from your input history in any order. + + The macro is a simple object which holds its value in an attribute, + but yap_ipython's display system checks for macros and executes them as + code instead of printing them when you type their name. + + You can view a macro's contents by explicitly printing it with:: + + print macro_name + + """ + opts,args = self.parse_options(parameter_s,'rq',mode='list') + if not args: # List existing macros + return sorted(k for k,v in self.shell.user_ns.items() if isinstance(v, Macro)) + if len(args) == 1: + raise UsageError( + "%macro insufficient args; usage '%macro name n1-n2 n3-4...") + name, codefrom = args[0], " ".join(args[1:]) + + #print 'rng',ranges # dbg + try: + lines = self.shell.find_user_code(codefrom, 'r' in opts) + except (ValueError, TypeError) as e: + print(e.args[0]) + return + macro = Macro(lines) + self.shell.define_macro(name, macro) + if not ( 'q' in opts) : + print('Macro `%s` created. To execute, type its name (without quotes).' % name) + print('=== Macro contents: ===') + print(macro, end=' ') + + @magic_arguments.magic_arguments() + @magic_arguments.argument('output', type=str, default='', nargs='?', + help="""The name of the variable in which to store output. + This is a utils.io.CapturedIO object with stdout/err attributes + for the text of the captured output. + + CapturedOutput also has a show() method for displaying the output, + and __call__ as well, so you can use that to quickly display the + output. + + If unspecified, captured output is discarded. + """ + ) + @magic_arguments.argument('--no-stderr', action="store_true", + help="""Don't capture stderr.""" + ) + @magic_arguments.argument('--no-stdout', action="store_true", + help="""Don't capture stdout.""" + ) + @magic_arguments.argument('--no-display', action="store_true", + help="""Don't capture yap_ipython's rich display.""" + ) + @cell_magic + def capture(self, line, cell): + """run the cell, capturing stdout, stderr, and yap_ipython's rich display() calls.""" + args = magic_arguments.parse_argstring(self.capture, line) + out = not args.no_stdout + err = not args.no_stderr + disp = not args.no_display + with capture_output(out, err, disp) as io: + self.shell.run_cell(cell) + if args.output: + self.shell.user_ns[args.output] = io + +def parse_breakpoint(text, current_file): + '''Returns (file, line) for file:line and (current_file, line) for line''' + colon = text.find(':') + if colon == -1: + return current_file, int(text) + else: + return text[:colon], int(text[colon+1:]) + +def _format_time(timespan, precision=3): + """Formats the timespan in a human readable form""" + + if timespan >= 60.0: + # we have more than a minute, format that in a human readable form + # Idea from http://snipplr.com/view/5713/ + parts = [("d", 60*60*24),("h", 60*60),("min", 60), ("s", 1)] + time = [] + leftover = timespan + for suffix, length in parts: + value = int(leftover / length) + if value > 0: + leftover = leftover % length + time.append(u'%s%s' % (str(value), suffix)) + if leftover < 1: + break + return " ".join(time) + + + # Unfortunately the unicode 'micro' symbol can cause problems in + # certain terminals. + # See bug: https://bugs.launchpad.net/ipython/+bug/348466 + # Try to prevent crashes by being more secure than it needs to + # E.g. eclipse is able to print a µ, but has no sys.stdout.encoding set. + units = [u"s", u"ms",u'us',"ns"] # the save value + if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding: + try: + u'\xb5'.encode(sys.stdout.encoding) + units = [u"s", u"ms",u'\xb5s',"ns"] + except: + pass + scaling = [1, 1e3, 1e6, 1e9] + + if timespan > 0.0: + order = min(-int(math.floor(math.log10(timespan)) // 3), 3) + else: + order = 3 + return u"%.*g %s" % (precision, timespan * scaling[order], units[order]) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/extension.py b/packages/python/yap_kernel/yap_ipython/core/magics/extension.py new file mode 100644 index 000000000..8cb33a4c1 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/extension.py @@ -0,0 +1,63 @@ +"""Implementation of magic functions for the extension machinery. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 +#----------------------------------------------------------------------------- + + +# Our own packages +from yap_ipython.core.error import UsageError +from yap_ipython.core.magic import Magics, magics_class, line_magic + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +@magics_class +class ExtensionMagics(Magics): + """Magics to manage the yap_ipython extensions system.""" + + @line_magic + def load_ext(self, module_str): + """Load an yap_ipython extension by its module name.""" + if not module_str: + raise UsageError('Missing module name.') + res = self.shell.extension_manager.load_extension(module_str) + + if res == 'already loaded': + print("The %s extension is already loaded. To reload it, use:" % module_str) + print(" %reload_ext", module_str) + elif res == 'no load function': + print("The %s module is not an yap_ipython extension." % module_str) + + @line_magic + def unload_ext(self, module_str): + """Unload an yap_ipython extension by its module name. + + Not all extensions can be unloaded, only those which define an + ``unload_ipython_extension`` function. + """ + if not module_str: + raise UsageError('Missing module name.') + + res = self.shell.extension_manager.unload_extension(module_str) + + if res == 'no unload function': + print("The %s extension doesn't define how to unload it." % module_str) + elif res == "not loaded": + print("The %s extension is not loaded." % module_str) + + @line_magic + def reload_ext(self, module_str): + """Reload an yap_ipython extension by its module name.""" + if not module_str: + raise UsageError('Missing module name.') + self.shell.extension_manager.reload_extension(module_str) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/history.py b/packages/python/yap_kernel/yap_ipython/core/magics/history.py new file mode 100644 index 000000000..1896eeea0 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/history.py @@ -0,0 +1,318 @@ +"""Implementation of magic functions related to History. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012, yap_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 os +import sys +from io import open as io_open + +# Our own packages +from yap_ipython.core.error import StdinNotImplementedError +from yap_ipython.core.magic import Magics, magics_class, line_magic +from yap_ipython.core.magic_arguments import (argument, magic_arguments, + parse_argstring) +from yap_ipython.testing.skipdoctest import skip_doctest +from yap_ipython.utils import io + +#----------------------------------------------------------------------------- +# Magics class implementation +#----------------------------------------------------------------------------- + + +_unspecified = object() + + +@magics_class +class HistoryMagics(Magics): + + @magic_arguments() + @argument( + '-n', dest='print_nums', action='store_true', default=False, + help=""" + print line numbers for each input. + This feature is only available if numbered prompts are in use. + """) + @argument( + '-o', dest='get_output', action='store_true', default=False, + help="also print outputs for each input.") + @argument( + '-p', dest='pyprompts', action='store_true', default=False, + help=""" + print classic '>>>' python prompts before each input. + This is useful for making documentation, and in conjunction + with -o, for producing doctest-ready output. + """) + @argument( + '-t', dest='raw', action='store_false', default=True, + help=""" + print the 'translated' history, as yap_ipython understands it. + yap_ipython filters your input and converts it all into valid Python + source before executing it (things like magics or aliases are turned + into function calls, for example). With this option, you'll see the + native history instead of the user-entered version: '%%cd /' will be + seen as 'get_ipython().run_line_magic("cd", "/")' instead of '%%cd /'. + """) + @argument( + '-f', dest='filename', + help=""" + FILENAME: instead of printing the output to the screen, redirect + it to the given file. The file is always overwritten, though *when + it can*, yap_ipython asks for confirmation first. In particular, running + the command 'history -f FILENAME' from the yap_ipython Notebook + interface will replace FILENAME even if it already exists *without* + confirmation. + """) + @argument( + '-g', dest='pattern', nargs='*', default=None, + help=""" + treat the arg as a glob pattern to search for in (full) history. + This includes the saved history (almost all commands ever written). + The pattern may contain '?' to match one unknown character and '*' + to match any number of unknown characters. Use '%%hist -g' to show + full saved history (may be very long). + """) + @argument( + '-l', dest='limit', type=int, nargs='?', default=_unspecified, + help=""" + get the last n lines from all sessions. Specify n as a single + arg, or the default is the last 10 lines. + """) + @argument( + '-u', dest='unique', action='store_true', + help=""" + when searching history using `-g`, show only unique history. + """) + @argument('range', nargs='*') + @skip_doctest + @line_magic + def history(self, parameter_s = ''): + """Print input history (_i variables), with most recent last. + + By default, input history is printed without line numbers so it can be + directly pasted into an editor. Use -n to show them. + + By default, all input history from the current session is displayed. + Ranges of history can be indicated using the syntax: + + ``4`` + Line 4, current session + ``4-6`` + Lines 4-6, current session + ``243/1-5`` + Lines 1-5, session 243 + ``~2/7`` + Line 7, session 2 before current + ``~8/1-~6/5`` + From the first line of 8 sessions ago, to the fifth line of 6 + sessions ago. + + Multiple ranges can be entered, separated by spaces + + The same syntax is used by %macro, %save, %edit, %rerun + + Examples + -------- + :: + + In [6]: %history -n 4-6 + 4:a = 12 + 5:print a**2 + 6:%history -n 4-6 + + """ + + args = parse_argstring(self.history, parameter_s) + + # For brevity + history_manager = self.shell.history_manager + + def _format_lineno(session, line): + """Helper function to format line numbers properly.""" + if session in (0, history_manager.session_number): + return str(line) + return "%s/%s" % (session, line) + + # Check if output to specific file was requested. + outfname = args.filename + if not outfname: + outfile = sys.stdout # default + # We don't want to close stdout at the end! + close_at_end = False + else: + if os.path.exists(outfname): + try: + ans = io.ask_yes_no("File %r exists. Overwrite?" % outfname) + except StdinNotImplementedError: + ans = True + if not ans: + print('Aborting.') + return + print("Overwriting file.") + outfile = io_open(outfname, 'w', encoding='utf-8') + close_at_end = True + + print_nums = args.print_nums + get_output = args.get_output + pyprompts = args.pyprompts + raw = args.raw + + pattern = None + limit = None if args.limit is _unspecified else args.limit + + if args.pattern is not None: + if args.pattern: + pattern = "*" + " ".join(args.pattern) + "*" + else: + pattern = "*" + hist = history_manager.search(pattern, raw=raw, output=get_output, + n=limit, unique=args.unique) + print_nums = True + elif args.limit is not _unspecified: + n = 10 if limit is None else limit + hist = history_manager.get_tail(n, raw=raw, output=get_output) + else: + if args.range: # Get history by ranges + hist = history_manager.get_range_by_str(" ".join(args.range), + raw, get_output) + else: # Just get history for the current session + hist = history_manager.get_range(raw=raw, output=get_output) + + # We could be displaying the entire history, so let's not try to pull + # it into a list in memory. Anything that needs more space will just + # misalign. + width = 4 + + for session, lineno, inline in hist: + # Print user history with tabs expanded to 4 spaces. The GUI + # clients use hard tabs for easier usability in auto-indented code, + # but we want to produce PEP-8 compliant history for safe pasting + # into an editor. + if get_output: + inline, output = inline + inline = inline.expandtabs(4).rstrip() + + multiline = "\n" in inline + line_sep = '\n' if multiline else ' ' + if print_nums: + print(u'%s:%s' % (_format_lineno(session, lineno).rjust(width), + line_sep), file=outfile, end=u'') + if pyprompts: + print(u">>> ", end=u"", file=outfile) + if multiline: + inline = "\n... ".join(inline.splitlines()) + "\n..." + print(inline, file=outfile) + if get_output and output: + print(output, file=outfile) + + if close_at_end: + outfile.close() + + @line_magic + def recall(self, arg): + r"""Repeat a command, or get command to input line for editing. + + %recall and %rep are equivalent. + + - %recall (no arguments): + + Place a string version of last computation result (stored in the + special '_' variable) to the next input prompt. Allows you to create + elaborate command lines without using copy-paste:: + + In[1]: l = ["hei", "vaan"] + In[2]: "".join(l) + Out[2]: heivaan + In[3]: %recall + In[4]: heivaan_ <== cursor blinking + + %recall 45 + + Place history line 45 on the next input prompt. Use %hist to find + out the number. + + %recall 1-4 + + Combine the specified lines into one cell, and place it on the next + input prompt. See %history for the slice syntax. + + %recall foo+bar + + If foo+bar can be evaluated in the user namespace, the result is + placed at the next input prompt. Otherwise, the history is searched + for lines which contain that substring, and the most recent one is + placed at the next input prompt. + """ + if not arg: # Last output + self.shell.set_next_input(str(self.shell.user_ns["_"])) + return + # Get history range + histlines = self.shell.history_manager.get_range_by_str(arg) + cmd = "\n".join(x[2] for x in histlines) + if cmd: + self.shell.set_next_input(cmd.rstrip()) + return + + try: # Variable in user namespace + cmd = str(eval(arg, self.shell.user_ns)) + except Exception: # Search for term in history + histlines = self.shell.history_manager.search("*"+arg+"*") + for h in reversed([x[2] for x in histlines]): + if 'recall' in h or 'rep' in h: + continue + self.shell.set_next_input(h.rstrip()) + return + else: + self.shell.set_next_input(cmd.rstrip()) + print("Couldn't evaluate or find in history:", arg) + + @line_magic + def rerun(self, parameter_s=''): + """Re-run previous input + + By default, you can specify ranges of input history to be repeated + (as with %history). With no arguments, it will repeat the last line. + + Options: + + -l : Repeat the last n lines of input, not including the + current command. + + -g foo : Repeat the most recent line which contains foo + """ + opts, args = self.parse_options(parameter_s, 'l:g:', mode='string') + if "l" in opts: # Last n lines + n = int(opts['l']) + hist = self.shell.history_manager.get_tail(n) + elif "g" in opts: # Search + p = "*"+opts['g']+"*" + hist = list(self.shell.history_manager.search(p)) + for l in reversed(hist): + if "rerun" not in l[2]: + hist = [l] # The last match which isn't a %rerun + break + else: + hist = [] # No matches except %rerun + elif args: # Specify history ranges + hist = self.shell.history_manager.get_range_by_str(args) + else: # Last line + hist = self.shell.history_manager.get_tail(1) + hist = [x[2] for x in hist] + if not hist: + print("No lines in history match specification") + return + histlines = "\n".join(hist) + print("=== Executing: ===") + print(histlines) + print("=== Output: ===") + self.shell.run_cell("\n".join(hist), store_history=False) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/logging.py b/packages/python/yap_kernel/yap_ipython/core/magics/logging.py new file mode 100644 index 000000000..d7fd52e0c --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/logging.py @@ -0,0 +1,195 @@ +"""Implementation of magic functions for yap_ipython's own logging. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 os +import sys + +# Our own packages +from yap_ipython.core.magic import Magics, magics_class, line_magic +from warnings import warn +from traitlets import Bool + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +@magics_class +class LoggingMagics(Magics): + """Magics related to all logging machinery.""" + + quiet = Bool(False, help= + """ + Suppress output of log state when logging is enabled + """ + ).tag(config=True) + + @line_magic + def logstart(self, parameter_s=''): + """Start logging anywhere in a session. + + %logstart [-o|-r|-t|-q] [log_name [log_mode]] + + If no name is given, it defaults to a file named 'ipython_log.py' in your + current directory, in 'rotate' mode (see below). + + '%logstart name' saves to file 'name' in 'backup' mode. It saves your + history up to that point and then continues logging. + + %logstart takes a second optional parameter: logging mode. This can be one + of (note that the modes are given unquoted): + + append + Keep logging at the end of any existing file. + + backup + Rename any existing file to name~ and start name. + + global + Append to a single logfile in your home directory. + + over + Overwrite any existing log. + + rotate + Create rotating logs: name.1~, name.2~, etc. + + Options: + + -o + log also yap_ipython's output. In this mode, all commands which + generate an Out[NN] prompt are recorded to the logfile, right after + their corresponding input line. The output lines are always + prepended with a '#[Out]# ' marker, so that the log remains valid + Python code. + + Since this marker is always the same, filtering only the output from + a log is very easy, using for example a simple awk call:: + + awk -F'#\\[Out\\]# ' '{if($2) {print $2}}' ipython_log.py + + -r + log 'raw' input. Normally, yap_ipython's logs contain the processed + input, so that user lines are logged in their final form, converted + into valid Python. For example, %Exit is logged as + _ip.magic("Exit"). If the -r flag is given, all input is logged + exactly as typed, with no transformations applied. + + -t + put timestamps before each input line logged (these are put in + comments). + + -q + suppress output of logstate message when logging is invoked + """ + + opts,par = self.parse_options(parameter_s,'ortq') + log_output = 'o' in opts + log_raw_input = 'r' in opts + timestamp = 't' in opts + quiet = 'q' in opts + + logger = self.shell.logger + + # if no args are given, the defaults set in the logger constructor by + # ipython remain valid + if par: + try: + logfname,logmode = par.split() + except: + logfname = par + logmode = 'backup' + else: + logfname = logger.logfname + logmode = logger.logmode + # put logfname into rc struct as if it had been called on the command + # line, so it ends up saved in the log header Save it in case we need + # to restore it... + old_logfile = self.shell.logfile + if logfname: + logfname = os.path.expanduser(logfname) + self.shell.logfile = logfname + + loghead = u'# yap_ipython log file\n\n' + try: + logger.logstart(logfname, loghead, logmode, log_output, timestamp, + log_raw_input) + except: + self.shell.logfile = old_logfile + warn("Couldn't start log: %s" % sys.exc_info()[1]) + else: + # log input history up to this point, optionally interleaving + # output if requested + + if timestamp: + # disable timestamping for the previous history, since we've + # lost those already (no time machine here). + logger.timestamp = False + + if log_raw_input: + input_hist = self.shell.history_manager.input_hist_raw + else: + input_hist = self.shell.history_manager.input_hist_parsed + + if log_output: + log_write = logger.log_write + output_hist = self.shell.history_manager.output_hist + for n in range(1,len(input_hist)-1): + log_write(input_hist[n].rstrip() + u'\n') + if n in output_hist: + log_write(repr(output_hist[n]),'output') + else: + logger.log_write(u'\n'.join(input_hist[1:])) + logger.log_write(u'\n') + if timestamp: + # re-enable timestamping + logger.timestamp = True + + if not (self.quiet or quiet): + print ('Activating auto-logging. ' + 'Current session state plus future input saved.') + logger.logstate() + + @line_magic + def logstop(self, parameter_s=''): + """Fully stop logging and close log file. + + In order to start logging again, a new %logstart call needs to be made, + possibly (though not necessarily) with a new filename, mode and other + options.""" + self.shell.logger.logstop() + + @line_magic + def logoff(self, parameter_s=''): + """Temporarily stop logging. + + You must have previously started logging.""" + self.shell.logger.switch_log(0) + + @line_magic + def logon(self, parameter_s=''): + """Restart logging. + + This function is for restarting logging which you've temporarily + stopped with %logoff. For starting logging for the first time, you + must use the %logstart function, which allows you to specify an + optional log filename.""" + + self.shell.logger.switch_log(1) + + @line_magic + def logstate(self, parameter_s=''): + """Print the status of the logging system.""" + + self.shell.logger.logstate() diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/namespace.py b/packages/python/yap_kernel/yap_ipython/core/magics/namespace.py new file mode 100644 index 000000000..9a0ffbe6d --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/namespace.py @@ -0,0 +1,702 @@ +"""Implementation of namespace-related magic functions. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 gc +import re +import sys + +# Our own packages +from yap_ipython.core import page +from yap_ipython.core.error import StdinNotImplementedError, UsageError +from yap_ipython.core.magic import Magics, magics_class, line_magic +from yap_ipython.testing.skipdoctest import skip_doctest +from yap_ipython.utils.encoding import DEFAULT_ENCODING +from yap_ipython.utils.openpy import read_py_file +from yap_ipython.utils.path import get_py_filename + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +@magics_class +class NamespaceMagics(Magics): + """Magics to manage various aspects of the user's namespace. + + These include listing variables, introspecting into them, etc. + """ + + @line_magic + def pinfo(self, parameter_s='', namespaces=None): + """Provide detailed information about an object. + + '%pinfo object' is just a synonym for object? or ?object.""" + + #print 'pinfo par: <%s>' % parameter_s # dbg + # detail_level: 0 -> obj? , 1 -> obj?? + detail_level = 0 + # We need to detect if we got called as 'pinfo pinfo foo', which can + # happen if the user types 'pinfo foo?' at the cmd line. + pinfo,qmark1,oname,qmark2 = \ + re.match('(pinfo )?(\?*)(.*?)(\??$)',parameter_s).groups() + if pinfo or qmark1 or qmark2: + detail_level = 1 + if "*" in oname: + self.psearch(oname) + else: + self.shell._inspect('pinfo', oname, detail_level=detail_level, + namespaces=namespaces) + + @line_magic + def pinfo2(self, parameter_s='', namespaces=None): + """Provide extra detailed information about an object. + + '%pinfo2 object' is just a synonym for object?? or ??object.""" + self.shell._inspect('pinfo', parameter_s, detail_level=1, + namespaces=namespaces) + + @skip_doctest + @line_magic + def pdef(self, parameter_s='', namespaces=None): + """Print the call signature for any callable object. + + If the object is a class, print the constructor information. + + Examples + -------- + :: + + In [3]: %pdef urllib.urlopen + urllib.urlopen(url, data=None, proxies=None) + """ + self.shell._inspect('pdef',parameter_s, namespaces) + + @line_magic + def pdoc(self, parameter_s='', namespaces=None): + """Print the docstring for an object. + + If the given object is a class, it will print both the class and the + constructor docstrings.""" + self.shell._inspect('pdoc',parameter_s, namespaces) + + @line_magic + def psource(self, parameter_s='', namespaces=None): + """Print (or run through pager) the source code for an object.""" + if not parameter_s: + raise UsageError('Missing object name.') + self.shell._inspect('psource',parameter_s, namespaces) + + @line_magic + def pfile(self, parameter_s='', namespaces=None): + """Print (or run through pager) the file where an object is defined. + + The file opens at the line where the object definition begins. yap_ipython + will honor the environment variable PAGER if set, and otherwise will + do its best to print the file in a convenient form. + + If the given argument is not an object currently defined, yap_ipython will + try to interpret it as a filename (automatically adding a .py extension + if needed). You can thus use %pfile as a syntax highlighting code + viewer.""" + + # first interpret argument as an object name + out = self.shell._inspect('pfile',parameter_s, namespaces) + # if not, try the input as a filename + if out == 'not found': + try: + filename = get_py_filename(parameter_s) + except IOError as msg: + print(msg) + return + page.page(self.shell.pycolorize(read_py_file(filename, skip_encoding_cookie=False))) + + @line_magic + def psearch(self, parameter_s=''): + """Search for object in namespaces by wildcard. + + %psearch [options] PATTERN [OBJECT TYPE] + + Note: ? can be used as a synonym for %psearch, at the beginning or at + the end: both a*? and ?a* are equivalent to '%psearch a*'. Still, the + rest of the command line must be unchanged (options come first), so + for example the following forms are equivalent + + %psearch -i a* function + -i a* function? + ?-i a* function + + Arguments: + + PATTERN + + where PATTERN is a string containing * as a wildcard similar to its + use in a shell. The pattern is matched in all namespaces on the + search path. By default objects starting with a single _ are not + matched, many yap_ipython generated objects have a single + underscore. The default is case insensitive matching. Matching is + also done on the attributes of objects and not only on the objects + in a module. + + [OBJECT TYPE] + + Is the name of a python type from the types module. The name is + given in lowercase without the ending type, ex. StringType is + written string. By adding a type here only objects matching the + given type are matched. Using all here makes the pattern match all + types (this is the default). + + Options: + + -a: makes the pattern match even objects whose names start with a + single underscore. These names are normally omitted from the + search. + + -i/-c: make the pattern case insensitive/sensitive. If neither of + these options are given, the default is read from your configuration + file, with the option ``InteractiveShell.wildcards_case_sensitive``. + If this option is not specified in your configuration file, yap_ipython's + internal default is to do a case sensitive search. + + -e/-s NAMESPACE: exclude/search a given namespace. The pattern you + specify can be searched in any of the following namespaces: + 'builtin', 'user', 'user_global','internal', 'alias', where + 'builtin' and 'user' are the search defaults. Note that you should + not use quotes when specifying namespaces. + + 'Builtin' contains the python module builtin, 'user' contains all + user data, 'alias' only contain the shell aliases and no python + objects, 'internal' contains objects used by yap_ipython. The + 'user_global' namespace is only used by embedded yap_ipython instances, + and it contains module-level globals. You can add namespaces to the + search with -s or exclude them with -e (these options can be given + more than once). + + Examples + -------- + :: + + %psearch a* -> objects beginning with an a + %psearch -e builtin a* -> objects NOT in the builtin space starting in a + %psearch a* function -> all functions beginning with an a + %psearch re.e* -> objects beginning with an e in module re + %psearch r*.e* -> objects that start with e in modules starting in r + %psearch r*.* string -> all strings in modules beginning with r + + Case sensitive search:: + + %psearch -c a* list all object beginning with lower case a + + Show objects beginning with a single _:: + + %psearch -a _* list objects beginning with a single underscore + """ + try: + parameter_s.encode('ascii') + except UnicodeEncodeError: + print('Python identifiers can only contain ascii characters.') + return + + # default namespaces to be searched + def_search = ['user_local', 'user_global', 'builtin'] + + # Process options/args + opts,args = self.parse_options(parameter_s,'cias:e:',list_all=True) + opt = opts.get + shell = self.shell + psearch = shell.inspector.psearch + + # select case options + if 'i' in opts: + ignore_case = True + elif 'c' in opts: + ignore_case = False + else: + ignore_case = not shell.wildcards_case_sensitive + + # Build list of namespaces to search from user options + def_search.extend(opt('s',[])) + ns_exclude = ns_exclude=opt('e',[]) + ns_search = [nm for nm in def_search if nm not in ns_exclude] + + # Call the actual search + try: + psearch(args,shell.ns_table,ns_search, + show_all=opt('a'),ignore_case=ignore_case) + except: + shell.showtraceback() + + @skip_doctest + @line_magic + def who_ls(self, parameter_s=''): + """Return a sorted list of all interactive variables. + + If arguments are given, only variables of types matching these + arguments are returned. + + Examples + -------- + + Define two variables and list them with who_ls:: + + In [1]: alpha = 123 + + In [2]: beta = 'test' + + In [3]: %who_ls + Out[3]: ['alpha', 'beta'] + + In [4]: %who_ls int + Out[4]: ['alpha'] + + In [5]: %who_ls str + Out[5]: ['beta'] + """ + + user_ns = self.shell.user_ns + user_ns_hidden = self.shell.user_ns_hidden + nonmatching = object() # This can never be in user_ns + out = [ i for i in user_ns + if not i.startswith('_') \ + and (user_ns[i] is not user_ns_hidden.get(i, nonmatching)) ] + + typelist = parameter_s.split() + if typelist: + typeset = set(typelist) + out = [i for i in out if type(user_ns[i]).__name__ in typeset] + + out.sort() + return out + + @skip_doctest + @line_magic + def who(self, parameter_s=''): + """Print all interactive variables, with some minimal formatting. + + If any arguments are given, only variables whose type matches one of + these are printed. For example:: + + %who function str + + will only list functions and strings, excluding all other types of + variables. To find the proper type names, simply use type(var) at a + command line to see how python prints type names. For example: + + :: + + In [1]: type('hello')\\ + Out[1]: + + indicates that the type name for strings is 'str'. + + ``%who`` always excludes executed names loaded through your configuration + file and things which are internal to yap_ipython. + + This is deliberate, as typically you may load many modules and the + purpose of %who is to show you only what you've manually defined. + + Examples + -------- + + Define two variables and list them with who:: + + In [1]: alpha = 123 + + In [2]: beta = 'test' + + In [3]: %who + alpha beta + + In [4]: %who int + alpha + + In [5]: %who str + beta + """ + + varlist = self.who_ls(parameter_s) + if not varlist: + if parameter_s: + print('No variables match your requested type.') + else: + print('Interactive namespace is empty.') + return + + # if we have variables, move on... + count = 0 + for i in varlist: + print(i+'\t', end=' ') + count += 1 + if count > 8: + count = 0 + print() + print() + + @skip_doctest + @line_magic + def whos(self, parameter_s=''): + """Like %who, but gives some extra information about each variable. + + The same type filtering of %who can be applied here. + + For all variables, the type is printed. Additionally it prints: + + - For {},[],(): their length. + + - For numpy arrays, a summary with shape, number of + elements, typecode and size in memory. + + - Everything else: a string representation, snipping their middle if + too long. + + Examples + -------- + + Define two variables and list them with whos:: + + In [1]: alpha = 123 + + In [2]: beta = 'test' + + In [3]: %whos + Variable Type Data/Info + -------------------------------- + alpha int 123 + beta str test + """ + + varnames = self.who_ls(parameter_s) + if not varnames: + if parameter_s: + print('No variables match your requested type.') + else: + print('Interactive namespace is empty.') + return + + # if we have variables, move on... + + # for these types, show len() instead of data: + seq_types = ['dict', 'list', 'tuple'] + + # for numpy arrays, display summary info + ndarray_type = None + if 'numpy' in sys.modules: + try: + from numpy import ndarray + except ImportError: + pass + else: + ndarray_type = ndarray.__name__ + + # Find all variable names and types so we can figure out column sizes + + # some types are well known and can be shorter + abbrevs = {'yap_ipython.core.macro.Macro' : 'Macro'} + def type_name(v): + tn = type(v).__name__ + return abbrevs.get(tn,tn) + + varlist = [self.shell.user_ns[n] for n in varnames] + + typelist = [] + for vv in varlist: + tt = type_name(vv) + + if tt=='instance': + typelist.append( abbrevs.get(str(vv.__class__), + str(vv.__class__))) + else: + typelist.append(tt) + + # column labels and # of spaces as separator + varlabel = 'Variable' + typelabel = 'Type' + datalabel = 'Data/Info' + colsep = 3 + # variable format strings + vformat = "{0:<{varwidth}}{1:<{typewidth}}" + aformat = "%s: %s elems, type `%s`, %s bytes" + # find the size of the columns to format the output nicely + varwidth = max(max(map(len,varnames)), len(varlabel)) + colsep + typewidth = max(max(map(len,typelist)), len(typelabel)) + colsep + # table header + print(varlabel.ljust(varwidth) + typelabel.ljust(typewidth) + \ + ' '+datalabel+'\n' + '-'*(varwidth+typewidth+len(datalabel)+1)) + # and the table itself + kb = 1024 + Mb = 1048576 # kb**2 + for vname,var,vtype in zip(varnames,varlist,typelist): + print(vformat.format(vname, vtype, varwidth=varwidth, typewidth=typewidth), end=' ') + if vtype in seq_types: + print("n="+str(len(var))) + elif vtype == ndarray_type: + vshape = str(var.shape).replace(',','').replace(' ','x')[1:-1] + if vtype==ndarray_type: + # numpy + vsize = var.size + vbytes = vsize*var.itemsize + vdtype = var.dtype + + if vbytes < 100000: + print(aformat % (vshape, vsize, vdtype, vbytes)) + else: + print(aformat % (vshape, vsize, vdtype, vbytes), end=' ') + if vbytes < Mb: + print('(%s kb)' % (vbytes/kb,)) + else: + print('(%s Mb)' % (vbytes/Mb,)) + else: + try: + vstr = str(var) + except UnicodeEncodeError: + vstr = var.encode(DEFAULT_ENCODING, + 'backslashreplace') + except: + vstr = "" % id(var) + vstr = vstr.replace('\n', '\\n') + if len(vstr) < 50: + print(vstr) + else: + print(vstr[:25] + "<...>" + vstr[-25:]) + + @line_magic + def reset(self, parameter_s=''): + """Resets the namespace by removing all names defined by the user, if + called without arguments, or by removing some types of objects, such + as everything currently in yap_ipython's In[] and Out[] containers (see + the parameters for details). + + Parameters + ---------- + -f : force reset without asking for confirmation. + + -s : 'Soft' reset: Only clears your namespace, leaving history intact. + References to objects may be kept. By default (without this option), + we do a 'hard' reset, giving you a new session and removing all + references to objects from the current session. + + in : reset input history + + out : reset output history + + dhist : reset directory history + + array : reset only variables that are NumPy arrays + + See Also + -------- + reset_selective : invoked as ``%reset_selective`` + + Examples + -------- + :: + + In [6]: a = 1 + + In [7]: a + Out[7]: 1 + + In [8]: 'a' in _ip.user_ns + Out[8]: True + + In [9]: %reset -f + + In [1]: 'a' in _ip.user_ns + Out[1]: False + + In [2]: %reset -f in + Flushing input history + + In [3]: %reset -f dhist in + Flushing directory history + Flushing input history + + Notes + ----- + Calling this magic from clients that do not implement standard input, + such as the ipython notebook interface, will reset the namespace + without confirmation. + """ + opts, args = self.parse_options(parameter_s,'sf', mode='list') + if 'f' in opts: + ans = True + else: + try: + ans = self.shell.ask_yes_no( + "Once deleted, variables cannot be recovered. Proceed (y/[n])?", + default='n') + except StdinNotImplementedError: + ans = True + if not ans: + print('Nothing done.') + return + + if 's' in opts: # Soft reset + user_ns = self.shell.user_ns + for i in self.who_ls(): + del(user_ns[i]) + elif len(args) == 0: # Hard reset + self.shell.reset(new_session = False) + + # reset in/out/dhist/array: previously extensinions/clearcmd.py + ip = self.shell + user_ns = self.shell.user_ns # local lookup, heavily used + + for target in args: + target = target.lower() # make matches case insensitive + if target == 'out': + print("Flushing output cache (%d entries)" % len(user_ns['_oh'])) + self.shell.displayhook.flush() + + elif target == 'in': + print("Flushing input history") + pc = self.shell.displayhook.prompt_count + 1 + for n in range(1, pc): + key = '_i'+repr(n) + user_ns.pop(key,None) + user_ns.update(dict(_i=u'',_ii=u'',_iii=u'')) + hm = ip.history_manager + # don't delete these, as %save and %macro depending on the + # length of these lists to be preserved + hm.input_hist_parsed[:] = [''] * pc + hm.input_hist_raw[:] = [''] * pc + # hm has internal machinery for _i,_ii,_iii, clear it out + hm._i = hm._ii = hm._iii = hm._i00 = u'' + + elif target == 'array': + # Support cleaning up numpy arrays + try: + from numpy import ndarray + # This must be done with items and not iteritems because + # we're going to modify the dict in-place. + for x,val in list(user_ns.items()): + if isinstance(val,ndarray): + del user_ns[x] + except ImportError: + print("reset array only works if Numpy is available.") + + elif target == 'dhist': + print("Flushing directory history") + del user_ns['_dh'][:] + + else: + print("Don't know how to reset ", end=' ') + print(target + ", please run `%reset?` for details") + + gc.collect() + + @line_magic + def reset_selective(self, parameter_s=''): + """Resets the namespace by removing names defined by the user. + + Input/Output history are left around in case you need them. + + %reset_selective [-f] regex + + No action is taken if regex is not included + + Options + -f : force reset without asking for confirmation. + + See Also + -------- + reset : invoked as ``%reset`` + + Examples + -------- + + We first fully reset the namespace so your output looks identical to + this example for pedagogical reasons; in practice you do not need a + full reset:: + + In [1]: %reset -f + + Now, with a clean namespace we can make a few variables and use + ``%reset_selective`` to only delete names that match our regexp:: + + In [2]: a=1; b=2; c=3; b1m=4; b2m=5; b3m=6; b4m=7; b2s=8 + + In [3]: who_ls + Out[3]: ['a', 'b', 'b1m', 'b2m', 'b2s', 'b3m', 'b4m', 'c'] + + In [4]: %reset_selective -f b[2-3]m + + In [5]: who_ls + Out[5]: ['a', 'b', 'b1m', 'b2s', 'b4m', 'c'] + + In [6]: %reset_selective -f d + + In [7]: who_ls + Out[7]: ['a', 'b', 'b1m', 'b2s', 'b4m', 'c'] + + In [8]: %reset_selective -f c + + In [9]: who_ls + Out[9]: ['a', 'b', 'b1m', 'b2s', 'b4m'] + + In [10]: %reset_selective -f b + + In [11]: who_ls + Out[11]: ['a'] + + Notes + ----- + Calling this magic from clients that do not implement standard input, + such as the ipython notebook interface, will reset the namespace + without confirmation. + """ + + opts, regex = self.parse_options(parameter_s,'f') + + if 'f' in opts: + ans = True + else: + try: + ans = self.shell.ask_yes_no( + "Once deleted, variables cannot be recovered. Proceed (y/[n])? ", + default='n') + except StdinNotImplementedError: + ans = True + if not ans: + print('Nothing done.') + return + user_ns = self.shell.user_ns + if not regex: + print('No regex pattern specified. Nothing done.') + return + else: + try: + m = re.compile(regex) + except TypeError: + raise TypeError('regex must be a string or compiled pattern') + for i in self.who_ls(): + if m.search(i): + del(user_ns[i]) + + @line_magic + def xdel(self, parameter_s=''): + """Delete a variable, trying to clear it from anywhere that + yap_ipython's machinery has references to it. By default, this uses + the identity of the named object in the user namespace to remove + references held under other names. The object is also removed + from the output history. + + Options + -n : Delete the specified name from all namespaces, without + checking their identity. + """ + opts, varname = self.parse_options(parameter_s,'n') + try: + self.shell.del_var(varname, ('n' in opts)) + except (NameError, ValueError) as e: + print(type(e).__name__ +": "+ str(e)) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/osm.py b/packages/python/yap_kernel/yap_ipython/core/magics/osm.py new file mode 100644 index 000000000..4593816c8 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/osm.py @@ -0,0 +1,792 @@ +"""Implementation of magic functions for interaction with the OS. + +Note: this module is named 'osm' instead of 'os' to avoid a collision with the +builtin. +""" +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import io +import os +import re +import sys +from pprint import pformat + +from yap_ipython.core import magic_arguments +from yap_ipython.core import oinspect +from yap_ipython.core import page +from yap_ipython.core.alias import AliasError, Alias +from yap_ipython.core.error import UsageError +from yap_ipython.core.magic import ( + Magics, compress_dhist, magics_class, line_magic, cell_magic, line_cell_magic +) +from yap_ipython.testing.skipdoctest import skip_doctest +from yap_ipython.utils.openpy import source_to_unicode +from yap_ipython.utils.process import abbrev_cwd +from yap_ipython.utils.terminal import set_term_title + + +@magics_class +class OSMagics(Magics): + """Magics to interact with the underlying OS (shell-type functionality). + """ + + @skip_doctest + @line_magic + def alias(self, parameter_s=''): + """Define an alias for a system command. + + '%alias alias_name cmd' defines 'alias_name' as an alias for 'cmd' + + Then, typing 'alias_name params' will execute the system command 'cmd + params' (from your underlying operating system). + + Aliases have lower precedence than magic functions and Python normal + variables, so if 'foo' is both a Python variable and an alias, the + alias can not be executed until 'del foo' removes the Python variable. + + You can use the %l specifier in an alias definition to represent the + whole line when the alias is called. For example:: + + In [2]: alias bracket echo "Input in brackets: <%l>" + In [3]: bracket hello world + Input in brackets: + + You can also define aliases with parameters using %s specifiers (one + per parameter):: + + In [1]: alias parts echo first %s second %s + In [2]: %parts A B + first A second B + In [3]: %parts A + Incorrect number of arguments: 2 expected. + parts is an alias to: 'echo first %s second %s' + + Note that %l and %s are mutually exclusive. You can only use one or + the other in your aliases. + + Aliases expand Python variables just like system calls using ! or !! + do: all expressions prefixed with '$' get expanded. For details of + the semantic rules, see PEP-215: + http://www.python.org/peps/pep-0215.html. This is the library used by + yap_ipython for variable expansion. If you want to access a true shell + variable, an extra $ is necessary to prevent its expansion by + yap_ipython:: + + In [6]: alias show echo + In [7]: PATH='A Python string' + In [8]: show $PATH + A Python string + In [9]: show $$PATH + /usr/local/lf9560/bin:/usr/local/intel/compiler70/ia32/bin:... + + You can use the alias facility to access all of $PATH. See the %rehashx + function, which automatically creates aliases for the contents of your + $PATH. + + If called with no parameters, %alias prints the current alias table + for your system. For posix systems, the default aliases are 'cat', + 'cp', 'mv', 'rm', 'rmdir', and 'mkdir', and other platform-specific + aliases are added. For windows-based systems, the default aliases are + 'copy', 'ddir', 'echo', 'ls', 'ldir', 'mkdir', 'ren', and 'rmdir'. + + You can see the definition of alias by adding a question mark in the + end:: + + In [1]: cat? + Repr: """ + + par = parameter_s.strip() + if not par: + aliases = sorted(self.shell.alias_manager.aliases) + # stored = self.shell.db.get('stored_aliases', {} ) + # for k, v in stored: + # atab.append(k, v[0]) + + print("Total number of aliases:", len(aliases)) + sys.stdout.flush() + return aliases + + # Now try to define a new one + try: + alias,cmd = par.split(None, 1) + except TypeError: + print(oinspect.getdoc(self.alias)) + return + + try: + self.shell.alias_manager.define_alias(alias, cmd) + except AliasError as e: + print(e) + # end magic_alias + + @line_magic + def unalias(self, parameter_s=''): + """Remove an alias""" + + aname = parameter_s.strip() + try: + self.shell.alias_manager.undefine_alias(aname) + except ValueError as e: + print(e) + return + + stored = self.shell.db.get('stored_aliases', {} ) + if aname in stored: + print("Removing %stored alias",aname) + del stored[aname] + self.shell.db['stored_aliases'] = stored + + @line_magic + def rehashx(self, parameter_s=''): + """Update the alias table with all executable files in $PATH. + + rehashx explicitly checks that every entry in $PATH is a file + with execute access (os.X_OK). + + Under Windows, it checks executability as a match against a + '|'-separated string of extensions, stored in the yap_ipython config + variable win_exec_ext. This defaults to 'exe|com|bat'. + + This function also resets the root module cache of module completer, + used on slow filesystems. + """ + from yap_ipython.core.alias import InvalidAliasError + + # for the benefit of module completer in ipy_completers.py + del self.shell.db['rootmodules_cache'] + + path = [os.path.abspath(os.path.expanduser(p)) for p in + os.environ.get('PATH','').split(os.pathsep)] + + syscmdlist = [] + # Now define isexec in a cross platform manner. + if os.name == 'posix': + isexec = lambda fname:os.path.isfile(fname) and \ + os.access(fname,os.X_OK) + else: + try: + winext = os.environ['pathext'].replace(';','|').replace('.','') + except KeyError: + winext = 'exe|com|bat|py' + if 'py' not in winext: + winext += '|py' + execre = re.compile(r'(.*)\.(%s)$' % winext,re.IGNORECASE) + isexec = lambda fname:os.path.isfile(fname) and execre.match(fname) + savedir = os.getcwd() + + # Now walk the paths looking for executables to alias. + try: + # write the whole loop for posix/Windows so we don't have an if in + # the innermost part + if os.name == 'posix': + for pdir in path: + try: + os.chdir(pdir) + dirlist = os.listdir(pdir) + except OSError: + continue + for ff in dirlist: + if isexec(ff): + try: + # Removes dots from the name since ipython + # will assume names with dots to be python. + if not self.shell.alias_manager.is_alias(ff): + self.shell.alias_manager.define_alias( + ff.replace('.',''), ff) + except InvalidAliasError: + pass + else: + syscmdlist.append(ff) + else: + no_alias = Alias.blacklist + for pdir in path: + try: + os.chdir(pdir) + dirlist = os.listdir(pdir) + except OSError: + continue + for ff in dirlist: + base, ext = os.path.splitext(ff) + if isexec(ff) and base.lower() not in no_alias: + if ext.lower() == '.exe': + ff = base + try: + # Removes dots from the name since ipython + # will assume names with dots to be python. + self.shell.alias_manager.define_alias( + base.lower().replace('.',''), ff) + except InvalidAliasError: + pass + syscmdlist.append(ff) + self.shell.db['syscmdlist'] = syscmdlist + finally: + os.chdir(savedir) + + @skip_doctest + @line_magic + def pwd(self, parameter_s=''): + """Return the current working directory path. + + Examples + -------- + :: + + In [9]: pwd + Out[9]: '/home/tsuser/sprint/ipython' + """ + try: + return os.getcwd() + except FileNotFoundError: + raise UsageError("CWD no longer exists - please use %cd to change directory.") + + @skip_doctest + @line_magic + def cd(self, parameter_s=''): + """Change the current working directory. + + This command automatically maintains an internal list of directories + you visit during your yap_ipython session, in the variable _dh. The + command %dhist shows this history nicely formatted. You can also + do 'cd -' to see directory history conveniently. + + Usage: + + cd 'dir': changes to directory 'dir'. + + cd -: changes to the last visited directory. + + cd -: changes to the n-th directory in the directory history. + + cd --foo: change to directory that matches 'foo' in history + + cd -b : jump to a bookmark set by %bookmark + (note: cd is enough if there is no + directory , but a bookmark with the name exists.) + 'cd -b ' allows you to tab-complete bookmark names. + + Options: + + -q: quiet. Do not print the working directory after the cd command is + executed. By default yap_ipython's cd command does print this directory, + since the default prompts do not display path information. + + Note that !cd doesn't work for this purpose because the shell where + !command runs is immediately discarded after executing 'command'. + + Examples + -------- + :: + + In [10]: cd parent/child + /home/tsuser/parent/child + """ + + try: + oldcwd = os.getcwd() + except FileNotFoundError: + # Happens if the CWD has been deleted. + oldcwd = None + + numcd = re.match(r'(-)(\d+)$',parameter_s) + # jump in directory history by number + if numcd: + nn = int(numcd.group(2)) + try: + ps = self.shell.user_ns['_dh'][nn] + except IndexError: + print('The requested directory does not exist in history.') + return + else: + opts = {} + elif parameter_s.startswith('--'): + ps = None + fallback = None + pat = parameter_s[2:] + dh = self.shell.user_ns['_dh'] + # first search only by basename (last component) + for ent in reversed(dh): + if pat in os.path.basename(ent) and os.path.isdir(ent): + ps = ent + break + + if fallback is None and pat in ent and os.path.isdir(ent): + fallback = ent + + # if we have no last part match, pick the first full path match + if ps is None: + ps = fallback + + if ps is None: + print("No matching entry in directory history") + return + else: + opts = {} + + + else: + opts, ps = self.parse_options(parameter_s, 'qb', mode='string') + # jump to previous + if ps == '-': + try: + ps = self.shell.user_ns['_dh'][-2] + except IndexError: + raise UsageError('%cd -: No previous directory to change to.') + # jump to bookmark if needed + else: + if not os.path.isdir(ps) or 'b' in opts: + bkms = self.shell.db.get('bookmarks', {}) + + if ps in bkms: + target = bkms[ps] + print('(bookmark:%s) -> %s' % (ps, target)) + ps = target + else: + if 'b' in opts: + raise UsageError("Bookmark '%s' not found. " + "Use '%%bookmark -l' to see your bookmarks." % ps) + + # at this point ps should point to the target dir + if ps: + try: + os.chdir(os.path.expanduser(ps)) + if hasattr(self.shell, 'term_title') and self.shell.term_title: + set_term_title(self.shell.term_title_format.format(cwd=abbrev_cwd())) + except OSError: + print(sys.exc_info()[1]) + else: + cwd = os.getcwd() + dhist = self.shell.user_ns['_dh'] + if oldcwd != cwd: + dhist.append(cwd) + self.shell.db['dhist'] = compress_dhist(dhist)[-100:] + + else: + os.chdir(self.shell.home_dir) + if hasattr(self.shell, 'term_title') and self.shell.term_title: + set_term_title(self.shell.term_title_format.format(cwd="~")) + cwd = os.getcwd() + dhist = self.shell.user_ns['_dh'] + + if oldcwd != cwd: + dhist.append(cwd) + self.shell.db['dhist'] = compress_dhist(dhist)[-100:] + if not 'q' in opts and self.shell.user_ns['_dh']: + print(self.shell.user_ns['_dh'][-1]) + + @line_magic + def env(self, parameter_s=''): + """Get, set, or list environment variables. + + Usage:\\ + + %env: lists all environment variables/values + %env var: get value for var + %env var val: set value for var + %env var=val: set value for var + %env var=$val: set value for var, using python expansion if possible + """ + if parameter_s.strip(): + split = '=' if '=' in parameter_s else ' ' + bits = parameter_s.split(split) + if len(bits) == 1: + key = parameter_s.strip() + if key in os.environ: + return os.environ[key] + else: + err = "Environment does not have key: {0}".format(key) + raise UsageError(err) + if len(bits) > 1: + return self.set_env(parameter_s) + return dict(os.environ) + + @line_magic + def set_env(self, parameter_s): + """Set environment variables. Assumptions are that either "val" is a + name in the user namespace, or val is something that evaluates to a + string. + + Usage:\\ + %set_env var val: set value for var + %set_env var=val: set value for var + %set_env var=$val: set value for var, using python expansion if possible + """ + split = '=' if '=' in parameter_s else ' ' + bits = parameter_s.split(split, 1) + if not parameter_s.strip() or len(bits)<2: + raise UsageError("usage is 'set_env var=val'") + var = bits[0].strip() + val = bits[1].strip() + if re.match(r'.*\s.*', var): + # an environment variable with whitespace is almost certainly + # not what the user intended. what's more likely is the wrong + # split was chosen, ie for "set_env cmd_args A=B", we chose + # '=' for the split and should have chosen ' '. to get around + # this, users should just assign directly to os.environ or use + # standard magic {var} expansion. + err = "refusing to set env var with whitespace: '{0}'" + err = err.format(val) + raise UsageError(err) + os.environ[var] = val + print('env: {0}={1}'.format(var,val)) + + @line_magic + def pushd(self, parameter_s=''): + """Place the current dir on stack and change directory. + + Usage:\\ + %pushd ['dirname'] + """ + + dir_s = self.shell.dir_stack + tgt = os.path.expanduser(parameter_s) + cwd = os.getcwd().replace(self.shell.home_dir,'~') + if tgt: + self.cd(parameter_s) + dir_s.insert(0,cwd) + return self.shell.magic('dirs') + + @line_magic + def popd(self, parameter_s=''): + """Change to directory popped off the top of the stack. + """ + if not self.shell.dir_stack: + raise UsageError("%popd on empty stack") + top = self.shell.dir_stack.pop(0) + self.cd(top) + print("popd ->",top) + + @line_magic + def dirs(self, parameter_s=''): + """Return the current directory stack.""" + + return self.shell.dir_stack + + @line_magic + def dhist(self, parameter_s=''): + """Print your history of visited directories. + + %dhist -> print full history\\ + %dhist n -> print last n entries only\\ + %dhist n1 n2 -> print entries between n1 and n2 (n2 not included)\\ + + This history is automatically maintained by the %cd command, and + always available as the global list variable _dh. You can use %cd - + to go to directory number . + + Note that most of time, you should view directory history by entering + cd -. + + """ + + dh = self.shell.user_ns['_dh'] + if parameter_s: + try: + args = map(int,parameter_s.split()) + except: + self.arg_err(self.dhist) + return + if len(args) == 1: + ini,fin = max(len(dh)-(args[0]),0),len(dh) + elif len(args) == 2: + ini,fin = args + fin = min(fin, len(dh)) + else: + self.arg_err(self.dhist) + return + else: + ini,fin = 0,len(dh) + print('Directory history (kept in _dh)') + for i in range(ini, fin): + print("%d: %s" % (i, dh[i])) + + @skip_doctest + @line_magic + def sc(self, parameter_s=''): + """Shell capture - run shell command and capture output (DEPRECATED use !). + + DEPRECATED. Suboptimal, retained for backwards compatibility. + + You should use the form 'var = !command' instead. Example: + + "%sc -l myfiles = ls ~" should now be written as + + "myfiles = !ls ~" + + myfiles.s, myfiles.l and myfiles.n still apply as documented + below. + + -- + %sc [options] varname=command + + yap_ipython will run the given command using commands.getoutput(), and + will then update the user's interactive namespace with a variable + called varname, containing the value of the call. Your command can + contain shell wildcards, pipes, etc. + + The '=' sign in the syntax is mandatory, and the variable name you + supply must follow Python's standard conventions for valid names. + + (A special format without variable name exists for internal use) + + Options: + + -l: list output. Split the output on newlines into a list before + assigning it to the given variable. By default the output is stored + as a single string. + + -v: verbose. Print the contents of the variable. + + In most cases you should not need to split as a list, because the + returned value is a special type of string which can automatically + provide its contents either as a list (split on newlines) or as a + space-separated string. These are convenient, respectively, either + for sequential processing or to be passed to a shell command. + + For example:: + + # Capture into variable a + In [1]: sc a=ls *py + + # a is a string with embedded newlines + In [2]: a + Out[2]: 'setup.py\\nwin32_manual_post_install.py' + + # which can be seen as a list: + In [3]: a.l + Out[3]: ['setup.py', 'win32_manual_post_install.py'] + + # or as a whitespace-separated string: + In [4]: a.s + Out[4]: 'setup.py win32_manual_post_install.py' + + # a.s is useful to pass as a single command line: + In [5]: !wc -l $a.s + 146 setup.py + 130 win32_manual_post_install.py + 276 total + + # while the list form is useful to loop over: + In [6]: for f in a.l: + ...: !wc -l $f + ...: + 146 setup.py + 130 win32_manual_post_install.py + + Similarly, the lists returned by the -l option are also special, in + the sense that you can equally invoke the .s attribute on them to + automatically get a whitespace-separated string from their contents:: + + In [7]: sc -l b=ls *py + + In [8]: b + Out[8]: ['setup.py', 'win32_manual_post_install.py'] + + In [9]: b.s + Out[9]: 'setup.py win32_manual_post_install.py' + + In summary, both the lists and strings used for output capture have + the following special attributes:: + + .l (or .list) : value as list. + .n (or .nlstr): value as newline-separated string. + .s (or .spstr): value as space-separated string. + """ + + opts,args = self.parse_options(parameter_s, 'lv') + # Try to get a variable name and command to run + try: + # the variable name must be obtained from the parse_options + # output, which uses shlex.split to strip options out. + var,_ = args.split('=', 1) + var = var.strip() + # But the command has to be extracted from the original input + # parameter_s, not on what parse_options returns, to avoid the + # quote stripping which shlex.split performs on it. + _,cmd = parameter_s.split('=', 1) + except ValueError: + var,cmd = '','' + # If all looks ok, proceed + split = 'l' in opts + out = self.shell.getoutput(cmd, split=split) + if 'v' in opts: + print('%s ==\n%s' % (var, pformat(out))) + if var: + self.shell.user_ns.update({var:out}) + else: + return out + + @line_cell_magic + def sx(self, line='', cell=None): + """Shell execute - run shell command and capture output (!! is short-hand). + + %sx command + + yap_ipython will run the given command using commands.getoutput(), and + return the result formatted as a list (split on '\\n'). Since the + output is _returned_, it will be stored in ipython's regular output + cache Out[N] and in the '_N' automatic variables. + + Notes: + + 1) If an input line begins with '!!', then %sx is automatically + invoked. That is, while:: + + !ls + + causes ipython to simply issue system('ls'), typing:: + + !!ls + + is a shorthand equivalent to:: + + %sx ls + + 2) %sx differs from %sc in that %sx automatically splits into a list, + like '%sc -l'. The reason for this is to make it as easy as possible + to process line-oriented shell output via further python commands. + %sc is meant to provide much finer control, but requires more + typing. + + 3) Just like %sc -l, this is a list with special attributes: + :: + + .l (or .list) : value as list. + .n (or .nlstr): value as newline-separated string. + .s (or .spstr): value as whitespace-separated string. + + This is very useful when trying to use such lists as arguments to + system commands.""" + + if cell is None: + # line magic + return self.shell.getoutput(line) + else: + opts,args = self.parse_options(line, '', 'out=') + output = self.shell.getoutput(cell) + out_name = opts.get('out', opts.get('o')) + if out_name: + self.shell.user_ns[out_name] = output + else: + return output + + system = line_cell_magic('system')(sx) + bang = cell_magic('!')(sx) + + @line_magic + def bookmark(self, parameter_s=''): + """Manage yap_ipython's bookmark system. + + %bookmark - set bookmark to current dir + %bookmark - set bookmark to + %bookmark -l - list all bookmarks + %bookmark -d - remove bookmark + %bookmark -r - remove all bookmarks + + You can later on access a bookmarked folder with:: + + %cd -b + + or simply '%cd ' if there is no directory called AND + there is such a bookmark defined. + + Your bookmarks persist through yap_ipython sessions, but they are + associated with each profile.""" + + opts,args = self.parse_options(parameter_s,'drl',mode='list') + if len(args) > 2: + raise UsageError("%bookmark: too many arguments") + + bkms = self.shell.db.get('bookmarks',{}) + + if 'd' in opts: + try: + todel = args[0] + except IndexError: + raise UsageError( + "%bookmark -d: must provide a bookmark to delete") + else: + try: + del bkms[todel] + except KeyError: + raise UsageError( + "%%bookmark -d: Can't delete bookmark '%s'" % todel) + + elif 'r' in opts: + bkms = {} + elif 'l' in opts: + bks = sorted(bkms) + if bks: + size = max(map(len, bks)) + else: + size = 0 + fmt = '%-'+str(size)+'s -> %s' + print('Current bookmarks:') + for bk in bks: + print(fmt % (bk, bkms[bk])) + else: + if not args: + raise UsageError("%bookmark: You must specify the bookmark name") + elif len(args)==1: + bkms[args[0]] = os.getcwd() + elif len(args)==2: + bkms[args[0]] = args[1] + self.shell.db['bookmarks'] = bkms + + @line_magic + def pycat(self, parameter_s=''): + """Show a syntax-highlighted file through a pager. + + This magic is similar to the cat utility, but it will assume the file + to be Python source and will show it with syntax highlighting. + + This magic command can either take a local filename, an url, + an history range (see %history) or a macro as argument :: + + %pycat myscript.py + %pycat 7-27 + %pycat myMacro + %pycat http://www.example.com/myscript.py + """ + if not parameter_s: + raise UsageError('Missing filename, URL, input history range, ' + 'or macro.') + + try : + cont = self.shell.find_user_code(parameter_s, skip_encoding_cookie=False) + except (ValueError, IOError): + print("Error: no such file, variable, URL, history range or macro") + return + + page.page(self.shell.pycolorize(source_to_unicode(cont))) + + @magic_arguments.magic_arguments() + @magic_arguments.argument( + '-a', '--append', action='store_true', default=False, + help='Append contents of the cell to an existing file. ' + 'The file will be created if it does not exist.' + ) + @magic_arguments.argument( + 'filename', type=str, + help='file to write' + ) + @cell_magic + def writefile(self, line, cell): + """Write the contents of the cell to a file. + + The file will be overwritten unless the -a (--append) flag is specified. + """ + args = magic_arguments.parse_argstring(self.writefile, line) + filename = os.path.expanduser(args.filename) + + if os.path.exists(filename): + if args.append: + print("Appending to %s" % filename) + else: + print("Overwriting %s" % filename) + else: + print("Writing %s" % filename) + + mode = 'a' if args.append else 'w' + with io.open(filename, mode, encoding='utf-8') as f: + f.write(cell) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/pylab.py b/packages/python/yap_kernel/yap_ipython/core/magics/pylab.py new file mode 100644 index 000000000..a6f1f1161 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/pylab.py @@ -0,0 +1,166 @@ +"""Implementation of magic functions for matplotlib/pylab support. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 The yap_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 +#----------------------------------------------------------------------------- + +# Our own packages +from traitlets.config.application import Application +from yap_ipython.core import magic_arguments +from yap_ipython.core.magic import Magics, magics_class, line_magic +from yap_ipython.testing.skipdoctest import skip_doctest +from warnings import warn +from yap_ipython.core.pylabtools import backends + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +magic_gui_arg = magic_arguments.argument( + 'gui', nargs='?', + help="""Name of the matplotlib backend to use %s. + If given, the corresponding matplotlib backend is used, + otherwise it will be matplotlib's default + (which you can set in your matplotlib config file). + """ % str(tuple(sorted(backends.keys()))) +) + + +@magics_class +class PylabMagics(Magics): + """Magics related to matplotlib's pylab support""" + + @skip_doctest + @line_magic + @magic_arguments.magic_arguments() + @magic_arguments.argument('-l', '--list', action='store_true', + help='Show available matplotlib backends') + @magic_gui_arg + def matplotlib(self, line=''): + """Set up matplotlib to work interactively. + + This function lets you activate matplotlib interactive support + at any point during an yap_ipython session. It does not import anything + into the interactive namespace. + + If you are using the inline matplotlib backend in the yap_ipython Notebook + you can set which figure formats are enabled using the following:: + + In [1]: from yap_ipython.display import set_matplotlib_formats + + In [2]: set_matplotlib_formats('pdf', 'svg') + + The default for inline figures sets `bbox_inches` to 'tight'. This can + cause discrepancies between the displayed image and the identical + image created using `savefig`. This behavior can be disabled using the + `%config` magic:: + + In [3]: %config InlineBackend.print_figure_kwargs = {'bbox_inches':None} + + In addition, see the docstring of + `yap_ipython.display.set_matplotlib_formats` and + `yap_ipython.display.set_matplotlib_close` for more information on + changing additional behaviors of the inline backend. + + Examples + -------- + To enable the inline backend for usage with the yap_ipython Notebook:: + + In [1]: %matplotlib inline + + In this case, where the matplotlib default is TkAgg:: + + In [2]: %matplotlib + Using matplotlib backend: TkAgg + + But you can explicitly request a different GUI backend:: + + In [3]: %matplotlib qt + + You can list the available backends using the -l/--list option:: + + In [4]: %matplotlib --list + Available matplotlib backends: ['osx', 'qt4', 'qt5', 'gtk3', 'notebook', 'wx', 'qt', 'nbagg', + 'gtk', 'tk', 'inline'] + """ + args = magic_arguments.parse_argstring(self.matplotlib, line) + if args.list: + backends_list = list(backends.keys()) + print("Available matplotlib backends: %s" % backends_list) + else: + gui, backend = self.shell.enable_matplotlib(args.gui) + self._show_matplotlib_backend(args.gui, backend) + + @skip_doctest + @line_magic + @magic_arguments.magic_arguments() + @magic_arguments.argument( + '--no-import-all', action='store_true', default=None, + help="""Prevent yap_ipython from performing ``import *`` into the interactive namespace. + + You can govern the default behavior of this flag with the + InteractiveShellApp.pylab_import_all configurable. + """ + ) + @magic_gui_arg + def pylab(self, line=''): + """Load numpy and matplotlib to work interactively. + + This function lets you activate pylab (matplotlib, numpy and + interactive support) at any point during an yap_ipython session. + + %pylab makes the following imports:: + + import numpy + import matplotlib + from matplotlib import pylab, mlab, pyplot + np = numpy + plt = pyplot + + from yap_ipython.display import display + from yap_ipython.core.pylabtools import figsize, getfigs + + from pylab import * + from numpy import * + + If you pass `--no-import-all`, the last two `*` imports will be excluded. + + See the %matplotlib magic for more details about activating matplotlib + without affecting the interactive namespace. + """ + args = magic_arguments.parse_argstring(self.pylab, line) + if args.no_import_all is None: + # get default from Application + if Application.initialized(): + app = Application.instance() + try: + import_all = app.pylab_import_all + except AttributeError: + import_all = True + else: + # nothing specified, no app - default True + import_all = True + else: + # invert no-import flag + import_all = not args.no_import_all + + gui, backend, clobbered = self.shell.enable_pylab(args.gui, import_all=import_all) + self._show_matplotlib_backend(args.gui, backend) + print ("Populating the interactive namespace from numpy and matplotlib") + if clobbered: + warn("pylab import has clobbered these variables: %s" % clobbered + + "\n`%matplotlib` prevents importing * from pylab and numpy" + ) + + def _show_matplotlib_backend(self, gui, backend): + """show matplotlib message backend message""" + if not gui or gui == 'auto': + print("Using matplotlib backend: %s" % backend) diff --git a/packages/python/yap_kernel/yap_ipython/core/magics/script.py b/packages/python/yap_kernel/yap_ipython/core/magics/script.py new file mode 100644 index 000000000..24ec6c37d --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/magics/script.py @@ -0,0 +1,279 @@ +"""Magic functions for running cells in various scripts.""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import errno +import os +import sys +import signal +import time +from subprocess import Popen, PIPE +import atexit + +from yap_ipython.core import magic_arguments +from yap_ipython.core.magic import ( + Magics, magics_class, line_magic, cell_magic +) +from yap_ipython.lib.backgroundjobs import BackgroundJobManager +from yap_ipython.utils import py3compat +from yap_ipython.utils.process import arg_split +from traitlets import List, Dict, default + +#----------------------------------------------------------------------------- +# Magic implementation classes +#----------------------------------------------------------------------------- + +def script_args(f): + """single decorator for adding script args""" + args = [ + magic_arguments.argument( + '--out', type=str, + help="""The variable in which to store stdout from the script. + If the script is backgrounded, this will be the stdout *pipe*, + instead of the stderr text itself. + """ + ), + magic_arguments.argument( + '--err', type=str, + help="""The variable in which to store stderr from the script. + If the script is backgrounded, this will be the stderr *pipe*, + instead of the stderr text itself. + """ + ), + magic_arguments.argument( + '--bg', action="store_true", + help="""Whether to run the script in the background. + If given, the only way to see the output of the command is + with --out/err. + """ + ), + magic_arguments.argument( + '--proc', type=str, + help="""The variable in which to store Popen instance. + This is used only when --bg option is given. + """ + ), + ] + for arg in args: + f = arg(f) + return f + +@magics_class +class ScriptMagics(Magics): + """Magics for talking to scripts + + This defines a base `%%script` cell magic for running a cell + with a program in a subprocess, and registers a few top-level + magics that call %%script with common interpreters. + """ + script_magics = List( + help="""Extra script cell magics to define + + This generates simple wrappers of `%%script foo` as `%%foo`. + + If you want to add script magics that aren't on your path, + specify them in script_paths + """, + ).tag(config=True) + @default('script_magics') + def _script_magics_default(self): + """default to a common list of programs""" + + defaults = [ + 'sh', + 'bash', + 'perl', + 'ruby', + 'python', + 'python2', + 'python3', + 'pypy', + ] + if os.name == 'nt': + defaults.extend([ + 'cmd', + ]) + + return defaults + + script_paths = Dict( + help="""Dict mapping short 'ruby' names to full paths, such as '/opt/secret/bin/ruby' + + Only necessary for items in script_magics where the default path will not + find the right interpreter. + """ + ).tag(config=True) + + def __init__(self, shell=None): + super(ScriptMagics, self).__init__(shell=shell) + self._generate_script_magics() + self.job_manager = BackgroundJobManager() + self.bg_processes = [] + atexit.register(self.kill_bg_processes) + + def __del__(self): + self.kill_bg_processes() + + def _generate_script_magics(self): + cell_magics = self.magics['cell'] + for name in self.script_magics: + cell_magics[name] = self._make_script_magic(name) + + def _make_script_magic(self, name): + """make a named magic, that calls %%script with a particular program""" + # expand to explicit path if necessary: + script = self.script_paths.get(name, name) + + @magic_arguments.magic_arguments() + @script_args + def named_script_magic(line, cell): + # if line, add it as cl-flags + if line: + line = "%s %s" % (script, line) + else: + line = script + return self.shebang(line, cell) + + # write a basic docstring: + named_script_magic.__doc__ = \ + """%%{name} script magic + + Run cells with {script} in a subprocess. + + This is a shortcut for `%%script {script}` + """.format(**locals()) + + return named_script_magic + + @magic_arguments.magic_arguments() + @script_args + @cell_magic("script") + def shebang(self, line, cell): + """Run a cell via a shell command + + The `%%script` line is like the #! line of script, + specifying a program (bash, perl, ruby, etc.) with which to run. + + The rest of the cell is run by that program. + + Examples + -------- + :: + + In [1]: %%script bash + ...: for i in 1 2 3; do + ...: echo $i + ...: done + 1 + 2 + 3 + """ + argv = arg_split(line, posix = not sys.platform.startswith('win')) + args, cmd = self.shebang.parser.parse_known_args(argv) + + try: + p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE) + except OSError as e: + if e.errno == errno.ENOENT: + print("Couldn't find program: %r" % cmd[0]) + return + else: + raise + + if not cell.endswith('\n'): + cell += '\n' + cell = cell.encode('utf8', 'replace') + if args.bg: + self.bg_processes.append(p) + self._gc_bg_processes() + if args.out: + self.shell.user_ns[args.out] = p.stdout + if args.err: + self.shell.user_ns[args.err] = p.stderr + self.job_manager.new(self._run_script, p, cell, daemon=True) + if args.proc: + self.shell.user_ns[args.proc] = p + return + + try: + out, err = p.communicate(cell) + except KeyboardInterrupt: + try: + p.send_signal(signal.SIGINT) + time.sleep(0.1) + if p.poll() is not None: + print("Process is interrupted.") + return + p.terminate() + time.sleep(0.1) + if p.poll() is not None: + print("Process is terminated.") + return + p.kill() + print("Process is killed.") + except OSError: + pass + except Exception as e: + print("Error while terminating subprocess (pid=%i): %s" \ + % (p.pid, e)) + return + out = py3compat.decode(out) + err = py3compat.decode(err) + if args.out: + self.shell.user_ns[args.out] = out + else: + sys.stdout.write(out) + sys.stdout.flush() + if args.err: + self.shell.user_ns[args.err] = err + else: + sys.stderr.write(err) + sys.stderr.flush() + + def _run_script(self, p, cell): + """callback for running the script in the background""" + p.stdin.write(cell) + p.stdin.close() + p.wait() + + @line_magic("killbgscripts") + def killbgscripts(self, _nouse_=''): + """Kill all BG processes started by %%script and its family.""" + self.kill_bg_processes() + print("All background processes were killed.") + + def kill_bg_processes(self): + """Kill all BG processes which are still running.""" + if not self.bg_processes: + return + for p in self.bg_processes: + if p.poll() is None: + try: + p.send_signal(signal.SIGINT) + except: + pass + time.sleep(0.1) + self._gc_bg_processes() + if not self.bg_processes: + return + for p in self.bg_processes: + if p.poll() is None: + try: + p.terminate() + except: + pass + time.sleep(0.1) + self._gc_bg_processes() + if not self.bg_processes: + return + for p in self.bg_processes: + if p.poll() is None: + try: + p.kill() + except: + pass + self._gc_bg_processes() + + def _gc_bg_processes(self): + self.bg_processes = [p for p in self.bg_processes if p.poll() is None] diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/__init__.py b/packages/python/yap_kernel/yap_ipython/core/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/bad_all.py b/packages/python/yap_kernel/yap_ipython/core/tests/bad_all.py new file mode 100644 index 000000000..a7716ab6f --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/bad_all.py @@ -0,0 +1,14 @@ +"""Module with bad __all__ + +To test https://github.com/ipython/ipython/issues/9678 +""" + +def evil(): + pass + +def puppies(): + pass + +__all__ = [evil, # Bad + 'puppies', # Good + ] diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/daft_extension/daft_extension.py b/packages/python/yap_kernel/yap_ipython/core/tests/daft_extension/daft_extension.py new file mode 100644 index 000000000..7a9954b81 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/daft_extension/daft_extension.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +Useless yap_ipython extension to test installing and loading extensions. +""" +some_vars = {'arq': 185} + +def load_ipython_extension(ip): + # set up simplified quantity input + ip.push(some_vars) + +def unload_ipython_extension(ip): + ip.drop_by_id(some_vars) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/nonascii.py b/packages/python/yap_kernel/yap_ipython/core/tests/nonascii.py new file mode 100644 index 000000000..78801dfd0 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/nonascii.py @@ -0,0 +1,4 @@ +# coding: iso-8859-5 +# (Unlikely to be the default encoding for most testers.) +# <- Cyrillic characters +u = '' diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/nonascii2.py b/packages/python/yap_kernel/yap_ipython/core/tests/nonascii2.py new file mode 100644 index 000000000..a1afce4f3 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/nonascii2.py @@ -0,0 +1,4 @@ +# coding: iso-8859-5 +# (Unlikely to be the default encoding for most testers.) +# БЖџрстуфхцчшщъыьэюя <- Cyrillic characters +'Ўт№Ф' diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/print_argv.py b/packages/python/yap_kernel/yap_ipython/core/tests/print_argv.py new file mode 100644 index 000000000..0e92bddcb --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/print_argv.py @@ -0,0 +1,2 @@ +import sys +print(sys.argv[1:]) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/refbug.py b/packages/python/yap_kernel/yap_ipython/core/tests/refbug.py new file mode 100644 index 000000000..d336c15f0 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/refbug.py @@ -0,0 +1,47 @@ +"""Minimal script to reproduce our nasty reference counting bug. + +The problem is related to https://github.com/ipython/ipython/issues/141 + +The original fix for that appeared to work, but John D. Hunter found a +matplotlib example which, when run twice in a row, would break. The problem +were references held by open figures to internals of Tkinter. + +This code reproduces the problem that John saw, without matplotlib. + +This script is meant to be called by other parts of the test suite that call it +via %run as if it were executed interactively by the user. As of 2011-05-29, +test_run.py calls it. +""" + +#----------------------------------------------------------------------------- +# Module imports +#----------------------------------------------------------------------------- +import sys + +from yap_ipython import get_ipython + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +# This needs to be here because nose and other test runners will import +# this module. Importing this module has potential side effects that we +# want to prevent. +if __name__ == '__main__': + + ip = get_ipython() + + if not '_refbug_cache' in ip.user_ns: + ip.user_ns['_refbug_cache'] = [] + + + aglobal = 'Hello' + def f(): + return aglobal + + cache = ip.user_ns['_refbug_cache'] + cache.append(f) + + def call_f(): + for func in cache: + print('lowercased:',func().lower()) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/simpleerr.py b/packages/python/yap_kernel/yap_ipython/core/tests/simpleerr.py new file mode 100644 index 000000000..34e697029 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/simpleerr.py @@ -0,0 +1,32 @@ +"""Error script. DO NOT EDIT FURTHER! It will break exception doctests!!!""" +import sys + +def div0(): + "foo" + x = 1 + y = 0 + x/y + +def sysexit(stat, mode): + raise SystemExit(stat, 'Mode = %s' % mode) + +def bar(mode): + "bar" + if mode=='div': + div0() + elif mode=='exit': + try: + stat = int(sys.argv[2]) + except: + stat = 1 + sysexit(stat, mode) + else: + raise ValueError('Unknown mode') + +if __name__ == '__main__': + try: + mode = sys.argv[1] + except IndexError: + mode = 'div' + + bar(mode) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/tclass.py b/packages/python/yap_kernel/yap_ipython/core/tests/tclass.py new file mode 100644 index 000000000..6bd9ffcd9 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/tclass.py @@ -0,0 +1,34 @@ +"""Simple script to be run *twice*, to check reference counting bugs. + +See test_run for details.""" + + +import sys + +# We want to ensure that while objects remain available for immediate access, +# objects from *previous* runs of the same script get collected, to avoid +# accumulating massive amounts of old references. +class C(object): + def __init__(self,name): + self.name = name + self.p = print + self.flush_stdout = sys.stdout.flush + + def __del__(self): + self.p('tclass.py: deleting object:',self.name) + self.flush_stdout() + +try: + name = sys.argv[1] +except IndexError: + pass +else: + if name.startswith('C'): + c = C(name) + +#print >> sys.stderr, "ARGV:", sys.argv # dbg + +# This next print statement is NOT debugging, we're making the check on a +# completely separate process so we verify by capturing stdout: +print('ARGV 1-:', sys.argv[1:]) +sys.stdout.flush() diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_alias.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_alias.py new file mode 100644 index 000000000..d77ab8482 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_alias.py @@ -0,0 +1,62 @@ +from yap_ipython.utils.capture import capture_output + +import nose.tools as nt + +def test_alias_lifecycle(): + name = 'test_alias1' + cmd = 'echo "Hello"' + am = _ip.alias_manager + am.clear_aliases() + am.define_alias(name, cmd) + assert am.is_alias(name) + nt.assert_equal(am.retrieve_alias(name), cmd) + nt.assert_in((name, cmd), am.aliases) + + # Test running the alias + orig_system = _ip.system + result = [] + _ip.system = result.append + try: + _ip.run_cell('%{}'.format(name)) + result = [c.strip() for c in result] + nt.assert_equal(result, [cmd]) + finally: + _ip.system = orig_system + + # Test removing the alias + am.undefine_alias(name) + assert not am.is_alias(name) + with nt.assert_raises(ValueError): + am.retrieve_alias(name) + nt.assert_not_in((name, cmd), am.aliases) + + +def test_alias_args_error(): + """Error expanding with wrong number of arguments""" + _ip.alias_manager.define_alias('parts', 'echo first %s second %s') + # capture stderr: + with capture_output() as cap: + _ip.run_cell('parts 1') + + nt.assert_equal(cap.stderr.split(':')[0], 'UsageError') + +def test_alias_args_commented(): + """Check that alias correctly ignores 'commented out' args""" + _ip.magic('alias commetarg echo this is %%s a commented out arg') + + with capture_output() as cap: + _ip.run_cell('commetarg') + + nt.assert_equal(cap.stdout, 'this is %s a commented out arg') + +def test_alias_args_commented_nargs(): + """Check that alias correctly counts args, excluding those commented out""" + am = _ip.alias_manager + alias_name = 'comargcount' + cmd = 'echo this is %%s a commented out arg and this is not %s' + + am.define_alias(alias_name, cmd) + assert am.is_alias(alias_name) + + thealias = am.get_alias(alias_name) + nt.assert_equal(thealias.nargs, 1) \ No newline at end of file diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_application.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_application.py new file mode 100644 index 000000000..93c14c449 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_application.py @@ -0,0 +1,73 @@ +# coding: utf-8 +"""Tests for yap_ipython.core.application""" + +import os +import tempfile + +import nose.tools as nt + +from traitlets import Unicode + +from yap_ipython.core.application import BaseYAPApplication +from yap_ipython.testing import decorators as dec +from yap_ipython.utils.tempdir import TemporaryDirectory + + +@dec.onlyif_unicode_paths +def test_unicode_cwd(): + """Check that yap_ipython starts with non-ascii characters in the path.""" + wd = tempfile.mkdtemp(suffix=u"€") + + old_wd = os.getcwd() + os.chdir(wd) + #raise Exception(repr(os.getcwd())) + try: + app = BaseYAPApplication() + # The lines below are copied from Application.initialize() + app.init_profile_dir() + app.init_config_files() + app.load_config_file(suppress_errors=False) + finally: + os.chdir(old_wd) + +@dec.onlyif_unicode_paths +def test_unicode_ipdir(): + """Check that yap_ipython starts with non-ascii characters in the IP dir.""" + ipdir = tempfile.mkdtemp(suffix=u"€") + + # Create the config file, so it tries to load it. + with open(os.path.join(ipdir, 'ipython_config.py'), "w") as f: + pass + + old_ipdir1 = os.environ.pop("IPYTHONDIR", None) + old_ipdir2 = os.environ.pop("IPYTHON_DIR", None) + os.environ["IPYTHONDIR"] = ipdir + try: + app = BaseYAPApplication() + # The lines below are copied from Application.initialize() + app.init_profile_dir() + app.init_config_files() + app.load_config_file(suppress_errors=False) + finally: + if old_ipdir1: + os.environ["IPYTHONDIR"] = old_ipdir1 + if old_ipdir2: + os.environ["IPYTHONDIR"] = old_ipdir2 + +def test_cli_priority(): + with TemporaryDirectory() as td: + + class TestApp(BaseYAPApplication): + test = Unicode().tag(config=True) + + # Create the config file, so it tries to load it. + with open(os.path.join(td, 'ipython_config.py'), "w") as f: + f.write("c.TestApp.test = 'config file'") + + app = TestApp() + app.initialize(['--profile-dir', td]) + nt.assert_equal(app.test, 'config file') + app = TestApp() + app.initialize(['--profile-dir', td, '--TestApp.test=cli']) + nt.assert_equal(app.test, 'cli') + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_autocall.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_autocall.py new file mode 100644 index 000000000..448d7dc59 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_autocall.py @@ -0,0 +1,73 @@ +"""These kinds of tests are less than ideal, but at least they run. + +This was an old test that was being run interactively in the top-level tests/ +directory, which we are removing. For now putting this here ensures at least +we do run the test, though ultimately this functionality should all be tested +with better-isolated tests that don't rely on the global instance in iptest. +""" +from yap_ipython.core.splitinput import LineInfo +from yap_ipython.core.prefilter import AutocallChecker +from yap_ipython.utils import py3compat +from yap_ipython.testing.globalipapp import get_ipython + + +ip = get_ipython() + + +@py3compat.doctest_refactor_print +def doctest_autocall(): + """ + In [1]: def f1(a,b,c): + ...: return a+b+c + ...: + + In [2]: def f2(a): + ...: return a + a + ...: + + In [3]: def r(x): + ...: return True + ...: + + In [4]: ;f2 a b c + Out[4]: 'a b ca b c' + + In [5]: assert _ == "a b ca b c" + + In [6]: ,f1 a b c + Out[6]: 'abc' + + In [7]: assert _ == 'abc' + + In [8]: print _ + abc + + In [9]: /f1 1,2,3 + Out[9]: 6 + + In [10]: assert _ == 6 + + In [11]: /f2 4 + Out[11]: 8 + + In [12]: assert _ == 8 + + In [12]: del f1, f2 + + In [13]: ,r a + Out[13]: True + + In [14]: assert _ == True + + In [15]: r'a' + Out[15]: 'a' + + In [16]: assert _ == 'a' + """ + + +def test_autocall_should_ignore_raw_strings(): + line_info = LineInfo("r'a'") + pm = ip.prefilter_manager + ac = AutocallChecker(shell=pm.shell, prefilter_manager=pm, config=pm.config) + assert ac.check(line_info) is None diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_compilerop.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_compilerop.py new file mode 100644 index 000000000..f4b3dfbc1 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_compilerop.py @@ -0,0 +1,73 @@ +# coding: utf-8 +"""Tests for the compilerop module. +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2010-2011 The yap_ipython Development Team. +# +# Distributed under the terms of the BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Stdlib imports +import linecache +import sys + +# Third-party imports +import nose.tools as nt + +# Our own imports +from yap_ipython.core import compilerop + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- + +def test_code_name(): + code = 'x=1' + name = compilerop.code_name(code) + nt.assert_true(name.startswith(' ncache) + +def setUp(): + # Check we're in a proper Python 2 environment (some imports, such + # as GTK, can change the default encoding, which can hide bugs.) + nt.assert_equal(sys.getdefaultencoding(), "utf-8") + +def test_cache_unicode(): + cp = compilerop.CachingCompiler() + ncache = len(linecache.cache) + cp.cache(u"t = 'žćčšđ'") + nt.assert_true(len(linecache.cache) > ncache) + +def test_compiler_check_cache(): + """Test the compiler properly manages the cache. + """ + # Rather simple-minded tests that just exercise the API + cp = compilerop.CachingCompiler() + cp.cache('x=1', 99) + # Ensure now that after clearing the cache, our entries survive + linecache.checkcache() + for k in linecache.cache: + if k.startswith(' (0, 10): + yield _test_complete, 'jedi >0.9 should complete and not crash', 'a=1;a.', 'real' + yield _test_complete, 'can infer first argument', 'a=(1,"foo");a[0].', 'real' + yield _test_complete, 'can infer second argument', 'a=(1,"foo");a[1].', 'capitalize' + yield _test_complete, 'cover duplicate completions', 'im', 'import', 0, 2 + + yield _test_not_complete, 'does not mix types', 'a=(1,"foo");a[0].', 'capitalize' + +def test_completion_have_signature(): + """ + Lets make sure jedi is capable of pulling out the signature of the function we are completing. + """ + ip = get_ipython() + with provisionalcompleter(): + completions = ip.Completer.completions('ope', 3) + c = next(completions) # should be `open` + assert 'file' in c.signature, "Signature of function was not found by completer" + assert 'encoding' in c.signature, "Signature of function was not found by completer" + + +def test_deduplicate_completions(): + """ + Test that completions are correctly deduplicated (even if ranges are not the same) + """ + ip = get_ipython() + ip.ex(textwrap.dedent(''' + class Z: + zoo = 1 + ''')) + with provisionalcompleter(): + l = list(_deduplicate_completions('Z.z', ip.Completer.completions('Z.z', 3))) + + assert len(l) == 1, 'Completions (Z.z) correctly deduplicate: %s ' % l + assert l[0].text == 'zoo' # and not `it.accumulate` + + +def test_greedy_completions(): + """ + Test the capability of the Greedy completer. + + Most of the test here do not really show off the greedy completer, for proof + each of the text bellow now pass with Jedi. The greedy completer is capable of more. + + See the :any:`test_dict_key_completion_contexts` + + """ + ip = get_ipython() + ip.ex('a=list(range(5))') + _,c = ip.complete('.',line='a[0].') + nt.assert_false('.real' in c, + "Shouldn't have completed on a[0]: %s"%c) + with greedy_completion(), provisionalcompleter(): + def _(line, cursor_pos, expect, message, completion): + _,c = ip.complete('.', line=line, cursor_pos=cursor_pos) + with provisionalcompleter(): + completions = ip.Completer.completions(line, cursor_pos) + nt.assert_in(expect, c, message%c) + nt.assert_in(completion, completions) + + yield _, 'a[0].', 5, 'a[0].real', "Should have completed on a[0].: %s", Completion(5,5, 'real') + yield _, 'a[0].r', 6, 'a[0].real', "Should have completed on a[0].r: %s", Completion(5,6, 'real') + + if sys.version_info > (3, 4): + yield _, 'a[0].from_', 10, 'a[0].from_bytes', "Should have completed on a[0].from_: %s", Completion(5, 10, 'from_bytes') + + +def test_omit__names(): + # also happens to test IPCompleter as a configurable + ip = get_ipython() + ip._hidden_attr = 1 + ip._x = {} + c = ip.Completer + ip.ex('ip=get_ipython()') + cfg = Config() + cfg.IPCompleter.omit__names = 0 + c.update_config(cfg) + with provisionalcompleter(): + s,matches = c.complete('ip.') + completions = set(c.completions('ip.', 3)) + + nt.assert_in('ip.__str__', matches) + nt.assert_in(Completion(3, 3, '__str__'), completions) + + nt.assert_in('ip._hidden_attr', matches) + nt.assert_in(Completion(3,3, "_hidden_attr"), completions) + + + cfg = Config() + cfg.IPCompleter.omit__names = 1 + c.update_config(cfg) + with provisionalcompleter(): + s,matches = c.complete('ip.') + completions = set(c.completions('ip.', 3)) + + nt.assert_not_in('ip.__str__', matches) + nt.assert_not_in(Completion(3,3,'__str__'), completions) + + # nt.assert_in('ip._hidden_attr', matches) + nt.assert_in(Completion(3,3, "_hidden_attr"), completions) + + cfg = Config() + cfg.IPCompleter.omit__names = 2 + c.update_config(cfg) + with provisionalcompleter(): + s,matches = c.complete('ip.') + completions = set(c.completions('ip.', 3)) + + nt.assert_not_in('ip.__str__', matches) + nt.assert_not_in(Completion(3,3,'__str__'), completions) + + nt.assert_not_in('ip._hidden_attr', matches) + nt.assert_not_in(Completion(3,3, "_hidden_attr"), completions) + + with provisionalcompleter(): + s,matches = c.complete('ip._x.') + completions = set(c.completions('ip._x.', 6)) + + nt.assert_in('ip._x.keys', matches) + nt.assert_in(Completion(6,6, "keys"), completions) + + del ip._hidden_attr + del ip._x + + +def test_limit_to__all__False_ok(): + """ + Limit to all is deprecated, once we remove it this test can go away. + """ + ip = get_ipython() + c = ip.Completer + ip.ex('class D: x=24') + ip.ex('d=D()') + cfg = Config() + cfg.IPCompleter.limit_to__all__ = False + c.update_config(cfg) + s, matches = c.complete('d.') + nt.assert_in('d.x', matches) + + +def test_get__all__entries_ok(): + class A(object): + __all__ = ['x', 1] + words = completer.get__all__entries(A()) + nt.assert_equal(words, ['x']) + + +def test_get__all__entries_no__all__ok(): + class A(object): + pass + words = completer.get__all__entries(A()) + nt.assert_equal(words, []) + + +def test_func_kw_completions(): + ip = get_ipython() + c = ip.Completer + ip.ex('def myfunc(a=1,b=2): return a+b') + s, matches = c.complete(None, 'myfunc(1,b') + nt.assert_in('b=', matches) + # Simulate completing with cursor right after b (pos==10): + s, matches = c.complete(None, 'myfunc(1,b)', 10) + nt.assert_in('b=', matches) + s, matches = c.complete(None, 'myfunc(a="escaped\\")string",b') + nt.assert_in('b=', matches) + #builtin function + s, matches = c.complete(None, 'min(k, k') + nt.assert_in('key=', matches) + + +def test_default_arguments_from_docstring(): + ip = get_ipython() + c = ip.Completer + kwd = c._default_arguments_from_docstring( + 'min(iterable[, key=func]) -> value') + nt.assert_equal(kwd, ['key']) + #with cython type etc + kwd = c._default_arguments_from_docstring( + 'Minuit.migrad(self, int ncall=10000, resume=True, int nsplit=1)\n') + nt.assert_equal(kwd, ['ncall', 'resume', 'nsplit']) + #white spaces + kwd = c._default_arguments_from_docstring( + '\n Minuit.migrad(self, int ncall=10000, resume=True, int nsplit=1)\n') + nt.assert_equal(kwd, ['ncall', 'resume', 'nsplit']) + +def test_line_magics(): + ip = get_ipython() + c = ip.Completer + s, matches = c.complete(None, 'lsmag') + nt.assert_in('%lsmagic', matches) + s, matches = c.complete(None, '%lsmag') + nt.assert_in('%lsmagic', matches) + + +def test_cell_magics(): + from yap_ipython.core.magic import register_cell_magic + + @register_cell_magic + def _foo_cellm(line, cell): + pass + + ip = get_ipython() + c = ip.Completer + + s, matches = c.complete(None, '_foo_ce') + nt.assert_in('%%_foo_cellm', matches) + s, matches = c.complete(None, '%%_foo_ce') + nt.assert_in('%%_foo_cellm', matches) + + +def test_line_cell_magics(): + from yap_ipython.core.magic import register_line_cell_magic + + @register_line_cell_magic + def _bar_cellm(line, cell): + pass + + ip = get_ipython() + c = ip.Completer + + # The policy here is trickier, see comments in completion code. The + # returned values depend on whether the user passes %% or not explicitly, + # and this will show a difference if the same name is both a line and cell + # magic. + s, matches = c.complete(None, '_bar_ce') + nt.assert_in('%_bar_cellm', matches) + nt.assert_in('%%_bar_cellm', matches) + s, matches = c.complete(None, '%_bar_ce') + nt.assert_in('%_bar_cellm', matches) + nt.assert_in('%%_bar_cellm', matches) + s, matches = c.complete(None, '%%_bar_ce') + nt.assert_not_in('%_bar_cellm', matches) + nt.assert_in('%%_bar_cellm', matches) + + +def test_magic_completion_order(): + ip = get_ipython() + c = ip.Completer + + # Test ordering of line and cell magics. + text, matches = c.complete("timeit") + nt.assert_equal(matches, ["%timeit", "%%timeit"]) + + +def test_magic_completion_shadowing(): + ip = get_ipython() + c = ip.Completer + + # Before importing matplotlib, %matplotlib magic should be the only option. + text, matches = c.complete("mat") + nt.assert_equal(matches, ["%matplotlib"]) + + # The newly introduced name should shadow the magic. + ip.run_cell("matplotlib = 1") + text, matches = c.complete("mat") + nt.assert_equal(matches, ["matplotlib"]) + + # After removing matplotlib from namespace, the magic should again be + # the only option. + del ip.user_ns["matplotlib"] + text, matches = c.complete("mat") + nt.assert_equal(matches, ["%matplotlib"]) + +def test_magic_completion_shadowing_explicit(): + """ + If the user try to complete a shadowed magic, and explicit % start should + still return the completions. + """ + ip = get_ipython() + c = ip.Completer + + # Before importing matplotlib, %matplotlib magic should be the only option. + text, matches = c.complete("%mat") + nt.assert_equal(matches, ["%matplotlib"]) + + ip.run_cell("matplotlib = 1") + + # After removing matplotlib from namespace, the magic should still be + # the only option. + text, matches = c.complete("%mat") + nt.assert_equal(matches, ["%matplotlib"]) + +def test_magic_config(): + ip = get_ipython() + c = ip.Completer + + s, matches = c.complete(None, 'conf') + nt.assert_in('%config', matches) + s, matches = c.complete(None, 'conf') + nt.assert_not_in('AliasManager', matches) + s, matches = c.complete(None, 'config ') + nt.assert_in('AliasManager', matches) + s, matches = c.complete(None, '%config ') + nt.assert_in('AliasManager', matches) + s, matches = c.complete(None, 'config Ali') + nt.assert_list_equal(['AliasManager'], matches) + s, matches = c.complete(None, '%config Ali') + nt.assert_list_equal(['AliasManager'], matches) + s, matches = c.complete(None, 'config AliasManager') + nt.assert_list_equal(['AliasManager'], matches) + s, matches = c.complete(None, '%config AliasManager') + nt.assert_list_equal(['AliasManager'], matches) + s, matches = c.complete(None, 'config AliasManager.') + nt.assert_in('AliasManager.default_aliases', matches) + s, matches = c.complete(None, '%config AliasManager.') + nt.assert_in('AliasManager.default_aliases', matches) + s, matches = c.complete(None, 'config AliasManager.de') + nt.assert_list_equal(['AliasManager.default_aliases'], matches) + s, matches = c.complete(None, 'config AliasManager.de') + nt.assert_list_equal(['AliasManager.default_aliases'], matches) + + +def test_magic_color(): + ip = get_ipython() + c = ip.Completer + + s, matches = c.complete(None, 'colo') + nt.assert_in('%colors', matches) + s, matches = c.complete(None, 'colo') + nt.assert_not_in('NoColor', matches) + s, matches = c.complete(None, '%colors') # No trailing space + nt.assert_not_in('NoColor', matches) + s, matches = c.complete(None, 'colors ') + nt.assert_in('NoColor', matches) + s, matches = c.complete(None, '%colors ') + nt.assert_in('NoColor', matches) + s, matches = c.complete(None, 'colors NoCo') + nt.assert_list_equal(['NoColor'], matches) + s, matches = c.complete(None, '%colors NoCo') + nt.assert_list_equal(['NoColor'], matches) + + +def test_match_dict_keys(): + """ + Test that match_dict_keys works on a couple of use case does return what + expected, and does not crash + """ + delims = ' \t\n`!@#$^&*()=+[{]}\\|;:\'",<>?' + + + keys = ['foo', b'far'] + assert match_dict_keys(keys, "b'", delims=delims) == ("'", 2 ,['far']) + assert match_dict_keys(keys, "b'f", delims=delims) == ("'", 2 ,['far']) + assert match_dict_keys(keys, 'b"', delims=delims) == ('"', 2 ,['far']) + assert match_dict_keys(keys, 'b"f', delims=delims) == ('"', 2 ,['far']) + + assert match_dict_keys(keys, "'", delims=delims) == ("'", 1 ,['foo']) + assert match_dict_keys(keys, "'f", delims=delims) == ("'", 1 ,['foo']) + assert match_dict_keys(keys, '"', delims=delims) == ('"', 1 ,['foo']) + assert match_dict_keys(keys, '"f', delims=delims) == ('"', 1 ,['foo']) + + match_dict_keys + + +def test_dict_key_completion_string(): + """Test dictionary key completion for string keys""" + ip = get_ipython() + complete = ip.Completer.complete + + ip.user_ns['d'] = {'abc': None} + + # check completion at different stages + _, matches = complete(line_buffer="d[") + nt.assert_in("'abc'", matches) + nt.assert_not_in("'abc']", matches) + + _, matches = complete(line_buffer="d['") + nt.assert_in("abc", matches) + nt.assert_not_in("abc']", matches) + + _, matches = complete(line_buffer="d['a") + nt.assert_in("abc", matches) + nt.assert_not_in("abc']", matches) + + # check use of different quoting + _, matches = complete(line_buffer="d[\"") + nt.assert_in("abc", matches) + nt.assert_not_in('abc\"]', matches) + + _, matches = complete(line_buffer="d[\"a") + nt.assert_in("abc", matches) + nt.assert_not_in('abc\"]', matches) + + # check sensitivity to following context + _, matches = complete(line_buffer="d[]", cursor_pos=2) + nt.assert_in("'abc'", matches) + + _, matches = complete(line_buffer="d['']", cursor_pos=3) + nt.assert_in("abc", matches) + nt.assert_not_in("abc'", matches) + nt.assert_not_in("abc']", matches) + + # check multiple solutions are correctly returned and that noise is not + ip.user_ns['d'] = {'abc': None, 'abd': None, 'bad': None, object(): None, + 5: None} + + _, matches = complete(line_buffer="d['a") + nt.assert_in("abc", matches) + nt.assert_in("abd", matches) + nt.assert_not_in("bad", matches) + assert not any(m.endswith((']', '"', "'")) for m in matches), matches + + # check escaping and whitespace + ip.user_ns['d'] = {'a\nb': None, 'a\'b': None, 'a"b': None, 'a word': None} + _, matches = complete(line_buffer="d['a") + nt.assert_in("a\\nb", matches) + nt.assert_in("a\\'b", matches) + nt.assert_in("a\"b", matches) + nt.assert_in("a word", matches) + assert not any(m.endswith((']', '"', "'")) for m in matches), matches + + # - can complete on non-initial word of the string + _, matches = complete(line_buffer="d['a w") + nt.assert_in("word", matches) + + # - understands quote escaping + _, matches = complete(line_buffer="d['a\\'") + nt.assert_in("b", matches) + + # - default quoting should work like repr + _, matches = complete(line_buffer="d[") + nt.assert_in("\"a'b\"", matches) + + # - when opening quote with ", possible to match with unescaped apostrophe + _, matches = complete(line_buffer="d[\"a'") + nt.assert_in("b", matches) + + # need to not split at delims that readline won't split at + if '-' not in ip.Completer.splitter.delims: + ip.user_ns['d'] = {'before-after': None} + _, matches = complete(line_buffer="d['before-af") + nt.assert_in('before-after', matches) + +def test_dict_key_completion_contexts(): + """Test expression contexts in which dict key completion occurs""" + ip = get_ipython() + complete = ip.Completer.complete + d = {'abc': None} + ip.user_ns['d'] = d + + class C: + data = d + ip.user_ns['C'] = C + ip.user_ns['get'] = lambda: d + + def assert_no_completion(**kwargs): + _, matches = complete(**kwargs) + nt.assert_not_in('abc', matches) + nt.assert_not_in('abc\'', matches) + nt.assert_not_in('abc\']', matches) + nt.assert_not_in('\'abc\'', matches) + nt.assert_not_in('\'abc\']', matches) + + def assert_completion(**kwargs): + _, matches = complete(**kwargs) + nt.assert_in("'abc'", matches) + nt.assert_not_in("'abc']", matches) + + # no completion after string closed, even if reopened + assert_no_completion(line_buffer="d['a'") + assert_no_completion(line_buffer="d[\"a\"") + assert_no_completion(line_buffer="d['a' + ") + assert_no_completion(line_buffer="d['a' + '") + + # completion in non-trivial expressions + assert_completion(line_buffer="+ d[") + assert_completion(line_buffer="(d[") + assert_completion(line_buffer="C.data[") + + # greedy flag + def assert_completion(**kwargs): + _, matches = complete(**kwargs) + nt.assert_in("get()['abc']", matches) + + assert_no_completion(line_buffer="get()[") + with greedy_completion(): + assert_completion(line_buffer="get()[") + assert_completion(line_buffer="get()['") + assert_completion(line_buffer="get()['a") + assert_completion(line_buffer="get()['ab") + assert_completion(line_buffer="get()['abc") + + + +def test_dict_key_completion_bytes(): + """Test handling of bytes in dict key completion""" + ip = get_ipython() + complete = ip.Completer.complete + + ip.user_ns['d'] = {'abc': None, b'abd': None} + + _, matches = complete(line_buffer="d[") + nt.assert_in("'abc'", matches) + nt.assert_in("b'abd'", matches) + + if False: # not currently implemented + _, matches = complete(line_buffer="d[b") + nt.assert_in("b'abd'", matches) + nt.assert_not_in("b'abc'", matches) + + _, matches = complete(line_buffer="d[b'") + nt.assert_in("abd", matches) + nt.assert_not_in("abc", matches) + + _, matches = complete(line_buffer="d[B'") + nt.assert_in("abd", matches) + nt.assert_not_in("abc", matches) + + _, matches = complete(line_buffer="d['") + nt.assert_in("abc", matches) + nt.assert_not_in("abd", matches) + + +def test_dict_key_completion_unicode_py3(): + """Test handling of unicode in dict key completion""" + ip = get_ipython() + complete = ip.Completer.complete + + ip.user_ns['d'] = {u'a\u05d0': None} + + # query using escape + if sys.platform != 'win32': + # Known failure on Windows + _, matches = complete(line_buffer="d['a\\u05d0") + nt.assert_in("u05d0", matches) # tokenized after \\ + + # query using character + _, matches = complete(line_buffer="d['a\u05d0") + nt.assert_in(u"a\u05d0", matches) + + with greedy_completion(): + # query using escape + _, matches = complete(line_buffer="d['a\\u05d0") + nt.assert_in("d['a\\u05d0']", matches) # tokenized after \\ + + # query using character + _, matches = complete(line_buffer="d['a\u05d0") + nt.assert_in(u"d['a\u05d0']", matches) + + + +@dec.skip_without('numpy') +def test_struct_array_key_completion(): + """Test dict key completion applies to numpy struct arrays""" + import numpy + ip = get_ipython() + complete = ip.Completer.complete + ip.user_ns['d'] = numpy.array([], dtype=[('hello', 'f'), ('world', 'f')]) + _, matches = complete(line_buffer="d['") + nt.assert_in("hello", matches) + nt.assert_in("world", matches) + # complete on the numpy struct itself + dt = numpy.dtype([('my_head', [('my_dt', '>u4'), ('my_df', '>u4')]), + ('my_data', '>f4', 5)]) + x = numpy.zeros(2, dtype=dt) + ip.user_ns['d'] = x[1] + _, matches = complete(line_buffer="d['") + nt.assert_in("my_head", matches) + nt.assert_in("my_data", matches) + # complete on a nested level + with greedy_completion(): + ip.user_ns['d'] = numpy.zeros(2, dtype=dt) + _, matches = complete(line_buffer="d[1]['my_head']['") + nt.assert_true(any(["my_dt" in m for m in matches])) + nt.assert_true(any(["my_df" in m for m in matches])) + + +@dec.skip_without('pandas') +def test_dataframe_key_completion(): + """Test dict key completion applies to pandas DataFrames""" + import pandas + ip = get_ipython() + complete = ip.Completer.complete + ip.user_ns['d'] = pandas.DataFrame({'hello': [1], 'world': [2]}) + _, matches = complete(line_buffer="d['") + nt.assert_in("hello", matches) + nt.assert_in("world", matches) + + +def test_dict_key_completion_invalids(): + """Smoke test cases dict key completion can't handle""" + ip = get_ipython() + complete = ip.Completer.complete + + ip.user_ns['no_getitem'] = None + ip.user_ns['no_keys'] = [] + ip.user_ns['cant_call_keys'] = dict + ip.user_ns['empty'] = {} + ip.user_ns['d'] = {'abc': 5} + + _, matches = complete(line_buffer="no_getitem['") + _, matches = complete(line_buffer="no_keys['") + _, matches = complete(line_buffer="cant_call_keys['") + _, matches = complete(line_buffer="empty['") + _, matches = complete(line_buffer="name_error['") + _, matches = complete(line_buffer="d['\\") # incomplete escape + +class KeyCompletable(object): + def __init__(self, things=()): + self.things = things + + def _ipython_key_completions_(self): + return list(self.things) + +def test_object_key_completion(): + ip = get_ipython() + ip.user_ns['key_completable'] = KeyCompletable(['qwerty', 'qwick']) + + _, matches = ip.Completer.complete(line_buffer="key_completable['qw") + nt.assert_in('qwerty', matches) + nt.assert_in('qwick', matches) + + +class NamedInstanceMetaclass(type): + def __getitem__(cls, item): + return cls.get_instance(item) + +class NamedInstanceClass(object, metaclass=NamedInstanceMetaclass): + def __init__(self, name): + if not hasattr(self.__class__, 'instances'): + self.__class__.instances = {} + self.__class__.instances[name] = self + + @classmethod + def _ipython_key_completions_(cls): + return cls.instances.keys() + + @classmethod + def get_instance(cls, name): + return cls.instances[name] + +def test_class_key_completion(): + ip = get_ipython() + NamedInstanceClass('qwerty') + NamedInstanceClass('qwick') + ip.user_ns['named_instance_class'] = NamedInstanceClass + + _, matches = ip.Completer.complete(line_buffer="named_instance_class['qw") + nt.assert_in('qwerty', matches) + nt.assert_in('qwick', matches) + +def test_tryimport(): + """ + Test that try-import don't crash on trailing dot, and import modules before + """ + from yap_ipython.core.completerlib import try_import + assert(try_import("yap_ipython.")) + + +def test_aimport_module_completer(): + ip = get_ipython() + _, matches = ip.complete('i', '%aimport i') + nt.assert_in('io', matches) + nt.assert_not_in('int', matches) + +def test_nested_import_module_completer(): + ip = get_ipython() + _, matches = ip.complete(None, 'import yap_ipython.co', 17) + nt.assert_in('yap_ipython.core', matches) + nt.assert_not_in('import yap_ipython.core', matches) + nt.assert_not_in('yap_ipython.display', matches) + +def test_import_module_completer(): + ip = get_ipython() + _, matches = ip.complete('i', 'import i') + nt.assert_in('io', matches) + nt.assert_not_in('int', matches) + +def test_from_module_completer(): + ip = get_ipython() + _, matches = ip.complete('B', 'from io import B', 16) + nt.assert_in('BytesIO', matches) + nt.assert_not_in('BaseException', matches) + +def test_snake_case_completion(): + ip = get_ipython() + ip.user_ns['some_three'] = 3 + ip.user_ns['some_four'] = 4 + _, matches = ip.complete("s_", "print(s_f") + nt.assert_in('some_three', matches) + nt.assert_in('some_four', matches) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_completerlib.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_completerlib.py new file mode 100644 index 000000000..6832f6530 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_completerlib.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""Tests for completerlib. + +""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import os +import shutil +import sys +import tempfile +import unittest +from os.path import join + +import nose.tools as nt + +from yap_ipython.core.completerlib import magic_run_completer, module_completion +from yap_ipython.utils.tempdir import TemporaryDirectory +from yap_ipython.testing.decorators import onlyif_unicode_paths + + +class MockEvent(object): + def __init__(self, line): + self.line = line + +#----------------------------------------------------------------------------- +# Test functions begin +#----------------------------------------------------------------------------- +class Test_magic_run_completer(unittest.TestCase): + files = [u"aao.py", u"a.py", u"b.py", u"aao.txt"] + dirs = [u"adir/", "bdir/"] + + def setUp(self): + self.BASETESTDIR = tempfile.mkdtemp() + for fil in self.files: + with open(join(self.BASETESTDIR, fil), "w") as sfile: + sfile.write("pass\n") + for d in self.dirs: + os.mkdir(join(self.BASETESTDIR, d)) + + self.oldpath = os.getcwd() + os.chdir(self.BASETESTDIR) + + def tearDown(self): + os.chdir(self.oldpath) + shutil.rmtree(self.BASETESTDIR) + + def test_1(self): + """Test magic_run_completer, should match two alterntives + """ + event = MockEvent(u"%run a") + mockself = None + match = set(magic_run_completer(mockself, event)) + self.assertEqual(match, {u"a.py", u"aao.py", u"adir/"}) + + def test_2(self): + """Test magic_run_completer, should match one alterntive + """ + event = MockEvent(u"%run aa") + mockself = None + match = set(magic_run_completer(mockself, event)) + self.assertEqual(match, {u"aao.py"}) + + def test_3(self): + """Test magic_run_completer with unterminated " """ + event = MockEvent(u'%run "a') + mockself = None + match = set(magic_run_completer(mockself, event)) + self.assertEqual(match, {u"a.py", u"aao.py", u"adir/"}) + + def test_completion_more_args(self): + event = MockEvent(u'%run a.py ') + match = set(magic_run_completer(None, event)) + self.assertEqual(match, set(self.files + self.dirs)) + + def test_completion_in_dir(self): + # Github issue #3459 + event = MockEvent(u'%run a.py {}'.format(join(self.BASETESTDIR, 'a'))) + print(repr(event.line)) + match = set(magic_run_completer(None, event)) + # We specifically use replace here rather than normpath, because + # at one point there were duplicates 'adir' and 'adir/', and normpath + # would hide the failure for that. + self.assertEqual(match, {join(self.BASETESTDIR, f).replace('\\','/') + for f in (u'a.py', u'aao.py', u'aao.txt', u'adir/')}) + +class Test_magic_run_completer_nonascii(unittest.TestCase): + @onlyif_unicode_paths + def setUp(self): + self.BASETESTDIR = tempfile.mkdtemp() + for fil in [u"aaø.py", u"a.py", u"b.py"]: + with open(join(self.BASETESTDIR, fil), "w") as sfile: + sfile.write("pass\n") + self.oldpath = os.getcwd() + os.chdir(self.BASETESTDIR) + + def tearDown(self): + os.chdir(self.oldpath) + shutil.rmtree(self.BASETESTDIR) + + @onlyif_unicode_paths + def test_1(self): + """Test magic_run_completer, should match two alterntives + """ + event = MockEvent(u"%run a") + mockself = None + match = set(magic_run_completer(mockself, event)) + self.assertEqual(match, {u"a.py", u"aaø.py"}) + + @onlyif_unicode_paths + def test_2(self): + """Test magic_run_completer, should match one alterntive + """ + event = MockEvent(u"%run aa") + mockself = None + match = set(magic_run_completer(mockself, event)) + self.assertEqual(match, {u"aaø.py"}) + + @onlyif_unicode_paths + def test_3(self): + """Test magic_run_completer with unterminated " """ + event = MockEvent(u'%run "a') + mockself = None + match = set(magic_run_completer(mockself, event)) + self.assertEqual(match, {u"a.py", u"aaø.py"}) + +# module_completer: + +def test_import_invalid_module(): + """Testing of issue https://github.com/ipython/ipython/issues/1107""" + invalid_module_names = {'foo-bar', 'foo:bar', '10foo'} + valid_module_names = {'foobar'} + with TemporaryDirectory() as tmpdir: + sys.path.insert( 0, tmpdir ) + for name in invalid_module_names | valid_module_names: + filename = os.path.join(tmpdir, name + '.py') + open(filename, 'w').close() + + s = set( module_completion('import foo') ) + intersection = s.intersection(invalid_module_names) + nt.assert_equal(intersection, set()) + + assert valid_module_names.issubset(s), valid_module_names.intersection(s) + + +def test_bad_module_all(): + """Test module with invalid __all__ + + https://github.com/ipython/ipython/issues/9678 + """ + testsdir = os.path.dirname(__file__) + sys.path.insert(0, testsdir) + try: + results = module_completion('from bad_all import ') + nt.assert_in('puppies', results) + for r in results: + nt.assert_is_instance(r, str) + finally: + sys.path.remove(testsdir) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_debugger.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_debugger.py new file mode 100644 index 000000000..52e52f66a --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_debugger.py @@ -0,0 +1,225 @@ +"""Tests for debugging machinery. +""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import sys +import warnings + +import nose.tools as nt + +from yap_ipython.core import debugger + +#----------------------------------------------------------------------------- +# Helper classes, from CPython's Pdb test suite +#----------------------------------------------------------------------------- + +class _FakeInput(object): + """ + A fake input stream for pdb's interactive debugger. Whenever a + line is read, print it (to simulate the user typing it), and then + return it. The set of lines to return is specified in the + constructor; they should not have trailing newlines. + """ + def __init__(self, lines): + self.lines = iter(lines) + + def readline(self): + line = next(self.lines) + print(line) + return line+'\n' + +class PdbTestInput(object): + """Context manager that makes testing Pdb in doctests easier.""" + + def __init__(self, input): + self.input = input + + def __enter__(self): + self.real_stdin = sys.stdin + sys.stdin = _FakeInput(self.input) + + def __exit__(self, *exc): + sys.stdin = self.real_stdin + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + +def test_longer_repr(): + try: + from reprlib import repr as trepr # Py 3 + except ImportError: + from repr import repr as trepr # Py 2 + + a = '1234567890'* 7 + ar = "'1234567890123456789012345678901234567890123456789012345678901234567890'" + a_trunc = "'123456789012...8901234567890'" + nt.assert_equal(trepr(a), a_trunc) + # The creation of our tracer modifies the repr module's repr function + # in-place, since that global is used directly by the stdlib's pdb module. + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + debugger.Tracer() + nt.assert_equal(trepr(a), ar) + +def test_ipdb_magics(): + '''Test calling some yap_ipython magics from ipdb. + + First, set up some test functions and classes which we can inspect. + + >>> class ExampleClass(object): + ... """Docstring for ExampleClass.""" + ... def __init__(self): + ... """Docstring for ExampleClass.__init__""" + ... pass + ... def __str__(self): + ... return "ExampleClass()" + + >>> def example_function(x, y, z="hello"): + ... """Docstring for example_function.""" + ... pass + + >>> old_trace = sys.gettrace() + + Create a function which triggers ipdb. + + >>> def trigger_ipdb(): + ... a = ExampleClass() + ... debugger.Pdb().set_trace() + + >>> with PdbTestInput([ + ... 'pdef example_function', + ... 'pdoc ExampleClass', + ... 'up', + ... 'down', + ... 'list', + ... 'pinfo a', + ... 'll', + ... 'continue', + ... ]): + ... trigger_ipdb() + --Return-- + None + > (3)trigger_ipdb() + 1 def trigger_ipdb(): + 2 a = ExampleClass() + ----> 3 debugger.Pdb().set_trace() + + ipdb> pdef example_function + example_function(x, y, z='hello') + ipdb> pdoc ExampleClass + Class docstring: + Docstring for ExampleClass. + Init docstring: + Docstring for ExampleClass.__init__ + ipdb> up + > (11)() + 7 'pinfo a', + 8 'll', + 9 'continue', + 10 ]): + ---> 11 trigger_ipdb() + + ipdb> down + None + > (3)trigger_ipdb() + 1 def trigger_ipdb(): + 2 a = ExampleClass() + ----> 3 debugger.Pdb().set_trace() + + ipdb> list + 1 def trigger_ipdb(): + 2 a = ExampleClass() + ----> 3 debugger.Pdb().set_trace() + + ipdb> pinfo a + Type: ExampleClass + String form: ExampleClass() + Namespace: Local... + Docstring: Docstring for ExampleClass. + Init docstring: Docstring for ExampleClass.__init__ + ipdb> ll + 1 def trigger_ipdb(): + 2 a = ExampleClass() + ----> 3 debugger.Pdb().set_trace() + + ipdb> continue + + Restore previous trace function, e.g. for coverage.py + + >>> sys.settrace(old_trace) + ''' + +def test_ipdb_magics2(): + '''Test ipdb with a very short function. + + >>> old_trace = sys.gettrace() + + >>> def bar(): + ... pass + + Run ipdb. + + >>> with PdbTestInput([ + ... 'continue', + ... ]): + ... debugger.Pdb().runcall(bar) + > (2)bar() + 1 def bar(): + ----> 2 pass + + ipdb> continue + + Restore previous trace function, e.g. for coverage.py + + >>> sys.settrace(old_trace) + ''' + +def can_quit(): + '''Test that quit work in ipydb + + >>> old_trace = sys.gettrace() + + >>> def bar(): + ... pass + + >>> with PdbTestInput([ + ... 'quit', + ... ]): + ... debugger.Pdb().runcall(bar) + > (2)bar() + 1 def bar(): + ----> 2 pass + + ipdb> quit + + Restore previous trace function, e.g. for coverage.py + + >>> sys.settrace(old_trace) + ''' + + +def can_exit(): + '''Test that quit work in ipydb + + >>> old_trace = sys.gettrace() + + >>> def bar(): + ... pass + + >>> with PdbTestInput([ + ... 'exit', + ... ]): + ... debugger.Pdb().runcall(bar) + > (2)bar() + 1 def bar(): + ----> 2 pass + + ipdb> exit + + Restore previous trace function, e.g. for coverage.py + + >>> sys.settrace(old_trace) + ''' diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_display.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_display.py new file mode 100644 index 000000000..1e4a50a7a --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_display.py @@ -0,0 +1,389 @@ +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import json +import os +import warnings + +from unittest import mock + +import nose.tools as nt + +from yap_ipython.core import display +from yap_ipython.core.getipython import get_ipython +from yap_ipython.utils.io import capture_output +from yap_ipython.utils.tempdir import NamedFileInTemporaryDirectory +from yap_ipython import paths as ipath +from yap_ipython.testing.tools import AssertPrints, AssertNotPrints + +import yap_ipython.testing.decorators as dec + +def test_image_size(): + """Simple test for display.Image(args, width=x,height=y)""" + thisurl = 'http://www.google.fr/images/srpr/logo3w.png' + img = display.Image(url=thisurl, width=200, height=200) + nt.assert_equal(u'' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl, metadata={'width':200, 'height':200}) + nt.assert_equal(u'' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl, width=200) + nt.assert_equal(u'' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl) + nt.assert_equal(u'' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl, unconfined=True) + nt.assert_equal(u'' % (thisurl), img._repr_html_()) + + +def test_image_mimes(): + fmt = get_ipython().display_formatter.format + for format in display.Image._ACCEPTABLE_EMBEDDINGS: + mime = display.Image._MIMETYPES[format] + img = display.Image(b'garbage', format=format) + data, metadata = fmt(img) + nt.assert_equal(sorted(data), sorted([mime, 'text/plain'])) + + +def test_geojson(): + + gj = display.GeoJSON(data={ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-81.327, 296.038] + }, + "properties": { + "name": "Inca City" + } + }, + url_template="http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/{basemap_id}/{z}/{x}/{y}.png", + layer_options={ + "basemap_id": "celestia_mars-shaded-16k_global", + "attribution": "Celestia/praesepe", + "minZoom": 0, + "maxZoom": 18, + }) + nt.assert_equal(u'', str(gj)) + +def test_retina_png(): + here = os.path.dirname(__file__) + img = display.Image(os.path.join(here, "2x2.png"), retina=True) + nt.assert_equal(img.height, 1) + nt.assert_equal(img.width, 1) + data, md = img._repr_png_() + nt.assert_equal(md['width'], 1) + nt.assert_equal(md['height'], 1) + +def test_retina_jpeg(): + here = os.path.dirname(__file__) + img = display.Image(os.path.join(here, "2x2.jpg"), retina=True) + nt.assert_equal(img.height, 1) + nt.assert_equal(img.width, 1) + data, md = img._repr_jpeg_() + nt.assert_equal(md['width'], 1) + nt.assert_equal(md['height'], 1) + +def test_base64image(): + display.Image("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AAAAACAAHiIbwzAAAAAElFTkSuQmCC") + +def test_image_filename_defaults(): + '''test format constraint, and validity of jpeg and png''' + tpath = ipath.get_ipython_package_dir() + nt.assert_raises(ValueError, display.Image, filename=os.path.join(tpath, 'testing/tests/badformat.zip'), + embed=True) + nt.assert_raises(ValueError, display.Image) + nt.assert_raises(ValueError, display.Image, data='this is not an image', format='badformat', embed=True) + # check boths paths to allow packages to test at build and install time + imgfile = os.path.join(tpath, 'core/tests/2x2.png') + img = display.Image(filename=imgfile) + nt.assert_equal('png', img.format) + nt.assert_is_not_none(img._repr_png_()) + img = display.Image(filename=os.path.join(tpath, 'testing/tests/logo.jpg'), embed=False) + nt.assert_equal('jpeg', img.format) + nt.assert_is_none(img._repr_jpeg_()) + +def _get_inline_config(): + from yap_kernel.pylab.config import InlineBackend + return InlineBackend.instance() + +@dec.skip_without('matplotlib') +def test_set_matplotlib_close(): + cfg = _get_inline_config() + cfg.close_figures = False + display.set_matplotlib_close() + assert cfg.close_figures + display.set_matplotlib_close(False) + assert not cfg.close_figures + +_fmt_mime_map = { + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'pdf': 'application/pdf', + 'retina': 'image/png', + 'svg': 'image/svg+xml', +} + +@dec.skip_without('matplotlib') +def test_set_matplotlib_formats(): + from matplotlib.figure import Figure + formatters = get_ipython().display_formatter.formatters + for formats in [ + ('png',), + ('pdf', 'svg'), + ('jpeg', 'retina', 'png'), + (), + ]: + active_mimes = {_fmt_mime_map[fmt] for fmt in formats} + display.set_matplotlib_formats(*formats) + for mime, f in formatters.items(): + if mime in active_mimes: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + +@dec.skip_without('matplotlib') +def test_set_matplotlib_formats_kwargs(): + from matplotlib.figure import Figure + ip = get_ipython() + cfg = _get_inline_config() + cfg.print_figure_kwargs.update(dict(foo='bar')) + kwargs = dict(quality=10) + display.set_matplotlib_formats('png', **kwargs) + formatter = ip.display_formatter.formatters['image/png'] + f = formatter.lookup_by_type(Figure) + cell = f.__closure__[0].cell_contents + expected = kwargs + expected.update(cfg.print_figure_kwargs) + nt.assert_equal(cell, expected) + +def test_display_available(): + """ + Test that display is available without import + + We don't really care if it's in builtin or anything else, but it should + always be available. + """ + ip = get_ipython() + with AssertNotPrints('NameError'): + ip.run_cell('display') + try: + ip.run_cell('del display') + except NameError: + pass # it's ok, it might be in builtins + # even if deleted it should be back + with AssertNotPrints('NameError'): + ip.run_cell('display') + +def test_textdisplayobj_pretty_repr(): + p = display.Pretty("This is a simple test") + nt.assert_equal(repr(p), '') + nt.assert_equal(p.data, 'This is a simple test') + + p._show_mem_addr = True + nt.assert_equal(repr(p), object.__repr__(p)) + +def test_displayobject_repr(): + h = display.HTML('
') + nt.assert_equal(repr(h), '') + h._show_mem_addr = True + nt.assert_equal(repr(h), object.__repr__(h)) + h._show_mem_addr = False + nt.assert_equal(repr(h), '') + + j = display.Javascript('') + nt.assert_equal(repr(j), '') + j._show_mem_addr = True + nt.assert_equal(repr(j), object.__repr__(j)) + j._show_mem_addr = False + nt.assert_equal(repr(j), '') + +def test_progress(): + p = display.ProgressBar(10) + nt.assert_in('0/10',repr(p)) + p.html_width = '100%' + p.progress = 5 + nt.assert_equal(p._repr_html_(), "") + +def test_progress_iter(): + with capture_output(display=False) as captured: + for i in display.ProgressBar(5): + out = captured.stdout + nt.assert_in('{0}/5'.format(i), out) + out = captured.stdout + nt.assert_in('5/5', out) + +def test_json(): + d = {'a': 5} + lis = [d] + md = {'expanded': False} + md2 = {'expanded': True} + j = display.JSON(d) + j2 = display.JSON(d, expanded=True) + nt.assert_equal(j._repr_json_(), (d, md)) + nt.assert_equal(j2._repr_json_(), (d, md2)) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + j = display.JSON(json.dumps(d)) + nt.assert_equal(len(w), 1) + nt.assert_equal(j._repr_json_(), (d, md)) + nt.assert_equal(j2._repr_json_(), (d, md2)) + + j = display.JSON(lis) + j2 = display.JSON(lis, expanded=True) + nt.assert_equal(j._repr_json_(), (lis, md)) + nt.assert_equal(j2._repr_json_(), (lis, md2)) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + j = display.JSON(json.dumps(lis)) + nt.assert_equal(len(w), 1) + nt.assert_equal(j._repr_json_(), (lis, md)) + nt.assert_equal(j2._repr_json_(), (lis, md2)) + +def test_video_embedding(): + """use a tempfile, with dummy-data, to ensure that video embedding doesn't crash""" + v = display.Video("http://ignored") + assert not v.embed + html = v._repr_html_() + nt.assert_not_in('src="data:', html) + nt.assert_in('src="http://ignored"', html) + + with nt.assert_raises(ValueError): + v = display.Video(b'abc') + + with NamedFileInTemporaryDirectory('test.mp4') as f: + f.write(b'abc') + f.close() + + v = display.Video(f.name) + assert not v.embed + html = v._repr_html_() + nt.assert_not_in('src="data:', html) + + v = display.Video(f.name, embed=True) + html = v._repr_html_() + nt.assert_in('src="data:video/mp4;base64,YWJj"',html) + + v = display.Video(f.name, embed=True, mimetype='video/other') + html = v._repr_html_() + nt.assert_in('src="data:video/other;base64,YWJj"',html) + + v = display.Video(b'abc', embed=True, mimetype='video/mp4') + html = v._repr_html_() + nt.assert_in('src="data:video/mp4;base64,YWJj"',html) + + v = display.Video(u'YWJj', embed=True, mimetype='video/xyz') + html = v._repr_html_() + nt.assert_in('src="data:video/xyz;base64,YWJj"',html) + + +def test_display_id(): + ip = get_ipython() + with mock.patch.object(ip.display_pub, 'publish') as pub: + handle = display.display('x') + nt.assert_is(handle, None) + handle = display.display('y', display_id='secret') + nt.assert_is_instance(handle, display.DisplayHandle) + handle2 = display.display('z', display_id=True) + nt.assert_is_instance(handle2, display.DisplayHandle) + nt.assert_not_equal(handle.display_id, handle2.display_id) + + nt.assert_equal(pub.call_count, 3) + args, kwargs = pub.call_args_list[0] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('x') + }, + 'metadata': {}, + }) + args, kwargs = pub.call_args_list[1] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('y') + }, + 'metadata': {}, + 'transient': { + 'display_id': handle.display_id, + }, + }) + args, kwargs = pub.call_args_list[2] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('z') + }, + 'metadata': {}, + 'transient': { + 'display_id': handle2.display_id, + }, + }) + + +def test_update_display(): + ip = get_ipython() + with mock.patch.object(ip.display_pub, 'publish') as pub: + with nt.assert_raises(TypeError): + display.update_display('x') + display.update_display('x', display_id='1') + display.update_display('y', display_id='2') + args, kwargs = pub.call_args_list[0] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('x') + }, + 'metadata': {}, + 'transient': { + 'display_id': '1', + }, + 'update': True, + }) + args, kwargs = pub.call_args_list[1] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('y') + }, + 'metadata': {}, + 'transient': { + 'display_id': '2', + }, + 'update': True, + }) + + +def test_display_handle(): + ip = get_ipython() + handle = display.DisplayHandle() + nt.assert_is_instance(handle.display_id, str) + handle = display.DisplayHandle('my-id') + nt.assert_equal(handle.display_id, 'my-id') + with mock.patch.object(ip.display_pub, 'publish') as pub: + handle.display('x') + handle.update('y') + + args, kwargs = pub.call_args_list[0] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('x') + }, + 'metadata': {}, + 'transient': { + 'display_id': handle.display_id, + } + }) + args, kwargs = pub.call_args_list[1] + nt.assert_equal(args, ()) + nt.assert_equal(kwargs, { + 'data': { + 'text/plain': repr('y') + }, + 'metadata': {}, + 'transient': { + 'display_id': handle.display_id, + }, + 'update': True, + }) + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_displayhook.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_displayhook.py new file mode 100644 index 000000000..30a2dbbc7 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_displayhook.py @@ -0,0 +1,103 @@ +from yap_ipython.testing.tools import AssertPrints, AssertNotPrints + +ip = get_ipython() + +def test_output_displayed(): + """Checking to make sure that output is displayed""" + + with AssertPrints('2'): + ip.run_cell('1+1', store_history=True) + + with AssertPrints('2'): + ip.run_cell('1+1 # comment with a semicolon;', store_history=True) + + with AssertPrints('2'): + ip.run_cell('1+1\n#commented_out_function();', store_history=True) + + +def test_output_quiet(): + """Checking to make sure that output is quiet""" + + with AssertNotPrints('2'): + ip.run_cell('1+1;', store_history=True) + + with AssertNotPrints('2'): + ip.run_cell('1+1; # comment with a semicolon', store_history=True) + + with AssertNotPrints('2'): + ip.run_cell('1+1;\n#commented_out_function()', store_history=True) + +def test_underscore_no_overrite_user(): + ip.run_cell('_ = 42', store_history=True) + ip.run_cell('1+1', store_history=True) + + with AssertPrints('42'): + ip.run_cell('print(_)', store_history=True) + + ip.run_cell('del _', store_history=True) + ip.run_cell('6+6', store_history=True) + with AssertPrints('12'): + ip.run_cell('_', store_history=True) + + +def test_underscore_no_overrite_builtins(): + ip.run_cell("import gettext ; gettext.install('foo')", store_history=True) + ip.run_cell('3+3', store_history=True) + + with AssertPrints('gettext'): + ip.run_cell('print(_)', store_history=True) + + ip.run_cell('_ = "userset"', store_history=True) + + with AssertPrints('userset'): + ip.run_cell('print(_)', store_history=True) + ip.run_cell('import builtins; del builtins._') + + +def test_interactivehooks_ast_modes(): + """ + Test that ast nodes can be triggered with different modes + """ + saved_mode = ip.ast_node_interactivity + ip.ast_node_interactivity = 'last_expr_or_assign' + + try: + with AssertPrints('2'): + ip.run_cell('a = 1+1', store_history=True) + + with AssertPrints('9'): + ip.run_cell('b = 1+8 # comment with a semicolon;', store_history=False) + + with AssertPrints('7'): + ip.run_cell('c = 1+6\n#commented_out_function();', store_history=True) + + ip.run_cell('d = 11', store_history=True) + with AssertPrints('12'): + ip.run_cell('d += 1', store_history=True) + + with AssertNotPrints('42'): + ip.run_cell('(u,v) = (41+1, 43-1)') + + finally: + ip.ast_node_interactivity = saved_mode + +def test_interactivehooks_ast_modes_semi_supress(): + """ + Test that ast nodes can be triggered with different modes and suppressed + by semicolon + """ + saved_mode = ip.ast_node_interactivity + ip.ast_node_interactivity = 'last_expr_or_assign' + + try: + with AssertNotPrints('2'): + ip.run_cell('x = 1+1;', store_history=True) + + with AssertNotPrints('7'): + ip.run_cell('y = 1+6; # comment with a semicolon', store_history=True) + + with AssertNotPrints('9'): + ip.run_cell('z = 1+8;\n#commented_out_function()', store_history=True) + + finally: + ip.ast_node_interactivity = saved_mode diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_events.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_events.py new file mode 100644 index 000000000..d95024cf9 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_events.py @@ -0,0 +1,87 @@ +from backcall import callback_prototype +import unittest +from unittest.mock import Mock +import nose.tools as nt + +from yap_ipython.core import events +import yap_ipython.testing.tools as tt + + +@events._define_event +def ping_received(): + pass + + +@events._define_event +def event_with_argument(argument): + pass + + +class CallbackTests(unittest.TestCase): + def setUp(self): + self.em = events.EventManager(get_ipython(), + {'ping_received': ping_received, + 'event_with_argument': event_with_argument}) + + def test_register_unregister(self): + cb = Mock() + + self.em.register('ping_received', cb) + self.em.trigger('ping_received') + self.assertEqual(cb.call_count, 1) + + self.em.unregister('ping_received', cb) + self.em.trigger('ping_received') + self.assertEqual(cb.call_count, 1) + + def test_bare_function_missed_unregister(self): + def cb1(): + ... + + def cb2(): + ... + + self.em.register('ping_received', cb1) + nt.assert_raises(ValueError, self.em.unregister, 'ping_received', cb2) + self.em.unregister('ping_received', cb1) + + def test_cb_error(self): + cb = Mock(side_effect=ValueError) + self.em.register('ping_received', cb) + with tt.AssertPrints("Error in callback"): + self.em.trigger('ping_received') + + def test_unregister_during_callback(self): + invoked = [False] * 3 + + def func1(*_): + invoked[0] = True + self.em.unregister('ping_received', func1) + self.em.register('ping_received', func3) + + def func2(*_): + invoked[1] = True + self.em.unregister('ping_received', func2) + + def func3(*_): + invoked[2] = True + + self.em.register('ping_received', func1) + self.em.register('ping_received', func2) + + self.em.trigger('ping_received') + self.assertEqual([True, True, False], invoked) + self.assertEqual([func3], self.em.callbacks['ping_received']) + + def test_ignore_event_arguments_if_no_argument_required(self): + call_count = [0] + def event_with_no_argument(): + call_count[0] += 1 + + self.em.register('event_with_argument', event_with_no_argument) + self.em.trigger('event_with_argument', 'the argument') + self.assertEqual(call_count[0], 1) + + self.em.unregister('event_with_argument', event_with_no_argument) + self.em.trigger('ping_received') + self.assertEqual(call_count[0], 1) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_extension.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_extension.py new file mode 100644 index 000000000..1fda92adb --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_extension.py @@ -0,0 +1,96 @@ +import os.path + +import nose.tools as nt + +import yap_ipython.testing.tools as tt +from yap_ipython.utils.syspathcontext import prepended_to_syspath +from yap_ipython.utils.tempdir import TemporaryDirectory + +ext1_content = """ +def load_ipython_extension(ip): + print("Running ext1 load") + +def unload_ipython_extension(ip): + print("Running ext1 unload") +""" + +ext2_content = """ +def load_ipython_extension(ip): + print("Running ext2 load") +""" + +ext3_content = """ +def load_ipython_extension(ip): + ip2 = get_ipython() + print(ip is ip2) +""" + +def test_extension_loading(): + em = get_ipython().extension_manager + with TemporaryDirectory() as td: + ext1 = os.path.join(td, 'ext1.py') + with open(ext1, 'w') as f: + f.write(ext1_content) + + ext2 = os.path.join(td, 'ext2.py') + with open(ext2, 'w') as f: + f.write(ext2_content) + + with prepended_to_syspath(td): + assert 'ext1' not in em.loaded + assert 'ext2' not in em.loaded + + # Load extension + with tt.AssertPrints("Running ext1 load"): + assert em.load_extension('ext1') is None + assert 'ext1' in em.loaded + + # Should refuse to load it again + with tt.AssertNotPrints("Running ext1 load"): + assert em.load_extension('ext1') == 'already loaded' + + # Reload + with tt.AssertPrints("Running ext1 unload"): + with tt.AssertPrints("Running ext1 load", suppress=False): + em.reload_extension('ext1') + + # Unload + with tt.AssertPrints("Running ext1 unload"): + assert em.unload_extension('ext1') is None + + # Can't unload again + with tt.AssertNotPrints("Running ext1 unload"): + assert em.unload_extension('ext1') == 'not loaded' + assert em.unload_extension('ext2') == 'not loaded' + + # Load extension 2 + with tt.AssertPrints("Running ext2 load"): + assert em.load_extension('ext2') is None + + # Can't unload this + assert em.unload_extension('ext2') == 'no unload function' + + # But can reload it + with tt.AssertPrints("Running ext2 load"): + em.reload_extension('ext2') + + +def test_extension_builtins(): + em = get_ipython().extension_manager + with TemporaryDirectory() as td: + ext3 = os.path.join(td, 'ext3.py') + with open(ext3, 'w') as f: + f.write(ext3_content) + + assert 'ext3' not in em.loaded + + with prepended_to_syspath(td): + # Load extension + with tt.AssertPrints("True"): + assert em.load_extension('ext3') is None + assert 'ext3' in em.loaded + + +def test_non_extension(): + em = get_ipython().extension_manager + nt.assert_equal(em.load_extension('sys'), "no load function") diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_formatters.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_formatters.py new file mode 100644 index 000000000..5f9fef0d5 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_formatters.py @@ -0,0 +1,533 @@ +"""Tests for the Formatters.""" + +import warnings +from math import pi + +try: + import numpy +except: + numpy = None +import nose.tools as nt + +from yap_ipython import get_ipython +from traitlets.config import Config +from yap_ipython.core.formatters import ( + PlainTextFormatter, HTMLFormatter, PDFFormatter, _mod_name_key, + DisplayFormatter, JSONFormatter, +) +from yap_ipython.utils.io import capture_output + +class A(object): + def __repr__(self): + return 'A()' + +class B(A): + def __repr__(self): + return 'B()' + +class C: + pass + +class BadRepr(object): + def __repr__(self): + raise ValueError("bad repr") + +class BadPretty(object): + _repr_pretty_ = None + +class GoodPretty(object): + def _repr_pretty_(self, pp, cycle): + pp.text('foo') + + def __repr__(self): + return 'GoodPretty()' + +def foo_printer(obj, pp, cycle): + pp.text('foo') + +def test_pretty(): + f = PlainTextFormatter() + f.for_type(A, foo_printer) + nt.assert_equal(f(A()), 'foo') + nt.assert_equal(f(B()), 'foo') + nt.assert_equal(f(GoodPretty()), 'foo') + # Just don't raise an exception for the following: + f(BadPretty()) + + f.pprint = False + nt.assert_equal(f(A()), 'A()') + nt.assert_equal(f(B()), 'B()') + nt.assert_equal(f(GoodPretty()), 'GoodPretty()') + + +def test_deferred(): + f = PlainTextFormatter() + +def test_precision(): + """test various values for float_precision.""" + f = PlainTextFormatter() + nt.assert_equal(f(pi), repr(pi)) + f.float_precision = 0 + if numpy: + po = numpy.get_printoptions() + nt.assert_equal(po['precision'], 0) + nt.assert_equal(f(pi), '3') + f.float_precision = 2 + if numpy: + po = numpy.get_printoptions() + nt.assert_equal(po['precision'], 2) + nt.assert_equal(f(pi), '3.14') + f.float_precision = '%g' + if numpy: + po = numpy.get_printoptions() + nt.assert_equal(po['precision'], 2) + nt.assert_equal(f(pi), '3.14159') + f.float_precision = '%e' + nt.assert_equal(f(pi), '3.141593e+00') + f.float_precision = '' + if numpy: + po = numpy.get_printoptions() + nt.assert_equal(po['precision'], 8) + nt.assert_equal(f(pi), repr(pi)) + +def test_bad_precision(): + """test various invalid values for float_precision.""" + f = PlainTextFormatter() + def set_fp(p): + f.float_precision=p + nt.assert_raises(ValueError, set_fp, '%') + nt.assert_raises(ValueError, set_fp, '%.3f%i') + nt.assert_raises(ValueError, set_fp, 'foo') + nt.assert_raises(ValueError, set_fp, -1) + +def test_for_type(): + f = PlainTextFormatter() + + # initial return, None + nt.assert_is(f.for_type(C, foo_printer), None) + # no func queries + nt.assert_is(f.for_type(C), foo_printer) + # shouldn't change anything + nt.assert_is(f.for_type(C), foo_printer) + # None should do the same + nt.assert_is(f.for_type(C, None), foo_printer) + nt.assert_is(f.for_type(C, None), foo_printer) + +def test_for_type_string(): + f = PlainTextFormatter() + + type_str = '%s.%s' % (C.__module__, 'C') + + # initial return, None + nt.assert_is(f.for_type(type_str, foo_printer), None) + # no func queries + nt.assert_is(f.for_type(type_str), foo_printer) + nt.assert_in(_mod_name_key(C), f.deferred_printers) + nt.assert_is(f.for_type(C), foo_printer) + nt.assert_not_in(_mod_name_key(C), f.deferred_printers) + nt.assert_in(C, f.type_printers) + +def test_for_type_by_name(): + f = PlainTextFormatter() + + mod = C.__module__ + + # initial return, None + nt.assert_is(f.for_type_by_name(mod, 'C', foo_printer), None) + # no func queries + nt.assert_is(f.for_type_by_name(mod, 'C'), foo_printer) + # shouldn't change anything + nt.assert_is(f.for_type_by_name(mod, 'C'), foo_printer) + # None should do the same + nt.assert_is(f.for_type_by_name(mod, 'C', None), foo_printer) + nt.assert_is(f.for_type_by_name(mod, 'C', None), foo_printer) + +def test_lookup(): + f = PlainTextFormatter() + + f.for_type(C, foo_printer) + nt.assert_is(f.lookup(C()), foo_printer) + with nt.assert_raises(KeyError): + f.lookup(A()) + +def test_lookup_string(): + f = PlainTextFormatter() + type_str = '%s.%s' % (C.__module__, 'C') + + f.for_type(type_str, foo_printer) + nt.assert_is(f.lookup(C()), foo_printer) + # should move from deferred to imported dict + nt.assert_not_in(_mod_name_key(C), f.deferred_printers) + nt.assert_in(C, f.type_printers) + +def test_lookup_by_type(): + f = PlainTextFormatter() + f.for_type(C, foo_printer) + nt.assert_is(f.lookup_by_type(C), foo_printer) + with nt.assert_raises(KeyError): + f.lookup_by_type(A) + +def test_lookup_by_type_string(): + f = PlainTextFormatter() + type_str = '%s.%s' % (C.__module__, 'C') + f.for_type(type_str, foo_printer) + + # verify insertion + nt.assert_in(_mod_name_key(C), f.deferred_printers) + nt.assert_not_in(C, f.type_printers) + + nt.assert_is(f.lookup_by_type(type_str), foo_printer) + # lookup by string doesn't cause import + nt.assert_in(_mod_name_key(C), f.deferred_printers) + nt.assert_not_in(C, f.type_printers) + + nt.assert_is(f.lookup_by_type(C), foo_printer) + # should move from deferred to imported dict + nt.assert_not_in(_mod_name_key(C), f.deferred_printers) + nt.assert_in(C, f.type_printers) + +def test_in_formatter(): + f = PlainTextFormatter() + f.for_type(C, foo_printer) + type_str = '%s.%s' % (C.__module__, 'C') + nt.assert_in(C, f) + nt.assert_in(type_str, f) + +def test_string_in_formatter(): + f = PlainTextFormatter() + type_str = '%s.%s' % (C.__module__, 'C') + f.for_type(type_str, foo_printer) + nt.assert_in(type_str, f) + nt.assert_in(C, f) + +def test_pop(): + f = PlainTextFormatter() + f.for_type(C, foo_printer) + nt.assert_is(f.lookup_by_type(C), foo_printer) + nt.assert_is(f.pop(C, None), foo_printer) + f.for_type(C, foo_printer) + nt.assert_is(f.pop(C), foo_printer) + with nt.assert_raises(KeyError): + f.lookup_by_type(C) + with nt.assert_raises(KeyError): + f.pop(C) + with nt.assert_raises(KeyError): + f.pop(A) + nt.assert_is(f.pop(A, None), None) + +def test_pop_string(): + f = PlainTextFormatter() + type_str = '%s.%s' % (C.__module__, 'C') + + with nt.assert_raises(KeyError): + f.pop(type_str) + + f.for_type(type_str, foo_printer) + f.pop(type_str) + with nt.assert_raises(KeyError): + f.lookup_by_type(C) + with nt.assert_raises(KeyError): + f.pop(type_str) + + f.for_type(C, foo_printer) + nt.assert_is(f.pop(type_str, None), foo_printer) + with nt.assert_raises(KeyError): + f.lookup_by_type(C) + with nt.assert_raises(KeyError): + f.pop(type_str) + nt.assert_is(f.pop(type_str, None), None) + + +def test_error_method(): + f = HTMLFormatter() + class BadHTML(object): + def _repr_html_(self): + raise ValueError("Bad HTML") + bad = BadHTML() + with capture_output() as captured: + result = f(bad) + nt.assert_is(result, None) + nt.assert_in("Traceback", captured.stdout) + nt.assert_in("Bad HTML", captured.stdout) + nt.assert_in("_repr_html_", captured.stdout) + +def test_nowarn_notimplemented(): + f = HTMLFormatter() + class HTMLNotImplemented(object): + def _repr_html_(self): + raise NotImplementedError + h = HTMLNotImplemented() + with capture_output() as captured: + result = f(h) + nt.assert_is(result, None) + nt.assert_equal("", captured.stderr) + nt.assert_equal("", captured.stdout) + +def test_warn_error_for_type(): + f = HTMLFormatter() + f.for_type(int, lambda i: name_error) + with capture_output() as captured: + result = f(5) + nt.assert_is(result, None) + nt.assert_in("Traceback", captured.stdout) + nt.assert_in("NameError", captured.stdout) + nt.assert_in("name_error", captured.stdout) + +def test_error_pretty_method(): + f = PlainTextFormatter() + class BadPretty(object): + def _repr_pretty_(self): + return "hello" + bad = BadPretty() + with capture_output() as captured: + result = f(bad) + nt.assert_is(result, None) + nt.assert_in("Traceback", captured.stdout) + nt.assert_in("_repr_pretty_", captured.stdout) + nt.assert_in("given", captured.stdout) + nt.assert_in("argument", captured.stdout) + + +def test_bad_repr_traceback(): + f = PlainTextFormatter() + bad = BadRepr() + with capture_output() as captured: + result = f(bad) + # catches error, returns None + nt.assert_is(result, None) + nt.assert_in("Traceback", captured.stdout) + nt.assert_in("__repr__", captured.stdout) + nt.assert_in("ValueError", captured.stdout) + + +class MakePDF(object): + def _repr_pdf_(self): + return 'PDF' + +def test_pdf_formatter(): + pdf = MakePDF() + f = PDFFormatter() + nt.assert_equal(f(pdf), 'PDF') + +def test_print_method_bound(): + f = HTMLFormatter() + class MyHTML(object): + def _repr_html_(self): + return "hello" + with capture_output() as captured: + result = f(MyHTML) + nt.assert_is(result, None) + nt.assert_not_in("FormatterWarning", captured.stderr) + + with capture_output() as captured: + result = f(MyHTML()) + nt.assert_equal(result, "hello") + nt.assert_equal(captured.stderr, "") + +def test_print_method_weird(): + + class TextMagicHat(object): + def __getattr__(self, key): + return key + + f = HTMLFormatter() + + text_hat = TextMagicHat() + nt.assert_equal(text_hat._repr_html_, '_repr_html_') + with capture_output() as captured: + result = f(text_hat) + + nt.assert_is(result, None) + nt.assert_not_in("FormatterWarning", captured.stderr) + + class CallableMagicHat(object): + def __getattr__(self, key): + return lambda : key + + call_hat = CallableMagicHat() + with capture_output() as captured: + result = f(call_hat) + + nt.assert_equal(result, None) + + class BadReprArgs(object): + def _repr_html_(self, extra, args): + return "html" + + bad = BadReprArgs() + with capture_output() as captured: + result = f(bad) + + nt.assert_is(result, None) + nt.assert_not_in("FormatterWarning", captured.stderr) + + +def test_format_config(): + """config objects don't pretend to support fancy reprs with lazy attrs""" + f = HTMLFormatter() + cfg = Config() + with capture_output() as captured: + result = f(cfg) + nt.assert_is(result, None) + nt.assert_equal(captured.stderr, "") + + with capture_output() as captured: + result = f(Config) + nt.assert_is(result, None) + nt.assert_equal(captured.stderr, "") + +def test_pretty_max_seq_length(): + f = PlainTextFormatter(max_seq_length=1) + lis = list(range(3)) + text = f(lis) + nt.assert_equal(text, '[0, ...]') + f.max_seq_length = 0 + text = f(lis) + nt.assert_equal(text, '[0, 1, 2]') + text = f(list(range(1024))) + lines = text.splitlines() + nt.assert_equal(len(lines), 1024) + + +def test_ipython_display_formatter(): + """Objects with _ipython_display_ defined bypass other formatters""" + f = get_ipython().display_formatter + catcher = [] + class SelfDisplaying(object): + def _ipython_display_(self): + catcher.append(self) + + class NotSelfDisplaying(object): + def __repr__(self): + return "NotSelfDisplaying" + + def _ipython_display_(self): + raise NotImplementedError + + save_enabled = f.ipython_display_formatter.enabled + f.ipython_display_formatter.enabled = True + + yes = SelfDisplaying() + no = NotSelfDisplaying() + + d, md = f.format(no) + nt.assert_equal(d, {'text/plain': repr(no)}) + nt.assert_equal(md, {}) + nt.assert_equal(catcher, []) + + d, md = f.format(yes) + nt.assert_equal(d, {}) + nt.assert_equal(md, {}) + nt.assert_equal(catcher, [yes]) + + f.ipython_display_formatter.enabled = save_enabled + + +def test_json_as_string_deprecated(): + class JSONString(object): + def _repr_json_(self): + return '{}' + + f = JSONFormatter() + with warnings.catch_warnings(record=True) as w: + d = f(JSONString()) + nt.assert_equal(d, {}) + nt.assert_equal(len(w), 1) + + +def test_repr_mime(): + class HasReprMime(object): + def _repr_mimebundle_(self, include=None, exclude=None): + return { + 'application/json+test.v2': { + 'x': 'y' + }, + 'plain/text' : '', + 'image/png' : 'i-overwrite' + } + + def _repr_png_(self): + return 'should-be-overwritten' + def _repr_html_(self): + return 'hi!' + + f = get_ipython().display_formatter + html_f = f.formatters['text/html'] + save_enabled = html_f.enabled + html_f.enabled = True + obj = HasReprMime() + d, md = f.format(obj) + html_f.enabled = save_enabled + + nt.assert_equal(sorted(d), ['application/json+test.v2', + 'image/png', + 'plain/text', + 'text/html', + 'text/plain']) + nt.assert_equal(md, {}) + + d, md = f.format(obj, include={'image/png'}) + nt.assert_equal(list(d.keys()), ['image/png'], + 'Include should filter out even things from repr_mimebundle') + nt.assert_equal(d['image/png'], 'i-overwrite', '_repr_mimebundle_ take precedence') + + + +def test_pass_correct_include_exclude(): + class Tester(object): + + def __init__(self, include=None, exclude=None): + self.include = include + self.exclude = exclude + + def _repr_mimebundle_(self, include, exclude, **kwargs): + if include and (include != self.include): + raise ValueError('include got modified: display() may be broken.') + if exclude and (exclude != self.exclude): + raise ValueError('exclude got modified: display() may be broken.') + + return None + + include = {'a', 'b', 'c'} + exclude = {'c', 'e' , 'f'} + + f = get_ipython().display_formatter + f.format(Tester(include=include, exclude=exclude), include=include, exclude=exclude) + f.format(Tester(exclude=exclude), exclude=exclude) + f.format(Tester(include=include), include=include) + + +def test_repr_mime_meta(): + class HasReprMimeMeta(object): + def _repr_mimebundle_(self, include=None, exclude=None): + data = { + 'image/png': 'base64-image-data', + } + metadata = { + 'image/png': { + 'width': 5, + 'height': 10, + } + } + return (data, metadata) + + f = get_ipython().display_formatter + obj = HasReprMimeMeta() + d, md = f.format(obj) + nt.assert_equal(sorted(d), ['image/png', 'text/plain']) + nt.assert_equal(md, { + 'image/png': { + 'width': 5, + 'height': 10, + } + }) + +def test_repr_mime_failure(): + class BadReprMime(object): + def _repr_mimebundle_(self, include=None, exclude=None): + raise RuntimeError + + f = get_ipython().display_formatter + obj = BadReprMime() + d, md = f.format(obj) + nt.assert_in('text/plain', d) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_handlers.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_handlers.py new file mode 100644 index 000000000..ba9ce2d51 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_handlers.py @@ -0,0 +1,97 @@ +"""Tests for input handlers. +""" +#----------------------------------------------------------------------------- +# Module imports +#----------------------------------------------------------------------------- + +# third party +import nose.tools as nt + +# our own packages +from yap_ipython.core import autocall +from yap_ipython.testing import tools as tt +from yap_ipython.testing.globalipapp import get_ipython +from yap_ipython.utils import py3compat + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +# Get the public instance of yap_ipython +ip = get_ipython() + +failures = [] +num_tests = 0 + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- + +class CallableIndexable(object): + def __getitem__(self, idx): return True + def __call__(self, *args, **kws): return True + + +class Autocallable(autocall.IPyAutocall): + def __call__(self): + return "called" + + +def run(tests): + """Loop through a list of (pre, post) inputs, where pre is the string + handed to ipython, and post is how that string looks after it's been + transformed (i.e. ipython's notion of _i)""" + tt.check_pairs(ip.prefilter_manager.prefilter_lines, tests) + + +def test_handlers(): + call_idx = CallableIndexable() + ip.user_ns['call_idx'] = call_idx + + # For many of the below, we're also checking that leading whitespace + # turns off the esc char, which it should unless there is a continuation + # line. + run([(i,py3compat.u_format(o)) for i,o in \ + [('"no change"', '"no change"'), # normal + (u"lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), # magic + #("a = b # PYTHON-MODE", '_i'), # emacs -- avoids _in cache + ]]) + + # Objects which are instances of IPyAutocall are *always* autocalled + autocallable = Autocallable() + ip.user_ns['autocallable'] = autocallable + + # auto + ip.magic('autocall 0') + # Only explicit escapes or instances of IPyAutocallable should get + # expanded + run([ + ('len "abc"', 'len "abc"'), + ('autocallable', 'autocallable()'), + # Don't add extra brackets (gh-1117) + ('autocallable()', 'autocallable()'), + ]) + ip.magic('autocall 1') + run([ + ('len "abc"', 'len("abc")'), + ('len "abc";', 'len("abc");'), # ; is special -- moves out of parens + # Autocall is turned off if first arg is [] and the object + # is both callable and indexable. Like so: + ('len [1,2]', 'len([1,2])'), # len doesn't support __getitem__... + ('call_idx [1]', 'call_idx [1]'), # call_idx *does*.. + ('call_idx 1', 'call_idx(1)'), + ('len', 'len'), # only at 2 does it auto-call on single args + ]) + ip.magic('autocall 2') + run([ + ('len "abc"', 'len("abc")'), + ('len "abc";', 'len("abc");'), + ('len [1,2]', 'len([1,2])'), + ('call_idx [1]', 'call_idx [1]'), + ('call_idx 1', 'call_idx(1)'), + # This is what's different: + ('len', 'len()'), # only at 2 does it auto-call on single args + ]) + ip.magic('autocall 1') + + nt.assert_equal(failures, []) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_history.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_history.py new file mode 100644 index 000000000..58f5b4358 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_history.py @@ -0,0 +1,210 @@ +# coding: utf-8 +"""Tests for the yap_ipython tab-completion machinery. +""" +#----------------------------------------------------------------------------- +# Module imports +#----------------------------------------------------------------------------- + +# stdlib +import io +import os +import sys +import tempfile +from datetime import datetime + +# third party +import nose.tools as nt + +# our own packages +from traitlets.config.loader import Config +from yap_ipython.utils.tempdir import TemporaryDirectory +from yap_ipython.core.history import HistoryManager, extract_hist_ranges + +def setUp(): + nt.assert_equal(sys.getdefaultencoding(), "utf-8") + +def test_history(): + ip = get_ipython() + with TemporaryDirectory() as tmpdir: + hist_manager_ori = ip.history_manager + hist_file = os.path.join(tmpdir, 'history.sqlite') + try: + ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file) + hist = [u'a=1', u'def f():\n test = 1\n return test', u"b='€Æ¾÷ß'"] + for i, h in enumerate(hist, start=1): + ip.history_manager.store_inputs(i, h) + + ip.history_manager.db_log_output = True + # Doesn't match the input, but we'll just check it's stored. + ip.history_manager.output_hist_reprs[3] = "spam" + ip.history_manager.store_output(3) + + nt.assert_equal(ip.history_manager.input_hist_raw, [''] + hist) + + # Detailed tests for _get_range_session + grs = ip.history_manager._get_range_session + nt.assert_equal(list(grs(start=2,stop=-1)), list(zip([0], [2], hist[1:-1]))) + nt.assert_equal(list(grs(start=-2)), list(zip([0,0], [2,3], hist[-2:]))) + nt.assert_equal(list(grs(output=True)), list(zip([0,0,0], [1,2,3], zip(hist, [None,None,'spam'])))) + + # Check whether specifying a range beyond the end of the current + # session results in an error (gh-804) + ip.magic('%hist 2-500') + + # Check that we can write non-ascii characters to a file + ip.magic("%%hist -f %s" % os.path.join(tmpdir, "test1")) + ip.magic("%%hist -pf %s" % os.path.join(tmpdir, "test2")) + ip.magic("%%hist -nf %s" % os.path.join(tmpdir, "test3")) + ip.magic("%%save %s 1-10" % os.path.join(tmpdir, "test4")) + + # New session + ip.history_manager.reset() + newcmds = [u"z=5", + u"class X(object):\n pass", + u"k='p'", + u"z=5"] + for i, cmd in enumerate(newcmds, start=1): + ip.history_manager.store_inputs(i, cmd) + gothist = ip.history_manager.get_range(start=1, stop=4) + nt.assert_equal(list(gothist), list(zip([0,0,0],[1,2,3], newcmds))) + # Previous session: + gothist = ip.history_manager.get_range(-1, 1, 4) + nt.assert_equal(list(gothist), list(zip([1,1,1],[1,2,3], hist))) + + newhist = [(2, i, c) for (i, c) in enumerate(newcmds, 1)] + + # Check get_hist_tail + gothist = ip.history_manager.get_tail(5, output=True, + include_latest=True) + expected = [(1, 3, (hist[-1], "spam"))] \ + + [(s, n, (c, None)) for (s, n, c) in newhist] + nt.assert_equal(list(gothist), expected) + + gothist = ip.history_manager.get_tail(2) + expected = newhist[-3:-1] + nt.assert_equal(list(gothist), expected) + + # Check get_hist_search + gothist = ip.history_manager.search("*test*") + nt.assert_equal(list(gothist), [(1,2,hist[1])] ) + + gothist = ip.history_manager.search("*=*") + nt.assert_equal(list(gothist), + [(1, 1, hist[0]), + (1, 2, hist[1]), + (1, 3, hist[2]), + newhist[0], + newhist[2], + newhist[3]]) + + gothist = ip.history_manager.search("*=*", n=4) + nt.assert_equal(list(gothist), + [(1, 3, hist[2]), + newhist[0], + newhist[2], + newhist[3]]) + + gothist = ip.history_manager.search("*=*", unique=True) + nt.assert_equal(list(gothist), + [(1, 1, hist[0]), + (1, 2, hist[1]), + (1, 3, hist[2]), + newhist[2], + newhist[3]]) + + gothist = ip.history_manager.search("*=*", unique=True, n=3) + nt.assert_equal(list(gothist), + [(1, 3, hist[2]), + newhist[2], + newhist[3]]) + + gothist = ip.history_manager.search("b*", output=True) + nt.assert_equal(list(gothist), [(1,3,(hist[2],"spam"))] ) + + # Cross testing: check that magic %save can get previous session. + testfilename = os.path.realpath(os.path.join(tmpdir, "test.py")) + ip.magic("save " + testfilename + " ~1/1-3") + with io.open(testfilename, encoding='utf-8') as testfile: + nt.assert_equal(testfile.read(), + u"# coding: utf-8\n" + u"\n".join(hist)+u"\n") + + # Duplicate line numbers - check that it doesn't crash, and + # gets a new session + ip.history_manager.store_inputs(1, "rogue") + ip.history_manager.writeout_cache() + nt.assert_equal(ip.history_manager.session_number, 3) + finally: + # Ensure saving thread is shut down before we try to clean up the files + ip.history_manager.save_thread.stop() + # Forcibly close database rather than relying on garbage collection + ip.history_manager.db.close() + # Restore history manager + ip.history_manager = hist_manager_ori + + +def test_extract_hist_ranges(): + instr = "1 2/3 ~4/5-6 ~4/7-~4/9 ~9/2-~7/5 ~10/" + expected = [(0, 1, 2), # 0 == current session + (2, 3, 4), + (-4, 5, 7), + (-4, 7, 10), + (-9, 2, None), # None == to end + (-8, 1, None), + (-7, 1, 6), + (-10, 1, None)] + actual = list(extract_hist_ranges(instr)) + nt.assert_equal(actual, expected) + +def test_magic_rerun(): + """Simple test for %rerun (no args -> rerun last line)""" + ip = get_ipython() + ip.run_cell("a = 10", store_history=True) + ip.run_cell("a += 1", store_history=True) + nt.assert_equal(ip.user_ns["a"], 11) + ip.run_cell("%rerun", store_history=True) + nt.assert_equal(ip.user_ns["a"], 12) + +def test_timestamp_type(): + ip = get_ipython() + info = ip.history_manager.get_session_info() + nt.assert_true(isinstance(info[1], datetime)) + +def test_hist_file_config(): + cfg = Config() + tfile = tempfile.NamedTemporaryFile(delete=False) + cfg.HistoryManager.hist_file = tfile.name + try: + hm = HistoryManager(shell=get_ipython(), config=cfg) + nt.assert_equal(hm.hist_file, cfg.HistoryManager.hist_file) + finally: + try: + os.remove(tfile.name) + except OSError: + # same catch as in testing.tools.TempFileMixin + # On Windows, even though we close the file, we still can't + # delete it. I have no clue why + pass + +def test_histmanager_disabled(): + """Ensure that disabling the history manager doesn't create a database.""" + cfg = Config() + cfg.HistoryAccessor.enabled = False + + ip = get_ipython() + with TemporaryDirectory() as tmpdir: + hist_manager_ori = ip.history_manager + hist_file = os.path.join(tmpdir, 'history.sqlite') + cfg.HistoryManager.hist_file = hist_file + try: + ip.history_manager = HistoryManager(shell=ip, config=cfg) + hist = [u'a=1', u'def f():\n test = 1\n return test', u"b='€Æ¾÷ß'"] + for i, h in enumerate(hist, start=1): + ip.history_manager.store_inputs(i, h) + nt.assert_equal(ip.history_manager.input_hist_raw, [''] + hist) + ip.history_manager.reset() + ip.history_manager.end_session() + finally: + ip.history_manager = hist_manager_ori + + # hist_file should not be created + nt.assert_false(os.path.exists(hist_file)) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_hooks.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_hooks.py new file mode 100644 index 000000000..afa8486fe --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_hooks.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +"""Tests for CommandChainDispatcher.""" + + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import nose.tools as nt +from yap_ipython.core.error import TryNext +from yap_ipython.core.hooks import CommandChainDispatcher + +#----------------------------------------------------------------------------- +# Local utilities +#----------------------------------------------------------------------------- + +# Define two classes, one which succeeds and one which raises TryNext. Each +# sets the attribute `called` to True when it is called. +class Okay(object): + def __init__(self, message): + self.message = message + self.called = False + def __call__(self): + self.called = True + return self.message + +class Fail(object): + def __init__(self, message): + self.message = message + self.called = False + def __call__(self): + self.called = True + raise TryNext(self.message) + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- + +def test_command_chain_dispatcher_ff(): + """Test two failing hooks""" + fail1 = Fail(u'fail1') + fail2 = Fail(u'fail2') + dp = CommandChainDispatcher([(0, fail1), + (10, fail2)]) + + try: + dp() + except TryNext as e: + nt.assert_equal(str(e), u'fail2') + else: + assert False, "Expected exception was not raised." + + nt.assert_true(fail1.called) + nt.assert_true(fail2.called) + +def test_command_chain_dispatcher_fofo(): + """Test a mixture of failing and succeeding hooks.""" + fail1 = Fail(u'fail1') + fail2 = Fail(u'fail2') + okay1 = Okay(u'okay1') + okay2 = Okay(u'okay2') + + dp = CommandChainDispatcher([(0, fail1), + # (5, okay1), # add this later + (10, fail2), + (15, okay2)]) + dp.add(okay1, 5) + + nt.assert_equal(dp(), u'okay1') + + nt.assert_true(fail1.called) + nt.assert_true(okay1.called) + nt.assert_false(fail2.called) + nt.assert_false(okay2.called) + +def test_command_chain_dispatcher_eq_priority(): + okay1 = Okay(u'okay1') + okay2 = Okay(u'okay2') + dp = CommandChainDispatcher([(1, okay1)]) + dp.add(okay2, 1) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_imports.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_imports.py new file mode 100644 index 000000000..b28823d9f --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_imports.py @@ -0,0 +1,52 @@ +# encoding: utf-8 + +def test_import_completer(): + from yap_ipython.core import completer + +def test_import_crashhandler(): + from yap_ipython.core import crashhandler + +def test_import_debugger(): + from yap_ipython.core import debugger + +def test_import_excolors(): + from yap_ipython.core import excolors + +def test_import_history(): + from yap_ipython.core import history + +def test_import_hooks(): + from yap_ipython.core import hooks + +def test_import_getipython(): + from yap_ipython.core import getipython + +def test_import_interactiveshell(): + from yap_ipython.core import interactiveshell + +def test_import_logger(): + from yap_ipython.core import logger + +def test_import_macro(): + from yap_ipython.core import macro + +def test_import_magic(): + from yap_ipython.core import magic + +def test_import_oinspect(): + from yap_ipython.core import oinspect + +def test_import_prefilter(): + from yap_ipython.core import prefilter + +def test_import_prompts(): + from yap_ipython.core import prompts + +def test_import_release(): + from yap_ipython.core import release + +def test_import_ultratb(): + from yap_ipython.core import ultratb + +def test_import_usage(): + from yap_ipython.core import usage diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_inputsplitter.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_inputsplitter.py new file mode 100644 index 000000000..9ed6627b0 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_inputsplitter.py @@ -0,0 +1,641 @@ +# -*- coding: utf-8 -*- +"""Tests for the inputsplitter module.""" + + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import unittest +import sys + +import nose.tools as nt + +from yap_ipython.core import inputsplitter as isp +from yap_ipython.core.inputtransformer import InputTransformer +from yap_ipython.core.tests.test_inputtransformer import syntax, syntax_ml +from yap_ipython.testing import tools as tt +from yap_ipython.utils import py3compat +from yap_ipython.utils.py3compat import input + +#----------------------------------------------------------------------------- +# Semi-complete examples (also used as tests) +#----------------------------------------------------------------------------- + +# Note: at the bottom, there's a slightly more complete version of this that +# can be useful during development of code here. + +def mini_interactive_loop(input_func): + """Minimal example of the logic of an interactive interpreter loop. + + This serves as an example, and it is used by the test system with a fake + raw_input that simulates interactive input.""" + + from yap_ipython.core.inputsplitter import InputSplitter + + isp = InputSplitter() + # In practice, this input loop would be wrapped in an outside loop to read + # input indefinitely, until some exit/quit command was issued. Here we + # only illustrate the basic inner loop. + while isp.push_accepts_more(): + indent = ' '*isp.get_indent_spaces() + prompt = '>>> ' + indent + line = indent + input_func(prompt) + isp.push(line) + + # Here we just return input so we can use it in a test suite, but a real + # interpreter would instead send it for execution somewhere. + src = isp.source_reset() + #print 'Input source was:\n', src # dbg + return src + +#----------------------------------------------------------------------------- +# Test utilities, just for local use +#----------------------------------------------------------------------------- + +def assemble(block): + """Assemble a block into multi-line sub-blocks.""" + return ['\n'.join(sub_block)+'\n' for sub_block in block] + + +def pseudo_input(lines): + """Return a function that acts like raw_input but feeds the input list.""" + ilines = iter(lines) + def raw_in(prompt): + try: + return next(ilines) + except StopIteration: + return '' + return raw_in + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- +def test_spaces(): + tests = [('', 0), + (' ', 1), + ('\n', 0), + (' \n', 1), + ('x', 0), + (' x', 1), + (' x',2), + (' x',4), + # Note: tabs are counted as a single whitespace! + ('\tx', 1), + ('\t x', 2), + ] + tt.check_pairs(isp.num_ini_spaces, tests) + + +def test_remove_comments(): + tests = [('text', 'text'), + ('text # comment', 'text '), + ('text # comment\n', 'text \n'), + ('text # comment \n', 'text \n'), + ('line # c \nline\n','line \nline\n'), + ('line # c \nline#c2 \nline\nline #c\n\n', + 'line \nline\nline\nline \n\n'), + ] + tt.check_pairs(isp.remove_comments, tests) + + +def test_get_input_encoding(): + encoding = isp.get_input_encoding() + nt.assert_true(isinstance(encoding, str)) + # simple-minded check that at least encoding a simple string works with the + # encoding we got. + nt.assert_equal(u'test'.encode(encoding), b'test') + + +class NoInputEncodingTestCase(unittest.TestCase): + def setUp(self): + self.old_stdin = sys.stdin + class X: pass + fake_stdin = X() + sys.stdin = fake_stdin + + def test(self): + # Verify that if sys.stdin has no 'encoding' attribute we do the right + # thing + enc = isp.get_input_encoding() + self.assertEqual(enc, 'ascii') + + def tearDown(self): + sys.stdin = self.old_stdin + + +class InputSplitterTestCase(unittest.TestCase): + def setUp(self): + self.isp = isp.InputSplitter() + + def test_reset(self): + isp = self.isp + isp.push('x=1') + isp.reset() + self.assertEqual(isp._buffer, []) + self.assertEqual(isp.get_indent_spaces(), 0) + self.assertEqual(isp.source, '') + self.assertEqual(isp.code, None) + self.assertEqual(isp._is_complete, False) + + def test_source(self): + self.isp._store('1') + self.isp._store('2') + self.assertEqual(self.isp.source, '1\n2\n') + self.assertEqual(len(self.isp._buffer)>0, True) + self.assertEqual(self.isp.source_reset(), '1\n2\n') + self.assertEqual(self.isp._buffer, []) + self.assertEqual(self.isp.source, '') + + def test_indent(self): + isp = self.isp # shorthand + isp.push('x=1') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n x=1') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('y=2\n') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_indent2(self): + isp = self.isp + isp.push('if 1:') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push(' x=1') + self.assertEqual(isp.get_indent_spaces(), 4) + # Blank lines shouldn't change the indent level + isp.push(' '*2) + self.assertEqual(isp.get_indent_spaces(), 4) + + def test_indent3(self): + isp = self.isp + # When a multiline statement contains parens or multiline strings, we + # shouldn't get confused. + isp.push("if 1:") + isp.push(" x = (1+\n 2)") + self.assertEqual(isp.get_indent_spaces(), 4) + + def test_indent4(self): + isp = self.isp + # whitespace after ':' should not screw up indent level + isp.push('if 1: \n x=1') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('y=2\n') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\t\n x=1') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('y=2\n') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_dedent_pass(self): + isp = self.isp # shorthand + # should NOT cause dedent + isp.push('if 1:\n passes = 5') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('if 1:\n pass') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n pass ') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_dedent_break(self): + isp = self.isp # shorthand + # should NOT cause dedent + isp.push('while 1:\n breaks = 5') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('while 1:\n break') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('while 1:\n break ') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_dedent_continue(self): + isp = self.isp # shorthand + # should NOT cause dedent + isp.push('while 1:\n continues = 5') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('while 1:\n continue') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('while 1:\n continue ') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_dedent_raise(self): + isp = self.isp # shorthand + # should NOT cause dedent + isp.push('if 1:\n raised = 4') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('if 1:\n raise TypeError()') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n raise') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n raise ') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_dedent_return(self): + isp = self.isp # shorthand + # should NOT cause dedent + isp.push('if 1:\n returning = 4') + self.assertEqual(isp.get_indent_spaces(), 4) + isp.push('if 1:\n return 5 + 493') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n return') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n return ') + self.assertEqual(isp.get_indent_spaces(), 0) + isp.push('if 1:\n return(0)') + self.assertEqual(isp.get_indent_spaces(), 0) + + def test_push(self): + isp = self.isp + self.assertEqual(isp.push('x=1'), True) + + def test_push2(self): + isp = self.isp + self.assertEqual(isp.push('if 1:'), False) + for line in [' x=1', '# a comment', ' y=2']: + print(line) + self.assertEqual(isp.push(line), True) + + def test_push3(self): + isp = self.isp + isp.push('if True:') + isp.push(' a = 1') + self.assertEqual(isp.push('b = [1,'), False) + + def test_push_accepts_more(self): + isp = self.isp + isp.push('x=1') + self.assertEqual(isp.push_accepts_more(), False) + + def test_push_accepts_more2(self): + isp = self.isp + isp.push('if 1:') + self.assertEqual(isp.push_accepts_more(), True) + isp.push(' x=1') + self.assertEqual(isp.push_accepts_more(), True) + isp.push('') + self.assertEqual(isp.push_accepts_more(), False) + + def test_push_accepts_more3(self): + isp = self.isp + isp.push("x = (2+\n3)") + self.assertEqual(isp.push_accepts_more(), False) + + def test_push_accepts_more4(self): + isp = self.isp + # When a multiline statement contains parens or multiline strings, we + # shouldn't get confused. + # FIXME: we should be able to better handle de-dents in statements like + # multiline strings and multiline expressions (continued with \ or + # parens). Right now we aren't handling the indentation tracking quite + # correctly with this, though in practice it may not be too much of a + # problem. We'll need to see. + isp.push("if 1:") + isp.push(" x = (2+") + isp.push(" 3)") + self.assertEqual(isp.push_accepts_more(), True) + isp.push(" y = 3") + self.assertEqual(isp.push_accepts_more(), True) + isp.push('') + self.assertEqual(isp.push_accepts_more(), False) + + def test_push_accepts_more5(self): + isp = self.isp + isp.push('try:') + isp.push(' a = 5') + isp.push('except:') + isp.push(' raise') + # We want to be able to add an else: block at this point, so it should + # wait for a blank line. + self.assertEqual(isp.push_accepts_more(), True) + + def test_continuation(self): + isp = self.isp + isp.push("import os, \\") + self.assertEqual(isp.push_accepts_more(), True) + isp.push("sys") + self.assertEqual(isp.push_accepts_more(), False) + + def test_syntax_error(self): + isp = self.isp + # Syntax errors immediately produce a 'ready' block, so the invalid + # Python can be sent to the kernel for evaluation with possible ipython + # special-syntax conversion. + isp.push('run foo') + self.assertEqual(isp.push_accepts_more(), False) + + def test_unicode(self): + self.isp.push(u"Pérez") + self.isp.push(u'\xc3\xa9') + self.isp.push(u"u'\xc3\xa9'") + + def test_line_continuation(self): + """ Test issue #2108.""" + isp = self.isp + # A blank line after a line continuation should not accept more + isp.push("1 \\\n\n") + self.assertEqual(isp.push_accepts_more(), False) + # Whitespace after a \ is a SyntaxError. The only way to test that + # here is to test that push doesn't accept more (as with + # test_syntax_error() above). + isp.push(r"1 \ ") + self.assertEqual(isp.push_accepts_more(), False) + # Even if the line is continuable (c.f. the regular Python + # interpreter) + isp.push(r"(1 \ ") + self.assertEqual(isp.push_accepts_more(), False) + + def test_check_complete(self): + isp = self.isp + self.assertEqual(isp.check_complete("a = 1"), ('complete', None)) + self.assertEqual(isp.check_complete("for a in range(5):"), ('incomplete', 4)) + self.assertEqual(isp.check_complete("raise = 2"), ('invalid', None)) + self.assertEqual(isp.check_complete("a = [1,\n2,"), ('incomplete', 0)) + self.assertEqual(isp.check_complete("def a():\n x=1\n global x"), ('invalid', None)) + +class InteractiveLoopTestCase(unittest.TestCase): + """Tests for an interactive loop like a python shell. + """ + def check_ns(self, lines, ns): + """Validate that the given input lines produce the resulting namespace. + + Note: the input lines are given exactly as they would be typed in an + auto-indenting environment, as mini_interactive_loop above already does + auto-indenting and prepends spaces to the input. + """ + src = mini_interactive_loop(pseudo_input(lines)) + test_ns = {} + exec(src, test_ns) + # We can't check that the provided ns is identical to the test_ns, + # because Python fills test_ns with extra keys (copyright, etc). But + # we can check that the given dict is *contained* in test_ns + for k,v in ns.items(): + self.assertEqual(test_ns[k], v) + + def test_simple(self): + self.check_ns(['x=1'], dict(x=1)) + + def test_simple2(self): + self.check_ns(['if 1:', 'x=2'], dict(x=2)) + + def test_xy(self): + self.check_ns(['x=1; y=2'], dict(x=1, y=2)) + + def test_abc(self): + self.check_ns(['if 1:','a=1','b=2','c=3'], dict(a=1, b=2, c=3)) + + def test_multi(self): + self.check_ns(['x =(1+','1+','2)'], dict(x=4)) + + +class IPythonInputTestCase(InputSplitterTestCase): + """By just creating a new class whose .isp is a different instance, we + re-run the same test battery on the new input splitter. + + In addition, this runs the tests over the syntax and syntax_ml dicts that + were tested by individual functions, as part of the OO interface. + + It also makes some checks on the raw buffer storage. + """ + + def setUp(self): + self.isp = isp.IPythonInputSplitter() + + def test_syntax(self): + """Call all single-line syntax tests from the main object""" + isp = self.isp + for example in syntax.values(): + for raw, out_t in example: + if raw.startswith(' '): + continue + + isp.push(raw+'\n') + out_raw = isp.source_raw + out = isp.source_reset() + self.assertEqual(out.rstrip(), out_t, + tt.pair_fail_msg.format("inputsplitter",raw, out_t, out)) + self.assertEqual(out_raw.rstrip(), raw.rstrip()) + + def test_syntax_multiline(self): + isp = self.isp + for example in syntax_ml.values(): + for line_pairs in example: + out_t_parts = [] + raw_parts = [] + for lraw, out_t_part in line_pairs: + if out_t_part is not None: + out_t_parts.append(out_t_part) + + if lraw is not None: + isp.push(lraw) + raw_parts.append(lraw) + + out_raw = isp.source_raw + out = isp.source_reset() + out_t = '\n'.join(out_t_parts).rstrip() + raw = '\n'.join(raw_parts).rstrip() + self.assertEqual(out.rstrip(), out_t) + self.assertEqual(out_raw.rstrip(), raw) + + def test_syntax_multiline_cell(self): + isp = self.isp + for example in syntax_ml.values(): + + out_t_parts = [] + for line_pairs in example: + raw = '\n'.join(r for r, _ in line_pairs if r is not None) + out_t = '\n'.join(t for _,t in line_pairs if t is not None) + out = isp.transform_cell(raw) + # Match ignoring trailing whitespace + self.assertEqual(out.rstrip(), out_t.rstrip()) + + def test_cellmagic_preempt(self): + isp = self.isp + for raw, name, line, cell in [ + ("%%cellm a\nIn[1]:", u'cellm', u'a', u'In[1]:'), + ("%%cellm \nline\n>>> hi", u'cellm', u'', u'line\n>>> hi'), + (">>> %%cellm \nline\n>>> hi", u'cellm', u'', u'line\nhi'), + ("%%cellm \n>>> hi", u'cellm', u'', u'>>> hi'), + ("%%cellm \nline1\nline2", u'cellm', u'', u'line1\nline2'), + ("%%cellm \nline1\\\\\nline2", u'cellm', u'', u'line1\\\\\nline2'), + ]: + expected = "get_ipython().run_cell_magic(%r, %r, %r)" % ( + name, line, cell + ) + out = isp.transform_cell(raw) + self.assertEqual(out.rstrip(), expected.rstrip()) + + def test_multiline_passthrough(self): + isp = self.isp + class CommentTransformer(InputTransformer): + def __init__(self): + self._lines = [] + + def push(self, line): + self._lines.append(line + '#') + + def reset(self): + text = '\n'.join(self._lines) + self._lines = [] + return text + + isp.physical_line_transforms.insert(0, CommentTransformer()) + + for raw, expected in [ + ("a=5", "a=5#"), + ("%ls foo", "get_ipython().run_line_magic(%r, %r)" % (u'ls', u'foo#')), + ("!ls foo\n%ls bar", "get_ipython().system(%r)\nget_ipython().run_line_magic(%r, %r)" % ( + u'ls foo#', u'ls', u'bar#' + )), + ("1\n2\n3\n%ls foo\n4\n5", "1#\n2#\n3#\nget_ipython().run_line_magic(%r, %r)\n4#\n5#" % (u'ls', u'foo#')), + ]: + out = isp.transform_cell(raw) + self.assertEqual(out.rstrip(), expected.rstrip()) + +#----------------------------------------------------------------------------- +# Main - use as a script, mostly for developer experiments +#----------------------------------------------------------------------------- + +if __name__ == '__main__': + # A simple demo for interactive experimentation. This code will not get + # picked up by any test suite. + from yap_ipython.core.inputsplitter import IPythonInputSplitter + + # configure here the syntax to use, prompt and whether to autoindent + #isp, start_prompt = InputSplitter(), '>>> ' + isp, start_prompt = IPythonInputSplitter(), 'In> ' + + autoindent = True + #autoindent = False + + try: + while True: + prompt = start_prompt + while isp.push_accepts_more(): + indent = ' '*isp.get_indent_spaces() + if autoindent: + line = indent + input(prompt+indent) + else: + line = input(prompt) + isp.push(line) + prompt = '... ' + + # Here we just return input so we can use it in a test suite, but a + # real interpreter would instead send it for execution somewhere. + #src = isp.source; raise EOFError # dbg + raw = isp.source_raw + src = isp.source_reset() + print('Input source was:\n', src) + print('Raw source was:\n', raw) + except EOFError: + print('Bye') + +# Tests for cell magics support + +def test_last_blank(): + nt.assert_false(isp.last_blank('')) + nt.assert_false(isp.last_blank('abc')) + nt.assert_false(isp.last_blank('abc\n')) + nt.assert_false(isp.last_blank('abc\na')) + + nt.assert_true(isp.last_blank('\n')) + nt.assert_true(isp.last_blank('\n ')) + nt.assert_true(isp.last_blank('abc\n ')) + nt.assert_true(isp.last_blank('abc\n\n')) + nt.assert_true(isp.last_blank('abc\nd\n\n')) + nt.assert_true(isp.last_blank('abc\nd\ne\n\n')) + nt.assert_true(isp.last_blank('abc \n \n \n\n')) + + +def test_last_two_blanks(): + nt.assert_false(isp.last_two_blanks('')) + nt.assert_false(isp.last_two_blanks('abc')) + nt.assert_false(isp.last_two_blanks('abc\n')) + nt.assert_false(isp.last_two_blanks('abc\n\na')) + nt.assert_false(isp.last_two_blanks('abc\n \n')) + nt.assert_false(isp.last_two_blanks('abc\n\n')) + + nt.assert_true(isp.last_two_blanks('\n\n')) + nt.assert_true(isp.last_two_blanks('\n\n ')) + nt.assert_true(isp.last_two_blanks('\n \n')) + nt.assert_true(isp.last_two_blanks('abc\n\n ')) + nt.assert_true(isp.last_two_blanks('abc\n\n\n')) + nt.assert_true(isp.last_two_blanks('abc\n\n \n')) + nt.assert_true(isp.last_two_blanks('abc\n\n \n ')) + nt.assert_true(isp.last_two_blanks('abc\n\n \n \n')) + nt.assert_true(isp.last_two_blanks('abc\nd\n\n\n')) + nt.assert_true(isp.last_two_blanks('abc\nd\ne\nf\n\n\n')) + + +class CellMagicsCommon(object): + + def test_whole_cell(self): + src = "%%cellm line\nbody\n" + out = self.sp.transform_cell(src) + ref = u"get_ipython().run_cell_magic('cellm', 'line', 'body')\n" + nt.assert_equal(out, py3compat.u_format(ref)) + + def test_cellmagic_help(self): + self.sp.push('%%cellm?') + nt.assert_false(self.sp.push_accepts_more()) + + def tearDown(self): + self.sp.reset() + + +class CellModeCellMagics(CellMagicsCommon, unittest.TestCase): + sp = isp.IPythonInputSplitter(line_input_checker=False) + + def test_incremental(self): + sp = self.sp + sp.push('%%cellm firstline\n') + nt.assert_true(sp.push_accepts_more()) #1 + sp.push('line2\n') + nt.assert_true(sp.push_accepts_more()) #2 + sp.push('\n') + # This should accept a blank line and carry on until the cell is reset + nt.assert_true(sp.push_accepts_more()) #3 + + def test_no_strip_coding(self): + src = '\n'.join([ + '%%writefile foo.py', + '# coding: utf-8', + 'print(u"üñîçø∂é")', + ]) + out = self.sp.transform_cell(src) + nt.assert_in('# coding: utf-8', out) + + +class LineModeCellMagics(CellMagicsCommon, unittest.TestCase): + sp = isp.IPythonInputSplitter(line_input_checker=True) + + def test_incremental(self): + sp = self.sp + sp.push('%%cellm line2\n') + nt.assert_true(sp.push_accepts_more()) #1 + sp.push('\n') + # In this case, a blank line should end the cell magic + nt.assert_false(sp.push_accepts_more()) #2 + +indentation_samples = [ + ('a = 1', 0), + ('for a in b:', 4), + ('def f():', 4), + ('def f(): #comment', 4), + ('a = ":#not a comment"', 0), + ('def f():\n a = 1', 4), + ('def f():\n return 1', 0), + ('for a in b:\n' + ' if a < 0:' + ' continue', 3), + ('a = {', 4), + ('a = {\n' + ' 1,', 5), + ('b = """123', 0), + ('', 0), + ('def f():\n pass', 0), + ('class Bar:\n def f():\n pass', 4), + ('class Bar:\n def f():\n raise', 4), +] + +def test_find_next_indent(): + for code, exp in indentation_samples: + res = isp.find_next_indent(code) + msg = "{!r} != {!r} (expected)\n Code: {!r}".format(res, exp, code) + assert res == exp, msg diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_inputtransformer.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_inputtransformer.py new file mode 100644 index 000000000..8e1837844 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_inputtransformer.py @@ -0,0 +1,494 @@ +import tokenize +import nose.tools as nt + +from yap_ipython.testing import tools as tt +from yap_ipython.utils import py3compat +u_fmt = py3compat.u_format + +from yap_ipython.core import inputtransformer as ipt + +def transform_and_reset(transformer): + transformer = transformer() + def transform(inp): + try: + return transformer.push(inp) + finally: + transformer.reset() + + return transform + +# Transformer tests +def transform_checker(tests, transformer, **kwargs): + """Utility to loop over test inputs""" + transformer = transformer(**kwargs) + try: + for inp, tr in tests: + if inp is None: + out = transformer.reset() + else: + out = transformer.push(inp) + nt.assert_equal(out, tr) + finally: + transformer.reset() + +# Data for all the syntax tests in the form of lists of pairs of +# raw/transformed input. We store it here as a global dict so that we can use +# it both within single-function tests and also to validate the behavior of the +# larger objects + +syntax = \ + dict(assign_system = + [(i,py3compat.u_format(o)) for i,o in \ + [(u'a =! ls', "a = get_ipython().getoutput('ls')"), + (u'b = !ls', "b = get_ipython().getoutput('ls')"), + (u'c= !ls', "c = get_ipython().getoutput('ls')"), + (u'd == !ls', u'd == !ls'), # Invalid syntax, but we leave == alone. + ('x=1', 'x=1'), # normal input is unmodified + (' ',' '), # blank lines are kept intact + # Tuple unpacking + (u"a, b = !echo 'a\\nb'", u"a, b = get_ipython().getoutput(\"echo 'a\\\\nb'\")"), + (u"a,= !echo 'a'", u"a, = get_ipython().getoutput(\"echo 'a'\")"), + (u"a, *bc = !echo 'a\\nb\\nc'", u"a, *bc = get_ipython().getoutput(\"echo 'a\\\\nb\\\\nc'\")"), + # Tuple unpacking with regular Python expressions, not our syntax. + (u"a, b = range(2)", u"a, b = range(2)"), + (u"a, = range(1)", u"a, = range(1)"), + (u"a, *bc = range(3)", u"a, *bc = range(3)"), + ]], + + assign_magic = + [(i,py3compat.u_format(o)) for i,o in \ + [(u'a =% who', "a = get_ipython().run_line_magic('who', '')"), + (u'b = %who', "b = get_ipython().run_line_magic('who', '')"), + (u'c= %ls', "c = get_ipython().run_line_magic('ls', '')"), + (u'd == %ls', u'd == %ls'), # Invalid syntax, but we leave == alone. + ('x=1', 'x=1'), # normal input is unmodified + (' ',' '), # blank lines are kept intact + (u"a, b = %foo", u"a, b = get_ipython().run_line_magic('foo', '')"), + ]], + + classic_prompt = + [('>>> x=1', 'x=1'), + ('x=1', 'x=1'), # normal input is unmodified + (' ', ' '), # blank lines are kept intact + ], + + ipy_prompt = + [('In [1]: x=1', 'x=1'), + ('x=1', 'x=1'), # normal input is unmodified + (' ',' '), # blank lines are kept intact + ], + + # Tests for the escape transformer to leave normal code alone + escaped_noesc = + [ (' ', ' '), + ('x=1', 'x=1'), + ], + + # System calls + escaped_shell = + [(i,py3compat.u_format(o)) for i,o in \ + [ (u'!ls', "get_ipython().system('ls')"), + # Double-escape shell, this means to capture the output of the + # subprocess and return it + (u'!!ls', "get_ipython().getoutput('ls')"), + ]], + + # Help/object info + escaped_help = + [(i,py3compat.u_format(o)) for i,o in \ + [ (u'?', 'get_ipython().show_usage()'), + (u'?x1', "get_ipython().run_line_magic('pinfo', 'x1')"), + (u'??x2', "get_ipython().run_line_magic('pinfo2', 'x2')"), + (u'?a.*s', "get_ipython().run_line_magic('psearch', 'a.*s')"), + (u'?%hist1', "get_ipython().run_line_magic('pinfo', '%hist1')"), + (u'?%%hist2', "get_ipython().run_line_magic('pinfo', '%%hist2')"), + (u'?abc = qwe', "get_ipython().run_line_magic('pinfo', 'abc')"), + ]], + + end_help = + [(i,py3compat.u_format(o)) for i,o in \ + [ (u'x3?', "get_ipython().run_line_magic('pinfo', 'x3')"), + (u'x4??', "get_ipython().run_line_magic('pinfo2', 'x4')"), + (u'%hist1?', "get_ipython().run_line_magic('pinfo', '%hist1')"), + (u'%hist2??', "get_ipython().run_line_magic('pinfo2', '%hist2')"), + (u'%%hist3?', "get_ipython().run_line_magic('pinfo', '%%hist3')"), + (u'%%hist4??', "get_ipython().run_line_magic('pinfo2', '%%hist4')"), + (u'f*?', "get_ipython().run_line_magic('psearch', 'f*')"), + (u'ax.*aspe*?', "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"), + (u'a = abc?', "get_ipython().set_next_input('a = abc');" + "get_ipython().run_line_magic('pinfo', 'abc')"), + (u'a = abc.qe??', "get_ipython().set_next_input('a = abc.qe');" + "get_ipython().run_line_magic('pinfo2', 'abc.qe')"), + (u'a = *.items?', "get_ipython().set_next_input('a = *.items');" + "get_ipython().run_line_magic('psearch', '*.items')"), + (u'plot(a?', "get_ipython().set_next_input('plot(a');" + "get_ipython().run_line_magic('pinfo', 'a')"), + (u'a*2 #comment?', 'a*2 #comment?'), + ]], + + # Explicit magic calls + escaped_magic = + [(i,py3compat.u_format(o)) for i,o in \ + [ (u'%cd', "get_ipython().run_line_magic('cd', '')"), + (u'%cd /home', "get_ipython().run_line_magic('cd', '/home')"), + # Backslashes need to be escaped. + (u'%cd C:\\User', "get_ipython().run_line_magic('cd', 'C:\\\\User')"), + (u' %magic', " get_ipython().run_line_magic('magic', '')"), + ]], + + # Quoting with separate arguments + escaped_quote = + [ (',f', 'f("")'), + (',f x', 'f("x")'), + (' ,f y', ' f("y")'), + (',f a b', 'f("a", "b")'), + ], + + # Quoting with single argument + escaped_quote2 = + [ (';f', 'f("")'), + (';f x', 'f("x")'), + (' ;f y', ' f("y")'), + (';f a b', 'f("a b")'), + ], + + # Simply apply parens + escaped_paren = + [ ('/f', 'f()'), + ('/f x', 'f(x)'), + (' /f y', ' f(y)'), + ('/f a b', 'f(a, b)'), + ], + + # Check that we transform prompts before other transforms + mixed = + [(i,py3compat.u_format(o)) for i,o in \ + [ (u'In [1]: %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"), + (u'>>> %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"), + (u'In [2]: !ls', "get_ipython().system('ls')"), + (u'In [3]: abs?', "get_ipython().run_line_magic('pinfo', 'abs')"), + (u'In [4]: b = %who', "b = get_ipython().run_line_magic('who', '')"), + ]], + ) + +# multiline syntax examples. Each of these should be a list of lists, with +# each entry itself having pairs of raw/transformed input. The union (with +# '\n'.join() of the transformed inputs is what the splitter should produce +# when fed the raw lines one at a time via push. +syntax_ml = \ + dict(classic_prompt = + [ [('>>> for i in range(10):','for i in range(10):'), + ('... print i',' print i'), + ('... ', ''), + ], + [('>>> a="""','a="""'), + ('... 123"""','123"""'), + ], + [('a="""','a="""'), + ('... 123','123'), + ('... 456"""','456"""'), + ], + [('a="""','a="""'), + ('>>> 123','123'), + ('... 456"""','456"""'), + ], + [('a="""','a="""'), + ('123','123'), + ('... 456"""','... 456"""'), + ], + [('....__class__','....__class__'), + ], + [('a=5', 'a=5'), + ('...', ''), + ], + [('>>> def f(x):', 'def f(x):'), + ('...', ''), + ('... return x', ' return x'), + ], + [('board = """....', 'board = """....'), + ('....', '....'), + ('...."""', '...."""'), + ], + ], + + ipy_prompt = + [ [('In [24]: for i in range(10):','for i in range(10):'), + (' ....: print i',' print i'), + (' ....: ', ''), + ], + [('In [24]: for i in range(10):','for i in range(10):'), + # Qt console prompts expand with spaces, not dots + (' ...: print i',' print i'), + (' ...: ', ''), + ], + [('In [24]: for i in range(10):','for i in range(10):'), + # Sometimes whitespace preceding '...' has been removed + ('...: print i',' print i'), + ('...: ', ''), + ], + [('In [24]: for i in range(10):','for i in range(10):'), + # Space after last continuation prompt has been removed (issue #6674) + ('...: print i',' print i'), + ('...:', ''), + ], + [('In [2]: a="""','a="""'), + (' ...: 123"""','123"""'), + ], + [('a="""','a="""'), + (' ...: 123','123'), + (' ...: 456"""','456"""'), + ], + [('a="""','a="""'), + ('In [1]: 123','123'), + (' ...: 456"""','456"""'), + ], + [('a="""','a="""'), + ('123','123'), + (' ...: 456"""',' ...: 456"""'), + ], + ], + + multiline_datastructure_prompt = + [ [('>>> a = [1,','a = [1,'), + ('... 2]','2]'), + ], + ], + + multiline_datastructure = + [ [('b = ("%s"', None), + ('# comment', None), + ('%foo )', 'b = ("%s"\n# comment\n%foo )'), + ], + ], + + multiline_string = + [ [("'''foo?", None), + ("bar'''", "'''foo?\nbar'''"), + ], + ], + + leading_indent = + [ [(' print "hi"','print "hi"'), + ], + [(' for a in range(5):','for a in range(5):'), + (' a*2',' a*2'), + ], + [(' a="""','a="""'), + (' 123"""','123"""'), + ], + [('a="""','a="""'), + (' 123"""',' 123"""'), + ], + ], + + cellmagic = + [ [(u'%%foo a', None), + (None, u_fmt("get_ipython().run_cell_magic('foo', 'a', '')")), + ], + [(u'%%bar 123', None), + (u'hello', None), + (None , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")), + ], + [(u'a=5', 'a=5'), + (u'%%cellmagic', '%%cellmagic'), + ], + ], + + escaped = + [ [('%abc def \\', None), + ('ghi', u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")), + ], + [('%abc def \\', None), + ('ghi\\', None), + (None, u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")), + ], + ], + + assign_magic = + [ [(u'a = %bc de \\', None), + (u'fg', u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")), + ], + [(u'a = %bc de \\', None), + (u'fg\\', None), + (None, u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")), + ], + ], + + assign_system = + [ [(u'a = !bc de \\', None), + (u'fg', u_fmt("a = get_ipython().getoutput('bc de fg')")), + ], + [(u'a = !bc de \\', None), + (u'fg\\', None), + (None, u_fmt("a = get_ipython().getoutput('bc de fg')")), + ], + ], + ) + + +def test_assign_system(): + tt.check_pairs(transform_and_reset(ipt.assign_from_system), syntax['assign_system']) + +def test_assign_magic(): + tt.check_pairs(transform_and_reset(ipt.assign_from_magic), syntax['assign_magic']) + +def test_classic_prompt(): + tt.check_pairs(transform_and_reset(ipt.classic_prompt), syntax['classic_prompt']) + for example in syntax_ml['classic_prompt']: + transform_checker(example, ipt.classic_prompt) + for example in syntax_ml['multiline_datastructure_prompt']: + transform_checker(example, ipt.classic_prompt) + + # Check that we don't transform the second line if the first is obviously + # yap_ipython syntax + transform_checker([ + (u'%foo', '%foo'), + (u'>>> bar', '>>> bar'), + ], ipt.classic_prompt) + + +def test_ipy_prompt(): + tt.check_pairs(transform_and_reset(ipt.ipy_prompt), syntax['ipy_prompt']) + for example in syntax_ml['ipy_prompt']: + transform_checker(example, ipt.ipy_prompt) + + # Check that we don't transform the second line if we're inside a cell magic + transform_checker([ + (u'%%foo', '%%foo'), + (u'In [1]: bar', 'In [1]: bar'), + ], ipt.ipy_prompt) + +def test_assemble_logical_lines(): + tests = \ + [ [(u"a = \\", None), + (u"123", u"a = 123"), + ], + [(u"a = \\", None), # Test resetting when within a multi-line string + (u"12 *\\", None), + (None, u"a = 12 *"), + ], + [(u"# foo\\", u"# foo\\"), # Comments can't be continued like this + ], + ] + for example in tests: + transform_checker(example, ipt.assemble_logical_lines) + +def test_assemble_python_lines(): + tests = \ + [ [(u"a = '''", None), + (u"abc'''", u"a = '''\nabc'''"), + ], + [(u"a = '''", None), # Test resetting when within a multi-line string + (u"def", None), + (None, u"a = '''\ndef"), + ], + [(u"a = [1,", None), + (u"2]", u"a = [1,\n2]"), + ], + [(u"a = [1,", None), # Test resetting when within a multi-line string + (u"2,", None), + (None, u"a = [1,\n2,"), + ], + [(u"a = '''", None), # Test line continuation within a multi-line string + (u"abc\\", None), + (u"def", None), + (u"'''", u"a = '''\nabc\\\ndef\n'''"), + ], + ] + syntax_ml['multiline_datastructure'] + for example in tests: + transform_checker(example, ipt.assemble_python_lines) + + +def test_help_end(): + tt.check_pairs(transform_and_reset(ipt.help_end), syntax['end_help']) + +def test_escaped_noesc(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_noesc']) + + +def test_escaped_shell(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_shell']) + + +def test_escaped_help(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_help']) + + +def test_escaped_magic(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_magic']) + + +def test_escaped_quote(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote']) + + +def test_escaped_quote2(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote2']) + + +def test_escaped_paren(): + tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_paren']) + + +def test_cellmagic(): + for example in syntax_ml['cellmagic']: + transform_checker(example, ipt.cellmagic) + + line_example = [(u'%%bar 123', None), + (u'hello', None), + (u'' , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")), + ] + transform_checker(line_example, ipt.cellmagic, end_on_blank_line=True) + +def test_has_comment(): + tests = [('text', False), + ('text #comment', True), + ('text #comment\n', True), + ('#comment', True), + ('#comment\n', True), + ('a = "#string"', False), + ('a = "#string" # comment', True), + ('a #comment not "string"', True), + ] + tt.check_pairs(ipt.has_comment, tests) + +@ipt.TokenInputTransformer.wrap +def decistmt(tokens): + """Substitute Decimals for floats in a string of statements. + + Based on an example from the tokenize module docs. + """ + result = [] + for toknum, tokval, _, _, _ in tokens: + if toknum == tokenize.NUMBER and '.' in tokval: # replace NUMBER tokens + for newtok in [ + (tokenize.NAME, 'Decimal'), + (tokenize.OP, '('), + (tokenize.STRING, repr(tokval)), + (tokenize.OP, ')') + ]: + yield newtok + else: + yield (toknum, tokval) + + + +def test_token_input_transformer(): + tests = [(u'1.2', u_fmt(u"Decimal ('1.2')")), + (u'"1.2"', u'"1.2"'), + ] + tt.check_pairs(transform_and_reset(decistmt), tests) + ml_tests = \ + [ [(u"a = 1.2; b = '''x", None), + (u"y'''", u_fmt(u"a =Decimal ('1.2');b ='''x\ny'''")), + ], + [(u"a = [1.2,", None), + (u"3]", u_fmt(u"a =[Decimal ('1.2'),\n3 ]")), + ], + [(u"a = '''foo", None), # Test resetting when within a multi-line string + (u"bar", None), + (None, u"a = '''foo\nbar"), + ], + ] + for example in ml_tests: + transform_checker(example, decistmt) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_interactiveshell.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_interactiveshell.py new file mode 100644 index 000000000..80dec38f7 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_interactiveshell.py @@ -0,0 +1,924 @@ +# -*- coding: utf-8 -*- +"""Tests for the key interactiveshell module. + +Historically the main classes in interactiveshell have been under-tested. This +module should grow as many single-method tests as possible to trap many of the +recurring bugs we seem to encounter with high-level interaction. +""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import ast +import os +import signal +import shutil +import sys +import tempfile +import unittest +from unittest import mock + +from os.path import join + +import nose.tools as nt + +from yap_ipython.core.error import InputRejected +from yap_ipython.core.inputtransformer import InputTransformer +from yap_ipython.testing.decorators import ( + skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist, +) +from yap_ipython.testing import tools as tt +from yap_ipython.utils.process import find_cmd + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- +# This is used by every single test, no point repeating it ad nauseam +ip = get_ipython() + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + +class DerivedInterrupt(KeyboardInterrupt): + pass + +class InteractiveShellTestCase(unittest.TestCase): + def test_naked_string_cells(self): + """Test that cells with only naked strings are fully executed""" + # First, single-line inputs + ip.run_cell('"a"\n') + self.assertEqual(ip.user_ns['_'], 'a') + # And also multi-line cells + ip.run_cell('"""a\nb"""\n') + self.assertEqual(ip.user_ns['_'], 'a\nb') + + def test_run_empty_cell(self): + """Just make sure we don't get a horrible error with a blank + cell of input. Yes, I did overlook that.""" + old_xc = ip.execution_count + res = ip.run_cell('') + self.assertEqual(ip.execution_count, old_xc) + self.assertEqual(res.execution_count, None) + + def test_run_cell_multiline(self): + """Multi-block, multi-line cells must execute correctly. + """ + src = '\n'.join(["x=1", + "y=2", + "if 1:", + " x += 1", + " y += 1",]) + res = ip.run_cell(src) + self.assertEqual(ip.user_ns['x'], 2) + self.assertEqual(ip.user_ns['y'], 3) + self.assertEqual(res.success, True) + self.assertEqual(res.result, None) + + def test_multiline_string_cells(self): + "Code sprinkled with multiline strings should execute (GH-306)" + ip.run_cell('tmp=0') + self.assertEqual(ip.user_ns['tmp'], 0) + res = ip.run_cell('tmp=1;"""a\nb"""\n') + self.assertEqual(ip.user_ns['tmp'], 1) + self.assertEqual(res.success, True) + self.assertEqual(res.result, "a\nb") + + def test_dont_cache_with_semicolon(self): + "Ending a line with semicolon should not cache the returned object (GH-307)" + oldlen = len(ip.user_ns['Out']) + for cell in ['1;', '1;1;']: + res = ip.run_cell(cell, store_history=True) + newlen = len(ip.user_ns['Out']) + self.assertEqual(oldlen, newlen) + self.assertIsNone(res.result) + i = 0 + #also test the default caching behavior + for cell in ['1', '1;1']: + ip.run_cell(cell, store_history=True) + newlen = len(ip.user_ns['Out']) + i += 1 + self.assertEqual(oldlen+i, newlen) + + def test_syntax_error(self): + res = ip.run_cell("raise = 3") + self.assertIsInstance(res.error_before_exec, SyntaxError) + + def test_In_variable(self): + "Verify that In variable grows with user input (GH-284)" + oldlen = len(ip.user_ns['In']) + ip.run_cell('1;', store_history=True) + newlen = len(ip.user_ns['In']) + self.assertEqual(oldlen+1, newlen) + self.assertEqual(ip.user_ns['In'][-1],'1;') + + def test_magic_names_in_string(self): + ip.run_cell('a = """\n%exit\n"""') + self.assertEqual(ip.user_ns['a'], '\n%exit\n') + + def test_trailing_newline(self): + """test that running !(command) does not raise a SyntaxError""" + ip.run_cell('!(true)\n', False) + ip.run_cell('!(true)\n\n\n', False) + + def test_gh_597(self): + """Pretty-printing lists of objects with non-ascii reprs may cause + problems.""" + class Spam(object): + def __repr__(self): + return "\xe9"*50 + import yap_ipython.core.formatters + f = yap_ipython.core.formatters.PlainTextFormatter() + f([Spam(),Spam()]) + + + def test_future_flags(self): + """Check that future flags are used for parsing code (gh-777)""" + ip.run_cell('from __future__ import barry_as_FLUFL') + try: + ip.run_cell('prfunc_return_val = 1 <> 2') + assert 'prfunc_return_val' in ip.user_ns + finally: + # Reset compiler flags so we don't mess up other tests. + ip.compile.reset_compiler_flags() + + def test_can_pickle(self): + "Can we pickle objects defined interactively (GH-29)" + ip = get_ipython() + ip.reset() + ip.run_cell(("class Mylist(list):\n" + " def __init__(self,x=[]):\n" + " list.__init__(self,x)")) + ip.run_cell("w=Mylist([1,2,3])") + + from pickle import dumps + + # We need to swap in our main module - this is only necessary + # inside the test framework, because yap_ipython puts the interactive module + # in place (but the test framework undoes this). + _main = sys.modules['__main__'] + sys.modules['__main__'] = ip.user_module + try: + res = dumps(ip.user_ns["w"]) + finally: + sys.modules['__main__'] = _main + self.assertTrue(isinstance(res, bytes)) + + def test_global_ns(self): + "Code in functions must be able to access variables outside them." + ip = get_ipython() + ip.run_cell("a = 10") + ip.run_cell(("def f(x):\n" + " return x + a")) + ip.run_cell("b = f(12)") + self.assertEqual(ip.user_ns["b"], 22) + + def test_bad_custom_tb(self): + """Check that InteractiveShell is protected from bad custom exception handlers""" + ip.set_custom_exc((IOError,), lambda etype,value,tb: 1/0) + self.assertEqual(ip.custom_exceptions, (IOError,)) + with tt.AssertPrints("Custom TB Handler failed", channel='stderr'): + ip.run_cell(u'raise IOError("foo")') + self.assertEqual(ip.custom_exceptions, ()) + + def test_bad_custom_tb_return(self): + """Check that InteractiveShell is protected from bad return types in custom exception handlers""" + ip.set_custom_exc((NameError,),lambda etype,value,tb, tb_offset=None: 1) + self.assertEqual(ip.custom_exceptions, (NameError,)) + with tt.AssertPrints("Custom TB Handler failed", channel='stderr'): + ip.run_cell(u'a=abracadabra') + self.assertEqual(ip.custom_exceptions, ()) + + def test_drop_by_id(self): + myvars = {"a":object(), "b":object(), "c": object()} + ip.push(myvars, interactive=False) + for name in myvars: + assert name in ip.user_ns, name + assert name in ip.user_ns_hidden, name + ip.user_ns['b'] = 12 + ip.drop_by_id(myvars) + for name in ["a", "c"]: + assert name not in ip.user_ns, name + assert name not in ip.user_ns_hidden, name + assert ip.user_ns['b'] == 12 + ip.reset() + + def test_var_expand(self): + ip.user_ns['f'] = u'Ca\xf1o' + self.assertEqual(ip.var_expand(u'echo $f'), u'echo Ca\xf1o') + self.assertEqual(ip.var_expand(u'echo {f}'), u'echo Ca\xf1o') + self.assertEqual(ip.var_expand(u'echo {f[:-1]}'), u'echo Ca\xf1') + self.assertEqual(ip.var_expand(u'echo {1*2}'), u'echo 2') + + self.assertEqual(ip.var_expand(u"grep x | awk '{print $1}'"), u"grep x | awk '{print $1}'") + + ip.user_ns['f'] = b'Ca\xc3\xb1o' + # This should not raise any exception: + ip.var_expand(u'echo $f') + + def test_var_expand_local(self): + """Test local variable expansion in !system and %magic calls""" + # !system + ip.run_cell('def test():\n' + ' lvar = "ttt"\n' + ' ret = !echo {lvar}\n' + ' return ret[0]\n') + res = ip.user_ns['test']() + nt.assert_in('ttt', res) + + # %magic + ip.run_cell('def makemacro():\n' + ' macroname = "macro_var_expand_locals"\n' + ' %macro {macroname} codestr\n') + ip.user_ns['codestr'] = "str(12)" + ip.run_cell('makemacro()') + nt.assert_in('macro_var_expand_locals', ip.user_ns) + + def test_var_expand_self(self): + """Test variable expansion with the name 'self', which was failing. + + See https://github.com/ipython/ipython/issues/1878#issuecomment-7698218 + """ + ip.run_cell('class cTest:\n' + ' classvar="see me"\n' + ' def test(self):\n' + ' res = !echo Variable: {self.classvar}\n' + ' return res[0]\n') + nt.assert_in('see me', ip.user_ns['cTest']().test()) + + def test_bad_var_expand(self): + """var_expand on invalid formats shouldn't raise""" + # SyntaxError + self.assertEqual(ip.var_expand(u"{'a':5}"), u"{'a':5}") + # NameError + self.assertEqual(ip.var_expand(u"{asdf}"), u"{asdf}") + # ZeroDivisionError + self.assertEqual(ip.var_expand(u"{1/0}"), u"{1/0}") + + def test_silent_postexec(self): + """run_cell(silent=True) doesn't invoke pre/post_run_cell callbacks""" + pre_explicit = mock.Mock() + pre_always = mock.Mock() + post_explicit = mock.Mock() + post_always = mock.Mock() + all_mocks = [pre_explicit, pre_always, post_explicit, post_always] + + ip.events.register('pre_run_cell', pre_explicit) + ip.events.register('pre_execute', pre_always) + ip.events.register('post_run_cell', post_explicit) + ip.events.register('post_execute', post_always) + + try: + ip.run_cell("1", silent=True) + assert pre_always.called + assert not pre_explicit.called + assert post_always.called + assert not post_explicit.called + # double-check that non-silent exec did what we expected + # silent to avoid + ip.run_cell("1") + assert pre_explicit.called + assert post_explicit.called + info, = pre_explicit.call_args[0] + result, = post_explicit.call_args[0] + self.assertEqual(info, result.info) + # check that post hooks are always called + [m.reset_mock() for m in all_mocks] + ip.run_cell("syntax error") + assert pre_always.called + assert pre_explicit.called + assert post_always.called + assert post_explicit.called + info, = pre_explicit.call_args[0] + result, = post_explicit.call_args[0] + self.assertEqual(info, result.info) + finally: + # remove post-exec + ip.events.unregister('pre_run_cell', pre_explicit) + ip.events.unregister('pre_execute', pre_always) + ip.events.unregister('post_run_cell', post_explicit) + ip.events.unregister('post_execute', post_always) + + def test_silent_noadvance(self): + """run_cell(silent=True) doesn't advance execution_count""" + ec = ip.execution_count + # silent should force store_history=False + ip.run_cell("1", store_history=True, silent=True) + + self.assertEqual(ec, ip.execution_count) + # double-check that non-silent exec did what we expected + # silent to avoid + ip.run_cell("1", store_history=True) + self.assertEqual(ec+1, ip.execution_count) + + def test_silent_nodisplayhook(self): + """run_cell(silent=True) doesn't trigger displayhook""" + d = dict(called=False) + + trap = ip.display_trap + save_hook = trap.hook + + def failing_hook(*args, **kwargs): + d['called'] = True + + try: + trap.hook = failing_hook + res = ip.run_cell("1", silent=True) + self.assertFalse(d['called']) + self.assertIsNone(res.result) + # double-check that non-silent exec did what we expected + # silent to avoid + ip.run_cell("1") + self.assertTrue(d['called']) + finally: + trap.hook = save_hook + + def test_ofind_line_magic(self): + from yap_ipython.core.magic import register_line_magic + + @register_line_magic + def lmagic(line): + "A line magic" + + # Get info on line magic + lfind = ip._ofind('lmagic') + info = dict(found=True, isalias=False, ismagic=True, + namespace = 'yap_ipython internal', obj= lmagic.__wrapped__, + parent = None) + nt.assert_equal(lfind, info) + + def test_ofind_cell_magic(self): + from yap_ipython.core.magic import register_cell_magic + + @register_cell_magic + def cmagic(line, cell): + "A cell magic" + + # Get info on cell magic + find = ip._ofind('cmagic') + info = dict(found=True, isalias=False, ismagic=True, + namespace = 'yap_ipython internal', obj= cmagic.__wrapped__, + parent = None) + nt.assert_equal(find, info) + + def test_ofind_property_with_error(self): + class A(object): + @property + def foo(self): + raise NotImplementedError() + a = A() + + found = ip._ofind('a.foo', [('locals', locals())]) + info = dict(found=True, isalias=False, ismagic=False, + namespace='locals', obj=A.foo, parent=a) + nt.assert_equal(found, info) + + def test_ofind_multiple_attribute_lookups(self): + class A(object): + @property + def foo(self): + raise NotImplementedError() + + a = A() + a.a = A() + a.a.a = A() + + found = ip._ofind('a.a.a.foo', [('locals', locals())]) + info = dict(found=True, isalias=False, ismagic=False, + namespace='locals', obj=A.foo, parent=a.a.a) + nt.assert_equal(found, info) + + def test_ofind_slotted_attributes(self): + class A(object): + __slots__ = ['foo'] + def __init__(self): + self.foo = 'bar' + + a = A() + found = ip._ofind('a.foo', [('locals', locals())]) + info = dict(found=True, isalias=False, ismagic=False, + namespace='locals', obj=a.foo, parent=a) + nt.assert_equal(found, info) + + found = ip._ofind('a.bar', [('locals', locals())]) + info = dict(found=False, isalias=False, ismagic=False, + namespace=None, obj=None, parent=a) + nt.assert_equal(found, info) + + def test_ofind_prefers_property_to_instance_level_attribute(self): + class A(object): + @property + def foo(self): + return 'bar' + a = A() + a.__dict__['foo'] = 'baz' + nt.assert_equal(a.foo, 'bar') + found = ip._ofind('a.foo', [('locals', locals())]) + nt.assert_is(found['obj'], A.foo) + + def test_custom_syntaxerror_exception(self): + called = [] + def my_handler(shell, etype, value, tb, tb_offset=None): + called.append(etype) + shell.showtraceback((etype, value, tb), tb_offset=tb_offset) + + ip.set_custom_exc((SyntaxError,), my_handler) + try: + ip.run_cell("1f") + # Check that this was called, and only once. + self.assertEqual(called, [SyntaxError]) + finally: + # Reset the custom exception hook + ip.set_custom_exc((), None) + + def test_custom_exception(self): + called = [] + def my_handler(shell, etype, value, tb, tb_offset=None): + called.append(etype) + shell.showtraceback((etype, value, tb), tb_offset=tb_offset) + + ip.set_custom_exc((ValueError,), my_handler) + try: + res = ip.run_cell("raise ValueError('test')") + # Check that this was called, and only once. + self.assertEqual(called, [ValueError]) + # Check that the error is on the result object + self.assertIsInstance(res.error_in_exec, ValueError) + finally: + # Reset the custom exception hook + ip.set_custom_exc((), None) + + def test_mktempfile(self): + filename = ip.mktempfile() + # Check that we can open the file again on Windows + with open(filename, 'w') as f: + f.write('abc') + + filename = ip.mktempfile(data='blah') + with open(filename, 'r') as f: + self.assertEqual(f.read(), 'blah') + + def test_new_main_mod(self): + # Smoketest to check that this accepts a unicode module name + name = u'jiefmw' + mod = ip.new_main_mod(u'%s.py' % name, name) + self.assertEqual(mod.__name__, name) + + def test_get_exception_only(self): + try: + raise KeyboardInterrupt + except KeyboardInterrupt: + msg = ip.get_exception_only() + self.assertEqual(msg, 'KeyboardInterrupt\n') + + try: + raise DerivedInterrupt("foo") + except KeyboardInterrupt: + msg = ip.get_exception_only() + self.assertEqual(msg, 'yap_ipython.core.tests.test_interactiveshell.DerivedInterrupt: foo\n') + + def test_inspect_text(self): + ip.run_cell('a = 5') + text = ip.object_inspect_text('a') + self.assertIsInstance(text, str) + + def test_last_execution_result(self): + """ Check that last execution result gets set correctly (GH-10702) """ + result = ip.run_cell('a = 5; a') + self.assertTrue(ip.last_execution_succeeded) + self.assertEqual(ip.last_execution_result.result, 5) + + result = ip.run_cell('a = x_invalid_id_x') + self.assertFalse(ip.last_execution_succeeded) + self.assertFalse(ip.last_execution_result.success) + self.assertIsInstance(ip.last_execution_result.error_in_exec, NameError) + + +class TestSafeExecfileNonAsciiPath(unittest.TestCase): + + @onlyif_unicode_paths + def setUp(self): + self.BASETESTDIR = tempfile.mkdtemp() + self.TESTDIR = join(self.BASETESTDIR, u"åäö") + os.mkdir(self.TESTDIR) + with open(join(self.TESTDIR, u"åäötestscript.py"), "w") as sfile: + sfile.write("pass\n") + self.oldpath = os.getcwd() + os.chdir(self.TESTDIR) + self.fname = u"åäötestscript.py" + + def tearDown(self): + os.chdir(self.oldpath) + shutil.rmtree(self.BASETESTDIR) + + @onlyif_unicode_paths + def test_1(self): + """Test safe_execfile with non-ascii path + """ + ip.safe_execfile(self.fname, {}, raise_exceptions=True) + +class ExitCodeChecks(tt.TempFileMixin): + def test_exit_code_ok(self): + self.system('exit 0') + self.assertEqual(ip.user_ns['_exit_code'], 0) + + def test_exit_code_error(self): + self.system('exit 1') + self.assertEqual(ip.user_ns['_exit_code'], 1) + + @skipif(not hasattr(signal, 'SIGALRM')) + def test_exit_code_signal(self): + self.mktmp("import signal, time\n" + "signal.setitimer(signal.ITIMER_REAL, 0.1)\n" + "time.sleep(1)\n") + self.system("%s %s" % (sys.executable, self.fname)) + self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGALRM) + + @onlyif_cmds_exist("csh") + def test_exit_code_signal_csh(self): + SHELL = os.environ.get('SHELL', None) + os.environ['SHELL'] = find_cmd("csh") + try: + self.test_exit_code_signal() + finally: + if SHELL is not None: + os.environ['SHELL'] = SHELL + else: + del os.environ['SHELL'] + +class TestSystemRaw(unittest.TestCase, ExitCodeChecks): + system = ip.system_raw + + @onlyif_unicode_paths + def test_1(self): + """Test system_raw with non-ascii cmd + """ + cmd = u'''python -c "'åäö'" ''' + ip.system_raw(cmd) + + @mock.patch('subprocess.call', side_effect=KeyboardInterrupt) + @mock.patch('os.system', side_effect=KeyboardInterrupt) + def test_control_c(self, *mocks): + try: + self.system("sleep 1 # wont happen") + except KeyboardInterrupt: + self.fail("system call should intercept " + "keyboard interrupt from subprocess.call") + self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGINT) + +# TODO: Exit codes are currently ignored on Windows. +class TestSystemPipedExitCode(unittest.TestCase, ExitCodeChecks): + system = ip.system_piped + + @skip_win32 + def test_exit_code_ok(self): + ExitCodeChecks.test_exit_code_ok(self) + + @skip_win32 + def test_exit_code_error(self): + ExitCodeChecks.test_exit_code_error(self) + + @skip_win32 + def test_exit_code_signal(self): + ExitCodeChecks.test_exit_code_signal(self) + +class TestModules(unittest.TestCase, tt.TempFileMixin): + def test_extraneous_loads(self): + """Test we're not loading modules on startup that we shouldn't. + """ + self.mktmp("import sys\n" + "print('numpy' in sys.modules)\n" + "print('ipyparallel' in sys.modules)\n" + "print('yap_kernel' in sys.modules)\n" + ) + out = "False\nFalse\nFalse\n" + tt.ipexec_validate(self.fname, out) + +class Negator(ast.NodeTransformer): + """Negates all number literals in an AST.""" + def visit_Num(self, node): + node.n = -node.n + return node + +class TestAstTransform(unittest.TestCase): + def setUp(self): + self.negator = Negator() + ip.ast_transformers.append(self.negator) + + def tearDown(self): + ip.ast_transformers.remove(self.negator) + + def test_run_cell(self): + with tt.AssertPrints('-34'): + ip.run_cell('print (12 + 22)') + + # A named reference to a number shouldn't be transformed. + ip.user_ns['n'] = 55 + with tt.AssertNotPrints('-55'): + ip.run_cell('print (n)') + + def test_timeit(self): + called = set() + def f(x): + called.add(x) + ip.push({'f':f}) + + with tt.AssertPrints("std. dev. of"): + ip.run_line_magic("timeit", "-n1 f(1)") + self.assertEqual(called, {-1}) + called.clear() + + with tt.AssertPrints("std. dev. of"): + ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)") + self.assertEqual(called, {-2, -3}) + + def test_time(self): + called = [] + def f(x): + called.append(x) + ip.push({'f':f}) + + # Test with an expression + with tt.AssertPrints("Wall time: "): + ip.run_line_magic("time", "f(5+9)") + self.assertEqual(called, [-14]) + called[:] = [] + + # Test with a statement (different code path) + with tt.AssertPrints("Wall time: "): + ip.run_line_magic("time", "a = f(-3 + -2)") + self.assertEqual(called, [5]) + + def test_macro(self): + ip.push({'a':10}) + # The AST transformation makes this do a+=-1 + ip.define_macro("amacro", "a+=1\nprint(a)") + + with tt.AssertPrints("9"): + ip.run_cell("amacro") + with tt.AssertPrints("8"): + ip.run_cell("amacro") + +class IntegerWrapper(ast.NodeTransformer): + """Wraps all integers in a call to Integer()""" + def visit_Num(self, node): + if isinstance(node.n, int): + return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()), + args=[node], keywords=[]) + return node + +class TestAstTransform2(unittest.TestCase): + def setUp(self): + self.intwrapper = IntegerWrapper() + ip.ast_transformers.append(self.intwrapper) + + self.calls = [] + def Integer(*args): + self.calls.append(args) + return args + ip.push({"Integer": Integer}) + + def tearDown(self): + ip.ast_transformers.remove(self.intwrapper) + del ip.user_ns['Integer'] + + def test_run_cell(self): + ip.run_cell("n = 2") + self.assertEqual(self.calls, [(2,)]) + + # This shouldn't throw an error + ip.run_cell("o = 2.0") + self.assertEqual(ip.user_ns['o'], 2.0) + + def test_timeit(self): + called = set() + def f(x): + called.add(x) + ip.push({'f':f}) + + with tt.AssertPrints("std. dev. of"): + ip.run_line_magic("timeit", "-n1 f(1)") + self.assertEqual(called, {(1,)}) + called.clear() + + with tt.AssertPrints("std. dev. of"): + ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)") + self.assertEqual(called, {(2,), (3,)}) + +class ErrorTransformer(ast.NodeTransformer): + """Throws an error when it sees a number.""" + def visit_Num(self, node): + raise ValueError("test") + +class TestAstTransformError(unittest.TestCase): + def test_unregistering(self): + err_transformer = ErrorTransformer() + ip.ast_transformers.append(err_transformer) + + with tt.AssertPrints("unregister", channel='stderr'): + ip.run_cell("1 + 2") + + # This should have been removed. + nt.assert_not_in(err_transformer, ip.ast_transformers) + + +class StringRejector(ast.NodeTransformer): + """Throws an InputRejected when it sees a string literal. + + Used to verify that NodeTransformers can signal that a piece of code should + not be executed by throwing an InputRejected. + """ + + def visit_Str(self, node): + raise InputRejected("test") + + +class TestAstTransformInputRejection(unittest.TestCase): + + def setUp(self): + self.transformer = StringRejector() + ip.ast_transformers.append(self.transformer) + + def tearDown(self): + ip.ast_transformers.remove(self.transformer) + + def test_input_rejection(self): + """Check that NodeTransformers can reject input.""" + + expect_exception_tb = tt.AssertPrints("InputRejected: test") + expect_no_cell_output = tt.AssertNotPrints("'unsafe'", suppress=False) + + # Run the same check twice to verify that the transformer is not + # disabled after raising. + with expect_exception_tb, expect_no_cell_output: + ip.run_cell("'unsafe'") + + with expect_exception_tb, expect_no_cell_output: + res = ip.run_cell("'unsafe'") + + self.assertIsInstance(res.error_before_exec, InputRejected) + +def test__IPYTHON__(): + # This shouldn't raise a NameError, that's all + __IPYTHON__ + + +class DummyRepr(object): + def __repr__(self): + return "DummyRepr" + + def _repr_html_(self): + return "dummy" + + def _repr_javascript_(self): + return "console.log('hi');", {'key': 'value'} + + +def test_user_variables(): + # enable all formatters + ip.display_formatter.active_types = ip.display_formatter.format_types + + ip.user_ns['dummy'] = d = DummyRepr() + keys = {'dummy', 'doesnotexist'} + r = ip.user_expressions({ key:key for key in keys}) + + nt.assert_equal(keys, set(r.keys())) + dummy = r['dummy'] + nt.assert_equal({'status', 'data', 'metadata'}, set(dummy.keys())) + nt.assert_equal(dummy['status'], 'ok') + data = dummy['data'] + metadata = dummy['metadata'] + nt.assert_equal(data.get('text/html'), d._repr_html_()) + js, jsmd = d._repr_javascript_() + nt.assert_equal(data.get('application/javascript'), js) + nt.assert_equal(metadata.get('application/javascript'), jsmd) + + dne = r['doesnotexist'] + nt.assert_equal(dne['status'], 'error') + nt.assert_equal(dne['ename'], 'NameError') + + # back to text only + ip.display_formatter.active_types = ['text/plain'] + +def test_user_expression(): + # enable all formatters + ip.display_formatter.active_types = ip.display_formatter.format_types + query = { + 'a' : '1 + 2', + 'b' : '1/0', + } + r = ip.user_expressions(query) + import pprint + pprint.pprint(r) + nt.assert_equal(set(r.keys()), set(query.keys())) + a = r['a'] + nt.assert_equal({'status', 'data', 'metadata'}, set(a.keys())) + nt.assert_equal(a['status'], 'ok') + data = a['data'] + metadata = a['metadata'] + nt.assert_equal(data.get('text/plain'), '3') + + b = r['b'] + nt.assert_equal(b['status'], 'error') + nt.assert_equal(b['ename'], 'ZeroDivisionError') + + # back to text only + ip.display_formatter.active_types = ['text/plain'] + + + + + +class TestSyntaxErrorTransformer(unittest.TestCase): + """Check that SyntaxError raised by an input transformer is handled by run_cell()""" + + class SyntaxErrorTransformer(InputTransformer): + + def push(self, line): + pos = line.find('syntaxerror') + if pos >= 0: + e = SyntaxError('input contains "syntaxerror"') + e.text = line + e.offset = pos + 1 + raise e + return line + + def reset(self): + pass + + def setUp(self): + self.transformer = TestSyntaxErrorTransformer.SyntaxErrorTransformer() + ip.input_splitter.python_line_transforms.append(self.transformer) + ip.input_transformer_manager.python_line_transforms.append(self.transformer) + + def tearDown(self): + ip.input_splitter.python_line_transforms.remove(self.transformer) + ip.input_transformer_manager.python_line_transforms.remove(self.transformer) + + def test_syntaxerror_input_transformer(self): + with tt.AssertPrints('1234'): + ip.run_cell('1234') + with tt.AssertPrints('SyntaxError: invalid syntax'): + ip.run_cell('1 2 3') # plain python syntax error + with tt.AssertPrints('SyntaxError: input contains "syntaxerror"'): + ip.run_cell('2345 # syntaxerror') # input transformer syntax error + with tt.AssertPrints('3456'): + ip.run_cell('3456') + + + +def test_warning_suppression(): + ip.run_cell("import warnings") + try: + with tt.AssertPrints("UserWarning: asdf", channel="stderr"): + ip.run_cell("warnings.warn('asdf')") + # Here's the real test -- if we run that again, we should get the + # warning again. Traditionally, each warning was only issued once per + # yap_ipython session (approximately), even if the user typed in new and + # different code that should have also triggered the warning, leading + # to much confusion. + with tt.AssertPrints("UserWarning: asdf", channel="stderr"): + ip.run_cell("warnings.warn('asdf')") + finally: + ip.run_cell("del warnings") + + +def test_deprecation_warning(): + ip.run_cell(""" +import warnings +def wrn(): + warnings.warn( + "I AM A WARNING", + DeprecationWarning + ) + """) + try: + with tt.AssertPrints("I AM A WARNING", channel="stderr"): + ip.run_cell("wrn()") + finally: + ip.run_cell("del warnings") + ip.run_cell("del wrn") + + +class TestImportNoDeprecate(tt.TempFileMixin): + + def setup(self): + """Make a valid python temp file.""" + self.mktmp(""" +import warnings +def wrn(): + warnings.warn( + "I AM A WARNING", + DeprecationWarning + ) +""") + + def test_no_dep(self): + """ + No deprecation warning should be raised from imported functions + """ + ip.run_cell("from {} import wrn".format(self.fname)) + + with tt.AssertNotPrints("I AM A WARNING"): + ip.run_cell("wrn()") + ip.run_cell("del wrn") diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_iplib.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_iplib.py new file mode 100644 index 000000000..d0a5c44f4 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_iplib.py @@ -0,0 +1,254 @@ +"""Tests for the key interactiveshell module, where the main ipython class is defined. +""" +#----------------------------------------------------------------------------- +# Module imports +#----------------------------------------------------------------------------- + +# third party +import nose.tools as nt + +# our own packages +from yap_ipython.testing.globalipapp import get_ipython + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +# Get the public instance of yap_ipython +ip = get_ipython() + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- + +def test_reset(): + """reset must clear most namespaces.""" + + # Check that reset runs without error + ip.reset() + + # Once we've reset it (to clear of any junk that might have been there from + # other tests, we can count how many variables are in the user's namespace + nvars_user_ns = len(ip.user_ns) + nvars_hidden = len(ip.user_ns_hidden) + + # Now add a few variables to user_ns, and check that reset clears them + ip.user_ns['x'] = 1 + ip.user_ns['y'] = 1 + ip.reset() + + # Finally, check that all namespaces have only as many variables as we + # expect to find in them: + nt.assert_equal(len(ip.user_ns), nvars_user_ns) + nt.assert_equal(len(ip.user_ns_hidden), nvars_hidden) + + +# Tests for reporting of exceptions in various modes, handling of SystemExit, +# and %tb functionality. This is really a mix of testing ultraTB and interactiveshell. + +def doctest_tb_plain(): + """ +In [18]: xmode plain +Exception reporting mode: Plain + +In [19]: run simpleerr.py +Traceback (most recent call last): + ...line 32, in + bar(mode) + ...line 16, in bar + div0() + ...line 8, in div0 + x/y +ZeroDivisionError: ... + """ + + +def doctest_tb_context(): + """ +In [3]: xmode context +Exception reporting mode: Context + +In [4]: run simpleerr.py +--------------------------------------------------------------------------- +ZeroDivisionError Traceback (most recent call last) + +... in () + 30 mode = 'div' + 31 +---> 32 bar(mode) + +... in bar(mode) + 14 "bar" + 15 if mode=='div': +---> 16 div0() + 17 elif mode=='exit': + 18 try: + +... in div0() + 6 x = 1 + 7 y = 0 +----> 8 x/y + 9 + 10 def sysexit(stat, mode): + +ZeroDivisionError: ... +""" + + +def doctest_tb_verbose(): + """ +In [5]: xmode verbose +Exception reporting mode: Verbose + +In [6]: run simpleerr.py +--------------------------------------------------------------------------- +ZeroDivisionError Traceback (most recent call last) + +... in () + 30 mode = 'div' + 31 +---> 32 bar(mode) + global bar = + global mode = 'div' + +... in bar(mode='div') + 14 "bar" + 15 if mode=='div': +---> 16 div0() + global div0 = + 17 elif mode=='exit': + 18 try: + +... in div0() + 6 x = 1 + 7 y = 0 +----> 8 x/y + x = 1 + y = 0 + 9 + 10 def sysexit(stat, mode): + +ZeroDivisionError: ... + """ + +def doctest_tb_sysexit(): + """ +In [17]: %xmode plain +Exception reporting mode: Plain + +In [18]: %run simpleerr.py exit +An exception has occurred, use %tb to see the full traceback. +SystemExit: (1, 'Mode = exit') + +In [19]: %run simpleerr.py exit 2 +An exception has occurred, use %tb to see the full traceback. +SystemExit: (2, 'Mode = exit') + +In [20]: %tb +Traceback (most recent call last): + File ... in + bar(mode) + File ... line 22, in bar + sysexit(stat, mode) + File ... line 11, in sysexit + raise SystemExit(stat, 'Mode = %s' % mode) +SystemExit: (2, 'Mode = exit') + +In [21]: %xmode context +Exception reporting mode: Context + +In [22]: %tb +--------------------------------------------------------------------------- +SystemExit Traceback (most recent call last) + +...() + 30 mode = 'div' + 31 +---> 32 bar(mode) + +...bar(mode) + 20 except: + 21 stat = 1 +---> 22 sysexit(stat, mode) + 23 else: + 24 raise ValueError('Unknown mode') + +...sysexit(stat, mode) + 9 + 10 def sysexit(stat, mode): +---> 11 raise SystemExit(stat, 'Mode = %s' % mode) + 12 + 13 def bar(mode): + +SystemExit: (2, 'Mode = exit') + +In [23]: %xmode verbose +Exception reporting mode: Verbose + +In [24]: %tb +--------------------------------------------------------------------------- +SystemExit Traceback (most recent call last) + +... in () + 30 mode = 'div' + 31 +---> 32 bar(mode) + global bar = + global mode = 'exit' + +... in bar(mode='exit') + 20 except: + 21 stat = 1 +---> 22 sysexit(stat, mode) + global sysexit = + stat = 2 + mode = 'exit' + 23 else: + 24 raise ValueError('Unknown mode') + +... in sysexit(stat=2, mode='exit') + 9 + 10 def sysexit(stat, mode): +---> 11 raise SystemExit(stat, 'Mode = %s' % mode) + global SystemExit = undefined + stat = 2 + mode = 'exit' + 12 + 13 def bar(mode): + +SystemExit: (2, 'Mode = exit') + """ + + +def test_run_cell(): + import textwrap + ip.run_cell('a = 10\na+=1') + ip.run_cell('assert a == 11\nassert 1') + + nt.assert_equal(ip.user_ns['a'], 11) + complex = textwrap.dedent(""" + if 1: + print "hello" + if 1: + print "world" + + if 2: + print "foo" + + if 3: + print "bar" + + if 4: + print "bar" + + """) + # Simply verifies that this kind of input is run + ip.run_cell(complex) + + +def test_db(): + """Test the internal database used for variable persistence.""" + ip.db['__unittest_'] = 12 + nt.assert_equal(ip.db['__unittest_'], 12) + del ip.db['__unittest_'] + assert '__unittest_' not in ip.db diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_logger.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_logger.py new file mode 100644 index 000000000..09b987886 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_logger.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Test yap_ipython.core.logger""" + +import os.path + +import nose.tools as nt +from yap_ipython.utils.tempdir import TemporaryDirectory + +_ip = get_ipython() + +def test_logstart_inaccessible_file(): + try: + _ip.logger.logstart(logfname="/") # Opening that filename will fail. + except IOError: + pass + else: + nt.assert_true(False) # The try block should never pass. + + try: + _ip.run_cell("a=1") # Check it doesn't try to log this + finally: + _ip.logger.log_active = False # If this fails, don't let later tests fail + +def test_logstart_unicode(): + with TemporaryDirectory() as tdir: + logfname = os.path.join(tdir, "test_unicode.log") + _ip.run_cell("'abc€'") + try: + _ip.magic("logstart -to %s" % logfname) + _ip.run_cell("'abc€'") + finally: + _ip.logger.logstop() diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_magic.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_magic.py new file mode 100644 index 000000000..f90aa2f54 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_magic.py @@ -0,0 +1,1073 @@ +# -*- coding: utf-8 -*- +"""Tests for various magic functions. + +Needs to be run by nose (to make ipython session available). +""" + +import io +import os +import re +import sys +import warnings +from unittest import TestCase +from importlib import invalidate_caches +from io import StringIO + +import nose.tools as nt + +import shlex + +from yap_ipython import get_ipython +from yap_ipython.core import magic +from yap_ipython.core.error import UsageError +from yap_ipython.core.magic import (Magics, magics_class, line_magic, + cell_magic, + register_line_magic, register_cell_magic) +from yap_ipython.core.magics import execution, script, code, logging +from yap_ipython.testing import decorators as dec +from yap_ipython.testing import tools as tt +from yap_ipython.utils import py3compat +from yap_ipython.utils.io import capture_output +from yap_ipython.utils.tempdir import TemporaryDirectory +from yap_ipython.utils.process import find_cmd + + + +_ip = get_ipython() + +@magic.magics_class +class DummyMagics(magic.Magics): pass + +def test_extract_code_ranges(): + instr = "1 3 5-6 7-9 10:15 17: :10 10- -13 :" + expected = [(0, 1), + (2, 3), + (4, 6), + (6, 9), + (9, 14), + (16, None), + (None, 9), + (9, None), + (None, 13), + (None, None)] + actual = list(code.extract_code_ranges(instr)) + nt.assert_equal(actual, expected) + +def test_extract_symbols(): + source = """import foo\na = 10\ndef b():\n return 42\n\n\nclass A: pass\n\n\n""" + symbols_args = ["a", "b", "A", "A,b", "A,a", "z"] + expected = [([], ['a']), + (["def b():\n return 42\n"], []), + (["class A: pass\n"], []), + (["class A: pass\n", "def b():\n return 42\n"], []), + (["class A: pass\n"], ['a']), + ([], ['z'])] + for symbols, exp in zip(symbols_args, expected): + nt.assert_equal(code.extract_symbols(source, symbols), exp) + + +def test_extract_symbols_raises_exception_with_non_python_code(): + source = ("=begin A Ruby program :)=end\n" + "def hello\n" + "puts 'Hello world'\n" + "end") + with nt.assert_raises(SyntaxError): + code.extract_symbols(source, "hello") + + +def test_magic_not_found(): + # magic not found raises UsageError + with nt.assert_raises(UsageError): + _ip.magic('doesntexist') + + # ensure result isn't success when a magic isn't found + result = _ip.run_cell('%doesntexist') + assert isinstance(result.error_in_exec, UsageError) + + +def test_cell_magic_not_found(): + # magic not found raises UsageError + with nt.assert_raises(UsageError): + _ip.run_cell_magic('doesntexist', 'line', 'cell') + + # ensure result isn't success when a magic isn't found + result = _ip.run_cell('%%doesntexist') + assert isinstance(result.error_in_exec, UsageError) + + +def test_magic_error_status(): + def fail(shell): + 1/0 + _ip.register_magic_function(fail) + result = _ip.run_cell('%fail') + assert isinstance(result.error_in_exec, ZeroDivisionError) + + +def test_config(): + """ test that config magic does not raise + can happen if Configurable init is moved too early into + Magics.__init__ as then a Config object will be registered as a + magic. + """ + ## should not raise. + _ip.magic('config') + +def test_config_available_configs(): + """ test that config magic prints available configs in unique and + sorted order. """ + with capture_output() as captured: + _ip.magic('config') + + stdout = captured.stdout + config_classes = stdout.strip().split('\n')[1:] + nt.assert_list_equal(config_classes, sorted(set(config_classes))) + +def test_config_print_class(): + """ test that config with a classname prints the class's options. """ + with capture_output() as captured: + _ip.magic('config TerminalInteractiveShell') + + stdout = captured.stdout + if not re.match("TerminalInteractiveShell.* options", stdout.splitlines()[0]): + print(stdout) + raise AssertionError("1st line of stdout not like " + "'TerminalInteractiveShell.* options'") + +def test_rehashx(): + # clear up everything + _ip.alias_manager.clear_aliases() + del _ip.db['syscmdlist'] + + _ip.magic('rehashx') + # Practically ALL ipython development systems will have more than 10 aliases + + nt.assert_true(len(_ip.alias_manager.aliases) > 10) + for name, cmd in _ip.alias_manager.aliases: + # we must strip dots from alias names + nt.assert_not_in('.', name) + + # rehashx must fill up syscmdlist + scoms = _ip.db['syscmdlist'] + nt.assert_true(len(scoms) > 10) + + +def test_magic_parse_options(): + """Test that we don't mangle paths when parsing magic options.""" + ip = get_ipython() + path = 'c:\\x' + m = DummyMagics(ip) + opts = m.parse_options('-f %s' % path,'f:')[0] + # argv splitting is os-dependent + if os.name == 'posix': + expected = 'c:x' + else: + expected = path + nt.assert_equal(opts['f'], expected) + +def test_magic_parse_long_options(): + """Magic.parse_options can handle --foo=bar long options""" + ip = get_ipython() + m = DummyMagics(ip) + opts, _ = m.parse_options('--foo --bar=bubble', 'a', 'foo', 'bar=') + nt.assert_in('foo', opts) + nt.assert_in('bar', opts) + nt.assert_equal(opts['bar'], "bubble") + + +@dec.skip_without('sqlite3') +def doctest_hist_f(): + """Test %hist -f with temporary filename. + + In [9]: import tempfile + + In [10]: tfile = tempfile.mktemp('.py','tmp-ipython-') + + In [11]: %hist -nl -f $tfile 3 + + In [13]: import os; os.unlink(tfile) + """ + + +@dec.skip_without('sqlite3') +def doctest_hist_r(): + """Test %hist -r + + XXX - This test is not recording the output correctly. For some reason, in + testing mode the raw history isn't getting populated. No idea why. + Disabling the output checking for now, though at least we do run it. + + In [1]: 'hist' in _ip.lsmagic() + Out[1]: True + + In [2]: x=1 + + In [3]: %hist -rl 2 + x=1 # random + %hist -r 2 + """ + + +@dec.skip_without('sqlite3') +def doctest_hist_op(): + """Test %hist -op + + In [1]: class b(float): + ...: pass + ...: + + In [2]: class s(object): + ...: def __str__(self): + ...: return 's' + ...: + + In [3]: + + In [4]: class r(b): + ...: def __repr__(self): + ...: return 'r' + ...: + + In [5]: class sr(s,r): pass + ...: + + In [6]: + + In [7]: bb=b() + + In [8]: ss=s() + + In [9]: rr=r() + + In [10]: ssrr=sr() + + In [11]: 4.5 + Out[11]: 4.5 + + In [12]: str(ss) + Out[12]: 's' + + In [13]: + + In [14]: %hist -op + >>> class b: + ... pass + ... + >>> class s(b): + ... def __str__(self): + ... return 's' + ... + >>> + >>> class r(b): + ... def __repr__(self): + ... return 'r' + ... + >>> class sr(s,r): pass + >>> + >>> bb=b() + >>> ss=s() + >>> rr=r() + >>> ssrr=sr() + >>> 4.5 + 4.5 + >>> str(ss) + 's' + >>> + """ + +def test_hist_pof(): + ip = get_ipython() + ip.run_cell(u"1+2", store_history=True) + #raise Exception(ip.history_manager.session_number) + #raise Exception(list(ip.history_manager._get_range_session())) + with TemporaryDirectory() as td: + tf = os.path.join(td, 'hist.py') + ip.run_line_magic('history', '-pof %s' % tf) + assert os.path.isfile(tf) + + +@dec.skip_without('sqlite3') +def test_macro(): + ip = get_ipython() + ip.history_manager.reset() # Clear any existing history. + cmds = ["a=1", "def b():\n return a**2", "print(a,b())"] + for i, cmd in enumerate(cmds, start=1): + ip.history_manager.store_inputs(i, cmd) + ip.magic("macro test 1-3") + nt.assert_equal(ip.user_ns["test"].value, "\n".join(cmds)+"\n") + + # List macros + nt.assert_in("test", ip.magic("macro")) + + +@dec.skip_without('sqlite3') +def test_macro_run(): + """Test that we can run a multi-line macro successfully.""" + ip = get_ipython() + ip.history_manager.reset() + cmds = ["a=10", "a+=1", py3compat.doctest_refactor_print("print a"), + "%macro test 2-3"] + for cmd in cmds: + ip.run_cell(cmd, store_history=True) + nt.assert_equal(ip.user_ns["test"].value, + py3compat.doctest_refactor_print("a+=1\nprint a\n")) + with tt.AssertPrints("12"): + ip.run_cell("test") + with tt.AssertPrints("13"): + ip.run_cell("test") + + +def test_magic_magic(): + """Test %magic""" + ip = get_ipython() + with capture_output() as captured: + ip.magic("magic") + + stdout = captured.stdout + nt.assert_in('%magic', stdout) + nt.assert_in('yap_ipython', stdout) + nt.assert_in('Available', stdout) + + +@dec.skipif_not_numpy +def test_numpy_reset_array_undec(): + "Test '%reset array' functionality" + _ip.ex('import numpy as np') + _ip.ex('a = np.empty(2)') + nt.assert_in('a', _ip.user_ns) + _ip.magic('reset -f array') + nt.assert_not_in('a', _ip.user_ns) + +def test_reset_out(): + "Test '%reset out' magic" + _ip.run_cell("parrot = 'dead'", store_history=True) + # test '%reset -f out', make an Out prompt + _ip.run_cell("parrot", store_history=True) + nt.assert_true('dead' in [_ip.user_ns[x] for x in ('_','__','___')]) + _ip.magic('reset -f out') + nt.assert_false('dead' in [_ip.user_ns[x] for x in ('_','__','___')]) + nt.assert_equal(len(_ip.user_ns['Out']), 0) + +def test_reset_in(): + "Test '%reset in' magic" + # test '%reset -f in' + _ip.run_cell("parrot", store_history=True) + nt.assert_true('parrot' in [_ip.user_ns[x] for x in ('_i','_ii','_iii')]) + _ip.magic('%reset -f in') + nt.assert_false('parrot' in [_ip.user_ns[x] for x in ('_i','_ii','_iii')]) + nt.assert_equal(len(set(_ip.user_ns['In'])), 1) + +def test_reset_dhist(): + "Test '%reset dhist' magic" + _ip.run_cell("tmp = [d for d in _dh]") # copy before clearing + _ip.magic('cd ' + os.path.dirname(nt.__file__)) + _ip.magic('cd -') + nt.assert_true(len(_ip.user_ns['_dh']) > 0) + _ip.magic('reset -f dhist') + nt.assert_equal(len(_ip.user_ns['_dh']), 0) + _ip.run_cell("_dh = [d for d in tmp]") #restore + +def test_reset_in_length(): + "Test that '%reset in' preserves In[] length" + _ip.run_cell("print 'foo'") + _ip.run_cell("reset -f in") + nt.assert_equal(len(_ip.user_ns['In']), _ip.displayhook.prompt_count+1) + +def test_tb_syntaxerror(): + """test %tb after a SyntaxError""" + ip = get_ipython() + ip.run_cell("for") + + # trap and validate stdout + save_stdout = sys.stdout + try: + sys.stdout = StringIO() + ip.run_cell("%tb") + out = sys.stdout.getvalue() + finally: + sys.stdout = save_stdout + # trim output, and only check the last line + last_line = out.rstrip().splitlines()[-1].strip() + nt.assert_equal(last_line, "SyntaxError: invalid syntax") + + +def test_time(): + ip = get_ipython() + + with tt.AssertPrints("Wall time: "): + ip.run_cell("%time None") + + ip.run_cell("def f(kmjy):\n" + " %time print (2*kmjy)") + + with tt.AssertPrints("Wall time: "): + with tt.AssertPrints("hihi", suppress=False): + ip.run_cell("f('hi')") + + +@dec.skip_win32 +def test_time2(): + ip = get_ipython() + + with tt.AssertPrints("CPU times: user "): + ip.run_cell("%time None") + +def test_time3(): + """Erroneous magic function calls, issue gh-3334""" + ip = get_ipython() + ip.user_ns.pop('run', None) + + with tt.AssertNotPrints("not found", channel='stderr'): + ip.run_cell("%%time\n" + "run = 0\n" + "run += 1") + +def test_doctest_mode(): + "Toggle doctest_mode twice, it should be a no-op and run without error" + _ip.magic('doctest_mode') + _ip.magic('doctest_mode') + + +def test_parse_options(): + """Tests for basic options parsing in magics.""" + # These are only the most minimal of tests, more should be added later. At + # the very least we check that basic text/unicode calls work OK. + m = DummyMagics(_ip) + nt.assert_equal(m.parse_options('foo', '')[1], 'foo') + nt.assert_equal(m.parse_options(u'foo', '')[1], u'foo') + + +def test_dirops(): + """Test various directory handling operations.""" + # curpath = lambda :os.path.splitdrive(os.getcwd())[1].replace('\\','/') + curpath = os.getcwd + startdir = os.getcwd() + ipdir = os.path.realpath(_ip.ipython_dir) + try: + _ip.magic('cd "%s"' % ipdir) + nt.assert_equal(curpath(), ipdir) + _ip.magic('cd -') + nt.assert_equal(curpath(), startdir) + _ip.magic('pushd "%s"' % ipdir) + nt.assert_equal(curpath(), ipdir) + _ip.magic('popd') + nt.assert_equal(curpath(), startdir) + finally: + os.chdir(startdir) + + +def test_xmode(): + # Calling xmode three times should be a no-op + xmode = _ip.InteractiveTB.mode + for i in range(3): + _ip.magic("xmode") + nt.assert_equal(_ip.InteractiveTB.mode, xmode) + +def test_reset_hard(): + monitor = [] + class A(object): + def __del__(self): + monitor.append(1) + def __repr__(self): + return "" + + _ip.user_ns["a"] = A() + _ip.run_cell("a") + + nt.assert_equal(monitor, []) + _ip.magic("reset -f") + nt.assert_equal(monitor, [1]) + +class TestXdel(tt.TempFileMixin): + def test_xdel(self): + """Test that references from %run are cleared by xdel.""" + src = ("class A(object):\n" + " monitor = []\n" + " def __del__(self):\n" + " self.monitor.append(1)\n" + "a = A()\n") + self.mktmp(src) + # %run creates some hidden references... + _ip.magic("run %s" % self.fname) + # ... as does the displayhook. + _ip.run_cell("a") + + monitor = _ip.user_ns["A"].monitor + nt.assert_equal(monitor, []) + + _ip.magic("xdel a") + + # Check that a's __del__ method has been called. + nt.assert_equal(monitor, [1]) + +def doctest_who(): + """doctest for %who + + In [1]: %reset -f + + In [2]: alpha = 123 + + In [3]: beta = 'beta' + + In [4]: %who int + alpha + + In [5]: %who str + beta + + In [6]: %whos + Variable Type Data/Info + ---------------------------- + alpha int 123 + beta str beta + + In [7]: %who_ls + Out[7]: ['alpha', 'beta'] + """ + +def test_whos(): + """Check that whos is protected against objects where repr() fails.""" + class A(object): + def __repr__(self): + raise Exception() + _ip.user_ns['a'] = A() + _ip.magic("whos") + +@py3compat.u_format +def doctest_precision(): + """doctest for %precision + + In [1]: f = get_ipython().display_formatter.formatters['text/plain'] + + In [2]: %precision 5 + Out[2]: '%.5f' + + In [3]: f.float_format + Out[3]: '%.5f' + + In [4]: %precision %e + Out[4]: '%e' + + In [5]: f(3.1415927) + Out[5]: '3.141593e+00' + """ + +def test_psearch(): + with tt.AssertPrints("dict.fromkeys"): + _ip.run_cell("dict.fr*?") + +def test_timeit_shlex(): + """test shlex issues with timeit (#1109)""" + _ip.ex("def f(*a,**kw): pass") + _ip.magic('timeit -n1 "this is a bug".count(" ")') + _ip.magic('timeit -r1 -n1 f(" ", 1)') + _ip.magic('timeit -r1 -n1 f(" ", 1, " ", 2, " ")') + _ip.magic('timeit -r1 -n1 ("a " + "b")') + _ip.magic('timeit -r1 -n1 f("a " + "b")') + _ip.magic('timeit -r1 -n1 f("a " + "b ")') + + +def test_timeit_arguments(): + "Test valid timeit arguments, should not cause SyntaxError (GH #1269)" + _ip.magic("timeit ('#')") + + +def test_timeit_special_syntax(): + "Test %%timeit with yap_ipython special syntax" + @register_line_magic + def lmagic(line): + ip = get_ipython() + ip.user_ns['lmagic_out'] = line + + # line mode test + _ip.run_line_magic('timeit', '-n1 -r1 %lmagic my line') + nt.assert_equal(_ip.user_ns['lmagic_out'], 'my line') + # cell mode test + _ip.run_cell_magic('timeit', '-n1 -r1', '%lmagic my line2') + nt.assert_equal(_ip.user_ns['lmagic_out'], 'my line2') + +def test_timeit_return(): + """ + test whether timeit -o return object + """ + + res = _ip.run_line_magic('timeit','-n10 -r10 -o 1') + assert(res is not None) + +def test_timeit_quiet(): + """ + test quiet option of timeit magic + """ + with tt.AssertNotPrints("loops"): + _ip.run_cell("%timeit -n1 -r1 -q 1") + +def test_timeit_return_quiet(): + with tt.AssertNotPrints("loops"): + res = _ip.run_line_magic('timeit', '-n1 -r1 -q -o 1') + assert (res is not None) + +def test_timeit_invalid_return(): + with nt.assert_raises_regex(SyntaxError, "outside function"): + _ip.run_line_magic('timeit', 'return') + +@dec.skipif(execution.profile is None) +def test_prun_special_syntax(): + "Test %%prun with yap_ipython special syntax" + @register_line_magic + def lmagic(line): + ip = get_ipython() + ip.user_ns['lmagic_out'] = line + + # line mode test + _ip.run_line_magic('prun', '-q %lmagic my line') + nt.assert_equal(_ip.user_ns['lmagic_out'], 'my line') + # cell mode test + _ip.run_cell_magic('prun', '-q', '%lmagic my line2') + nt.assert_equal(_ip.user_ns['lmagic_out'], 'my line2') + +@dec.skipif(execution.profile is None) +def test_prun_quotes(): + "Test that prun does not clobber string escapes (GH #1302)" + _ip.magic(r"prun -q x = '\t'") + nt.assert_equal(_ip.user_ns['x'], '\t') + +def test_extension(): + # Debugging information for failures of this test + print('sys.path:') + for p in sys.path: + print(' ', p) + print('CWD', os.getcwd()) + + nt.assert_raises(ImportError, _ip.magic, "load_ext daft_extension") + daft_path = os.path.join(os.path.dirname(__file__), "daft_extension") + sys.path.insert(0, daft_path) + try: + _ip.user_ns.pop('arq', None) + invalidate_caches() # Clear import caches + _ip.magic("load_ext daft_extension") + nt.assert_equal(_ip.user_ns['arq'], 185) + _ip.magic("unload_ext daft_extension") + assert 'arq' not in _ip.user_ns + finally: + sys.path.remove(daft_path) + + +def test_notebook_export_json(): + _ip = get_ipython() + _ip.history_manager.reset() # Clear any existing history. + cmds = [u"a=1", u"def b():\n return a**2", u"print('noël, été', b())"] + for i, cmd in enumerate(cmds, start=1): + _ip.history_manager.store_inputs(i, cmd) + with TemporaryDirectory() as td: + outfile = os.path.join(td, "nb.ipynb") + _ip.magic("notebook -e %s" % outfile) + + +class TestEnv(TestCase): + + def test_env(self): + env = _ip.magic("env") + self.assertTrue(isinstance(env, dict)) + + def test_env_get_set_simple(self): + env = _ip.magic("env var val1") + self.assertEqual(env, None) + self.assertEqual(os.environ['var'], 'val1') + self.assertEqual(_ip.magic("env var"), 'val1') + env = _ip.magic("env var=val2") + self.assertEqual(env, None) + self.assertEqual(os.environ['var'], 'val2') + + def test_env_get_set_complex(self): + env = _ip.magic("env var 'val1 '' 'val2") + self.assertEqual(env, None) + self.assertEqual(os.environ['var'], "'val1 '' 'val2") + self.assertEqual(_ip.magic("env var"), "'val1 '' 'val2") + env = _ip.magic('env var=val2 val3="val4') + self.assertEqual(env, None) + self.assertEqual(os.environ['var'], 'val2 val3="val4') + + def test_env_set_bad_input(self): + self.assertRaises(UsageError, lambda: _ip.magic("set_env var")) + + def test_env_set_whitespace(self): + self.assertRaises(UsageError, lambda: _ip.magic("env var A=B")) + + +class CellMagicTestCase(TestCase): + + def check_ident(self, magic): + # Manually called, we get the result + out = _ip.run_cell_magic(magic, 'a', 'b') + nt.assert_equal(out, ('a','b')) + # Via run_cell, it goes into the user's namespace via displayhook + _ip.run_cell('%%' + magic +' c\nd') + nt.assert_equal(_ip.user_ns['_'], ('c','d')) + + def test_cell_magic_func_deco(self): + "Cell magic using simple decorator" + @register_cell_magic + def cellm(line, cell): + return line, cell + + self.check_ident('cellm') + + def test_cell_magic_reg(self): + "Cell magic manually registered" + def cellm(line, cell): + return line, cell + + _ip.register_magic_function(cellm, 'cell', 'cellm2') + self.check_ident('cellm2') + + def test_cell_magic_class(self): + "Cell magics declared via a class" + @magics_class + class MyMagics(Magics): + + @cell_magic + def cellm3(self, line, cell): + return line, cell + + _ip.register_magics(MyMagics) + self.check_ident('cellm3') + + def test_cell_magic_class2(self): + "Cell magics declared via a class, #2" + @magics_class + class MyMagics2(Magics): + + @cell_magic('cellm4') + def cellm33(self, line, cell): + return line, cell + + _ip.register_magics(MyMagics2) + self.check_ident('cellm4') + # Check that nothing is registered as 'cellm33' + c33 = _ip.find_cell_magic('cellm33') + nt.assert_equal(c33, None) + +def test_file(): + """Basic %%file""" + ip = get_ipython() + with TemporaryDirectory() as td: + fname = os.path.join(td, 'file1') + ip.run_cell_magic("file", fname, u'\n'.join([ + 'line1', + 'line2', + ])) + with open(fname) as f: + s = f.read() + nt.assert_in('line1\n', s) + nt.assert_in('line2', s) + +def test_file_var_expand(): + """%%file $filename""" + ip = get_ipython() + with TemporaryDirectory() as td: + fname = os.path.join(td, 'file1') + ip.user_ns['filename'] = fname + ip.run_cell_magic("file", '$filename', u'\n'.join([ + 'line1', + 'line2', + ])) + with open(fname) as f: + s = f.read() + nt.assert_in('line1\n', s) + nt.assert_in('line2', s) + +def test_file_unicode(): + """%%file with unicode cell""" + ip = get_ipython() + with TemporaryDirectory() as td: + fname = os.path.join(td, 'file1') + ip.run_cell_magic("file", fname, u'\n'.join([ + u'liné1', + u'liné2', + ])) + with io.open(fname, encoding='utf-8') as f: + s = f.read() + nt.assert_in(u'liné1\n', s) + nt.assert_in(u'liné2', s) + +def test_file_amend(): + """%%file -a amends files""" + ip = get_ipython() + with TemporaryDirectory() as td: + fname = os.path.join(td, 'file2') + ip.run_cell_magic("file", fname, u'\n'.join([ + 'line1', + 'line2', + ])) + ip.run_cell_magic("file", "-a %s" % fname, u'\n'.join([ + 'line3', + 'line4', + ])) + with open(fname) as f: + s = f.read() + nt.assert_in('line1\n', s) + nt.assert_in('line3\n', s) + + +def test_script_config(): + ip = get_ipython() + ip.config.ScriptMagics.script_magics = ['whoda'] + sm = script.ScriptMagics(shell=ip) + nt.assert_in('whoda', sm.magics['cell']) + +@dec.skip_win32 +def test_script_out(): + ip = get_ipython() + ip.run_cell_magic("script", "--out output sh", "echo 'hi'") + nt.assert_equal(ip.user_ns['output'], 'hi\n') + +@dec.skip_win32 +def test_script_err(): + ip = get_ipython() + ip.run_cell_magic("script", "--err error sh", "echo 'hello' >&2") + nt.assert_equal(ip.user_ns['error'], 'hello\n') + +@dec.skip_win32 +def test_script_out_err(): + ip = get_ipython() + ip.run_cell_magic("script", "--out output --err error sh", "echo 'hi'\necho 'hello' >&2") + nt.assert_equal(ip.user_ns['output'], 'hi\n') + nt.assert_equal(ip.user_ns['error'], 'hello\n') + +@dec.skip_win32 +def test_script_bg_out(): + ip = get_ipython() + ip.run_cell_magic("script", "--bg --out output sh", "echo 'hi'") + nt.assert_equal(ip.user_ns['output'].read(), b'hi\n') + +@dec.skip_win32 +def test_script_bg_err(): + ip = get_ipython() + ip.run_cell_magic("script", "--bg --err error sh", "echo 'hello' >&2") + nt.assert_equal(ip.user_ns['error'].read(), b'hello\n') + +@dec.skip_win32 +def test_script_bg_out_err(): + ip = get_ipython() + ip.run_cell_magic("script", "--bg --out output --err error sh", "echo 'hi'\necho 'hello' >&2") + nt.assert_equal(ip.user_ns['output'].read(), b'hi\n') + nt.assert_equal(ip.user_ns['error'].read(), b'hello\n') + +def test_script_defaults(): + ip = get_ipython() + for cmd in ['sh', 'bash', 'perl', 'ruby']: + try: + find_cmd(cmd) + except Exception: + pass + else: + nt.assert_in(cmd, ip.magics_manager.magics['cell']) + + +@magics_class +class FooFoo(Magics): + """class with both %foo and %%foo magics""" + @line_magic('foo') + def line_foo(self, line): + "I am line foo" + pass + + @cell_magic("foo") + def cell_foo(self, line, cell): + "I am cell foo, not line foo" + pass + +def test_line_cell_info(): + """%%foo and %foo magics are distinguishable to inspect""" + ip = get_ipython() + ip.magics_manager.register(FooFoo) + oinfo = ip.object_inspect('foo') + nt.assert_true(oinfo['found']) + nt.assert_true(oinfo['ismagic']) + + oinfo = ip.object_inspect('%%foo') + nt.assert_true(oinfo['found']) + nt.assert_true(oinfo['ismagic']) + nt.assert_equal(oinfo['docstring'], FooFoo.cell_foo.__doc__) + + oinfo = ip.object_inspect('%foo') + nt.assert_true(oinfo['found']) + nt.assert_true(oinfo['ismagic']) + nt.assert_equal(oinfo['docstring'], FooFoo.line_foo.__doc__) + +def test_multiple_magics(): + ip = get_ipython() + foo1 = FooFoo(ip) + foo2 = FooFoo(ip) + mm = ip.magics_manager + mm.register(foo1) + nt.assert_true(mm.magics['line']['foo'].__self__ is foo1) + mm.register(foo2) + nt.assert_true(mm.magics['line']['foo'].__self__ is foo2) + +def test_alias_magic(): + """Test %alias_magic.""" + ip = get_ipython() + mm = ip.magics_manager + + # Basic operation: both cell and line magics are created, if possible. + ip.run_line_magic('alias_magic', 'timeit_alias timeit') + nt.assert_in('timeit_alias', mm.magics['line']) + nt.assert_in('timeit_alias', mm.magics['cell']) + + # --cell is specified, line magic not created. + ip.run_line_magic('alias_magic', '--cell timeit_cell_alias timeit') + nt.assert_not_in('timeit_cell_alias', mm.magics['line']) + nt.assert_in('timeit_cell_alias', mm.magics['cell']) + + # Test that line alias is created successfully. + ip.run_line_magic('alias_magic', '--line env_alias env') + nt.assert_equal(ip.run_line_magic('env', ''), + ip.run_line_magic('env_alias', '')) + + # Test that line alias with parameters passed in is created successfully. + ip.run_line_magic('alias_magic', '--line history_alias history --params ' + shlex.quote('3')) + nt.assert_in('history_alias', mm.magics['line']) + + +def test_save(): + """Test %save.""" + ip = get_ipython() + ip.history_manager.reset() # Clear any existing history. + cmds = [u"a=1", u"def b():\n return a**2", u"print(a, b())"] + for i, cmd in enumerate(cmds, start=1): + ip.history_manager.store_inputs(i, cmd) + with TemporaryDirectory() as tmpdir: + file = os.path.join(tmpdir, "testsave.py") + ip.run_line_magic("save", "%s 1-10" % file) + with open(file) as f: + content = f.read() + nt.assert_equal(content.count(cmds[0]), 1) + nt.assert_in('coding: utf-8', content) + ip.run_line_magic("save", "-a %s 1-10" % file) + with open(file) as f: + content = f.read() + nt.assert_equal(content.count(cmds[0]), 2) + nt.assert_in('coding: utf-8', content) + + +def test_store(): + """Test %store.""" + ip = get_ipython() + ip.run_line_magic('load_ext', 'storemagic') + + # make sure the storage is empty + ip.run_line_magic('store', '-z') + ip.user_ns['var'] = 42 + ip.run_line_magic('store', 'var') + ip.user_ns['var'] = 39 + ip.run_line_magic('store', '-r') + nt.assert_equal(ip.user_ns['var'], 42) + + ip.run_line_magic('store', '-d var') + ip.user_ns['var'] = 39 + ip.run_line_magic('store' , '-r') + nt.assert_equal(ip.user_ns['var'], 39) + + +def _run_edit_test(arg_s, exp_filename=None, + exp_lineno=-1, + exp_contents=None, + exp_is_temp=None): + ip = get_ipython() + M = code.CodeMagics(ip) + last_call = ['',''] + opts,args = M.parse_options(arg_s,'prxn:') + filename, lineno, is_temp = M._find_edit_target(ip, args, opts, last_call) + + if exp_filename is not None: + nt.assert_equal(exp_filename, filename) + if exp_contents is not None: + with io.open(filename, 'r', encoding='utf-8') as f: + contents = f.read() + nt.assert_equal(exp_contents, contents) + if exp_lineno != -1: + nt.assert_equal(exp_lineno, lineno) + if exp_is_temp is not None: + nt.assert_equal(exp_is_temp, is_temp) + + +def test_edit_interactive(): + """%edit on interactively defined objects""" + ip = get_ipython() + n = ip.execution_count + ip.run_cell(u"def foo(): return 1", store_history=True) + + try: + _run_edit_test("foo") + except code.InteractivelyDefined as e: + nt.assert_equal(e.index, n) + else: + raise AssertionError("Should have raised InteractivelyDefined") + + +def test_edit_cell(): + """%edit [cell id]""" + ip = get_ipython() + + ip.run_cell(u"def foo(): return 1", store_history=True) + + # test + _run_edit_test("1", exp_contents=ip.user_ns['In'][1], exp_is_temp=True) + +def test_bookmark(): + ip = get_ipython() + ip.run_line_magic('bookmark', 'bmname') + with tt.AssertPrints('bmname'): + ip.run_line_magic('bookmark', '-l') + ip.run_line_magic('bookmark', '-d bmname') + +def test_ls_magic(): + ip = get_ipython() + json_formatter = ip.display_formatter.formatters['application/json'] + json_formatter.enabled = True + lsmagic = ip.magic('lsmagic') + with warnings.catch_warnings(record=True) as w: + j = json_formatter(lsmagic) + nt.assert_equal(sorted(j), ['cell', 'line']) + nt.assert_equal(w, []) # no warnings + +def test_strip_initial_indent(): + def sii(s): + lines = s.splitlines() + return '\n'.join(code.strip_initial_indent(lines)) + + nt.assert_equal(sii(" a = 1\nb = 2"), "a = 1\nb = 2") + nt.assert_equal(sii(" a\n b\nc"), "a\n b\nc") + nt.assert_equal(sii("a\n b"), "a\n b") + +def test_logging_magic_quiet_from_arg(): + _ip.config.LoggingMagics.quiet = False + lm = logging.LoggingMagics(shell=_ip) + with TemporaryDirectory() as td: + try: + with tt.AssertNotPrints(re.compile("Activating.*")): + lm.logstart('-q {}'.format( + os.path.join(td, "quiet_from_arg.log"))) + finally: + _ip.logger.logstop() + +def test_logging_magic_quiet_from_config(): + _ip.config.LoggingMagics.quiet = True + lm = logging.LoggingMagics(shell=_ip) + with TemporaryDirectory() as td: + try: + with tt.AssertNotPrints(re.compile("Activating.*")): + lm.logstart(os.path.join(td, "quiet_from_config.log")) + finally: + _ip.logger.logstop() + +def test_logging_magic_not_quiet(): + _ip.config.LoggingMagics.quiet = False + lm = logging.LoggingMagics(shell=_ip) + with TemporaryDirectory() as td: + try: + with tt.AssertPrints(re.compile("Activating.*")): + lm.logstart(os.path.join(td, "not_quiet.log")) + finally: + _ip.logger.logstop() + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_magic_arguments.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_magic_arguments.py new file mode 100644 index 000000000..681e109b7 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_magic_arguments.py @@ -0,0 +1,118 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2010-2011, yap_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. +#----------------------------------------------------------------------------- + +import argparse +from nose.tools import assert_equal + +from yap_ipython.core.magic_arguments import (argument, argument_group, kwds, + magic_arguments, parse_argstring, real_name) + + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +def magic_foo1(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo1, args) + + +@magic_arguments() +def magic_foo2(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo2, args) + + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +@argument_group('Group') +@argument('-b', '--bar', help="a grouped argument") +@argument_group('Second Group') +@argument('-z', '--baz', help="another grouped argument") +def magic_foo3(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo3, args) + + +@magic_arguments() +@kwds(argument_default=argparse.SUPPRESS) +@argument('-f', '--foo', help="an argument") +def magic_foo4(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo4, args) + + +@magic_arguments('frobnicate') +@argument('-f', '--foo', help="an argument") +def magic_foo5(self, args): + """ A docstring. + """ + return parse_argstring(magic_foo5, args) + + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +def magic_magic_foo(self, args): + """ A docstring. + """ + return parse_argstring(magic_magic_foo, args) + + +@magic_arguments() +@argument('-f', '--foo', help="an argument") +def foo(self, args): + """ A docstring. + """ + return parse_argstring(foo, args) + + +def test_magic_arguments(): + assert_equal(magic_foo1.__doc__, '::\n\n %foo1 [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') + assert_equal(getattr(magic_foo1, 'argcmd_name', None), None) + assert_equal(real_name(magic_foo1), 'foo1') + assert_equal(magic_foo1(None, ''), argparse.Namespace(foo=None)) + assert hasattr(magic_foo1, 'has_arguments') + + assert_equal(magic_foo2.__doc__, '::\n\n %foo2\n\n A docstring.\n') + assert_equal(getattr(magic_foo2, 'argcmd_name', None), None) + assert_equal(real_name(magic_foo2), 'foo2') + assert_equal(magic_foo2(None, ''), argparse.Namespace()) + assert hasattr(magic_foo2, 'has_arguments') + + assert_equal(magic_foo3.__doc__, '::\n\n %foo3 [-f FOO] [-b BAR] [-z BAZ]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n\nGroup:\n -b BAR, --bar BAR a grouped argument\n\nSecond Group:\n -z BAZ, --baz BAZ another grouped argument\n') + assert_equal(getattr(magic_foo3, 'argcmd_name', None), None) + assert_equal(real_name(magic_foo3), 'foo3') + assert_equal(magic_foo3(None, ''), + argparse.Namespace(bar=None, baz=None, foo=None)) + assert hasattr(magic_foo3, 'has_arguments') + + assert_equal(magic_foo4.__doc__, '::\n\n %foo4 [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') + assert_equal(getattr(magic_foo4, 'argcmd_name', None), None) + assert_equal(real_name(magic_foo4), 'foo4') + assert_equal(magic_foo4(None, ''), argparse.Namespace()) + assert hasattr(magic_foo4, 'has_arguments') + + assert_equal(magic_foo5.__doc__, '::\n\n %frobnicate [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') + assert_equal(getattr(magic_foo5, 'argcmd_name', None), 'frobnicate') + assert_equal(real_name(magic_foo5), 'frobnicate') + assert_equal(magic_foo5(None, ''), argparse.Namespace(foo=None)) + assert hasattr(magic_foo5, 'has_arguments') + + assert_equal(magic_magic_foo.__doc__, '::\n\n %magic_foo [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') + assert_equal(getattr(magic_magic_foo, 'argcmd_name', None), None) + assert_equal(real_name(magic_magic_foo), 'magic_foo') + assert_equal(magic_magic_foo(None, ''), argparse.Namespace(foo=None)) + assert hasattr(magic_magic_foo, 'has_arguments') + + assert_equal(foo.__doc__, '::\n\n %foo [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') + assert_equal(getattr(foo, 'argcmd_name', None), None) + assert_equal(real_name(foo), 'foo') + assert_equal(foo(None, ''), argparse.Namespace(foo=None)) + assert hasattr(foo, 'has_arguments') diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_magic_terminal.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_magic_terminal.py new file mode 100644 index 000000000..e6b0f48da --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_magic_terminal.py @@ -0,0 +1,197 @@ +"""Tests for various magic functions specific to the terminal frontend. + +Needs to be run by nose (to make ipython session available). +""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import sys +from io import StringIO +from unittest import TestCase + +import nose.tools as nt + +from yap_ipython.testing import tools as tt + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- +ip = get_ipython() + +#----------------------------------------------------------------------------- +# Test functions begin +#----------------------------------------------------------------------------- + +def check_cpaste(code, should_fail=False): + """Execute code via 'cpaste' and ensure it was executed, unless + should_fail is set. + """ + ip.user_ns['code_ran'] = False + + src = StringIO() + if not hasattr(src, 'encoding'): + # yap_ipython expects stdin to have an encoding attribute + src.encoding = None + src.write(code) + src.write('\n--\n') + src.seek(0) + + stdin_save = sys.stdin + sys.stdin = src + + try: + context = tt.AssertPrints if should_fail else tt.AssertNotPrints + with context("Traceback (most recent call last)"): + ip.magic('cpaste') + + if not should_fail: + assert ip.user_ns['code_ran'], "%r failed" % code + finally: + sys.stdin = stdin_save + +def test_cpaste(): + """Test cpaste magic""" + + def runf(): + """Marker function: sets a flag when executed. + """ + ip.user_ns['code_ran'] = True + return 'runf' # return string so '+ runf()' doesn't result in success + + tests = {'pass': ["runf()", + "In [1]: runf()", + "In [1]: if 1:\n ...: runf()", + "> > > runf()", + ">>> runf()", + " >>> runf()", + ], + + 'fail': ["1 + runf()", + "++ runf()", + ]} + + ip.user_ns['runf'] = runf + + for code in tests['pass']: + check_cpaste(code) + + for code in tests['fail']: + check_cpaste(code, should_fail=True) + + +class PasteTestCase(TestCase): + """Multiple tests for clipboard pasting""" + + def paste(self, txt, flags='-q'): + """Paste input text, by default in quiet mode""" + ip.hooks.clipboard_get = lambda : txt + ip.magic('paste '+flags) + + def setUp(self): + # Inject fake clipboard hook but save original so we can restore it later + self.original_clip = ip.hooks.clipboard_get + + def tearDown(self): + # Restore original hook + ip.hooks.clipboard_get = self.original_clip + + def test_paste(self): + ip.user_ns.pop('x', None) + self.paste('x = 1') + nt.assert_equal(ip.user_ns['x'], 1) + ip.user_ns.pop('x') + + def test_paste_pyprompt(self): + ip.user_ns.pop('x', None) + self.paste('>>> x=2') + nt.assert_equal(ip.user_ns['x'], 2) + ip.user_ns.pop('x') + + def test_paste_py_multi(self): + self.paste(""" + >>> x = [1,2,3] + >>> y = [] + >>> for i in x: + ... y.append(i**2) + ... + """) + nt.assert_equal(ip.user_ns['x'], [1,2,3]) + nt.assert_equal(ip.user_ns['y'], [1,4,9]) + + def test_paste_py_multi_r(self): + "Now, test that self.paste -r works" + self.test_paste_py_multi() + nt.assert_equal(ip.user_ns.pop('x'), [1,2,3]) + nt.assert_equal(ip.user_ns.pop('y'), [1,4,9]) + nt.assert_false('x' in ip.user_ns) + ip.magic('paste -r') + nt.assert_equal(ip.user_ns['x'], [1,2,3]) + nt.assert_equal(ip.user_ns['y'], [1,4,9]) + + def test_paste_email(self): + "Test pasting of email-quoted contents" + self.paste("""\ + >> def foo(x): + >> return x + 1 + >> xx = foo(1.1)""") + nt.assert_equal(ip.user_ns['xx'], 2.1) + + def test_paste_email2(self): + "Email again; some programs add a space also at each quoting level" + self.paste("""\ + > > def foo(x): + > > return x + 1 + > > yy = foo(2.1) """) + nt.assert_equal(ip.user_ns['yy'], 3.1) + + def test_paste_email_py(self): + "Email quoting of interactive input" + self.paste("""\ + >> >>> def f(x): + >> ... return x+1 + >> ... + >> >>> zz = f(2.5) """) + nt.assert_equal(ip.user_ns['zz'], 3.5) + + def test_paste_echo(self): + "Also test self.paste echoing, by temporarily faking the writer" + w = StringIO() + writer = ip.write + ip.write = w.write + code = """ + a = 100 + b = 200""" + try: + self.paste(code,'') + out = w.getvalue() + finally: + ip.write = writer + nt.assert_equal(ip.user_ns['a'], 100) + nt.assert_equal(ip.user_ns['b'], 200) + nt.assert_equal(out, code+"\n## -- End pasted text --\n") + + def test_paste_leading_commas(self): + "Test multiline strings with leading commas" + tm = ip.magics_manager.registry['TerminalMagics'] + s = '''\ +a = """ +,1,2,3 +"""''' + ip.user_ns.pop('foo', None) + tm.store_or_execute(s, 'foo') + nt.assert_in('foo', ip.user_ns) + + + def test_paste_trailing_question(self): + "Test pasting sources with trailing question marks" + tm = ip.magics_manager.registry['TerminalMagics'] + s = '''\ +def funcfoo(): + if True: #am i true? + return 'fooresult' +''' + ip.user_ns.pop('funcfoo', None) + self.paste(s) + nt.assert_equal(ip.user_ns['funcfoo'](), 'fooresult') diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_oinspect.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_oinspect.py new file mode 100644 index 000000000..dba4c7dbf --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_oinspect.py @@ -0,0 +1,437 @@ +"""Tests for the object inspection functionality. +""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + + +from inspect import Signature, Parameter +import os +import re +import sys + +import nose.tools as nt + +from .. import oinspect +from yap_ipython.core.magic import (Magics, magics_class, line_magic, + cell_magic, line_cell_magic, + register_line_magic, register_cell_magic, + register_line_cell_magic) +from decorator import decorator +from yap_ipython import get_ipython +from yap_ipython.testing.tools import AssertPrints, AssertNotPrints +from yap_ipython.utils.path import compress_user + + +#----------------------------------------------------------------------------- +# Globals and constants +#----------------------------------------------------------------------------- + +inspector = oinspect.Inspector() +ip = get_ipython() + +#----------------------------------------------------------------------------- +# Local utilities +#----------------------------------------------------------------------------- + +# WARNING: since this test checks the line number where a function is +# defined, if any code is inserted above, the following line will need to be +# updated. Do NOT insert any whitespace between the next line and the function +# definition below. +THIS_LINE_NUMBER = 41 # Put here the actual number of this line + +from unittest import TestCase + +class Test(TestCase): + + def test_find_source_lines(self): + self.assertEqual(oinspect.find_source_lines(Test.test_find_source_lines), + THIS_LINE_NUMBER+6) + + +# A couple of utilities to ensure these tests work the same from a source or a +# binary install +def pyfile(fname): + return os.path.normcase(re.sub('.py[co]$', '.py', fname)) + + +def match_pyfiles(f1, f2): + nt.assert_equal(pyfile(f1), pyfile(f2)) + + +def test_find_file(): + match_pyfiles(oinspect.find_file(test_find_file), os.path.abspath(__file__)) + + +def test_find_file_decorated1(): + + @decorator + def noop1(f): + def wrapper(*a, **kw): + return f(*a, **kw) + return wrapper + + @noop1 + def f(x): + "My docstring" + + match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) + nt.assert_equal(f.__doc__, "My docstring") + + +def test_find_file_decorated2(): + + @decorator + def noop2(f, *a, **kw): + return f(*a, **kw) + + @noop2 + @noop2 + @noop2 + def f(x): + "My docstring 2" + + match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) + nt.assert_equal(f.__doc__, "My docstring 2") + + +def test_find_file_magic(): + run = ip.find_line_magic('run') + nt.assert_not_equal(oinspect.find_file(run), None) + + +# A few generic objects we can then inspect in the tests below + +class Call(object): + """This is the class docstring.""" + + def __init__(self, x, y=1): + """This is the constructor docstring.""" + + def __call__(self, *a, **kw): + """This is the call docstring.""" + + def method(self, x, z=2): + """Some method's docstring""" + +class HasSignature(object): + """This is the class docstring.""" + __signature__ = Signature([Parameter('test', Parameter.POSITIONAL_OR_KEYWORD)]) + + def __init__(self, *args): + """This is the init docstring""" + + +class SimpleClass(object): + def method(self, x, z=2): + """Some method's docstring""" + + +class OldStyle: + """An old-style class for testing.""" + pass + + +def f(x, y=2, *a, **kw): + """A simple function.""" + + +def g(y, z=3, *a, **kw): + pass # no docstring + + +@register_line_magic +def lmagic(line): + "A line magic" + + +@register_cell_magic +def cmagic(line, cell): + "A cell magic" + + +@register_line_cell_magic +def lcmagic(line, cell=None): + "A line/cell magic" + + +@magics_class +class SimpleMagics(Magics): + @line_magic + def Clmagic(self, cline): + "A class-based line magic" + + @cell_magic + def Ccmagic(self, cline, ccell): + "A class-based cell magic" + + @line_cell_magic + def Clcmagic(self, cline, ccell=None): + "A class-based line/cell magic" + + +class Awkward(object): + def __getattr__(self, name): + raise Exception(name) + +class NoBoolCall: + """ + callable with `__bool__` raising should still be inspect-able. + """ + + def __call__(self): + """does nothing""" + pass + + def __bool__(self): + """just raise NotImplemented""" + raise NotImplementedError('Must be implemented') + + +class SerialLiar(object): + """Attribute accesses always get another copy of the same class. + + unittest.mock.call does something similar, but it's not ideal for testing + as the failure mode is to eat all your RAM. This gives up after 10k levels. + """ + def __init__(self, max_fibbing_twig, lies_told=0): + if lies_told > 10000: + raise RuntimeError('Nose too long, honesty is the best policy') + self.max_fibbing_twig = max_fibbing_twig + self.lies_told = lies_told + max_fibbing_twig[0] = max(max_fibbing_twig[0], lies_told) + + def __getattr__(self, item): + return SerialLiar(self.max_fibbing_twig, self.lies_told + 1) + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + +def test_info(): + "Check that Inspector.info fills out various fields as expected." + i = inspector.info(Call, oname='Call') + nt.assert_equal(i['type_name'], 'type') + expted_class = str(type(type)) # (Python 3) or + nt.assert_equal(i['base_class'], expted_class) + nt.assert_regex(i['string_form'], "") + fname = __file__ + if fname.endswith(".pyc"): + fname = fname[:-1] + # case-insensitive comparison needed on some filesystems + # e.g. Windows: + nt.assert_equal(i['file'].lower(), compress_user(fname).lower()) + nt.assert_equal(i['definition'], None) + nt.assert_equal(i['docstring'], Call.__doc__) + nt.assert_equal(i['source'], None) + nt.assert_true(i['isclass']) + nt.assert_equal(i['init_definition'], "Call(x, y=1)") + nt.assert_equal(i['init_docstring'], Call.__init__.__doc__) + + i = inspector.info(Call, detail_level=1) + nt.assert_not_equal(i['source'], None) + nt.assert_equal(i['docstring'], None) + + c = Call(1) + c.__doc__ = "Modified instance docstring" + i = inspector.info(c) + nt.assert_equal(i['type_name'], 'Call') + nt.assert_equal(i['docstring'], "Modified instance docstring") + nt.assert_equal(i['class_docstring'], Call.__doc__) + nt.assert_equal(i['init_docstring'], Call.__init__.__doc__) + nt.assert_equal(i['call_docstring'], Call.__call__.__doc__) + +def test_class_signature(): + info = inspector.info(HasSignature, 'HasSignature') + nt.assert_equal(info['init_definition'], "HasSignature(test)") + nt.assert_equal(info['init_docstring'], HasSignature.__init__.__doc__) + +def test_info_awkward(): + # Just test that this doesn't throw an error. + inspector.info(Awkward()) + +def test_bool_raise(): + inspector.info(NoBoolCall()) + +def test_info_serialliar(): + fib_tracker = [0] + inspector.info(SerialLiar(fib_tracker)) + + # Nested attribute access should be cut off at 100 levels deep to avoid + # infinite loops: https://github.com/ipython/ipython/issues/9122 + nt.assert_less(fib_tracker[0], 9000) + +def test_calldef_none(): + # We should ignore __call__ for all of these. + for obj in [f, SimpleClass().method, any, str.upper]: + print(obj) + i = inspector.info(obj) + nt.assert_is(i['call_def'], None) + +def f_kwarg(pos, *, kwonly): + pass + +def test_definition_kwonlyargs(): + i = inspector.info(f_kwarg, oname='f_kwarg') # analysis:ignore + nt.assert_equal(i['definition'], "f_kwarg(pos, *, kwonly)") + +def test_getdoc(): + class A(object): + """standard docstring""" + pass + + class B(object): + """standard docstring""" + def getdoc(self): + return "custom docstring" + + class C(object): + """standard docstring""" + def getdoc(self): + return None + + a = A() + b = B() + c = C() + + nt.assert_equal(oinspect.getdoc(a), "standard docstring") + nt.assert_equal(oinspect.getdoc(b), "custom docstring") + nt.assert_equal(oinspect.getdoc(c), "standard docstring") + + +def test_empty_property_has_no_source(): + i = inspector.info(property(), detail_level=1) + nt.assert_is(i['source'], None) + + +def test_property_sources(): + import posixpath + # A simple adder whose source and signature stays + # the same across Python distributions + def simple_add(a, b): + "Adds two numbers" + return a + b + + class A(object): + @property + def foo(self): + return 'bar' + + foo = foo.setter(lambda self, v: setattr(self, 'bar', v)) + + dname = property(posixpath.dirname) + adder = property(simple_add) + + i = inspector.info(A.foo, detail_level=1) + nt.assert_in('def foo(self):', i['source']) + nt.assert_in('lambda self, v:', i['source']) + + i = inspector.info(A.dname, detail_level=1) + nt.assert_in('def dirname(p)', i['source']) + + i = inspector.info(A.adder, detail_level=1) + nt.assert_in('def simple_add(a, b)', i['source']) + + +def test_property_docstring_is_in_info_for_detail_level_0(): + class A(object): + @property + def foobar(self): + """This is `foobar` property.""" + pass + + ip.user_ns['a_obj'] = A() + nt.assert_equal( + 'This is `foobar` property.', + ip.object_inspect('a_obj.foobar', detail_level=0)['docstring']) + + ip.user_ns['a_cls'] = A + nt.assert_equal( + 'This is `foobar` property.', + ip.object_inspect('a_cls.foobar', detail_level=0)['docstring']) + + +def test_pdef(): + # See gh-1914 + def foo(): pass + inspector.pdef(foo, 'foo') + + +def test_pinfo_nonascii(): + # See gh-1177 + from . import nonascii2 + ip.user_ns['nonascii2'] = nonascii2 + ip._inspect('pinfo', 'nonascii2', detail_level=1) + + +def test_pinfo_docstring_no_source(): + """Docstring should be included with detail_level=1 if there is no source""" + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'str.format', detail_level=0) + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'str.format', detail_level=1) + + +def test_pinfo_no_docstring_if_source(): + """Docstring should not be included with detail_level=1 if source is found""" + def foo(): + """foo has a docstring""" + + ip.user_ns['foo'] = foo + + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'foo', detail_level=0) + with AssertPrints('Source:'): + ip._inspect('pinfo', 'foo', detail_level=1) + with AssertNotPrints('Docstring:'): + ip._inspect('pinfo', 'foo', detail_level=1) + + +def test_pinfo_docstring_if_detail_and_no_source(): + """ Docstring should be displayed if source info not available """ + obj_def = '''class Foo(object): + """ This is a docstring for Foo """ + def bar(self): + """ This is a docstring for Foo.bar """ + pass + ''' + + ip.run_cell(obj_def) + ip.run_cell('foo = Foo()') + + with AssertNotPrints("Source:"): + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'foo', detail_level=0) + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'foo', detail_level=1) + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'foo.bar', detail_level=0) + + with AssertNotPrints('Docstring:'): + with AssertPrints('Source:'): + ip._inspect('pinfo', 'foo.bar', detail_level=1) + + +def test_pinfo_magic(): + with AssertPrints('Docstring:'): + ip._inspect('pinfo', 'lsmagic', detail_level=0) + + with AssertPrints('Source:'): + ip._inspect('pinfo', 'lsmagic', detail_level=1) + + +def test_init_colors(): + # ensure colors are not present in signature info + info = inspector.info(HasSignature) + init_def = info['init_definition'] + nt.assert_not_in('[0m', init_def) + + +def test_builtin_init(): + info = inspector.info(list) + init_def = info['init_definition'] + # Python < 3.4 can't get init definition from builtins, + # but still exercise the inspection in case of error-raising bugs. + if sys.version_info >= (3,4): + nt.assert_is_not_none(init_def) + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_page.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_page.py new file mode 100644 index 000000000..9a57fb764 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_page.py @@ -0,0 +1,20 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2010-2011 The yap_ipython Development Team. +# +# Distributed under the terms of the BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- +import io + +# N.B. For the test suite, page.page is overridden (see yap_ipython.testing.globalipapp) +from yap_ipython.core import page + +def test_detect_screen_size(): + """Simple smoketest for page._detect_screen_size.""" + try: + page._detect_screen_size(True, 25) + except (TypeError, io.UnsupportedOperation): + # This can happen in the test suite, because stdout may not have a + # fileno. + pass diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_paths.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_paths.py new file mode 100644 index 000000000..344fad513 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_paths.py @@ -0,0 +1,200 @@ +import errno +import os +import shutil +import sys +import tempfile +import warnings +from unittest.mock import patch + +import nose.tools as nt +from testpath import modified_env, assert_isdir, assert_isfile + +from yap_ipython import paths +from yap_ipython.testing.decorators import skip_win32 +from yap_ipython.utils.tempdir import TemporaryDirectory + +TMP_TEST_DIR = os.path.realpath(tempfile.mkdtemp()) +HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir") +XDG_TEST_DIR = os.path.join(HOME_TEST_DIR, "xdg_test_dir") +XDG_CACHE_DIR = os.path.join(HOME_TEST_DIR, "xdg_cache_dir") +IP_TEST_DIR = os.path.join(HOME_TEST_DIR,'.ipython') + +def setup(): + """Setup testenvironment for the module: + + - Adds dummy home dir tree + """ + # Do not mask exceptions here. In particular, catching WindowsError is a + # problem because that exception is only defined on Windows... + os.makedirs(IP_TEST_DIR) + os.makedirs(os.path.join(XDG_TEST_DIR, 'ipython')) + os.makedirs(os.path.join(XDG_CACHE_DIR, 'ipython')) + + +def teardown(): + """Teardown testenvironment for the module: + + - Remove dummy home dir tree + """ + # Note: we remove the parent test dir, which is the root of all test + # subdirs we may have created. Use shutil instead of os.removedirs, so + # that non-empty directories are all recursively removed. + shutil.rmtree(TMP_TEST_DIR) + +def patch_get_home_dir(dirpath): + return patch.object(paths, 'get_home_dir', return_value=dirpath) + + +def test_get_ipython_dir_1(): + """test_get_ipython_dir_1, Testcase to see if we can call get_ipython_dir without Exceptions.""" + env_ipdir = os.path.join("someplace", ".ipython") + with patch.object(paths, '_writable_dir', return_value=True), \ + modified_env({'IPYTHONDIR': env_ipdir}): + ipdir = paths.get_ipython_dir() + + nt.assert_equal(ipdir, env_ipdir) + +def test_get_ipython_dir_2(): + """test_get_ipython_dir_2, Testcase to see if we can call get_ipython_dir without Exceptions.""" + with patch_get_home_dir('someplace'), \ + patch.object(paths, 'get_xdg_dir', return_value=None), \ + patch.object(paths, '_writable_dir', return_value=True), \ + patch('os.name', "posix"), \ + modified_env({'IPYTHON_DIR': None, + 'IPYTHONDIR': None, + 'XDG_CONFIG_HOME': None + }): + ipdir = paths.get_ipython_dir() + + nt.assert_equal(ipdir, os.path.join("someplace", ".ipython")) + +def test_get_ipython_dir_3(): + """test_get_ipython_dir_3, move XDG if defined, and .ipython doesn't exist.""" + tmphome = TemporaryDirectory() + try: + with patch_get_home_dir(tmphome.name), \ + patch('os.name', 'posix'), \ + modified_env({ + 'IPYTHON_DIR': None, + 'IPYTHONDIR': None, + 'XDG_CONFIG_HOME': XDG_TEST_DIR, + }), warnings.catch_warnings(record=True) as w: + ipdir = paths.get_ipython_dir() + + nt.assert_equal(ipdir, os.path.join(tmphome.name, ".ipython")) + if sys.platform != 'darwin': + nt.assert_equal(len(w), 1) + nt.assert_in('Moving', str(w[0])) + finally: + tmphome.cleanup() + +def test_get_ipython_dir_4(): + """test_get_ipython_dir_4, warn if XDG and home both exist.""" + with patch_get_home_dir(HOME_TEST_DIR), \ + patch('os.name', 'posix'): + try: + os.mkdir(os.path.join(XDG_TEST_DIR, 'ipython')) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + + with modified_env({ + 'IPYTHON_DIR': None, + 'IPYTHONDIR': None, + 'XDG_CONFIG_HOME': XDG_TEST_DIR, + }), warnings.catch_warnings(record=True) as w: + ipdir = paths.get_ipython_dir() + + nt.assert_equal(ipdir, os.path.join(HOME_TEST_DIR, ".ipython")) + if sys.platform != 'darwin': + nt.assert_equal(len(w), 1) + nt.assert_in('Ignoring', str(w[0])) + +def test_get_ipython_dir_5(): + """test_get_ipython_dir_5, use .ipython if exists and XDG defined, but doesn't exist.""" + with patch_get_home_dir(HOME_TEST_DIR), \ + patch('os.name', 'posix'): + try: + os.rmdir(os.path.join(XDG_TEST_DIR, 'ipython')) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + with modified_env({ + 'IPYTHON_DIR': None, + 'IPYTHONDIR': None, + 'XDG_CONFIG_HOME': XDG_TEST_DIR, + }): + ipdir = paths.get_ipython_dir() + + nt.assert_equal(ipdir, IP_TEST_DIR) + +def test_get_ipython_dir_6(): + """test_get_ipython_dir_6, use home over XDG if defined and neither exist.""" + xdg = os.path.join(HOME_TEST_DIR, 'somexdg') + os.mkdir(xdg) + shutil.rmtree(os.path.join(HOME_TEST_DIR, '.ipython')) + print(paths._writable_dir) + with patch_get_home_dir(HOME_TEST_DIR), \ + patch.object(paths, 'get_xdg_dir', return_value=xdg), \ + patch('os.name', 'posix'), \ + modified_env({ + 'IPYTHON_DIR': None, + 'IPYTHONDIR': None, + 'XDG_CONFIG_HOME': None, + }), warnings.catch_warnings(record=True) as w: + ipdir = paths.get_ipython_dir() + + nt.assert_equal(ipdir, os.path.join(HOME_TEST_DIR, '.ipython')) + nt.assert_equal(len(w), 0) + +def test_get_ipython_dir_7(): + """test_get_ipython_dir_7, test home directory expansion on IPYTHONDIR""" + home_dir = os.path.normpath(os.path.expanduser('~')) + with modified_env({'IPYTHONDIR': os.path.join('~', 'somewhere')}), \ + patch.object(paths, '_writable_dir', return_value=True): + ipdir = paths.get_ipython_dir() + nt.assert_equal(ipdir, os.path.join(home_dir, 'somewhere')) + +@skip_win32 +def test_get_ipython_dir_8(): + """test_get_ipython_dir_8, test / home directory""" + with patch.object(paths, '_writable_dir', lambda path: bool(path)), \ + patch.object(paths, 'get_xdg_dir', return_value=None), \ + modified_env({ + 'IPYTHON_DIR': None, + 'IPYTHONDIR': None, + 'HOME': '/', + }): + nt.assert_equal(paths.get_ipython_dir(), '/.ipython') + + +def test_get_ipython_cache_dir(): + with modified_env({'HOME': HOME_TEST_DIR}): + if os.name == 'posix' and sys.platform != 'darwin': + # test default + os.makedirs(os.path.join(HOME_TEST_DIR, ".cache")) + with modified_env({'XDG_CACHE_HOME': None}): + ipdir = paths.get_ipython_cache_dir() + nt.assert_equal(os.path.join(HOME_TEST_DIR, ".cache", "ipython"), + ipdir) + assert_isdir(ipdir) + + # test env override + with modified_env({"XDG_CACHE_HOME": XDG_CACHE_DIR}): + ipdir = paths.get_ipython_cache_dir() + assert_isdir(ipdir) + nt.assert_equal(ipdir, os.path.join(XDG_CACHE_DIR, "ipython")) + else: + nt.assert_equal(paths.get_ipython_cache_dir(), + paths.get_ipython_dir()) + +def test_get_ipython_package_dir(): + ipdir = paths.get_ipython_package_dir() + assert_isdir(ipdir) + + +def test_get_ipython_module_path(): + ipapp_path = paths.get_ipython_module_path('yap_ipython.terminal.ipapp') + assert_isfile(ipapp_path) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_prefilter.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_prefilter.py new file mode 100644 index 000000000..b24d0c2fa --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_prefilter.py @@ -0,0 +1,119 @@ +"""Tests for input manipulation machinery.""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +import nose.tools as nt + +from yap_ipython.core.prefilter import AutocallChecker +from yap_ipython.testing.globalipapp import get_ipython + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- +ip = get_ipython() + +def test_prefilter(): + """Test user input conversions""" + + # pairs of (raw, expected correct) input + pairs = [ ('2+2','2+2'), + ] + + for raw, correct in pairs: + nt.assert_equal(ip.prefilter(raw), correct) + +def test_prefilter_shadowed(): + def dummy_magic(line): pass + + prev_automagic_state = ip.automagic + ip.automagic = True + ip.autocall = 0 + + try: + # These should not be transformed - they are shadowed by other names + for name in ['if', 'zip', 'get_ipython']: # keyword, builtin, global + ip.register_magic_function(dummy_magic, magic_name=name) + res = ip.prefilter(name+' foo') + nt.assert_equal(res, name+' foo') + del ip.magics_manager.magics['line'][name] + + # These should be transformed + for name in ['fi', 'piz', 'nohtypi_teg']: + ip.register_magic_function(dummy_magic, magic_name=name) + res = ip.prefilter(name+' foo') + nt.assert_not_equal(res, name+' foo') + del ip.magics_manager.magics['line'][name] + + finally: + ip.automagic = prev_automagic_state + +def test_autocall_binops(): + """See https://github.com/ipython/ipython/issues/81""" + ip.magic('autocall 2') + f = lambda x: x + ip.user_ns['f'] = f + try: + nt.assert_equal(ip.prefilter('f 1'),'f(1)') + for t in ['f +1', 'f -1']: + nt.assert_equal(ip.prefilter(t), t) + + # Run tests again with a more permissive exclude_regexp, which will + # allow transformation of binary operations ('f -1' -> 'f(-1)'). + pm = ip.prefilter_manager + ac = AutocallChecker(shell=pm.shell, prefilter_manager=pm, + config=pm.config) + try: + ac.priority = 1 + ac.exclude_regexp = r'^[,&^\|\*/]|^is |^not |^in |^and |^or ' + pm.sort_checkers() + + nt.assert_equal(ip.prefilter('f -1'), 'f(-1)') + nt.assert_equal(ip.prefilter('f +1'), 'f(+1)') + finally: + pm.unregister_checker(ac) + finally: + ip.magic('autocall 0') + del ip.user_ns['f'] + + +def test_issue_114(): + """Check that multiline string literals don't expand as magic + see http://github.com/ipython/ipython/issues/114""" + + template = '"""\n%s\n"""' + # Store the current value of multi_line_specials and turn it off before + # running test, since it could be true (case in which the test doesn't make + # sense, as multiline string literals *will* expand as magic in that case). + msp = ip.prefilter_manager.multi_line_specials + ip.prefilter_manager.multi_line_specials = False + try: + for mgk in ip.magics_manager.lsmagic()['line']: + raw = template % mgk + nt.assert_equal(ip.prefilter(raw), raw) + finally: + ip.prefilter_manager.multi_line_specials = msp + + +def test_prefilter_attribute_errors(): + """Capture exceptions thrown by user objects on attribute access. + + See http://github.com/ipython/ipython/issues/988.""" + + class X(object): + def __getattr__(self, k): + raise ValueError('broken object') + def __call__(self, x): + return x + + # Create a callable broken object + ip.user_ns['x'] = X() + ip.magic('autocall 2') + try: + # Even if x throws an attribute error when looking at its rewrite + # attribute, we should not crash. So the test here is simply making + # the prefilter call and not having an exception. + ip.prefilter('x 1') + finally: + del ip.user_ns['x'] + ip.magic('autocall 0') diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_profile.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_profile.py new file mode 100644 index 000000000..c8578c625 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_profile.py @@ -0,0 +1,163 @@ +# coding: utf-8 +"""Tests for profile-related functions. + +Currently only the startup-dir functionality is tested, but more tests should +be added for: + + * ipython profile create + * ipython profile list + * ipython profile create --parallel + * security dir permissions + +Authors +------- + +* MinRK + +""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import os +import shutil +import sys +import tempfile + +from unittest import TestCase + +import nose.tools as nt + +from yap_ipython.core.profileapp import list_profiles_in, list_bundled_profiles +from yap_ipython.core.profiledir import ProfileDir + +from yap_ipython.testing import decorators as dec +from yap_ipython.testing import tools as tt +from yap_ipython.utils import py3compat +from yap_ipython.utils.process import getoutput +from yap_ipython.utils.tempdir import TemporaryDirectory + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- +TMP_TEST_DIR = tempfile.mkdtemp() +HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir") +IP_TEST_DIR = os.path.join(HOME_TEST_DIR,'.ipython') + +# +# Setup/teardown functions/decorators +# + +def setup(): + """Setup test environment for the module: + + - Adds dummy home dir tree + """ + # Do not mask exceptions here. In particular, catching WindowsError is a + # problem because that exception is only defined on Windows... + os.makedirs(IP_TEST_DIR) + + +def teardown(): + """Teardown test environment for the module: + + - Remove dummy home dir tree + """ + # Note: we remove the parent test dir, which is the root of all test + # subdirs we may have created. Use shutil instead of os.removedirs, so + # that non-empty directories are all recursively removed. + shutil.rmtree(TMP_TEST_DIR) + + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- +def win32_without_pywin32(): + if sys.platform == 'win32': + try: + import pywin32 + except ImportError: + return True + return False + + +class ProfileStartupTest(TestCase): + def setUp(self): + # create profile dir + self.pd = ProfileDir.create_profile_dir_by_name(IP_TEST_DIR, 'test') + self.options = ['--ipython-dir', IP_TEST_DIR, '--profile', 'test'] + self.fname = os.path.join(TMP_TEST_DIR, 'test.py') + + def tearDown(self): + # We must remove this profile right away so its presence doesn't + # confuse other tests. + shutil.rmtree(self.pd.location) + + def init(self, startup_file, startup, test): + # write startup python file + with open(os.path.join(self.pd.startup_dir, startup_file), 'w') as f: + f.write(startup) + # write simple test file, to check that the startup file was run + with open(self.fname, 'w') as f: + f.write(py3compat.doctest_refactor_print(test)) + + def validate(self, output): + tt.ipexec_validate(self.fname, output, '', options=self.options) + + @dec.skipif(win32_without_pywin32(), "Test requires pywin32 on Windows") + def test_startup_py(self): + self.init('00-start.py', 'zzz=123\n', + py3compat.doctest_refactor_print('print zzz\n')) + self.validate('123') + + @dec.skipif(win32_without_pywin32(), "Test requires pywin32 on Windows") + def test_startup_ipy(self): + self.init('00-start.ipy', '%xmode plain\n', '') + self.validate('Exception reporting mode: Plain') + + +def test_list_profiles_in(): + # No need to remove these directories and files, as they will get nuked in + # the module-level teardown. + td = tempfile.mkdtemp(dir=TMP_TEST_DIR) + for name in ('profile_foo', 'profile_hello', 'not_a_profile'): + os.mkdir(os.path.join(td, name)) + if dec.unicode_paths: + os.mkdir(os.path.join(td, u'profile_ünicode')) + + with open(os.path.join(td, 'profile_file'), 'w') as f: + f.write("I am not a profile directory") + profiles = list_profiles_in(td) + + # unicode normalization can turn u'ünicode' into u'u\0308nicode', + # so only check for *nicode, and that creating a ProfileDir from the + # name remains valid + found_unicode = False + for p in list(profiles): + if p.endswith('nicode'): + pd = ProfileDir.find_profile_dir_by_name(td, p) + profiles.remove(p) + found_unicode = True + break + if dec.unicode_paths: + nt.assert_true(found_unicode) + nt.assert_equal(set(profiles), {'foo', 'hello'}) + + +def test_list_bundled_profiles(): + # This variable will need to be updated when a new profile gets bundled + bundled = sorted(list_bundled_profiles()) + nt.assert_equal(bundled, []) + + +def test_profile_create_ipython_dir(): + """ipython profile create respects --ipython-dir""" + with TemporaryDirectory() as td: + getoutput([sys.executable, '-m', 'yap_ipython', 'profile', 'create', + 'foo', '--ipython-dir=%s' % td]) + profile_dir = os.path.join(td, 'profile_foo') + assert os.path.exists(profile_dir) + ipython_config = os.path.join(profile_dir, 'ipython_config.py') + assert os.path.exists(ipython_config) + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_prompts.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_prompts.py new file mode 100644 index 000000000..1cd23365a --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_prompts.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 +"""Tests for prompt generation.""" + +import unittest + +from yap_ipython.core.prompts import LazyEvaluate +from yap_ipython.testing.globalipapp import get_ipython + +ip = get_ipython() + + +class PromptTests(unittest.TestCase): + def test_lazy_eval_unicode(self): + u = u'ünicødé' + lz = LazyEvaluate(lambda : u) + self.assertEqual(str(lz), u) + self.assertEqual(format(lz), u) + + def test_lazy_eval_nonascii_bytes(self): + u = u'ünicødé' + b = u.encode('utf8') + lz = LazyEvaluate(lambda : b) + # unicode(lz) would fail + self.assertEqual(str(lz), str(b)) + self.assertEqual(format(lz), str(b)) + + def test_lazy_eval_float(self): + f = 0.503 + lz = LazyEvaluate(lambda : f) + + self.assertEqual(str(lz), str(f)) + self.assertEqual(format(lz), str(f)) + self.assertEqual(format(lz, '.1'), '0.5') + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_pylabtools.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_pylabtools.py new file mode 100644 index 000000000..beca15934 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_pylabtools.py @@ -0,0 +1,246 @@ +"""Tests for pylab tools module. +""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + + +from io import UnsupportedOperation, BytesIO + +import matplotlib +matplotlib.use('Agg') +from matplotlib.figure import Figure + +from nose import SkipTest +import nose.tools as nt + +from matplotlib import pyplot as plt +import numpy as np + +from yap_ipython.core.getipython import get_ipython +from yap_ipython.core.interactiveshell import InteractiveShell +from yap_ipython.core.display import _PNG, _JPEG +from .. import pylabtools as pt + +from yap_ipython.testing import decorators as dec + + +def test_figure_to_svg(): + # simple empty-figure test + fig = plt.figure() + nt.assert_equal(pt.print_figure(fig, 'svg'), None) + + plt.close('all') + + # simple check for at least svg-looking output + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + ax.plot([1,2,3]) + plt.draw() + svg = pt.print_figure(fig, 'svg')[:100].lower() + nt.assert_in(u'doctype svg', svg) + +def _check_pil_jpeg_bytes(): + """Skip if PIL can't write JPEGs to BytesIO objects""" + # PIL's JPEG plugin can't write to BytesIO objects + # Pillow fixes this + from PIL import Image + buf = BytesIO() + img = Image.new("RGB", (4,4)) + try: + img.save(buf, 'jpeg') + except Exception as e: + ename = e.__class__.__name__ + raise SkipTest("PIL can't write JPEG to BytesIO: %s: %s" % (ename, e)) + +@dec.skip_without("PIL.Image") +def test_figure_to_jpeg(): + _check_pil_jpeg_bytes() + # simple check for at least jpeg-looking output + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + ax.plot([1,2,3]) + plt.draw() + jpeg = pt.print_figure(fig, 'jpeg', quality=50)[:100].lower() + assert jpeg.startswith(_JPEG) + +def test_retina_figure(): + # simple empty-figure test + fig = plt.figure() + nt.assert_equal(pt.retina_figure(fig), None) + plt.close('all') + + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + ax.plot([1,2,3]) + plt.draw() + png, md = pt.retina_figure(fig) + assert png.startswith(_PNG) + nt.assert_in('width', md) + nt.assert_in('height', md) + +_fmt_mime_map = { + 'png': 'image/png', + 'jpeg': 'image/jpeg', + 'pdf': 'application/pdf', + 'retina': 'image/png', + 'svg': 'image/svg+xml', +} + +def test_select_figure_formats_str(): + ip = get_ipython() + for fmt, active_mime in _fmt_mime_map.items(): + pt.select_figure_formats(ip, fmt) + for mime, f in ip.display_formatter.formatters.items(): + if mime == active_mime: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + +def test_select_figure_formats_kwargs(): + ip = get_ipython() + kwargs = dict(quality=10, bbox_inches='tight') + pt.select_figure_formats(ip, 'png', **kwargs) + formatter = ip.display_formatter.formatters['image/png'] + f = formatter.lookup_by_type(Figure) + cell = f.__closure__[0].cell_contents + nt.assert_equal(cell, kwargs) + + # check that the formatter doesn't raise + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + ax.plot([1,2,3]) + plt.draw() + formatter.enabled = True + png = formatter(fig) + assert png.startswith(_PNG) + +def test_select_figure_formats_set(): + ip = get_ipython() + for fmts in [ + {'png', 'svg'}, + ['png'], + ('jpeg', 'pdf', 'retina'), + {'svg'}, + ]: + active_mimes = {_fmt_mime_map[fmt] for fmt in fmts} + pt.select_figure_formats(ip, fmts) + for mime, f in ip.display_formatter.formatters.items(): + if mime in active_mimes: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + +def test_select_figure_formats_bad(): + ip = get_ipython() + with nt.assert_raises(ValueError): + pt.select_figure_formats(ip, 'foo') + with nt.assert_raises(ValueError): + pt.select_figure_formats(ip, {'png', 'foo'}) + with nt.assert_raises(ValueError): + pt.select_figure_formats(ip, ['retina', 'pdf', 'bar', 'bad']) + +def test_import_pylab(): + ns = {} + pt.import_pylab(ns, import_all=False) + nt.assert_true('plt' in ns) + nt.assert_equal(ns['np'], np) + +class TestPylabSwitch(object): + class Shell(InteractiveShell): + def enable_gui(self, gui): + pass + + def setup(self): + import matplotlib + def act_mpl(backend): + matplotlib.rcParams['backend'] = backend + + # Save rcParams since they get modified + self._saved_rcParams = matplotlib.rcParams + self._saved_rcParamsOrig = matplotlib.rcParamsOrig + matplotlib.rcParams = dict(backend='Qt4Agg') + matplotlib.rcParamsOrig = dict(backend='Qt4Agg') + + # Mock out functions + self._save_am = pt.activate_matplotlib + pt.activate_matplotlib = act_mpl + self._save_ip = pt.import_pylab + pt.import_pylab = lambda *a,**kw:None + self._save_cis = pt.configure_inline_support + pt.configure_inline_support = lambda *a,**kw:None + + def teardown(self): + pt.activate_matplotlib = self._save_am + pt.import_pylab = self._save_ip + pt.configure_inline_support = self._save_cis + import matplotlib + matplotlib.rcParams = self._saved_rcParams + matplotlib.rcParamsOrig = self._saved_rcParamsOrig + + def test_qt(self): + s = self.Shell() + gui, backend = s.enable_matplotlib(None) + nt.assert_equal(gui, 'qt') + nt.assert_equal(s.pylab_gui_select, 'qt') + + gui, backend = s.enable_matplotlib('inline') + nt.assert_equal(gui, 'inline') + nt.assert_equal(s.pylab_gui_select, 'qt') + + gui, backend = s.enable_matplotlib('qt') + nt.assert_equal(gui, 'qt') + nt.assert_equal(s.pylab_gui_select, 'qt') + + gui, backend = s.enable_matplotlib('inline') + nt.assert_equal(gui, 'inline') + nt.assert_equal(s.pylab_gui_select, 'qt') + + gui, backend = s.enable_matplotlib() + nt.assert_equal(gui, 'qt') + nt.assert_equal(s.pylab_gui_select, 'qt') + + def test_inline(self): + s = self.Shell() + gui, backend = s.enable_matplotlib('inline') + nt.assert_equal(gui, 'inline') + nt.assert_equal(s.pylab_gui_select, None) + + gui, backend = s.enable_matplotlib('inline') + nt.assert_equal(gui, 'inline') + nt.assert_equal(s.pylab_gui_select, None) + + gui, backend = s.enable_matplotlib('qt') + nt.assert_equal(gui, 'qt') + nt.assert_equal(s.pylab_gui_select, 'qt') + + def test_inline_twice(self): + "Using '%matplotlib inline' twice should not reset formatters" + + ip = self.Shell() + gui, backend = ip.enable_matplotlib('inline') + nt.assert_equal(gui, 'inline') + + fmts = {'png'} + active_mimes = {_fmt_mime_map[fmt] for fmt in fmts} + pt.select_figure_formats(ip, fmts) + + gui, backend = ip.enable_matplotlib('inline') + nt.assert_equal(gui, 'inline') + + for mime, f in ip.display_formatter.formatters.items(): + if mime in active_mimes: + nt.assert_in(Figure, f) + else: + nt.assert_not_in(Figure, f) + + def test_qt_gtk(self): + s = self.Shell() + gui, backend = s.enable_matplotlib('qt') + nt.assert_equal(gui, 'qt') + nt.assert_equal(s.pylab_gui_select, 'qt') + + gui, backend = s.enable_matplotlib('gtk') + nt.assert_equal(gui, 'qt') + nt.assert_equal(s.pylab_gui_select, 'qt') + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_run.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_run.py new file mode 100644 index 000000000..3d56fd609 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_run.py @@ -0,0 +1,560 @@ +# encoding: utf-8 +"""Tests for code execution (%run and related), which is particularly tricky. + +Because of how %run manages namespaces, and the fact that we are trying here to +verify subtle object deletion and reference counting issues, the %run tests +will be kept in this separate file. This makes it easier to aggregate in one +place the tricks needed to handle it; most other magics are much easier to test +and we do so in a common test_magic file. + +Note that any test using `run -i` should make sure to do a `reset` afterwards, +as otherwise it may influence later tests. +""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + + + +import functools +import os +from os.path import join as pjoin +import random +import string +import sys +import textwrap +import unittest +from unittest.mock import patch + +import nose.tools as nt +from nose import SkipTest + +from yap_ipython.testing import decorators as dec +from yap_ipython.testing import tools as tt +from yap_ipython.utils import py3compat +from yap_ipython.utils.io import capture_output +from yap_ipython.utils.tempdir import TemporaryDirectory +from yap_ipython.core import debugger + + +def doctest_refbug(): + """Very nasty problem with references held by multiple runs of a script. + See: https://github.com/ipython/ipython/issues/141 + + In [1]: _ip.clear_main_mod_cache() + # random + + In [2]: %run refbug + + In [3]: call_f() + lowercased: hello + + In [4]: %run refbug + + In [5]: call_f() + lowercased: hello + lowercased: hello + """ + + +def doctest_run_builtins(): + r"""Check that %run doesn't damage __builtins__. + + In [1]: import tempfile + + In [2]: bid1 = id(__builtins__) + + In [3]: fname = tempfile.mkstemp('.py')[1] + + In [3]: f = open(fname,'w') + + In [4]: dummy= f.write('pass\n') + + In [5]: f.flush() + + In [6]: t1 = type(__builtins__) + + In [7]: %run $fname + + In [7]: f.close() + + In [8]: bid2 = id(__builtins__) + + In [9]: t2 = type(__builtins__) + + In [10]: t1 == t2 + Out[10]: True + + In [10]: bid1 == bid2 + Out[10]: True + + In [12]: try: + ....: os.unlink(fname) + ....: except: + ....: pass + ....: + """ + + +def doctest_run_option_parser(): + r"""Test option parser in %run. + + In [1]: %run print_argv.py + [] + + In [2]: %run print_argv.py print*.py + ['print_argv.py'] + + In [3]: %run -G print_argv.py print*.py + ['print*.py'] + + """ + + +@dec.skip_win32 +def doctest_run_option_parser_for_posix(): + r"""Test option parser in %run (Linux/OSX specific). + + You need double quote to escape glob in POSIX systems: + + In [1]: %run print_argv.py print\\*.py + ['print*.py'] + + You can't use quote to escape glob in POSIX systems: + + In [2]: %run print_argv.py 'print*.py' + ['print_argv.py'] + + """ + + +@dec.skip_if_not_win32 +def doctest_run_option_parser_for_windows(): + r"""Test option parser in %run (Windows specific). + + In Windows, you can't escape ``*` `by backslash: + + In [1]: %run print_argv.py print\\*.py + ['print\\*.py'] + + You can use quote to escape glob: + + In [2]: %run print_argv.py 'print*.py' + ['print*.py'] + + """ + + +@py3compat.doctest_refactor_print +def doctest_reset_del(): + """Test that resetting doesn't cause errors in __del__ methods. + + In [2]: class A(object): + ...: def __del__(self): + ...: print str("Hi") + ...: + + In [3]: a = A() + + In [4]: get_ipython().reset() + Hi + + In [5]: 1+1 + Out[5]: 2 + """ + +# For some tests, it will be handy to organize them in a class with a common +# setup that makes a temp file + +class TestMagicRunPass(tt.TempFileMixin): + + def setup(self): + content = "a = [1,2,3]\nb = 1" + self.mktmp(content) + + def run_tmpfile(self): + _ip = get_ipython() + # This fails on Windows if self.tmpfile.name has spaces or "~" in it. + # See below and ticket https://bugs.launchpad.net/bugs/366353 + _ip.magic('run %s' % self.fname) + + def run_tmpfile_p(self): + _ip = get_ipython() + # This fails on Windows if self.tmpfile.name has spaces or "~" in it. + # See below and ticket https://bugs.launchpad.net/bugs/366353 + _ip.magic('run -p %s' % self.fname) + + def test_builtins_id(self): + """Check that %run doesn't damage __builtins__ """ + _ip = get_ipython() + # Test that the id of __builtins__ is not modified by %run + bid1 = id(_ip.user_ns['__builtins__']) + self.run_tmpfile() + bid2 = id(_ip.user_ns['__builtins__']) + nt.assert_equal(bid1, bid2) + + def test_builtins_type(self): + """Check that the type of __builtins__ doesn't change with %run. + + However, the above could pass if __builtins__ was already modified to + be a dict (it should be a module) by a previous use of %run. So we + also check explicitly that it really is a module: + """ + _ip = get_ipython() + self.run_tmpfile() + nt.assert_equal(type(_ip.user_ns['__builtins__']),type(sys)) + + def test_run_profile( self ): + """Test that the option -p, which invokes the profiler, do not + crash by invoking execfile""" + self.run_tmpfile_p() + + def test_run_debug_twice(self): + # https://github.com/ipython/ipython/issues/10028 + _ip = get_ipython() + with tt.fake_input(['c']): + _ip.magic('run -d %s' % self.fname) + with tt.fake_input(['c']): + _ip.magic('run -d %s' % self.fname) + + def test_run_debug_twice_with_breakpoint(self): + """Make a valid python temp file.""" + _ip = get_ipython() + with tt.fake_input(['b 2', 'c', 'c']): + _ip.magic('run -d %s' % self.fname) + + with tt.fake_input(['c']): + with tt.AssertNotPrints('KeyError'): + _ip.magic('run -d %s' % self.fname) + + +class TestMagicRunSimple(tt.TempFileMixin): + + def test_simpledef(self): + """Test that simple class definitions work.""" + src = ("class foo: pass\n" + "def f(): return foo()") + self.mktmp(src) + _ip.magic('run %s' % self.fname) + _ip.run_cell('t = isinstance(f(), foo)') + nt.assert_true(_ip.user_ns['t']) + + def test_obj_del(self): + """Test that object's __del__ methods are called on exit.""" + if sys.platform == 'win32': + try: + import win32api + except ImportError: + raise SkipTest("Test requires pywin32") + src = ("class A(object):\n" + " def __del__(self):\n" + " print 'object A deleted'\n" + "a = A()\n") + self.mktmp(py3compat.doctest_refactor_print(src)) + if dec.module_not_available('sqlite3'): + err = 'WARNING: yap_ipython History requires SQLite, your history will not be saved\n' + else: + err = None + tt.ipexec_validate(self.fname, 'object A deleted', err) + + def test_aggressive_namespace_cleanup(self): + """Test that namespace cleanup is not too aggressive GH-238 + + Returning from another run magic deletes the namespace""" + # see ticket https://github.com/ipython/ipython/issues/238 + + with tt.TempFileMixin() as empty: + empty.mktmp('') + # On Windows, the filename will have \users in it, so we need to use the + # repr so that the \u becomes \\u. + src = ("ip = get_ipython()\n" + "for i in range(5):\n" + " try:\n" + " ip.magic(%r)\n" + " except NameError as e:\n" + " print(i)\n" + " break\n" % ('run ' + empty.fname)) + self.mktmp(src) + _ip.magic('run %s' % self.fname) + _ip.run_cell('ip == get_ipython()') + nt.assert_equal(_ip.user_ns['i'], 4) + + def test_run_second(self): + """Test that running a second file doesn't clobber the first, gh-3547 + """ + self.mktmp("avar = 1\n" + "def afunc():\n" + " return avar\n") + + with tt.TempFileMixin() as empty: + empty.mktmp("") + + _ip.magic('run %s' % self.fname) + _ip.magic('run %s' % empty.fname) + nt.assert_equal(_ip.user_ns['afunc'](), 1) + + @dec.skip_win32 + def test_tclass(self): + mydir = os.path.dirname(__file__) + tc = os.path.join(mydir, 'tclass') + src = ("%%run '%s' C-first\n" + "%%run '%s' C-second\n" + "%%run '%s' C-third\n") % (tc, tc, tc) + self.mktmp(src, '.ipy') + out = """\ +ARGV 1-: ['C-first'] +ARGV 1-: ['C-second'] +tclass.py: deleting object: C-first +ARGV 1-: ['C-third'] +tclass.py: deleting object: C-second +tclass.py: deleting object: C-third +""" + if dec.module_not_available('sqlite3'): + err = 'WARNING: yap_ipython History requires SQLite, your history will not be saved\n' + else: + err = None + tt.ipexec_validate(self.fname, out, err) + + def test_run_i_after_reset(self): + """Check that %run -i still works after %reset (gh-693)""" + src = "yy = zz\n" + self.mktmp(src) + _ip.run_cell("zz = 23") + try: + _ip.magic('run -i %s' % self.fname) + nt.assert_equal(_ip.user_ns['yy'], 23) + finally: + _ip.magic('reset -f') + + _ip.run_cell("zz = 23") + try: + _ip.magic('run -i %s' % self.fname) + nt.assert_equal(_ip.user_ns['yy'], 23) + finally: + _ip.magic('reset -f') + + def test_unicode(self): + """Check that files in odd encodings are accepted.""" + mydir = os.path.dirname(__file__) + na = os.path.join(mydir, 'nonascii.py') + _ip.magic('run "%s"' % na) + nt.assert_equal(_ip.user_ns['u'], u'Ўт№Ф') + + def test_run_py_file_attribute(self): + """Test handling of `__file__` attribute in `%run .py`.""" + src = "t = __file__\n" + self.mktmp(src) + _missing = object() + file1 = _ip.user_ns.get('__file__', _missing) + _ip.magic('run %s' % self.fname) + file2 = _ip.user_ns.get('__file__', _missing) + + # Check that __file__ was equal to the filename in the script's + # namespace. + nt.assert_equal(_ip.user_ns['t'], self.fname) + + # Check that __file__ was not leaked back into user_ns. + nt.assert_equal(file1, file2) + + def test_run_ipy_file_attribute(self): + """Test handling of `__file__` attribute in `%run `.""" + src = "t = __file__\n" + self.mktmp(src, ext='.ipy') + _missing = object() + file1 = _ip.user_ns.get('__file__', _missing) + _ip.magic('run %s' % self.fname) + file2 = _ip.user_ns.get('__file__', _missing) + + # Check that __file__ was equal to the filename in the script's + # namespace. + nt.assert_equal(_ip.user_ns['t'], self.fname) + + # Check that __file__ was not leaked back into user_ns. + nt.assert_equal(file1, file2) + + def test_run_formatting(self): + """ Test that %run -t -N does not raise a TypeError for N > 1.""" + src = "pass" + self.mktmp(src) + _ip.magic('run -t -N 1 %s' % self.fname) + _ip.magic('run -t -N 10 %s' % self.fname) + + def test_ignore_sys_exit(self): + """Test the -e option to ignore sys.exit()""" + src = "import sys; sys.exit(1)" + self.mktmp(src) + with tt.AssertPrints('SystemExit'): + _ip.magic('run %s' % self.fname) + + with tt.AssertNotPrints('SystemExit'): + _ip.magic('run -e %s' % self.fname) + + def test_run_nb(self): + """Test %run notebook.ipynb""" + from nbformat import v4, writes + nb = v4.new_notebook( + cells=[ + v4.new_markdown_cell("The Ultimate Question of Everything"), + v4.new_code_cell("answer=42") + ] + ) + src = writes(nb, version=4) + self.mktmp(src, ext='.ipynb') + + _ip.magic("run %s" % self.fname) + + nt.assert_equal(_ip.user_ns['answer'], 42) + + def test_file_options(self): + src = ('import sys\n' + 'a = " ".join(sys.argv[1:])\n') + self.mktmp(src) + test_opts = '-x 3 --verbose' + _ip.run_line_magic("run", '{0} {1}'.format(self.fname, test_opts)) + nt.assert_equal(_ip.user_ns['a'], test_opts) + + +class TestMagicRunWithPackage(unittest.TestCase): + + def writefile(self, name, content): + path = os.path.join(self.tempdir.name, name) + d = os.path.dirname(path) + if not os.path.isdir(d): + os.makedirs(d) + with open(path, 'w') as f: + f.write(textwrap.dedent(content)) + + def setUp(self): + self.package = package = 'tmp{0}'.format(''.join([random.choice(string.ascii_letters) for i in range(10)])) + """Temporary (probably) valid python package name.""" + + self.value = int(random.random() * 10000) + + self.tempdir = TemporaryDirectory() + self.__orig_cwd = os.getcwd() + sys.path.insert(0, self.tempdir.name) + + self.writefile(os.path.join(package, '__init__.py'), '') + self.writefile(os.path.join(package, 'sub.py'), """ + x = {0!r} + """.format(self.value)) + self.writefile(os.path.join(package, 'relative.py'), """ + from .sub import x + """) + self.writefile(os.path.join(package, 'absolute.py'), """ + from {0}.sub import x + """.format(package)) + self.writefile(os.path.join(package, 'args.py'), """ + import sys + a = " ".join(sys.argv[1:]) + """.format(package)) + + def tearDown(self): + os.chdir(self.__orig_cwd) + sys.path[:] = [p for p in sys.path if p != self.tempdir.name] + self.tempdir.cleanup() + + def check_run_submodule(self, submodule, opts=''): + _ip.user_ns.pop('x', None) + _ip.magic('run {2} -m {0}.{1}'.format(self.package, submodule, opts)) + self.assertEqual(_ip.user_ns['x'], self.value, + 'Variable `x` is not loaded from module `{0}`.' + .format(submodule)) + + def test_run_submodule_with_absolute_import(self): + self.check_run_submodule('absolute') + + def test_run_submodule_with_relative_import(self): + """Run submodule that has a relative import statement (#2727).""" + self.check_run_submodule('relative') + + def test_prun_submodule_with_absolute_import(self): + self.check_run_submodule('absolute', '-p') + + def test_prun_submodule_with_relative_import(self): + self.check_run_submodule('relative', '-p') + + def with_fake_debugger(func): + @functools.wraps(func) + def wrapper(*args, **kwds): + with patch.object(debugger.Pdb, 'run', staticmethod(eval)): + return func(*args, **kwds) + return wrapper + + @with_fake_debugger + def test_debug_run_submodule_with_absolute_import(self): + self.check_run_submodule('absolute', '-d') + + @with_fake_debugger + def test_debug_run_submodule_with_relative_import(self): + self.check_run_submodule('relative', '-d') + + def test_module_options(self): + _ip.user_ns.pop('a', None) + test_opts = '-x abc -m test' + _ip.run_line_magic('run', '-m {0}.args {1}'.format(self.package, test_opts)) + nt.assert_equal(_ip.user_ns['a'], test_opts) + + def test_module_options_with_separator(self): + _ip.user_ns.pop('a', None) + test_opts = '-x abc -m test' + _ip.run_line_magic('run', '-m {0}.args -- {1}'.format(self.package, test_opts)) + nt.assert_equal(_ip.user_ns['a'], test_opts) + +def test_run__name__(): + with TemporaryDirectory() as td: + path = pjoin(td, 'foo.py') + with open(path, 'w') as f: + f.write("q = __name__") + + _ip.user_ns.pop('q', None) + _ip.magic('run {}'.format(path)) + nt.assert_equal(_ip.user_ns.pop('q'), '__main__') + + _ip.magic('run -n {}'.format(path)) + nt.assert_equal(_ip.user_ns.pop('q'), 'foo') + + try: + _ip.magic('run -i -n {}'.format(path)) + nt.assert_equal(_ip.user_ns.pop('q'), 'foo') + finally: + _ip.magic('reset -f') + + +def test_run_tb(): + """Test traceback offset in %run""" + with TemporaryDirectory() as td: + path = pjoin(td, 'foo.py') + with open(path, 'w') as f: + f.write('\n'.join([ + "def foo():", + " return bar()", + "def bar():", + " raise RuntimeError('hello!')", + "foo()", + ])) + with capture_output() as io: + _ip.magic('run {}'.format(path)) + out = io.stdout + nt.assert_not_in("execfile", out) + nt.assert_in("RuntimeError", out) + nt.assert_equal(out.count("---->"), 3) + +@dec.knownfailureif(sys.platform == 'win32', "writes to io.stdout aren't captured on Windows") +def test_script_tb(): + """Test traceback offset in `ipython script.py`""" + with TemporaryDirectory() as td: + path = pjoin(td, 'foo.py') + with open(path, 'w') as f: + f.write('\n'.join([ + "def foo():", + " return bar()", + "def bar():", + " raise RuntimeError('hello!')", + "foo()", + ])) + out, err = tt.ipexec(path) + nt.assert_not_in("execfile", out) + nt.assert_in("RuntimeError", out) + nt.assert_equal(out.count("---->"), 3) + diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_shellapp.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_shellapp.py new file mode 100644 index 000000000..c68cf56f9 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_shellapp.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +"""Tests for shellapp module. + +Authors +------- +* Bradley Froehle +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2012 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +import unittest + +from yap_ipython.testing import decorators as dec +from yap_ipython.testing import tools as tt + +sqlite_err_maybe = dec.module_not_available('sqlite3') +SQLITE_NOT_AVAILABLE_ERROR = ('WARNING: yap_ipython History requires SQLite,' + ' your history will not be saved\n') + +class TestFileToRun(unittest.TestCase, tt.TempFileMixin): + """Test the behavior of the file_to_run parameter.""" + + def test_py_script_file_attribute(self): + """Test that `__file__` is set when running `ipython file.py`""" + src = "print(__file__)\n" + self.mktmp(src) + + err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None + tt.ipexec_validate(self.fname, self.fname, err) + + def test_ipy_script_file_attribute(self): + """Test that `__file__` is set when running `ipython file.ipy`""" + src = "print(__file__)\n" + self.mktmp(src, ext='.ipy') + + err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None + tt.ipexec_validate(self.fname, self.fname, err) + + # The commands option to ipexec_validate doesn't work on Windows, and it + # doesn't seem worth fixing + @dec.skip_win32 + def test_py_script_file_attribute_interactively(self): + """Test that `__file__` is not set after `ipython -i file.py`""" + src = "True\n" + self.mktmp(src) + + out, err = tt.ipexec(self.fname, options=['-i'], + commands=['"__file__" in globals()', 'exit()']) + self.assertIn("False", out) diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_splitinput.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_splitinput.py new file mode 100644 index 000000000..4db015fc0 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_splitinput.py @@ -0,0 +1,38 @@ +# coding: utf-8 +import nose.tools as nt + +from yap_ipython.core.splitinput import split_user_input, LineInfo +from yap_ipython.testing import tools as tt + +tests = [ + ('x=1', ('', '', 'x', '=1')), + ('?', ('', '?', '', '')), + ('??', ('', '??', '', '')), + (' ?', (' ', '?', '', '')), + (' ??', (' ', '??', '', '')), + ('??x', ('', '??', 'x', '')), + ('?x=1', ('', '?', 'x', '=1')), + ('!ls', ('', '!', 'ls', '')), + (' !ls', (' ', '!', 'ls', '')), + ('!!ls', ('', '!!', 'ls', '')), + (' !!ls', (' ', '!!', 'ls', '')), + (',ls', ('', ',', 'ls', '')), + (';ls', ('', ';', 'ls', '')), + (' ;ls', (' ', ';', 'ls', '')), + ('f.g(x)', ('', '', 'f.g', '(x)')), + ('f.g (x)', ('', '', 'f.g', '(x)')), + ('?%hist1', ('', '?', '%hist1', '')), + ('?%%hist2', ('', '?', '%%hist2', '')), + ('??%hist3', ('', '??', '%hist3', '')), + ('??%%hist4', ('', '??', '%%hist4', '')), + ('?x*', ('', '?', 'x*', '')), + ] +tests.append((u"Pérez Fernando", (u'', u'', u'Pérez', u'Fernando'))) + +def test_split_user_input(): + return tt.check_pairs(split_user_input, tests) + +def test_LineInfo(): + """Simple test for LineInfo construction and str()""" + linfo = LineInfo(' %cd /home') + nt.assert_equal(str(linfo), 'LineInfo [ |%|cd|/home]') diff --git a/packages/python/yap_kernel/yap_ipython/core/tests/test_ultratb.py b/packages/python/yap_kernel/yap_ipython/core/tests/test_ultratb.py new file mode 100644 index 000000000..2600ab571 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/core/tests/test_ultratb.py @@ -0,0 +1,400 @@ +# encoding: utf-8 +"""Tests for yap_ipython.core.ultratb +""" +import io +import logging +import sys +import os.path +from textwrap import dedent +import traceback +import unittest +from unittest import mock + +from ..ultratb import ColorTB, VerboseTB, find_recursion + + +from yap_ipython.testing import tools as tt +from yap_ipython.testing.decorators import onlyif_unicode_paths +from yap_ipython.utils.syspathcontext import prepended_to_syspath +from yap_ipython.utils.tempdir import TemporaryDirectory + +ip = get_ipython() + +file_1 = """1 +2 +3 +def f(): + 1/0 +""" + +file_2 = """def f(): + 1/0 +""" + +class ChangedPyFileTest(unittest.TestCase): + def test_changing_py_file(self): + """Traceback produced if the line where the error occurred is missing? + + https://github.com/ipython/ipython/issues/1456 + """ + with TemporaryDirectory() as td: + fname = os.path.join(td, "foo.py") + with open(fname, "w") as f: + f.write(file_1) + + with prepended_to_syspath(td): + ip.run_cell("import foo") + + with tt.AssertPrints("ZeroDivisionError"): + ip.run_cell("foo.f()") + + # Make the file shorter, so the line of the error is missing. + with open(fname, "w") as f: + f.write(file_2) + + # For some reason, this was failing on the *second* call after + # changing the file, so we call f() twice. + with tt.AssertNotPrints("Internal Python error", channel='stderr'): + with tt.AssertPrints("ZeroDivisionError"): + ip.run_cell("foo.f()") + with tt.AssertPrints("ZeroDivisionError"): + ip.run_cell("foo.f()") + +iso_8859_5_file = u'''# coding: iso-8859-5 + +def fail(): + """дбИЖ""" + 1/0 # дбИЖ +''' + +class NonAsciiTest(unittest.TestCase): + @onlyif_unicode_paths + def test_nonascii_path(self): + # Non-ascii directory name as well. + with TemporaryDirectory(suffix=u'é') as td: + fname = os.path.join(td, u"fooé.py") + with open(fname, "w") as f: + f.write(file_1) + + with prepended_to_syspath(td): + ip.run_cell("import foo") + + with tt.AssertPrints("ZeroDivisionError"): + ip.run_cell("foo.f()") + + def test_iso8859_5(self): + with TemporaryDirectory() as td: + fname = os.path.join(td, 'dfghjkl.py') + + with io.open(fname, 'w', encoding='iso-8859-5') as f: + f.write(iso_8859_5_file) + + with prepended_to_syspath(td): + ip.run_cell("from dfghjkl import fail") + + with tt.AssertPrints("ZeroDivisionError"): + with tt.AssertPrints(u'дбИЖ', suppress=False): + ip.run_cell('fail()') + + def test_nonascii_msg(self): + cell = u"raise Exception('é')" + expected = u"Exception('é')" + ip.run_cell("%xmode plain") + with tt.AssertPrints(expected): + ip.run_cell(cell) + + ip.run_cell("%xmode verbose") + with tt.AssertPrints(expected): + ip.run_cell(cell) + + ip.run_cell("%xmode context") + with tt.AssertPrints(expected): + ip.run_cell(cell) + + +class NestedGenExprTestCase(unittest.TestCase): + """ + Regression test for the following issues: + https://github.com/ipython/ipython/issues/8293 + https://github.com/ipython/ipython/issues/8205 + """ + def test_nested_genexpr(self): + code = dedent( + """\ + class SpecificException(Exception): + pass + + def foo(x): + raise SpecificException("Success!") + + sum(sum(foo(x) for _ in [0]) for x in [0]) + """ + ) + with tt.AssertPrints('SpecificException: Success!', suppress=False): + ip.run_cell(code) + + +indentationerror_file = """if True: +zoon() +""" + +class IndentationErrorTest(unittest.TestCase): + def test_indentationerror_shows_line(self): + # See issue gh-2398 + with tt.AssertPrints("IndentationError"): + with tt.AssertPrints("zoon()", suppress=False): + ip.run_cell(indentationerror_file) + + with TemporaryDirectory() as td: + fname = os.path.join(td, "foo.py") + with open(fname, "w") as f: + f.write(indentationerror_file) + + with tt.AssertPrints("IndentationError"): + with tt.AssertPrints("zoon()", suppress=False): + ip.magic('run %s' % fname) + +se_file_1 = """1 +2 +7/ +""" + +se_file_2 = """7/ +""" + +class SyntaxErrorTest(unittest.TestCase): + def test_syntaxerror_without_lineno(self): + with tt.AssertNotPrints("TypeError"): + with tt.AssertPrints("line unknown"): + ip.run_cell("raise SyntaxError()") + + def test_syntaxerror_no_stacktrace_at_compile_time(self): + syntax_error_at_compile_time = """ +def foo(): + .. +""" + with tt.AssertPrints("SyntaxError"): + ip.run_cell(syntax_error_at_compile_time) + + with tt.AssertNotPrints("foo()"): + ip.run_cell(syntax_error_at_compile_time) + + def test_syntaxerror_stacktrace_when_running_compiled_code(self): + syntax_error_at_runtime = """ +def foo(): + eval("..") + +def bar(): + foo() + +bar() +""" + with tt.AssertPrints("SyntaxError"): + ip.run_cell(syntax_error_at_runtime) + # Assert syntax error during runtime generate stacktrace + with tt.AssertPrints(["foo()", "bar()"]): + ip.run_cell(syntax_error_at_runtime) + + def test_changing_py_file(self): + with TemporaryDirectory() as td: + fname = os.path.join(td, "foo.py") + with open(fname, 'w') as f: + f.write(se_file_1) + + with tt.AssertPrints(["7/", "SyntaxError"]): + ip.magic("run " + fname) + + # Modify the file + with open(fname, 'w') as f: + f.write(se_file_2) + + # The SyntaxError should point to the correct line + with tt.AssertPrints(["7/", "SyntaxError"]): + ip.magic("run " + fname) + + def test_non_syntaxerror(self): + # SyntaxTB may be called with an error other than a SyntaxError + # See e.g. gh-4361 + try: + raise ValueError('QWERTY') + except ValueError: + with tt.AssertPrints('QWERTY'): + ip.showsyntaxerror() + + +class Python3ChainedExceptionsTest(unittest.TestCase): + DIRECT_CAUSE_ERROR_CODE = """ +try: + x = 1 + 2 + print(not_defined_here) +except Exception as e: + x += 55 + x - 1 + y = {} + raise KeyError('uh') from e + """ + + EXCEPTION_DURING_HANDLING_CODE = """ +try: + x = 1 + 2 + print(not_defined_here) +except Exception as e: + x += 55 + x - 1 + y = {} + raise KeyError('uh') + """ + + SUPPRESS_CHAINING_CODE = """ +try: + 1/0 +except Exception: + raise ValueError("Yikes") from None + """ + + def test_direct_cause_error(self): + with tt.AssertPrints(["KeyError", "NameError", "direct cause"]): + ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE) + + def test_exception_during_handling_error(self): + with tt.AssertPrints(["KeyError", "NameError", "During handling"]): + ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE) + + def test_suppress_exception_chaining(self): + with tt.AssertNotPrints("ZeroDivisionError"), \ + tt.AssertPrints("ValueError", suppress=False): + ip.run_cell(self.SUPPRESS_CHAINING_CODE) + + +class RecursionTest(unittest.TestCase): + DEFINITIONS = """ +def non_recurs(): + 1/0 + +def r1(): + r1() + +def r3a(): + r3b() + +def r3b(): + r3c() + +def r3c(): + r3a() + +def r3o1(): + r3a() + +def r3o2(): + r3o1() +""" + def setUp(self): + ip.run_cell(self.DEFINITIONS) + + def test_no_recursion(self): + with tt.AssertNotPrints("frames repeated"): + ip.run_cell("non_recurs()") + + def test_recursion_one_frame(self): + with tt.AssertPrints("1 frames repeated"): + ip.run_cell("r1()") + + def test_recursion_three_frames(self): + with tt.AssertPrints("3 frames repeated"): + ip.run_cell("r3o2()") + + def test_find_recursion(self): + captured = [] + def capture_exc(*args, **kwargs): + captured.append(sys.exc_info()) + with mock.patch.object(ip, 'showtraceback', capture_exc): + ip.run_cell("r3o2()") + + self.assertEqual(len(captured), 1) + etype, evalue, tb = captured[0] + self.assertIn("recursion", str(evalue)) + + records = ip.InteractiveTB.get_records(tb, 3, ip.InteractiveTB.tb_offset) + for r in records[:10]: + print(r[1:4]) + + # The outermost frames should be: + # 0: the 'cell' that was running when the exception came up + # 1: r3o2() + # 2: r3o1() + # 3: r3a() + # Then repeating r3b, r3c, r3a + last_unique, repeat_length = find_recursion(etype, evalue, records) + self.assertEqual(last_unique, 2) + self.assertEqual(repeat_length, 3) + + +#---------------------------------------------------------------------------- + +# module testing (minimal) +def test_handlers(): + def spam(c, d_e): + (d, e) = d_e + x = c + d + y = c * d + foo(x, y) + + def foo(a, b, bar=1): + eggs(a, b + bar) + + def eggs(f, g, z=globals()): + h = f + g + i = f - g + return h / i + + buff = io.StringIO() + + buff.write('') + buff.write('*** Before ***') + try: + buff.write(spam(1, (2, 3))) + except: + traceback.print_exc(file=buff) + + handler = ColorTB(ostream=buff) + buff.write('*** ColorTB ***') + try: + buff.write(spam(1, (2, 3))) + except: + handler(*sys.exc_info()) + buff.write('') + + handler = VerboseTB(ostream=buff) + buff.write('*** VerboseTB ***') + try: + buff.write(spam(1, (2, 3))) + except: + handler(*sys.exc_info()) + buff.write('') + + +class TokenizeFailureTest(unittest.TestCase): + """Tests related to https://github.com/ipython/ipython/issues/6864.""" + + def testLogging(self): + message = "An unexpected error occurred while tokenizing input" + cell = 'raise ValueError("""a\nb""")' + + stream = io.StringIO() + handler = logging.StreamHandler(stream) + logger = logging.getLogger() + loglevel = logger.level + logger.addHandler(handler) + self.addCleanup(lambda: logger.removeHandler(handler)) + self.addCleanup(lambda: logger.setLevel(loglevel)) + + logger.setLevel(logging.INFO) + with tt.AssertNotPrints(message): + ip.run_cell(cell) + self.assertNotIn(message, stream.getvalue()) + + logger.setLevel(logging.DEBUG) + with tt.AssertNotPrints(message): + ip.run_cell(cell) + self.assertIn(message, stream.getvalue()) diff --git a/packages/python/yap_kernel/yap_ipython/extensions/tests/__init__.py b/packages/python/yap_kernel/yap_ipython/extensions/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/python/yap_kernel/yap_ipython/extensions/tests/test_autoreload.py b/packages/python/yap_kernel/yap_ipython/extensions/tests/test_autoreload.py new file mode 100644 index 000000000..c258c86f4 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/extensions/tests/test_autoreload.py @@ -0,0 +1,342 @@ +"""Tests for autoreload extension. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012 yap_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 +#----------------------------------------------------------------------------- + +import os +import sys +import tempfile +import textwrap +import shutil +import random +import time +from io import StringIO + +import nose.tools as nt +import yap_ipython.testing.tools as tt + +from yap_ipython.testing.decorators import skipif + +from yap_ipython.extensions.autoreload import AutoreloadMagics +from yap_ipython.core.events import EventManager, pre_run_cell + +#----------------------------------------------------------------------------- +# Test fixture +#----------------------------------------------------------------------------- + +noop = lambda *a, **kw: None + +class FakeShell(object): + + def __init__(self): + self.ns = {} + self.events = EventManager(self, {'pre_run_cell', pre_run_cell}) + self.auto_magics = AutoreloadMagics(shell=self) + self.events.register('pre_run_cell', self.auto_magics.pre_run_cell) + + register_magics = set_hook = noop + + def run_code(self, code): + self.events.trigger('pre_run_cell') + exec(code, self.ns) + self.auto_magics.post_execute_hook() + + def push(self, items): + self.ns.update(items) + + def magic_autoreload(self, parameter): + self.auto_magics.autoreload(parameter) + + def magic_aimport(self, parameter, stream=None): + self.auto_magics.aimport(parameter, stream=stream) + self.auto_magics.post_execute_hook() + + +class Fixture(object): + """Fixture for creating test module files""" + + test_dir = None + old_sys_path = None + filename_chars = "abcdefghijklmopqrstuvwxyz0123456789" + + def setUp(self): + self.test_dir = tempfile.mkdtemp() + self.old_sys_path = list(sys.path) + sys.path.insert(0, self.test_dir) + self.shell = FakeShell() + + def tearDown(self): + shutil.rmtree(self.test_dir) + sys.path = self.old_sys_path + + self.test_dir = None + self.old_sys_path = None + self.shell = None + + def get_module(self): + module_name = "tmpmod_" + "".join(random.sample(self.filename_chars,20)) + if module_name in sys.modules: + del sys.modules[module_name] + file_name = os.path.join(self.test_dir, module_name + ".py") + return module_name, file_name + + def write_file(self, filename, content): + """ + Write a file, and force a timestamp difference of at least one second + + Notes + ----- + Python's .pyc files record the timestamp of their compilation + with a time resolution of one second. + + Therefore, we need to force a timestamp difference between .py + and .pyc, without having the .py file be timestamped in the + future, and without changing the timestamp of the .pyc file + (because that is stored in the file). The only reliable way + to achieve this seems to be to sleep. + """ + + # Sleep one second + eps + time.sleep(1.05) + + # Write + f = open(filename, 'w') + try: + f.write(content) + finally: + f.close() + + def new_module(self, code): + mod_name, mod_fn = self.get_module() + f = open(mod_fn, 'w') + try: + f.write(code) + finally: + f.close() + return mod_name, mod_fn + +#----------------------------------------------------------------------------- +# Test automatic reloading +#----------------------------------------------------------------------------- + +class TestAutoreload(Fixture): + + @skipif(sys.version_info < (3, 6)) + def test_reload_enums(self): + import enum + mod_name, mod_fn = self.new_module(textwrap.dedent(""" + from enum import Enum + class MyEnum(Enum): + A = 'A' + B = 'B' + """)) + self.shell.magic_autoreload("2") + self.shell.magic_aimport(mod_name) + self.write_file(mod_fn, textwrap.dedent(""" + from enum import Enum + class MyEnum(Enum): + A = 'A' + B = 'B' + C = 'C' + """)) + with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): + self.shell.run_code("pass") # trigger another reload + + + def _check_smoketest(self, use_aimport=True): + """ + Functional test for the automatic reloader using either + '%autoreload 1' or '%autoreload 2' + """ + + mod_name, mod_fn = self.new_module(""" +x = 9 + +z = 123 # this item will be deleted + +def foo(y): + return y + 3 + +class Baz(object): + def __init__(self, x): + self.x = x + def bar(self, y): + return self.x + y + @property + def quux(self): + return 42 + def zzz(self): + '''This method will be deleted below''' + return 99 + +class Bar: # old-style class: weakref doesn't work for it on Python < 2.7 + def foo(self): + return 1 +""") + + # + # Import module, and mark for reloading + # + if use_aimport: + self.shell.magic_autoreload("1") + self.shell.magic_aimport(mod_name) + stream = StringIO() + self.shell.magic_aimport("", stream=stream) + nt.assert_in(("Modules to reload:\n%s" % mod_name), stream.getvalue()) + + with nt.assert_raises(ImportError): + self.shell.magic_aimport("tmpmod_as318989e89ds") + else: + self.shell.magic_autoreload("2") + self.shell.run_code("import %s" % mod_name) + stream = StringIO() + self.shell.magic_aimport("", stream=stream) + nt.assert_true("Modules to reload:\nall-except-skipped" in + stream.getvalue()) + nt.assert_in(mod_name, self.shell.ns) + + mod = sys.modules[mod_name] + + # + # Test module contents + # + old_foo = mod.foo + old_obj = mod.Baz(9) + old_obj2 = mod.Bar() + + def check_module_contents(): + nt.assert_equal(mod.x, 9) + nt.assert_equal(mod.z, 123) + + nt.assert_equal(old_foo(0), 3) + nt.assert_equal(mod.foo(0), 3) + + obj = mod.Baz(9) + nt.assert_equal(old_obj.bar(1), 10) + nt.assert_equal(obj.bar(1), 10) + nt.assert_equal(obj.quux, 42) + nt.assert_equal(obj.zzz(), 99) + + obj2 = mod.Bar() + nt.assert_equal(old_obj2.foo(), 1) + nt.assert_equal(obj2.foo(), 1) + + check_module_contents() + + # + # Simulate a failed reload: no reload should occur and exactly + # one error message should be printed + # + self.write_file(mod_fn, """ +a syntax error +""") + + with tt.AssertPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): + self.shell.run_code("pass") # trigger reload + with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): + self.shell.run_code("pass") # trigger another reload + check_module_contents() + + # + # Rewrite module (this time reload should succeed) + # + self.write_file(mod_fn, """ +x = 10 + +def foo(y): + return y + 4 + +class Baz(object): + def __init__(self, x): + self.x = x + def bar(self, y): + return self.x + y + 1 + @property + def quux(self): + return 43 + +class Bar: # old-style class + def foo(self): + return 2 +""") + + def check_module_contents(): + nt.assert_equal(mod.x, 10) + nt.assert_false(hasattr(mod, 'z')) + + nt.assert_equal(old_foo(0), 4) # superreload magic! + nt.assert_equal(mod.foo(0), 4) + + obj = mod.Baz(9) + nt.assert_equal(old_obj.bar(1), 11) # superreload magic! + nt.assert_equal(obj.bar(1), 11) + + nt.assert_equal(old_obj.quux, 43) + nt.assert_equal(obj.quux, 43) + + nt.assert_false(hasattr(old_obj, 'zzz')) + nt.assert_false(hasattr(obj, 'zzz')) + + obj2 = mod.Bar() + nt.assert_equal(old_obj2.foo(), 2) + nt.assert_equal(obj2.foo(), 2) + + self.shell.run_code("pass") # trigger reload + check_module_contents() + + # + # Another failure case: deleted file (shouldn't reload) + # + os.unlink(mod_fn) + + self.shell.run_code("pass") # trigger reload + check_module_contents() + + # + # Disable autoreload and rewrite module: no reload should occur + # + if use_aimport: + self.shell.magic_aimport("-" + mod_name) + stream = StringIO() + self.shell.magic_aimport("", stream=stream) + nt.assert_true(("Modules to skip:\n%s" % mod_name) in + stream.getvalue()) + + # This should succeed, although no such module exists + self.shell.magic_aimport("-tmpmod_as318989e89ds") + else: + self.shell.magic_autoreload("0") + + self.write_file(mod_fn, """ +x = -99 +""") + + self.shell.run_code("pass") # trigger reload + self.shell.run_code("pass") + check_module_contents() + + # + # Re-enable autoreload: reload should now occur + # + if use_aimport: + self.shell.magic_aimport(mod_name) + else: + self.shell.magic_autoreload("") + + self.shell.run_code("pass") # trigger reload + nt.assert_equal(mod.x, -99) + + def test_smoketest_aimport(self): + self._check_smoketest(use_aimport=True) + + def test_smoketest_autoreload(self): + self._check_smoketest(use_aimport=False) diff --git a/packages/python/yap_kernel/yap_ipython/extensions/tests/test_storemagic.py b/packages/python/yap_kernel/yap_ipython/extensions/tests/test_storemagic.py new file mode 100644 index 000000000..373a71692 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/extensions/tests/test_storemagic.py @@ -0,0 +1,50 @@ +import tempfile, os + +from traitlets.config.loader import Config +import nose.tools as nt + +ip = get_ipython() +ip.magic('load_ext storemagic') + +def test_store_restore(): + ip.user_ns['foo'] = 78 + ip.magic('alias bar echo "hello"') + tmpd = tempfile.mkdtemp() + ip.magic('cd ' + tmpd) + ip.magic('store foo') + ip.magic('store bar') + + # Check storing + nt.assert_equal(ip.db['autorestore/foo'], 78) + nt.assert_in('bar', ip.db['stored_aliases']) + + # Remove those items + ip.user_ns.pop('foo', None) + ip.alias_manager.undefine_alias('bar') + ip.magic('cd -') + ip.user_ns['_dh'][:] = [] + + # Check restoring + ip.magic('store -r') + nt.assert_equal(ip.user_ns['foo'], 78) + assert ip.alias_manager.is_alias('bar') + nt.assert_in(os.path.realpath(tmpd), ip.user_ns['_dh']) + + os.rmdir(tmpd) + +def test_autorestore(): + ip.user_ns['foo'] = 95 + ip.magic('store foo') + del ip.user_ns['foo'] + c = Config() + c.StoreMagics.autorestore = False + orig_config = ip.config + try: + ip.config = c + ip.extension_manager.reload_extension('storemagic') + nt.assert_not_in('foo', ip.user_ns) + c.StoreMagics.autorestore = True + ip.extension_manager.reload_extension('storemagic') + nt.assert_equal(ip.user_ns['foo'], 95) + finally: + ip.config = orig_config diff --git a/packages/python/yap_kernel/yap_ipython/external/decorators/__init__.py b/packages/python/yap_kernel/yap_ipython/external/decorators/__init__.py new file mode 100644 index 000000000..dd8f52b71 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/external/decorators/__init__.py @@ -0,0 +1,9 @@ +try: + from numpy.testing.decorators import * + from numpy.testing.noseclasses import KnownFailure +except ImportError: + from ._decorators import * + try: + from ._numpy_testing_noseclasses import KnownFailure + except ImportError: + pass diff --git a/packages/python/yap_kernel/yap_ipython/external/decorators/_decorators.py b/packages/python/yap_kernel/yap_ipython/external/decorators/_decorators.py new file mode 100644 index 000000000..c80cdad30 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/external/decorators/_decorators.py @@ -0,0 +1,143 @@ +""" +Decorators for labeling and modifying behavior of test objects. + +Decorators that merely return a modified version of the original +function object are straightforward. Decorators that return a new +function object need to use +:: + + nose.tools.make_decorator(original_function)(decorator) + +in returning the decorator, in order to preserve meta-data such as +function name, setup and teardown functions and so on - see +``nose.tools`` for more information. + +""" + +# yap_ipython changes: make this work if numpy not available +# Original code: +try: + from ._numpy_testing_noseclasses import KnownFailureTest +except: + pass + +# End yap_ipython changes + + +def skipif(skip_condition, msg=None): + """ + Make function raise SkipTest exception if a given condition is true. + + If the condition is a callable, it is used at runtime to dynamically + make the decision. This is useful for tests that may require costly + imports, to delay the cost until the test suite is actually executed. + + Parameters + ---------- + skip_condition : bool or callable + Flag to determine whether to skip the decorated test. + msg : str, optional + Message to give on raising a SkipTest exception. Default is None. + + Returns + ------- + decorator : function + Decorator which, when applied to a function, causes SkipTest + to be raised when `skip_condition` is True, and the function + to be called normally otherwise. + + Notes + ----- + The decorator itself is decorated with the ``nose.tools.make_decorator`` + function in order to transmit function name, and various other metadata. + + """ + + def skip_decorator(f): + # Local import to avoid a hard nose dependency and only incur the + # import time overhead at actual test-time. + import nose + + # Allow for both boolean or callable skip conditions. + if callable(skip_condition): + skip_val = lambda : skip_condition() + else: + skip_val = lambda : skip_condition + + def get_msg(func,msg=None): + """Skip message with information about function being skipped.""" + if msg is None: + out = 'Test skipped due to test condition' + else: + out = '\n'+msg + + return "Skipping test: %s%s" % (func.__name__,out) + + # We need to define *two* skippers because Python doesn't allow both + # return with value and yield inside the same function. + def skipper_func(*args, **kwargs): + """Skipper for normal test functions.""" + if skip_val(): + raise nose.SkipTest(get_msg(f,msg)) + else: + return f(*args, **kwargs) + + def skipper_gen(*args, **kwargs): + """Skipper for test generators.""" + if skip_val(): + raise nose.SkipTest(get_msg(f,msg)) + else: + for x in f(*args, **kwargs): + yield x + + # Choose the right skipper to use when building the actual decorator. + if nose.util.isgenerator(f): + skipper = skipper_gen + else: + skipper = skipper_func + + return nose.tools.make_decorator(f)(skipper) + + return skip_decorator + +def knownfailureif(fail_condition, msg=None): + """ + Make function raise KnownFailureTest exception if given condition is true. + + Parameters + ---------- + fail_condition : bool + Flag to determine whether to mark the decorated test as a known + failure (if True) or not (if False). + msg : str, optional + Message to give on raising a KnownFailureTest exception. + Default is None. + + Returns + ------- + decorator : function + Decorator, which, when applied to a function, causes KnownFailureTest to + be raised when `fail_condition` is True and the test fails. + + Notes + ----- + The decorator itself is decorated with the ``nose.tools.make_decorator`` + function in order to transmit function name, and various other metadata. + + """ + if msg is None: + msg = 'Test skipped due to known failure' + + def knownfail_decorator(f): + # Local import to avoid a hard nose dependency and only incur the + # import time overhead at actual test-time. + import nose + + def knownfailer(*args, **kwargs): + if fail_condition: + raise KnownFailureTest(msg) + else: + return f(*args, **kwargs) + return nose.tools.make_decorator(f)(knownfailer) + + return knownfail_decorator diff --git a/packages/python/yap_kernel/yap_ipython/external/decorators/_numpy_testing_noseclasses.py b/packages/python/yap_kernel/yap_ipython/external/decorators/_numpy_testing_noseclasses.py new file mode 100644 index 000000000..ea6935a42 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/external/decorators/_numpy_testing_noseclasses.py @@ -0,0 +1,41 @@ +# yap_ipython: modified copy of numpy.testing.noseclasses, so +# yap_ipython.external._decorators works without numpy being installed. + +# These classes implement a "known failure" error class. + +import os + +from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin + +class KnownFailureTest(Exception): + '''Raise this exception to mark a test as a known failing test.''' + pass + + +class KnownFailure(ErrorClassPlugin): + '''Plugin that installs a KNOWNFAIL error class for the + KnownFailureClass exception. When KnownFailureTest is raised, + the exception will be logged in the knownfail attribute of the + result, 'K' or 'KNOWNFAIL' (verbose) will be output, and the + exception will not be counted as an error or failure.''' + enabled = True + knownfail = ErrorClass(KnownFailureTest, + label='KNOWNFAIL', + isfailure=False) + + def options(self, parser, env=os.environ): + env_opt = 'NOSE_WITHOUT_KNOWNFAIL' + parser.add_option('--no-knownfail', action='store_true', + dest='noKnownFail', default=env.get(env_opt, False), + help='Disable special handling of KnownFailureTest ' + 'exceptions') + + def configure(self, options, conf): + if not self.can_configure: + return + self.conf = conf + disable = getattr(options, 'noKnownFail', False) + if disable: + self.enabled = False + + diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/__init__.py b/packages/python/yap_kernel/yap_ipython/lib/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_backgroundjobs.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_backgroundjobs.py new file mode 100644 index 000000000..aa8efd0ed --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_backgroundjobs.py @@ -0,0 +1,88 @@ +"""Tests for pylab tools module. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2011, the yap_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 imports +import time + +# Third-party imports +import nose.tools as nt + +# Our own imports +from yap_ipython.lib import backgroundjobs as bg + +#----------------------------------------------------------------------------- +# Globals and constants +#----------------------------------------------------------------------------- +t_short = 0.0001 # very short interval to wait on jobs + +#----------------------------------------------------------------------------- +# Local utilities +#----------------------------------------------------------------------------- +def sleeper(interval=t_short, *a, **kw): + args = dict(interval=interval, + other_args=a, + kw_args=kw) + time.sleep(interval) + return args + +def crasher(interval=t_short, *a, **kw): + time.sleep(interval) + raise Exception("Dead job with interval %s" % interval) + +#----------------------------------------------------------------------------- +# Classes and functions +#----------------------------------------------------------------------------- + +def test_result(): + """Test job submission and result retrieval""" + jobs = bg.BackgroundJobManager() + j = jobs.new(sleeper) + j.join() + nt.assert_equal(j.result['interval'], t_short) + + +def test_flush(): + """Test job control""" + jobs = bg.BackgroundJobManager() + j = jobs.new(sleeper) + j.join() + nt.assert_equal(len(jobs.completed), 1) + nt.assert_equal(len(jobs.dead), 0) + jobs.flush() + nt.assert_equal(len(jobs.completed), 0) + + +def test_dead(): + """Test control of dead jobs""" + jobs = bg.BackgroundJobManager() + j = jobs.new(crasher) + j.join() + nt.assert_equal(len(jobs.completed), 0) + nt.assert_equal(len(jobs.dead), 1) + jobs.flush() + nt.assert_equal(len(jobs.dead), 0) + + +def test_longer(): + """Test control of longer-running jobs""" + jobs = bg.BackgroundJobManager() + # Sleep for long enough for the following two checks to still report the + # job as running, but not so long that it makes the test suite noticeably + # slower. + j = jobs.new(sleeper, 0.1) + nt.assert_equal(len(jobs.running), 1) + nt.assert_equal(len(jobs.completed), 0) + j.join() + nt.assert_equal(len(jobs.running), 0) + nt.assert_equal(len(jobs.completed), 1) diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_clipboard.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_clipboard.py new file mode 100644 index 000000000..a7f65c3f7 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_clipboard.py @@ -0,0 +1,21 @@ +import nose.tools as nt + +from yap_ipython.core.error import TryNext +from yap_ipython.lib.clipboard import ClipboardEmpty +from yap_ipython.testing.decorators import skip_if_no_x11 + +@skip_if_no_x11 +def test_clipboard_get(): + # Smoketest for clipboard access - we can't easily guarantee that the + # clipboard is accessible and has something on it, but this tries to + # exercise the relevant code anyway. + try: + a = get_ipython().hooks.clipboard_get() + except ClipboardEmpty: + # Nothing in clipboard to get + pass + except TryNext: + # No clipboard access API available + pass + else: + nt.assert_is_instance(a, str) diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_deepreload.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_deepreload.py new file mode 100644 index 000000000..2d9e2a945 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_deepreload.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""Test suite for the deepreload module.""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import os + +import nose.tools as nt + +from yap_ipython.utils.syspathcontext import prepended_to_syspath +from yap_ipython.utils.tempdir import TemporaryDirectory +from yap_ipython.lib.deepreload import reload as dreload + +def test_deepreload(): + "Test that dreload does deep reloads and skips excluded modules." + with TemporaryDirectory() as tmpdir: + with prepended_to_syspath(tmpdir): + with open(os.path.join(tmpdir, 'A.py'), 'w') as f: + f.write("class Object(object):\n pass\n") + with open(os.path.join(tmpdir, 'B.py'), 'w') as f: + f.write("import A\n") + import A + import B + + # Test that A is not reloaded. + obj = A.Object() + dreload(B, exclude=['A']) + nt.assert_true(isinstance(obj, A.Object)) + + # Test that A is reloaded. + obj = A.Object() + dreload(B) + nt.assert_false(isinstance(obj, A.Object)) diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_display.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_display.py new file mode 100644 index 000000000..c3db84aff --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_display.py @@ -0,0 +1,177 @@ +"""Tests for yap_ipython.lib.display. + +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2012, the yap_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 +#----------------------------------------------------------------------------- +from tempfile import NamedTemporaryFile, mkdtemp +from os.path import split, join as pjoin, dirname + +# Third-party imports +import nose.tools as nt + +# Our own imports +from yap_ipython.lib import display +from yap_ipython.testing.decorators import skipif_not_numpy + +#----------------------------------------------------------------------------- +# Classes and functions +#----------------------------------------------------------------------------- + +#-------------------------- +# FileLink tests +#-------------------------- + +def test_instantiation_FileLink(): + """FileLink: Test class can be instantiated""" + fl = display.FileLink('example.txt') + +def test_warning_on_non_existant_path_FileLink(): + """FileLink: Calling _repr_html_ on non-existant files returns a warning + """ + fl = display.FileLink('example.txt') + nt.assert_true(fl._repr_html_().startswith('Path (example.txt)')) + +def test_existing_path_FileLink(): + """FileLink: Calling _repr_html_ functions as expected on existing filepath + """ + tf = NamedTemporaryFile() + fl = display.FileLink(tf.name) + actual = fl._repr_html_() + expected = "%s
" % (tf.name,tf.name) + nt.assert_equal(actual,expected) + +def test_existing_path_FileLink_repr(): + """FileLink: Calling repr() functions as expected on existing filepath + """ + tf = NamedTemporaryFile() + fl = display.FileLink(tf.name) + actual = repr(fl) + expected = tf.name + nt.assert_equal(actual,expected) + +def test_error_on_directory_to_FileLink(): + """FileLink: Raises error when passed directory + """ + td = mkdtemp() + nt.assert_raises(ValueError,display.FileLink,td) + +#-------------------------- +# FileLinks tests +#-------------------------- + +def test_instantiation_FileLinks(): + """FileLinks: Test class can be instantiated + """ + fls = display.FileLinks('example') + +def test_warning_on_non_existant_path_FileLinks(): + """FileLinks: Calling _repr_html_ on non-existant files returns a warning + """ + fls = display.FileLinks('example') + nt.assert_true(fls._repr_html_().startswith('Path (example)')) + +def test_existing_path_FileLinks(): + """FileLinks: Calling _repr_html_ functions as expected on existing dir + """ + td = mkdtemp() + tf1 = NamedTemporaryFile(dir=td) + tf2 = NamedTemporaryFile(dir=td) + fl = display.FileLinks(td) + actual = fl._repr_html_() + actual = actual.split('\n') + actual.sort() + # the links should always have forward slashes, even on windows, so replace + # backslashes with forward slashes here + expected = ["%s/
" % td, + "  %s
" %\ + (tf2.name.replace("\\","/"),split(tf2.name)[1]), + "  %s
" %\ + (tf1.name.replace("\\","/"),split(tf1.name)[1])] + expected.sort() + # We compare the sorted list of links here as that's more reliable + nt.assert_equal(actual,expected) + +def test_existing_path_FileLinks_alt_formatter(): + """FileLinks: Calling _repr_html_ functions as expected w/ an alt formatter + """ + td = mkdtemp() + tf1 = NamedTemporaryFile(dir=td) + tf2 = NamedTemporaryFile(dir=td) + def fake_formatter(dirname,fnames,included_suffixes): + return ["hello","world"] + fl = display.FileLinks(td,notebook_display_formatter=fake_formatter) + actual = fl._repr_html_() + actual = actual.split('\n') + actual.sort() + expected = ["hello","world"] + expected.sort() + # We compare the sorted list of links here as that's more reliable + nt.assert_equal(actual,expected) + +def test_existing_path_FileLinks_repr(): + """FileLinks: Calling repr() functions as expected on existing directory """ + td = mkdtemp() + tf1 = NamedTemporaryFile(dir=td) + tf2 = NamedTemporaryFile(dir=td) + fl = display.FileLinks(td) + actual = repr(fl) + actual = actual.split('\n') + actual.sort() + expected = ['%s/' % td, ' %s' % split(tf1.name)[1],' %s' % split(tf2.name)[1]] + expected.sort() + # We compare the sorted list of links here as that's more reliable + nt.assert_equal(actual,expected) + +def test_existing_path_FileLinks_repr_alt_formatter(): + """FileLinks: Calling repr() functions as expected w/ alt formatter + """ + td = mkdtemp() + tf1 = NamedTemporaryFile(dir=td) + tf2 = NamedTemporaryFile(dir=td) + def fake_formatter(dirname,fnames,included_suffixes): + return ["hello","world"] + fl = display.FileLinks(td,terminal_display_formatter=fake_formatter) + actual = repr(fl) + actual = actual.split('\n') + actual.sort() + expected = ["hello","world"] + expected.sort() + # We compare the sorted list of links here as that's more reliable + nt.assert_equal(actual,expected) + +def test_error_on_file_to_FileLinks(): + """FileLinks: Raises error when passed file + """ + td = mkdtemp() + tf1 = NamedTemporaryFile(dir=td) + nt.assert_raises(ValueError,display.FileLinks,tf1.name) + +def test_recursive_FileLinks(): + """FileLinks: Does not recurse when recursive=False + """ + td = mkdtemp() + tf = NamedTemporaryFile(dir=td) + subtd = mkdtemp(dir=td) + subtf = NamedTemporaryFile(dir=subtd) + fl = display.FileLinks(td) + actual = str(fl) + actual = actual.split('\n') + nt.assert_equal(len(actual), 4, actual) + fl = display.FileLinks(td, recursive=False) + actual = str(fl) + actual = actual.split('\n') + nt.assert_equal(len(actual), 2, actual) + +@skipif_not_numpy +def test_audio_from_file(): + path = pjoin(dirname(__file__), 'test.wav') + display.Audio(filename=path) diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_editorhooks.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_editorhooks.py new file mode 100644 index 000000000..28c15e79d --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_editorhooks.py @@ -0,0 +1,34 @@ +"""Test installing editor hooks""" +import sys +from unittest import mock + +import nose.tools as nt + +from yap_ipython import get_ipython +from yap_ipython.lib import editorhooks + +def test_install_editor(): + called = [] + def fake_popen(*args, **kwargs): + called.append({ + 'args': args, + 'kwargs': kwargs, + }) + return mock.MagicMock(**{'wait.return_value': 0}) + editorhooks.install_editor('foo -l {line} -f {filename}', wait=False) + + with mock.patch('subprocess.Popen', fake_popen): + get_ipython().hooks.editor('the file', 64) + + nt.assert_equal(len(called), 1) + args = called[0]['args'] + kwargs = called[0]['kwargs'] + + nt.assert_equal(kwargs, {'shell': True}) + + if sys.platform.startswith('win'): + expected = ['foo', '-l', '64', '-f', 'the file'] + else: + expected = "foo -l 64 -f 'the file'" + cmd = args[0] + nt.assert_equal(cmd, expected) diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_imports.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_imports.py new file mode 100644 index 000000000..8b0f42a88 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_imports.py @@ -0,0 +1,11 @@ +# encoding: utf-8 +from yap_ipython.testing import decorators as dec + +def test_import_backgroundjobs(): + from yap_ipython.lib import backgroundjobs + +def test_import_deepreload(): + from yap_ipython.lib import deepreload + +def test_import_demo(): + from yap_ipython.lib import demo diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_latextools.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_latextools.py new file mode 100644 index 000000000..2cf06fb04 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_latextools.py @@ -0,0 +1,134 @@ +# encoding: utf-8 +"""Tests for yap_ipython.utils.path.py""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. +from unittest.mock import patch +import nose.tools as nt + +from yap_ipython.lib import latextools +from yap_ipython.testing.decorators import onlyif_cmds_exist, skipif_not_matplotlib +from yap_ipython.utils.process import FindCmdError + + +def test_latex_to_png_dvipng_fails_when_no_cmd(): + """ + `latex_to_png_dvipng` should return None when there is no required command + """ + for command in ['latex', 'dvipng']: + yield (check_latex_to_png_dvipng_fails_when_no_cmd, command) + + +def check_latex_to_png_dvipng_fails_when_no_cmd(command): + def mock_find_cmd(arg): + if arg == command: + raise FindCmdError + + with patch.object(latextools, "find_cmd", mock_find_cmd): + nt.assert_equal(latextools.latex_to_png_dvipng("whatever", True), + None) + + +@onlyif_cmds_exist('latex', 'dvipng') +def test_latex_to_png_dvipng_runs(): + """ + Test that latex_to_png_dvipng just runs without error. + """ + def mock_kpsewhich(filename): + nt.assert_equal(filename, "breqn.sty") + return None + + for (s, wrap) in [(u"$$x^2$$", False), (u"x^2", True)]: + yield (latextools.latex_to_png_dvipng, s, wrap) + + with patch.object(latextools, "kpsewhich", mock_kpsewhich): + yield (latextools.latex_to_png_dvipng, s, wrap) + +@skipif_not_matplotlib +def test_latex_to_png_mpl_runs(): + """ + Test that latex_to_png_mpl just runs without error. + """ + def mock_kpsewhich(filename): + nt.assert_equal(filename, "breqn.sty") + return None + + for (s, wrap) in [("$x^2$", False), ("x^2", True)]: + yield (latextools.latex_to_png_mpl, s, wrap) + + with patch.object(latextools, "kpsewhich", mock_kpsewhich): + yield (latextools.latex_to_png_mpl, s, wrap) + +@skipif_not_matplotlib +def test_latex_to_html(): + img = latextools.latex_to_html("$x^2$") + nt.assert_in("data:image/png;base64,iVBOR", img) + + +def test_genelatex_no_wrap(): + """ + Test genelatex with wrap=False. + """ + def mock_kpsewhich(filename): + assert False, ("kpsewhich should not be called " + "(called with {0})".format(filename)) + + with patch.object(latextools, "kpsewhich", mock_kpsewhich): + nt.assert_equal( + '\n'.join(latextools.genelatex("body text", False)), + r'''\documentclass{article} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{amssymb} +\usepackage{bm} +\pagestyle{empty} +\begin{document} +body text +\end{document}''') + + +def test_genelatex_wrap_with_breqn(): + """ + Test genelatex with wrap=True for the case breqn.sty is installed. + """ + def mock_kpsewhich(filename): + nt.assert_equal(filename, "breqn.sty") + return "path/to/breqn.sty" + + with patch.object(latextools, "kpsewhich", mock_kpsewhich): + nt.assert_equal( + '\n'.join(latextools.genelatex("x^2", True)), + r'''\documentclass{article} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{amssymb} +\usepackage{bm} +\usepackage{breqn} +\pagestyle{empty} +\begin{document} +\begin{dmath*} +x^2 +\end{dmath*} +\end{document}''') + + +def test_genelatex_wrap_without_breqn(): + """ + Test genelatex with wrap=True for the case breqn.sty is not installed. + """ + def mock_kpsewhich(filename): + nt.assert_equal(filename, "breqn.sty") + return None + + with patch.object(latextools, "kpsewhich", mock_kpsewhich): + nt.assert_equal( + '\n'.join(latextools.genelatex("x^2", True)), + r'''\documentclass{article} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{amssymb} +\usepackage{bm} +\pagestyle{empty} +\begin{document} +$$x^2$$ +\end{document}''') diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_lexers.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_lexers.py new file mode 100644 index 000000000..aca11cc08 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_lexers.py @@ -0,0 +1,129 @@ +"""Test lexers module""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +from unittest import TestCase +from pygments.token import Token +from pygments.lexers import BashLexer + +from .. import lexers + + +class TestLexers(TestCase): + """Collection of lexers tests""" + def setUp(self): + self.lexer = lexers.IPythonLexer() + self.bash_lexer = BashLexer() + + def testIPythonLexer(self): + fragment = '!echo $HOME\n' + tokens = [ + (Token.Operator, '!'), + ] + tokens.extend(self.bash_lexer.get_tokens(fragment[1:])) + self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) + + fragment_2 = '!' + fragment + tokens_2 = [ + (Token.Operator, '!!'), + ] + tokens[1:] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = '\t %%!\n' + fragment[1:] + tokens_2 = [ + (Token.Text, '\t '), + (Token.Operator, '%%!'), + (Token.Text, '\n'), + ] + tokens[1:] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = 'x = ' + fragment + tokens_2 = [ + (Token.Name, 'x'), + (Token.Text, ' '), + (Token.Operator, '='), + (Token.Text, ' '), + ] + tokens + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = 'x, = ' + fragment + tokens_2 = [ + (Token.Name, 'x'), + (Token.Punctuation, ','), + (Token.Text, ' '), + (Token.Operator, '='), + (Token.Text, ' '), + ] + tokens + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = 'x, = %sx ' + fragment[1:] + tokens_2 = [ + (Token.Name, 'x'), + (Token.Punctuation, ','), + (Token.Text, ' '), + (Token.Operator, '='), + (Token.Text, ' '), + (Token.Operator, '%'), + (Token.Keyword, 'sx'), + (Token.Text, ' '), + ] + tokens[1:] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = 'f = %R function () {}\n' + tokens_2 = [ + (Token.Name, 'f'), + (Token.Text, ' '), + (Token.Operator, '='), + (Token.Text, ' '), + (Token.Operator, '%'), + (Token.Keyword, 'R'), + (Token.Text, ' function () {}\n'), + ] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = '\t%%xyz\n$foo\n' + tokens_2 = [ + (Token.Text, '\t'), + (Token.Operator, '%%'), + (Token.Keyword, 'xyz'), + (Token.Text, '\n$foo\n'), + ] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = '%system?\n' + tokens_2 = [ + (Token.Operator, '%'), + (Token.Keyword, 'system'), + (Token.Operator, '?'), + (Token.Text, '\n'), + ] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = 'x != y\n' + tokens_2 = [ + (Token.Name, 'x'), + (Token.Text, ' '), + (Token.Operator, '!='), + (Token.Text, ' '), + (Token.Name, 'y'), + (Token.Text, '\n'), + ] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment_2 = ' ?math.sin\n' + tokens_2 = [ + (Token.Text, ' '), + (Token.Operator, '?'), + (Token.Text, 'math.sin'), + (Token.Text, '\n'), + ] + self.assertEqual(tokens_2, list(self.lexer.get_tokens(fragment_2))) + + fragment = ' *int*?\n' + tokens = [ + (Token.Text, ' *int*'), + (Token.Operator, '?'), + (Token.Text, '\n'), + ] + self.assertEqual(tokens, list(self.lexer.get_tokens(fragment))) diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_pretty.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_pretty.py new file mode 100644 index 000000000..85ea92501 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_pretty.py @@ -0,0 +1,423 @@ +# coding: utf-8 +"""Tests for yap_ipython.lib.pretty.""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + + +from collections import Counter, defaultdict, deque, OrderedDict +import types +import string +import unittest + +import nose.tools as nt + +from yap_ipython.lib import pretty +from yap_ipython.testing.decorators import skip_without + +from io import StringIO + + +class MyList(object): + def __init__(self, content): + self.content = content + def _repr_pretty_(self, p, cycle): + if cycle: + p.text("MyList(...)") + else: + with p.group(3, "MyList(", ")"): + for (i, child) in enumerate(self.content): + if i: + p.text(",") + p.breakable() + else: + p.breakable("") + p.pretty(child) + + +class MyDict(dict): + def _repr_pretty_(self, p, cycle): + p.text("MyDict(...)") + +class MyObj(object): + def somemethod(self): + pass + + +class Dummy1(object): + def _repr_pretty_(self, p, cycle): + p.text("Dummy1(...)") + +class Dummy2(Dummy1): + _repr_pretty_ = None + +class NoModule(object): + pass + +NoModule.__module__ = None + +class Breaking(object): + def _repr_pretty_(self, p, cycle): + with p.group(4,"TG: ",":"): + p.text("Breaking(") + p.break_() + p.text(")") + +class BreakingRepr(object): + def __repr__(self): + return "Breaking(\n)" + +class BreakingReprParent(object): + def _repr_pretty_(self, p, cycle): + with p.group(4,"TG: ",":"): + p.pretty(BreakingRepr()) + +class BadRepr(object): + + def __repr__(self): + return 1/0 + + +def test_indentation(): + """Test correct indentation in groups""" + count = 40 + gotoutput = pretty.pretty(MyList(range(count))) + expectedoutput = "MyList(\n" + ",\n".join(" %d" % i for i in range(count)) + ")" + + nt.assert_equal(gotoutput, expectedoutput) + + +def test_dispatch(): + """ + Test correct dispatching: The _repr_pretty_ method for MyDict + must be found before the registered printer for dict. + """ + gotoutput = pretty.pretty(MyDict()) + expectedoutput = "MyDict(...)" + + nt.assert_equal(gotoutput, expectedoutput) + + +def test_callability_checking(): + """ + Test that the _repr_pretty_ method is tested for callability and skipped if + not. + """ + gotoutput = pretty.pretty(Dummy2()) + expectedoutput = "Dummy1(...)" + + nt.assert_equal(gotoutput, expectedoutput) + + +def test_sets(): + """ + Test that set and frozenset use Python 3 formatting. + """ + objects = [set(), frozenset(), set([1]), frozenset([1]), set([1, 2]), + frozenset([1, 2]), set([-1, -2, -3])] + expected = ['set()', 'frozenset()', '{1}', 'frozenset({1})', '{1, 2}', + 'frozenset({1, 2})', '{-3, -2, -1}'] + for obj, expected_output in zip(objects, expected): + got_output = pretty.pretty(obj) + yield nt.assert_equal, got_output, expected_output + + +@skip_without('xxlimited') +def test_pprint_heap_allocated_type(): + """ + Test that pprint works for heap allocated types. + """ + import xxlimited + output = pretty.pretty(xxlimited.Null) + nt.assert_equal(output, 'xxlimited.Null') + +def test_pprint_nomod(): + """ + Test that pprint works for classes with no __module__. + """ + output = pretty.pretty(NoModule) + nt.assert_equal(output, 'NoModule') + +def test_pprint_break(): + """ + Test that p.break_ produces expected output + """ + output = pretty.pretty(Breaking()) + expected = "TG: Breaking(\n ):" + nt.assert_equal(output, expected) + +def test_pprint_break_repr(): + """ + Test that p.break_ is used in repr + """ + output = pretty.pretty(BreakingReprParent()) + expected = "TG: Breaking(\n ):" + nt.assert_equal(output, expected) + +def test_bad_repr(): + """Don't catch bad repr errors""" + with nt.assert_raises(ZeroDivisionError): + pretty.pretty(BadRepr()) + +class BadException(Exception): + def __str__(self): + return -1 + +class ReallyBadRepr(object): + __module__ = 1 + @property + def __class__(self): + raise ValueError("I am horrible") + + def __repr__(self): + raise BadException() + +def test_really_bad_repr(): + with nt.assert_raises(BadException): + pretty.pretty(ReallyBadRepr()) + + +class SA(object): + pass + +class SB(SA): + pass + +class TestsPretty(unittest.TestCase): + + def test_super_repr(self): + # "" + output = pretty.pretty(super(SA)) + self.assertRegex(output, r"") + + # ">" + sb = SB() + output = pretty.pretty(super(SA, sb)) + self.assertRegex(output, r">") + + + def test_long_list(self): + lis = list(range(10000)) + p = pretty.pretty(lis) + last2 = p.rsplit('\n', 2)[-2:] + self.assertEqual(last2, [' 999,', ' ...]']) + + def test_long_set(self): + s = set(range(10000)) + p = pretty.pretty(s) + last2 = p.rsplit('\n', 2)[-2:] + self.assertEqual(last2, [' 999,', ' ...}']) + + def test_long_tuple(self): + tup = tuple(range(10000)) + p = pretty.pretty(tup) + last2 = p.rsplit('\n', 2)[-2:] + self.assertEqual(last2, [' 999,', ' ...)']) + + def test_long_dict(self): + d = { n:n for n in range(10000) } + p = pretty.pretty(d) + last2 = p.rsplit('\n', 2)[-2:] + self.assertEqual(last2, [' 999: 999,', ' ...}']) + + def test_unbound_method(self): + output = pretty.pretty(MyObj.somemethod) + self.assertIn('MyObj.somemethod', output) + + +class MetaClass(type): + def __new__(cls, name): + return type.__new__(cls, name, (object,), {'name': name}) + + def __repr__(self): + return "[CUSTOM REPR FOR CLASS %s]" % self.name + + +ClassWithMeta = MetaClass('ClassWithMeta') + + +def test_metaclass_repr(): + output = pretty.pretty(ClassWithMeta) + nt.assert_equal(output, "[CUSTOM REPR FOR CLASS ClassWithMeta]") + + +def test_unicode_repr(): + u = u"üniçodé" + ustr = u + + class C(object): + def __repr__(self): + return ustr + + c = C() + p = pretty.pretty(c) + nt.assert_equal(p, u) + p = pretty.pretty([c]) + nt.assert_equal(p, u'[%s]' % u) + + +def test_basic_class(): + def type_pprint_wrapper(obj, p, cycle): + if obj is MyObj: + type_pprint_wrapper.called = True + return pretty._type_pprint(obj, p, cycle) + type_pprint_wrapper.called = False + + stream = StringIO() + printer = pretty.RepresentationPrinter(stream) + printer.type_pprinters[type] = type_pprint_wrapper + printer.pretty(MyObj) + printer.flush() + output = stream.getvalue() + + nt.assert_equal(output, '%s.MyObj' % __name__) + nt.assert_true(type_pprint_wrapper.called) + + +def test_collections_defaultdict(): + # Create defaultdicts with cycles + a = defaultdict() + a.default_factory = a + b = defaultdict(list) + b['key'] = b + + # Dictionary order cannot be relied on, test against single keys. + cases = [ + (defaultdict(list), 'defaultdict(list, {})'), + (defaultdict(list, {'key': '-' * 50}), + "defaultdict(list,\n" + " {'key': '--------------------------------------------------'})"), + (a, 'defaultdict(defaultdict(...), {})'), + (b, "defaultdict(list, {'key': defaultdict(...)})"), + ] + for obj, expected in cases: + nt.assert_equal(pretty.pretty(obj), expected) + + +def test_collections_ordereddict(): + # Create OrderedDict with cycle + a = OrderedDict() + a['key'] = a + + cases = [ + (OrderedDict(), 'OrderedDict()'), + (OrderedDict((i, i) for i in range(1000, 1010)), + 'OrderedDict([(1000, 1000),\n' + ' (1001, 1001),\n' + ' (1002, 1002),\n' + ' (1003, 1003),\n' + ' (1004, 1004),\n' + ' (1005, 1005),\n' + ' (1006, 1006),\n' + ' (1007, 1007),\n' + ' (1008, 1008),\n' + ' (1009, 1009)])'), + (a, "OrderedDict([('key', OrderedDict(...))])"), + ] + for obj, expected in cases: + nt.assert_equal(pretty.pretty(obj), expected) + + +def test_collections_deque(): + # Create deque with cycle + a = deque() + a.append(a) + + cases = [ + (deque(), 'deque([])'), + (deque(i for i in range(1000, 1020)), + 'deque([1000,\n' + ' 1001,\n' + ' 1002,\n' + ' 1003,\n' + ' 1004,\n' + ' 1005,\n' + ' 1006,\n' + ' 1007,\n' + ' 1008,\n' + ' 1009,\n' + ' 1010,\n' + ' 1011,\n' + ' 1012,\n' + ' 1013,\n' + ' 1014,\n' + ' 1015,\n' + ' 1016,\n' + ' 1017,\n' + ' 1018,\n' + ' 1019])'), + (a, 'deque([deque(...)])'), + ] + for obj, expected in cases: + nt.assert_equal(pretty.pretty(obj), expected) + +def test_collections_counter(): + class MyCounter(Counter): + pass + cases = [ + (Counter(), 'Counter()'), + (Counter(a=1), "Counter({'a': 1})"), + (MyCounter(a=1), "MyCounter({'a': 1})"), + ] + for obj, expected in cases: + nt.assert_equal(pretty.pretty(obj), expected) + +def test_mappingproxy(): + MP = types.MappingProxyType + underlying_dict = {} + mp_recursive = MP(underlying_dict) + underlying_dict[2] = mp_recursive + underlying_dict[3] = underlying_dict + + cases = [ + (MP({}), "mappingproxy({})"), + (MP({None: MP({})}), "mappingproxy({None: mappingproxy({})})"), + (MP({k: k.upper() for k in string.ascii_lowercase}), + "mappingproxy({'a': 'A',\n" + " 'b': 'B',\n" + " 'c': 'C',\n" + " 'd': 'D',\n" + " 'e': 'E',\n" + " 'f': 'F',\n" + " 'g': 'G',\n" + " 'h': 'H',\n" + " 'i': 'I',\n" + " 'j': 'J',\n" + " 'k': 'K',\n" + " 'l': 'L',\n" + " 'm': 'M',\n" + " 'n': 'N',\n" + " 'o': 'O',\n" + " 'p': 'P',\n" + " 'q': 'Q',\n" + " 'r': 'R',\n" + " 's': 'S',\n" + " 't': 'T',\n" + " 'u': 'U',\n" + " 'v': 'V',\n" + " 'w': 'W',\n" + " 'x': 'X',\n" + " 'y': 'Y',\n" + " 'z': 'Z'})"), + (mp_recursive, "mappingproxy({2: {...}, 3: {2: {...}, 3: {...}}})"), + (underlying_dict, + "{2: mappingproxy({2: {...}, 3: {...}}), 3: {...}}"), + ] + for obj, expected in cases: + nt.assert_equal(pretty.pretty(obj), expected) + +def test_function_pretty(): + "Test pretty print of function" + # posixpath is a pure python module, its interface is consistent + # across Python distributions + import posixpath + nt.assert_equal(pretty.pretty(posixpath.join), '') + + # custom function + def meaning_of_life(question=None): + if question: + return 42 + return "Don't panic" + + nt.assert_in('meaning_of_life(question=None)', pretty.pretty(meaning_of_life)) + diff --git a/packages/python/yap_kernel/yap_ipython/lib/tests/test_security.py b/packages/python/yap_kernel/yap_ipython/lib/tests/test_security.py new file mode 100644 index 000000000..549fa67ba --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/lib/tests/test_security.py @@ -0,0 +1,26 @@ +# coding: utf-8 +from yap_ipython.lib import passwd +from yap_ipython.lib.security import passwd_check, salt_len +import nose.tools as nt + +def test_passwd_structure(): + p = passwd('passphrase') + algorithm, salt, hashed = p.split(':') + nt.assert_equal(algorithm, 'sha1') + nt.assert_equal(len(salt), salt_len) + nt.assert_equal(len(hashed), 40) + +def test_roundtrip(): + p = passwd('passphrase') + nt.assert_equal(passwd_check(p, 'passphrase'), True) + +def test_bad(): + p = passwd('passphrase') + nt.assert_equal(passwd_check(p, p), False) + nt.assert_equal(passwd_check(p, 'a:b:c:d'), False) + nt.assert_equal(passwd_check(p, 'a:b'), False) + +def test_passwd_check_unicode(): + # GH issue #4524 + phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' + assert passwd_check(phash, u"łe¶ŧ←↓→") \ No newline at end of file diff --git a/packages/python/yap_kernel/yap_ipython/terminal/tests/__init__.py b/packages/python/yap_kernel/yap_ipython/terminal/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/python/yap_kernel/yap_ipython/terminal/tests/test_embed.py b/packages/python/yap_kernel/yap_ipython/terminal/tests/test_embed.py new file mode 100644 index 000000000..d90e1b934 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/terminal/tests/test_embed.py @@ -0,0 +1,132 @@ +"""Test embedding of yap_ipython""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2013 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import os +import subprocess +import sys +import nose.tools as nt +from yap_ipython.utils.tempdir import NamedFileInTemporaryDirectory +from yap_ipython.testing.decorators import skip_win32 + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + + +_sample_embed = b""" +import yap_ipython + +a = 3 +b = 14 +print(a, '.', b) + +yap_ipython.embed() + +print('bye!') +""" + +_exit = b"exit\r" + +def test_ipython_embed(): + """test that `yap_ipython.embed()` works""" + with NamedFileInTemporaryDirectory('file_with_embed.py') as f: + f.write(_sample_embed) + f.flush() + f.close() # otherwise msft won't be able to read the file + + # run `python file_with_embed.py` + cmd = [sys.executable, f.name] + env = os.environ.copy() + env['IPY_TEST_SIMPLE_PROMPT'] = '1' + + p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate(_exit) + std = out.decode('UTF-8') + + nt.assert_equal(p.returncode, 0) + nt.assert_in('3 . 14', std) + if os.name != 'nt': + # TODO: Fix up our different stdout references, see issue gh-14 + nt.assert_in('yap_ipython', std) + nt.assert_in('bye!', std) + +@skip_win32 +def test_nest_embed(): + """test that `yap_ipython.embed()` is nestable""" + import pexpect + ipy_prompt = r']:' #ansi color codes give problems matching beyond this + env = os.environ.copy() + env['IPY_TEST_SIMPLE_PROMPT'] = '1' + + + child = pexpect.spawn(sys.executable, ['-m', 'yap_ipython', '--colors=nocolor'], + env=env) + child.expect(ipy_prompt) + child.sendline("import yap_ipython") + child.expect(ipy_prompt) + child.sendline("ip0 = get_ipython()") + #enter first nested embed + child.sendline("yap_ipython.embed()") + #skip the banner until we get to a prompt + try: + prompted = -1 + while prompted != 0: + prompted = child.expect([ipy_prompt, '\r\n']) + except pexpect.TIMEOUT as e: + print(e) + #child.interact() + child.sendline("embed1 = get_ipython()"); child.expect(ipy_prompt) + child.sendline("print('true' if embed1 is not ip0 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline("print('true' if yap_ipython.get_ipython() is embed1 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + #enter second nested embed + child.sendline("yap_ipython.embed()") + #skip the banner until we get to a prompt + try: + prompted = -1 + while prompted != 0: + prompted = child.expect([ipy_prompt, '\r\n']) + except pexpect.TIMEOUT as e: + print(e) + #child.interact() + child.sendline("embed2 = get_ipython()"); child.expect(ipy_prompt) + child.sendline("print('true' if embed2 is not embed1 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline("print('true' if embed2 is yap_ipython.get_ipython() else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline('exit') + #back at first embed + child.expect(ipy_prompt) + child.sendline("print('true' if get_ipython() is embed1 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline("print('true' if yap_ipython.get_ipython() is embed1 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline('exit') + #back at launching scope + child.expect(ipy_prompt) + child.sendline("print('true' if get_ipython() is ip0 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline("print('true' if yap_ipython.get_ipython() is ip0 else 'false')") + assert(child.expect(['true\r\n', 'false\r\n']) == 0) + child.expect(ipy_prompt) + child.sendline('exit') + child.close() diff --git a/packages/python/yap_kernel/yap_ipython/terminal/tests/test_help.py b/packages/python/yap_kernel/yap_ipython/terminal/tests/test_help.py new file mode 100644 index 000000000..e0ee90f02 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/terminal/tests/test_help.py @@ -0,0 +1,28 @@ +"""Test help output of various yap_ipython entry points""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import yap_ipython.testing.tools as tt + + +def test_ipython_help(): + tt.help_all_output_test() + +def test_profile_help(): + tt.help_all_output_test("profile") + +def test_profile_list_help(): + tt.help_all_output_test("profile list") + +def test_profile_create_help(): + tt.help_all_output_test("profile create") + +def test_locate_help(): + tt.help_all_output_test("locate") + +def test_locate_profile_help(): + tt.help_all_output_test("locate profile") + +def test_trust_help(): + tt.help_all_output_test("trust") diff --git a/packages/python/yap_kernel/yap_ipython/terminal/tests/test_interactivshell.py b/packages/python/yap_kernel/yap_ipython/terminal/tests/test_interactivshell.py new file mode 100644 index 000000000..37ef622d7 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/terminal/tests/test_interactivshell.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +"""Tests for the TerminalInteractiveShell and related pieces.""" +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import sys +import unittest + +from yap_ipython.core.inputtransformer import InputTransformer +from yap_ipython.testing import tools as tt +from yap_ipython.utils.capture import capture_output + +from yap_ipython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context +import nose.tools as nt + +class TestElide(unittest.TestCase): + + def test_elide(self): + _elide('concatenate((a1, a2, ...), axis') # do not raise + _elide('concatenate((a1, a2, ..), . axis') # do not raise + nt.assert_equal(_elide('aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh'), 'aaaa.b…g.hhhhhh') + + +class TestContextAwareCompletion(unittest.TestCase): + + def test_adjust_completion_text_based_on_context(self): + # Adjusted case + nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a=)', 7), 'arg1') + + # Untouched cases + nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a)', 7), 'arg1=') + nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a', 7), 'arg1=') + nt.assert_equal(_adjust_completion_text_based_on_context('%magic', 'func1(a=)', 7), '%magic') + nt.assert_equal(_adjust_completion_text_based_on_context('func2', 'func1(a=)', 7), 'func2') + +# Decorator for interaction loop tests ----------------------------------------- + +class mock_input_helper(object): + """Machinery for tests of the main interact loop. + + Used by the mock_input decorator. + """ + def __init__(self, testgen): + self.testgen = testgen + self.exception = None + self.ip = get_ipython() + + def __enter__(self): + self.orig_prompt_for_code = self.ip.prompt_for_code + self.ip.prompt_for_code = self.fake_input + return self + + def __exit__(self, etype, value, tb): + self.ip.prompt_for_code = self.orig_prompt_for_code + + def fake_input(self): + try: + return next(self.testgen) + except StopIteration: + self.ip.keep_running = False + return u'' + except: + self.exception = sys.exc_info() + self.ip.keep_running = False + return u'' + +def mock_input(testfunc): + """Decorator for tests of the main interact loop. + + Write the test as a generator, yield-ing the input strings, which yap_ipython + will see as if they were typed in at the prompt. + """ + def test_method(self): + testgen = testfunc(self) + with mock_input_helper(testgen) as mih: + mih.ip.interact() + + if mih.exception is not None: + # Re-raise captured exception + etype, value, tb = mih.exception + import traceback + traceback.print_tb(tb, file=sys.stdout) + del tb # Avoid reference loop + raise value + + return test_method + +# Test classes ----------------------------------------------------------------- + +class InteractiveShellTestCase(unittest.TestCase): + def rl_hist_entries(self, rl, n): + """Get last n readline history entries as a list""" + return [rl.get_history_item(rl.get_current_history_length() - x) + for x in range(n - 1, -1, -1)] + + @mock_input + def test_inputtransformer_syntaxerror(self): + ip = get_ipython() + transformer = SyntaxErrorTransformer() + ip.input_splitter.python_line_transforms.append(transformer) + ip.input_transformer_manager.python_line_transforms.append(transformer) + + try: + #raise Exception + with tt.AssertPrints('4', suppress=False): + yield u'print(2*2)' + + with tt.AssertPrints('SyntaxError: input contains', suppress=False): + yield u'print(2345) # syntaxerror' + + with tt.AssertPrints('16', suppress=False): + yield u'print(4*4)' + + finally: + ip.input_splitter.python_line_transforms.remove(transformer) + ip.input_transformer_manager.python_line_transforms.remove(transformer) + + def test_plain_text_only(self): + ip = get_ipython() + formatter = ip.display_formatter + assert formatter.active_types == ['text/plain'] + assert not formatter.ipython_display_formatter.enabled + + class Test(object): + def __repr__(self): + return "" % id(self) + + def _repr_html_(self): + return '' + + # verify that HTML repr isn't computed + obj = Test() + data, _ = formatter.format(obj) + self.assertEqual(data, {'text/plain': repr(obj)}) + + class Test2(Test): + def _ipython_display_(self): + from yap_ipython.display import display + display('') + + # verify that _ipython_display_ shortcut isn't called + obj = Test2() + with capture_output() as captured: + data, _ = formatter.format(obj) + + self.assertEqual(data, {'text/plain': repr(obj)}) + assert captured.stdout == '' + + + +class SyntaxErrorTransformer(InputTransformer): + def push(self, line): + pos = line.find('syntaxerror') + if pos >= 0: + e = SyntaxError('input contains "syntaxerror"') + e.text = line + e.offset = pos + 1 + raise e + return line + + def reset(self): + pass + +class TerminalMagicsTestCase(unittest.TestCase): + def test_paste_magics_blankline(self): + """Test that code with a blank line doesn't get split (gh-3246).""" + ip = get_ipython() + s = ('def pasted_func(a):\n' + ' b = a+1\n' + '\n' + ' return b') + + tm = ip.magics_manager.registry['TerminalMagics'] + tm.store_or_execute(s, name=None) + + self.assertEqual(ip.user_ns['pasted_func'](54), 55) diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/__init__.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/dtexample.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/dtexample.py new file mode 100644 index 000000000..4c0f2f279 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/dtexample.py @@ -0,0 +1,157 @@ +"""Simple example using doctests. + +This file just contains doctests both using plain python and yap_ipython prompts. +All tests should be loaded by nose. +""" + +def pyfunc(): + """Some pure python tests... + + >>> pyfunc() + 'pyfunc' + + >>> import os + + >>> 2+3 + 5 + + >>> for i in range(3): + ... print(i, end=' ') + ... print(i+1, end=' ') + ... + 0 1 1 2 2 3 + """ + return 'pyfunc' + +def ipfunc(): + """Some ipython tests... + + In [1]: import os + + In [3]: 2+3 + Out[3]: 5 + + In [26]: for i in range(3): + ....: print(i, end=' ') + ....: print(i+1, end=' ') + ....: + 0 1 1 2 2 3 + + + Examples that access the operating system work: + + In [1]: !echo hello + hello + + In [2]: !echo hello > /tmp/foo_iptest + + In [3]: !cat /tmp/foo_iptest + hello + + In [4]: rm -f /tmp/foo_iptest + + It's OK to use '_' for the last result, but do NOT try to use yap_ipython's + numbered history of _NN outputs, since those won't exist under the + doctest environment: + + In [7]: 'hi' + Out[7]: 'hi' + + In [8]: print(repr(_)) + 'hi' + + In [7]: 3+4 + Out[7]: 7 + + In [8]: _+3 + Out[8]: 10 + + In [9]: ipfunc() + Out[9]: 'ipfunc' + """ + return 'ipfunc' + + +def ranfunc(): + """A function with some random output. + + Normal examples are verified as usual: + >>> 1+3 + 4 + + But if you put '# random' in the output, it is ignored: + >>> 1+3 + junk goes here... # random + + >>> 1+2 + again, anything goes #random + if multiline, the random mark is only needed once. + + >>> 1+2 + You can also put the random marker at the end: + # random + + >>> 1+2 + # random + .. or at the beginning. + + More correct input is properly verified: + >>> ranfunc() + 'ranfunc' + """ + return 'ranfunc' + + +def random_all(): + """A function where we ignore the output of ALL examples. + + Examples: + + # all-random + + This mark tells the testing machinery that all subsequent examples should + be treated as random (ignoring their output). They are still executed, + so if a they raise an error, it will be detected as such, but their + output is completely ignored. + + >>> 1+3 + junk goes here... + + >>> 1+3 + klasdfj; + + >>> 1+2 + again, anything goes + blah... + """ + pass + +def iprand(): + """Some ipython tests with random output. + + In [7]: 3+4 + Out[7]: 7 + + In [8]: print('hello') + world # random + + In [9]: iprand() + Out[9]: 'iprand' + """ + return 'iprand' + +def iprand_all(): + """Some ipython tests with fully random output. + + # all-random + + In [7]: 1 + Out[7]: 99 + + In [8]: print('hello') + world + + In [9]: iprand_all() + Out[9]: 'junk' + """ + return 'iprand_all' diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/ipdoctest.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/ipdoctest.py new file mode 100644 index 000000000..002336524 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/ipdoctest.py @@ -0,0 +1,764 @@ +"""Nose Plugin that supports yap_ipython doctests. + +Limitations: + +- When generating examples for use as doctests, make sure that you have + pretty-printing OFF. This can be done either by setting the + ``PlainTextFormatter.pprint`` option in your configuration file to False, or + by interactively disabling it with %Pprint. This is required so that yap_ipython + output matches that of normal Python, which is used by doctest for internal + execution. + +- Do not rely on specific prompt numbers for results (such as using + '_34==True', for example). For yap_ipython tests run via an external process the + prompt numbers may be different, and yap_ipython tests run as normal python code + won't even have these special _NN variables set at all. +""" + +#----------------------------------------------------------------------------- +# Module imports + +# From the standard library +import builtins as builtin_mod +import doctest +import inspect +import logging +import os +import re +import sys +from importlib import import_module +from io import StringIO + +from testpath import modified_env + +from inspect import getmodule + +# We are overriding the default doctest runner, so we need to import a few +# things from doctest directly +from doctest import (REPORTING_FLAGS, REPORT_ONLY_FIRST_FAILURE, + _unittest_reportflags, DocTestRunner, + _extract_future_flags, pdb, _OutputRedirectingPdb, + _exception_traceback, + linecache) + +# Third-party modules + +from nose.plugins import doctests, Plugin +from nose.util import anyp, tolist + +#----------------------------------------------------------------------------- +# Module globals and other constants +#----------------------------------------------------------------------------- + +log = logging.getLogger(__name__) + + +#----------------------------------------------------------------------------- +# Classes and functions +#----------------------------------------------------------------------------- + +def is_extension_module(filename): + """Return whether the given filename is an extension module. + + This simply checks that the extension is either .so or .pyd. + """ + return os.path.splitext(filename)[1].lower() in ('.so','.pyd') + + +class DocTestSkip(object): + """Object wrapper for doctests to be skipped.""" + + ds_skip = """Doctest to skip. + >>> 1 #doctest: +SKIP + """ + + def __init__(self,obj): + self.obj = obj + + def __getattribute__(self,key): + if key == '__doc__': + return DocTestSkip.ds_skip + else: + return getattr(object.__getattribute__(self,'obj'),key) + +# Modified version of the one in the stdlib, that fixes a python bug (doctests +# not found in extension modules, http://bugs.python.org/issue3158) +class DocTestFinder(doctest.DocTestFinder): + + def _from_module(self, module, object): + """ + Return true if the given object is defined in the given + module. + """ + if module is None: + return True + elif inspect.isfunction(object): + return module.__dict__ is object.__globals__ + elif inspect.isbuiltin(object): + return module.__name__ == object.__module__ + elif inspect.isclass(object): + return module.__name__ == object.__module__ + elif inspect.ismethod(object): + # This one may be a bug in cython that fails to correctly set the + # __module__ attribute of methods, but since the same error is easy + # to make by extension code writers, having this safety in place + # isn't such a bad idea + return module.__name__ == object.__self__.__class__.__module__ + elif inspect.getmodule(object) is not None: + return module is inspect.getmodule(object) + elif hasattr(object, '__module__'): + return module.__name__ == object.__module__ + elif isinstance(object, property): + return True # [XX] no way not be sure. + elif inspect.ismethoddescriptor(object): + # Unbound PyQt signals reach this point in Python 3.4b3, and we want + # to avoid throwing an error. See also http://bugs.python.org/issue3158 + return False + else: + raise ValueError("object must be a class or function, got %r" % object) + + def _find(self, tests, obj, name, module, source_lines, globs, seen): + """ + Find tests for the given object and any contained objects, and + add them to `tests`. + """ + print('_find for:', obj, name, module) # dbg + if hasattr(obj,"skip_doctest"): + #print 'SKIPPING DOCTEST FOR:',obj # dbg + obj = DocTestSkip(obj) + + doctest.DocTestFinder._find(self,tests, obj, name, module, + source_lines, globs, seen) + + # Below we re-run pieces of the above method with manual modifications, + # because the original code is buggy and fails to correctly identify + # doctests in extension modules. + + # Local shorthands + from inspect import isroutine, isclass + + # Look for tests in a module's contained objects. + if inspect.ismodule(obj) and self._recurse: + for valname, val in obj.__dict__.items(): + valname1 = '%s.%s' % (name, valname) + if ( (isroutine(val) or isclass(val)) + and self._from_module(module, val) ): + + self._find(tests, val, valname1, module, source_lines, + globs, seen) + + # Look for tests in a class's contained objects. + if inspect.isclass(obj) and self._recurse: + #print 'RECURSE into class:',obj # dbg + for valname, val in obj.__dict__.items(): + # Special handling for staticmethod/classmethod. + if isinstance(val, staticmethod): + val = getattr(obj, valname) + if isinstance(val, classmethod): + val = getattr(obj, valname).__func__ + + # Recurse to methods, properties, and nested classes. + if ((inspect.isfunction(val) or inspect.isclass(val) or + inspect.ismethod(val) or + isinstance(val, property)) and + self._from_module(module, val)): + valname = '%s.%s' % (name, valname) + self._find(tests, val, valname, module, source_lines, + globs, seen) + + +class IPDoctestOutputChecker(doctest.OutputChecker): + """Second-chance checker with support for random tests. + + If the default comparison doesn't pass, this checker looks in the expected + output string for flags that tell us to ignore the output. + """ + + random_re = re.compile(r'#\s*random\s+') + + def check_output(self, want, got, optionflags): + """Check output, accepting special markers embedded in the output. + + If the output didn't pass the default validation but the special string + '#random' is included, we accept it.""" + + # Let the original tester verify first, in case people have valid tests + # that happen to have a comment saying '#random' embedded in. + ret = doctest.OutputChecker.check_output(self, want, got, + optionflags) + if not ret and self.random_re.search(want): + #print >> sys.stderr, 'RANDOM OK:',want # dbg + return True + + return ret + + +class DocTestCase(doctests.DocTestCase): + """Proxy for DocTestCase: provides an address() method that + returns the correct address for the doctest case. Otherwise + acts as a proxy to the test case. To provide hints for address(), + an obj may also be passed -- this will be used as the test object + for purposes of determining the test address, if it is provided. + """ + + # Note: this method was taken from numpy's nosetester module. + + # Subclass nose.plugins.doctests.DocTestCase to work around a bug in + # its constructor that blocks non-default arguments from being passed + # down into doctest.DocTestCase + + def __init__(self, test, optionflags=0, setUp=None, tearDown=None, + checker=None, obj=None, result_var='_'): + self._result_var = result_var + doctests.DocTestCase.__init__(self, test, + optionflags=optionflags, + setUp=setUp, tearDown=tearDown, + checker=checker) + # Now we must actually copy the original constructor from the stdlib + # doctest class, because we can't call it directly and a bug in nose + # means it never gets passed the right arguments. + + self._dt_optionflags = optionflags + self._dt_checker = checker + self._dt_test = test + self._dt_test_globs_ori = test.globs + self._dt_setUp = setUp + self._dt_tearDown = tearDown + + # XXX - store this runner once in the object! + runner = IPDocTestRunner(optionflags=optionflags, + checker=checker, verbose=False) + self._dt_runner = runner + + + # Each doctest should remember the directory it was loaded from, so + # things like %run work without too many contortions + self._ori_dir = os.path.dirname(test.filename) + + # Modified runTest from the default stdlib + def runTest(self): + test = self._dt_test + runner = self._dt_runner + + old = sys.stdout + new = StringIO() + optionflags = self._dt_optionflags + + if not (optionflags & REPORTING_FLAGS): + # The option flags don't include any reporting flags, + # so add the default reporting flags + optionflags |= _unittest_reportflags + + try: + # Save our current directory and switch out to the one where the + # test was originally created, in case another doctest did a + # directory change. We'll restore this in the finally clause. + curdir = os.getcwd() + #print 'runTest in dir:', self._ori_dir # dbg + os.chdir(self._ori_dir) + + runner.DIVIDER = "-"*70 + failures, tries = runner.run(test,out=new.write, + clear_globs=False) + finally: + sys.stdout = old + os.chdir(curdir) + + if failures: + raise self.failureException(self.format_failure(new.getvalue())) + + def setUp(self): + """Modified test setup that syncs with ipython namespace""" + #print "setUp test", self._dt_test.examples # dbg + if isinstance(self._dt_test.examples[0], IPExample): + # for yap_ipython examples *only*, we swap the globals with the ipython + # namespace, after updating it with the globals (which doctest + # fills with the necessary info from the module being tested). + self.user_ns_orig = {} + self.user_ns_orig.update(_ip.user_ns) + _ip.user_ns.update(self._dt_test.globs) + # We must remove the _ key in the namespace, so that Python's + # doctest code sets it naturally + _ip.user_ns.pop('_', None) + _ip.user_ns['__builtins__'] = builtin_mod + self._dt_test.globs = _ip.user_ns + + super(DocTestCase, self).setUp() + + def tearDown(self): + + # Undo the test.globs reassignment we made, so that the parent class + # teardown doesn't destroy the ipython namespace + if isinstance(self._dt_test.examples[0], IPExample): + self._dt_test.globs = self._dt_test_globs_ori + _ip.user_ns.clear() + _ip.user_ns.update(self.user_ns_orig) + + # XXX - fperez: I am not sure if this is truly a bug in nose 0.11, but + # it does look like one to me: its tearDown method tries to run + # + # delattr(builtin_mod, self._result_var) + # + # without checking that the attribute really is there; it implicitly + # assumes it should have been set via displayhook. But if the + # displayhook was never called, this doesn't necessarily happen. I + # haven't been able to find a little self-contained example outside of + # ipython that would show the problem so I can report it to the nose + # team, but it does happen a lot in our code. + # + # So here, we just protect as narrowly as possible by trapping an + # attribute error whose message would be the name of self._result_var, + # and letting any other error propagate. + try: + super(DocTestCase, self).tearDown() + except AttributeError as exc: + if exc.args[0] != self._result_var: + raise + + +# A simple subclassing of the original with a different class name, so we can +# distinguish and treat differently yap_ipython examples from pure python ones. +class IPExample(doctest.Example): pass + + +class IPExternalExample(doctest.Example): + """Doctest examples to be run in an external process.""" + + def __init__(self, source, want, exc_msg=None, lineno=0, indent=0, + options=None): + # Parent constructor + doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options) + + # An EXTRA newline is needed to prevent pexpect hangs + self.source += '\n' + + +class IPDocTestParser(doctest.DocTestParser): + """ + A class used to parse strings containing doctest examples. + + Note: This is a version modified to properly recognize yap_ipython input and + convert any yap_ipython examples into valid Python ones. + """ + # This regular expression is used to find doctest examples in a + # string. It defines three groups: `source` is the source code + # (including leading indentation and prompts); `indent` is the + # indentation of the first (PS1) line of the source code; and + # `want` is the expected output (including leading indentation). + + # Classic Python prompts or default yap_ipython ones + _PS1_PY = r'>>>' + _PS2_PY = r'\.\.\.' + + _PS1_IP = r'In\ \[\d+\]:' + _PS2_IP = r'\ \ \ \.\.\.+:' + + _RE_TPL = r''' + # Source consists of a PS1 line followed by zero or more PS2 lines. + (?P + (?:^(?P [ ]*) (?P %s) .*) # PS1 line + (?:\n [ ]* (?P %s) .*)*) # PS2 lines + \n? # a newline + # Want consists of any non-blank lines that do not start with PS1. + (?P (?:(?![ ]*$) # Not a blank line + (?![ ]*%s) # Not a line starting with PS1 + (?![ ]*%s) # Not a line starting with PS2 + .*$\n? # But any other line + )*) + ''' + + _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY), + re.MULTILINE | re.VERBOSE) + + _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP), + re.MULTILINE | re.VERBOSE) + + # Mark a test as being fully random. In this case, we simply append the + # random marker ('#random') to each individual example's output. This way + # we don't need to modify any other code. + _RANDOM_TEST = re.compile(r'#\s*all-random\s+') + + # Mark tests to be executed in an external process - currently unsupported. + _EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL') + + def ip2py(self,source): + """Convert input yap_ipython source into valid Python.""" + block = _ip.input_transformer_manager.transform_cell(source) + if len(block.splitlines()) == 1: + return _ip.prefilter(block) + else: + return block + + def parse(self, string, name=''): + """ + Divide the given string into examples and intervening text, + and return them as a list of alternating Examples and strings. + Line numbers for the Examples are 0-based. The optional + argument `name` is a name identifying this string, and is only + used for error messages. + """ + + #print 'Parse string:\n',string # dbg + + string = string.expandtabs() + # If all lines begin with the same indentation, then strip it. + min_indent = self._min_indent(string) + if min_indent > 0: + string = '\n'.join([l[min_indent:] for l in string.split('\n')]) + + output = [] + charno, lineno = 0, 0 + + # We make 'all random' tests by adding the '# random' mark to every + # block of output in the test. + if self._RANDOM_TEST.search(string): + random_marker = '\n# random' + else: + random_marker = '' + + # Whether to convert the input from ipython to python syntax + ip2py = False + # Find all doctest examples in the string. First, try them as Python + # examples, then as yap_ipython ones + terms = list(self._EXAMPLE_RE_PY.finditer(string)) + if terms: + # Normal Python example + #print '-'*70 # dbg + #print 'PyExample, Source:\n',string # dbg + #print '-'*70 # dbg + Example = doctest.Example + else: + # It's an ipython example. Note that IPExamples are run + # in-process, so their syntax must be turned into valid python. + # IPExternalExamples are run out-of-process (via pexpect) so they + # don't need any filtering (a real ipython will be executing them). + terms = list(self._EXAMPLE_RE_IP.finditer(string)) + if self._EXTERNAL_IP.search(string): + #print '-'*70 # dbg + #print 'IPExternalExample, Source:\n',string # dbg + #print '-'*70 # dbg + Example = IPExternalExample + else: + #print '-'*70 # dbg + #print 'IPExample, Source:\n',string # dbg + #print '-'*70 # dbg + Example = IPExample + ip2py = True + + for m in terms: + # Add the pre-example text to `output`. + output.append(string[charno:m.start()]) + # Update lineno (lines before this example) + lineno += string.count('\n', charno, m.start()) + # Extract info from the regexp match. + (source, options, want, exc_msg) = \ + self._parse_example(m, name, lineno,ip2py) + + # Append the random-output marker (it defaults to empty in most + # cases, it's only non-empty for 'all-random' tests): + want += random_marker + + if Example is IPExternalExample: + options[doctest.NORMALIZE_WHITESPACE] = True + want += '\n' + + # Create an Example, and add it to the list. + if not self._IS_BLANK_OR_COMMENT(source): + output.append(Example(source, want, exc_msg, + lineno=lineno, + indent=min_indent+len(m.group('indent')), + options=options)) + # Update lineno (lines inside this example) + lineno += string.count('\n', m.start(), m.end()) + # Update charno. + charno = m.end() + # Add any remaining post-example text to `output`. + output.append(string[charno:]) + return output + + def _parse_example(self, m, name, lineno,ip2py=False): + """ + Given a regular expression match from `_EXAMPLE_RE` (`m`), + return a pair `(source, want)`, where `source` is the matched + example's source code (with prompts and indentation stripped); + and `want` is the example's expected output (with indentation + stripped). + + `name` is the string's name, and `lineno` is the line number + where the example starts; both are used for error messages. + + Optional: + `ip2py`: if true, filter the input via yap_ipython to convert the syntax + into valid python. + """ + + # Get the example's indentation level. + indent = len(m.group('indent')) + + # Divide source into lines; check that they're properly + # indented; and then strip their indentation & prompts. + source_lines = m.group('source').split('\n') + + # We're using variable-length input prompts + ps1 = m.group('ps1') + ps2 = m.group('ps2') + ps1_len = len(ps1) + + self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len) + if ps2: + self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno) + + source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines]) + + if ip2py: + # Convert source input from yap_ipython into valid Python syntax + source = self.ip2py(source) + + # Divide want into lines; check that it's properly indented; and + # then strip the indentation. Spaces before the last newline should + # be preserved, so plain rstrip() isn't good enough. + want = m.group('want') + want_lines = want.split('\n') + if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]): + del want_lines[-1] # forget final newline & spaces after it + self._check_prefix(want_lines, ' '*indent, name, + lineno + len(source_lines)) + + # Remove ipython output prompt that might be present in the first line + want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0]) + + want = '\n'.join([wl[indent:] for wl in want_lines]) + + # If `want` contains a traceback message, then extract it. + m = self._EXCEPTION_RE.match(want) + if m: + exc_msg = m.group('msg') + else: + exc_msg = None + + # Extract options from the source. + options = self._find_options(source, name, lineno) + + return source, options, want, exc_msg + + def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len): + """ + Given the lines of a source string (including prompts and + leading indentation), check to make sure that every prompt is + followed by a space character. If any line is not followed by + a space character, then raise ValueError. + + Note: yap_ipython-modified version which takes the input prompt length as a + parameter, so that prompts of variable length can be dealt with. + """ + space_idx = indent+ps1_len + min_len = space_idx+1 + for i, line in enumerate(lines): + if len(line) >= min_len and line[space_idx] != ' ': + raise ValueError('line %r of the docstring for %s ' + 'lacks blank after %s: %r' % + (lineno+i+1, name, + line[indent:space_idx], line)) + + +SKIP = doctest.register_optionflag('SKIP') + + +class IPDocTestRunner(doctest.DocTestRunner,object): + """Test runner that synchronizes the yap_ipython namespace with test globals. + """ + + def run(self, test, compileflags=None, out=None, clear_globs=True): + + # Hack: ipython needs access to the execution context of the example, + # so that it can propagate user variables loaded by %run into + # test.globs. We put them here into our modified %run as a function + # attribute. Our new %run will then only make the namespace update + # when called (rather than unconconditionally updating test.globs here + # for all examples, most of which won't be calling %run anyway). + #_ip._ipdoctest_test_globs = test.globs + #_ip._ipdoctest_test_filename = test.filename + + test.globs.update(_ip.user_ns) + + # Override terminal size to standardise traceback format + with modified_env({'COLUMNS': '80', 'LINES': '24'}): + return super(IPDocTestRunner,self).run(test, + compileflags,out,clear_globs) + + +class DocFileCase(doctest.DocFileCase): + """Overrides to provide filename + """ + def address(self): + return (self._dt_test.filename, None, None) + + +class ExtensionDoctest(doctests.Doctest): + """Nose Plugin that supports doctests in extension modules. + """ + name = 'extdoctest' # call nosetests with --with-extdoctest + enabled = True + + def options(self, parser, env=os.environ): + Plugin.options(self, parser, env) + parser.add_option('--doctest-tests', action='store_true', + dest='doctest_tests', + default=env.get('NOSE_DOCTEST_TESTS',True), + help="Also look for doctests in test modules. " + "Note that classes, methods and functions should " + "have either doctests or non-doctest tests, " + "not both. [NOSE_DOCTEST_TESTS]") + parser.add_option('--doctest-extension', action="append", + dest="doctestExtension", + help="Also look for doctests in files with " + "this extension [NOSE_DOCTEST_EXTENSION]") + # Set the default as a list, if given in env; otherwise + # an additional value set on the command line will cause + # an error. + env_setting = env.get('NOSE_DOCTEST_EXTENSION') + if env_setting is not None: + parser.set_defaults(doctestExtension=tolist(env_setting)) + + + def configure(self, options, config): + Plugin.configure(self, options, config) + # Pull standard doctest plugin out of config; we will do doctesting + config.plugins.plugins = [p for p in config.plugins.plugins + if p.name != 'doctest'] + self.doctest_tests = options.doctest_tests + self.extension = tolist(options.doctestExtension) + + self.parser = doctest.DocTestParser() + self.finder = DocTestFinder() + self.checker = IPDoctestOutputChecker() + self.globs = None + self.extraglobs = None + + + def loadTestsFromExtensionModule(self,filename): + bpath,mod = os.path.split(filename) + modname = os.path.splitext(mod)[0] + try: + sys.path.append(bpath) + module = import_module(modname) + tests = list(self.loadTestsFromModule(module)) + finally: + sys.path.pop() + return tests + + # NOTE: the method below is almost a copy of the original one in nose, with + # a few modifications to control output checking. + + def loadTestsFromModule(self, module): + #print '*** ipdoctest - lTM',module # dbg + + if not self.matches(module.__name__): + log.debug("Doctest doesn't want module %s", module) + return + + tests = self.finder.find(module,globs=self.globs, + extraglobs=self.extraglobs) + if not tests: + return + + # always use whitespace and ellipsis options + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + + tests.sort() + module_file = module.__file__ + if module_file[-4:] in ('.pyc', '.pyo'): + module_file = module_file[:-1] + for test in tests: + if not test.examples: + continue + if not test.filename: + test.filename = module_file + + yield DocTestCase(test, + optionflags=optionflags, + checker=self.checker) + + + def loadTestsFromFile(self, filename): + #print "ipdoctest - from file", filename # dbg + if is_extension_module(filename): + for t in self.loadTestsFromExtensionModule(filename): + yield t + else: + if self.extension and anyp(filename.endswith, self.extension): + name = os.path.basename(filename) + dh = open(filename) + try: + doc = dh.read() + finally: + dh.close() + test = self.parser.get_doctest( + doc, globs={'__file__': filename}, name=name, + filename=filename, lineno=0) + if test.examples: + #print 'FileCase:',test.examples # dbg + yield DocFileCase(test) + else: + yield False # no tests to load + + +class IPythonDoctest(ExtensionDoctest): + """Nose Plugin that supports doctests in extension modules. + """ + name = 'ipdoctest' # call nosetests with --with-ipdoctest + enabled = True + + def makeTest(self, obj, parent): + """Look for doctests in the given object, which will be a + function, method or class. + """ + #print 'Plugin analyzing:', obj, parent # dbg + # always use whitespace and ellipsis options + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + + doctests = self.finder.find(obj, module=getmodule(parent)) + if doctests: + for test in doctests: + if len(test.examples) == 0: + continue + + yield DocTestCase(test, obj=obj, + optionflags=optionflags, + checker=self.checker) + + def options(self, parser, env=os.environ): + #print "Options for nose plugin:", self.name # dbg + Plugin.options(self, parser, env) + parser.add_option('--ipdoctest-tests', action='store_true', + dest='ipdoctest_tests', + default=env.get('NOSE_IPDOCTEST_TESTS',True), + help="Also look for doctests in test modules. " + "Note that classes, methods and functions should " + "have either doctests or non-doctest tests, " + "not both. [NOSE_IPDOCTEST_TESTS]") + parser.add_option('--ipdoctest-extension', action="append", + dest="ipdoctest_extension", + help="Also look for doctests in files with " + "this extension [NOSE_IPDOCTEST_EXTENSION]") + # Set the default as a list, if given in env; otherwise + # an additional value set on the command line will cause + # an error. + env_setting = env.get('NOSE_IPDOCTEST_EXTENSION') + if env_setting is not None: + parser.set_defaults(ipdoctest_extension=tolist(env_setting)) + + def configure(self, options, config): + #print "Configuring nose plugin:", self.name # dbg + Plugin.configure(self, options, config) + # Pull standard doctest plugin out of config; we will do doctesting + config.plugins.plugins = [p for p in config.plugins.plugins + if p.name != 'doctest'] + self.doctest_tests = options.ipdoctest_tests + self.extension = tolist(options.ipdoctest_extension) + + self.parser = IPDocTestParser() + self.finder = DocTestFinder(parser=self.parser) + self.checker = IPDoctestOutputChecker() + self.globs = None + self.extraglobs = None diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/iptest.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/iptest.py new file mode 100644 index 000000000..e24e22a83 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/iptest.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +"""Nose-based test runner. +""" + +from nose.core import main +from nose.plugins.builtin import plugins +from nose.plugins.doctests import Doctest + +from . import ipdoctest +from .ipdoctest import IPDocTestRunner + +if __name__ == '__main__': + print('WARNING: this code is incomplete!') + print() + + pp = [x() for x in plugins] # activate all builtin plugins first + main(testRunner=IPDocTestRunner(), + plugins=pp+[ipdoctest.IPythonDoctest(),Doctest()]) diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/setup.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/setup.py new file mode 100644 index 000000000..b94aab666 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +"""A Nose plugin to support yap_ipython doctests. +""" + +from setuptools import setup + +setup(name='yap_ipython doctest plugin', + version='0.1', + author='The yap_ipython Team', + description = 'Nose plugin to load yap_ipython-extended doctests', + license = 'LGPL', + py_modules = ['ipdoctest'], + entry_points = { + 'nose.plugins.0.10': ['ipdoctest = ipdoctest:IPythonDoctest', + 'extdoctest = ipdoctest:ExtensionDoctest', + ], + }, + ) diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/show_refs.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/show_refs.py new file mode 100644 index 000000000..b2c70adfc --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/show_refs.py @@ -0,0 +1,19 @@ +"""Simple script to show reference holding behavior. + +This is used by a companion test case. +""" + +import gc + +class C(object): + def __del__(self): + pass + #print 'deleting object...' # dbg + +if __name__ == '__main__': + c = C() + + c_refs = gc.get_referrers(c) + ref_ids = list(map(id,c_refs)) + + print('c referrers:',list(map(type,c_refs))) diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/simple.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/simple.py new file mode 100644 index 000000000..f0b060c07 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/simple.py @@ -0,0 +1,33 @@ +"""Simple example using doctests. + +This file just contains doctests both using plain python and yap_ipython prompts. +All tests should be loaded by nose. +""" + +def pyfunc(): + """Some pure python tests... + + >>> pyfunc() + 'pyfunc' + + >>> import os + + >>> 2+3 + 5 + + >>> for i in range(3): + ... print(i, end=' ') + ... print(i+1, end=' ') + ... + 0 1 1 2 2 3 + """ + return 'pyfunc' + + +def ipyfunc2(): + """Some pure python tests... + + >>> 1+1 + 2 + """ + return 'pyfunc2' diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/simplevars.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/simplevars.py new file mode 100644 index 000000000..cac0b7531 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/simplevars.py @@ -0,0 +1,2 @@ +x = 1 +print('x is:',x) diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/test_ipdoctest.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/test_ipdoctest.py new file mode 100644 index 000000000..9d148230a --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/test_ipdoctest.py @@ -0,0 +1,80 @@ +"""Tests for the ipdoctest machinery itself. + +Note: in a file named test_X, functions whose only test is their docstring (as +a doctest) and which have no test functionality of their own, should be called +'doctest_foo' instead of 'test_foo', otherwise they get double-counted (the +empty function call is counted as a test, which just inflates tests numbers +artificially). +""" +from yap_ipython.utils.py3compat import doctest_refactor_print + +@doctest_refactor_print +def doctest_simple(): + """ipdoctest must handle simple inputs + + In [1]: 1 + Out[1]: 1 + + In [2]: print 1 + 1 + """ + +@doctest_refactor_print +def doctest_multiline1(): + """The ipdoctest machinery must handle multiline examples gracefully. + + In [2]: for i in range(4): + ...: print i + ...: + 0 + 1 + 2 + 3 + """ + +@doctest_refactor_print +def doctest_multiline2(): + """Multiline examples that define functions and print output. + + In [7]: def f(x): + ...: return x+1 + ...: + + In [8]: f(1) + Out[8]: 2 + + In [9]: def g(x): + ...: print 'x is:',x + ...: + + In [10]: g(1) + x is: 1 + + In [11]: g('hello') + x is: hello + """ + + +def doctest_multiline3(): + """Multiline examples with blank lines. + + In [12]: def h(x): + ....: if x>1: + ....: return x**2 + ....: # To leave a blank line in the input, you must mark it + ....: # with a comment character: + ....: # + ....: # otherwise the doctest parser gets confused. + ....: else: + ....: return -1 + ....: + + In [13]: h(5) + Out[13]: 25 + + In [14]: h(1) + Out[14]: -1 + + In [15]: h(0) + Out[15]: -1 + """ diff --git a/packages/python/yap_kernel/yap_ipython/testing/plugin/test_refs.py b/packages/python/yap_kernel/yap_ipython/testing/plugin/test_refs.py new file mode 100644 index 000000000..50d085713 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/plugin/test_refs.py @@ -0,0 +1,46 @@ +"""Some simple tests for the plugin while running scripts. +""" +# Module imports +# Std lib +import inspect + +# Our own + +#----------------------------------------------------------------------------- +# Testing functions + +def test_trivial(): + """A trivial passing test.""" + pass + +def doctest_run(): + """Test running a trivial script. + + In [13]: run simplevars.py + x is: 1 + """ + +def doctest_runvars(): + """Test that variables defined in scripts get loaded correcly via %run. + + In [13]: run simplevars.py + x is: 1 + + In [14]: x + Out[14]: 1 + """ + +def doctest_ivars(): + """Test that variables defined interactively are picked up. + In [5]: zz=1 + + In [6]: zz + Out[6]: 1 + """ + +def doctest_refs(): + """DocTest reference holding issues when running scripts. + + In [32]: run show_refs.py + c referrers: [<... 'dict'>] + """ diff --git a/packages/python/yap_kernel/yap_ipython/testing/tests/__init__.py b/packages/python/yap_kernel/yap_ipython/testing/tests/__init__.py new file mode 100644 index 000000000..f751f68a9 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/tests/__init__.py @@ -0,0 +1,10 @@ +# encoding: utf-8 +__docformat__ = "restructuredtext en" +#------------------------------------------------------------------------------- +# Copyright (C) 2005 Fernando Perez +# Brian E Granger +# Benjamin Ragan-Kelley +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#------------------------------------------------------------------------------- diff --git a/packages/python/yap_kernel/yap_ipython/testing/tests/test_decorators.py b/packages/python/yap_kernel/yap_ipython/testing/tests/test_decorators.py new file mode 100644 index 000000000..2ddaeff2d --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/tests/test_decorators.py @@ -0,0 +1,164 @@ +"""Tests for the decorators we've created for yap_ipython. +""" + +# Module imports +# Std lib +import inspect +import sys + +# Third party +import nose.tools as nt + +# Our own +from yap_ipython.testing import decorators as dec + +#----------------------------------------------------------------------------- +# Utilities + +# Note: copied from OInspect, kept here so the testing stuff doesn't create +# circular dependencies and is easier to reuse. +def getargspec(obj): + """Get the names and default values of a function's arguments. + + A tuple of four things is returned: (args, varargs, varkw, defaults). + 'args' is a list of the argument names (it may contain nested lists). + 'varargs' and 'varkw' are the names of the * and ** arguments or None. + 'defaults' is an n-tuple of the default values of the last n arguments. + + Modified version of inspect.getargspec from the Python Standard + Library.""" + + if inspect.isfunction(obj): + func_obj = obj + elif inspect.ismethod(obj): + func_obj = obj.__func__ + else: + raise TypeError('arg is not a Python function') + args, varargs, varkw = inspect.getargs(func_obj.__code__) + return args, varargs, varkw, func_obj.__defaults__ + +#----------------------------------------------------------------------------- +# Testing functions + +@dec.as_unittest +def trivial(): + """A trivial test""" + pass + + +@dec.skip +def test_deliberately_broken(): + """A deliberately broken test - we want to skip this one.""" + 1/0 + +@dec.skip('Testing the skip decorator') +def test_deliberately_broken2(): + """Another deliberately broken test - we want to skip this one.""" + 1/0 + + +# Verify that we can correctly skip the doctest for a function at will, but +# that the docstring itself is NOT destroyed by the decorator. +def doctest_bad(x,y=1,**k): + """A function whose doctest we need to skip. + + >>> 1+1 + 3 + """ + print('x:',x) + print('y:',y) + print('k:',k) + + +def call_doctest_bad(): + """Check that we can still call the decorated functions. + + >>> doctest_bad(3,y=4) + x: 3 + y: 4 + k: {} + """ + pass + + +def test_skip_dt_decorator(): + """Doctest-skipping decorator should preserve the docstring. + """ + # Careful: 'check' must be a *verbatim* copy of the doctest_bad docstring! + check = """A function whose doctest we need to skip. + + >>> 1+1 + 3 + """ + # Fetch the docstring from doctest_bad after decoration. + val = doctest_bad.__doc__ + + nt.assert_equal(check,val,"doctest_bad docstrings don't match") + + +# Doctest skipping should work for class methods too +class FooClass(object): + """FooClass + + Example: + + >>> 1+1 + 2 + """ + + def __init__(self,x): + """Make a FooClass. + + Example: + + >>> f = FooClass(3) + junk + """ + print('Making a FooClass.') + self.x = x + + def bar(self,y): + """Example: + + >>> ff = FooClass(3) + >>> ff.bar(0) + boom! + >>> 1/0 + bam! + """ + return 1/y + + def baz(self,y): + """Example: + + >>> ff2 = FooClass(3) + Making a FooClass. + >>> ff2.baz(3) + True + """ + return self.x==y + + +def test_skip_dt_decorator2(): + """Doctest-skipping decorator should preserve function signature. + """ + # Hardcoded correct answer + dtargs = (['x', 'y'], None, 'k', (1,)) + # Introspect out the value + dtargsr = getargspec(doctest_bad) + assert dtargsr==dtargs, \ + "Incorrectly reconstructed args for doctest_bad: %s" % (dtargsr,) + + +@dec.skip_linux +def test_linux(): + nt.assert_false(sys.platform.startswith('linux'),"This test can't run under linux") + +@dec.skip_win32 +def test_win32(): + nt.assert_not_equal(sys.platform,'win32',"This test can't run under windows") + +@dec.skip_osx +def test_osx(): + nt.assert_not_equal(sys.platform,'darwin',"This test can't run under osx") + diff --git a/packages/python/yap_kernel/yap_ipython/testing/tests/test_ipunittest.py b/packages/python/yap_kernel/yap_ipython/testing/tests/test_ipunittest.py new file mode 100644 index 000000000..485674292 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/tests/test_ipunittest.py @@ -0,0 +1,137 @@ +"""Tests for yap_ipython's test support utilities. + +These are decorators that allow standalone functions and docstrings to be seen +as tests by unittest, replicating some of nose's functionality. Additionally, +yap_ipython-syntax docstrings can be auto-converted to '>>>' so that ipython +sessions can be copy-pasted as tests. + +This file can be run as a script, and it will call unittest.main(). We must +check that it works with unittest as well as with nose... + + +Notes: + +- Using nosetests --with-doctest --doctest-tests testfile.py + will find docstrings as tests wherever they are, even in methods. But + if we use ipython syntax in the docstrings, they must be decorated with + @ipdocstring. This is OK for test-only code, but not for user-facing + docstrings where we want to keep the ipython syntax. + +- Using nosetests --with-doctest file.py + also finds doctests if the file name doesn't have 'test' in it, because it is + treated like a normal module. But if nose treats the file like a test file, + then for normal classes to be doctested the extra --doctest-tests is + necessary. + +- running this script with python (it has a __main__ section at the end) misses + one docstring test, the one embedded in the Foo object method. Since our + approach relies on using decorators that create standalone TestCase + instances, it can only be used for functions, not for methods of objects. +Authors +------- + +- Fernando Perez +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2009-2011 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +from yap_ipython.testing.ipunittest import ipdoctest, ipdocstring +from yap_ipython.utils.py3compat import doctest_refactor_print + +#----------------------------------------------------------------------------- +# Test classes and functions +#----------------------------------------------------------------------------- +@ipdoctest +@doctest_refactor_print +def simple_dt(): + """ + >>> print 1+1 + 2 + """ + + +@ipdoctest +@doctest_refactor_print +def ipdt_flush(): + """ +In [20]: print 1 +1 + +In [26]: for i in range(4): + ....: print i + ....: + ....: +0 +1 +2 +3 + +In [27]: 3+4 +Out[27]: 7 +""" + + +@ipdoctest +@doctest_refactor_print +def ipdt_indented_test(): + """ + In [20]: print 1 + 1 + + In [26]: for i in range(4): + ....: print i + ....: + ....: + 0 + 1 + 2 + 3 + + In [27]: 3+4 + Out[27]: 7 + """ + + +class Foo(object): + """For methods, the normal decorator doesn't work. + + But rewriting the docstring with ip2py does, *but only if using nose + --with-doctest*. Do we want to have that as a dependency? + """ + + @ipdocstring + @doctest_refactor_print + def ipdt_method(self): + """ + In [20]: print 1 + 1 + + In [26]: for i in range(4): + ....: print i + ....: + ....: + 0 + 1 + 2 + 3 + + In [27]: 3+4 + Out[27]: 7 + """ + + @doctest_refactor_print + def normaldt_method(self): + """ + >>> print 1+1 + 2 + """ diff --git a/packages/python/yap_kernel/yap_ipython/testing/tests/test_tools.py b/packages/python/yap_kernel/yap_ipython/testing/tests/test_tools.py new file mode 100644 index 000000000..a2307d6fb --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/testing/tests/test_tools.py @@ -0,0 +1,136 @@ +# encoding: utf-8 +""" +Tests for testing.tools +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2011 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import os +import unittest + +import nose.tools as nt + +from yap_ipython.testing import decorators as dec +from yap_ipython.testing import tools as tt + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + +@dec.skip_win32 +def test_full_path_posix(): + spath = '/foo/bar.py' + result = tt.full_path(spath,['a.txt','b.txt']) + nt.assert_equal(result, ['/foo/a.txt', '/foo/b.txt']) + spath = '/foo' + result = tt.full_path(spath,['a.txt','b.txt']) + nt.assert_equal(result, ['/a.txt', '/b.txt']) + result = tt.full_path(spath,'a.txt') + nt.assert_equal(result, ['/a.txt']) + + +@dec.skip_if_not_win32 +def test_full_path_win32(): + spath = 'c:\\foo\\bar.py' + result = tt.full_path(spath,['a.txt','b.txt']) + nt.assert_equal(result, ['c:\\foo\\a.txt', 'c:\\foo\\b.txt']) + spath = 'c:\\foo' + result = tt.full_path(spath,['a.txt','b.txt']) + nt.assert_equal(result, ['c:\\a.txt', 'c:\\b.txt']) + result = tt.full_path(spath,'a.txt') + nt.assert_equal(result, ['c:\\a.txt']) + + +def test_parser(): + err = ("FAILED (errors=1)", 1, 0) + fail = ("FAILED (failures=1)", 0, 1) + both = ("FAILED (errors=1, failures=1)", 1, 1) + for txt, nerr, nfail in [err, fail, both]: + nerr1, nfail1 = tt.parse_test_output(txt) + nt.assert_equal(nerr, nerr1) + nt.assert_equal(nfail, nfail1) + + +def test_temp_pyfile(): + src = 'pass\n' + fname, fh = tt.temp_pyfile(src) + assert os.path.isfile(fname) + fh.close() + with open(fname) as fh2: + src2 = fh2.read() + nt.assert_equal(src2, src) + +class TestAssertPrints(unittest.TestCase): + def test_passing(self): + with tt.AssertPrints("abc"): + print("abcd") + print("def") + print(b"ghi") + + def test_failing(self): + def func(): + with tt.AssertPrints("abc"): + print("acd") + print("def") + print(b"ghi") + + self.assertRaises(AssertionError, func) + + +class Test_ipexec_validate(unittest.TestCase, tt.TempFileMixin): + def test_main_path(self): + """Test with only stdout results. + """ + self.mktmp("print('A')\n" + "print('B')\n" + ) + out = "A\nB" + tt.ipexec_validate(self.fname, out) + + def test_main_path2(self): + """Test with only stdout results, expecting windows line endings. + """ + self.mktmp("print('A')\n" + "print('B')\n" + ) + out = "A\r\nB" + tt.ipexec_validate(self.fname, out) + + def test_exception_path(self): + """Test exception path in exception_validate. + """ + self.mktmp("import sys\n" + "print('A')\n" + "print('B')\n" + "print('C', file=sys.stderr)\n" + "print('D', file=sys.stderr)\n" + ) + out = "A\nB" + tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\nD") + + def test_exception_path2(self): + """Test exception path in exception_validate, expecting windows line endings. + """ + self.mktmp("import sys\n" + "print('A')\n" + "print('B')\n" + "print('C', file=sys.stderr)\n" + "print('D', file=sys.stderr)\n" + ) + out = "A\r\nB" + tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\r\nD") + + + def tearDown(self): + # tear down correctly the mixin, + # unittest.TestCase.tearDown does nothing + tt.TempFileMixin.tearDown(self) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/__init__.py b/packages/python/yap_kernel/yap_ipython/utils/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_capture.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_capture.py new file mode 100644 index 000000000..8fc7e1bdd --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_capture.py @@ -0,0 +1,159 @@ +# encoding: utf-8 +"""Tests for yap_ipython.utils.capture""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2013 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + + +import sys + +import nose.tools as nt + +from yap_ipython.utils import capture + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +_mime_map = dict( + _repr_png_="image/png", + _repr_jpeg_="image/jpeg", + _repr_svg_="image/svg+xml", + _repr_html_="text/html", + _repr_json_="application/json", + _repr_javascript_="application/javascript", +) + +basic_data = { + 'image/png' : b'binarydata', + 'text/html' : "bold", +} +basic_metadata = { + 'image/png' : { + 'width' : 10, + 'height' : 20, + }, +} + +full_data = { + 'image/png' : b'binarydata', + 'image/jpeg' : b'binarydata', + 'image/svg+xml' : "", + 'text/html' : "bold", + 'application/javascript' : "alert();", + 'application/json' : "{}", +} +full_metadata = { + 'image/png' : {"png" : "exists"}, + 'image/jpeg' : {"jpeg" : "exists"}, + 'image/svg+xml' : {"svg" : "exists"}, + 'text/html' : {"html" : "exists"}, + 'application/javascript' : {"js" : "exists"}, + 'application/json' : {"json" : "exists"}, +} + +hello_stdout = "hello, stdout" +hello_stderr = "hello, stderr" + +#----------------------------------------------------------------------------- +# Test Functions +#----------------------------------------------------------------------------- + +def test_rich_output_empty(): + """RichOutput with no args""" + rich = capture.RichOutput() + for method, mime in _mime_map.items(): + yield nt.assert_equal, getattr(rich, method)(), None + +def test_rich_output(): + """test RichOutput basics""" + data = basic_data + metadata = basic_metadata + rich = capture.RichOutput(data=data, metadata=metadata) + yield nt.assert_equal, rich._repr_html_(), data['text/html'] + yield nt.assert_equal, rich._repr_png_(), (data['image/png'], metadata['image/png']) + yield nt.assert_equal, rich._repr_latex_(), None + yield nt.assert_equal, rich._repr_javascript_(), None + yield nt.assert_equal, rich._repr_svg_(), None + +def test_rich_output_no_metadata(): + """test RichOutput with no metadata""" + data = full_data + rich = capture.RichOutput(data=data) + for method, mime in _mime_map.items(): + yield nt.assert_equal, getattr(rich, method)(), data[mime] + +def test_rich_output_metadata(): + """test RichOutput with metadata""" + data = full_data + metadata = full_metadata + rich = capture.RichOutput(data=data, metadata=metadata) + for method, mime in _mime_map.items(): + yield nt.assert_equal, getattr(rich, method)(), (data[mime], metadata[mime]) + +def test_rich_output_display(): + """test RichOutput.display + + This is a bit circular, because we are actually using the capture code we are testing + to test itself. + """ + data = full_data + rich = capture.RichOutput(data=data) + with capture.capture_output() as cap: + rich.display() + yield nt.assert_equal, len(cap.outputs), 1 + rich2 = cap.outputs[0] + yield nt.assert_equal, rich2.data, rich.data + yield nt.assert_equal, rich2.metadata, rich.metadata + +def test_capture_output(): + """capture_output works""" + rich = capture.RichOutput(data=full_data) + with capture.capture_output() as cap: + print(hello_stdout, end="") + print(hello_stderr, end="", file=sys.stderr) + rich.display() + yield nt.assert_equal, hello_stdout, cap.stdout + yield nt.assert_equal, hello_stderr, cap.stderr + +def test_capture_output_no_stdout(): + """test capture_output(stdout=False)""" + rich = capture.RichOutput(data=full_data) + with capture.capture_output(stdout=False) as cap: + print(hello_stdout, end="") + print(hello_stderr, end="", file=sys.stderr) + rich.display() + yield nt.assert_equal, "", cap.stdout + yield nt.assert_equal, hello_stderr, cap.stderr + yield nt.assert_equal, len(cap.outputs), 1 + +def test_capture_output_no_stderr(): + """test capture_output(stderr=False)""" + rich = capture.RichOutput(data=full_data) + # add nested capture_output so stderr doesn't make it to nose output + with capture.capture_output(), capture.capture_output(stderr=False) as cap: + print(hello_stdout, end="") + print(hello_stderr, end="", file=sys.stderr) + rich.display() + yield nt.assert_equal, hello_stdout, cap.stdout + yield nt.assert_equal, "", cap.stderr + yield nt.assert_equal, len(cap.outputs), 1 + +def test_capture_output_no_display(): + """test capture_output(display=False)""" + rich = capture.RichOutput(data=full_data) + with capture.capture_output(display=False) as cap: + print(hello_stdout, end="") + print(hello_stderr, end="", file=sys.stderr) + rich.display() + yield nt.assert_equal, hello_stdout, cap.stdout + yield nt.assert_equal, hello_stderr, cap.stderr + yield nt.assert_equal, cap.outputs, [] \ No newline at end of file diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_decorators.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_decorators.py new file mode 100644 index 000000000..f2c8b79f8 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_decorators.py @@ -0,0 +1,10 @@ +from yap_ipython.utils import decorators + +def test_flag_calls(): + @decorators.flag_calls + def f(): + pass + + assert not f.called + f() + assert f.called \ No newline at end of file diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_dir2.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_dir2.py new file mode 100644 index 000000000..2f505fe0c --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_dir2.py @@ -0,0 +1,58 @@ +import nose.tools as nt +from yap_ipython.utils.dir2 import dir2 + + +class Base(object): + x = 1 + z = 23 + + +def test_base(): + res = dir2(Base()) + assert ('x' in res) + assert ('z' in res) + assert ('y' not in res) + assert ('__class__' in res) + nt.assert_equal(res.count('x'), 1) + nt.assert_equal(res.count('__class__'), 1) + +def test_SubClass(): + + class SubClass(Base): + y = 2 + + res = dir2(SubClass()) + assert ('y' in res) + nt.assert_equal(res.count('y'), 1) + nt.assert_equal(res.count('x'), 1) + + +def test_SubClass_with_trait_names_attr(): + # usecase: trait_names is used in a class describing psychological classification + + class SubClass(Base): + y = 2 + trait_names = 44 + + res = dir2(SubClass()) + assert('trait_names' in res) + + +def test_misbehaving_object_without_trait_names(): + # dir2 shouldn't raise even when objects are dumb and raise + # something other than AttribteErrors on bad getattr. + + class MisbehavingGetattr(object): + def __getattr__(self): + raise KeyError("I should be caught") + + def some_method(self): + pass + + class SillierWithDir(MisbehavingGetattr): + def __dir__(self): + return ['some_method'] + + for bad_klass in (MisbehavingGetattr, SillierWithDir): + res = dir2(bad_klass()) + assert('some_method' in res) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_imports.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_imports.py new file mode 100644 index 000000000..c4e6f066b --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_imports.py @@ -0,0 +1,20 @@ +# encoding: utf-8 + +def test_import_coloransi(): + from yap_ipython.utils import coloransi + +def test_import_generics(): + from yap_ipython.utils import generics + +def test_import_ipstruct(): + from yap_ipython.utils import ipstruct + +def test_import_PyColorize(): + from yap_ipython.utils import PyColorize + +def test_import_strdispatch(): + from yap_ipython.utils import strdispatch + +def test_import_wildcard(): + from yap_ipython.utils import wildcard + diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_importstring.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_importstring.py new file mode 100644 index 000000000..57be3728b --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_importstring.py @@ -0,0 +1,39 @@ +"""Tests for yap_ipython.utils.importstring.""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2013 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import nose.tools as nt + +from yap_ipython.utils.importstring import import_item + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + +def test_import_plain(): + "Test simple imports" + import os + os2 = import_item('os') + nt.assert_true(os is os2) + + +def test_import_nested(): + "Test nested imports from the stdlib" + from os import path + path2 = import_item('os.path') + nt.assert_true(path is path2) + + +def test_import_raises(): + "Test that failing imports raise the right exception" + nt.assert_raises(ImportError, import_item, 'yap_ipython.foobar') + diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_io.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_io.py new file mode 100644 index 000000000..a2d2a9e68 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_io.py @@ -0,0 +1,94 @@ +# encoding: utf-8 +"""Tests for io.py""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + + +import io as stdlib_io +import os.path +import stat +import sys +from io import StringIO + +from subprocess import Popen, PIPE +import unittest + +import nose.tools as nt + +from yap_ipython.testing.decorators import skipif, skip_win32 +from yap_ipython.utils.io import IOStream, Tee, capture_output +from yap_ipython.utils.py3compat import doctest_refactor_print +from yap_ipython.utils.tempdir import TemporaryDirectory + + +def test_tee_simple(): + "Very simple check with stdout only" + chan = StringIO() + text = 'Hello' + tee = Tee(chan, channel='stdout') + print(text, file=chan) + nt.assert_equal(chan.getvalue(), text+"\n") + + +class TeeTestCase(unittest.TestCase): + + def tchan(self, channel, check='close'): + trap = StringIO() + chan = StringIO() + text = 'Hello' + + std_ori = getattr(sys, channel) + setattr(sys, channel, trap) + + tee = Tee(chan, channel=channel) + print(text, end='', file=chan) + setattr(sys, channel, std_ori) + trap_val = trap.getvalue() + nt.assert_equal(chan.getvalue(), text) + if check=='close': + tee.close() + else: + del tee + + def test(self): + for chan in ['stdout', 'stderr']: + for check in ['close', 'del']: + self.tchan(chan, check) + +def test_io_init(): + """Test that io.stdin/out/err exist at startup""" + for name in ('stdin', 'stdout', 'stderr'): + cmd = doctest_refactor_print("from yap_ipython.utils import io;print io.%s.__class__"%name) + p = Popen([sys.executable, '-c', cmd], + stdout=PIPE) + p.wait() + classname = p.stdout.read().strip().decode('ascii') + # __class__ is a reference to the class object in Python 3, so we can't + # just test for string equality. + assert 'yap_ipython.utils.io.IOStream' in classname, classname + +def test_IOStream_init(): + """IOStream initializes from a file-like object missing attributes. """ + # Cause a failure from getattr and dir(). (Issue #6386) + class BadStringIO(StringIO): + def __dir__(self): + attrs = super(StringIO, self).__dir__() + attrs.append('name') + return attrs + + iostream = IOStream(BadStringIO()) + iostream.write('hi, bad iostream\n') + assert not hasattr(iostream, 'name') + +def test_capture_output(): + """capture_output() context works""" + + with capture_output() as io: + print('hi, stdout') + print('hi, stderr', file=sys.stderr) + + nt.assert_equal(io.stdout, 'hi, stdout\n') + nt.assert_equal(io.stderr, 'hi, stderr\n') + + diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_module_paths.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_module_paths.py new file mode 100644 index 000000000..3228c3cf8 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_module_paths.py @@ -0,0 +1,127 @@ +# encoding: utf-8 +"""Tests for yap_ipython.utils.module_paths.py""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2011 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + + +import os +import shutil +import sys +import tempfile + +from os.path import join, abspath, split + +from yap_ipython.testing.tools import make_tempfile + +import yap_ipython.utils.module_paths as mp + +import nose.tools as nt + +env = os.environ +TEST_FILE_PATH = split(abspath(__file__))[0] +TMP_TEST_DIR = tempfile.mkdtemp() +# +# Setup/teardown functions/decorators +# + +old_syspath = sys.path + +def make_empty_file(fname): + f = open(fname, 'w') + f.close() + + +def setup(): + """Setup testenvironment for the module: + + """ + # Do not mask exceptions here. In particular, catching WindowsError is a + # problem because that exception is only defined on Windows... + os.makedirs(join(TMP_TEST_DIR, "xmod")) + os.makedirs(join(TMP_TEST_DIR, "nomod")) + make_empty_file(join(TMP_TEST_DIR, "xmod/__init__.py")) + make_empty_file(join(TMP_TEST_DIR, "xmod/sub.py")) + make_empty_file(join(TMP_TEST_DIR, "pack.py")) + make_empty_file(join(TMP_TEST_DIR, "packpyc.pyc")) + sys.path = [TMP_TEST_DIR] + +def teardown(): + """Teardown testenvironment for the module: + + - Remove tempdir + - restore sys.path + """ + # Note: we remove the parent test dir, which is the root of all test + # subdirs we may have created. Use shutil instead of os.removedirs, so + # that non-empty directories are all recursively removed. + shutil.rmtree(TMP_TEST_DIR) + sys.path = old_syspath + + +def test_get_init_1(): + """See if get_init can find __init__.py in this testdir""" + with make_tempfile(join(TMP_TEST_DIR, "__init__.py")): + assert mp.get_init(TMP_TEST_DIR) + +def test_get_init_2(): + """See if get_init can find __init__.pyw in this testdir""" + with make_tempfile(join(TMP_TEST_DIR, "__init__.pyw")): + assert mp.get_init(TMP_TEST_DIR) + +def test_get_init_3(): + """get_init can't find __init__.pyc in this testdir""" + with make_tempfile(join(TMP_TEST_DIR, "__init__.pyc")): + nt.assert_is_none(mp.get_init(TMP_TEST_DIR)) + +def test_get_init_4(): + """get_init can't find __init__ in empty testdir""" + nt.assert_is_none(mp.get_init(TMP_TEST_DIR)) + + +def test_find_mod_1(): + modpath = join(TMP_TEST_DIR, "xmod", "__init__.py") + nt.assert_equal(mp.find_mod("xmod"), modpath) + +def test_find_mod_2(): + modpath = join(TMP_TEST_DIR, "xmod", "__init__.py") + nt.assert_equal(mp.find_mod("xmod"), modpath) + +def test_find_mod_3(): + modpath = join(TMP_TEST_DIR, "xmod", "sub.py") + nt.assert_equal(mp.find_mod("xmod.sub"), modpath) + +def test_find_mod_4(): + modpath = join(TMP_TEST_DIR, "pack.py") + nt.assert_equal(mp.find_mod("pack"), modpath) + +def test_find_mod_5(): + modpath = join(TMP_TEST_DIR, "packpyc.pyc") + nt.assert_equal(mp.find_mod("packpyc"), modpath) + +def test_find_module_1(): + modpath = join(TMP_TEST_DIR, "xmod") + nt.assert_equal(mp.find_module("xmod"), modpath) + +def test_find_module_2(): + """Testing sys.path that is empty""" + nt.assert_is_none(mp.find_module("xmod", [])) + +def test_find_module_3(): + """Testing sys.path that is empty""" + nt.assert_is_none(mp.find_module(None, None)) + +def test_find_module_4(): + """Testing sys.path that is empty""" + nt.assert_is_none(mp.find_module(None)) + +def test_find_module_5(): + nt.assert_is_none(mp.find_module("xmod.nopack")) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_openpy.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_openpy.py new file mode 100644 index 000000000..08a29c7e5 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_openpy.py @@ -0,0 +1,39 @@ +import io +import os.path +import nose.tools as nt + +from yap_ipython.utils import openpy + +mydir = os.path.dirname(__file__) +nonascii_path = os.path.join(mydir, '../../core/tests/nonascii.py') + +def test_detect_encoding(): + f = open(nonascii_path, 'rb') + enc, lines = openpy.detect_encoding(f.readline) + nt.assert_equal(enc, 'iso-8859-5') + +def test_read_file(): + read_specified_enc = io.open(nonascii_path, encoding='iso-8859-5').read() + read_detected_enc = openpy.read_py_file(nonascii_path, skip_encoding_cookie=False) + nt.assert_equal(read_detected_enc, read_specified_enc) + assert u'coding: iso-8859-5' in read_detected_enc + + read_strip_enc_cookie = openpy.read_py_file(nonascii_path, skip_encoding_cookie=True) + assert u'coding: iso-8859-5' not in read_strip_enc_cookie + +def test_source_to_unicode(): + with io.open(nonascii_path, 'rb') as f: + source_bytes = f.read() + nt.assert_equal(openpy.source_to_unicode(source_bytes, skip_encoding_cookie=False).splitlines(), + source_bytes.decode('iso-8859-5').splitlines()) + + source_no_cookie = openpy.source_to_unicode(source_bytes, skip_encoding_cookie=True) + nt.assert_not_in(u'coding: iso-8859-5', source_no_cookie) + +def test_list_readline(): + l = ['a', 'b'] + readline = openpy._list_readline(l) + nt.assert_equal(readline(), 'a') + nt.assert_equal(readline(), 'b') + with nt.assert_raises(StopIteration): + readline() \ No newline at end of file diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_path.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_path.py new file mode 100644 index 000000000..a02272e77 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_path.py @@ -0,0 +1,481 @@ +# encoding: utf-8 +"""Tests for yap_ipython.utils.path.py""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import os +import shutil +import sys +import tempfile +import unittest +from contextlib import contextmanager +from unittest.mock import patch +from os.path import join, abspath +from imp import reload + +from nose import SkipTest, with_setup +import nose.tools as nt + +import yap_ipython +from yap_ipython import paths +from yap_ipython.testing import decorators as dec +from yap_ipython.testing.decorators import (skip_if_not_win32, skip_win32, + onlyif_unicode_paths,) +from yap_ipython.testing.tools import make_tempfile, AssertPrints +from yap_ipython.utils import path +from yap_ipython.utils.tempdir import TemporaryDirectory + +# Platform-dependent imports +try: + import winreg as wreg +except ImportError: + #Fake _winreg module on non-windows platforms + import types + wr_name = "winreg" + sys.modules[wr_name] = types.ModuleType(wr_name) + try: + import winreg as wreg + except ImportError: + import _winreg as wreg + #Add entries that needs to be stubbed by the testing code + (wreg.OpenKey, wreg.QueryValueEx,) = (None, None) + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- +env = os.environ +TMP_TEST_DIR = tempfile.mkdtemp() +HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir") +# +# Setup/teardown functions/decorators +# + +def setup(): + """Setup testenvironment for the module: + + - Adds dummy home dir tree + """ + # Do not mask exceptions here. In particular, catching WindowsError is a + # problem because that exception is only defined on Windows... + os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython')) + + +def teardown(): + """Teardown testenvironment for the module: + + - Remove dummy home dir tree + """ + # Note: we remove the parent test dir, which is the root of all test + # subdirs we may have created. Use shutil instead of os.removedirs, so + # that non-empty directories are all recursively removed. + shutil.rmtree(TMP_TEST_DIR) + + +def setup_environment(): + """Setup testenvironment for some functions that are tested + in this module. In particular this functions stores attributes + and other things that we need to stub in some test functions. + This needs to be done on a function level and not module level because + each testfunction needs a pristine environment. + """ + global oldstuff, platformstuff + oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, yap_ipython.__file__, os.getcwd()) + +def teardown_environment(): + """Restore things that were remembered by the setup_environment function + """ + (oldenv, os.name, sys.platform, path.get_home_dir, yap_ipython.__file__, old_wd) = oldstuff + os.chdir(old_wd) + reload(path) + + for key in list(env): + if key not in oldenv: + del env[key] + env.update(oldenv) + if hasattr(sys, 'frozen'): + del sys.frozen + +# Build decorator that uses the setup_environment/setup_environment +with_environment = with_setup(setup_environment, teardown_environment) + +@skip_if_not_win32 +@with_environment +def test_get_home_dir_1(): + """Testcase for py2exe logic, un-compressed lib + """ + unfrozen = path.get_home_dir() + sys.frozen = True + + #fake filename for yap_ipython.__init__ + yap_ipython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/yap_ipython/__init__.py")) + + home_dir = path.get_home_dir() + nt.assert_equal(home_dir, unfrozen) + + +@skip_if_not_win32 +@with_environment +def test_get_home_dir_2(): + """Testcase for py2exe logic, compressed lib + """ + unfrozen = path.get_home_dir() + sys.frozen = True + #fake filename for yap_ipython.__init__ + yap_ipython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/yap_ipython/__init__.py")).lower() + + home_dir = path.get_home_dir(True) + nt.assert_equal(home_dir, unfrozen) + + +@with_environment +def test_get_home_dir_3(): + """get_home_dir() uses $HOME if set""" + env["HOME"] = HOME_TEST_DIR + home_dir = path.get_home_dir(True) + # get_home_dir expands symlinks + nt.assert_equal(home_dir, os.path.realpath(env["HOME"])) + + +@with_environment +def test_get_home_dir_4(): + """get_home_dir() still works if $HOME is not set""" + + if 'HOME' in env: del env['HOME'] + # this should still succeed, but we don't care what the answer is + home = path.get_home_dir(False) + +@with_environment +def test_get_home_dir_5(): + """raise HomeDirError if $HOME is specified, but not a writable dir""" + env['HOME'] = abspath(HOME_TEST_DIR+'garbage') + # set os.name = posix, to prevent My Documents fallback on Windows + os.name = 'posix' + nt.assert_raises(path.HomeDirError, path.get_home_dir, True) + +# Should we stub wreg fully so we can run the test on all platforms? +@skip_if_not_win32 +@with_environment +def test_get_home_dir_8(): + """Using registry hack for 'My Documents', os=='nt' + + HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing. + """ + os.name = 'nt' + # Remove from stub environment all keys that may be set + for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']: + env.pop(key, None) + + class key: + def Close(self): + pass + + with patch.object(wreg, 'OpenKey', return_value=key()), \ + patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]): + home_dir = path.get_home_dir() + nt.assert_equal(home_dir, abspath(HOME_TEST_DIR)) + +@with_environment +def test_get_xdg_dir_0(): + """test_get_xdg_dir_0, check xdg_dir""" + reload(path) + path._writable_dir = lambda path: True + path.get_home_dir = lambda : 'somewhere' + os.name = "posix" + sys.platform = "linux2" + env.pop('IPYTHON_DIR', None) + env.pop('IPYTHONDIR', None) + env.pop('XDG_CONFIG_HOME', None) + + nt.assert_equal(path.get_xdg_dir(), os.path.join('somewhere', '.config')) + + +@with_environment +def test_get_xdg_dir_1(): + """test_get_xdg_dir_1, check nonexistent xdg_dir""" + reload(path) + path.get_home_dir = lambda : HOME_TEST_DIR + os.name = "posix" + sys.platform = "linux2" + env.pop('IPYTHON_DIR', None) + env.pop('IPYTHONDIR', None) + env.pop('XDG_CONFIG_HOME', None) + nt.assert_equal(path.get_xdg_dir(), None) + +@with_environment +def test_get_xdg_dir_2(): + """test_get_xdg_dir_2, check xdg_dir default to ~/.config""" + reload(path) + path.get_home_dir = lambda : HOME_TEST_DIR + os.name = "posix" + sys.platform = "linux2" + env.pop('IPYTHON_DIR', None) + env.pop('IPYTHONDIR', None) + env.pop('XDG_CONFIG_HOME', None) + cfgdir=os.path.join(path.get_home_dir(), '.config') + if not os.path.exists(cfgdir): + os.makedirs(cfgdir) + + nt.assert_equal(path.get_xdg_dir(), cfgdir) + +@with_environment +def test_get_xdg_dir_3(): + """test_get_xdg_dir_3, check xdg_dir not used on OS X""" + reload(path) + path.get_home_dir = lambda : HOME_TEST_DIR + os.name = "posix" + sys.platform = "darwin" + env.pop('IPYTHON_DIR', None) + env.pop('IPYTHONDIR', None) + env.pop('XDG_CONFIG_HOME', None) + cfgdir=os.path.join(path.get_home_dir(), '.config') + if not os.path.exists(cfgdir): + os.makedirs(cfgdir) + + nt.assert_equal(path.get_xdg_dir(), None) + +def test_filefind(): + """Various tests for filefind""" + f = tempfile.NamedTemporaryFile() + # print 'fname:',f.name + alt_dirs = paths.get_ipython_dir() + t = path.filefind(f.name, alt_dirs) + # print 'found:',t + + +@dec.skip_if_not_win32 +def test_get_long_path_name_win32(): + with TemporaryDirectory() as tmpdir: + + # Make a long path. Expands the path of tmpdir prematurely as it may already have a long + # path component, so ensure we include the long form of it + long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name') + os.makedirs(long_path) + + # Test to see if the short path evaluates correctly. + short_path = os.path.join(tmpdir, 'THISIS~1') + evaluated_path = path.get_long_path_name(short_path) + nt.assert_equal(evaluated_path.lower(), long_path.lower()) + + +@dec.skip_win32 +def test_get_long_path_name(): + p = path.get_long_path_name('/usr/local') + nt.assert_equal(p,'/usr/local') + +@dec.skip_win32 # can't create not-user-writable dir on win +@with_environment +def test_not_writable_ipdir(): + tmpdir = tempfile.mkdtemp() + os.name = "posix" + env.pop('IPYTHON_DIR', None) + env.pop('IPYTHONDIR', None) + env.pop('XDG_CONFIG_HOME', None) + env['HOME'] = tmpdir + ipdir = os.path.join(tmpdir, '.ipython') + os.mkdir(ipdir, 0o555) + try: + open(os.path.join(ipdir, "_foo_"), 'w').close() + except IOError: + pass + else: + # I can still write to an unwritable dir, + # assume I'm root and skip the test + raise SkipTest("I can't create directories that I can't write to") + with AssertPrints('is not a writable location', channel='stderr'): + ipdir = paths.get_ipython_dir() + env.pop('IPYTHON_DIR', None) + +@with_environment +def test_get_py_filename(): + os.chdir(TMP_TEST_DIR) + with make_tempfile('foo.py'): + nt.assert_equal(path.get_py_filename('foo.py'), 'foo.py') + nt.assert_equal(path.get_py_filename('foo'), 'foo.py') + with make_tempfile('foo'): + nt.assert_equal(path.get_py_filename('foo'), 'foo') + nt.assert_raises(IOError, path.get_py_filename, 'foo.py') + nt.assert_raises(IOError, path.get_py_filename, 'foo') + nt.assert_raises(IOError, path.get_py_filename, 'foo.py') + true_fn = 'foo with spaces.py' + with make_tempfile(true_fn): + nt.assert_equal(path.get_py_filename('foo with spaces'), true_fn) + nt.assert_equal(path.get_py_filename('foo with spaces.py'), true_fn) + nt.assert_raises(IOError, path.get_py_filename, '"foo with spaces.py"') + nt.assert_raises(IOError, path.get_py_filename, "'foo with spaces.py'") + +@onlyif_unicode_paths +def test_unicode_in_filename(): + """When a file doesn't exist, the exception raised should be safe to call + str() on - i.e. in Python 2 it must only have ASCII characters. + + https://github.com/ipython/ipython/issues/875 + """ + try: + # these calls should not throw unicode encode exceptions + path.get_py_filename('fooéè.py', force_win32=False) + except IOError as ex: + str(ex) + + +class TestShellGlob(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.filenames_start_with_a = ['a0', 'a1', 'a2'] + cls.filenames_end_with_b = ['0b', '1b', '2b'] + cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b + cls.tempdir = TemporaryDirectory() + td = cls.tempdir.name + + with cls.in_tempdir(): + # Create empty files + for fname in cls.filenames: + open(os.path.join(td, fname), 'w').close() + + @classmethod + def tearDownClass(cls): + cls.tempdir.cleanup() + + @classmethod + @contextmanager + def in_tempdir(cls): + save = os.getcwd() + try: + os.chdir(cls.tempdir.name) + yield + finally: + os.chdir(save) + + def check_match(self, patterns, matches): + with self.in_tempdir(): + # glob returns unordered list. that's why sorted is required. + nt.assert_equal(sorted(path.shellglob(patterns)), + sorted(matches)) + + def common_cases(self): + return [ + (['*'], self.filenames), + (['a*'], self.filenames_start_with_a), + (['*c'], ['*c']), + (['*', 'a*', '*b', '*c'], self.filenames + + self.filenames_start_with_a + + self.filenames_end_with_b + + ['*c']), + (['a[012]'], self.filenames_start_with_a), + ] + + @skip_win32 + def test_match_posix(self): + for (patterns, matches) in self.common_cases() + [ + ([r'\*'], ['*']), + ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a), + ([r'a\[012]'], ['a[012]']), + ]: + yield (self.check_match, patterns, matches) + + @skip_if_not_win32 + def test_match_windows(self): + for (patterns, matches) in self.common_cases() + [ + # In windows, backslash is interpreted as path + # separator. Therefore, you can't escape glob + # using it. + ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a), + ([r'a\[012]'], [r'a\[012]']), + ]: + yield (self.check_match, patterns, matches) + + +def test_unescape_glob(): + nt.assert_equal(path.unescape_glob(r'\*\[\!\]\?'), '*[!]?') + nt.assert_equal(path.unescape_glob(r'\\*'), r'\*') + nt.assert_equal(path.unescape_glob(r'\\\*'), r'\*') + nt.assert_equal(path.unescape_glob(r'\\a'), r'\a') + nt.assert_equal(path.unescape_glob(r'\a'), r'\a') + + +@onlyif_unicode_paths +def test_ensure_dir_exists(): + with TemporaryDirectory() as td: + d = os.path.join(td, '∂ir') + path.ensure_dir_exists(d) # create it + assert os.path.isdir(d) + path.ensure_dir_exists(d) # no-op + f = os.path.join(td, 'ƒile') + open(f, 'w').close() # touch + with nt.assert_raises(IOError): + path.ensure_dir_exists(f) + +class TestLinkOrCopy(object): + def setUp(self): + self.tempdir = TemporaryDirectory() + self.src = self.dst("src") + with open(self.src, "w") as f: + f.write("Hello, world!") + + def tearDown(self): + self.tempdir.cleanup() + + def dst(self, *args): + return os.path.join(self.tempdir.name, *args) + + def assert_inode_not_equal(self, a, b): + nt.assert_not_equal(os.stat(a).st_ino, os.stat(b).st_ino, + "%r and %r do reference the same indoes" %(a, b)) + + def assert_inode_equal(self, a, b): + nt.assert_equal(os.stat(a).st_ino, os.stat(b).st_ino, + "%r and %r do not reference the same indoes" %(a, b)) + + def assert_content_equal(self, a, b): + with open(a) as a_f: + with open(b) as b_f: + nt.assert_equal(a_f.read(), b_f.read()) + + @skip_win32 + def test_link_successful(self): + dst = self.dst("target") + path.link_or_copy(self.src, dst) + self.assert_inode_equal(self.src, dst) + + @skip_win32 + def test_link_into_dir(self): + dst = self.dst("some_dir") + os.mkdir(dst) + path.link_or_copy(self.src, dst) + expected_dst = self.dst("some_dir", os.path.basename(self.src)) + self.assert_inode_equal(self.src, expected_dst) + + @skip_win32 + def test_target_exists(self): + dst = self.dst("target") + open(dst, "w").close() + path.link_or_copy(self.src, dst) + self.assert_inode_equal(self.src, dst) + + @skip_win32 + def test_no_link(self): + real_link = os.link + try: + del os.link + dst = self.dst("target") + path.link_or_copy(self.src, dst) + self.assert_content_equal(self.src, dst) + self.assert_inode_not_equal(self.src, dst) + finally: + os.link = real_link + + @skip_if_not_win32 + def test_windows(self): + dst = self.dst("target") + path.link_or_copy(self.src, dst) + self.assert_content_equal(self.src, dst) + + def test_link_twice(self): + # Linking the same file twice shouldn't leave duplicates around. + # See https://github.com/ipython/ipython/issues/6450 + dst = self.dst('target') + path.link_or_copy(self.src, dst) + path.link_or_copy(self.src, dst) + self.assert_inode_equal(self.src, dst) + nt.assert_equal(sorted(os.listdir(self.tempdir.name)), ['src', 'target']) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_process.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_process.py new file mode 100644 index 000000000..6a6e77d09 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_process.py @@ -0,0 +1,145 @@ +# encoding: utf-8 +""" +Tests for platutils.py +""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2008-2011 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import sys +import os +from unittest import TestCase + +import nose.tools as nt + +from yap_ipython.utils.process import (find_cmd, FindCmdError, arg_split, + system, getoutput, getoutputerror, + get_output_error_code) +from yap_ipython.testing import decorators as dec +from yap_ipython.testing import tools as tt + +python = os.path.basename(sys.executable) + +#----------------------------------------------------------------------------- +# Tests +#----------------------------------------------------------------------------- + + +@dec.skip_win32 +def test_find_cmd_ls(): + """Make sure we can find the full path to ls.""" + path = find_cmd('ls') + nt.assert_true(path.endswith('ls')) + + +def has_pywin32(): + try: + import win32api + except ImportError: + return False + return True + + +@dec.onlyif(has_pywin32, "This test requires win32api to run") +def test_find_cmd_pythonw(): + """Try to find pythonw on Windows.""" + path = find_cmd('pythonw') + assert path.lower().endswith('pythonw.exe'), path + + +@dec.onlyif(lambda : sys.platform != 'win32' or has_pywin32(), + "This test runs on posix or in win32 with win32api installed") +def test_find_cmd_fail(): + """Make sure that FindCmdError is raised if we can't find the cmd.""" + nt.assert_raises(FindCmdError,find_cmd,'asdfasdf') + + +@dec.skip_win32 +def test_arg_split(): + """Ensure that argument lines are correctly split like in a shell.""" + tests = [['hi', ['hi']], + [u'hi', [u'hi']], + ['hello there', ['hello', 'there']], + # \u01ce == \N{LATIN SMALL LETTER A WITH CARON} + # Do not use \N because the tests crash with syntax error in + # some cases, for example windows python2.6. + [u'h\u01cello', [u'h\u01cello']], + ['something "with quotes"', ['something', '"with quotes"']], + ] + for argstr, argv in tests: + nt.assert_equal(arg_split(argstr), argv) + +@dec.skip_if_not_win32 +def test_arg_split_win32(): + """Ensure that argument lines are correctly split like in a shell.""" + tests = [['hi', ['hi']], + [u'hi', [u'hi']], + ['hello there', ['hello', 'there']], + [u'h\u01cello', [u'h\u01cello']], + ['something "with quotes"', ['something', 'with quotes']], + ] + for argstr, argv in tests: + nt.assert_equal(arg_split(argstr), argv) + + +class SubProcessTestCase(TestCase, tt.TempFileMixin): + def setUp(self): + """Make a valid python temp file.""" + lines = [ "import sys", + "print('on stdout', end='', file=sys.stdout)", + "print('on stderr', end='', file=sys.stderr)", + "sys.stdout.flush()", + "sys.stderr.flush()"] + self.mktmp('\n'.join(lines)) + + def test_system(self): + status = system('%s "%s"' % (python, self.fname)) + self.assertEqual(status, 0) + + def test_system_quotes(self): + status = system('%s -c "import sys"' % python) + self.assertEqual(status, 0) + + def test_getoutput(self): + out = getoutput('%s "%s"' % (python, self.fname)) + # we can't rely on the order the line buffered streams are flushed + try: + self.assertEqual(out, 'on stderron stdout') + except AssertionError: + self.assertEqual(out, 'on stdouton stderr') + + def test_getoutput_quoted(self): + out = getoutput('%s -c "print (1)"' % python) + self.assertEqual(out.strip(), '1') + + #Invalid quoting on windows + @dec.skip_win32 + def test_getoutput_quoted2(self): + out = getoutput("%s -c 'print (1)'" % python) + self.assertEqual(out.strip(), '1') + out = getoutput("%s -c 'print (\"1\")'" % python) + self.assertEqual(out.strip(), '1') + + def test_getoutput_error(self): + out, err = getoutputerror('%s "%s"' % (python, self.fname)) + self.assertEqual(out, 'on stdout') + self.assertEqual(err, 'on stderr') + + def test_get_output_error_code(self): + quiet_exit = '%s -c "import sys; sys.exit(1)"' % python + out, err, code = get_output_error_code(quiet_exit) + self.assertEqual(out, '') + self.assertEqual(err, '') + self.assertEqual(code, 1) + out, err, code = get_output_error_code('%s "%s"' % (python, self.fname)) + self.assertEqual(out, 'on stdout') + self.assertEqual(err, 'on stderr') + self.assertEqual(code, 0) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_pycolorize.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_pycolorize.py new file mode 100644 index 000000000..52478655d --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_pycolorize.py @@ -0,0 +1,78 @@ +# coding: utf-8 +"""Test suite for our color utilities. + +Authors +------- + +* Min RK +""" +#----------------------------------------------------------------------------- +# Copyright (C) 2011 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING.txt, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# third party +import nose.tools as nt + +# our own +from yap_ipython.utils.PyColorize import Parser +import io + +#----------------------------------------------------------------------------- +# Test functions +#----------------------------------------------------------------------------- + +sample = u""" +def function(arg, *args, kwarg=True, **kwargs): + ''' + this is docs + ''' + pass is True + False == None + + with io.open(ru'unicode'): + raise ValueError("\n escape \r sequence") + + print("wěird ünicoðe") + +class Bar(Super): + + def __init__(self): + super(Bar, self).__init__(1**2, 3^4, 5 or 6) +""" + +def test_loop_colors(): + + for style in ('Linux', 'NoColor','LightBG', 'Neutral'): + + def test_unicode_colorize(): + p = Parser(style=style) + f1 = p.format('1/0', 'str') + f2 = p.format(u'1/0', 'str') + nt.assert_equal(f1, f2) + + def test_parse_sample(): + """and test writing to a buffer""" + buf = io.StringIO() + p = Parser(style=style) + p.format(sample, buf) + buf.seek(0) + f1 = buf.read() + + nt.assert_not_in('ERROR', f1) + + def test_parse_error(): + p = Parser(style=style) + f1 = p.format(')', 'str') + if style != 'NoColor': + nt.assert_in('ERROR', f1) + + yield test_unicode_colorize + yield test_parse_sample + yield test_parse_error diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_shimmodule.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_shimmodule.py new file mode 100644 index 000000000..5ec7358f9 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_shimmodule.py @@ -0,0 +1,13 @@ +import sys +import warnings + +from yap_ipython.utils.shimmodule import ShimWarning + + +def test_shim_warning(): + sys.modules.pop('yap_ipython.config', None) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + import yap_ipython.config + assert len(w) == 1 + assert issubclass(w[-1].category, ShimWarning) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_sysinfo.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_sysinfo.py new file mode 100644 index 000000000..11677e2c5 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_sysinfo.py @@ -0,0 +1,17 @@ +# coding: utf-8 +"""Test suite for our sysinfo utilities.""" + +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import json +import nose.tools as nt + +from yap_ipython.utils import sysinfo + + +def test_json_getsysinfo(): + """ + test that it is easily jsonable and don't return bytes somewhere. + """ + json.dumps(sysinfo.get_sys_info()) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_tempdir.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_tempdir.py new file mode 100644 index 000000000..6bcd071c4 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_tempdir.py @@ -0,0 +1,28 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2012- The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +import os + +from yap_ipython.utils.tempdir import NamedFileInTemporaryDirectory +from yap_ipython.utils.tempdir import TemporaryWorkingDirectory + + +def test_named_file_in_temporary_directory(): + with NamedFileInTemporaryDirectory('filename') as file: + name = file.name + assert not file.closed + assert os.path.exists(name) + file.write(b'test') + assert file.closed + assert not os.path.exists(name) + +def test_temporary_working_directory(): + with TemporaryWorkingDirectory() as dir: + assert os.path.exists(dir) + assert os.path.realpath(os.curdir) == os.path.realpath(dir) + assert not os.path.exists(dir) + assert os.path.abspath(os.curdir) != dir diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_text.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_text.py new file mode 100644 index 000000000..792f1d4b1 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_text.py @@ -0,0 +1,221 @@ +# encoding: utf-8 +"""Tests for yap_ipython.utils.text""" + +#----------------------------------------------------------------------------- +# Copyright (C) 2011 The yap_ipython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +import os +import math +import random +import sys + +import nose.tools as nt +try: + from pathlib import Path +except ImportError: + # for Python 3.3 + from pathlib2 import Path + +from yap_ipython.utils import text + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +def test_columnize(): + """Basic columnize tests.""" + size = 5 + items = [l*size for l in 'abcd'] + + out = text.columnize(items, displaywidth=80) + nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n') + out = text.columnize(items, displaywidth=25) + nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + out = text.columnize(items, displaywidth=12) + nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + out = text.columnize(items, displaywidth=10) + nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n') + + out = text.columnize(items, row_first=True, displaywidth=80) + nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n') + out = text.columnize(items, row_first=True, displaywidth=25) + nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n') + out = text.columnize(items, row_first=True, displaywidth=12) + nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n') + out = text.columnize(items, row_first=True, displaywidth=10) + nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n') + + out = text.columnize(items, displaywidth=40, spread=True) + nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n') + out = text.columnize(items, displaywidth=20, spread=True) + nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + out = text.columnize(items, displaywidth=12, spread=True) + nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + out = text.columnize(items, displaywidth=10, spread=True) + nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n') + + +def test_columnize_random(): + """Test with random input to hopfully catch edge case """ + for row_first in [True, False]: + for nitems in [random.randint(2,70) for i in range(2,20)]: + displaywidth = random.randint(20,200) + rand_len = [random.randint(2,displaywidth) for i in range(nitems)] + items = ['x'*l for l in rand_len] + out = text.columnize(items, row_first=row_first, displaywidth=displaywidth) + longer_line = max([len(x) for x in out.split('\n')]) + longer_element = max(rand_len) + if longer_line > displaywidth: + print("Columnize displayed something lager than displaywidth : %s " % longer_line) + print("longer element : %s " % longer_element) + print("displaywidth : %s " % displaywidth) + print("number of element : %s " % nitems) + print("size of each element :\n %s" % rand_len) + assert False, "row_first={0}".format(row_first) + +def test_columnize_medium(): + """Test with inputs than shouldn't be wider than 80""" + size = 40 + items = [l*size for l in 'abc'] + for row_first in [True, False]: + out = text.columnize(items, row_first=row_first, displaywidth=80) + nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first)) + +def test_columnize_long(): + """Test columnize with inputs longer than the display window""" + size = 11 + items = [l*size for l in 'abc'] + for row_first in [True, False]: + out = text.columnize(items, row_first=row_first, displaywidth=size-1) + nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first)) + +def eval_formatter_check(f): + ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"café", b="café") + s = f.format("{n} {n//4} {stuff.split()[0]}", **ns) + nt.assert_equal(s, "12 3 hello") + s = f.format(' '.join(['{n//%i}'%i for i in range(1,8)]), **ns) + nt.assert_equal(s, "12 6 4 3 2 2 1") + s = f.format('{[n//i for i in range(1,8)]}', **ns) + nt.assert_equal(s, "[12, 6, 4, 3, 2, 2, 1]") + s = f.format("{stuff!s}", **ns) + nt.assert_equal(s, ns['stuff']) + s = f.format("{stuff!r}", **ns) + nt.assert_equal(s, repr(ns['stuff'])) + + # Check with unicode: + s = f.format("{u}", **ns) + nt.assert_equal(s, ns['u']) + # This decodes in a platform dependent manner, but it shouldn't error out + s = f.format("{b}", **ns) + + nt.assert_raises(NameError, f.format, '{dne}', **ns) + +def eval_formatter_slicing_check(f): + ns = dict(n=12, pi=math.pi, stuff='hello there', os=os) + s = f.format(" {stuff.split()[:]} ", **ns) + nt.assert_equal(s, " ['hello', 'there'] ") + s = f.format(" {stuff.split()[::-1]} ", **ns) + nt.assert_equal(s, " ['there', 'hello'] ") + s = f.format("{stuff[::2]}", **ns) + nt.assert_equal(s, ns['stuff'][::2]) + + nt.assert_raises(SyntaxError, f.format, "{n:x}", **ns) + +def eval_formatter_no_slicing_check(f): + ns = dict(n=12, pi=math.pi, stuff='hello there', os=os) + + s = f.format('{n:x} {pi**2:+f}', **ns) + nt.assert_equal(s, "c +9.869604") + + s = f.format('{stuff[slice(1,4)]}', **ns) + nt.assert_equal(s, 'ell') + + if sys.version_info >= (3, 4): + # String formatting has changed in Python 3.4, so this now works. + s = f.format("{a[:]}", a=[1, 2]) + nt.assert_equal(s, "[1, 2]") + else: + nt.assert_raises(SyntaxError, f.format, "{a[:]}") + +def test_eval_formatter(): + f = text.EvalFormatter() + eval_formatter_check(f) + eval_formatter_no_slicing_check(f) + +def test_full_eval_formatter(): + f = text.FullEvalFormatter() + eval_formatter_check(f) + eval_formatter_slicing_check(f) + +def test_dollar_formatter(): + f = text.DollarFormatter() + eval_formatter_check(f) + eval_formatter_slicing_check(f) + + ns = dict(n=12, pi=math.pi, stuff='hello there', os=os) + s = f.format("$n", **ns) + nt.assert_equal(s, "12") + s = f.format("$n.real", **ns) + nt.assert_equal(s, "12") + s = f.format("$n/{stuff[:5]}", **ns) + nt.assert_equal(s, "12/hello") + s = f.format("$n $$HOME", **ns) + nt.assert_equal(s, "12 $HOME") + s = f.format("${foo}", foo="HOME") + nt.assert_equal(s, "$HOME") + + +def test_long_substr(): + data = ['hi'] + nt.assert_equal(text.long_substr(data), 'hi') + + +def test_long_substr2(): + data = ['abc', 'abd', 'abf', 'ab'] + nt.assert_equal(text.long_substr(data), 'ab') + +def test_long_substr_empty(): + data = [] + nt.assert_equal(text.long_substr(data), '') + +def test_strip_email(): + src = """\ + >> >>> def f(x): + >> ... return x+1 + >> ... + >> >>> zz = f(2.5)""" + cln = """\ +>>> def f(x): +... return x+1 +... +>>> zz = f(2.5)""" + nt.assert_equal(text.strip_email_quotes(src), cln) + + +def test_strip_email2(): + src = '> > > list()' + cln = 'list()' + nt.assert_equal(text.strip_email_quotes(src), cln) + +def test_LSString(): + lss = text.LSString("abc\ndef") + nt.assert_equal(lss.l, ['abc', 'def']) + nt.assert_equal(lss.s, 'abc def') + lss = text.LSString(os.getcwd()) + nt.assert_is_instance(lss.p[0], Path) + +def test_SList(): + sl = text.SList(['a 11', 'b 1', 'a 2']) + nt.assert_equal(sl.n, 'a 11\nb 1\na 2') + nt.assert_equal(sl.s, 'a 11 b 1 a 2') + nt.assert_equal(sl.grep(lambda x: x.startswith('a')), text.SList(['a 11', 'a 2'])) + nt.assert_equal(sl.fields(0), text.SList(['a', 'b', 'a'])) + nt.assert_equal(sl.sort(field=1, nums=True), text.SList(['b 1', 'a 2', 'a 11'])) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_tokenutil.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_tokenutil.py new file mode 100644 index 000000000..e5197b93f --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_tokenutil.py @@ -0,0 +1,133 @@ +"""Tests for tokenutil""" +# Copyright (c) yap_ipython Development Team. +# Distributed under the terms of the Modified BSD License. + +import nose.tools as nt + +from yap_ipython.utils.tokenutil import token_at_cursor, line_at_cursor + +def expect_token(expected, cell, cursor_pos): + token = token_at_cursor(cell, cursor_pos) + offset = 0 + for line in cell.splitlines(): + if offset + len(line) >= cursor_pos: + break + else: + offset += len(line)+1 + column = cursor_pos - offset + line_with_cursor = '%s|%s' % (line[:column], line[column:]) + nt.assert_equal(token, expected, + "Expected %r, got %r in: %r (pos %i)" % ( + expected, token, line_with_cursor, cursor_pos) + ) + +def test_simple(): + cell = "foo" + for i in range(len(cell)): + expect_token("foo", cell, i) + +def test_function(): + cell = "foo(a=5, b='10')" + expected = 'foo' + # up to `foo(|a=` + for i in range(cell.find('a=') + 1): + expect_token("foo", cell, i) + # find foo after `=` + for i in [cell.find('=') + 1, cell.rfind('=') + 1]: + expect_token("foo", cell, i) + # in between `5,|` and `|b=` + for i in range(cell.find(','), cell.find('b=')): + expect_token("foo", cell, i) + +def test_multiline(): + cell = '\n'.join([ + 'a = 5', + 'b = hello("string", there)' + ]) + expected = 'hello' + start = cell.index(expected) + 1 + for i in range(start, start + len(expected)): + expect_token(expected, cell, i) + expected = 'hello' + start = cell.index(expected) + 1 + for i in range(start, start + len(expected)): + expect_token(expected, cell, i) + +def test_multiline_token(): + cell = '\n'.join([ + '"""\n\nxxxxxxxxxx\n\n"""', + '5, """', + 'docstring', + 'multiline token', + '""", [', + '2, 3, "complicated"]', + 'b = hello("string", there)' + ]) + expected = 'hello' + start = cell.index(expected) + 1 + for i in range(start, start + len(expected)): + expect_token(expected, cell, i) + expected = 'hello' + start = cell.index(expected) + 1 + for i in range(start, start + len(expected)): + expect_token(expected, cell, i) + +def test_nested_call(): + cell = "foo(bar(a=5), b=10)" + expected = 'foo' + start = cell.index('bar') + 1 + for i in range(start, start + 3): + expect_token(expected, cell, i) + expected = 'bar' + start = cell.index('a=') + for i in range(start, start + 3): + expect_token(expected, cell, i) + expected = 'foo' + start = cell.index(')') + 1 + for i in range(start, len(cell)-1): + expect_token(expected, cell, i) + +def test_attrs(): + cell = "a = obj.attr.subattr" + expected = 'obj' + idx = cell.find('obj') + 1 + for i in range(idx, idx + 3): + expect_token(expected, cell, i) + idx = cell.find('.attr') + 2 + expected = 'obj.attr' + for i in range(idx, idx + 4): + expect_token(expected, cell, i) + idx = cell.find('.subattr') + 2 + expected = 'obj.attr.subattr' + for i in range(idx, len(cell)): + expect_token(expected, cell, i) + +def test_line_at_cursor(): + cell = "" + (line, offset) = line_at_cursor(cell, cursor_pos=11) + nt.assert_equal(line, "") + nt.assert_equal(offset, 0) + + # The position after a newline should be the start of the following line. + cell = "One\nTwo\n" + (line, offset) = line_at_cursor(cell, cursor_pos=4) + nt.assert_equal(line, "Two\n") + nt.assert_equal(offset, 4) + + # The end of a cell should be on the last line + cell = "pri\npri" + (line, offset) = line_at_cursor(cell, cursor_pos=7) + nt.assert_equal(line, "pri") + nt.assert_equal(offset, 4) + +def test_muliline_statement(): + cell = """a = (1, + 3) + +int() +map() +""" + for c in range(16, 22): + yield lambda: expect_token("int", cell, c) + for c in range(22, 28): + yield lambda: expect_token("map", cell, c) diff --git a/packages/python/yap_kernel/yap_ipython/utils/tests/test_wildcard.py b/packages/python/yap_kernel/yap_ipython/utils/tests/test_wildcard.py new file mode 100644 index 000000000..f75c96749 --- /dev/null +++ b/packages/python/yap_kernel/yap_ipython/utils/tests/test_wildcard.py @@ -0,0 +1,143 @@ +"""Some tests for the wildcard utilities.""" + +#----------------------------------------------------------------------------- +# Library imports +#----------------------------------------------------------------------------- +# Stdlib +import unittest + +# Our own +from yap_ipython.utils import wildcard + +#----------------------------------------------------------------------------- +# Globals for test +#----------------------------------------------------------------------------- + +class obj_t(object): + pass + +root = obj_t() +l = ["arna","abel","ABEL","active","bob","bark","abbot"] +q = ["kate","loop","arne","vito","lucifer","koppel"] +for x in l: + o = obj_t() + setattr(root,x,o) + for y in q: + p = obj_t() + setattr(o,y,p) +root._apan = obj_t() +root._apan.a = 10 +root._apan._a = 20 +root._apan.__a = 20 +root.__anka = obj_t() +root.__anka.a = 10 +root.__anka._a = 20 +root.__anka.__a = 20 + +root._APAN = obj_t() +root._APAN.a = 10 +root._APAN._a = 20 +root._APAN.__a = 20 +root.__ANKA = obj_t() +root.__ANKA.a = 10 +root.__ANKA._a = 20 +root.__ANKA.__a = 20 + +#----------------------------------------------------------------------------- +# Test cases +#----------------------------------------------------------------------------- + +class Tests (unittest.TestCase): + def test_case(self): + ns=root.__dict__ + tests=[ + ("a*", ["abbot","abel","active","arna",]), + ("?b*.?o*",["abbot.koppel","abbot.loop","abel.koppel","abel.loop",]), + ("_a*", []), + ("_*anka", ["__anka",]), + ("_*a*", ["__anka",]), + ] + for pat,res in tests: + res.sort() + a=sorted(wildcard.list_namespace(ns,"all",pat,ignore_case=False, + show_all=False).keys()) + self.assertEqual(a,res) + + def test_case_showall(self): + ns=root.__dict__ + tests=[ + ("a*", ["abbot","abel","active","arna",]), + ("?b*.?o*",["abbot.koppel","abbot.loop","abel.koppel","abel.loop",]), + ("_a*", ["_apan"]), + ("_*anka", ["__anka",]), + ("_*a*", ["__anka","_apan",]), + ] + for pat,res in tests: + res.sort() + a=sorted(wildcard.list_namespace(ns,"all",pat,ignore_case=False, + show_all=True).keys()) + self.assertEqual(a,res) + + + def test_nocase(self): + ns=root.__dict__ + tests=[ + ("a*", ["abbot","abel","ABEL","active","arna",]), + ("?b*.?o*",["abbot.koppel","abbot.loop","abel.koppel","abel.loop", + "ABEL.koppel","ABEL.loop",]), + ("_a*", []), + ("_*anka", ["__anka","__ANKA",]), + ("_*a*", ["__anka","__ANKA",]), + ] + for pat,res in tests: + res.sort() + a=sorted(wildcard.list_namespace(ns,"all",pat,ignore_case=True, + show_all=False).keys()) + self.assertEqual(a,res) + + def test_nocase_showall(self): + ns=root.__dict__ + tests=[ + ("a*", ["abbot","abel","ABEL","active","arna",]), + ("?b*.?o*",["abbot.koppel","abbot.loop","abel.koppel","abel.loop", + "ABEL.koppel","ABEL.loop",]), + ("_a*", ["_apan","_APAN"]), + ("_*anka", ["__anka","__ANKA",]), + ("_*a*", ["__anka","__ANKA","_apan","_APAN"]), + ] + for pat,res in tests: + res.sort() + a=sorted(wildcard.list_namespace(ns,"all",pat,ignore_case=True, + show_all=True).keys()) + a.sort() + self.assertEqual(a,res) + + def test_dict_attributes(self): + """Dictionaries should be indexed by attributes, not by keys. This was + causing Github issue 129.""" + ns = {"az":{"king":55}, "pq":{1:0}} + tests = [ + ("a*", ["az"]), + ("az.k*", ["az.keys"]), + ("pq.k*", ["pq.keys"]) + ] + for pat, res in tests: + res.sort() + a = sorted(wildcard.list_namespace(ns, "all", pat, ignore_case=False, + show_all=True).keys()) + self.assertEqual(a, res) + + def test_dict_dir(self): + class A(object): + def __init__(self): + self.a = 1 + self.b = 2 + def __getattribute__(self, name): + if name=="a": + raise AttributeError + return object.__getattribute__(self, name) + + a = A() + adict = wildcard.dict_dir(a) + assert "a" not in adict # change to assertNotIn method in >= 2.7 + self.assertEqual(adict["b"], 2)