-
Notifications
You must be signed in to change notification settings - Fork 0
extending
It's easy (and generally necessary) to extend Prismscript to get the functionality required by your application.
Interpreter objects have a register_scoped_functions
method that allows you to pass in a sequence of (name, function) tuples that map a scoped identifier (pretty much anything with a dot in it) to an arbitrary function. This function will then be directly callable from within Prismscript. (The method can be invoked multiple times; definitions will be merged just like dictionary key-value pairs)
You may define optional arguments and add a **kwargs
parameter to serve as a buffer if your API is likely to evolve and serve scripts written at different points in time. Other than that, the conditions by which you must abide are pretty minimal.
If you want to introduce a new data-type, all you have to do is make sure your function (which can actually be a class definition, since those are callable, too) returns an object that has members that abide by Prismscript's requirements. Do that and it will just work.
If you've got instantiated objects or modules that you would like to expose to Prismscript as a namespace of functions, the autodiscover
module's scan()
function makes setup trivial.
A good example of using it to digest a module is the standard library:
import prismscript.auto_discover
import prismscript.stdlib
standard_library = prismscript.auto_discover.scan(prismscript.stdlib, '')
This short example recursively crawls through the Prismscript standard library and adds a reference to every non-underscore-prefixed function it encounters.
If the library were to be mounted under, say, prismscript.stdlib
, so that the math.pow()
function would be called by script-writers as prismscript.stdlib.math.pow()
, that empty string would just need to become 'prismscript.stdlib'
. This allows you a great deal of flexibility in deciding how to set up a namespace for your application.
In the event that you want to expose an object as a flat namespace (example: you've instantiated a wrapper that controls a physical piece of machinery and you want to expose things like machine.spin_wheel()
to the script-context), you can pass the object to prismscript.auto_discover.scan()
with an appropriate namespace-prefix ('machine'
) and all of its functions will be ready to use, referring to that specific machine.
Objects provided to discover_functions()
may optionally have a __no_recurse
attribute, which must be a sequence. Any module-objects in this sequence will not be recursively scanned. This is important when trying to hide potentially dangerous parts of your implementation and for avoiding infinite recursion, as would normally happen if you were to import the os
module from Python without mangling it with a statement like import os as _os
, which is ugly and unnatural.
It is entirely possible for your scripts to define their own functions and nodes at runtime, allowing you to do such nifty things as download procedurally generated logic from a webservice and run it without any need to modify the code deployed in a production environment, or to have static one-off versions of your scripts for cases that differ only slightly.
Obviously, something like this carries with it some, possibly significant (dependng on how much functionality you've exposed to the Prismscript interpreter; by default, it's pretty well nerfed, aside from resource-usage), risks, so you'll probably want to define a custom function that implements some sort of security policy and expose that instead of the extend_namespace call, which should also be where your retrieval logic will live. Basic usage is simple enough, though:
interpeter.register_scoped_functions((('interperter.extend', interpreter.extend_namespace),))
What this does is bind the scoped function interpreter.extend in the scripting environment to an interpreter instance's extend_namespace
method, which takes any well-formed Prismscript code (as a string) and overlays it onto the running namespace, allowing new nodes and functions to be defined from within the executing script itself. Exceptions related to structural issues may be thrown, which is yet another good reason to wrap the extend_namespace
method in a function of your own.
Yeah, there have to be exceptions. Fortunately, they're only needed in really non-standard cases.
If you're designing a system that doesn't have any need for asynchronous processing, you can pretend this section doesn't even exist. However, if your custom functions need to start an action, ask for additional information, and block until that information is available, keep reading.
If your function meets the aforementioned criteria, you'll need to make it a Python generator so it becomes part of the recursive coroutine hierarchy. This is a lot easier than it may sound; the following code contains a mostly realistic example:
def weird_function(x):
if x == 5:
response = yield ('weird_function', 'spam') #Let the caller know that this function wants spam. What you yield is entirely up to you.
if response == 'eggs':
yield 'cheese' #Now the function isn't identifying itself. It just says it wants cheese and it's ignoring the response.
else:
raise prismscript.processor.interpreter.StatementReturn('Hi!')
raise prismscript.processor.interpreter.StatementReturn('Cheddar')
You'll notice, immediately, that there's a rather significant anti-pattern here: exceptions being used for flow-control. This is needed to maintain some semblance of sanity, given that Python generators disallow explicit returns, so just accept it. (You could also raise StatementExit
to do the same thing as an exit
statement)
What's significant is the yield
statements. Every time one of those appears, execution of the whole interpreter will be suspended until a value is sent back in; that value will be received at the point where the yield
occurred and your function will resume operation. How you use this model is entirely up to you. Just be aware that, if you want to return a value from this context, you need to raise StatementReturn
.