Stream Python into PowerPoint with pp_stream

I like Python, don’t get me wrong, but it’s not Microsoft PowerPoint™, which is clearly the superior software in every way. Unfortunately, my managers don’t agree with me. When I do things in PowerPoint, they ask things like, “why aren’t you working?” and “why did we ever hire you?” Alas, I’m stuck working in Python.

I scoured the internet for solutions to this problem, but unfortunately there weren’t any that I found acceptable. Jupyter has a --to slides option, but it’s not the same. Why can’t I just output everything into PowerPoint directly? Why isn’t this the default stdout on every computer in existence?

Well, worry not, fellow Microsoft Office fans. I’ve created a Python wrapper that solves this problem: PowerPoint Stream, or @pp_stream() for short, from my Python PowerPoint Pro (pppp) module. Just import the wrapper, wrap it around something, and Python will stream all your output directly into PowerPoint.

from pppp import pp_stream

@pp_stream('my_file.pptx')
def my_func():
    print('hello world')

if __name__ == '__main__':
    my_func()

And here’s the output for the code:

Hoorah! It works! It only took two extra lines to make it happen: an import statement, and the pp_stream. The code for this is at the bottom of the page, but I want to walk through the creation of this glorious, beautiful, perfect module.

Getting Python to Take Over PowerPoint

Python and I have at least one thing in common: We can both use PowerPoint. The difference is that I use PowerPoint by interacting with it on my computer screen, whereas Python needs to do it through the Component Object Model.

import win32com.client as win32 # pip install pywin32
from pywintypes import com_error

Now we need to actually initialize an instance of the app. You can do this pretty simply with app = win32.Dispatch('PowerPoint.Application'), but the issue with this implementation is that it’s not Pythonic:

  • Too many dots. Wordy. Pythonic code is simpler than this.
  • CamelCase for a method that has a single parameter, and that parameter never changes because each time we’re pulling up PowerPoint. (This is, after all, a PowerPoint module.)
  • If you want to set some properties of app, you need to set those on separate lines, not as a parameter when initializing it.
  • The object returned doesn’t make use of any of Python’s magic methods.

Ideally, we want our code to do something like this:

app = PowerPointApp(*args)

The only issue with this code is that it doesn’t work. But we can make it work! One option would be to create a very simple factory function, which resolves (most) of our issues:

def PowerPointApp(threaded=True): # Factory method
    if threaded:
        import pythoncom
        pythoncom.CoInitialize()
    app = win32.Dispatch('PowerPoint.Application')
    return app

The code works and it’s pretty Pythonic. But can we do better? Yes. Without any knowledge of win32com and no desire to read into it, you might be tempted to import contextmanager from contextlib and use a try/finally setup to properly tear down the app instance. Thankfully though, gencache has a function GetClassForProgID() that facilitates subclassing, which is preferable. We can use super() so that we don’t need to mess too much with COM.

(Note: COM is very finicky. You have to clean up a folder called gen_py in your temp dir and previously initialize an instance of an application to get this to work 100% of the time. Yes, I agree that it’s a mess. I’ll include the cleanup in the full version at the bottom of this post.)

PowerPoint doesn’t allow you to set app.Visible to False for some odd reason (you can do that for Excel and Word though?), so there isn’t much we need to do after running super().__init__(), but it’s nice to know you could put things there if you really wanted.

_PowerPoint = win32.gencache.GetClassForProgID('PowerPoint.Application')

class PowerPointApp(_PowerPoint):
    
    def __new__(cls):
        return super().__new__(cls)
        
    def __init__(self, threaded=True):
        if threaded:
            import pythoncom
            pythoncom.CoInitialize()
        super().__init__()

One issue with using win32.Dispatch() is that it’s leaky: the app instance doesn’t tear down properly unless you use the .Quit() method. Now that we’re subclassing, we can deal with this trivially:

def __del__(self):
    try:
        if self.Presentations.Count == 0:
            self.Quit()
    except com_error:
        pass

Spoiler alert: Later we’re going to want to use a context manager. So let’s add that in:

def __enter__(self):
    return self

def __exit__(self, exctype, excval, exctb):
    self.__del__()

Finally, the app.Presentations.Add() way of creating a new file isn’t as syntactically Pythonic as we’d like it to be. We can fix this:

def new_ppt(self):
    return self.Presentations.Add()

Now our PowerPointApp() class is about where it needs to be.

Writing in PowerPoint from Python

The next step is to figure out how to write things in PowerPoint. So I scoured through the documentation, clicked F2 in Visual Basic for Applications a few times, and did a little guesswork. Here’s what I came up with:

insert_text = 'hello world'
ppt_layout = ppt.SlideMaster.CustomLayouts(2)
new_slide = ppt.Slides.AddSlide(ppt.Slides.Count + 1, ppt_layout)
new_slide.Shapes(2).TextFrame.TextRange.Text = insert_text

It’s not pretty and it’s not Pythonic, but we can make it pretty and Pythonic by hiding the ugly stuff. That’s what functions are for.

def add_slide(ppt=None, insert_text=None):
    # ugly stuff here

To recap, this is what our code looks like so far to write 'hello world' into a PowerPoint presentation:

app = PowerPointApp()
ppt = app.new_ppt()
add_slide(ppt, 'hello world')

That’s it, three whole lines. Looks good, almost like real Python code.

Hacking the print() Statement

It’s not that hard to stick print() into str variables.

import io
from contextlib import redirect_stdout

f = io.StringIO()
with redirect_stdout(f):
    print('hello')
    print('world')

string_output = f.getvalue()

And since we can already put strings into PowerPoint, making the wrapper should be easy as pie.

…Except for one issue: we can’t tell the difference between new lines in a single print() statement versus multiple, separate prints. It’d be nice if we could print each print() into its own slide, and maintain the line breaks defined by the user.

assert string_output == 'hello\nworld\n'

There’s a solution to this: we can monkeypatch print in the wrapper we’re going to make. The trick here is to choose a delimiter other than \n. We’re going to parameterize this for users’ sake, but for our purposes we’ll be using the null char, \0, by default. (If you want to parameterize this in the actual wrapper, that’s on you.) We also want to override the user’s end keyword argument if they assign it.

class DelimitPrint(object):
    def __init__(self, delimit):
        self._print = print
        self.delimit = delimit or '\0'
    
    def __enter__(self):
        def custom_print(*args, **kwargs):
            new_kwargs = kwargs
            new_kwargs['end'] = self.delimit
            self._print(*args, **new_kwargs)
            
        builtins.print = custom_print
    
    def __exit__(self, *args, **kwargs):
        builtins.print = self._print

Combine it all, and make sure the context manager tears down correctly– we wouldn’t want to accidentally monkeypatch print forever!

f = io.StringIO()
g = io.StringIO()

with redirect_stdout(f):
    with DelimitPrint('\0'):
        print('hello', end='\n')
        print('world')

with redirect_stdout(g):
    print('hello')
    print('world')

assert f.getvalue() == 'hello\0world\0'
assert g.getvalue() == 'hello\nworld\n'

Letting the pp_stream Flow

The last step is to combine all this into the wrapper. There’s a lot of context management going on, so try to keep up with the indentation!

def pp_stream(filename):
    def wrapper(func):
        def print_to_pp(*args, **kwargs):
            delimit = '\0'
            with DelimitPrint(delimit):
                with PowerPointApp() as app:
                    f = io.StringIO()
                    with redirect_stdout(f):
                        ppt = app.new_ppt()
                        try:
                            user_func = func(*args, **kwargs)
                        finally:
                            for s in f.getvalue().split(delimit)[:-1]:
                                add_slide(ppt, s)
                            ppt.SaveAs(filename)
                            ppt.Close()
                            return user_func
        return print_to_pp
    return wrapper

What’s going on? The first 3 lines and last 2 lines are typical wrapper stuff. Lines 4-8 are just all the context managers we made. Lines 9-17 is where the PowerPoint magic happens.

And we’re done!

The Full Code

Sigh, I know what you degenerates are here for. You came here for the pp_stream, not to read. I’ll have you know I took a whole evening off of job searching to make sure my pp_stream wrapper came out golden, so you better freaking enjoy it.

import builtins
import io
import win32com.client as win32
from contextlib import redirect_stdout
from pywintypes import com_error

_PowerPoint = win32.gencache.GetClassForProgID('PowerPoint.Application')
if _PowerPoint is None:
    p = win32.gencache.EnsureDispatch('PowerPoint.Application')
    if p.Presentations.Count == 0:
        p.Quit()
    _PowerPoint = win32.gencache.GetClassForProgID('PowerPoint.Application')

class PowerPointApp(_PowerPoint):
    
    def __new__(cls):
        return super().__new__(cls)
        
    def __init__(self, threaded=True):
        if threaded:
            import pythoncom
            pythoncom.CoInitialize()
        super().__init__()
    
    def __del__(self):
        try:
            if self.Presentations.Count == 0:
                self.Quit()
        except com_error:
            pass
    
    def new_ppt(self):
        return self.Presentations.Add()
        
    def __enter__(self):
        return self
        
    def __exit__(self, exctype, excval, exctb):
        self.__del__()

def add_slide(ppt=None, insert_text=None):
    ppt_layout = ppt.SlideMaster.CustomLayouts(2)
    new_slide = ppt.Slides.AddSlide(ppt.Slides.Count + 1, ppt_layout)
    new_slide.Shapes(2).TextFrame.TextRange.Text = insert_text

class DelimitPrint(object):
    def __init__(self, delimit):
        self._print = print
        self.delimit = delimit or '\0'
    
    def __enter__(self):
        def custom_print(*args, **kwargs):
            new_kwargs = kwargs
            new_kwargs['end'] = self.delimit
            self._print(*args, **new_kwargs)
            
        builtins.print = custom_print
    
    def __exit__(self, *args, **kwargs):
        builtins.print = self._print

def pp_stream(filename):
    def wrapper(func):
        def print_to_pp(*args, **kwargs):
            delimit = '\0'
            with DelimitPrint(delimit):
                with PowerPointApp() as app:
                    f = io.StringIO()
                    with redirect_stdout(f):
                        ppt = app.new_ppt()
                        try:
                            user_func = func(*args, **kwargs)
                        finally:
                            for s in f.getvalue().split(delimit)[:-1]:
                                add_slide(ppt, s)
                            ppt.SaveAs(filename)
                            ppt.Close()
                            return user_func
        return print_to_pp
    return wrapper

if __name__ == '__main__':
    
    @pp_stream('test_me.pptx')
    def my_function():
        print('Hello!\nI\'m happy you\'re using my code.')
        print('See? It works!')
    
    my_function()

%d bloggers like this: