API#
This is the full API reference for all public classes and functions.
Decorators#
- otf.function(f=None, /, *, strict=True)#
Wraps the decorated function in its own portable environment.
The function be will be compiled in its own module and will only have access to globals passed in via the
environment()
decorator:@function @environment(i=0) def counter() -> int: "A counter that increments by 1 every time it's called" global i i+=1 return i
By default the decorator will vet the function for issues like using globals that aren’t declared in its environment:
>>> @function ... def counter() -> int: ... global i ... i+=1 ... return i Traceback (most recent call last): ... SyntaxError: variable 'i' not found in the environment
This can be turned off with the
strict
argument:@function(strict=False) def counter() -> int: ...
- Parameters
strict – do not check the wrapped function.
- Return type
- otf.environment(**kwargs)#
Attach an environment to the function.
All keyword arguments will be declared as variables in the function’s globals.
Runtime Classes#
- class otf.Closure(environment, target)#
A callable otf function
- class otf.Environment(**kwargs)#
An otf environment contains functions and values.
- function(fn=None, /, *, lazy=False)#
A decorator to add a function to this environment.
The decorator can be called directly on a function:
@env.function def f(x: int) -> int: ...
Or with arguments:
@env.function(lazy=True) def f(x: int) -> int: ...
- workflow(fn)#
Workflow decorator
Turn a function written with
async
/await
into a workflow.
- class otf.Suspension(code, *, position, environment, variables, awaiting=None)#
Represents a checkpoint in the execution of a workflow
A suspension captures the code of a workflow, a position in that code and all the local variables. It’s a continuation that can be reified.
- property lineno#
The line we are currently awaiting on (set to None if we haven’t entered the body of the function yet).
- class otf.Workflow(environment, origin)#
- freeze(*args, **kwargs)#
Create a suspension of this workflow on the first line
All the runtime classes support pickle
but we also have our own
serialization format.
Serialisation#
The otf serialisation format is designed to be read by humans. It is a subset of python’s syntax:
>>> otf.dump_text([1, 2, 3, 4, None])
'[1, 2, 3, 4, None]'
>>> otf.load_text("""
... # You can have comments in the values you load
... {
... tuple([2, 1]): 5,
... tuple([3, 1]): 13,
... tuple([4, 1]): 65533,
... }
... """)
{(2, 1): 5, (3, 1): 13, (4, 1): 65533}
The library is extensible and can handle arbitrary python values.
Shared references:
>>> v = []
>>> otf.dump_text([v, v])
'[[], ref(1)]'
Non finite floats:
>>> otf.dump_text([1., 2., -math.inf])
'[1.0, 2.0, -inf]'
Adding support for new types:
>>> import fractions
...
>>> @otf.register
... def _(fraction: fractions.Fraction):
... return fractions.Fraction, str(fraction)
...
>>> otf.dump_text(fractions.Fraction(227, 73))
"fractions.Fraction('227/73')"
Serialisation functions and classes#
- otf.dump_text(obj, indent=None, width=60, format=None)
Serialise obj
Dumps supports several formats. Let’s take a sample value with a shared reference:
>>> v = {'nan': math.nan, '1_5':[1,2,3,4,5]} >>> v2 = [v, v]
COMPACT
means it will all be printed on one line.>>> print(dump_text(v2, format = COMPACT)) [{'nan': nan, '1_5': [1, 2, 3, 4, 5]}, ref(10)]
PRETTY
will use the width and indent argument to pretty print the output.>>> print(dump_text(v2, format = PRETTY, width=20)) [ { 'nan': nan, '1_5': [ 1, 2, 3, 4, 5 ] }, ref(10) ]
EXECUTABLE
will print code that can run in a python environment where the last statement is the value we’re building:>>> print(dump_text(v2, format = EXECUTABLE, width=40)) _0 = { 'nan': float("nan"), '1_5': [1, 2, 3, 4, 5] } [_0, _0]
- Parameters
obj – The value to serialise
indent (int | None) – indentation (for the
PRETTY
andEXECUTABLE
formats)width (int) – Maximum line length (for the
PRETTY
andEXECUTABLE
formats).format – One of
None
,COMPACT
,PRETTY
,EXECUTABLE
. If the value isNone
then the format will beCOMPACT
if indent wasn’t specified andPRETTY
otherwise.
- otf.load_text(s)
Load a value encoded as a string
- Parameters
s (str) –
- otf.register(function=None, /, *, type=None, pickle=False)
Register a function to use while packing object of a given type.
function is expected to take objects of type T and to return a tuple describing how to recreate the object: a function and serialisable value.
If type is not specified,
register()
uses the type annotation on the first argument to deduce which type register function for.If
register()
is used as a simple decorator (with no arguments) it acts as though the default values for all of it parameters.Here are three equivalent ways to add support for the
complex
type:>>> @register ... def _reduce_complex(c: complex): ... return complex, (c.real, c.imag) >>> @register() ... def _reduce_complex(c: complex): ... return complex, (c.real, c.imag) >>> @register(type=complex) ... def _reduce_complex(c: complex): ... return complex, (c.real, c.imag)
- Parameters
function – The reduction we are registering
type – The type we are registering the function for
pickle – If set to
True
, function is registered viacopyreg.pickle()
to be used inpickle
.
- class otf.NamedReference(obj)#
Serialisable wrapper around a module, class or function.
NamedReference
are serialised by copying the name to the wrapped object:>>> import math >>> fl = NamedReference(math.floor) >>> dump_text(fl) "otf.NamedReference('math.floor')"
If the wrapped object is callable then the wrapped will pass calls transparently to the object:
>>> fl(12.6) 12
Attribute accesses are also transparently passed through to the wrapped value:
>>> m = NamedReference(math) >>> m.ceil(5.6) 6
This means that, in most cases, the
NamedReference
wrapper is transparent. Sometimes you actually need to access the wrapped value:>>> WrappedComplex = NamedReference(complex) >>> k = WrappedComplex(5) >>> isinstance(k, WrappedComplex) Traceback (most recent call last): ... TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union >>>
You can use the
~
operator to access the wrapped value:>>> ~WrappedComplex <class 'complex'> >>> isinstance(k, ~WrappedComplex) True
- Parameters
v (class|module|function) – The object that will be wrapped.
- Raises
ValueError – if obj cannot be reloaded via its name (e.g.: if obj is defined inside a function).
TypeError – if obj is not a module, class or function or if obj is a lambda.