Text and Templates

Often the job of output is not about individual text lines, but about creating multi-line files such as scripts and reports. This often leads away from standard output mechanisms toward template packages, but say has you covered here as well.

from say import Text

# assume `hostname` and `filepath` already defined

script = Text()
script += """

    # Output the results of a ping command to the given file

    ping {hostname!r} >{filepath!r}


Then script.sh will contain:


# Output the results of a ping command to the given file

ping 'server1234.example.com' >'ping-results.txt'

Text objects are basically a list of text lines. In most cases, when you add text (either as multi-line strings or lists of strings), Text will automatically interpolate variables the same way say does. One can simply print or say Text objects, as their str() value is the full text you would assume. Text objects have both text and lines properties which can be either accessed or assigned to.

+= incremental assignment automatically removes blank starting and ending lines, and any whitespace prefix that is common to all of the lines (i.e. it will dedent any given text). This ensures you don’t need to give up nice Python program formatting just to include a template.

While += is a handy way of incrementally building text, it isn’t strictly necessary in the simple example above; the Text(...) constructor itself accepts a string or set of lines.

Other in-place operators are: |= for adding text while preserving leading white space (no dedent) and &= adds text verbatim–without dedent or string interpolation.

One can read_from() a file (appending the contents of the file to the given text object, with optional interpolation and dedenting). One can also write_to() a file. Use the append flag if you wish to add to rather than overwrite the file of a given name, and you can set an output encoding if you like (encoding='utf-8' is the default).

So far we’ve discussed Text objects almost like strings, but they also act as lists of individual lines (strings). They are, for example, indexable via [], and they are iterable. Their len() is the number of lines they contain. One can append() or extend() them with one or multiple strings, respectively. append() takes a keyword parameter interpolate that controls whether {} expressions in the string are interpolated. extend() additionally takes a dedent flag that, if true, will automatically remove blank starting and ending lines, and any whitespace prefix that is common to all of the lines.

If t is a Text instance, str(t) will be the full string representing it. If you wish to move from multiple lines to a single-line, joined string, ' '.join(t) does the trick.

Text objects, unlike strings, are mutable. The replace(x, y) method will replace all instances of x with y in situ. If given just one argument, a dict, all the keys will be replaced with their corresponding values.

Text doesn’t have the full set of text-onboarding options seen in textdata, but it should suit many circumstances. If you need more, textdata can be used alongside Text.

Finally, it’s possible to use a Text object like a file and write to it. So:

t = Text()
say.set(files=[sys.stdout, t])


will now append each thing said to both sys.stdout and t.

There is a related class Template that does not interpolate its format variables when constructed, but rather when explicitly rendered. This suits certain form-filling operations:

t = Template("Dear {name},\n\nWelcome to our club!\n")
for name in 'Joe Jane Jeremey'.split():
    print t.render()