AutoComplete.el : Python Code Completion in Emacs

January 21, 2009 at 10:01 PM | categories: python, emacs, linux | View Comments

A friend of mine and I like to show off to each other little Emacs tips and tricks we learn. Today, he introduced to me the very cool AutoComplete.el package. AutoComplete.el is intriguing to me because, unlike ropemacs mode which I've blogged about before, the completions AutoComplete.el provides are inline with your code in a dropdown box instead of in a seperate window (windows in Emacs are what most people call frames). However, AutoComplete is only a completion framework, it doesn't know anything about Python. Instead, it allows the user to plug into it, feeding it whatever sorts of intelligent completion you want, including Rope.

Setup

The two most important completions I want to integrate into it are Rope and yasnippet. AutoComplete can handle both of them nicely. You'll need to install the very latest development version (as of December '08) of Rope, Ropemacs and Ropemode:

sudo apt-get install mercurial
mkdir /tmp/rope && cd /tmp/rope
hg clone http://bitbucket.org/agr/rope
hg clone http://bitbucket.org/agr/ropemacs
hg clone http://bitbucket.org/agr/ropemode
sudo easy_install rope
ln -s ../ropemode/ropemode ropemacs/
sudo easy_install ropemacs

You'll also need to install Pymacs and Yasnippet if you haven't already:

mkdir -p ~/.emacs.d/vendor && cd ~/.emacs.d/vendor
wget http://pymacs.progiciels-bpi.ca/archives/Pymacs-0.23.tar.gz
tar xfv Pymacs-0.23.tar.gz
cd Pymacs-0.23
make
sudo easy_install .
cd ~/.emacs.d/vendor
wget http://yasnippet.googlecode.com/files/yasnippet-0.5.9.tar.bz2
tar xfv yasnippet-0.5.9.tar.bz2
cd ~/.emacs.d
ln -s vendor/yasnippet-0.5.9/snippets/ .

Make sure Pymacs and Yasnippet get into your load path, in your .emacs:

(add-to-list 'load-path "~/.emacs.d/vendor")
(progn (cd "~/.emacs.d/vendor")
       (normal-top-level-add-subdirs-to-load-path))

Install AutoComplete.el 0.1.0 (0.2.0 is now out, and is not compatible with this post, I'll try and update later):

cd ~/.emacs.d/vendor
wget http://www.emacswiki.org/emacs/?action=browse;id=auto-complete.el;raw=1;revision=5

Now to add some more elisp to your .emacs somewhere:

(require 'python)
(require 'auto-complete)
(require 'yasnippet)

(autoload 'python-mode "python-mode" "Python Mode." t)
(add-to-list 'auto-mode-alist '("\\.py\\'" . python-mode))
(add-to-list 'interpreter-mode-alist '("python" . python-mode))

;; Initialize Pymacs                                                                                           
(autoload 'pymacs-apply "pymacs")
(autoload 'pymacs-call "pymacs")
(autoload 'pymacs-eval "pymacs" nil t)
(autoload 'pymacs-exec "pymacs" nil t)
(autoload 'pymacs-load "pymacs" nil t)
;; Initialize Rope                                                                                             
(pymacs-load "ropemacs" "rope-")
(setq ropemacs-enable-autoimport t)

;; Initialize Yasnippet                                                                                        
;Don't map TAB to yasnippet                                                                                    
;In fact, set it to something we'll never use because                                                          
;we'll only ever trigger it indirectly.                                                                        
(setq yas/trigger-key (kbd "C-c <kp-multiply>"))
(yas/initialize)
(yas/load-directory "~/.emacs.d/snippets")



;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;                                         
;;; Auto-completion                                                                                            
;;;  Integrates:                                                                                               
;;;   1) Rope                                                                                                  
;;;   2) Yasnippet                                                                                             
;;;   all with AutoComplete.el                                                                                 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;                                         
(defun prefix-list-elements (list prefix)
  (let (value)
    (nreverse
     (dolist (element list value)
      (setq value (cons (format "%s%s" prefix element) value))))))
(defvar ac-source-rope
  '((candidates
     . (lambda ()
         (prefix-list-elements (rope-completions) ac-target))))
  "Source for Rope")
(defun ac-python-find ()
  "Python `ac-find-function'."
  (require 'thingatpt)
  (let ((symbol (car-safe (bounds-of-thing-at-point 'symbol))))
    (if (null symbol)
        (if (string= "." (buffer-substring (- (point) 1) (point)))
            (point)
          nil)
      symbol)))
(defun ac-python-candidate ()
  "Python `ac-candidates-function'"
  (let (candidates)
    (dolist (source ac-sources)
      (if (symbolp source)
          (setq source (symbol-value source)))
      (let* ((ac-limit (or (cdr-safe (assq 'limit source)) ac-limit))
             (requires (cdr-safe (assq 'requires source)))
             cand)
        (if (or (null requires)
                (>= (length ac-target) requires))
            (setq cand
                  (delq nil
                        (mapcar (lambda (candidate)
                                  (propertize candidate 'source source))
                                (funcall (cdr (assq 'candidates source)))))))
        (if (and (> ac-limit 1)
                 (> (length cand) ac-limit))
            (setcdr (nthcdr (1- ac-limit) cand) nil))
        (setq candidates (append candidates cand))))
    (delete-dups candidates)))
(add-hook 'python-mode-hook
          (lambda ()
                 (auto-complete-mode 1)
                 (set (make-local-variable 'ac-sources)
                      (append ac-sources '(ac-source-rope) '(ac-source-yasnippet)))
                 (set (make-local-variable 'ac-find-function) 'ac-python-find)
                 (set (make-local-variable 'ac-candidate-function) 'ac-python-candidate)
                 (set (make-local-variable 'ac-auto-start) nil)))

;;Ryan's python specific tab completion                                                                        
(defun ryan-python-tab ()
  ; Try the following:                                                                                         
  ; 1) Do a yasnippet expansion                                                                                
  ; 2) Do a Rope code completion                                                                               
  ; 3) Do an indent                                                                                            
  (interactive)
  (if (eql (ac-start) 0)
      (indent-for-tab-command)))

(defadvice ac-start (before advice-turn-on-auto-start activate)
  (set (make-local-variable 'ac-auto-start) t))
(defadvice ac-cleanup (after advice-turn-off-auto-start activate)
  (set (make-local-variable 'ac-auto-start) nil))

(define-key python-mode-map "\t" 'ryan-python-tab)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;                                         
;;; End Auto Completion                                                                                        
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Github

These changes are also applied to my Emacs environment on Github. I'm continuously trying to improve my emacs setup, so if you're reading this a few months/years after this post was made, you may want to check there for improvements.

Update Jan 30 2009: I made some modifications to the tab complete order, a regular indent is applied first before autocompletion. Autocompletion is also not applied unless you are at the end of a word. This is useful when you press Tab at the beginning of a line to indent/outdent.

Usage

Once you got everything setup, usage is real easy:

  1. Open up a python (.py) file
  2. Press TAB when you want to use code completion or to insert a snippet.

The first time you attempt to use code completion you'll be prompted to enter the root of your project directory.

Future

It wasn't too long ago when Python code completion inside Emacs was just a pipedream. So now that we have it, let's make some more pipedreams: I'd like to popup some nice contextual help for method arguments as well as python documentation for the current method/class being completed.

The possibilities are pretty much endless. I love Emacs.

Read and Post Comments

My Emacs config on Github

January 19, 2009 at 05:01 PM | categories: python, emacs | View Comments

The most popular post on this blog is Emacs as a powerful Python IDE. I get quite a few emails regarding that post and I realized the other day that I've done a lot of customizations to my .emacs, and subsequently that post is starting to get a little dated. Time is a curious creature, it seems like yesterday that I wrote that post, but it's been almost 9 months now.

I've been learning and playing around with git recently, so I've decided to post my entire .emacs config on github.

Here are the latest instructions for emulating my Emacs environment on Ubuntu 8.10:

  • Enable the universe repository
  • sudo apt-get update
  • sudo apt-get install emacs-snapshot git-core automake libgconf2-dev texinfo python-setuptools
  • cd ~
  • git clone git://github.com/EnigmaCurry/emacs .emacs.d
  • cd ~/.emacs.d
  • git submodule init
  • git submodule update
  • cd ~/.emacs.d/vendor
  • ./vendor-compile.sh
  • sudo easy_install "http://downloads.sourceforge.net/rope/rope-0.9.1.tar.gz?modtime=1225268769&big_mirror=0"
  • sudo easy_install "http://downloads.sourceforge.net/rope/ropemacs-0.6.tar.gz?modtime=1223039342&big_mirror=0"

I've done some reorganization of the directory structure:

  1. There actually is no ~/.emacs file anymore, having realized that Emacs looks for ~/.emacs.d/init.el. Convenient!
  2. All third party packages are now installed in ~/.emacs.d/vendor. This was a great tip I saw in the Emacs Peepcode screencast. (it's $9, but worth it).
  3. I've taken the suggestion that greg made in the comments to use submodules for some of the vendor packages. The vendor-compile.sh script does the compiling of those packages.
  4. All private code goes in ~/.emacs.private. This lets me keep passwords and such outside of the main git repository and allows me to publish my config more easily. Before I did this I had to spend time sanitizing the code before each release.

I have appreciated your many encouraging Emails and comments. :)

Read and Post Comments

Shortened URLs in Emacs using is.gd (like tinyurl)

September 15, 2008 at 11:11 AM | categories: python, emacs | View Comments

I've casually been teaching myself emacs lisp lately. Today I wrote a utility that shortens long urls within regions using the http://is.gd URL shortening service. There's plenty of existing code out there that is more lisp like, but this is supposed to be a learning experience for me so I did it myself. I like python and so I used python for most of the heavy lifting.

I created a directory to hold all of my emacs specific python functions: ~/.emacs.d/ryan-pymacs-extensions

I wrote the following python function, shorten_url.py in that directory:

!/usr/bin/env python
# -*- coding: utf-8 -*-

__author__ = "Ryan McGuire (ryan@enigmacurry.com)"
__date__   = "Mon Sep 15 12:27:14 2008"

import doctest
import urllib2
import re

def shorten_with_is_gd(url):
    """Shorten a URL with is.gd

    >>> shorten_with_is_gd('http://www.enigmacurry.com')
    'http://is.gd/FFP'

    """
    u = urllib2.urlopen("http://is.gd/api.php?longurl="+url)
    return u.read()

def shorten_in_text(text):
    """Shorten all the urls found inside some text

    >>> shorten_in_text('Hi from http://www.enigmacurry.com')
    'Hi from http://is.gd/FFP'
    
    """
    replacements = {} #URL -> is.gd URL
    #Only check for urls that start with "http://" for now
    for m in re.finditer("http://[^ \n\r]*", text):
        try:
            replacements[m.group()] = shorten_with_is_gd(m.group())
        except:
            replacements[m.group()] = m.group()
    for url,replacement in replacements.items():
        text = text.replace(url, replacement)
    return text

if __name__ == '__main__':
    doctest.testmod(verbose=True)

and the following lisp makes "M-x shorten-url" do the rest of the replacement work:

;add ~/.emacs.d/ryan-python-extensions to python path
(pymacs-exec "import sys, os")
(pymacs-exec "sys.path.append(os.path.join(os.path.expanduser('~'),'.emacs.d','ryan-pymacs-extensions'))")

;;Shorten URLs with is.gd
(pymacs-exec "import shorten_url")
(defun shorten-url (start end)
  (interactive "r")
  (let ((region (buffer-substring start end)))
    (let ((rt (pymacs-eval (format "shorten_url.shorten_in_text('''%s''')" region))))
      (kill-region start end)
      (insert rt)
      )
  ))
Read and Post Comments

Importing foreign (non-iCal) calendars into Google Calendar

August 19, 2008 at 03:24 PM | categories: python, free state project | View Comments

Google Calendar is highly useful. I use it to keep track of all my appointments and due dates and I get helpful reminders when dates approach via email and SMS. Not only that, but it allows me to collaborate with other people's calendars as well, and they don't even have to use Google Calendar because Google supports the industry standard iCalendar format. Things are great.

Unfortunately, iCalendar format is pretty new, and not everyone is using it.

Because Google Calendar is so useful, it is annoying when you find a calendar that is not in iCal format. Two of note that I want to follow are

Both of these calendars are running Simple Machines Forum, version 1.x which does not support iCalendar format (presumably they will in 2.0).

So I wrote an exporter: Download SMF iCal exporter

The exporter scrapes the calendar page on an SMF enabled site and dumps out an iCal compatible file.

Usage: smf_ical_converter.py -u http://yourforum.com/index.php -o cal.ics

Download a SMF 1.x forum calendar and dump in iCal format

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -u URL, --url=URL     URL of forum (up to and including /index.php)
  -n Name, --name=Name  Name of the Forum / Calendar (name goes in .ics file)
  -i EXPR, --ignore-re=EXPR
                        Ignore any event containing this regular expression
                        (specify as many -i as you want)
  -o File, --output=File
                        iCal filename to write
  -v, --verbose         Be verbose about process
  --months-backward=NUM
                        Number of months to go backward
  --months-forward=NUM  Number of months to go forward

I have this tool running in a cron job to keep up to date with the two above mentioned calendars. You can import these URLs directly into your Google Calendar:

Update 09/16/08: I uploaded version 2 of this application. It has a better README and it now supports user specific date ranges.

Read and Post Comments

Emacs IRC (ERC) with Noticeable Notifications

August 07, 2008 at 12:35 AM | categories: python, lisp, emacs, linux | View Comments

I use ERC for all my IRC chatting. I finally got fed up with not noticing someones message because I didn't have emacs focused. So I spent my evening concocting a more noticeable messaging system through Pymacs and libnotify.

Half way though implementing this, wouldn't you know it, I found ErcPageMe which does exactly what I wanted. I figured I was learning quite a bit and I continued writing my own version. I expanded on their code and (at least for me) made some improvements. So kudos go to whoever wrote ErcPageMe :)

The following code will pop up a message on your gnome desktop alerting you whenever you receive a personal message or when someone mentions your nickname in a channel. It also avoids notification for the same user in the same channel if they triggered a message within the last 30 seconds.

Emacs ERC Notification through libnotify

Here is my lisp and embedded python/pymacs code:

(defun notify-desktop (title message &optional duration &optional icon)
  "Pop up a message on the desktop with an optional duration (forever otherwise)"
  (pymacs-exec "import pynotify")
  (pymacs-exec "pynotify.init('Emacs')")
  (if icon 
      (pymacs-exec (format "msg = pynotify.Notification('%s','%s','%s')"
                           title message icon))
    (pymacs-exec (format "msg = pynotify.Notification('%s','%s')" title message))
    ) 
  (if duration 
      (pymacs-exec (format "msg.set_timeout(%s)" duration))
    )
  (pymacs-exec "msg.show()")
  )

;; Notify me when someone wants to talk to me.
;; Heavily based off of ErcPageMe on emacswiki.org, with some improvements.
;; I wanted to learn and I used my own notification system with pymacs
;; Delay is on a per user, per channel basis now.
(defvar erc-page-nick-alist nil
  "Alist of 'nickname|target' and last time they triggered a notification"
  )
(defun erc-notify-allowed (nick target &optional delay)
  "Return true if a certain nick has waited long enough to notify"
  (unless delay (setq delay 30))
  (let ((cur-time (time-to-seconds (current-time)))
        (cur-assoc (assoc (format "%s|%s" nick target) erc-page-nick-alist))
        (last-time))
    (if cur-assoc
        (progn
          (setq last-time (cdr cur-assoc))
          (setcdr cur-assoc cur-time)
          (> (abs (- cur-time last-time)) delay))
      (push (cons (format "%s|%s" nick target) cur-time) erc-page-nick-alist)
      t)
    )
  )
(defun erc-notify-PRIVMSG (proc parsed)
  (let ((nick (car (erc-parse-user (erc-response.sender parsed))))
	(target (car (erc-response.command-args parsed)))
	(msg (erc-response.contents parsed)))
    ;;Handle true private/direct messages (non channel)
    (when (and (not (erc-is-message-ctcp-and-not-action-p msg))
               (erc-current-nick-p target)
	       (erc-notify-allowed nick target)
	       )
      ;Do actual notification
      (ding)
      (notify-desktop (format "%s - %s" nick
                              (format-time-string "%b %d %I:%M %p"))
                      msg 0 "gnome-emacs")
      )
    ;;Handle channel messages when my nick is mentioned
    (when (and (not (erc-is-message-ctcp-and-not-action-p msg))
               (string-match (erc-current-nick) msg)
               (erc-notify-allowed nick target)
	       )
      ;Do actual notification
      (ding)
      (notify-desktop (format "%s - %s" target
                              (format-time-string "%b %d %I:%M %p"))
                      (format "%s: %s" nick msg) 0 "gnome-emacs")
      )
    )
      
  )

(add-hook 'erc-server-PRIVMSG-functions 'erc-notify-PRIVMSG)
Read and Post Comments

« Previous Page -- Next Page »