A few months ago I wrote up an article on using PyTools, Visual Studio, and Python all together. I received some much appreciated positive feedback for it, but really for me it was about exploring. I had dabbled with Python a few years back and hadn't really touched it much since. I spend the bulk of my programming time in Visual Studio, so it was a great opportunity to try and bridge that gap when looking at something like IronPython.
I had an individual contact me via the Dev Leader Facebook group that had come across my original article. However, he wanted a little bit more out of it. Since I had my initial exploring out of the way, I figured it was probably worth trying to come up with a semi-useful example. I could get two birds with one stone here--Help out at least one person, and get another blog post written up!
The request was really around taking the output from a Python script and being able to display it in a WinForm application. I took it one step further and created an application that either lets you choose a Python script from your file system or let you type in a basic script directly on the form. There isn't any fancy editor tools on the form, but someone could easily take this application and extend it into a little Python editor if they wanted to.
Leveraging IronPython
In my original PyTools article, I mention how to get IronPython installed into your Visual Studio project. In Visual Studio 2012 (and likely a very similar approach for other versions of Visual Studio), the following steps should get you setup with IronPython in your project:
- Open an existing project or start a new one.
- Make sure your project is set to be at least .NET 4.0
- Right click on the project within your solution explorer and select "Properties"
- Switch to the "Application" tab.
- Under "Target framework", select ".NET Framework 4.0".
- Right click on the project within your solution explorer and select "Manage NuGet Packages...".
- In the "Search Online" text field on the top right, search for "IronPython".
- Select "IronPython" from within the search results and press the "Install" button.
- Follow the instructions, and you should be good to go!
Now that we have IronPython in a project, we'll need to actually look at some code that gets us up and running with executing Python code from within C#. If you followed my original post, you'll know that it's pretty simple:
var py = Python.CreateEngine();
py.Execute("your python code here");
And there you have it. If it seems easy, that's because it is. But what about the part about getting the output from Python? What if I wanted to print something to the console in Python and see what it spits out? After all, that's the goal I was setting out to accomplish with this article. If you try the following code, you'll notice you see a whole lot of nothing:
var py = Python.CreateEngine();
py.Execute("print('I wish I could see this in the console...')");
What gives? How are we supposed to see the output from IronPython? Well, it all has to do with setting the output Stream of the IronPython engine. It has a nice little method for letting you specify what stream to output to:
var py = Python.CreateEngine();
py.Runtime.IO.SetOutput(yourStreamInstanceHere);
In this example, I wanted to output the stream directly into my own TextBox. To accomplish this, I wrote up my own little stream wrapper that takes in a TextBox and appends the stream contents directly to the Text property of the TextBox. Here's what my stream implementation looks like:
private class ScriptOutputStream : Stream
{
private readonly TextBox _control;
public ScriptOutputStream(TextBox control)
{
_control = control;
}
public override bool CanRead
{
get { return false; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { throw new NotImplementedException(); }
}
public override long Position
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
public override void Flush()
{
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
_control.Text += Encoding.GetEncoding(1252).GetString(buffer, offset, count);
}
}
Now while this isn't pretty, it serves one purpose: Use the stream API to allow binary data to be appended to a TextBox. The magic is happening inside of the Write() method where I take the binary data that IronPython will be providing to us, convert it to a string via code page 1252 encoding, and then append that directly to the control's Text property. In order to use this, we just need to set it up on our IronPython engine:
var py = Python.CreateEngine();
py.Runtime.IO.SetOutput(new ScriptOutputStream(txtYourTextBoxInstance), Encoding.GetEncoding(1252));
Now, any time you output to the console in IronPython you'll get your console output directly in your TextBox! The ScriptOutputStream implementation and calling SetOutput() are really the key points in getting output from IronPython.
The Application at a Glance
I wanted to take this example a little bit further than the initial request. I didn't just want to show that I could take the IronPython output and put it in a form control, I wanted to demonstrate being able to pick the Python code to run too!
Firstly, you're able to browse for Python scripts using the default radio button. Just type in the path to your script or use the browse button:
Next, press "Run Script", and you're off! This simply uses a StreamReader to get the contents of the file and then once in the contents are stored in a string, they are passed into the IronPython engine's Execute() method. As you might have guessed, my "helloworld.py" script just contains a single line that prints out "Hello, World!". Nothing too fancy in there!
Let's try running a script that we type into the input TextBox instead. There's some basic error handling so if your script doesn't execute, I'll print out the exception and the stack trace to go along with it. In this case, I tried executing a Python script that was just "asd". Clearly, this is invalid and shouldn't run:
That should be along the lines of what we expected--The script isn't valid, and IronPython tells us why. What other errors can we see? Well, the IronPython engine will also let you know if you have bad syntax:
Finally, if we want to see some working Python we can do some console printing. Let's try a little HelloWorld-esque script:
Summary
This sample was pretty short but that just demonstrates how easy it is! Passing in a script from C# into the IronPython is straight forward, but getting the output from IronPython is a bit trickier. If you're not familiar with the different parts of the IronPython engine, it can be difficult to find the things you need to get this working. With a simple custom stream implementation we're able to get the output from IronPython easily. All we had to do was create our own stream implementation and pass it into the SetOutput() method that's available via the IronPython engine class. Now we can easily hook the output of our Python scripts!
As always, all of the source for you to try this out is available online:
- PasteBin (Just the MainForm where the bulk of the code resides)
- Google Code
- GitHub
- BitBucket
Some next steps might include:
- Creating your own Python IDE. Figure out some nice text-editing features and you can run Python scripts right from your application.
- Creating a test script dashboard. Do you write test scripts for other applications in Python? Why not have a dashboard that can report on the results of these scripts?
- Add in some game scripting! Sure, you could have done this with IronPython alone, but maybe now you can skip the WinForms part of this and just make your own stream wrapper for getting script output. Cook up some simple scripts in a scripting engine and voila! You can easily pass information into Python and get the results back out.
Let me know in the comments if you come up with some other cool ideas for how you can leverage this!