├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── README.rst ├── THANKS ├── bipython ├── __init__.py └── inspection_standalone.py ├── requirements.txt └── setup.py /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [bipython](http://bipython.org/) 2 | 3 | [![bipython logo](http://bipython.org/images/bipython_logo.png)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipython 2 | pyzmq 3 | bpython 4 | pygments 5 | urwid 6 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------