execute Python within ConTeXt

There's a quick and dirty solution using buffers. Some remarks:

  • I've defined (1) \startpython ... \stoppython to process Python content immediately, (2) \processpythonbuffer to process a buffer as a Python file with print functions, and (3) \processpythonfile to do the same as (1), but using a file instead of a buffer. (1) serves to get the result ipso facto, whereas the last ones are useful to delay or reuse a Python piece of code. I realized you asked for the first, so better late...
  • For buffers I'm creating a temporary file named ctxpythonbuffer.py which is executed and then removed. Unless you have a file with such a name, you won't have any problem. I'd use \savebuffer, but it seems to be slower.
  • Both for buffers and files I'm using io.popen to capture command line output. I don't know if Python has bindings to Lua or vice versa, but you could do further research. Btw if you want to go deeper, you may find Luigi Scarso's experiment, LuaTeX lunatic, interesting. Details here and here. I won't, as I don't like Python.
  • python3 is hardcoded, but you may change it to python, python2 or whatever command your OS recognizes as a valid call to Python. If you want to add, say, a switch between versions or other settings, CLD manual is a nice starting point to write your own commands via Lua. Bonus: you learn another programming language, if you haven't embraced Lua yet ;)
#abc.py
#Dummy file
print("a")
print("b")
print("c")
%\setuppapersize[A6]
\startluacode

userdata = userdata or {}
local implement = interfaces.implement
local suffix = ".py"

--Adapted from:
--https://gist.github.com/dukeofgaming/453cf950abd99c3dc8fc
local pythonbuffer = 
function(file)
    local handle = assert(io.popen(string.format("python3 %s", file), 'r'))
    local output = assert(handle:read('*a'))
    handle:close()
    return output
end

userdata.processpythonbuffer =
function (content) 
    local name = "ctxpythonbuffer.py"
    io.savedata(name,content)
    result = pythonbuffer(name)
    os.remove(name)
    return result
end

userdata.processpythonfile =
function (name)
    assert(name ~= "", "File name needed")
    name = name:find(suffix.."$") and name or name..suffix
    return pythonbuffer(name)
end

implement{
    name        = "processpythonbuffer",
    public      = true,
    arguments   = {"string"},
    actions     = function(s)
        context(userdata.processpythonbuffer(buffers.getcontent(s)))
    end
}

implement{
    name        = "processpythonfile",
    public      = true,
    arguments = {"string"},
    actions     = function(s)
        context(userdata.processpythonfile(s))
    end
}

\stopluacode
%Buffer name isn't really important
%You could use another, less verbose...
\def\startpython%
    {\def\stoppython{\processpythonbuffer{ctx_python_buffer}}%
    \dostartbuffer[ctx_python_buffer][startpython][stoppython]}
\starttext
\startimath
\startpython
import math
print(math.cos(3.141592))
\stoppython
\stopimath

\startpython
Sentence1 = "{fmt} is {adj}".format(fmt="Con\\TeX t", adj="great")
print(Sentence1)
\stoppython

%https://www.programiz.com/python-programming/examples
\startbuffer[hcf]
# Python program to find H.C.F of two numbers

# define a function
def compute_hcf(x, y):

# choose the smaller number
    if x > y:
        smaller = y
    else:
        smaller = x
    for i in range(1, smaller+1):
        if((x % i == 0) and (y % i == 0)):
            hcf = i 
    return hcf

num1 = 54 
num2 = 24

print("The H.C.F. is", compute_hcf(num1, num2))
\stopbuffer

\startbuffer[powersof2]
# Display the powers of 2 using anonymous function

terms = 10

# Uncomment code below to take input from the user
# terms = int(input("How many terms? "))

# use anonymous function
result = list(map(lambda x: 2 ** x, range(terms)))

print("The total terms are:",terms)
for i in range(terms):
   print("2 raised to power",i,"is",result[i])
\stopbuffer

%Adapted from
%https://www.w3schools.com/python/
\startbuffer[anotherpython]
b = "I prefer Lua over Python"
print(b[9:])
a = ("d", "e", "k", "n", "u", "t", "h")
x = slice(2)
print(a[x])
\stopbuffer
\processpythonbuffer{hcf}

\processpythonbuffer{anotherpython}

\processpythonfile{abc}

%\startlines ... \stoplines is the rough equivalent of Plain/LaTeX \obeylines
\startlines
\processpythonbuffer{powersof2}
\stoplines

\stoptext

enter image description here


For comparison, here is how you would implement something similar to Jairo's solution using the t-filter module.

\usemodule[filter]
\defineexternalfilter
  [python]
  [
    filtercommand={python \externalfilterinputfile\space > \externalfilteroutputfile},
    output={\externalfilterbasefile.tex},
    cache=yes,
    % directory=temp, if you want to redirect all temp files to a subdir
  ]

Yup, that's it! The option cache=yes caches the results, so python is rerun only if the content of the buffer or environment has changed. You can also store the output in a temp directory so that the results are out of sight. See the documentation of the filter module for other features (including proper XML export!)

The t-filter module writes everything to external files and as such might be slightly slower that the pipe.io method proposed by Jario.

The example given by Jairo works with a little change: \process<filter>buffer provided by the filter module uses square brackets instead of curly brackets. For completeness, here is the complete example:

\usemodule[filter]
\defineexternalfilter
  [python]
  [
    filtercommand={python \externalfilterinputfile\space > \externalfilteroutputfile},
    output={\externalfilterbasefile.tex},
    cache=yes,
    % directory=temp, if you want to redirect all temp files to a subdir
  ]


\starttext
\startimath
\startpython
import math
print(math.cos(3.141592))
\stoppython
\stopimath

\startpython
Sentence1 = "{fmt} is {adj}".format(fmt="Con\\TeX t", adj="great")
print(Sentence1)
\stoppython

%https://www.programiz.com/python-programming/examples
\startbuffer[hcf]
# Python program to find H.C.F of two numbers

# define a function
def compute_hcf(x, y):

# choose the smaller number
    if x > y:
        smaller = y
    else:
        smaller = x
    for i in range(1, smaller+1):
        if((x % i == 0) and (y % i == 0)):
            hcf = i 
    return hcf

num1 = 54 
num2 = 24

print("The H.C.F. is", compute_hcf(num1, num2))
\stopbuffer

\startbuffer[powersof2]
# Display the powers of 2 using anonymous function

terms = 10

# Uncomment code below to take input from the user
# terms = int(input("How many terms? "))

# use anonymous function
result = list(map(lambda x: 2 ** x, range(terms)))

print("The total terms are:",terms)
for i in range(terms):
   print("2 raised to power",i,"is",result[i])
\stopbuffer

%Adapted from
%https://www.w3schools.com/python/
\startbuffer[anotherpython]
b = "I prefer Lua over Python"
print(b[9:])
a = ("d", "e", "k", "n", "u", "t", "h")
x = slice(2)
print(a[x])
\stopbuffer
\processpythonbuffer[hcf]

\processpythonbuffer[anotherpython]

\processpythonfile{abc.py}

\startlines
\processpythonbuffer[powersof2]
\stoplines

\stoptext

Tags:

Python

Context