API Extensions

One of VisiData’s core design goals is extensibility. Many of its features can exist in isolation, and can be enabled or disabled independently, without affecting other features.

So, VisiData encourages plugin authors to provide new features in a similarly modular form. These features can be enabled by importing the module, or left disabled by not importing it. Modules should degrade or fail gracefully if they depend on another module which is not available.

The Extensible base class

Python classes usually implement all methods and members in the class definition, which exists in a single source file. A modular VisiData feature, by contrast, is self-contained within a separate source file, while providing methods on previously-defined classes.

Core functionality can be changed through ‘monkey-patching’, which is the ability for modules loaded after startup to add or change methods on existing classes.

To make this a bit easier, the core classes in VisiData inherit from the Extensible class, which provides some helper functions and decorators to make monkey-patching easier and more consistent. All of their subclasses are then also naturally Extensible.

Extensible API

class visidata.Extensible
visidata.Extensible.api(func)

This decorator defines a member function on a specific class.

Because this is a member function, the first parameter is the instance itself. If this function were defined in the class, the first parameter would be named self by Python convention. When members are defined in other files, it is better to use a specific local object name instead of self. Use sheet for any Sheet type, col for any Column type, and vd for VisiData (which will shadow the global vd object, but as it is a singleton, they will be identical).

@VisiData.api
def vd_func(vd, ...):
    pass

@Sheet.api
def sheet_func(sheet, ...):
    pass

@Column.api
def col_func(col, ...):
    pass

Extensible.api can be used either to add new member functions, or to override existing members. To call the original function, use func.__wrapped__:

@Sheet.api
def addRow(sheet, row):
    # do something first
    addRow.__wrapped__(sheet, row)

....

sheet.addRow(row)
visidata.Extensible.class_api(func)

@class_api works much like @api, but for class methods:

@Sheet.class_api
@classmethod
def addCommand(cls, ...):

This method is used internally but may not be all that useful for plugin and module authors. Note that @classmethod must still be provided, and the order of multiple decorators is crucial: @<class>.api must come before @classmethod.

visidata.Extensible.property(func)

This acts just like the @property decorator, if it were defined inline to the class.

visidata.Extensible.lazy_property(func)

Return func() on first access and cache result; return cached result thereafter.

This works like @property, except it only computes the value on first access, and then caches it for every subsequent usage.

Note

Because of how Python instantiates classes, extensions monkey-patched into a class are not also added to already-instantiated objects. So global sheets defined by a plugin should be added to the VisiData object as a @VisiData.lazy_property. This way, they are not created until their first use, which allows them to take advantage of Sheet extensions that were loaded after the plugin.

visidata.Extensible.init(membername, initfunc=<function Extensible.<lambda>>, copy=False)

Append equivalent of self.<membername> = initfunc() to <cls>.__init__.

If a module wants to store some data on an Extensible class, it can add a member with a call to that class’ init():

TableSheet.init('foo', dict)

This monkey-patches TableSheet.__init__ to add the instance member foo to every TableSheet on construction, and to initialize it with an empty dict. To provide an initial non-object value:

TableSheet.init('bar', lambda: 42)

Note

This will not work with the vd because it is instantiated very early. Instead, assign member variables directly on vd in the toplevel scope:

vd.bar = []

This member can then be used like any other member of the class.

By default, when an instance of the class is copied, a member specified with this init() is reset to a newly constructed value (by calling the constructor again). If copy is True, then a copy is made of the member for the new instance.

visidata.vd.global_api(func)

Make global func() and identical vd.func()

All features (and plugins) should expose functions and classes to plugins in one of these ways:

  1. with @VisiData.api (or @Sheet.api): for most methods

These can be used in an execstr as though they were global (attributes on vd and sheet are both implicitly in scope in an execstr).

Outside of an execstr, use vd.funcname(…) (or sheet.funcname(…)).

Note that classes can be annotated with @VisiData.api also.

  1. with addGlobals({“funcname”: funcname}): for classes and methods internal to VisiData

These can be used via direct import:

from visidata import SomeInternalClass from plugins.myplugin import HelperClass

This is acceptable for commonly-used classes.

See getGlobals() and addGlobals().

What to extend: Sheet, Column, VisiData, or globals?

Look at what the function uses. If it uses a specific column, use @Column.api with col as the first “self” argument, and if you need access to the sheet, use col.sheet.

If the function naturally takes a sheet, use @Sheet.api with first argument of sheet.

Otherwise, extend the VisiData object if neither Column nor Sheet are relevant. vd is always available as a global regardless.

Classes and functions which don’t use vd or sheet at all are candidates for the list of bare globals in __all__.

The vd singleton

The VisiData class is a singleton object, containing all of VisiData’s global functions and state for the current session. This object should always be available as vd.

Calling conventions

When calling functions on vd or sheet outside of a Command execstr, they should be properly qualified:

@Sheet.api
def show_hello(sheet):
    vd.status(sheet.options.disp_hello)

BaseSheet.addCommand(None, 'show-hello', 'show_hello()')

The current Sheet and the VisiData object are both in scope for execstrs, so within an execstr, the sheet. or vd. may be omitted, as in the hello world example:

BaseSheet.addCommand(None, 'show-hello', 'status(options.disp_hello)')