Python plugin

Strictly for the sake of this plugin I started Snaked’s development. It requires rope for it’s work.

Pretty editor title formating

Package modules are presented like package.module or package instead module.py or __init__.py. Very useful extension to distinguish similar file names. Must have for every Pytonier.

Goto Definition

Default F3 shortcut navigates to symbol definition under cursor. It also handles symbols in quotes and comments. Like:

def func(param):
   """Some function

   :param param: Some param of some_package.some_module.SomeClass
   """
   pass

To see param class you need to place cursor on SomeClass and hit F3. Or if rope can infer param type you can place cursor there and hit F3.

Code completion

Snaked uses gtksourceview2’s completion framework. I’ve only implement python provider. All UI work are done by gtksourceview.

_images/complete.png

<ctrl>space activates popup. Also there is support for showing pydocs in detailed proposal information.

Outline navigation

This dialog provides easy jumping into needed module block.

_images/outline1.png _images/outline2.png

Type hints

This is the most exiting Snaked’s part. It allows to provide additional type information to rope for better type inferring and as consequence better completion and code navigation.

What hints can be provided:

  • Function/method param types.
  • Function/method return type.
  • Replacing module attribute.
  • Adding attributes to class.

Usage

There is special file to configure hints: .ropeproject/ropehints.py in your project root. It is ordinary python file which must define function init(provider), where provider is default project hint provider with build-in scope matcher and doc string hint support.

Take note, without configured hints you have doc string hint provider anyway.

Snaked’s hint providers

class snaked.plugins.python.ropehints.CompositeHintProvider(project)

Default snaked’s hint provider

It is created automatically for each rope project and passed to .ropeproject/ropehints.py init function as first parameter.

Contains build-in ScopeHintProvider with it’s scope matcher accessed via self.db and DocStringHintProvider.

Also provides hints for re module. Custom providers can be added via add_hint_provider():

def init(provider):
    provider.db.add_class_attribute('django\.http\.HttpRequest$', 'render$', 'app.render')

    from snaked.plugins.python.djangohints import DjangoHintProvider
    provider.add_hint_provider(DjangoHintProvider(provider, 'settings'))
add_hint_provider(provider)

Inserts provider into collection.

Last added provider has max priority.

class snaked.plugins.python.ropehints.ScopeHintProvider(project, scope_matcher)

Working horse of type hinting

Common usage is in conjunction with ReScopeMatcher (also see examples there)

__init__(project, scope_matcher)
Parameters:
  • project – rope project
  • scope_matcher – one of ScopeMatcher implementations.
class snaked.plugins.python.dochints.DocStringHintProvider(project)

Allows to hint functions/method parameters and return values types through doc strings

This is build-in provider so you have no do any addition steps to enable it.

Hints can be provided using Sphinx syntax:

def func(text):
    '''
    :type text: str
    :rtype: str
    '''
    # now in func body 'text' parameter's type is resolved into 'str'

# And all rest code now knows ``func`` return type is also 'str'

Snaked’s scope matchers

class snaked.plugins.python.ropehints.ReScopeMatcher

ScopeHintProvider matcher based on regular expressions

Matching is done with re.match function (pay attention to differences with re.search)

See add_attribute(), add_param_hint().

add_attribute(scope, name, object_type)

Add attribute type hint for module or class/object

Can be used to provide module attributes types, for example in case or complex module loading (werkzeug) or complex runtime behavior (flask). Here is example .ropeproject/ropehints.py:

def init(provider):
    provider.db.add_attribute('flask$', 'request', 'flask.wrappers.Request()')
    provider.db.add_attribute('werkzeug$', 'Request', 'werkzeug.wrappers.Request')

What happens here? flask.request is magic proxy to isolate thread contexts from each other. In runtime it is flask.wrappers.Request object (in default flask setup), so first line adds this missing information. But this is not enough. flask.wrappers.Request is a child of werkzeug.Request which can not be resolved because of werkzeug’s module loading system. So there is second line adding necessary mapping: module attribute werkzeug.Request should be werkzeug.wrappers.Request indeed. Take note about parentheses, flask.request is instance so type declared with them as opposite to werkzeug.Request which is class.

Also one can add class attributes hint. Here is example from my django project:

provider.db.add_attribute('django\.http\.HttpRequest$', 'cur', 'app.Cur')
provider.db.add_attribute('django\.http\.HttpRequest$', 'render', 'app.render')

Here are some explanations. Every request object has cur attribute (I know about contexts, don’t ask me why I need it) which is instance of app.Cur, so first line injects such info. Second line resolves render function also bounded to request.

add_param_hint(scope, name, object_type)

Add function/method parameter or return value type hint.

Very useful in case of mass type hinting. For example part of snaked’s ropehints.py:

def init(provider):
    provider.db.add_param_hint('.*', 'editor$', 'snaked.core.editor.Editor()')
    provider.db.add_param_hint('snaked\.plugins\..*?\.init$', 'manager$',
        'snaked.core.plugins.ShortcutsHolder()')

Snaked consist of many small functions passing around current text buffer (named editor) as parameter and first line allows to provide such type hint. Second line resolves all plugins init function’s manager parameter.

Or take look at Django’s view function’s request resolving:

provider.db.add_param_hint('.*\.views\..*', 'request$', 'django.http.HttpRequest()')

If name is return type hint is provided for function’s return value type. Following example shows it:

provider.db.add_param_hint('re\.compile$', 'return$', 're.RegexObject()')
provider.db.add_param_hint('re\.search$', 'return$', 're.MatchObject()')
provider.db.add_param_hint('re\.match$', 'return$', 're.MatchObject()')
provider.db.add_attribute('re$', 'RegexObject', 'snaked.plugins.python.stub.RegexObject')
provider.db.add_attribute('re$', 'MatchObject', 'snaked.plugins.python.stub.MatchObject')

Take notice, re.compile, re.search, re.match return absent classes which are mapped with add_attribute_hint() to existing stubs later.

Django hints

Look at image:

_images/django-hints.png

Cool, isn’t it? Simply add django support into your .ropeproject/ropehints.py:

def init(provider):
    from snaked.plugins.python.djangohints import add_django_support
    add_django_support(provider)

Note

Django hints were developed against django 0.97 (yeah, I maintain such old project) codebase and not tested on current versions. Get me know if you will have any issues.

PyGtk hints

Image again:

_images/pygtk-hints.png

Who is there? BuilderAware is a simple wrapper which delegates missing attributes to GtkBuilder. Window is BuilderAware class constructed from glade file. vbox1 is a GtkVBox defined in glade file and PyGtk hint provider resolves class attributes from it.

Besides that, goto definition (F3) opens glade file and place cursor at vbox1 declaration.

And more, if there are any signal handlers in glade file they parameters also will be resolved.

You only need to add pygtk support and assign glade file to class via ropehints.py:

def init(provider):
    from snaked.plugins.python.pygtkhints import add_gtk_support
    add_gtk_support(provider)

And define glade file in class pydoc:

class Window(BuilderAware):
   """glade-file: main.glade"""
   ...

Unit testing

This is a holy grail of modern development. Honestly, I didn’t plan to integrate unit testing support in snaked – running py.test from terminal completely satisfied my needs, but during heavy tests reorganization I realized too much time was spent for snaked/terminal switching and searching fail’s cause in py.test output.

Plugin completely based on py.test capabilities and you need latest (2.0) its version. Unit testing features:

  • Test framework agnostic. Really killer feature – one shoot and three bunnies are dead: py.test itself, unittest and nose.
  • Test environment configuration are done by ordinary conftest.py and pytest.ini.
  • Common GUI for testing process which can be founded in other IDEs.
  • Tests output is not messed and can be seen for each test individually (thanks for py.test).
  • Quick jump to test fail cause. Also one can navigate through traceback. Without any mouse, fast ant easy.
_images/unittest.png

Shortcuts

  • <ctrl><shift>F10 runs all tests in project.
  • <ctrl>F10 runs tests defined in scope under cursor. It can be test function/method, test case class or whole module.
  • <shift><alt>x reruns last tests.
  • <alt>1 toggles test result window.
  • Enter in test list view jums to failed test line.
  • <alt>u/<alt>n mnemonics navigate through traceback.

Running django tests

At first you need to configure django environment. Create conftest.py module in the root of you project:

import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'

Then you should tell py.test which modules are tests proper. Create pytest.ini file in project root:

[pytest]
python_files = tests.py

That’s all. Now you can run django tests with py.test and snaked.