├── requirements.txt
├── .gitignore
├── MANIFEST.in
├── THANKS
├── Makefile
├── LICENSE
├── setup.py
├── README.rst
├── bipython
├── inspection_standalone.py
└── __init__.py
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | ipython
2 | pyzmq
3 | bpython
4 | pygments
5 | urwid
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | sexual preference
2 | # "github only lets me fork, though I would really like to spoon" -- @scopatz
3 | # "so say we all" -- @ivanov
4 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md *.py
2 | include LICENSE
3 | include THANKS
4 |
5 | graft bipython
6 |
7 | global-exclude *.pyc
8 | global-exclude .git
9 |
--------------------------------------------------------------------------------
/THANKS:
--------------------------------------------------------------------------------
1 | Thanks to the following folks who contributed to bipython!
2 |
3 | commits Name
4 | ------- -------------
5 | 86 Paul Ivanov
6 | 1 Anthony Scopatz
7 | 1 Robert Kern
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | thanks:
2 | if [ -f THANKS ]; then rm THANKS; fi
3 | echo "Thanks to the following folks who contributed to bipython!" > THANKS
4 | echo "" >> THANKS
5 | echo "commits Name" >> THANKS
6 | echo "------- -------------" >> THANKS
7 | git shortlog -sn >> THANKS
8 |
9 | .PHONE: thanks
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Paul Ivanov
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification,
6 | are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation and/or
13 | other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its contributors
16 | may be used to endorse or promote products derived from this software without
17 | specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | import sys
4 | try:
5 | from setuptools import setup
6 | have_setuptools = True
7 | except ImportError:
8 | from distutils.core import setup
9 | have_setuptools = False
10 |
11 | import bipython
12 | VERSION = bipython.__version__
13 | M_VERSION = VERSION[:VERSION.rfind('.')]
14 |
15 | setup_kwargs = {
16 | "version": VERSION,
17 | "description": 'bipython: the boldly indiscriminate python interpreter',
18 | "author": 'Paul Ivanov',
19 | "author_email": 'pi@berkeley.edu',
20 | "url": 'http://bipython.org/',
21 | "download_url": "https://github.com/ivanov/bipython/zipball/" + M_VERSION,
22 | "keywords": ["Interactive", "Interpreter", "Shell", "bpython", "ipython",
23 | "urwid", ],
24 | "classifiers": [
25 | "License :: OSI Approved :: BSD License",
26 | "Development Status :: 3 - Alpha",
27 | "Environment :: Console",
28 | "Intended Audience :: Developers",
29 | "Programming Language :: Python",
30 | "Operating System :: OS Independent",
31 | "Topic :: Software Development :: Interpreters",
32 | "Topic :: Utilities",
33 | ],
34 | "zip_safe": False,
35 | "data_files": [("", ['LICENSE', 'README.md', 'README.rst']),],
36 | }
37 |
38 | if have_setuptools:
39 | setup_kwargs['install_requires'] = [
40 | 'Pygments >= 1.6',
41 | 'urwid >= 1.1.1',
42 | 'bpython >= 0.12',
43 | 'pyzmq >= 2.1.11',
44 | 'ipython >= 1.0',
45 | ]
46 |
47 | if __name__ == '__main__':
48 | with open('README.rst') as f:
49 | descr = f.read()
50 | setup(
51 | name='bipython',
52 | packages=['bipython'],
53 | entry_points={'console_scripts': [
54 | 'bipython = bipython:main',
55 | 'bipython%s = bipython:main' % sys.version_info.major
56 | ],},
57 | long_description=descr,
58 | **setup_kwargs
59 | )
60 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | bipython
2 | ========
3 |
4 | .. figure:: http://bipython.org/images/bipython_logo.png
5 | :alt: bipython logo
6 |
7 | the boldly indiscriminate python interpreter
8 | --------------------------------------------
9 |
10 | *"...because you shouldn't have to choose."*
11 |
12 | PROLOGUE
13 | --------
14 |
15 | | Two interpreters, both alike in dignity,
16 | | In fair Pythona, where we lay our scene,
17 | | From ancient grudge break to new mutiny,
18 | | Where civil code makes git commits unclean.
19 | | From forth the fatal loins of these two foes
20 | | A newer kind of stranger's given life;
21 | | Whose misadventured piteous overthrows
22 | | Doth with its birth bury its parents' strife.
23 |
24 | ACT I
25 | -----
26 |
27 | *Enter ``bpython`` and ``ipython``*
28 |
29 | `**``bpython``** `__
30 |
31 | | I'm a fancy terminal-based interface to the Python interpreter. I give you
32 | | inline syntax highlighting and auto-completion prompts as you type, and I'll
33 | | even automatically show you a little tooltip with a docstring and parameter
34 | | list as soon as you hit ``(`` to make the function call, so you always know
35 | | what you're doing! I'm svelte and proud of it - I don't try to do all of the
36 | | shenanigans that ``ipython`` does with the shell and the web, but the cool kids
37 | | love my rewind feature for demos. I strive to make interactive python coding
38 | | a joy!
39 |
40 | `**``ipython``** `__
41 |
42 | | I'm an awesome *suite* of interactive computing ideas that work together.
43 | | For millennia, I've given you tab-completion and object introspection via
44 | | ``obj?`` instead of ``help(obj)`` in Python. I also have sweet shell features,
45 | | special magic commands (``%run``, ``%timeit``, ``%matplotlib``, etc.) and a
46 | | history mechanism for both input (command history) and output (results
47 | | caching).
48 | |
49 | | More recently, I've decoupled the REPL into clients and kernels, allowing
50 | | them to run on independent of each other. One popular client is the
51 | | IPython Notebook which allows you to write code and prose using a web
52 | | browser, sending code to the kernel for execution and getting rich media
53 | | results back inline. The decoupling of clients and kernels also allows
54 | | multiple clients to interact with the same kernel, so you can hook-up to
55 | | that same running kernel from the terminal. The terminal workflow makes
56 | | more sense for some things, but my user interface there isn't as polished
57 | | as ``bpython``'s.
58 |
59 | *Enter ``bipython``*
60 |
61 | `**``bipython``** `__
62 |
63 | By your powers combined... I am **``bipython``**!
64 |
65 | *Exeunt*
66 |
67 | The Power is Yours!
68 | -------------------
69 |
70 | ::
71 |
72 | pip install bipython
73 |
74 | ``bipython`` requires ipython, pyzmq, bpython, and urwid.
75 |
76 | For now, you'll need to have a running ipython kernel before running
77 | ``bipython``. You can do this by either opening a notebook or running
78 | ``ipython console``. It won't always be like this, I'll fix it as soon
79 | as I can, but it'll be sooner `with your help over
80 | ivanov/bipython `__.
81 |
82 | After that, just run ``bipython`` and enjoy the ride.
83 |
84 | Copyright (c) 2014, `Paul Ivanov `__
85 |
--------------------------------------------------------------------------------
/bipython/inspection_standalone.py:
--------------------------------------------------------------------------------
1 | # The MIT License
2 | #
3 | # Copyright (c) 2009-2011 the bpython authors.
4 | #
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in
13 | # all copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | # THE SOFTWARE.
22 | #
23 |
24 | from __future__ import with_statement
25 | import collections
26 | import inspect
27 | import keyword
28 | import pydoc
29 | import re
30 | import types
31 |
32 | from pygments.token import Token
33 |
34 | # inlining from bpython._py3compat import PythonLexer, py3
35 | import sys
36 |
37 | py3 = (sys.version_info[0] == 3)
38 |
39 | if py3:
40 | from pygments.lexers import Python3Lexer as PythonLexer
41 | else:
42 | from pygments.lexers import PythonLexer
43 |
44 | # end inlining from bpython._py3compat import PythonLexer, py3
45 | #
46 | try:
47 | collections.Callable
48 | has_collections_callable = True
49 | except AttributeError:
50 | has_collections_callable = False
51 | try:
52 | types.InstanceType
53 | has_instance_type = True
54 | except AttributeError:
55 | has_instance_type = False
56 |
57 | if not py3:
58 | _name = re.compile(r'[a-zA-Z_]\w*$')
59 |
60 |
61 | class AttrCleaner(object):
62 | """A context manager that tries to make an object not exhibit side-effects
63 | on attribute lookup."""
64 |
65 | def __init__(self, obj):
66 | self.obj = obj
67 |
68 | def __enter__(self):
69 | """Try to make an object not exhibit side-effects on attribute
70 | lookup."""
71 | type_ = type(self.obj)
72 | __getattribute__ = None
73 | __getattr__ = None
74 | # Dark magic:
75 | # If __getattribute__ doesn't exist on the class and __getattr__ does
76 | # then __getattr__ will be called when doing
77 | # getattr(type_, '__getattribute__', None)
78 | # so we need to first remove the __getattr__, then the
79 | # __getattribute__, then look up the attributes and then restore the
80 | # original methods. :-(
81 | # The upshot being that introspecting on an object to display its
82 | # attributes will avoid unwanted side-effects.
83 | if py3 or type_ != types.InstanceType:
84 | __getattr__ = getattr(type_, '__getattr__', None)
85 | if __getattr__ is not None:
86 | try:
87 | setattr(type_, '__getattr__', (lambda *_, **__: None))
88 | except TypeError:
89 | __getattr__ = None
90 | __getattribute__ = getattr(type_, '__getattribute__', None)
91 | if __getattribute__ is not None:
92 | try:
93 | setattr(type_, '__getattribute__', object.__getattribute__)
94 | except TypeError:
95 | # XXX: This happens for e.g. built-in types
96 | __getattribute__ = None
97 | self.attribs = (__getattribute__, __getattr__)
98 | # /Dark magic
99 |
100 | def __exit__(self, exc_type, exc_val, exc_tb):
101 | """Restore an object's magic methods."""
102 | type_ = type(self.obj)
103 | __getattribute__, __getattr__ = self.attribs
104 | # Dark magic:
105 | if __getattribute__ is not None:
106 | setattr(type_, '__getattribute__', __getattribute__)
107 | if __getattr__ is not None:
108 | setattr(type_, '__getattr__', __getattr__)
109 | # /Dark magic
110 |
111 | class _Repr(object):
112 | """
113 | Helper for `fixlongargs()`: Returns the given value in `__repr__()`.
114 | """
115 |
116 | def __init__(self, value):
117 | self.value = value
118 |
119 | def __repr__(self):
120 | return self.value
121 |
122 | __str__ = __repr__
123 |
124 | def parsekeywordpairs(signature):
125 | tokens = PythonLexer().get_tokens(signature)
126 | preamble = True
127 | stack = []
128 | substack = []
129 | parendepth = 0
130 | for token, value in tokens:
131 | if preamble:
132 | if token is Token.Punctuation and value == u"(":
133 | preamble = False
134 | continue
135 |
136 | if token is Token.Punctuation:
137 | if value in [u'(', u'{', u'[']:
138 | parendepth += 1
139 | elif value in [u')', u'}', u']']:
140 | parendepth -= 1
141 | elif value == ':' and parendepth == -1:
142 | # End of signature reached
143 | break
144 | if ((value == ',' and parendepth == 0) or
145 | (value == ')' and parendepth == -1)):
146 | stack.append(substack)
147 | substack = []
148 | continue
149 |
150 | if value and (parendepth > 0 or value.strip()):
151 | substack.append(value)
152 |
153 | d = {}
154 | for item in stack:
155 | if len(item) >= 3:
156 | d[item[0]] = ''.join(item[2:])
157 | return d
158 |
159 |
160 | def fixlongargs(f, argspec):
161 | """Functions taking default arguments that are references to other objects
162 | whose str() is too big will cause breakage, so we swap out the object
163 | itself with the name it was referenced with in the source by parsing the
164 | source itself !"""
165 | if argspec[3] is None:
166 | # No keyword args, no need to do anything
167 | return
168 | values = list(argspec[3])
169 | if not values:
170 | return
171 | keys = argspec[0][-len(values):]
172 | try:
173 | src = inspect.getsourcelines(f)
174 | except (IOError, IndexError):
175 | # IndexError is raised in inspect.findsource(), can happen in
176 | # some situations. See issue #94.
177 | return
178 | signature = ''.join(src[0])
179 | kwparsed = parsekeywordpairs(signature)
180 |
181 | for i, (key, value) in enumerate(zip(keys, values)):
182 | if len(repr(value)) != len(kwparsed[key]):
183 | values[i] = _Repr(kwparsed[key])
184 |
185 | argspec[3] = values
186 |
187 |
188 | def getpydocspec(f, func):
189 | try:
190 | argspec = pydoc.getdoc(f)
191 | except NameError:
192 | return None
193 |
194 | rx = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)')
195 | s = rx.search(argspec)
196 | if s is None:
197 | return None
198 |
199 | if not hasattr(f, '__name__') or s.groups()[0] != f.__name__:
200 | return None
201 |
202 | args = list()
203 | defaults = list()
204 | varargs = varkwargs = None
205 | kwonly_args = list()
206 | kwonly_defaults = dict()
207 | for arg in s.group(2).split(','):
208 | arg = arg.strip()
209 | if arg.startswith('**'):
210 | varkwargs = arg[2:]
211 | elif arg.startswith('*'):
212 | varargs = arg[1:]
213 | else:
214 | arg, _, default = arg.partition('=')
215 | if varargs is not None:
216 | kwonly_args.append(arg)
217 | if default:
218 | kwonly_defaults[arg] = default
219 | else:
220 | args.append(arg)
221 | if default:
222 | defaults.append(default)
223 |
224 | return [func, (args, varargs, varkwargs, defaults,
225 | kwonly_args, kwonly_defaults)]
226 |
227 |
228 | def getargspec(func, f):
229 | # Check if it's a real bound method or if it's implicitly calling __init__
230 | # (i.e. FooClass(...) and not FooClass.__init__(...) -- the former would
231 | # not take 'self', the latter would:
232 | try:
233 | func_name = getattr(f, '__name__', None)
234 | except:
235 | # if calling foo.__name__ would result in an error
236 | func_name = None
237 |
238 | try:
239 | is_bound_method = ((inspect.ismethod(f) and f.__self__ is not None)
240 | or (func_name == '__init__' and not
241 | func.endswith('.__init__')))
242 | except:
243 | # if f is a method from a xmlrpclib.Server instance, func_name ==
244 | # '__init__' throws xmlrpclib.Fault (see #202)
245 | return None
246 | try:
247 | if py3:
248 | argspec = inspect.getfullargspec(f)
249 | else:
250 | argspec = inspect.getargspec(f)
251 |
252 | argspec = list(argspec)
253 | fixlongargs(f, argspec)
254 | argspec = [func, argspec, is_bound_method]
255 | except (TypeError, KeyError):
256 | with AttrCleaner(f):
257 | argspec = getpydocspec(f, func)
258 | if argspec is None:
259 | return None
260 | if inspect.ismethoddescriptor(f):
261 | argspec[1][0].insert(0, 'obj')
262 | argspec.append(is_bound_method)
263 | return argspec
264 |
265 |
266 | def is_eval_safe_name(string):
267 | if py3:
268 | return all(part.isidentifier() and not keyword.iskeyword(part)
269 | for part in string.split('.'))
270 | else:
271 | return all(_name.match(part) and not keyword.iskeyword(part)
272 | for part in string.split('.'))
273 |
274 |
275 | def is_callable(obj):
276 | if has_instance_type and isinstance(obj, types.InstanceType):
277 | # Work around a CPython bug, see CPython issue #7624
278 | return callable(obj)
279 | elif has_collections_callable:
280 | return isinstance(obj, collections.Callable)
281 | else:
282 | return callable(obj)
283 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [bipython](http://bipython.org/)
2 |
3 | [](http://bipython.org/)
4 |
5 |
6 |
7 | the boldly indiscriminate python interpreter
8 | --------------------------------------------
9 |
10 | *"...because you shouldn't have to choose."*
11 |
12 |
13 | Watch the [extended demo](http://bipython.org/pages/demo.html) which shows off
14 | the strengths and limitations of `bpython` and `ipython`, and shows off how
15 | `bipython` combines the two in a complementary manner so you get the best of
16 | both worlds.
17 |
18 | PROLOGUE
19 | --------
20 |
21 | > Two interpreters, both alike in dignity,
22 | > In fair Pythona, where we lay our scene,
23 | > From ancient grudge break to new mutiny,
24 | > Where civil code makes git commits unclean.
25 | > From forth the fatal loins of these two foes
26 | > A newer kind of stranger's given life;
27 | > Whose misadventured piteous overthrows
28 | > Doth with its birth bury its parents' strife.
29 |
30 | ACT I
31 | ------
32 |
33 | *Enter `bpython` and `ipython`*
34 |
35 | [**`bpython`**](http://bpython-interpreter.org/)
36 |
37 | > I'm a fancy terminal-based interface to the Python interpreter. I give you
38 | > inline syntax highlighting and auto-completion prompts as you type, and I'll
39 | > even automatically show you a little tooltip with a docstring and parameter
40 | > list as soon as you hit `(` to make the function call, so you always know
41 | > what you're doing! I'm svelte and proud of it - I don't try to do all of the
42 | > shenanigans that `ipython` does with the shell and the web, but the cool kids
43 | > love my rewind feature for demos. I strive to make interactive python coding
44 | > a joy!
45 |
46 | [**`ipython`**](http://ipython.org/)
47 |
48 | > I'm an awesome *suite* of interactive computing ideas that work together.
49 | > For millennia, I've given you tab-completion and object introspection via
50 | > `obj?` instead of `help(obj)` in Python. I also have sweet shell features,
51 | > special magic commands (`%run`, `%timeit`, `%matplotlib`, etc.) and a
52 | > history mechanism for both input (command history) and output (results
53 | > caching).
54 |
55 | > More recently, I've decoupled the REPL into clients and kernels, allowing
56 | > them to run on independent of each other. One popular client is the
57 | > IPython Notebook which allows you to write code and prose using a web
58 | > browser, sending code to the kernel for execution and getting rich media
59 | > results back inline. The decoupling of clients and kernels also allows
60 | > multiple clients to interact with the same kernel, so you can hook-up to
61 | > that same running kernel from the terminal. The terminal workflow makes
62 | > more sense for some things, but my user interface there isn't as polished
63 | > as `bpython`'s.
64 |
65 | *Enter `bipython`*
66 |
67 | [**`bipython`**](http://bipython.org/)
68 |
69 | > By your powers combined... I am **`bipython`**!
70 |
71 |
72 | *Exeunt*
73 |
74 |
75 | The Power is Yours!
76 | -------------------
77 |
78 | pip install bipython
79 |
80 | `bipython` requires ipython, pyzmq, bpython, and urwid.
81 |
82 | For now, you'll need to have a running ipython kernel before running `bipython`.
83 | You can do this by either opening a notebook or running `ipython console`.
84 | It won't always be like this, I'll fix it as soon as I can, but it'll be sooner
85 | [with your help over ivanov/bipython](https://github.com/ivanov/bipython).
86 |
87 | After that, just run `bipython` and enjoy the ride.
88 |
89 |
90 | Copyright (c) 2014, [Paul Ivanov](http://pirsquared.org/blog)
91 |
92 |
93 | TODO / KNOWN ISSUES:
94 | -------------------
95 |
96 | [ ] MUSTFIX: multiline input not yet supported - limitation inherited from bpython's
97 | urwid code, which I found out too late. (4am on April 1st)
98 |
99 | [ ] multiline input will be a bit tricky, will need to hold off and not
100 | submit to ipython until the multiline is completed.
101 |
102 | [ ] would also be nice to get local completion in the case of long input cells
103 |
104 | [x] MUSTFIX: up/down arrow keys for history don't work yet.
105 |
106 | [x] maybe i should hook into interp and just turn that into a no-op,
107 | that way i can keep the current (cheap) history as is?
108 |
109 | [ ] history saves only last input, make it work with all
110 |
111 | [ ] make history work *across* sessions, not just current one
112 |
113 | [ ] make multiline history work
114 |
115 | [ ] handle History.enabled = False case gracefully as well
116 |
117 | [ ] use `history_request` for history instead of hand (de)serializing
118 |
119 | [ ] ctrl-d send delete when input is non-empty (should just send it)
120 |
121 | [x] MUSTFIX: Python 3 compatability (all of my dependencies meet them)
122 |
123 | [ ] see if I can put in workaround for stable bpython
124 | - v0.12 works, so says Anthony ( though cheap history won't work there)
125 | - can I also make 0.11-1.1 work? (that's what Ubuntu 13.10 shipped)
126 |
127 | [ ] make bipython work with ipython master (3.x message spec / API changes)
128 |
129 | [ ] implement Rewind feature
130 |
131 | [x] next( keeps repeating the docstring)
132 |
133 | [x] got monospaced theme picked out for pelican
134 |
135 | [x] insert "fork me on github" overlay there.
136 |
137 | [x] colorize in and out prompts
138 |
139 | [x] re-colorize the blue docstring stuff - make it green
140 |
141 | [x] only show docstring in tooltip
142 |
143 | [x] change prompt
144 |
145 | [ ] Maybe: set the username to bipython, and don't print those in the In prompt
146 | if we've sent them (this won't work in an ideal manner if we have more than
147 | one bipython client connected, but we can cross that bridge when we get
148 | to it (using a uuid suffix or something like that)
149 |
150 | [x] stream does not get printed currently.
151 |
152 | [ ] colorize tracebacks too
153 | - for this, we'll need an ansi escape parser - IPython has one that's
154 | implemented in javascript IIRC?
155 |
156 | [x] oops: while True: time.sleep(1); print "hi" breaks it
157 | - i think i need to listen to busy / idle events and react accordingly
158 | (giving back or not giving back the prompt in bipython)
159 | - or just hand the Queue.Empty case
160 |
161 | [x] handle keyboard interrupt
162 | - this is important! (of course it only works locally, but better than
163 | nothing)
164 |
165 | [ ] oh crap! I need twisted event loop for the urwid stuff to work?! goddamn
166 | it. Investigate how easy it is to port what I have back to the cli
167 | version of bpython code
168 |
169 | [x] LOW: make ctrl-w delete word - with '.' being a word separator
170 |
171 | [ ] how do i keep the completion tooltip from going on top of wherever i'm
172 | typing - seems like it's hardcoded to do that after going half-way down
173 | the screen
174 |
175 | [x] alt: use escape to remove it? (though it's slow)
176 | [ ] trigger some sort of faster redraw?
177 |
178 | [ ] colorize / pygemntize the pyin results - damn it - that requires hooking
179 | into the lexer again...
180 |
181 | [ ] setup sigalarm or setup eventloop to check for new messages' arrival
182 | - we already do it on typing, i think...
183 |
184 | [x] print from elswhere - then in bipython freezes it (if the
185 | completion thing was already open
186 | - this can be fixed by not printing docstring like i do for debugging
187 | - actually, no, that doesn't work
188 |
189 | [x] ctrl-w shouldn't remove space before the cur word.
190 |
191 | [ ] look for ps1 ps2 for hints where continuation happens
192 |
193 | [ ] when receiving stream message, write to stdout history, the way urwid
194 | likes to do
195 |
196 | [ ] looks like i won't finish this today, no joke :\
197 |
198 | [x] update execution_count in place while typing
199 |
200 | [x] just need the highlighting to start AFTER In[ ]
201 | (it doesn't account for caption)
202 | [ ] doesn't seem to want to color it green though
203 |
204 | [ ] start its own kernel if --existing flag not given
205 |
206 | [ ] gracefully handle input/output newlines when we didn't initiate it.
207 |
208 | [ ] obj? doesn't show up in bipython - intercept it to be a oinfo req like in
209 | vim-ipython - yes. do that.
210 |
211 | [x] make logo
212 |
213 | related projects:
214 | bpython-interpreter.org
215 | ipython.org
216 | vim-ipython
217 |
218 | [ ] flag
219 | thank you Michael Page - 1998
220 | Pantone Color #226--Magenta (Hex: #D70270) (RGB: 215, 2, 112)
221 | Pantone Color #258--Deep Lavender (Hex: #734F96) (RGB: 115, 79, 150)
222 | Pantone Color #286--Royal (Hex: #0038A8) (RGB: 0, 56, 168)
223 |
224 | The flag's aspect ratio is not fixed but 2:3 and 3:5 are often used, in
225 | common with many other flags
226 |
227 | [x] make bipython twitter account
228 | [ ] tweet at https://twitter.com/bpythonrepl and @IPythonDev
229 |
230 | [x] merge in anthony's old spooning forking commit.
231 |
232 | [ ] use python setup.py register to register it.
233 |
234 | [x] perform the git surgery to put all of my commits into bipython repo (so
235 | i can start getting anthony's feedback, if he wants to / can play)
236 |
237 | [ ] TODO: ansi color escape handling
238 |
239 | [ ] or at least strip it out
240 |
241 | [x] MUSTFIX: tab-completion of magics.
242 |
243 | [x] fix introduced regression: tab completing on something that has no
244 | matched will delete the match
245 |
246 | [x] tab-completion should trigger docstring tooltip update
247 |
248 | [ ] another bug: `xdel^h`
249 | - first tab expand %, second one adds an extra % to the front
250 |
251 | [ ] figure out how much bpython i need.
252 |
253 | [ ] make animation of bpython and ipython logos going toward each other,
254 | then an explosion and the bipython logo emerging from the ashes.
255 |
256 | [x] make sure we get the pid to enable keyboard interrupt on start -
257 | otherwise, if we try to get it after we launch something we want to
258 | interrupt, we're screwed.
259 |
260 | [x] Ctrl-C should process messages as well - should block until interrupt
261 | completed
262 |
263 | [ ] set a time-out for completion and return empty if it's too slow (or the
264 | kernel is busy)
265 |
266 | [x] starting while True: print time.sleep(1) will print above the current
267 | line
268 |
269 | [x] looks like if there was output already on submission, stuff gets printed
270 | there.
271 |
272 | [x] process io_pub message on every completion to put them into the bpython
273 | user interface.
274 |
275 |
276 | [x] getting the argspec as bpython does it requires pulling in all of
277 | bpython/introspection.py - which is a bit much just to get the __init__
278 | handling. Let's just do the simple thing.
279 |
280 | let's remember to document that we're not going to use the AttrCleaner
281 |
282 | damn, doesn't look like that's gonna work. ok, let's port it all over to be
283 | standalone (so where ipython kernel is running doesn't need bpython)
284 |
285 | [ ] also - will need to think about how to gracefully handle non-python
286 | kernels with this.
287 |
288 | [ ] don't ship inspection_standalone every time you connect, check if
289 | another bipython connection has already executed it on the kernel.
290 |
291 | [ ] syntax highlighting for ipython magics - otherwise PythonLexer will choke
292 |
293 | [ ] ask bob to be added to related projects on bpython-interpreter.org
294 |
295 | [x] make screencast demo
296 |
297 | [ ] process command line argument to connect to the right kernel
298 |
299 | [ ] tests: make tests for the generic client, then we won't be in as bad of
300 | a shape here
301 |
302 | [ ] OO refactor of client code as a mixin
303 |
304 | [ ] up-arrow shouldn't search for partial completion (since that won't work)
305 |
306 | [ ] cheap completion is broken again :(
307 |
308 | [ ] non-ascii completion breaks bipython (yay unicode)
309 |
310 | [ ] run completion for "import " since ipython supports that.
311 |
312 | [ ] implement `get_bipython()` command to inspect bipython as I work on it
313 |
314 | [ ] implement pager payload (pygments.lexers.text.*Lexer?)
315 | - Make foo? and foo?? work
316 |
317 | [ ] use pygments.lexers.agile.PythonTracebackLexer for tracebacks
318 |
319 | [ ] make bipython favicon.ico and upload it
320 |
321 | [ ] check bipython_logo.png references on the website
322 |
323 | [ ] stop printing the version number on login (it's annoying)
324 |
--------------------------------------------------------------------------------
/bipython/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Paul Ivanov
2 | # Distributed under the terms of the Modified BSD License.
3 | # The full license is in the LICENSE file distributed with this software.
4 |
5 | """bipython: the boldly indiscriminate Python interpreter
6 |
7 | http://bipython.org
8 | """
9 | from __future__ import absolute_import, with_statement, division
10 | from __future__ import print_function
11 |
12 | __author__ = 'Paul Ivanov '
13 | __copyright__ = 'Copyright (c) 2014 Paul Ivanov'
14 | __license__ = 'BSD'
15 | __version__ = '0.1.3'
16 |
17 | import sys
18 | import os
19 | import time
20 | import locale
21 | import signal
22 | from types import ModuleType
23 | from optparse import Option
24 |
25 | from pygments.token import Token
26 |
27 | from bpython import args as bpargs, repl, translations
28 | from bpython.formatter import theme_map
29 | from bpython.importcompletion import find_coroutine
30 | from bpython.translations import _
31 |
32 | from bpython.keys import urwid_key_dispatch as key_dispatch
33 | from bpython._py3compat import PythonLexer, py3
34 |
35 | import urwid
36 | import inspect
37 | from inspect import ArgSpec # we eval an ArgSpec repr, see ipython_get_argspec
38 |
39 | try:
40 | #python 3
41 | from queue import Empty
42 | except ImportError:
43 | #python 2
44 | from Queue import Empty
45 |
46 | Parenthesis = Token.Punctuation.Parenthesis
47 |
48 | try:
49 | import subprocess
50 | commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=os.path.dirname(__file__)).strip()
51 | __version__ += ' [' + commit + ']'
52 | except:
53 | pass
54 |
55 | version = '%s (Python %s) ' % (__version__, sys.version.split()[0])
56 |
57 |
58 |
59 | # Urwid colors are:
60 | # 'black', 'dark red', 'dark green', 'brown', 'dark blue',
61 | # 'dark magenta', 'dark cyan', 'light gray', 'dark gray',
62 | # 'light red', 'light green', 'yellow', 'light blue',
63 | # 'light magenta', 'light cyan', 'white'
64 | # and bpython has:
65 | # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default
66 |
67 | COLORMAP = {
68 | 'k': 'black',
69 | 'r': 'dark red', # or light red?
70 | 'g': 'dark green', # or light green?
71 | 'y': 'yellow',
72 | 'b': 'dark blue', # or light blue?
73 | 'm': 'dark magenta', # or light magenta?
74 | 'c': 'dark cyan', # or light cyan?
75 | 'w': 'white',
76 | 'd': 'default',
77 | }
78 |
79 | # Add our keys to the urwid command_map
80 | bipy_func = """
81 | def get_object(name):
82 | attributes = name.split('.')
83 | obj = eval(attributes.pop(0))
84 | while attributes:
85 | #with AttrCleaner(obj):
86 | obj = getattr(obj, attributes.pop(0))
87 | return obj
88 |
89 | def bipy_argspec(func):
90 | try:
91 | f = get_object(func)
92 | except (AttributeError, NameError, SyntaxError):
93 | return False
94 |
95 | if inspect.isclass(f):
96 | try:
97 | if f.__init__ is not object.__init__:
98 | f = f.__init__
99 | except AttributeError:
100 | return None
101 | return getargspec(func, f)
102 | """
103 | hack_path = os.path.dirname(__file__)
104 | with open(os.path.join(hack_path, 'inspection_standalone.py')) as f:
105 | bipy_func = f.read() + bipy_func
106 |
107 |
108 |
109 | try:
110 | from twisted.internet import protocol
111 | from twisted.protocols import basic
112 | except ImportError:
113 | pass
114 | else:
115 |
116 | class EvalProtocol(basic.LineOnlyReceiver):
117 |
118 | delimiter = '\n'
119 |
120 | def __init__(self, myrepl):
121 | self.repl = myrepl
122 |
123 | def lineReceived(self, line):
124 | # HACK!
125 | # TODO: deal with encoding issues here...
126 | self.repl.main_loop.process_input(line)
127 | self.repl.main_loop.process_input(['enter'])
128 |
129 |
130 | class EvalFactory(protocol.ServerFactory):
131 |
132 | def __init__(self, myrepl):
133 | self.repl = myrepl
134 |
135 | def buildProtocol(self, addr):
136 | return EvalProtocol(self.repl)
137 |
138 | # XXX: copy-paste eng from vim-ipython
139 | import re
140 | # from http://serverfault.com/questions/71285/in-centos-4-4-how-can-i-strip-escape-sequences-from-a-text-file
141 | strip = re.compile('\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]')
142 |
143 | def strip_color_escapes(s):
144 | return strip.sub('',s)
145 |
146 |
147 | # If Twisted is not available urwid has no TwistedEventLoop attribute.
148 | # Code below will try to import reactor before using TwistedEventLoop.
149 | # I assume TwistedEventLoop will be available if that import succeeds.
150 | if urwid.VERSION < (1, 0, 0) and hasattr(urwid, 'TwistedEventLoop'):
151 | class TwistedEventLoop(urwid.TwistedEventLoop):
152 |
153 | """TwistedEventLoop modified to properly stop the reactor.
154 |
155 | urwid 0.9.9 and 0.9.9.1 crash the reactor on ExitMainLoop instead
156 | of stopping it. One obvious way this breaks is if anything used
157 | the reactor's thread pool: that thread pool is not shut down if
158 | the reactor is not stopped, which means python hangs on exit
159 | (joining the non-daemon threadpool threads that never exit). And
160 | the default resolver is the ThreadedResolver, so if we looked up
161 | any names we hang on exit. That is bad enough that we hack up
162 | urwid a bit here to exit properly.
163 | """
164 |
165 | def handle_exit(self, f):
166 | def wrapper(*args, **kwargs):
167 | try:
168 | return f(*args, **kwargs)
169 | except urwid.ExitMainLoop:
170 | # This is our change.
171 | self.reactor.stop()
172 | except:
173 | # This is the same as in urwid.
174 | # We are obviously not supposed to ever hit this.
175 | import sys
176 | print(sys.exc_info())
177 | self._exc_info = sys.exc_info()
178 | self.reactor.crash()
179 | return wrapper
180 | else:
181 | TwistedEventLoop = getattr(urwid, 'TwistedEventLoop', None)
182 |
183 |
184 | class StatusbarEdit(urwid.Edit):
185 | """Wrapper around urwid.Edit used for the prompt in Statusbar.
186 |
187 | This class only adds a single signal that is emitted if the user presses
188 | Enter."""
189 |
190 | signals = urwid.Edit.signals + ['prompt_enter']
191 |
192 | def __init__(self, *args, **kwargs):
193 | self.single = False
194 | urwid.Edit.__init__(self, *args, **kwargs)
195 |
196 | def keypress(self, size, key):
197 | if self.single:
198 | urwid.emit_signal(self, 'prompt_enter', self, key)
199 | elif key == 'enter':
200 | urwid.emit_signal(self, 'prompt_enter', self, self.get_edit_text())
201 | else:
202 | return urwid.Edit.keypress(self, size, key)
203 |
204 | urwid.register_signal(StatusbarEdit, 'prompt_enter')
205 |
206 | class Statusbar(object):
207 |
208 | """Statusbar object, ripped off from bpython.cli.
209 |
210 | This class provides the status bar at the bottom of the screen.
211 | It has message() and prompt() methods for user interactivity, as
212 | well as settext() and clear() methods for changing its appearance.
213 |
214 | The check() method needs to be called repeatedly if the statusbar is
215 | going to be aware of when it should update its display after a message()
216 | has been called (it'll display for a couple of seconds and then disappear).
217 |
218 | It should be called as:
219 | foo = Statusbar('Initial text to display')
220 | or, for a blank statusbar:
221 | foo = Statusbar()
222 |
223 | The "widget" attribute is an urwid widget.
224 | """
225 |
226 | signals = ['prompt_result']
227 |
228 | def __init__(self, config, s=None, main_loop=None):
229 | self.config = config
230 | self.timer = None
231 | self.main_loop = main_loop
232 | self.s = s or ''
233 |
234 | self.text = urwid.Text(('main', self.s))
235 | # use wrap mode 'clip' to just cut off at the end of line
236 | self.text.set_wrap_mode('clip')
237 |
238 | self.edit = StatusbarEdit(('main', ''))
239 | urwid.connect_signal(self.edit, 'prompt_enter', self._on_prompt_enter)
240 |
241 | self.widget = urwid.Columns([self.text, self.edit])
242 |
243 | def _check(self, callback, userdata=None):
244 | """This is the method is called from the timer to reset the status bar."""
245 | self.timer = None
246 | self.settext(self.s)
247 |
248 | def message(self, s, n=3):
249 | """Display a message for a short n seconds on the statusbar and return
250 | it to its original state."""
251 |
252 | self.settext(s)
253 | self.timer = self.main_loop.set_alarm_in(n, self._check)
254 |
255 | def _reset_timer(self):
256 | """Reset the timer from message."""
257 | if self.timer is not None:
258 | self.main_loop.remove_alarm(self.timer)
259 | self.timer = None
260 |
261 | def prompt(self, s=None, single=False):
262 | """Prompt the user for some input (with the optional prompt 's'). After
263 | the user hit enter the signal 'prompt_result' will be emited and the
264 | status bar will be reset. If single is True, the first keypress will be
265 | returned."""
266 |
267 | self._reset_timer()
268 |
269 | self.edit.single = single
270 | self.edit.set_caption(('main', s or '?'))
271 | self.edit.set_edit_text('')
272 | # hide the text and display the edit widget
273 | if not self.edit in self.widget.widget_list:
274 | self.widget.widget_list.append(self.edit)
275 | if self.text in self.widget.widget_list:
276 | self.widget.widget_list.remove(self.text)
277 | self.widget.set_focus_column(0)
278 |
279 | def settext(self, s, permanent=False):
280 | """Set the text on the status bar to a new value. If permanent is True,
281 | the new value will be permanent. If that status bar is in prompt mode,
282 | the prompt will be aborted. """
283 |
284 | self._reset_timer()
285 |
286 | # hide the edit and display the text widget
287 | if self.edit in self.widget.widget_list:
288 | self.widget.widget_list.remove(self.edit)
289 | if not self.text in self.widget.widget_list:
290 | self.widget.widget_list.append(self.text)
291 |
292 | self.text.set_text(('main', s))
293 | if permanent:
294 | self.s = s
295 |
296 | def clear(self):
297 | """Clear the status bar."""
298 | self.settext('')
299 |
300 | def _on_prompt_enter(self, edit, new_text):
301 | """Reset the statusbar and pass the input from the prompt to the caller
302 | via 'prompt_result'."""
303 | self.settext(self.s)
304 | urwid.emit_signal(self, 'prompt_result', new_text)
305 |
306 | urwid.register_signal(Statusbar, 'prompt_result')
307 |
308 |
309 | def decoding_input_filter(keys, raw):
310 | """Input filter for urwid which decodes each key with the locale's
311 | preferred encoding.'"""
312 | encoding = locale.getpreferredencoding()
313 | converted_keys = list()
314 | for key in keys:
315 | if isinstance(key, basestring):
316 | converted_keys.append(key.decode(encoding))
317 | else:
318 | converted_keys.append(key)
319 | return converted_keys
320 |
321 | def format_tokens(tokensource):
322 | for token, text in tokensource:
323 | if text == '\n':
324 | continue
325 |
326 | # TODO: something about inversing Parenthesis
327 | while token not in theme_map:
328 | token = token.parent
329 | yield (theme_map[token], text)
330 |
331 |
332 | class BPythonEdit(urwid.Edit):
333 |
334 | """Customized editor *very* tightly interwoven with URWIDRepl.
335 |
336 | Changes include:
337 |
338 | - The edit text supports markup, not just the caption.
339 | This works by calling set_edit_markup from the change event
340 | as well as whenever markup changes while text does not.
341 |
342 | - The widget can be made readonly, which currently just means
343 | it is no longer selectable and stops drawing the cursor.
344 |
345 | This is currently a one-way operation, but that is just because
346 | I only need and test the readwrite->readonly transition.
347 |
348 | - move_cursor_to_coords is ignored
349 | (except for internal calls from keypress or mouse_event).
350 |
351 | - arrow up/down are ignored.
352 |
353 | - an "edit-pos-changed" signal is emitted when edit_pos changes.
354 | """
355 |
356 | signals = ['edit-pos-changed']
357 |
358 | def __init__(self, config, *args, **kwargs):
359 | self._bpy_text = ''
360 | self._bpy_attr = []
361 | self._bpy_selectable = True
362 | self._bpy_may_move_cursor = False
363 | self.config = config
364 | self.tab_length = config.tab_length
365 | urwid.Edit.__init__(self, *args, **kwargs)
366 |
367 | def set_edit_pos(self, pos):
368 | urwid.Edit.set_edit_pos(self, pos)
369 | self._emit("edit-pos-changed", self.edit_pos)
370 |
371 | def get_edit_pos(self):
372 | return self._edit_pos
373 |
374 | edit_pos = property(get_edit_pos, set_edit_pos)
375 |
376 | def make_readonly(self):
377 | self._bpy_selectable = False
378 | # This is necessary to prevent the listbox we are in getting
379 | # fresh cursor coords of None from get_cursor_coords
380 | # immediately after we go readonly and then getting a cached
381 | # canvas that still has the cursor set. It spots that
382 | # inconsistency and raises.
383 | self._invalidate()
384 |
385 | def set_edit_markup(self, markup):
386 | """Call this when markup changes but the underlying text does not.
387 |
388 | You should arrange for this to be called from the 'change' signal.
389 | """
390 | if markup:
391 | self._bpy_text, self._bpy_attr = urwid.decompose_tagmarkup(markup)
392 | else:
393 | # decompose_tagmarkup in some urwids fails on the empty list
394 | self._bpy_text, self._bpy_attr = '', []
395 | # This is redundant when we're called off the 'change' signal.
396 | # I'm assuming this is cheap, making that ok.
397 | self._invalidate()
398 |
399 | def get_text(self):
400 | return self._caption + self._bpy_text, self._attrib + self._bpy_attr
401 |
402 | def selectable(self):
403 | return self._bpy_selectable
404 |
405 | def get_cursor_coords(self, *args, **kwargs):
406 | # urwid gets confused if a nonselectable widget has a cursor position.
407 | if not self._bpy_selectable:
408 | return None
409 | return urwid.Edit.get_cursor_coords(self, *args, **kwargs)
410 |
411 | def render(self, size, focus=False):
412 | # XXX I do not want to have to do this, but listbox gets confused
413 | # if I do not (getting None out of get_cursor_coords because
414 | # we just became unselectable, then having this render a cursor)
415 | if not self._bpy_selectable:
416 | focus = False
417 | return urwid.Edit.render(self, size, focus=focus)
418 |
419 | def get_pref_col(self, size):
420 | # Need to make this deal with us being nonselectable
421 | if not self._bpy_selectable:
422 | return 'left'
423 | return urwid.Edit.get_pref_col(self, size)
424 |
425 | def move_cursor_to_coords(self, *args):
426 | if self._bpy_may_move_cursor:
427 | return urwid.Edit.move_cursor_to_coords(self, *args)
428 | return False
429 |
430 | def keypress(self, size, key):
431 | if urwid.command_map[key] in ['cursor up', 'cursor down']:
432 | # Do not handle up/down arrow, leave them for the repl.
433 | #sys.stderr.write("cursor keys")
434 | return key
435 |
436 | self._bpy_may_move_cursor = True
437 | try:
438 | if urwid.command_map[key] == 'cursor max left':
439 | self.edit_pos = 0
440 | elif urwid.command_map[key] == 'cursor max right':
441 | self.edit_pos = len(self.get_edit_text())
442 | elif urwid.command_map[key] == 'clear word':
443 | # ^w
444 | if self.edit_pos == 0:
445 | return
446 | line = self.get_edit_text()
447 | # delete any space left of the cursor
448 | p = len(line[:self.edit_pos].strip())
449 | line = line[:p] + line[self.edit_pos:]
450 | # delete a full word
451 | # XXX: fugly word splitting heuristics, but better than just
452 | # slitting on space
453 | np = max(
454 | line.rfind(' ', 0, p),
455 | line.rfind('.', 0, p-1),
456 | line.rfind('(', 0, p-1),
457 | line.rfind('=', 0, p-1)
458 | ) + 1
459 | if np == -1:
460 | line = line[p:]
461 | np = 0
462 | else:
463 | line = line[:np] + line[p:]
464 | self.set_edit_text(line)
465 | self.edit_pos = np
466 | elif urwid.command_map[key] == 'clear line':
467 | line = self.get_edit_text()
468 | self.set_edit_text(line[self.edit_pos:])
469 | self.edit_pos = 0
470 | elif key == 'backspace':
471 | line = self.get_edit_text()
472 | cpos = len(line) - self.edit_pos
473 | if not (cpos or len(line) % self.tab_length or line.strip()):
474 | self.set_edit_text(line[:-self.tab_length])
475 | else:
476 | return urwid.Edit.keypress(self, size, key)
477 | else:
478 | # TODO: Add in specific keypress fetching code here
479 | return urwid.Edit.keypress(self, size, key)
480 | return None
481 | finally:
482 | self._bpy_may_move_cursor = False
483 |
484 | def mouse_event(self, *args):
485 | self._bpy_may_move_cursor = True
486 | try:
487 | return urwid.Edit.mouse_event(self, *args)
488 | finally:
489 | self._bpy_may_move_cursor = False
490 |
491 | class BPythonListBox(urwid.ListBox):
492 | """Like `urwid.ListBox`, except that it does not eat up and
493 | down keys.
494 | """
495 | def keypress(self, size, key):
496 | if key not in ["up", "down"]:
497 | return urwid.ListBox.keypress(self, size, key)
498 | return key
499 |
500 | class Tooltip(urwid.BoxWidget):
501 |
502 | """Container inspired by Overlay to position our tooltip.
503 |
504 | bottom_w should be a BoxWidget.
505 | The top window currently has to be a listbox to support shrinkwrapping.
506 |
507 | This passes keyboard events to the bottom instead of the top window.
508 |
509 | It also positions the top window relative to the cursor position
510 | from the bottom window and hides it if there is no cursor.
511 | """
512 |
513 | def __init__(self, bottom_w, listbox):
514 | self.__super.__init__()
515 |
516 | self.bottom_w = bottom_w
517 | self.listbox = listbox
518 | # TODO: this linebox should use the 'main' color.
519 | self.top_w = urwid.LineBox(listbox)
520 | self.tooltip_focus = False
521 |
522 | def selectable(self):
523 | return self.bottom_w.selectable()
524 |
525 | def keypress(self, size, key):
526 | return self.bottom_w.keypress(size, key)
527 |
528 | def mouse_event(self, size, event, button, col, row, focus):
529 | # TODO: pass to top widget if visible and inside it.
530 | if not hasattr(self.bottom_w, 'mouse_event'):
531 | return False
532 |
533 | return self.bottom_w.mouse_event(
534 | size, event, button, col, row, focus)
535 |
536 | def get_cursor_coords(self, size):
537 | return self.bottom_w.get_cursor_coords(size)
538 |
539 | def render(self, size, focus=False):
540 | maxcol, maxrow = size
541 | bottom_c = self.bottom_w.render(size, focus)
542 | cursor = bottom_c.cursor
543 | if not cursor:
544 | # Hide the tooltip if there is no cursor.
545 | return bottom_c
546 |
547 | cursor_x, cursor_y = cursor
548 | if cursor_y * 2 < maxrow:
549 | # Cursor is in the top half. Tooltip goes below it:
550 | y = cursor_y + 1
551 | rows = maxrow - y
552 | else:
553 | # Cursor is in the bottom half. Tooltip fills the area above:
554 | y = 0
555 | rows = cursor_y
556 |
557 | # HACK: shrink-wrap the tooltip. This is ugly in multiple ways:
558 | # - It only works on a listbox.
559 | # - It assumes the wrapping LineBox eats one char on each edge.
560 | # - It is a loop.
561 | # (ideally it would check how much free space there is,
562 | # instead of repeatedly trying smaller sizes)
563 | while 'bottom' in self.listbox.ends_visible((maxcol - 2, rows - 3)):
564 | rows -= 1
565 |
566 | # If we're displaying above the cursor move the top edge down:
567 | if not y:
568 | y = cursor_y - rows
569 |
570 | # Render *both* windows focused. This is probably not normal in urwid,
571 | # but it works nicely.
572 | top_c = self.top_w.render((maxcol, rows),
573 | focus and self.tooltip_focus)
574 |
575 | combi_c = urwid.CanvasOverlay(top_c, bottom_c, 0, y)
576 | # Use the cursor coordinates from the bottom canvas.
577 | canvas = urwid.CompositeCanvas(combi_c)
578 | canvas.cursor = cursor
579 | return canvas
580 |
581 | class URWIDInteraction(repl.Interaction):
582 | def __init__(self, config, statusbar, frame):
583 | repl.Interaction.__init__(self, config, statusbar)
584 | self.frame = frame
585 | urwid.connect_signal(statusbar, 'prompt_result', self._prompt_result)
586 | self.callback = None
587 |
588 | def confirm(self, q, callback):
589 | """Ask for yes or no and call callback to return the result"""
590 |
591 | def callback_wrapper(result):
592 | callback(result.lower() in (_('y'), _('yes')))
593 |
594 | self.prompt(q, callback_wrapper, single=True)
595 |
596 | def notify(self, s, n=10):
597 | return self.statusbar.message(s, n)
598 |
599 | def prompt(self, s, callback=None, single=False):
600 | """Prompt the user for input. The result will be returned via calling
601 | callback. Note that there can only be one prompt active. But the
602 | callback can already start a new prompt."""
603 |
604 | if self.callback is not None:
605 | raise Exception('Prompt already in progress')
606 |
607 | self.callback = callback
608 | self.statusbar.prompt(s, single=single)
609 | self.frame.set_focus('footer')
610 |
611 | def _prompt_result(self, text):
612 | self.frame.set_focus('body')
613 | if self.callback is not None:
614 | # The callback might want to start another prompt, so reset it
615 | # before calling the callback.
616 | callback = self.callback
617 | self.callback = None
618 | callback(text)
619 |
620 |
621 | class NotIPythonKernel(Exception):
622 | pass
623 |
624 | class IPythonHistory(repl.History):
625 | """A history mechanism that interacts with IPython.
626 |
627 | This relies on the standard IPython kernel, because it uses
628 | `get_ipyton().history_manager` to fetch results.
629 |
630 | As a fall back, local readline completion should be implemented when a new
631 | instance of IPythonHistory can not initialize and raises an error.
632 | """
633 |
634 | def __init__(self, repl):
635 | """The required argument is a handle on the repl, which will be ued to
636 | communicate with the IPython kernel. If a connection cannot be made,
637 | or no expected results are returned, we raise a NotIPythonKernel
638 | error, so that the vanilla readline completion can continue to be used
639 | as a fallback.
640 | """
641 | msg_id = repl.send_ipython('', silent=False, user_expressions={ 'hist':
642 | "list(get_ipython().history_manager.get_range())"})
643 | # XXX: for now we only grab history from current sesssion
644 | #"list(get_ipython().history_manager.get_tail(100))"})
645 | #silent=True)
646 |
647 |
648 | output = repl.ipython_get_child_msg(msg_id)['content']
649 | hist = eval(output['user_expressions']['hist']['data']['text/plain'])
650 | self.hist = hist
651 | repl.debug_docstring = str(hist)
652 | repl.debug_docstring = ''
653 | self.entries = ['']
654 | self.index = 0
655 | self.saved_line = ''
656 | self.duplicates = True # allow duplicates
657 | self.repl = repl
658 | self.load()
659 | #raise NotIPythonKernel()
660 |
661 | def load(self, *args, **kwargs):
662 | """Load history from a live IPython session.
663 |
664 | Arguments are ignored, and are only listed here for API compatibility
665 | with bpython's History class, which takes `filename` and `encoding`
666 | arguments, but those don't make sense in this instance.
667 | """
668 | # XXX: stopgap: get the history from ipython, write it to a file, and
669 | # proceed with the normal load after that
670 | for line in self.hist:
671 | self.append(line[-1])
672 | self.repl.stdout_hist += "\n" + line[-1]
673 |
674 | def save(self, *args, **kw):
675 | pass
676 |
677 | class URWIDRepl(repl.Repl):
678 |
679 | _time_between_redraws = .05 # seconds
680 | rl_history_reset = False
681 |
682 | def __init__(self, event_loop, palette, interpreter, config):
683 | repl.Repl.__init__(self, interpreter, config)
684 |
685 | self._redraw_handle = None
686 | self._redraw_pending = False
687 | self._redraw_time = 0
688 |
689 | self.listbox = BPythonListBox(urwid.SimpleListWalker([]))
690 |
691 | self.tooltip = urwid.ListBox(urwid.SimpleListWalker([]))
692 | self.tooltip.grid = None
693 | self.overlay = Tooltip(self.listbox, self.tooltip)
694 | self.stdout_hist = ''
695 |
696 | self.frame = urwid.Frame(self.overlay)
697 |
698 | if urwid.get_encoding_mode() == 'narrow':
699 | input_filter = decoding_input_filter
700 | else:
701 | input_filter = None
702 |
703 | # This constructs a raw_display.Screen, which nabs sys.stdin/out.
704 | self.main_loop = urwid.MainLoop(
705 | self.frame, palette,
706 | event_loop=event_loop, unhandled_input=self.handle_input,
707 | input_filter=input_filter, handle_mouse=False)
708 |
709 | # String is straight from bpython.cli
710 | self.statusbar = Statusbar(config,
711 | _(" <%s> Rewind <%s> Save <%s> Pastebin "
712 | " <%s> Pager <%s> Show Source ") %
713 | (config.undo_key, config.save_key, config.pastebin_key,
714 | config.last_output_key, config.show_source_key), self.main_loop)
715 | self.frame.set_footer(self.statusbar.widget)
716 | self.interact = URWIDInteraction(self.config, self.statusbar, self.frame)
717 |
718 | self.edits = []
719 | self.edit = None
720 | self.current_output = None
721 | self._completion_update_suppressed = False
722 |
723 | # Bulletproof: this is a value extract_exit_value accepts.
724 | self.exit_value = ()
725 |
726 | load_urwid_command_map(config)
727 | self.debug_docstring = ''
728 | self.ipython = self.connect_ipython_kernel()
729 |
730 | self.ipy_execution_count = '0'
731 | self.docstring_widget = None
732 |
733 | @property
734 | def ipy_ps1(self):
735 | return "In [%d]: " % (int(self.ipy_execution_count) + 1)
736 |
737 | def connect_ipython_kernel(self, s=''):
738 | """create kernel manager from IPKernelApp string
739 | such as '--shell=47378 --iopub=39859 --stdin=36778 --hb=52668' for IPython 0.11
740 | or just 'kernel-12345.json' for IPython 0.12
741 |
742 | XXX: copy-paste engineering from vim-ipython. Proceed with caution
743 | """
744 | def echo(x):
745 | print(x)
746 |
747 | try:
748 | import IPython
749 | except ImportError:
750 | raise ImportError("Could not find IPython. bipython needs it")
751 | from IPython.config.loader import KeyValueConfigLoader
752 | try:
753 | from IPython.kernel import (
754 | KernelManager,
755 | find_connection_file,
756 | )
757 | except ImportError:
758 | # IPython < 1.0
759 | from IPython.zmq.blockingkernelmanager import BlockingKernelManager as KernelManager
760 | from IPython.zmq.kernelapp import kernel_aliases
761 | try:
762 | from IPython.lib.kernel import find_connection_file
763 | except ImportError:
764 | # < 0.12, no find_connection_file
765 | pass
766 |
767 | s = s.replace('--existing', '')
768 | if 'connection_file' in KernelManager.class_trait_names():
769 | # 0.12 uses files instead of a collection of ports
770 | # include default IPython search path
771 | # filefind also allows for absolute paths, in which case the search
772 | # is ignored
773 | try:
774 | # XXX: the following approach will be brittle, depending on what
775 | # connection strings will end up looking like in the future, and
776 | # whether or not they are allowed to have spaces. I'll have to sync
777 | # up with the IPython team to address these issues -pi
778 | if '--profile' in s:
779 | k,p = s.split('--profile')
780 | k = k.lstrip().rstrip() # kernel part of the string
781 | p = p.lstrip().rstrip() # profile part of the string
782 | fullpath = find_connection_file(k,p)
783 | else:
784 | fullpath = find_connection_file(s.lstrip().rstrip())
785 | except IOError as e:
786 | self.echod(":IPython " + s + " failed")
787 | self.echod("^-- failed '" + s + "' not found")
788 | over_the_line()
789 | km = KernelManager(connection_file = fullpath)
790 | km.load_connection_file()
791 | else:
792 | if s == '':
793 | self.echod(":IPython 0.11 requires the full connection string")
794 | over_the_line()
795 | loader = KeyValueConfigLoader(s.split(), aliases=kernel_aliases)
796 | cfg = loader.load_config()['KernelApp']
797 | try:
798 | ip = '127.0.0.1'
799 | km = KernelManager(
800 | shell_address=(ip, cfg['shell_port']),
801 | sub_address=(ip, cfg['iopub_port']),
802 | stdin_address=(ip, cfg['stdin_port']),
803 | hb_address=(ip, cfg['hb_port']))
804 | except KeyError as e:
805 | self.echod(":IPython " +s + " failed")
806 | self.echod("^-- failed --"+e.message.replace('_port','')+" not specified")
807 | over_the_line()
808 |
809 |
810 | try:
811 | kc = km.client()
812 | except AttributeError:
813 | # 0.13
814 | kc = km
815 | kc.start_channels()
816 |
817 | self.send_ipython = kc.shell_channel.execute
818 | #XXX: backwards compatibility for IPython < 0.13
819 | sc = kc.shell_channel
820 | num_oinfo_args = len(inspect.getargspec(sc.object_info).args)
821 | if num_oinfo_args == 2:
822 | # patch the object_info method which used to only take one argument
823 | klass = sc.__class__
824 | klass._oinfo_orig = klass.object_info
825 | klass.object_info = lambda s,x,y: s._oinfo_orig(x)
826 |
827 | #XXX: backwards compatibility for IPython < 1.0
828 | if not hasattr(kc, 'iopub_channel'):
829 | kc.iopub_channel = kc.sub_channel
830 | self.km = km
831 | self.kc = kc
832 | print(km)
833 | msg_id = self.send_ipython('# bpython ' + version + ' connected\n')
834 | try:
835 | child = self.ipython_get_child_msg(msg_id)
836 | except Empty:
837 | over_the_line()
838 |
839 | self.send_ipython(bipy_func, silent=True)
840 | # TODO: get a proper history navigator
841 | #
842 | try:
843 | self.rl_history = IPythonHistory(self)
844 | except NotIPythonKernel:
845 | # We must not be running an IPython Kernel
846 | #sys.stderr.write(
847 | self.debug_docstring = "could not access IPython history, falling back to readline"
848 | sys.stderr.flush()
849 | pass
850 | self.ipython_set_pid()
851 |
852 | return km
853 |
854 | def _get_args(self):
855 | """Check if an unclosed parenthesis exists, then attempt to get the
856 | argspec() for it. On success, update self.argspec and return True,
857 | otherwise set self.argspec to None and return False"""
858 |
859 | self.current_func = None
860 |
861 |
862 | if not self.config.arg_spec:
863 | self.echo('i suck')
864 | return False
865 |
866 | #self.echod('\ri rule')
867 | # Get the name of the current function and where we are in
868 | # the arguments
869 | stack = [['', 0, '']]
870 | try:
871 | for (token, value) in PythonLexer().get_tokens(
872 | self.current_line()):
873 | if token is Token.Punctuation:
874 | if value in '([{':
875 | stack.append(['', 0, value])
876 | elif value in ')]}':
877 | stack.pop()
878 | elif value == ',':
879 | try:
880 | stack[-1][1] += 1
881 | except TypeError:
882 | stack[-1][1] = ''
883 | stack[-1][0] = ''
884 | elif value == ':' and stack[-1][2] == 'lambda':
885 | stack.pop()
886 | else:
887 | stack[-1][0] = ''
888 | elif (token is Token.Name or token in Token.Name.subtypes or
889 | token is Token.Operator and value == '.'):
890 | stack[-1][0] += value
891 | elif token is Token.Operator and value == '=':
892 | stack[-1][1] = stack[-1][0]
893 | stack[-1][0] = ''
894 | elif token is Token.Keyword and value == 'lambda':
895 | stack.append(['', 0, value])
896 | else:
897 | stack[-1][0] = ''
898 | while stack[-1][2] in '[{':
899 | stack.pop()
900 | _, arg_number, _ = stack.pop()
901 | func, _, _ = stack.pop()
902 | except IndexError:
903 | return False
904 | if not func:
905 | return False
906 |
907 | #self.echod('here we go, func is ' + func)
908 | self.current_func = func
909 |
910 | # XXX: this code needs to run on the ipython side
911 | # - can we recreate an argspec on this side after getting it
912 | # from the other side. ... break on through to the other side!
913 | #
914 | # looks like we can. Just need to get an ArgSpec back
915 |
916 | self.argspec = self.ipython_get_argspec(func)
917 |
918 | if self.argspec:
919 | self.argspec.append(arg_number)
920 | return True
921 | return False
922 |
923 |
924 | def complete(self, tab=False):
925 | "bipython completion - punt to ipython"
926 | self.docstring = ''
927 |
928 | returned = self.ipython_process_msgs()
929 |
930 |
931 | #if returned:
932 | # self.docstring = "\n".join(returned)
933 |
934 | if self.debug_docstring:
935 | self.docstring = self.debug_docstring
936 |
937 | if not self._get_args():
938 | self.argspec = None
939 |
940 | if self.current_func is not None:
941 | self.ipython_get_doc(self.current_func)
942 |
943 | pos = self.edit.edit_pos
944 | text = self.edit.get_edit_text()
945 |
946 | cw = self.cw() or ''
947 |
948 | if not cw and not tab:
949 | # don't trigger automatic completion on empty lines
950 | self.matches = []
951 | self.matches_iter.update()
952 | return False or self.docstring #and self.docstring.find('ipython') != -1
953 | #else:
954 | # self.docstring = 'yak yak yak!'
955 | self.matches = self.ipython_complete(cw, text, pos)
956 | self.matches_iter.update(cw, self.matches)
957 | return bool(self.matches) or self.docstring
958 |
959 | #if tab:
960 | # return bool(self.matches)
961 | #return False
962 | #cs = self.current_string()
963 |
964 | # Subclasses of Repl need to implement echo, current_line, cw
965 | def echod(self, orig_s):
966 | #self.write(orig_s)
967 | if self.edit:
968 | self.edit.set_caption(orig_s)
969 |
970 | def echo(self, orig_s):
971 | got_string = not isinstance(orig_s, list)
972 | s = orig_s
973 | if got_string:
974 | s = orig_s.rstrip('\n')
975 | if True:
976 | if self.current_output is None:
977 | # XXX: hacky post-parsing of output here..
978 | if not got_string:
979 | self.current_output = orig_s
980 | else:
981 | self.current_output = urwid.Text(('output', s))
982 | if self.edit is None:
983 | self.listbox.body.append(self.current_output)
984 | # Focus the widget we just added to force the
985 | # listbox to scroll. This causes output to scroll
986 | # if the user runs a blocking call that prints
987 | # more than a screenful, instead of staying
988 | # scrolled to the previous input line and then
989 | # jumping to the bottom when done.
990 | self.listbox.set_focus(len(self.listbox.body) - 1)
991 | else:
992 | self.listbox.body.insert(-1, self.current_output)
993 | # The edit widget should be focused and *stay* focused.
994 | # XXX TODO: make sure the cursor stays in the same spot.
995 | self.listbox.set_focus(len(self.listbox.body) - 1)
996 | else:
997 | # XXX this assumes this all has "output" markup applied.
998 | if got_string:
999 | self.current_output.set_text(
1000 | ('output', self.current_output.text + s))
1001 | else:
1002 | self.current_output.set_text(
1003 | [('output', self.current_output.text)] + s)
1004 |
1005 | if got_string and orig_s.endswith('\n'):
1006 | self.current_output = None
1007 |
1008 | # If we hit this repeatedly in a loop the redraw is rather
1009 | # slow (testcase: pprint(__builtins__). So if we have recently
1010 | # drawn the screen already schedule a call in the future.
1011 | #
1012 | # Unfortunately we may hit this function repeatedly through a
1013 | # blocking call triggered by the user, in which case our
1014 | # timeout will not run timely as we do not return to urwid's
1015 | # eventloop. So we manually check if our timeout has long
1016 | # since expired, and redraw synchronously if it has.
1017 | if self._redraw_handle is None:
1018 | self.main_loop.draw_screen()
1019 |
1020 | def maybe_redraw(loop, self):
1021 | if self._redraw_pending:
1022 | loop.draw_screen()
1023 | self._redraw_pending = False
1024 |
1025 | self._redraw_handle = None
1026 |
1027 | self._redraw_handle = self.main_loop.set_alarm_in(
1028 | self._time_between_redraws, maybe_redraw, self)
1029 | self._redraw_time = time.time()
1030 | else:
1031 | self._redraw_pending = True
1032 | now = time.time()
1033 | if now - self._redraw_time > 2 * self._time_between_redraws:
1034 | # The timeout is well past expired, assume we're
1035 | # blocked and redraw synchronously.
1036 | self.main_loop.draw_screen()
1037 | self._redraw_time = now
1038 |
1039 | def current_line(self):
1040 | """Return the current line (the one the cursor is in)."""
1041 | if self.edit is None:
1042 | return ''
1043 | return self.edit.get_edit_text()
1044 |
1045 | def cw(self):
1046 | """Return the current word (incomplete word left of cursor)."""
1047 | if self.edit is None:
1048 | return
1049 |
1050 | pos = self.edit.edit_pos
1051 | text = self.edit.get_edit_text()
1052 | if pos != len(text):
1053 | # Disable autocomplete if not at end of line, like cli does.
1054 | # XXX: I think we can make this compeltion work here -pi
1055 | return
1056 |
1057 | # Stolen from cli. TODO: clean up and split out.
1058 | if (not text or
1059 | (not text[-1].isalnum() and text[-1] not in ('.', '_'))):
1060 | return
1061 |
1062 | # Seek backwards in text for the first non-identifier char:
1063 | for i, c in enumerate(reversed(text)):
1064 | if not c.isalnum() and c not in ('.', '_'):
1065 | break
1066 | else:
1067 | # No non-identifiers, return everything.
1068 | return text
1069 | # Return everything to the right of the non-identifier.
1070 | return text[-i:]
1071 |
1072 | @property
1073 | def cpos(self):
1074 | if self.edit is not None:
1075 | return len(self.current_line()) - self.edit.edit_pos
1076 | return 0
1077 |
1078 | def _populate_completion(self):
1079 | widget_list = self.tooltip.body
1080 | while widget_list:
1081 | widget_list.pop()
1082 | # This is just me flailing around wildly. TODO: actually write.
1083 | if self.complete():
1084 | if self.argspec:
1085 | # This is mostly just stolen from the cli module.
1086 | func_name, args, is_bound, in_arg = self.argspec
1087 | args, varargs, varkw, defaults = args[:4]
1088 | kwonly, kwonly_defaults = [], {}
1089 | markup = [('bold name', func_name),
1090 | ('name', ': (')]
1091 |
1092 | # the isinstance checks if we're in a positional arg
1093 | # (instead of a keyword arg), I think
1094 | if is_bound and isinstance(in_arg, int):
1095 | in_arg += 1
1096 |
1097 | # bpython.cli checks if this goes off the edge and
1098 | # does clever wrapping. I do not (yet).
1099 | for k, i in enumerate(args):
1100 | if defaults and k + 1 > len(args) - len(defaults):
1101 | kw = repr(defaults[k - (len(args) - len(defaults))])
1102 | else:
1103 | kw = None
1104 |
1105 | if not k and str(i) == 'self':
1106 | color = 'name'
1107 | else:
1108 | color = 'token'
1109 |
1110 | if k == in_arg or i == in_arg:
1111 | color = 'bold ' + color
1112 |
1113 | if not py3:
1114 | # See issue #138: We need to format tuple unpacking correctly
1115 | # We use the undocumented function inspection.strseq() for
1116 | # that. Fortunately, that madness is gone in Python 3.
1117 | markup.append((color, inspect.strseq(i, str)))
1118 | else:
1119 | markup.append((color, str(i)))
1120 | if kw is not None:
1121 | markup.extend([('punctuation', '='),
1122 | ('token', kw)])
1123 | if k != len(args) - 1:
1124 | markup.append(('punctuation', ', '))
1125 |
1126 | if varargs:
1127 | if args:
1128 | markup.append(('punctuation', ', '))
1129 | markup.append(('token', '*' + varargs))
1130 |
1131 | if kwonly:
1132 | if not varargs:
1133 | if args:
1134 | markup.append(('punctuation', ', '))
1135 | markup.append(('punctuation', '*'))
1136 | for arg in kwonly:
1137 | if arg == in_arg:
1138 | color = 'bold token'
1139 | else:
1140 | color = 'token'
1141 | markup.extend([('punctuation', ', '),
1142 | (color, arg)])
1143 | if arg in kwonly_defaults:
1144 | markup.extend([('punctuation', '='),
1145 | ('token', kwonly_defaults[arg])])
1146 |
1147 | if varkw:
1148 | if args or varargs or kwonly:
1149 | markup.append(('punctuation', ', '))
1150 | markup.append(('token', '**' + varkw))
1151 | markup.append(('punctuation', ')'))
1152 | widget_list.append(urwid.Text(markup))
1153 | if self.matches:
1154 | attr_map = {}
1155 | focus_map = {'main': 'operator'}
1156 | texts = [urwid.AttrMap(urwid.Text(('main', match)),
1157 | attr_map, focus_map)
1158 | for match in self.matches]
1159 | width = max(text.original_widget.pack()[0] for text in texts)
1160 | gridflow = urwid.GridFlow(texts, width, 1, 0, 'left')
1161 | widget_list.append(gridflow)
1162 | self.tooltip.grid = gridflow
1163 | self.overlay.tooltip_focus = False
1164 | else:
1165 | self.tooltip.grid = None
1166 | self.frame.body = self.overlay
1167 | else:
1168 | self.frame.body = self.listbox
1169 | self.tooltip.grid = None
1170 | self.docstring_widget = urwid.Text(('comment', ''))
1171 | widget_list.append(self.docstring_widget)
1172 | self._populate_docstring()
1173 |
1174 | def _populate_docstring(self):
1175 | "Make visible the docstring"
1176 | self.docstring_widget.set_text(('comment', self.docstring))
1177 |
1178 | def clear_docstring(self):
1179 | "remove the docstring"
1180 | self.docstring_widget.set_text('')
1181 |
1182 | def reprint_line(self, lineno, tokens):
1183 | edit = self.edits[-len(self.buffer) + lineno - 1]
1184 | edit.set_edit_markup(list(format_tokens(tokens)))
1185 |
1186 | def getstdout(self):
1187 | """This method returns the 'spoofed' stdout buffer, for writing to a
1188 | file or sending to a pastebin or whatever."""
1189 |
1190 | return self.stdout_hist + '\n'
1191 |
1192 | def ask_confirmation(self, q):
1193 | """Ask for yes or no and return boolean"""
1194 | try:
1195 | reply = self.statusbar.prompt(q)
1196 | except ValueError:
1197 | return False
1198 |
1199 | return reply.lower() in ('y', 'yes')
1200 |
1201 | def reevaluate(self):
1202 | """Clear the buffer, redraw the screen and re-evaluate the history"""
1203 |
1204 | self.evaluating = True
1205 | self.stdout_hist = ''
1206 | self.f_string = ''
1207 | self.buffer = []
1208 | self.scr.erase()
1209 | self.s_hist = []
1210 | # Set cursor position to -1 to prevent paren matching
1211 | self.cpos = -1
1212 |
1213 | self.prompt(False)
1214 |
1215 | self.iy, self.ix = self.scr.getyx()
1216 | for line in self.history:
1217 | if py3:
1218 | self.stdout_hist += line + '\n'
1219 | else:
1220 | self.stdout_hist += line.encode(locale.getpreferredencoding()) + '\n'
1221 | self.print_line(line)
1222 | self.s_hist[-1] += self.f_string
1223 | # I decided it was easier to just do this manually
1224 | # than to make the print_line and history stuff more flexible.
1225 | self.scr.addstr('\n')
1226 | more = self.push(line)
1227 | self.prompt(more)
1228 | self.iy, self.ix = self.scr.getyx()
1229 |
1230 | self.cpos = 0
1231 | indent = repl.next_indentation(self.s, self.config.tab_length)
1232 | self.s = ''
1233 | self.scr.refresh()
1234 |
1235 | if self.buffer:
1236 | for _ in range(indent):
1237 | self.tab()
1238 |
1239 | self.evaluating = False
1240 | #map(self.push, self.history)
1241 | #^-- That's how simple this method was at first :(
1242 |
1243 | def write(self, s):
1244 | """For overriding stdout defaults"""
1245 | if '\x04' in s:
1246 | for block in s.split('\x04'):
1247 | self.write(block)
1248 | return
1249 | if s.rstrip() and '\x03' in s:
1250 | t = s.split('\x03')[1]
1251 | else:
1252 | t = s
1253 |
1254 | if not py3 and isinstance(t, unicode):
1255 | t = t.encode(locale.getpreferredencoding())
1256 |
1257 | if not self.stdout_hist:
1258 | self.stdout_hist = t
1259 | else:
1260 | self.stdout_hist += t
1261 |
1262 | self.echo(s)
1263 | self.s_hist.append(s.rstrip())
1264 |
1265 | def ipython_set_pid(self):
1266 | """
1267 | Explicitly ask the ipython kernel for its pid
1268 | """
1269 | lines = '\n'.join(['import os', '_pid = os.getpid()'])
1270 | msg_id = self.send_ipython(lines, silent=True, user_variables=['_pid'])
1271 |
1272 | # wait to get message back from kernel
1273 | try:
1274 | child = self.ipython_get_child_msg(msg_id)
1275 | except Empty:
1276 | #self.echo("no reply from IPython kernel")
1277 | self.ipy_pid = None
1278 | return
1279 | try:
1280 | pid = int(child['content']['user_variables']['_pid'])
1281 | except TypeError: # change in IPython 1.0.dev moved this out
1282 | pid = int(child['content']['user_variables']['_pid']['data']['text/plain'])
1283 | except KeyError: # change in IPython 1.0.dev moved this out
1284 | #self.echo("Could not get PID information, kernel not running Python?")
1285 | pass
1286 | self.ipy_pid = pid
1287 |
1288 | def ipython_interrupt_kernel_hack(self, signal_to_send=None):
1289 | """
1290 | Sends the interrupt signal to the remote kernel. This side steps the
1291 | (non-functional) ipython interrupt mechanisms.
1292 | Only works on posix.
1293 | """
1294 | pid = self.ipy_pid
1295 | if pid is None:
1296 | # Avoid errors if we couldn't get pid originally,
1297 | # by trying to obtain it now
1298 | self.ipython_set_pid()
1299 | pid = self.ipy_pid
1300 |
1301 | if pid is None:
1302 | self.echo("cannot get kernel PID, Ctrl-C will not be supported")
1303 | return
1304 | if not signal_to_send:
1305 | signal_to_send = signal.SIGINT
1306 |
1307 | self.echo("\n(KeyboardInterrupt)") # (sent to ipython: pid " +
1308 | #"%i with signal %s)" % (pid, signal_to_send))
1309 | try:
1310 | os.kill(pid, int(signal_to_send))
1311 | except OSError:
1312 | self.echo("unable to kill pid %d" % pid)
1313 | pid = None
1314 | self.ipython_process_msgs()
1315 |
1316 | def ipython_get_argspec(self, func):
1317 | self.send_ipython('', silent=True,
1318 | user_expressions={'argspec': 'bipy_argspec("'+func+'")'})
1319 | #for msg in self.kc.shell_channel.get_msgs():
1320 | # #msg = self.kc.get_shell_msg()['content']
1321 | # if 'argspec' not in msg['user_exprsessions']:
1322 | # self.echod("skipping" + str(msg))
1323 | # else:
1324 | # break
1325 | msg = self.kc.get_shell_msg()['content']
1326 | aspec = msg['user_expressions']['argspec']
1327 | #self.echod(aspec['data'])
1328 | if 'ename' in aspec:
1329 | self.echod("got an error")
1330 | return None
1331 |
1332 | return eval(aspec['data']['text/plain']) # relies on ArgSpec
1333 |
1334 |
1335 | def ipython_complete(self, base, current_line, pos=None):
1336 | #self.echo('\ncomplete called' + base + ' ' + current_line)
1337 | msg_id = self.kc.shell_channel.complete(base, current_line, pos)
1338 | try:
1339 | #self.echod('\ntrying to get match for ' + base + " XXX")
1340 | m = self.ipython_get_child_msg(msg_id)
1341 | matches = m['content']['matches']
1342 | #matches.insert(0,base) # the "no completion" version #not for bp
1343 | # we need to be careful with unicode, because we can have unicode
1344 | # completions for filenames (for the %run magic, for example). So the next
1345 | # line will fail on those:
1346 | #completions= [str(u) for u in matches]
1347 | # because str() won't work for non-ascii characters
1348 | # and we also have problems with unicode in vim, hence the following:
1349 | #self.echo("\nmatches: " + " ".join(matches))
1350 | return matches
1351 | except Empty:
1352 | self.echo("no reply from IPython kernel")
1353 | return ['']
1354 |
1355 | def ipython_get_child_msg(self, msg_id):
1356 | # XXX: message handling should be split into its own process in the future
1357 | while True:
1358 | # get_msg will raise with Empty exception if no messages arrive in 1 second
1359 | m = self.kc.shell_channel.get_msg(timeout=1)
1360 | if m['parent_header']['msg_id'] == msg_id:
1361 | #self.echod('\n\tshell_channel: ' + str(m['content']))
1362 | break
1363 | else:
1364 | #got a message, but not the one we were looking for
1365 | #self.echod('\n\tshell_channel (skipping): ' + str(m['content']))
1366 | pass
1367 | return m
1368 |
1369 | def ipython_get_doc(self, func):
1370 | #self.debug_docstring = 'doc called for ' + func
1371 | #self.stdout_hist += "\nDEBUG: doc called for " + func
1372 | #self.send_ipython('# ' + self.debug_docstring)
1373 | try:
1374 | level = 0
1375 | msg_id = self.kc.shell_channel.object_info(func, level)
1376 | doc = self.ipython_get_doc_msg(msg_id)
1377 | if len(doc) == 0:
1378 | doc = ['']
1379 | self.docstring = "\n".join(doc)
1380 | except (IndexError, TypeError):
1381 | self.docstring = ''
1382 | self._populate_docstring()
1383 |
1384 | def ipython_get_doc_msg(self, msg_id):
1385 | n = 13 # longest field name (empirically)
1386 | b=[]
1387 | try:
1388 | content = self.ipython_get_child_msg(msg_id)['content']
1389 | except Empty:
1390 | # timeout occurred
1391 | return ["no reply from IPython kernel"]
1392 |
1393 | if not content['found']:
1394 | return b
1395 |
1396 | # XXX: in vim-ipython I do all of these:
1397 | #
1398 | # for field in ['type_name','base_class','string_form','namespace',
1399 | # 'file','length','definition','source','docstring']:
1400 | #
1401 | # But with argspec inspection, that seems too verbose.
1402 | #
1403 | ds = content.get('docstring','')
1404 | if ds == '':
1405 | b = ['']
1406 | else:
1407 | b = [ ds, '' ]
1408 | for field in ['base_class','string_form','namespace',
1409 | 'file','length','definition','source']:
1410 | c = content.get(field,None)
1411 | if c:
1412 | if field in ['definition']:
1413 | c = strip_color_escapes(c).rstrip()
1414 | s = field.replace('_',' ').title()+':'
1415 | s = s.ljust(n)
1416 | if c.find('\n')==-1:
1417 | b.append(s+c)
1418 | else:
1419 | b.append(s)
1420 | b.extend(c.splitlines())
1421 | return b
1422 |
1423 | def ipython_process_msgs(self):
1424 | #b = ['\nIPY msgs']
1425 | b = ['']
1426 | status_prompt_out = '\nOut[%(line)d]: '
1427 | status_prompt_in = '\n\nIn [%(line)d]: '
1428 | msgs = self.kc.iopub_channel.get_msgs()
1429 | for m in msgs:
1430 | #db.append(str(m).splitlines())
1431 | s = ''
1432 | #self.echod('\n\tiopub channel: ' + str(m['content']))
1433 | if 'msg_type' not in m['header']:
1434 | # debug information
1435 | #echo('skipping a message on sub_channel','WarningMsg')
1436 | #echo(str(m))
1437 | continue
1438 | header = m['header']['msg_type']
1439 | if header == 'status':
1440 | continue
1441 | elif header == 'stream':
1442 | # TODO: alllow for distinguishing between stdout and stderr (using
1443 | # custom syntax markers in the vim-ipython buffer perhaps), or by
1444 | # also echoing the message to the status bar
1445 | s = strip_color_escapes(m['content']['data'])
1446 | #self.echod('ipython stream' + s)
1447 | elif header == 'pyout':
1448 | s = [('error', status_prompt_out % {'line':
1449 | m['content']['execution_count']})]
1450 | s += [('output', m['content']['data']['text/plain'])]
1451 | elif header == 'display_data':
1452 | # TODO: handle other display data types (HMTL? images?)
1453 | s += m['content']['data']['text/plain']
1454 | elif header == 'pyin':
1455 | # TODO: the next line allows us to resend a line to ipython if
1456 | # %doctest_mode is on. In the future, IPython will send the
1457 | # execution_count on subchannel, so this will need to be updated
1458 | # once that happens
1459 | # TODO: ignore if we're the ones who sent this, since that's
1460 | # already been typed out by the user and is still on the
1461 | # screen.
1462 | line_number = m['content'].get('execution_count', 0)
1463 | if line_number != self.ipy_execution_count:
1464 | #XXX: ignore these for now, assume we've typed them
1465 | prompt = status_prompt_in % {'line': line_number}
1466 | s = prompt
1467 | # add a continuation line (with trailing spaces if the prompt has them)
1468 | dots = '.' * len(prompt.rstrip())
1469 | dots += prompt[len(prompt.rstrip()):]
1470 | s += m['content']['code'].rstrip().replace('\n', '\n' + dots)
1471 | # TODO - recolorize output here
1472 | # call on_input_change for the right lines
1473 | #tokens = self.tokenize(code, False)
1474 | #edit.set_edit_markup(list(format_tokens(tokens)))
1475 | if 'execution_count' in m['content']:
1476 | self.ipy_execution_count = m['content']['execution_count']
1477 |
1478 | elif header == 'pyerr':
1479 | c = m['content']
1480 | # XXX: when we learn how to parse color escapes for urwid to
1481 | # handle nicely, don't strip them on the next line
1482 | s = "\n".join(map(strip_color_escapes,c['traceback']))
1483 | s += c['ename'] + ":" + c['evalue']
1484 |
1485 | if isinstance(s, list):
1486 | b.extend(s)
1487 | elif s.find('\n') == -1:
1488 | b.append(s)
1489 | else:
1490 | b.extend(s.splitlines())
1491 | self.echo(s)
1492 | return b
1493 |
1494 | def push(self, s, insert_into_history=True):
1495 | # Restore the original SIGINT handler. This is needed to be able
1496 | # to break out of infinite loops. If the interpreter itself
1497 | # sees this it prints 'KeyboardInterrupt' and returns (good).
1498 | orig_handler = signal.getsignal(signal.SIGINT)
1499 | signal.signal(signal.SIGINT, signal.default_int_handler)
1500 | # Pretty blindly adapted from bpython.cli
1501 | try:
1502 | msg_id = self.send_ipython(s)
1503 | #self.rl_history.enter(s)
1504 | if hasattr(repl.Repl, 'insert_into_history'):
1505 | # this is only in unreleased version of bpython
1506 | self.insert_into_history(s)
1507 | # on the IPython side, at least for the Python kernel, history
1508 | # is managed for us by the history manager, so there's no need
1509 | # to do anything here.
1510 | if self.edit is not None:
1511 | self.edit.make_readonly()
1512 | self.buffer = []
1513 | self.edit = None
1514 | ret_msg = self.ipython_get_child_msg(msg_id)
1515 | if 'execution_count' in ret_msg['content']:
1516 | self.ipy_execution_count = ret_msg['content']['execution_count']
1517 | #self.echod('\n shell: ' + str(ret_msg['content']))
1518 | #self.send_ipython("###retmsg " + str(ret_msg))
1519 | #self.send_ipython("###retmsg " + str(returned))
1520 | #self.prompt(
1521 | #self.echod("\n#ipython".join(returned))
1522 | #x = repl.Repl.push(self, s, insert_into_history)
1523 | #self.echod("\n#ipython".join(returned))
1524 | #self.send_ipython("x = " + str(x))
1525 | #+ "\n".join(returned)
1526 | return False
1527 | except SystemExit as e:
1528 | self.exit_value = e.args
1529 | raise urwid.ExitMainLoop()
1530 | except KeyboardInterrupt:
1531 | # KeyboardInterrupt happened between the except block around
1532 | # user code execution and this code. This should be rare,
1533 | # but make sure to not kill bpython here, so leaning on
1534 | # ctrl+c to kill buggy code running inside bpython is safe.
1535 | self.keyboard_interrupt()
1536 | except Empty:
1537 | # let's wait until Ctrl-C or we get some results
1538 | self.prompt(False)
1539 | while True:
1540 | # we've submitted, so any pending output should go below
1541 | try:
1542 | self.ipython_process_msgs()
1543 | ret_msg = self.ipython_get_child_msg(msg_id)
1544 | if 'execution_count' in ret_msg['content']:
1545 | self.ipy_execution_count = ret_msg['content']['execution_count']
1546 | except Empty:
1547 | pass
1548 | except KeyboardInterrupt:
1549 | self.keyboard_interrupt()
1550 | break
1551 | else:
1552 | break
1553 | finally:
1554 | signal.signal(signal.SIGINT, orig_handler)
1555 |
1556 | def start(self):
1557 | self.prompt(False)
1558 |
1559 | def keyboard_interrupt(self):
1560 | # If the user is currently editing, interrupt him. This
1561 | # mirrors what the regular python REPL does.
1562 | self.ipython_interrupt_kernel_hack()
1563 | if self.edit is not None:
1564 | # XXX this is a lot of code, and I am not sure it is
1565 | # actually enough code. Needs some testing.
1566 | #self.edit.insert_text('^C')
1567 | self.edit.set_edit_markup(('error','^C'))
1568 | self.edit.make_readonly()
1569 | self.edit = None
1570 | self.buffer = []
1571 | self.echo('\rKeyboardInterruptA')
1572 | self.prompt(False)
1573 | else:
1574 | # I do not quite remember if this is reachable, but let's
1575 | # be safe.
1576 | self.echo('KeyboardInterruptB')
1577 |
1578 | time.sleep(.5) # give the kill signal a chance to get processed
1579 | self.ipython_process_msgs()
1580 |
1581 | def prompt(self, more):
1582 | # Clear current output here, or output resulting from the
1583 | # current prompt run will end up appended to the edit widget
1584 | # sitting above this prompt:
1585 | self.current_output = None
1586 | # XXX is this the right place?
1587 | #self.rl_history.reset()
1588 | # XXX what is s_hist?
1589 |
1590 | # We need the caption to use unicode as urwid normalizes later
1591 | # input to be the same type, using ascii as encoding. If the
1592 | # caption is bytes this breaks typing non-ascii into bpython.
1593 | # Currently this decodes using ascii as I do not know where
1594 | # ps1 is getting loaded from. If anyone wants to make
1595 | # non-ascii prompts work feel free to fix this.
1596 | if not more:
1597 | caption = ('prompt', "\n" + self.ipy_ps1)
1598 | self.stdout_hist += self.ps1
1599 | else:
1600 | caption = ('prompt_more', self.ps2.decode('ascii'))
1601 | self.stdout_hist += self.ps2
1602 | self.edit = BPythonEdit(self.config, caption=caption)
1603 |
1604 | urwid.connect_signal(self.edit, 'change', self.on_input_change)
1605 | urwid.connect_signal(self.edit, 'edit-pos-changed',
1606 | self.on_edit_pos_changed)
1607 | # Do this after connecting the change signal handler:
1608 | self.edit.insert_text(4 * self.next_indentation() * ' ')
1609 | self.edits.append(self.edit)
1610 | self.listbox.body.append(self.edit)
1611 | self.listbox.set_focus(len(self.listbox.body) - 1)
1612 | # Hide the tooltip
1613 | self.frame.body = self.listbox
1614 |
1615 | def on_input_change(self, edit, text):
1616 | # TODO: we get very confused here if "text" contains newlines,
1617 | # so we cannot put our edit widget in multiline mode yet.
1618 | # That is probably fixable... !!!!! ARGH!!!
1619 | # Yes, fix this -pi
1620 | #if not edit.startswith(self.ipy_ps1):
1621 | #edit.set_caption(self.ipy_ps1)
1622 | tokens = self.tokenize(text, False)
1623 | #self.debug_docstring = str(list(format_tokens(tokens)))
1624 | ipy_tok = [('token', u''), ('number', self.ipy_ps1)]
1625 |
1626 | edit.set_edit_markup(ipy_tok + list(format_tokens(tokens)))
1627 | if not self._completion_update_suppressed:
1628 | # If we call this synchronously the get_edit_text() in repl.cw
1629 | # still returns the old text...
1630 | self.main_loop.set_alarm_in(
1631 | 0, lambda *args: self._populate_completion())
1632 |
1633 | def on_edit_pos_changed(self, edit, position):
1634 | """Gets called when the cursor position inside the edit changed.
1635 | Rehighlight the current line because there might be a paren under
1636 | the cursor now."""
1637 | tokens = self.tokenize(self.current_line(), False)
1638 | edit.set_edit_markup(list(format_tokens(tokens)))
1639 |
1640 | def handle_input(self, event):
1641 | # Since most of the input handling here should be handled in the edit
1642 | # instead, we return here early if the edit doesn't have the focus.
1643 | if self.frame.get_focus() != 'body':
1644 | return
1645 |
1646 | if event == 'enter':
1647 | inp = self.edit.get_edit_text()
1648 | self.history.append(inp)
1649 | self.edit.make_readonly()
1650 | # XXX what is this s_hist thing?
1651 | self.stdout_hist += inp + '\n'
1652 | self.edit = None
1653 | # This may take a while, so force a redraw first:
1654 | self.main_loop.draw_screen()
1655 | more = self.push(inp)
1656 | self.prompt(more)
1657 | # XXX: fetching all history is expensive, but better than nothing
1658 | # for now
1659 | self.rl_history_reset = True
1660 | elif event == 'ctrl d':
1661 | # ctrl+d on an empty line exits, otherwise deletes
1662 | if self.edit is not None:
1663 | if not self.edit.get_edit_text():
1664 | raise urwid.ExitMainLoop()
1665 | else:
1666 | self.main_loop.process_input(['delete'])
1667 | elif urwid.command_map[event] == 'cursor up':
1668 | # "back" from bpython.cli
1669 | if self.rl_history_reset:
1670 | self.rl_history = IPythonHistory(self)
1671 | self.rl_history_reset = False
1672 | self.rl_history.enter(self.edit.get_edit_text())
1673 | self.edit.set_edit_text('')
1674 | self.edit.insert_text(self.rl_history.back()) # + "#previous")
1675 | elif urwid.command_map[event] == 'cursor down':
1676 | # "fwd" from bpython.cli
1677 | self.rl_history.enter(self.edit.get_edit_text())
1678 | self.edit.set_edit_text('')
1679 | self.edit.insert_text(self.rl_history.forward()) # + "#next")
1680 | elif urwid.command_map[event] == 'next selectable':
1681 | self.tab()
1682 | elif urwid.command_map[event] == 'prev selectable':
1683 | self.tab(True)
1684 | elif event == 'esc':
1685 | self.ipython_get_doc('')
1686 | #self.clear_docstring() # why is this so slow?, ARGH!
1687 | # XXX: tab redraws really quickly
1688 | else:
1689 | self.echo(repr(event))
1690 |
1691 | def tab(self, back=False):
1692 | """Process the tab key being hit.
1693 |
1694 | If the line is blank or has only whitespace: indent.
1695 |
1696 | If there is text before the cursor: cycle completions.
1697 |
1698 | If `back` is True cycle backwards through completions, and return
1699 | instead of indenting.
1700 |
1701 | Returns True if the key was handled.
1702 | """
1703 | self._completion_update_suppressed = True
1704 | try:
1705 | # Heavily inspired by cli's tab.
1706 | text = self.edit.get_edit_text()
1707 | if not text.lstrip() and not back:
1708 | x_pos = len(text) - self.cpos
1709 | num_spaces = x_pos % self.config.tab_length
1710 | if not num_spaces:
1711 | num_spaces = self.config.tab_length
1712 |
1713 | self.edit.insert_text(' ' * num_spaces)
1714 | return True
1715 |
1716 | if not self.matches_iter:
1717 | self.complete(tab=True)
1718 | cw = self.current_string() or self.cw()
1719 | if not cw:
1720 | return True
1721 | else:
1722 | cw = self.matches_iter.current_word
1723 |
1724 |
1725 | if self.matches:
1726 | self.edit.set_edit_text(text[:-len(cw)])
1727 | if self.matches_iter:
1728 | self.edit.set_edit_text(
1729 | text[:-len(self.matches_iter.current())])
1730 |
1731 | if back:
1732 | current_match = self.matches_iter.previous()
1733 | else:
1734 | current_match = next(self.matches_iter)
1735 | if current_match:
1736 | self.overlay.tooltip_focus = True
1737 | if self.tooltip.grid:
1738 | self.tooltip.grid.set_focus(self.matches_iter.index)
1739 | self.edit.insert_text(current_match)
1740 | self.ipython_get_doc(current_match)
1741 | return True
1742 | finally:
1743 | self._completion_update_suppressed = False
1744 |
1745 | def main(args=None, locals_=None, banner=None):
1746 | translations.init()
1747 |
1748 | import argparse
1749 | parser = argparse.ArgumentParser(
1750 | description='the boldly indiscriminate Python interpreter')
1751 | parser.add_argument( '-v','--version', action='version',
1752 | version='%(prog)s ' + version)
1753 | parser.parse_known_args()
1754 |
1755 | # ok, it's not nice, i'm hiding all of these params, but LTS.
1756 | #
1757 | # TODO: maybe support displays other than raw_display?
1758 | config, options, exec_args = bpargs.parse(args, (
1759 | 'Urwid options', None, [
1760 | Option('--twisted', '-T', action='store_true',
1761 | help=_('Run twisted reactor.')),
1762 | Option('--reactor', '-r',
1763 | help=_('Select specific reactor (see --help-reactors). '
1764 | 'Implies --twisted.')),
1765 | Option('--help-reactors', action='store_true',
1766 | help=_('List available reactors for -r.')),
1767 | Option('--plugin', '-p',
1768 | help=_('twistd plugin to run (use twistd for a list). '
1769 | 'Use "--" to pass further options to the plugin.')),
1770 | Option('--server', '-s', type='int',
1771 | help=_('Port to run an eval server on (forces Twisted).')),
1772 | ]))
1773 |
1774 | if options.help_reactors:
1775 | try:
1776 | from twisted.application import reactors
1777 | # Stolen from twisted.application.app (twistd).
1778 | for r in reactors.getReactorTypes():
1779 | print(' %-4s\t%s' % (r.shortName, r.description))
1780 | except ImportError:
1781 | sys.stderr.write('No reactors are available. Please install '
1782 | 'twisted for reactor support.\n')
1783 | return
1784 |
1785 | # XXX: had to interject myself here to fix the blueness of comments
1786 | config.color_scheme['comment'] = 'g'
1787 | config.color_scheme['prompt'] = 'b'
1788 |
1789 | palette = [
1790 | (name, COLORMAP[color.lower()], 'default',
1791 | 'bold' if color.isupper() else 'default')
1792 | for name, color in config.color_scheme.items()]
1793 | palette.extend([
1794 | ('bold ' + name, color + ',bold', background, monochrome)
1795 | for name, color, background, monochrome in palette])
1796 |
1797 | if options.server or options.plugin:
1798 | options.twisted = True
1799 |
1800 | if options.reactor:
1801 | try:
1802 | from twisted.application import reactors
1803 | except ImportError:
1804 | sys.stderr.write('No reactors are available. Please install '
1805 | 'twisted for reactor support.\n')
1806 | return
1807 | try:
1808 | # XXX why does this not just return the reactor it installed?
1809 | reactor = reactors.installReactor(options.reactor)
1810 | if reactor is None:
1811 | from twisted.internet import reactor
1812 | except reactors.NoSuchReactor:
1813 | sys.stderr.write('Reactor %s does not exist\n' % (
1814 | options.reactor,))
1815 | return
1816 | event_loop = TwistedEventLoop(reactor)
1817 | elif options.twisted:
1818 | try:
1819 | from twisted.internet import reactor
1820 | except ImportError:
1821 | sys.stderr.write('No reactors are available. Please install '
1822 | 'twisted for reactor support.\n')
1823 | return
1824 | event_loop = TwistedEventLoop(reactor)
1825 | else:
1826 | # None, not urwid.SelectEventLoop(), to work with
1827 | # screens that do not support external event loops.
1828 | event_loop = None
1829 | # TODO: there is also a glib event loop. Do we want that one?
1830 |
1831 | # __main__ construction from bpython.cli
1832 | if locals_ is None:
1833 | main_mod = sys.modules['__main__'] = ModuleType('__main__')
1834 | locals_ = main_mod.__dict__
1835 |
1836 | if options.plugin:
1837 | try:
1838 | from twisted import plugin
1839 | from twisted.application import service
1840 | except ImportError:
1841 | sys.stderr.write('No twisted plugins are available. Please install '
1842 | 'twisted for twisted plugin support.\n')
1843 | return
1844 |
1845 | for plug in plugin.getPlugins(service.IServiceMaker):
1846 | if plug.tapname == options.plugin:
1847 | break
1848 | else:
1849 | sys.stderr.write('Plugin %s does not exist\n' % (options.plugin,))
1850 | return
1851 | plugopts = plug.options()
1852 | plugopts.parseOptions(exec_args)
1853 | serv = plug.makeService(plugopts)
1854 | locals_['service'] = serv
1855 | reactor.callWhenRunning(serv.startService)
1856 | exec_args = []
1857 | interpreter = repl.Interpreter(locals_, locale.getpreferredencoding())
1858 |
1859 | # This nabs sys.stdin/out via urwid.MainLoop
1860 | myrepl = URWIDRepl(event_loop, palette, interpreter, config)
1861 |
1862 | if options.server:
1863 | factory = EvalFactory(myrepl)
1864 | reactor.listenTCP(options.server, factory, interface='127.0.0.1')
1865 |
1866 | if options.reactor:
1867 | # Twisted sets a sigInt handler that stops the reactor unless
1868 | # it sees a different custom signal handler.
1869 | def sigint(*args):
1870 | reactor.callFromThread(myrepl.keyboard_interrupt)
1871 | signal.signal(signal.SIGINT, sigint)
1872 |
1873 | # Save stdin, stdout and stderr for later restoration
1874 | orig_stdin = sys.stdin
1875 | orig_stdout = sys.stdout
1876 | orig_stderr = sys.stderr
1877 | # urwid's screen start() and stop() calls currently hit sys.stdin
1878 | # directly (via RealTerminal.tty_signal_keys), so start the screen
1879 | # before swapping sys.std*, and swap them back before restoring
1880 | # the screen. This also avoids crashes if our redirected sys.std*
1881 | # are called before we get around to starting the mainloop
1882 | # (urwid raises an exception if we try to draw to the screen
1883 | # before starting it).
1884 | def run_with_screen_before_mainloop():
1885 | try:
1886 | # Currently we just set this to None because I do not
1887 | # expect code hitting stdin to work. For example: exit()
1888 | # (not sys.exit, site.py's exit) tries to close sys.stdin,
1889 | # which breaks urwid's shutdown. bpython.cli sets this to
1890 | # a fake object that reads input through curses and
1891 | # returns it. When using twisted I do not think we can do
1892 | # that because sys.stdin.read and friends block, and we
1893 | # cannot re-enter the reactor. If using urwid's own
1894 | # mainloop we *might* be able to do something similar and
1895 | # re-enter its mainloop.
1896 | sys.stdin = None #FakeStdin(myrepl)
1897 | sys.stdout = myrepl
1898 | sys.stderr = myrepl
1899 |
1900 | myrepl.main_loop.set_alarm_in(0, start)
1901 |
1902 | while True:
1903 | try:
1904 | myrepl.main_loop.run()
1905 | except KeyboardInterrupt:
1906 | # HACK: if we run under a twisted mainloop this should
1907 | # never happen: we have a SIGINT handler set.
1908 | # If we use the urwid select-based loop we just restart
1909 | # that loop if interrupted, instead of trying to cook
1910 | # up an equivalent to reactor.callFromThread (which
1911 | # is what our Twisted sigint handler does)
1912 | myrepl.main_loop.set_alarm_in(
1913 | 0, lambda *args: myrepl.keyboard_interrupt())
1914 | continue
1915 | break
1916 |
1917 | finally:
1918 | sys.stdin = orig_stdin
1919 | sys.stderr = orig_stderr
1920 | sys.stdout = orig_stdout
1921 |
1922 | # This needs more thought. What needs to happen inside the mainloop?
1923 | def start(main_loop, user_data):
1924 | if exec_args:
1925 | bpargs.exec_code(interpreter, exec_args)
1926 | if not options.interactive:
1927 | raise urwid.ExitMainLoop()
1928 | if not exec_args:
1929 | sys.path.insert(0, '')
1930 | # this is CLIRepl.startup inlined.
1931 | filename = os.environ.get('PYTHONSTARTUP')
1932 | if filename and os.path.isfile(filename):
1933 | with open(filename, 'r') as f:
1934 | if py3:
1935 | interpreter.runsource(f.read(), filename, 'exec')
1936 | else:
1937 | interpreter.runsource(f.read(), filename, 'exec',
1938 | encode=False)
1939 |
1940 | if banner is not None:
1941 | repl.write(banner)
1942 | repl.write('\n')
1943 | myrepl.start()
1944 |
1945 | # This bypasses main_loop.set_alarm_in because we must *not*
1946 | # hit the draw_screen call (it's unnecessary and slow).
1947 | def run_find_coroutine():
1948 | if find_coroutine():
1949 | main_loop.event_loop.alarm(0, run_find_coroutine)
1950 |
1951 | run_find_coroutine()
1952 |
1953 | myrepl.main_loop.screen.run_wrapper(run_with_screen_before_mainloop)
1954 |
1955 | if config.flush_output and not options.quiet:
1956 | sys.stdout.write(myrepl.getstdout())
1957 | if hasattr(sys.stdout, "flush"):
1958 | sys.stdout.flush()
1959 | return repl.extract_exit_value(myrepl.exit_value)
1960 |
1961 | def load_urwid_command_map(config):
1962 | urwid.command_map[key_dispatch[config.up_one_line_key]] = 'cursor up'
1963 | urwid.command_map[key_dispatch[config.down_one_line_key]] = 'cursor down'
1964 | urwid.command_map[key_dispatch['C-a']] = 'cursor max left'
1965 | urwid.command_map[key_dispatch['C-e']] = 'cursor max right'
1966 | urwid.command_map[key_dispatch[config.pastebin_key]] = 'pastebin'
1967 | urwid.command_map[key_dispatch['C-f']] = 'cursor right'
1968 | urwid.command_map[key_dispatch['C-b']] = 'cursor left'
1969 | urwid.command_map[key_dispatch['C-d']] = 'delete'
1970 | urwid.command_map[key_dispatch[config.clear_word_key]] = 'clear word'
1971 | urwid.command_map[key_dispatch[config.clear_line_key]] = 'clear line'
1972 |
1973 | """
1974 | 'clear_screen': 'C-l',
1975 | 'cut_to_buffer': 'C-k',
1976 | 'down_one_line': 'C-n',
1977 | 'exit': '',
1978 | 'last_output': 'F9',
1979 | 'pastebin': 'F8',
1980 | 'save': 'C-s',
1981 | 'show_source': 'F2',
1982 | 'suspend': 'C-z',
1983 | 'undo': 'C-r',
1984 | 'up_one_line': 'C-p',
1985 | 'yank_from_buffer': 'C-y'},
1986 | """
1987 | def over_the_line():
1988 | "This is a league game, Smokey"
1989 | import sys
1990 | sys.stderr.write("\n")
1991 | sys.stderr.write("""Unable to connect to IPython:
1992 | Either it's busy executing, or you haven't started one.
1993 | use `ipython console` in another shell first, or open a
1994 | new IPython Notebook\n""")
1995 | sys.exit(1)
1996 |
1997 | if __name__ == '__main__':
1998 | sys.exit(main())
1999 |
2000 |
--------------------------------------------------------------------------------