GlobalItems object references don't seem to honor interfaces

I am evaluating AlterNet so my experience is limited. We want to expose application objects to Python script. However, we don’t want to expose all the methods in our objects to script. My first thought was to use an interface on the exposed class to “filter” what is exposed. I modified the Python.Wpf ObjectReference to try this. I created a class “ScriptApi” that has a method I don’t want to use in script. I then created an interface IScriptApi that does not include the “hidden” method. Then I passed the IScript API into a ScriptGlobalItem like this:

                var api = (IScriptApi)new ScriptApi();
                var item2 = new ScriptGlobalItem("Script", api);
                scriptRun.GlobalItems.Clear();
                scriptRun.GlobalItems.Add(item2);

When I test it, the Python script can see all the methods on ScriptApi.
image

Controlling access to our internal application objects is a critical feature for us. Our current scripting solution allows us to do that using a custom attribute that works on both classes and methods within the class. We could make “wrapper” objects to expose to the script but that would be a messy and inelegant solution. Is there a better one?

We have a similar requirement for classes to expose in script. Ideally, not every class in a given assembly would be visible in Python. Again, we can build separate assemblies that only contain the classes we want to expose but that will be a fairly significant refactoring of our application. Maybe we could leverage namespaces and imports to control this?

Thanks,
Nathan

Hi Nathan,

The easiest way to filter out some members would be to handle NeedCodeCompletion event in the editor and remove unneeded entries from the Provider . However, the parser will still understand these identifiers and will provide QuickInfo help for them.

To avoid that, the Python semantic model needs to be extended to skip adding special types/methods. Right now, we only check for IsSpecialName and a few other to decide whether to include type/method to the semantic model/intellisense.

protected virtual bool IsSpecialType(Type type)
{
return type.IsSpecialName || type.IsNotPublic || type.IsNestedPrivate ||
type.Name.StartsWith(“$$”) || type.Name.StartsWith(“<”) || type.Name.StartsWith(“__”);
}

Could you let us know what attribute you’re using to suppress types/methods to be added to the IntelliSense?

We will then provide an example of how this can be achieved.

As far as scripting is concerned, the user will still be able to call these methods, even if they’re filtered out from the intellisense.

To control which assemblies are added to the Script you need to use ScriptRun.ScriptSource.References property.
This makes all types from this assembly potentially accessible by the script. I can’t seem to find a way to add a specific type from the assembly to the Python.NET.

Having said that, you can limit types to be used by not popuplating ScriptRun.ScriptSource.Imports property and providing startup script that will limit Python code to use only certain types from the namespace :

Right now, when Imports are specified, we add the following code to the startup script:

from
from import *

It should be possible to specify only types allowed to be used instead of *

To see how it works, you can comment the following lines in the ObjectReference demo:

scriptRun.ScriptSource.Imports.Add("System");
scriptRun.ScriptSource.Imports.Add("System.Windows");

And add the following lines to the beginning of the script:

from System import TimeSpan, Random
from System.Windows import Thickness

These lines can be provided via startup script (but I don’t think you can limit user to add them manually to the script, which will give him an access to any type in the assembly).

Kind regards,
Dmitry

Dmitry,
In our current scripting solution classes and methods not marked with the custom “[Scriptable]” attribute are ignored by the parsing engine. They cannot be called from script at all. This is only for assemblies referenced through the script engine API, if the user references the assembly they can do anything. It sounds like that is not possible in AlterNet Studio, so we would have to create “wrapper” objects to expose to Python that only contain the methods we want exposed. Not impossible but a major refactor of our object model.

Using the startup script to filter classes should work for us. By “startup script” do you mean IScriptSource.StartupFile? If I am reading the docs correctly, we would have to emit our imports into this script then call the entry point in the user’s script file. I will play with that today and see if I can get it working.

It is OK for the user to reference our assemblies and import anything they like. Probably 85% of our users never look at a script anyway, but those who do tend to know what they are doing.

Much thanks,
Nathan

Dmitry,
One more follow-up. I would expect this code to expose an “IScriptApi” to Python, but it exposes a ScriptApi.

            IScriptApi api = new ScriptApi();
            var scriptGlobalItem = new ScriptGlobalItem("Script", api);
            scriptRun.GlobalItems.Clear();
            scriptRun.GlobalItems.Add(scriptGlobalItem);

It seems like ScriptGlobalItem should take a .NET type so engine knows what type the global object should be. Is there another way to specify the type for the global?

Thanks,
Nathan

Hi Nathan,

Internally Python.NET uses obj.GetType() to determine the type of the .NET object passed via GlobalItems.

Having said that, we register Convert function that accepts the type of the object as a second argument, here is how it can be used in Python code:

api = clr.Convert(Script, IScriptApi);

We will look to see if we can use it to support typed ScriptGlobalItem (where the type of the object is explicitly set).

Kind regards,
Dmitry

Dmitry,
I understand. I will try the same thing in IronPython to see what it does. Thanks for looking into this!

Nathan

I think I see what has to happen to support this. I am looking at Python.Net since I don’t think IronPython will work for us. The Python.Net ConverterExtension does this:
public static PyObject ToPython(this object? o)
{
if (o is null) return Runtime.None;
return Converter.ToPython(o, o.GetType()).MoveToPyObject();
}
What we really need is another method that does ToPython(this object? o, Type type). It looks like the Converter class will do that properly. Unfortunately, the Converter class is internal. So unless we get this added to PNet we would have to do a custom build of PNet or maybe use some reflection to get at the internal class. I may try one of those to make sure the converter handles the interface type the way I think it does.

Hi Nathan,

We’ve reported this issue to PythonNet GitHub, and already prepared a pull request to support this change. More information here:

We will include this change in the coming minor update (which we normally push via NuGet), with the workaround exposing internal classes/methods of PythonNET runtime. Once our change is made available by PythonNET team, we will remove this workaround from the further updates.

And yes, looking at IronPython source code, it does not seem possible to do similar things there.

Kind regards,
Dmitry

Hi Nathan,

We’ve just released a new update via NuGet (version 9.5.2), it now supports passing Script global item alongside its type to the Python scripter.

In case you pass an interface as the type of the object, only methods of this interface will be accessible from the script and displayed in the editor’s IntelliSense.

Kind regards,
Dmitry

Thanks! I got pulled off on other priorities for a while but this looks good.

1 Like