Operations

The most common thing to do in a plugin is to add a new operation. In the walkthrough presented on this page, we’ll implement a new operation on the main menu. Our operation will calculate a proportion, like:

\[\frac{1}{2} = \frac{3}{x}\]

We’ll pass the parameters in order, so when the stack reads [1, 2, 3], we will obtain 6.

Creating a plugin

Create a new file in your esc plugins directory and paste in the following template:

"""
{name}.py - {description}
Copyright (c) 2019 {your name}.

{additional details}
"""

from esc.commands import Operation, main_menu

Change the sections in brackets to appropriate values for your plugins. Of course, the docstring is just a suggestion. Reload esc and ensure no errors occur. If you do get an exception, you probably made a syntax error; fix the file as necessary to resolve it.

Writing a function

Operations are written as Python functions decorated with @Operation. We’ll start with the function and then look at the decorator.

How should we write this function? Let’s generalize the example of a proportion calculation above, where d is the number we’re solving for:

\[\begin{split}\begin{align} \frac{a}{b} &= \frac{c}{d}\\ bc &= ad\\ d &= \frac{bc}{a}\\ \end{align}\end{split}\]

We’ll go ahead and use these letters for our variable names; they’ll serve as well as anything else since this is a very general operation that could be used for just about anything. Translating the algebraic notation above into Python:

def proportion(a, b, c):
    return b * c / a

We can specify any number of parameters we want here and name them anything we want. When the user runs our operation, esc will check the function’s parameter list to see how many parameters it has, slice that many items off the bottom of the stack, and bind them to the parameters in order. If there aren’t enough items available, the user will get an error message telling them so. When we’re done, we can return a single value or a tuple of values, and those values will replace the parameters that we received on the stack.

This is an oversimplification, as there are additional options that can change much of this behavior; we’ll get to those in the discussion of the @Operation decorator.

Note

esc uses the Decimal library to implement decimal arithmetic similar to that of many handheld calculators. All function arguments are thus Decimal objects. Most operations on Decimals yield other Decimals, so you probably will not even notice if you’re doing normal arithmetic on your arguments. If you ever get confused, check out the linked library documentation.

Return values from functions may be Decimal objects or any type that can be converted to a Decimal (string, integer, or float). Beware of returning floats except for numbers that are already irrational, as all the precision will be kept when converting back to the internal Decimal representation, even the rounding error inherent in binary floating-point values, which may result in silly values like 1.000000000083 appearing on the stack. If your function uses non-integer literals anywhere, it’s a good idea to head this issue off by using the Decimal constructor to create them, like from decimal import Decimal; x = Decimal("2.54").

Creating an @Operation

If you save the file and start esc, you won’t get any errors, but you won’t have any new operations either. In order to get an operation to show up, we need to add the @Operation decorator described earlier. That will make our code look like this:

@Operation(key='P', menu=main_menu, push=1,
           description="proportion from abc",
           log_as="{0}:{1} :: {2}:[{3}]")
def proportion(a, b, c):
    """
    Quickly calculate a proportion.
    If the bottom three items on the stack are A, B, and C,
    then calculate D where A : B = C : D.
    """
    return b * c / a

Most of this is probably fairly self-explanatory, but a couple of points are worth noting.

  • log_as is a format string whose positional placeholders will be replaced with a chain of the arguments to the function and the return values from the function. The formatted version will be used in the history window and help system.
  • The function’s docstring is used as the description in the help system.

@Operation can get more complicated, so without further ado here are the dirty details:

esc.commands.Operation(key, menu, push, description=None, retain=False, log_as=None, simulate=True)[source]

Decorator to register a function on a menu and make it available for use as an esc operation.

Parameters:
  • key – The key on the keyboard to press to trigger this operation on the menu.
  • menu – The Menu to place this operation on. The simplest choice is main_menu, which you can import from :mod:esc.commands.
  • push – The number of items the decorated function will return to the stack on success. 0 means nothing is ever returned; -1 means a variable number of things are returned.
  • description – A very brief description of the operation this function implements, to be displayed next to it on the menu. If this is None (the default), the operation is “anonymous” and will be displayed at the top of the menu with just its key.
  • retain – If True, the items bound to this function’s arguments will remain on the stack on successful execution. The default is False (meaning the function’s return value replaces whatever was there before – the usual behavior of an RPN calculator).
  • log_as

    A specification describing what appears in the History window after executing this function. It may be None (the default), UNOP or BINOP, a .format() string, or a callable.

    • If it is None, the description is used.
    • If it is the module constant esc.commands.UNOP or esc.commands.BINOP, the log string is a default suitable for many unary or binary operations: for UNOP it is description argument = return and for BINOP it is argument key argument = return.

      Note

      If the function being decorated does not take one or two arguments, respectively, using UNOP or BINOP will raise a ProgrammingError.

    • If it is a format string, positional placeholders are replaced with the parameters to the function in sequence, then the return values. Thus, a function with two arguments bos and sos returning a tuple of two values replaces {0} with bos, {1} with sos, and {2} and {3} with the two return values.
    • If it is a callable, the parameters will be examined and bound by name to the following (none of these parameters are required, but arguments other than these will raise a ProgrammingError).
      args:a list of the arguments the function requested
      retval:a list of values the function returned
      registry:the current Registry instance

      The function should return an appropriate string.

  • simulate – If True (the default), function execution will be simulated when the user looks at the help page for the function, so they can see what would happen to the stack if they actually chose the function. You should disable this option if your function is extremely slow or has side effects (e.g., changing the system clipboard, editing registers).

In addition to placing the function on the menu, the function is wrapped with the following magic.

  1. Function parameters are bound according to the following rules:

    • Most parameters are bound to a slice of values at the bottom of the stack, by position. If the function has one parameter, it receives bos; if the function has two parameters, the first receives sos and the second bos; and so on. The parameters can have any names (see exceptions below). Using bos and sos is conventional for general operations, but if the operation is implementing some kind of formula, it may be more useful to name the parameters for their meaning in the formula.
    • By default, passed parameters are of type Decimal. If the parameter name ends with _str, it instead receives a string representation (this is exactly what shows up in the calculator window, so it’s helpful when doing something display-oriented like copying to the clipboard). If the parameter name ends with _stackitem, it receives the complete StackItem, containing both of those representations and a few other things besides.
    • A varargs parameter, like *args, receives the entire contents of the stack as a tuple. This is invalid with any other parameters except registry. The _str and _stackitem suffixes still work. Again, it can have any name; *stack is conventional for esc operations.
    • The special parameter name registry receives a Registry instance containing the current state of all registers. Using this parameter is generally discouraged; see Registry for details.
    • The special parameter name testing receives a boolean describing whether the current execution is a test (see esc.functest.TestCase()). This can be useful if your function has side effects that you don’t want to execute during testing, but you’d still like to test the rest of the function.
  2. The function has a callable attached to it as an attribute, called ensure, which can be used to test the function at startup to ensure the function never stops calculating the correct answers due to updates or other issues:

    def add(sos, bos):
        return sos + bos
    add.ensure(before=[1, 2, 3], after=[1, 5])
    

    See TestCase for further information on this testing feature.

Writing tests

You probably don’t want a calculator that returns the wrong results, so it’s important to test your custom function! You could simply load esc and try it out, and that’s a good idea regardless, but esc also offers built-in tests. These tests run automatically every time esc starts up; if they ever fail, esc will raise a ProgrammingError and refuse to load. This way, even if a new version of esc makes breaking changes you don’t know about or you accidentally modify and break your function, you can be confident that esc won’t return incorrect results (at least to the extent of your test coverage).

We can define automatic tests using the ensure attribute which the @Operation decorator adds to our function. Let’s define a test that tests the example we discussed at the start of this page:

proportion.ensure(before=[1, 2, 3], after=[6])

Let’s test an error condition too. What happens if calculating our proportion requires a divide by zero? Without special-casing that in our function, we would hope it informs the user that she can’t divide by zero, which esc does by raising a ZeroDivisionError which is caught by the interface.

proportion.ensure(before=[0, 2, 3], raises=ZeroDivisionError)

And it’s that easy. If you don’t get a ProgrammingError after restarting esc, your tests pass.

Here’s the full scoop on defining tests:

class esc.functest.TestCase(before, after=None, raises=None, close=False)[source]

Test case defined with the .ensure() attribute of functions decorated with @Operation.

Parameters:
  • before – Required. A list of Decimals or values that can be coerced to Decimals. These values will be pushed onto a test stack that the operation will consume values from.
  • after – Optional (either this or raises is required). A list of Decimals or values that can be coerced to Decimals. After the function is executed and changes the stack, the stack is compared to this list, and the test passes if they are identical.
  • raises – Optional (either this or after is required). An exception type you expect the function to raise. This checks both the top-level exception type and any nested exceptions (since esc wraps many types of exceptions in FunctionExecutionErrors). The test passes if executing the function (including all the machinery inside esc surrounding your actual function’s execution, like pulling values off the stack) raises an exception of this type.
  • close – If True, instead of doing an exact Decimal comparison, math.isclose() is used to perform a floating-point comparison. This is useful if dealing with irrational numbers or other sources of rounding error which may make it difficult to define the exact result in your test.

Test cases are executed every time esc starts (this is not a performance issue in practice unless you have a lot of plugins). If a test ever fails, a ProgrammingError is raised. Preventing the whole program from starting may sound extreme, but wrong calculations are pretty bad news!

Test cases are not inherently associated with an operation due to scope issues: Since the EscOperation itself is not returned to the functions file, only a decorated function, the function author can’t access the EscOperation. Instead, they are associated with the function itself, and when the test() method is called on an EscOperation, it retrieves the test cases from the function and passes itself into the execute() method of each test case.

Putting it all together

Launch esc again. If you’ve made any mistakes, esc will hopefully catch them for you here and describe why you have an error or your test failed. Type in three values that can be used to calculate a proportion, choose the function from the menu, and you should be set!

Handling errors

esc handles many kinds of errors that could occur in your functions for you:

  • If there aren’t enough items on the stack to bind to all your arguments, your function won’t even be called and the user will be told there aren’t enough items on the stack.
  • If your function raises a ValueError, the user will be informed a domain error has occurred (many math functions raise this exception in this case).
  • If your function raises a ZeroDivisionError, the user will be informed he cannot divide by zero.
  • If your function raises a Decimal InvalidOperation, the user will be informed the result is undefined. (The Decimal library in esc is configured so that Infinity is a valid result which occurs when extremely large numbers are put together such that the available precision is exceeded, but any result that would return NaN like 0 / 0 raises InvalidOperation.)

However, at times this will not be sufficient. One of the most common cases occurs when you need to work with the entire stack. In this case, you need to check yourself to see if there are sufficient items, as esc doesn’t know how many you need. To do so, simply check the length of your *args tuple and raise an InsufficientItemsError if it’s too short:

def my_operation(*stack):
    if len(stack) < 2:
        raise InsufficientItemsError(number_required=2)
    # do stuff
class esc.oops.InsufficientItemsError(number_required, msg=None)[source]

Raised directly by operations functions that request the entire stack to indicate not enough items are on the stack to finish their work, or by the menu logic when an operation requests more parameters than items on the stack.

Functions may use the simplified form of the exception, providing an int describing the number of items that should have been on the stack for the number_required constructor parameter. esc will then reraise the exception with a more useful message; a fallback message is provided in case this doesn’t happen for some reason.

Another case arises when your function encounters some arbitrary condition that prevents it from continuing. As a silly example, perhaps the result of your formula is undefined if the sum of its input values is 6. Raising a FunctionExecutionError with a message argument will cause the function’s execution to stop and the message to be printed to the status bar. (The stack will remain unchanged.) As noted below, the message should be concise so it fits in the status bar – it will be truncated if it doesn’t fit.

def my_operation(sos, bos):
    if sos + bos == 6:
        raise FunctionExecutionError("I don't like the number 6.")
    # do stuff
class esc.oops.FunctionExecutionError[source]

A broad exception type that occurs when the code within an operation function couldn’t be executed successfully for some reason. Examples include:

  • a number is in the middle of being entered and isn’t a valid number
  • a function performed an undefined operation like dividing by zero
  • there are too many or too few items on the stack
  • a function directly raises this error due to invalid input or inability to complete its task for some other reason

FunctionExecutionErrors normally result in the __str__ of the exception being printed to the status bar, so exception messages should be concise.

A FunctionExecutionError may be raised directly, or one of its subclasses may be used.

Warning

If you do something complicated in your function that could result in an exception other than the types listed above, be aware that if you let an exception of another type bubble up from your function, esc will crash and show the traceback. This is generally reasonable behavior if you don’t expect the error, since it makes it easy to spot and fix the problem, but if the error is an expected possibility you’ll probably want to catch it and give the user a helpful error message describing the problem by raising a FunctionExecutionError.