Snippets.py un module pour générer des bouts de code

En ces temps de confinement, voici une commande snippets.py qui permet d’interpréter des fichiers texte combinant explications et bouts de code Python.
Voir le docstring pour la syntaxe très simple des fichiers snippets. Exécutez

./snippets.py -

pour lancer l’interprétation.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
snippets combiner du texte libre avec des snippets python

Les fichiers d'entrée sont des fichiers texte incluant des bouts de code
Python terminés par une ligne de la forme `### n` où n est le nombre de lignes
du code Python qui précède ce marqueur

Le hello world d'un fichier snippets

print("hello", "snippets")
### 1

exemple de boucle sur un triplet
triplet = 1, 2, 3
for v in triplet:
    print(v)
### 3

Python c'est super, n'est-ce pas ?
Python = True
great = True
it = False
res = Python is great is not it
print("résultat", res)
### 5

Les lignes d'explication (hors bout de code) sont imprimées telle que.
Pour un bout de code (snippet), le programme imprime le code et les résultats
de son évaluation par l'interpréteur Python, le tout encadré par les chaînes
`begin_code`et `end_code`.

try:
    import aujourd
    now = aujourd.hui
except ImportError:
    from datetime import date
    now = date.today()
print("C'est fini pour aujourd'hui", now)
### 7

Au revoir!
"""

from itertools import groupby

_marker = '###'

def n_marker(line, marker=None):
    """retourne
    0 si la ligne ne commence pas par "###"
    n si la ligne est de la forme '### n'
    1 si le nombre n n'est pas indiqué
    """
    if marker is None:
        marker = _marker
    if not line.startswith(marker):
        return 0
    n = 1
    nc = len(marker)
    line = line[nc:]
    words = line.split()
    if words:
        n = int(words[0])
    return n

def drop_trailing(lines):
    """drop newlines and trailing spaces in iterable `lines`
    """
    for line in lines:
        yield line.rstrip()


def decoupe(lines, marker=None):
    """generator function which yields all lines outside of a snippet
    snippet code yields a single code line `marker + '\n'.join(code_part)`
    """
    if marker is None:
        marker = _marker
    # lines = iter(lines)
    lines = drop_trailing(lines)
    bloc = []
    for upto, g in groupby(lines, n_marker):
        if upto:
            if not bloc:
                print("successive marker lines", marker, "upto", upto)
                continue
            for line in bloc[:-upto]:
                yield line
            code_part = bloc[-upto:]
            code = marker + '\n'.join(code_part)
            yield code
            bloc = []
        else:
            bloc.extend(g)
            # all the lines upto the marker line excluded

    if bloc:  # trailing bloc after last snippet
        for line in bloc:
            yield line


def _test_decoupe(lines=None, marker=None, code_begin='# code'):
    """imprime la sortie de decoupe
    """
    if not lines:
        lines = __doc__.splitlines()
        if not lines[-1]:
            lines.pop()
    if marker is None:
        marker = _marker
    nc = len(marker)
    for line in decoupe(lines, marker):
        if not line.startswith(marker):
            print(line)
        else:
            if code_begin:
                print(code_begin)
            print(line[nc:])

def with_snippets(lines, marker=None, begin_code="```python", end_code="```"):
    """Process a snippet iterable
    `lines` iterable au format snippets avec des marqueurs '### [n]'
    """
    if marker is None:
        marker = _marker
    nc = len(marker)
    for line in decoupe(lines, marker):
        if not line.startswith(marker):
            print(line)
        else:
            if begin_code:
                print(begin_code)
            line = line[nc:]
            print(line)
            cod = compile(line, "<stdin>", "exec")
            eval(cod, globals())
            if end_code:
                print(end_code)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="""interpréter de fichiers texte contenant des snippets \
 de code python terminés par une ligne `marqueur` N""",
        epilog="""\
Si le seul fichier donné est '-', %(prog)s interprète snippets.__doc__ \n
Pour lister le code brut de snippets.__doc__ tapez \n
  "%(prog)s --marker '#NOT_FOUND' -"
""")
    parser.add_argument(
        "--marker", dest="marker", default="###",
        help="marqueur de fin de snippet" " '###' par défaut")
    parser.add_argument(
        "--begin_code", dest="begin_code", default="```python",
        help="balise de début de snippet," " '```python' par défaut")
    parser.add_argument(
        "--end_code", dest="end_code", default="```",
        help="balise de fin de snippet," " '```' par défaut")
    parser.add_argument(
        "fnames", nargs="+", metavar="fname", type=str,
        help="Nom de fichier à interprèter")
    args = parser.parse_args()
    _marker = args.marker

    fnames = args.fnames

    if fnames and fnames[0] == '-':
        del fnames[0] # compensate argparse behaviour
    if not fnames:
        g_lines = __doc__.splitlines()
        with_snippets(g_lines, begin_code=args.begin_code, end_code=args.end_code)
        # _test_decoupe()
    else:
        for fname in fnames:
            if len(fnames) > 1:
                print("Fichier", fname, '\n')
            with open(fname) as fin:
                with_snippets(fin, begin_code=args.begin_code, end_code=args.end_code)

# vim: set shiftwidth=4 softtabstop=4 expandtab tabstop=4:

Pour les curieux ce module n’utilise que l’interpréteur Python et la fonction groupby de itertools.

Cordialement,
Regards,
Mit freundlichen Grüßen,
مع تحياتي الخالصة


F. Petitjean
Ingénieur civil du Génie Maritime.

« Moi, lorsque je n’ai rien à dire, je veux qu’on le sache. » (R. Devos)

« Celui qui, parti de rien, n’est arrivé nulle part n’a de merci à dire à personne !! »
Pierre Dac