Swanky Python: Python development using Emacs' SLIME mode for Common Lisp

3 months ago 1

Installation

In the long term, this will be packaged for MELPA and PyPI for easy installation, but at present this project is nowhere near stable enough.

As of now, this is a new project in the "works on my machine" stage of development. If you run into problems installing it, or it installs but some features are broken, please open an issue or write me an email and I'll try and help debug it.

Emacs/elisp

Install slime and slime-star and clone this git repository. Then in your emacs config add:

(add-to-list 'load-path "~/path/to/swanky_python/slimy-python") (push 'slime-py slime-contribs)

This is the configuration that I use, with vim style keybindings and using doom emacs, consult, and company. It'd be much appreciated if people could contribute sample configurations based on vanilla emacs with standard keybindings, and integrating with other completion frameworks besides company.

Python

With pip:

pip install -e ~/research/swanky_python/ With uv:

uv pip install -e ~/path/to/swanky_python/

You probably want uv pip rather than uv add as the rest of your team might not be happy with the weird new dependency. Though if you're working alone or somehow all collaborators are emacs users ok with trying experimental unstable software, you could use uv add --dev

The -e flag is to install in editable mode, as this is in very early-stage development, and you will inevitably run into bugs in the course of normal usage that you will need to edit and fix.

Connecting

Automatically

Run M-x slime which will start a new python process in the current directory, load the swank python backend, and connect to it from emacs.

To configure this, set slime-lisp-implementations which by default contains python and uv-python, which run python3 -i or uv run python -i. Then run M-- M-x slime (with a negative argument), for it to prompt you with the list of implementations to select from, or set slime-default-lisp with the name of the implementation to use by default.

Manually

In your python program run:

import swanky_python swanky_python.start()

start() optionally takes one argument, a port number to listen on. If not provided it will choose a random available port, and return the port number it chose. It starts a background thread listening on localhost for a connection from emacs.

In emacs run M-x slime-connect, enter the host (localhost) and port number, and you should be connected!

Conflicts with Common Lisp (CL)

Right now we just clobber the behavior of slime that needs to be different for python than CL. You need to start a separate emacs process and make sure slime-py is not in slime-contribs before starting slime, in order to have it work normally for CL again. Long term I'd like to make a system where SLIME can enable different language specific behavior based on the backend implementation you're connected to or the language mode of the active buffer, so that different language backends for SLIME can coexist within the same emacs process. For now though, when you load slime-py it just overwrites some parts of slime to behave as needed for python rather than for CL. See Why SLIME? for more on why I decided to build on top of slime rather than forking it.

Why SLIME?

In its beginning, although developed for CL, SLIME was seen with the potential to support multiple languages, and basic proof-of-concept backends were written for ruby, R, javascript, and more. But they supported a small subset of the functionality of the CL backend, and were abandoned. Currently it is only being developed for CL and makes many lisp or CL specific assumptions. Even other lisp dialects (clojure, scheme, racket), have borrowed some code from SLIME but made their own language-specific implementations (CIDER, Geiser, Racket mode).

I still think that slime can make a great generic emacs frontend to other dynamic languages, or even static languages that support hot code reloading and runtime introspection. I'd like to make full-featured backends for not just python, but also javascript/typescript, ruby, elixir, and more. It makes since to share code rather than having separate projects for each one. To work with python, I've had to make surprisingly few changes in the elisp frontend, almost all the code for this project is in the python backend.

Features

Presentations

Python objects are displayed as presentations rather than as plaintext. This includes results in the repl, function arguments and local variables in backtrace buffers, values within the object inspector, and function arguments and return values in trace buffers. This means it shows a text representation of the object, but keeps a reference to the actual python object on the backend, so you can open it in the inspector, copy it to the repl, assign it to a variable, display documentation, and more. You can also add custom actions to the right click menu for presentations of different kinds of objects.

Inspector

Any presentation can be opened in the inspector, which for most objects will show their attributes, although you can write custom inspector displays for your classes. slime-inspector-filter will bring up a transient menu where you can toggle the display of different attribute types like dunder and private ones. A useful shortcut is slime-inspect-last-result which will open the inspector on the last evaluation result.

Evaluation

When editing python code you can evaluate the whole file (slime-compile-and-load-file), the current class (slime-py-eval-class), current function (slime-eval-defun), current block (slime-eval-python-block-at-point), current selection (slime-eval-region), current statement (slime-eval-python-statement-at-point), or current identifier (slime-eval-sexp-at-point). This will evaluate within the context of the module associated with the active file. The module that the REPL evaluates within can be changed with slime-repl-set-package.

Backtraces

On any uncaught exception, slime opens an interactive backtrace buffer. Here you can see all stack frames with their arguments and local variables as presentations. With the cursor on a stack frame, sldb-show-source will show the source code with the expression that triggered the exception highlighted, after which you can select a region and run slime-py-eval-region-in-frame, or sldb-set-repl-to-frame will spawn a repl in the context of that stack frame.

Documentation Browser

The various slime-help functions show documentation. This is mostly done by introspection on doc strings, type signatures, and attributes in the running process. So for example you can see in the video that python tells us the click package distribution is installed and a short description, but we need to import it before we can see documentation on its classes and methods. Also you can see it calls click a 'Common Lisp ASDF system', and upcases names as for CL. For now it's a bit rough around the edges. Eventually I'll have it display more information like where inherited behavior comes from, as in the Django Class-Based-View Inspector.

Thread View

slime-list-threads presents a table with information on all running threads. From there you can press d or slime-thread-debug to get a backtrace buffer for any thread.

Async Task View

If an async event loop is running, slime-list-threads will default to presenting a table with information on all asyncio tasks, and slime-py-toggle-threads-tasks switches between viewing threads and tasks. Here you can press x or slime-thread-kill to cancel a task, subject to the limitations of task cancellation in asyncio. In the video you can see hung requests finally return with "Internal Server Error" when we cancel the associated Task. As for threads you can open a backtrace buffer, but it's considerably less useful as async in python is stackless. It uses Task.get_stack() which for suspended coroutines just returns the stack frame where that task was created, and for running coroutines returns the call stack up to the frame where the task was created, but not the currently executing frame inside the Task. To get that we can toggle to the thread view and get the backtrace for the event loop thread.

To use this, the swank backend must to be started after the event loop has started, from the same thread.

Tracing

You can trace functions and methods with slime-trace-dialog-toggle-trace, and untrace all traced functions with slime-trace-dialog-untrace-all. All arguments and return values for traced functions will be shown as presentations in the *slime-trace* buffer. However, the presentations work by simply holding on to a reference of the object, not by making a deep copy of it. So while the text in the *slime-trace* buffer is the repr of the object at the time it was passed, by the time you inspect the presentation it may have mutated. For example in the video the file object is closed by the time we inspect it.

Debugging

At present there's no full debugger. The options for debugging are the repl and backtrace buffer, the trace, thread, and async views, improved print debugging with pp (print presentation), and the integrated AI. In the future I plan to integrate with dape for traditional step debugging, and add more advanced tracing debugging.

To use pp import it with from swanky_python.swank_common import pp. It's like basic print debugging except that it prints a presentation of the objects rather than text, so you can go back and inspect it, copy it to the repl, and more. Use it by:

pp(object)

or:

pp("text description", obj1, obj2, ...)

When passed a single object it returns the same object, so that any expression can be substituted with pp(expression)

Completion and Autodoc

When the cursor is within any function call, slime-autodoc-mode shows the function signature in the echo buffer, with the current argument highlighted. For completion we use jedi, which does suprisingly good type inference. For example in the video despite function foo being untyped, it sees that it's called with a str and provides str completion options.

Autoreload

We use code adapted from the autoreload extension for IPython to update old references to functions and classes with the new version when code is changed. The goal is to enable interactive development where you never need to restart your program on changes. If you run into situations where you do need to restart python for changes to take effect, please open an issue.

Integrated AI

It comes with a local AI model which is quite effective for debugging.

Security

~/.slime-secret

When the swanky python backend listens for a connection on TCP, the first message it receives needs to contain the contents of ~/.slime-secret

When you are running python and emacs as the same user, you don't need to worry about this, as emacs will generate a random ~/.slime-secret file if it doesn't exist, readable only by the current user.

For remote development, you'll need to manually create a ~/.slime-secret file where python is running, with the same contents as ~/.slime-secret for your emacs user. This is to ensure some other user can't connect to your python process and run code in it.

All features require loading the code in python

Swanky python will not automatically run python code you open, you need to explicitly load it with slime-compile-and-load-file. But all features like go to definition depend on having the code loaded, so this won't be the best tool for reading untrusted python code.

Exploiting SLIME from swank

If you're connected via swank to a python process on some server or in some untrusted container, you don't want it having access to your emacs process. The python process can send elisp for emacs to evaluate if slime-enable-evaluate-in-emacs is enabled, although it's off by default. Also in order to let jedi infer types and provide better completions, we allow python to request the contents of the active emacs buffer. Emacs will only send it if it's in python-mode or slime-repl-mode, but be aware that if you're connected to swank running in an untrusted container or environment you shouldn't open sensitive python code from another project.

If you find any other way that the swank backend can run code in or retrieve information from emacs, I consider it a security issue so please let me know.

Sandboxed development

A way swanky python can improve your security is by making it easier to develop inside a container or other sandbox. You just need to forward the TCP port to connect to it from emacs, set ~/.slime-secret, and you can have your python environment fully sandboxed.

Contributing

Hacking.org

Contains a haphazard dump of information on how things work, limitations of current features, ideas for improving them and future features, and links to other projects to get ideas from.

Documentation

So far I've just had time to make a brief overview and video demo of the main features. Really we need comprehensive documentation like the slime and sly manuals.

Artwork

A swanky environment for python should be swanky. At a minimum we need a non-AI generated logo for the project, and ascii art greetings like in sly when a repl is spawned.

Emacs configurations

My configuration for using this is based on doom emacs with vim keybindings and using company for completions. I'd like to include a variety of sample configurations so people can start with something that works well with their setup.

Ideas

Do you have experience with other development environments like Jupyter notebooks, PyCharm, VSCode, LispWorks, or gtoolkit? Let us know what functionality you find useful that we're missing. Did you just fix some tricky bug in your software and have an idea how your development environment could have made it faster to track down the cause? Any and all ideas are welcome.

Testing

At present you don't even have to try to uncover issues, you're bound to run into some just in the course of using this for normal python development. Please report all issues so that in the long term we can make this a stable environment like SLIME is for CL.

Coding

There's an endless list of bugs to fix and features to add. If you're just familiar with python and not elisp or emacs development that's no problem. Right now about 90% of the code is in python and 10% in elisp. If you want to work on some feature that requires elisp just let me know and I can handle the elisp part.

Contact

As codeberg doesn't have separate discussion functionality like github, feel free to use the issue tracker for general discussions. You can also email me.

Read Entire Article