Coroutines in Action (yield from) : Implementing a coroutine to act as a piping function.


Before diving into this tutorial. If you don't know what a coroutine is and how to write a basic one, please read this article here, before reading on.

The goal of this tutorial is demonstrate the functionality of a coroutine through a simple application. Implementing a coroutine within your code can extremely simply the task you're trying to write. In this example, I will show how a coroutine can be constructed to serve as a pipe that feeds values into a function.

For the context of this article, I want to identify three types of functions.

- The main function: the function where the calls are made by the author
- An intermediate function: a generator that acts as a pipe between the main function and the end function.
- An end function: a sub-generator function that receives values from the main function through the intermediate function.

You must be wondering how we make the intermediate function act like a pipe. In my first post we talked about the different states a generator could be in. Any generator that is suspended at yield, occupied the state 'GEN_SUSPENDED'. 

To make the intermediate function act as a pipe between the main function and the end function, we must keep it in the 'GEN_SUSPENDED' state. 

The example I will show you is script that sends text (string) to certain email addresses that are fetched from a json file. Take a look. 


import json

import smtplib

def main():
    with open('contacts.json', 'r') as fil:
        decoded = json.load(fil)
        results = {}
        for contact in decoded['persons']:
            emailContact = str(contact['email'])
            emailSend = pipe('example@gmail.com', 'password')
            next(emailSend)
            emailSend.send([emailContact, results])
        emailSend.send([None,0])

        print(results)

To understand why the emails are extracted this way, take a look at the json file below. 

{
    "persons": [
        {
            "city": "CityNameOne",
            "name": "NameOne",
            "email": "emailOne@gmail.com"
        },
        {
            "city": "CityNameTwo",
            "name": "NameTwo",
            "email": "emailTwo@gmail.com"
        },
        {
            "city": "CityNameThree",
            "name": "NameThree",
            "email": "emailThree@gmail.com"
        }
    ]

}

Now that we have that part covered,  lets look a the pipe function, how it's called, and what it does. 

def pipe(fromaddr, frompass):
    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login(fromaddr, frompass)
    while True:

        _ = yield from sender(server)

def sender(server):
    text = 'Hello World'
    while True:
        pipeIP = yield
        toaddr = pipeIP[0]
        results = pipeIP[1]
        if toaddr is None:
            break
        server.sendmail('exmample@gmail.com', toaddr, text)

        results[toaddr] = 'confirmed'

The two arguments passed to the pipe function in main are the email address and password you will use to send from. Inside the pipe function, the server is initiated and used to login. However the more important part is the yield from. 

yield from 

First thing to know about yield from is that it's not like yield. If you've every used asyncio, you're familiar with await. Consider yield from as await. 

When the intermediate function calls yield from endFunction( ), it's as if the end function takes over from the intermediate function. Effectively, it's like the main function is directly connected to the end function. 

next(emailSend) in the main function, primes the function. Now whenever pipe is sent a value, it's sent into the sender instance through yield from. In the same context, pipe will remain suspended as long as the end function (sender) is being sent values from the main function. 

For every contact (for contact in decoded['persons']) a new pipe instance is created. After all the contacts have been fetched, and sent to, when then send(None). 

Sending None into the pipe causes the sender function to end, and let the pipe function continue executing from the LHS of 'yield from'. Meaning, since the first time (first contact) we called next(emailSend) until the only time we called send(None) the pipe function was suspended on the RHS of yield from acting like a pipe to the end function. 

This means that if we never executes send(None), the pipe function will be suspended at yield from forever. 

To get a better understanding we can use the inspect library.

from inspect import getgeneratorstate

def main():
    with open('contacts.json', 'r') as fil:
        decoded = json.load(fil)
        results = {}
        for contact in decoded['persons']:
            emailContact = str(contact['email'])
            emailSend = pipe('example@gmail.com', 'password')
            print(getgeneratorstate(emailSend))
            next(emailSend)
            print(getgeneratorstate(emailSend))
            emailSend.send([emailContact, results])
            print(getgeneratorstate(emailSend))
        emailSend.send([None,0])
        print(getgeneratorstate(emailSend))
        print(results) 

For three contacts, the output would look like this 

GEN_CREATED
GEN_SUSPENDED
GEN_SUSPENDED
GEN_CREATED
GEN_SUSPENDED
GEN_SUSPENDED
GEN_CREATED
GEN_SUSPENDED
GEN_SUSPENDED

GEN_SUSPENDED

As you can see a pipe instance has been created (and suspended) three times, once for each contact.