Skip to content

TheGamby/PyExpression

Repository files navigation

PyExpression

PyExpression is a small, controlled, Python-like expression and mini-script engine for C#.

It is built for hosts that want a scripting layer without taking on a full Python runtime or exposing arbitrary .NET object access.

What it is good at:

  • evaluating small user-defined rules and formulas
  • running predictable mini-scripts with loops, conditions, and collections
  • exposing only an approved callable surface through PyLib
  • extending that callable surface through explicit plugin assemblies

What it is deliberately not:

  • a Python implementation
  • an embedded CPython runtime
  • IronPython
  • Roslyn scripting
  • a bridge to arbitrary .NET objects, the file system, or the network

Repository Overview

This repository contains four projects:

  • PyExpression - the core library and the only project published as a NuGet package
  • PyFilter.Methods - an example plugin assembly that shows how to add custom methods; it is not published by CI
  • PyExpression.tests - xUnit tests for the engine and plugin loading
  • PyExpressionSandbox - a Windows-only WPF demo app for interactive experimentation

Before You Start

These are the main points that save people time:

  • The library currently targets net10.0. You need the .NET 10 SDK to build this repository.
  • PyExpressionSandbox is WPF and therefore Windows-only.
  • The core library and tests can be built on non-Windows machines. The sandbox cannot.
  • Plugins are not auto-loaded. If you want extra methods, register them explicitly or load them from a folder.
  • PyLib.Default only contains the built-in MathMethods provider.
  • yield values are only exposed through StreamAsync(...). If you call Execute(), you get the final return value, not a stream.

Installation

If the package version you want has already been published to nuget.org:

dotnet add package PyExpression

Package details:

  • Package ID: PyExpression
  • Target framework: net10.0

If the package is not available yet, build from source using the commands in Building From Source. NuGet publishing is tag-driven; normal branch pushes and pull requests do not publish packages.

Quick Start

One-off execution

using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;

var result = PyExpressionEngine.Execute(
    """
    x = n * 2
    return x
    """,
    new PyVar("n", 9));

Console.WriteLine(result.Value); // 18

Instance API

using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;

var expression = new PyExpressionEngine(
    """
    if n > 10:
        return 100
    else:
        return 50
    """)
{
    Vars = new[]
    {
        new PyVar("n", 11)
    }
};

var result = expression.Execute();
Console.WriteLine(result.Value); // 100

Reuse a parsed script with Prepare()

If you run the same script repeatedly with different variables, prepare it once and reuse it:

using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;

var expression = new PyExpressionEngine("return n * 2");
var prepared = expression.Prepare();

var first = expression.Execute(prepared, new[] { new PyVar("n", 7) });
var second = expression.Execute(prepared, new[] { new PyVar("n", 9) });

Console.WriteLine(first.Value);  // 14
Console.WriteLine(second.Value); // 18

Stream yield values

using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;

var expression = new PyExpressionEngine(
    """
    i = 0
    while i < 3:
        i = i + 1
        yield i
    """);

await foreach (var item in expression.StreamAsync())
{
    Console.WriteLine(item);
}

Supported Language Surface

Supported literals:

  • integers
  • doubles
  • booleans
  • strings

Supported expressions:

  • +, -, *, /, %
  • ==, !=, <, <=, >, >=
  • and, or, not

Supported statements:

  • assignment
  • return
  • if / else
  • while
  • for ... in range(...)
  • break
  • continue
  • yield
  • yield from

Supported collections:

  • list literals
  • dictionary literals
  • index access
  • indexed assignment for lists and dictionaries

Built-in methods in PyLib.Default:

  • abs
  • min
  • max
  • sum
  • sorted
  • keys
  • values
  • round
  • floor
  • ceil
  • pow
  • sqrt
  • len
  • clamp

Deliberately Unsupported

These omissions are intentional:

  • import
  • user-defined functions inside the script
  • class definitions
  • arbitrary reflection
  • access to arbitrary .NET objects
  • file system access
  • network access
  • process control
  • dynamic assembly execution from script code

If you need additional capabilities, the expected extension point is PyLib, not unrestricted host access.

Public API at a Glance

Important public types:

  • PyExpression.PyExpression - the main execution class
  • PyVar - named variables passed into or returned from the engine
  • PyLib - registry of approved callable methods
  • IPyMethods / PyMethods - plugin provider contract and helper base class
  • IPyPluginManifest / PyPluginManifest - plugin metadata
  • PluginLoadResult - per-assembly plugin load result

Common members:

  • PyExpression.Execute(...)
  • PyExpression.ExecuteAsync(...)
  • PyExpression.StreamAsync(...)
  • PyExpression.Prepare(...)
  • PyExpression.ContainsYield()
  • PyLib.Register(...)
  • PyLib.LoadPluginsFromFolder(...)

For a type-by-type reference, see docs/api-reference.md.

Plugins

Plugins are controlled method provider assemblies that implement IPyMethods.

Important behavior:

  • Plugins are not discovered automatically.
  • The default library does not scan the application directory.
  • Folder loading scans only the specified folder for *.dll; it does not recurse into subfolders.
  • Folder loading instantiates provider types via Activator.CreateInstance(...), so provider types must be public, non-abstract, and constructible without custom arguments.

Register a provider directly

using PyExpression;
using PyFilter.Methods;
using PyExpressionEngine = PyExpression.PyExpression;

var library = new PyLib().Register(new PyFilterMethods());

var expression = new PyExpressionEngine("return contains(\"hello world\", \"world\")")
{
    Library = library
};

var result = expression.Execute();
Console.WriteLine(result.Value); // True

Scan an assembly for providers

var library = new PyLib().Register(typeof(PyFilter.Methods.PyFilterMethods).Assembly);

Load plugin DLLs from a folder

var library = new PyLib();
var results = library.LoadPluginsFromFolder(@"C:\plugins");

foreach (var result in results)
{
    Console.WriteLine($"{result.AssemblyName}: success={result.Success}");
}

Example plugin in this repository

PyFilter.Methods shows a real plugin assembly and registers:

  • contains
  • starts_with
  • ends_with
  • matches
  • is_null_or_whitespace

If you are building your own plugin, start with how-to-create-a-plugin.md.

Error Handling

The engine uses explicit exception types so callers can distinguish between parse failures, runtime failures, missing variables, missing methods, invalid argument counts, and iteration-limit violations.

Relevant exception types:

  • PyParseException
  • PyRuntimeException
  • PyUnknownVariableException
  • PyUnknownMethodException
  • PyMethodArgumentException
  • PyIterationLimitExceededException

Example:

using PyExpression;
using PyExpressionEngine = PyExpression.PyExpression;

try
{
    var result = PyExpressionEngine.Execute("return missing");
}
catch (PyUnknownVariableException ex)
{
    Console.WriteLine(ex.Message);
}

Common Gotchas

These are the things most likely to trip up first-time users:

  • Execute() returns the final result. It does not enumerate yield values.
  • A script without return completes successfully and returns null.
  • Assigning to an input variable updates the supplied PyVar instance in place.
  • Extra methods are unavailable until they are registered in the Library used by that PyExpression instance.
  • PyExpressionSandbox failing on macOS or Linux is expected; build the library and tests directly instead.
  • PyFilter.Methods is sample code. The CI publish path only packs PyExpression.

Example Scripts

Variables

n = 5
x = n * 2
return x

Conditions

if n > 10:
    return 100
else:
    return 50

While loop

i = 0
sum = 0
while i < 5:
    sum = sum + i
    i = i + 1
return sum

For loop

sum = 0
for i in range(1, 6, 2):
    sum = sum + i
return sum

Yield stream

i = 0
while i < 3:
    i = i + 1
    yield i

Building From Source

Cross-platform: core library and tests

These commands work on Windows, macOS, and Linux:

dotnet build PyExpression/PyExpression.csproj -c Release
dotnet build PyFilter.Methods/PyFilter.Methods.csproj -c Release
dotnet test PyExpression.tests/PyExpression.tests.csproj -c Release

If you only want to validate the engine, the test command is usually enough because it builds the referenced projects automatically:

dotnet test PyExpression.tests/PyExpression.tests.csproj -c Release

Windows-only: sandbox app and full solution

The sandbox is a WPF application and only builds on Windows:

dotnet build PyExpressionSandbox/PyExpressionSandbox.csproj -c Release
dotnet build PyExpressionSandbox.slnx -c Release

Create a local NuGet package

dotnet pack PyExpression/PyExpression.csproj -c Release -o ./artifacts/nuget

This packs only PyExpression, which is the same packaging boundary used by CI.

CI and NuGet Publishing

GitHub Actions behavior in this repository:

  • branch pushes and pull requests run restore, build, and test
  • version tags matching v* additionally pack PyExpression
  • trusted publishing is used to publish the resulting package to nuget.org

Examples:

  • v1.2.3 -> stable release publish
  • v1.2.3-rc.1 -> prerelease publish

Only PyExpression is published. PyFilter.Methods remains an example plugin project inside the repository.

Test Coverage

The test project covers:

  • variable access and reassignment
  • arithmetic and operator precedence
  • comparisons and boolean logic
  • conditions
  • while and for ... in range(...)
  • break and continue
  • lists, dictionaries, indexing, and indexed assignment
  • built-in methods
  • direct plugin registration, assembly scanning, and folder-based loading
  • async execution
  • yield and yield from
  • parse and runtime error cases

Repository Layout

.
|-- .github/
|   `-- workflows/
|       `-- ci.yml
|-- PyExpression/
|-- PyExpression.tests/
|-- PyExpressionSandbox/
|-- PyExpressionSandbox.slnx
|-- PyFilter.Methods/
|-- docs/
|-- how-to-create-a-plugin.md
|-- LICENSE
`-- README.md

Documentation

License

This project is licensed under the MIT License. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors

Languages