From 858c6e0902546210d96ddc5a708ee9a2eddc7e42 2010-01-11 05:58:09 From: Fernando Perez Date: 2010-01-11 05:58:09 Subject: [PATCH] Ported the IPython Sphinx directive to 0.11. This was originally written by John Hunter for the 0.10 API, now it works with 0.11. We still need to automate its test suite, but at least now it runs and the script itself can be executed as a test that produces screen output and figures in a subdir. --- diff --git a/IPython/__init__.py b/IPython/__init__.py index c8ad53e..90d7565 100755 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -16,10 +16,10 @@ IPython is a set of tools for interactive and exploratory computing in Python. #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- +from __future__ import absolute_import import os import sys -from IPython.core import release #----------------------------------------------------------------------------- # Setup everything @@ -38,12 +38,16 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "extensions")) #----------------------------------------------------------------------------- # In some cases, these are causing circular imports. -from IPython.core.iplib import InteractiveShell -from IPython.core.embed import embed -from IPython.core.error import TryNext -from IPython.testing import test +from .config.loader import Config +from .core import release +from .core.application import Application +from .core.ipapp import IPythonApp +from .core.embed import embed +from .core.error import TryNext +from .core.iplib import InteractiveShell +from .testing import test -from IPython.lib import ( +from .lib import ( enable_wx, disable_wx, enable_gtk, disable_gtk, enable_qt4, disable_qt4, diff --git a/IPython/core/application.py b/IPython/core/application.py index ec347a5..d916a47 100755 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -84,27 +84,62 @@ app_cl_args = ( ) class Application(object): - """Load a config, construct components and set them running.""" + """Load a config, construct components and set them running. + + The configuration of an application can be done via four different Config + objects, which are loaded and ultimately merged into a single one used from + that point on by the app. These are: + + 1. default_config: internal defaults, implemented in code. + 2. file_config: read from the filesystem. + 3. command_line_config: read from the system's command line flags. + 4. constructor_config: passed parametrically to the constructor. + + During initialization, 3 is actually read before 2, since at the + command-line one may override the location of the file to be read. But the + above is the order in which the merge is made. + + There is a final config object can be created and passed to the + constructor: override_config. If it exists, this completely overrides the + configs 2-4 above (the default is still used to ensure that all needed + fields at least are created). This makes it easier to create + parametrically (e.g. in testing or sphinx plugins) objects with a known + configuration, that are unaffected by whatever arguments may be present in + sys.argv or files in the user's various directories. + """ name = u'ipython' description = 'IPython: an enhanced interactive Python shell.' #: usage message printed by argparse. If None, auto-generate usage = None config_file_name = u'ipython_config.py' - # Track the default and actual separately because some messages are - # only printed if we aren't using the default. + #: Track the default and actual separately because some messages are + #: only printed if we aren't using the default. default_config_file_name = config_file_name default_log_level = logging.WARN - # Set by --profile option + #: Set by --profile option profile_name = None #: User's ipython directory, typically ~/.ipython/ ipython_dir = None + #: internal defaults, implemented in code. + default_config = None + #: read from the filesystem + file_config = None + #: read from the system's command line flags + command_line_config = None + #: passed parametrically to the constructor. + constructor_config = None + #: final override, if given supercedes file/command/constructor configs + override_config = None #: A reference to the argv to be used (typically ends up being sys.argv[1:]) argv = None #: Default command line arguments. Subclasses should create a new tuple #: that *includes* these. cl_arguments = app_cl_args + #: extra arguments computed by the command-line loader + extra_args = None + # Private attributes _exiting = False _initialized = False @@ -112,8 +147,10 @@ class Application(object): # Class choices for things that will be instantiated at runtime. _CrashHandler = crashhandler.CrashHandler - def __init__(self, argv=None): + def __init__(self, argv=None, constructor_config=None, override_config=None): self.argv = sys.argv[1:] if argv is None else argv + self.constructor_config = constructor_config + self.override_config = override_config self.init_logger() def init_logger(self): @@ -134,7 +171,14 @@ class Application(object): log_level = property(_get_log_level, _set_log_level) def initialize(self): - """Start the application.""" + """Initialize the application. + + Loads all configuration information and sets all application state, but + does not start any relevant processing (typically some kind of event + loop). + + Once this method has been called, the application is flagged as + initialized and the method becomes a no-op.""" if self._initialized: return @@ -144,31 +188,50 @@ class Application(object): # handler is in place, we can let any subsequent exception propagate, # as our handler will log it with much better detail than the default. self.attempt(self.create_crash_handler) + + # Configuration phase + # Default config (internally hardwired in application code) self.create_default_config() self.log_default_config() self.set_default_config_log_level() - self.pre_load_command_line_config() - self.load_command_line_config() - self.set_command_line_config_log_level() - self.post_load_command_line_config() - self.log_command_line_config() + + if self.override_config is None: + # Command-line config + self.pre_load_command_line_config() + self.load_command_line_config() + self.set_command_line_config_log_level() + self.post_load_command_line_config() + self.log_command_line_config() + + # Find resources needed for filesystem access, using information from + # the above two self.find_ipython_dir() self.find_resources() self.find_config_file_name() self.find_config_file_paths() - self.pre_load_file_config() - self.load_file_config() - self.set_file_config_log_level() - self.post_load_file_config() - self.log_file_config() + + if self.override_config is None: + # File-based config + self.pre_load_file_config() + self.load_file_config() + self.set_file_config_log_level() + self.post_load_file_config() + self.log_file_config() + + # Merge all config objects into a single one the app can then use self.merge_configs() self.log_master_config() + + # Construction phase self.pre_construct() self.construct() self.post_construct() + + # Done, flag as such and self._initialized = True def start(self): + """Start the application.""" self.initialize() self.start_app() @@ -356,9 +419,19 @@ class Application(object): """Merge the default, command line and file config objects.""" config = Config() config._merge(self.default_config) - config._merge(self.file_config) - config._merge(self.command_line_config) + if self.override_config is None: + config._merge(self.file_config) + config._merge(self.command_line_config) + if self.constructor_config is not None: + config._merge(self.constructor_config) + else: + config._merge(self.override_config) + # XXX fperez - propose to Brian we rename master_config to simply + # config, I think this is going to be heavily used in examples and + # application code and the name is shorter/easier to find/remember. + # For now, just alias it... self.master_config = config + self.config = config def log_master_config(self): self.log.debug("Master config created:") diff --git a/IPython/core/ipapp.py b/IPython/core/ipapp.py index 37a92bd..95dd2c1 100755 --- a/IPython/core/ipapp.py +++ b/IPython/core/ipapp.py @@ -361,20 +361,33 @@ class IPythonApp(Application): # Private and configuration attributes _CrashHandler = crashhandler.IPythonCrashHandler - def __init__(self, argv=None, **shell_params): + def __init__(self, argv=None, + constructor_config=None, override_config=None, + **shell_params): """Create a new IPythonApp. + See the parent class for details on how configuration is handled. + Parameters ---------- argv : optional, list If given, used as the command-line argv environment to read arguments from. + constructor_config : optional, Config + If given, additional config that is merged last, after internal + defaults, command-line and file-based configs. + + override_config : optional, Config + If given, config that overrides all others unconditionally (except + for internal defaults, which ensure that all parameters exist). + shell_params : optional, dict All other keywords are passed to the :class:`iplib.InteractiveShell` constructor. """ - super(IPythonApp, self).__init__(argv) + super(IPythonApp, self).__init__(argv, constructor_config, + override_config) self.shell_params = shell_params def create_default_config(self): @@ -449,8 +462,7 @@ class IPythonApp(Application): # unless the -i flag (Global.force_interact) is true. code_to_run = config.Global.get('code_to_run','') file_to_run = False - if len(self.extra_args)>=1: - if self.extra_args[0]: + if self.extra_args and self.extra_args[0]: file_to_run = True if file_to_run or code_to_run: if not config.Global.force_interact: diff --git a/docs/sphinxext/ipython_directive.py b/docs/sphinxext/ipython_directive.py new file mode 100644 index 0000000..a7dc1c2 --- /dev/null +++ b/docs/sphinxext/ipython_directive.py @@ -0,0 +1,641 @@ +# -*- coding: utf-8 -*- +"""Sphinx directive to support embedded IPython code. + +This directive allows pasting of entire interactive IPython sessions, prompts +and all, and their code will actually get re-executed at doc build time, with +all prompts renumbered sequentially. + +To enable this directive, simply list it in your Sphinx ``conf.py`` file +(making sure the directory where you placed it is visible to sphinx, as is +needed for all Sphinx directives). + +By default this directive assumes that your prompts are unchanged IPython ones, +but this can be customized. For example, the following code in your Sphinx +config file will configure this directive for the following input/output +prompts ``Yade [1]:`` and ``-> [1]:``:: + + import ipython_directive as id + id.rgxin =re.compile(r'(?:In |Yade )\[(\d+)\]:\s?(.*)\s*') + id.rgxout=re.compile(r'(?:Out| -> )\[(\d+)\]:\s?(.*)\s*') + id.fmtin ='Yade [%d]:' + id.fmtout=' -> [%d]:' + + from IPython import Config + id.CONFIG = Config( + prompt_in1="Yade [\#]:", + prompt_in2=" .\D..", + prompt_out=" -> [\#]:" + ) + id.reconfig_shell() + + import ipython_console_highlighting as ich + ich.IPythonConsoleLexer.input_prompt= + re.compile("(Yade \[[0-9]+\]: )|( \.\.\.+:)") + ich.IPythonConsoleLexer.output_prompt= + re.compile("(( -> )|(Out)\[[0-9]+\]: )|( \.\.\.+:)") + ich.IPythonConsoleLexer.continue_prompt=re.compile(" \.\.\.+:") + + +ToDo +---- + +- Turn the ad-hoc test() function into a real test suite. +- Break up ipython-specific functionality from matplotlib stuff into better + separated code. +- Make sure %bookmarks used internally are removed on exit. + + +Authors +------- + +- John D Hunter: orignal author. +- Fernando Perez: refactoring, documentation, cleanups, port to 0.11. +- VáclavŠmilauer : Prompt generalizations. +""" + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +# Stdlib +import cStringIO +import imp +import os +import re +import shutil +import sys +import warnings + +# To keep compatibility with various python versions +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +# Third-party +import matplotlib +import sphinx +from docutils.parsers.rst import directives + +matplotlib.use('Agg') + +# Our own +from IPython import Config, IPythonApp +from IPython.utils.genutils import Term, Tee + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +sphinx_version = sphinx.__version__.split(".") +# The split is necessary for sphinx beta versions where the string is +# '6b1' +sphinx_version = tuple([int(re.split('[a-z]', x)[0]) + for x in sphinx_version[:2]]) + +COMMENT, INPUT, OUTPUT = range(3) +CONFIG = Config() +rgxin = re.compile('In \[(\d+)\]:\s?(.*)\s*') +rgxout = re.compile('Out\[(\d+)\]:\s?(.*)\s*') +fmtin = 'In [%d]:' +fmtout = 'Out[%d]:' + +#----------------------------------------------------------------------------- +# Functions and class declarations +#----------------------------------------------------------------------------- +def block_parser(part): + """ + part is a string of ipython text, comprised of at most one + input, one ouput, comments, and blank lines. The block parser + parses the text into a list of:: + + blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...] + + where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and + data is, depending on the type of token:: + + COMMENT : the comment string + + INPUT: the (DECORATOR, INPUT_LINE, REST) where + DECORATOR: the input decorator (or None) + INPUT_LINE: the input as string (possibly multi-line) + REST : any stdout generated by the input line (not OUTPUT) + + + OUTPUT: the output string, possibly multi-line + """ + + block = [] + lines = part.split('\n') + N = len(lines) + i = 0 + decorator = None + while 1: + + if i==N: + # nothing left to parse -- the last line + break + + line = lines[i] + i += 1 + line_stripped = line.strip() + if line_stripped.startswith('#'): + block.append((COMMENT, line)) + continue + + if line_stripped.startswith('@'): + # we're assuming at most one decorator -- may need to + # rethink + decorator = line_stripped + continue + + # does this look like an input line? + matchin = rgxin.match(line) + if matchin: + lineno, inputline = int(matchin.group(1)), matchin.group(2) + + # the ....: continuation string + continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2)) + Nc = len(continuation) + # input lines can continue on for more than one line, if + # we have a '\' line continuation char or a function call + # echo line 'print'. The input line can only be + # terminated by the end of the block or an output line, so + # we parse out the rest of the input line if it is + # multiline as well as any echo text + + rest = [] + while i2: + if debug: + print '\n'.join(lines) + else: + #print 'INSERTING %d lines'%len(lines) + state_machine.insert_input( + lines, state_machine.input_lines.source(0)) + + return [] + +ipython_directive.DEBUG = False +ipython_directive.DEBUG = True # dbg + +# Enable as a proper Sphinx directive +def setup(app): + setup.app = app + options = {'suppress': directives.flag, + 'doctest': directives.flag, + 'verbatim': directives.flag, + } + + app.add_directive('ipython', ipython_directive, True, (0, 2, 0), **options) + + +# Simple smoke test, needs to be converted to a proper automatic test. +def test(): + + examples = [ + r""" +In [9]: pwd +Out[9]: '/home/jdhunter/py4science/book' + +In [10]: cd bookdata/ +/home/jdhunter/py4science/book/bookdata + +In [2]: from pylab import * + +In [2]: ion() + +In [3]: im = imread('stinkbug.png') + +@savefig mystinkbug.png width=4in +In [4]: imshow(im) +Out[4]: + +""", + r""" + +In [1]: x = 'hello world' + +# string methods can be +# used to alter the string +@doctest +In [2]: x.upper() +Out[2]: 'HELLO WORLD' + +@verbatim +In [3]: x.st +x.startswith x.strip +""", + r""" + +In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\ + .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv' + +In [131]: print url.split('&') +['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv'] + +In [60]: import urllib + +""", + r"""\ + +In [133]: import numpy.random + +@suppress +In [134]: numpy.random.seed(2358) + +@doctest +In [135]: np.random.rand(10,2) +Out[135]: +array([[ 0.64524308, 0.59943846], + [ 0.47102322, 0.8715456 ], + [ 0.29370834, 0.74776844], + [ 0.99539577, 0.1313423 ], + [ 0.16250302, 0.21103583], + [ 0.81626524, 0.1312433 ], + [ 0.67338089, 0.72302393], + [ 0.7566368 , 0.07033696], + [ 0.22591016, 0.77731835], + [ 0.0072729 , 0.34273127]]) + +""", + + r""" +In [106]: print x +jdh + +In [109]: for i in range(10): + .....: print i + .....: + .....: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +""", + + r""" + +In [144]: from pylab import * + +In [145]: ion() + +# use a semicolon to suppress the output +@savefig test_hist.png width=4in +In [151]: hist(np.random.randn(10000), 100); + + +@savefig test_plot.png width=4in +In [151]: plot(np.random.randn(10000), 'o'); + """, + + r""" +# use a semicolon to suppress the output +In [151]: plt.clf() + +@savefig plot_simple.png width=4in +In [151]: plot([1,2,3]) + +@savefig hist_simple.png width=4in +In [151]: hist(np.random.randn(10000), 100); + +""", + r""" +# update the current fig +In [151]: ylabel('number') + +In [152]: title('normal distribution') + + +@savefig hist_with_text.png +In [153]: grid(True) + + """, + ] + + #ipython_directive.DEBUG = True # dbg + #options = dict(suppress=True) # dbg + options = dict() + for example in examples: + content = example.split('\n') + ipython_directive('debug', arguments=None, options=options, + content=content, lineno=0, + content_offset=None, block_text=None, + state=None, state_machine=None, + ) + +# Run test suite as a script +if __name__=='__main__': + if not os.path.isdir('_static'): + os.mkdir('_static') + test() + print 'All OK? Check figures in _static/'