Host libraries and functions
Three ways to put your own code in front of a Python program, and the difference between them is about state, not convenience.
var bash = Bash.CreateBuilder()
.WithPython(new PythonOptions
{
Libraries =
[
PythonLibrary.FromFactory("tickets", context => BuildTicketsModule(context)),
PythonLibrary.FromSource("formatting", "def table(rows):\n ..."),
],
HostFunctions =
{
["disk_usage"] = (context, args) => new PyInt(Total(context.RequireFileSystem(), args)),
},
})
.Build();
| Point | Shape | Lifetime |
|---|---|---|
Libraries |
PythonLibrary |
built afresh for every run, and only if that run imports it |
AdditionalModules |
a PyObject |
one object, shared by every run |
HostFunctions |
Func<PythonHostContext, PyObject[], PyObject> |
a function in the program's globals, handed the run's environment |
ExternalFunctions |
Func<PyObject[], PyObject> |
the same, for host code that needs no environment |
A library is a recipe, not a module
`AdditionalModules` is for constants only
One object handed to every run is fine for a lookup table that never changes, and wrong for anything a program can mutate: that would be one run's state becoming the next one's, and in a multi-tenant host, one tenant's state becoming another's.
Libraries builds a fresh module per run, which is why it is the route a stateful library takes. A
library written in Python has module-level state by construction, so FromSource is always a
Libraries entry.
Three ways to write one:
// In C#, with the run's environment.
PythonLibrary.FromFactory("tickets", context => …);
// In C#, as a bag of functions.
PythonLibrary.FromFunctions("tickets", new Dictionary<string, Func<PythonHostContext, PyObject[], PyObject>>
{
["list"] = (context, args) => …,
});
// In Python.
PythonLibrary.FromSource("formatting", """
def table(rows):
width = max(len(r[0]) for r in rows)
return "\n".join(f"{r[0]:<{width}} {r[1]}" for r in rows)
""");
PythonHostContext
C# called from Python is handed the environment rather than reaching for one. The context carries the virtual filesystem, the working directory as it stands right now, the environment the script exported, and the run's limits and clock, and nothing else.
Because it reads the environment live, a cd earlier in the surrounding shell script is where host
code finds itself too.
RequireFileSystem() is the guard for host code that needs storage where the host configured none:
it raises a Python OSError, so the program can handle it. A .NET exception escaping into a
sandboxed program would be the sandbox leaking rather than an error the program could catch.
Reaching Python from the shell
Everything above is configured on the joined Computerwelt package's WithPython(...), so a
library registered here is importable from any python invocation inside a shell script:
python -c "import tickets; print(len(tickets.list()))"
Read next
- Extending the shell — the same idea, for commands.
- Building an agent sandbox — the whole thing put together.