tonybaloney / CSnakes

https://tonybaloney.github.io/CSnakes/
MIT License
320 stars 23 forks source link

Load Python from String or File Path #290

Open nwoolls opened 1 month ago

nwoolls commented 1 month ago

I apologize if I've overlooked this. I am trying out this project to compare to other options as the support and stated goals seem outstanding (saw on .NET video on YouTube). Congrats on the project and visibility!

What I'd like to be able to do for our project is load Python code from a string or an arbitrary file that is not present at build time. This is for a general scripting engine where we allow JavaScript and Python.

While the code generation is awesome and we may use it for future solutions, that's not what we are aiming to use at this time. I see from the docs that you can in fact bypass codegen, but it still seems to load a module by name rather than by file path or passing in a string of Python.

Is this currently possible or on the roadmap? Or is that outside the scope of this project. Thanks for the project!

tonybaloney commented 1 month ago

Like most things in Python, there's 100 ways to do this.

Could you share a bit more about your requirement?

This doc explains some of the complexity https://docs.python.org/3/library/functions.html#eval

If you were able to share a Python equivalent of what you're trying to do, then I'd know which APIs need implementing.

nwoolls commented 1 month ago

So - just as an example from your own samples, what we're looking for is the ability to execute e.g.

def format_name(name: str, max_length: int = 50) -> str:
    return "Hello {}".format(name.capitalize())[:max_length]

By simply assigning the above string value. Right now there's Import.ImportModule which expects a module name. So perhaps Import.ImportCode or something along those lines where you pass in the code that would have been in the .py file.

For our own use cases these would be .py files provided at runtime, so not in a predefined folder. And they each define a single function that gets called from .NET.

I can give more concrete examples if the above does not suffice. In a broader sense, being able to load and execute any of the Python code found in your simple example by specifying a string containing the Python code rather than referencing a file that was there at build time would do what we're looking for.

I can give examples of how we're currently doing this with another Python NuGet package if that helps.

minesworld commented 3 weeks ago

So - just as an example from your own samples, what we're looking for is the ability to execute e.g.

def format_name(name: str, max_length: int = 50) -> str:
  return "Hello {}".format(name.capitalize())[:max_length]

Its unclear what you want to do.... .

But anything is already possible: either using Runtime.CAPI (if the needed CPython DLL calls are missing, its no problem to add them) or even easier/crazier:

make a python "wrapper" - a .py file included in your project which defines a function like

def makemodule(source: str, name:  str) -> object:
  [... lookup on the web how to to use the importlib etc. ]
  return module

call that in c# via the CSnakes PythonEnvironment and import the result as module into python.

Include the source generator of CSnakes in your app and do something like

var result = PythonParser.TryParseFunctionDefinitions(code, out PythonFunctionDefinition[] functions, out GeneratorError[]? errors);

if (errors) throw new Exception();

if (result)
{
    IEnumerable<MethodDefinition> methods = ModuleReflection.MethodsFromFunctionDefinitions(functions, fileName);
    string source = PythonStaticGenerator.FormatClassFromMethods(@namespace, pascalFileName, methods, fileName, functions);
}

After that use some code from the web to load the generated source into dotnet at runtime...

But I guess you asked for a wrapper over exec (define the function) and eval (execute it) ... Just look up how you can solve that problem in pure python and wrap it into one or two functions you can call from C# via the PythonEnvironment . As a starting point on the python side take a look at https://stackoverflow.com/questions/12698028/why-is-pythons-eval-rejecting-this-multiline-string-and-how-can-i-fix-it

Or the .py wrapper only creates a function object and returns it. On the C# side its a PyObject where you can use the Call() methods to invoke the defined functions with the desired args and kwargs ...

In case you don't want to use a wrapper, you must include the CSnakes project into your code to be able to write the equivalent code under the CSnakes.Runtime etc. namespace.

minesworld commented 3 weeks ago

Take a look at my refactored fork of CSnakes at https://github.com/minesworld/CSnakes/tree/minesworld

The main difference is: the CSnakes.Runtime.Python base object is PythonObject instead of PyObject. Just different name to make clear that it isn't a CPython PyObject but a dotnet wrapper on such one. Renaming of the other objects will follow...

There you find a CSnakes.Runtime.Python.Import.CreateAndImportModuleFromSource function which give you back a PyModule wrapped as a PythonObject.

You could use it like:

var module = Import.CreateAndImportModuleFromSource("xxx", source, filepath or "<string>");
var function = module.GetAttr("format_name");
function.Call(...)

The problem defining a function from source is how to obtain it as source could define many functions, variables, imports etc. .

This is solved here by using a module.

Another option would be to Compile the source, Run it using two PyDict for global and local variables. And get the function from the local variables dict ...

The name of the function is either known, or all dict items / module.GetAttr("__dict__") could be searched for a callable object (the function) or CSnakes Parser could be used to get the function names.

But thats not my cup of tea for now...

I wrote that function to create and run a "__main__" module. Works, providing a file path makes it debugable from VS Code using debugpy. Only sys.args can't be used yet...

PS: will rename CreateAndImportModuleFromSource , doesn't feel right looking at it...