imprimer du code Python

Introduction

Le but : imprimer de façon lisible le code Python des partitions de musique électronique en ayant accès aux fontes cmtt, cmbtt, cmsltt de TeX.

Le système d'édition Latex comporte quelques paquetages pour aider à la mise en page de code informatique. Ce qui nous intéresse ici est d'éditer du code Python qui comporte:

Je m'étonne que Latex - qui est un outil destiné surtout aux scientifiques - ne comporte aucun paquetage dont l'usage permettrait d'appliquer simplement, confortablement, ces exigences pourtant limitées.

Il y a bien dans «Le petit livre de TeX» de Raymond Séroul (chap. 13) les sections Typographie d'un programme et Une macro programme élaborée, mais c'est très complexe, car le tout est écrit en TeX. Une autre solution consisterait à écrire un python qui écrit du TeX.

Plusieurs obstacles :


Le code

#!/usr/bin/python
#-*-coding:utf-8-*-
# py2tex.py

"""
Fonctions et classes pour imprimer les partitions: 
c.-à-d. mettre en forme les programmes, les pythons, en mettant 
- en gras les mots réservés,
- en italiques les informations et documentations.

Selon le choix, on obtient une version PDF(r) ou PostScript(r) 
en passant par une version en 'plain tex'.
TeX me semble le mieux adapté car les caractères tt sont plus
agréables que les Helvetica disponibles en PS de base.

Comme d'habitude, le python n'est pas destiné à être utilisé de
façon fermée, mais est destiné à des utilisateurs du langage Python
qui essaieront de l'adapter à leurs besoins personnels, d'où
l'absence d'options de lancement.

On initialise une classe avec un grand titre, un sous-titre, le nom
de l'auteur, puis le titre et le nom d'auteur à rappeler sur chaque page,
une mention de copyright à porter sur la première page.

Cette classe prépare une fichier <nom>.tex à partir du <nom>.py

ECART1 = 3 : écart entre le grand titre de la première ligne et le
           soustitre de la 2e ligne
ECART2 = 5 : écart entre la ligne de sous-titre et la barre horizontale
ECART3 = 8 : écart entre la barre horizontale et le début du texte
"""
import keyword as _k
import re

__author__ = """Copyright (c) 2009 Raymond Séroul et 
René Bastian (Wissembourg, FRANCE)"""
__date__ = "2009.12.25"

ECART1 = 3
ECART2 = 5
ECART3 = 8

def nonascii(texte):
    """transcrit les caractères non ascii en TeX - 
    tout n'est pas testé."""
    dicononascii = {
        "ç" : "\\c c",        
        "Ç" : "\\c C",
        "é" : "\\'e",
        "É" : "\\'E",
        "è" : "\\`e",
        "à" : "\\`a",

        "è" : "\\`e",
        "ì" : "\\`i",
        "ò" : "\\`o",
        "ù" : "\\`u",

        "â" : "\\^a",
        "ê" : "\\^e",
        "î" : "\\^i",
        "ô" : "\\^o",
        "û" : "\\^u",

        "ä" : '\\"a',
        "ë" : '\\"e',
        "ä" : '\\"a',
        "ü" : '\\"u',
        "ö" : '\\"o',
                        
        "Â" : "\\^A",
        "Ê" : "\\^E",
        "Î" : "\\^I",
        "Ô" : "\\^O",
        "Û" : "\\^U",

        "Ä" : '\\"A',
        "Ë" : '\\"E',
        "Ï" : '\\"I',
        "Ö" : '\\"O',
        "Ü" : '\\"U',
        
        "À" : "\\`A",
        "È" : "\\`E",
        "Ì" : "\\`I",
        "Ò" : "\\`O",
        "Ù" : "\\`U",
        
        "ÿ" : '\\"y',
        "Ÿ" : '\\"Y'
        }

    for acc in dicononascii.keys():
        texte = texte.replace(acc, dicononascii[acc])
    return texte

def fairetitrepython(titre, stitre, date, auteur):
    """fabrique le titre en haut d'un python;
    fonction à adapter à ce qu'on veut
    """
    p = " ".join(["\line{\\ftitre ", titre, "}\\vskip", str(ECART1), "mm"])
    p += " ".join(["\line{\\fstitre ", stitre, "-", date, "\hfill", 
                   auteur, "}"])
    p += " ".join(["\\bigskip\hrule\\bigskip\n"])
    return p

def preparer(texte, eliminer=False):
    """rassemble différentes fonctions pour avoir un
    texte texifié.
    L'ordre est important à cause de collisions."""
    # le texte est formaté, mais il faut se débarasser de l'antislash
    texte = eliminercomments(texte)
    texte = enleverlignesvides(texte)
    texte = antislash(texte)
    texte = formater(texte)    
    # on change les caractères
    texte = specials(texte)
    texte = nonascii(texte)
    texte = italiserstrings(texte, eliminer)
    texte = traitermotsreserves(texte)
    texte = blancsinitials(texte)
    return texte

def antislash(texte):
    "traite les \ "
    return texte.replace("\\", "\\char`\\\\")
 
def specials(texte):
    """change les caractères spéciaux de TeX
    Il faut encore tester les accolades !!.
    À traiter avec char ?"""
    speciaux = ["_", "&", "%", "$", "#", "^", "~"]
    dico = {
        "$" : "\$",
        "#" : "\#",
        "^" : "\^",
        "_" : "\_",
        "&" : "\&",
        "~" : "\~",
        "%" : "\%"
        }
    for c in speciaux:
        texte = texte.replace(c, dico[c])
    return texte

def motsreservesrs(ligne):
    """mettre en gras les mots réservés; version RS"""
    mots = ligne.split(" ")
    r = []
    for mot in mots:
        if mot in _k.kwlist:
            r.append("\\\\"+ mot)
        elif mot in  ["else:", "try:", "finally:", "raise:",
                      "else:\n", "try:\n", "finally:\n", "raise:\n"]:
            r.append("\\\\"+ mot[:-1] + ":")
        else:
            r.append(mot)
    return " ".join(r)

def motsreserves(ligne):
    """mettre en gras les mots réservés"""
    mots = ligne.split(" ")
    r = []
    for mot in mots:
        if mot in _k.kwlist:
            r.append("{\\fgras{}"+ mot + "}")
        elif mot in  ["else:", "try:", "finally:", "raise:",
                      "else:\n", "try:\n", "finally:\n", "raise:\n"]:
            r.append("{\\fgras{}"+ mot[:-1] + "}:")
        else:
            r.append(mot)
    return " ".join(r)

def traitermotsreserves(texte):
    "traite les mots réservés de Python"
    c = texte.split("\n")
    r = []
    for l in c:
        r.append(motsreserves(l))
    return "\n".join(r)

def eliminercomments(texte):
    """ enlever les commentaires """
    lignes = texte.split("\n")
    gr = [ligne.split("#")[0] for ligne in lignes]
    return "\n".join(gr)

def enleverlignesvides(texte):
    " enlève les lignes vides ou blanches "
    lignes = texte.split("\n")
    r = []
    for ligne in lignes:
        if ligne.isspace():
            pass
        else:
            r.append(ligne)
    s = "\n".join(r)
    while s.count("\n\n") > 0:
        s = s.replace("\n\n", "\n")
    return s

def blancinitial(ligne):
    "traite les blancs des débuts de ligne"
    i = 0
    r = ""
    try:
        while ligne[i] == " ":
            r += "\ "
            i += 1
    except IndexError:
        pass
    r += ligne[i:]
    r = str(r)
    return r

def blancsinitials(texte):
    """ trabscrit les blancs initiaux """
    coll = texte.split("\n")
    r = []
    for ligne in coll:
        if len(ligne) == 0:
            pass
        elif ligne[0] == " ":
            r.append(blancinitial(ligne))
        else:
            r.append(ligne)
    return "\n".join(r)

def formater(texte):
    """ajoute une ligne vide avant les classes et les fonctions ou méthodes."""
    saut = "\n\saut\n"
    c = texte.split("\n")
    r = []
    for a in c:
        if a[:6] == "class ": 
            r.append(saut + a)
        elif a[:4] == "def ":
            r.append(saut + a)
        elif a[:11] == "if __name__":
            r.append(saut + a)
        elif a[:8] == "    def ":
            if a[:14] == "    def __init":
                r.append(a)
            else:
                r.append(saut + a)
        elif a[:10] == "__author__":
            r.append("\n" + a)
        else:
            r.append(a)
    return "\n".join(r)

def italiserstrings(texte, eliminer=False):
    """toutes les strings sont imprimées en italique"""
    chlong = re.compile(r'""".+?"""', re.DOTALL)
    chbref = re.compile(r'".+?"', re.DOTALL)
    stexte = texte.replace('"""', '&&&')
    stexte = stexte.replace('""', '$$')
    motifs = chbref.finditer(stexte)
    r = []
    px = 0
    for x in motifs:
        p0, p1 = x.span()
        r.append(stexte[px:p0])
        r.append('{\\fital{}' + stexte[p0:p1] + '}')
        
        px = p1
    r.append(stexte[px:])
    stexte = "".join(r)

    stexte = stexte.replace('$$', '""')
    stexte = stexte.replace('&&&', '"""')
    motifs = chlong.finditer(stexte)
    r = []
    px = 0
    for x in motifs:
        p0, p1 = x.span()
        r.append(stexte[px:p0]
        if eliminer:
            r.append('{\\fital{} xxx }')
        else:
            r.append('{\\fital{}' + stexte[p0:p1] + '}')
        px = p1
    r.append(stexte[px:])    
    return "".join(r)
        
class Texifieur(object):
    "classe pour texifier un python"
    def __init__(self, gtitre, stitre, auteur, titreh="", 
                 auteurh="", acopyright=""):
        """
        gtitre : titre qu'on veut donner au texte
        stitre : sous-titre ...
        auteur : auteur
        titreh, auteurh : titre et auteur utilisés sur le haut de chaque page
          s'ils doivent être différents de gtitre et auteur
          (n'est pas encore utilisable)
        acopyright : mention qui n'est pas encore utilisée
        
        Pour régler l'espacement vertical des hauts de page, on peut intervenir sur les
        medskip et bigskip dans self.prelude.
        """
        self.gtitre = gtitre
        self.stitre = stitre
        self.auteur = auteur
        self.titreh = titreh
        self.auteurh = auteurh
        self.copyright = acopyright
        self.npage = None
        self.prelude = """
\\newif\ifpagetitre \pagetitretrue
\\newtoks\hautpagetitre \hautpagetitre={\hfil}
\\newtoks\\baspagetitre \\baspagetitre={\hfil}
\\newtoks\\auteurcourant \\auteurcourant={\hfil}
\\newtoks\\titrecourant \\titrecourant={\hfil}
\\newtoks\hautpagegauche \\newtoks\hautpagedroite
\\newtoks\\baspagegauche \\newtoks\\baspagedroite
\\topskip=20pt
\hsize=160mm \\vsize=250mm % ADAPTER À L'IMPRIMANTE
\leftskip=-5mm % au lieu de -5

\\font\\fgras=cmbtt10 % at 32pt % GRAS DES MOTS RÉSERVÉS

\\font\\fitalfont=cmsltt10 % at 10pt % ITALIQUE DES TEXTES
\def \\fital {\\fitalfont \\baselineskip=11pt} % OK 

\\font\\ftitrefont=cmsltt10 at 24pt % TITRE au lieu de 16
\\def \\ftitre {\\ftitrefont  \\baselineskip=20pt} % OK 

\\font\\fstitrefont=cmsltt10 at 12pt % SOUS-TITRE
\def \\fstitre {\\fstitrefont \\baselineskip=15pt} % OK 

\\font\\fauteurfont=cmsltt10 at 12pt % SOUS-TITRE
\def \\fauteur {\\fauteurfont \\baselineskip=15pt} % OK 

\\font\\fgrasgrosfont=cmbtt10 at 32pt % TITRE DE PREMIÈRE PAGE
\def \\fgrasgros {\\fgrasgrosfont \\baselineskip=36pt} % OK 

\\font\\ttfont=cmtt10 %at 32pt % TITRE DE PREMIÈRE PAGE
\def \\tt {\\ttfont \\baselineskip=12pt} % OK 


\def\saut {\\vskip 3mm plus 20pt minus 20pt} % OK

\long \def\\begincode {\\begingroup
\\saut
\obeylines}
\def \endcode {\endgroup}

\hautpagegauche{\\vbox{\line{\\fstitre\\folio\hfill\\the\\titrecourant\hfill\\the\\auteurcourant}%
\medskip\hrule\\bigskip}}
\hautpagedroite{\\vbox{\line{\\the\\auteurcourant\hfill\\the\\titrecourant\hfill\\folio}%
\medskip\hrule\\bigskip}}

\\baspagetitre={\\vbox{\\vskip 10pt\\vbox{\hrule width 124mm}%
{\copyright\ \uppercase\expandafter{\\romannumeral 2010} Ren\\'e Bastian - 
Tous droits r\\'eserv\\'es pour tous pays}}} % je n'en ai pas besoin 

\\baspagetitre={}

\headline={\ifpagetitre\\the\hautpagetitre
\else\ifodd\pageno\\the\hautpagedroite
\else\\the\hautpagegauche\\fi\\fi}

\\footline={\ifpagetitre\\the\\baspagetitre
\global\pagetitrefalse
\else\ifodd\pageno\\the\\baspagedroite
\else\\the\\baspagegauche
\\fi\\fi}
"""
        self.postlude = "\\bye"

    def evt(self, texte, primal):
        "nouvelle version"
        texte = "".join(texte)
        if primal:
            R = [self.prelude]
        else:
            R = [] 
        if self.npage:
            R.append(self.npage)
        titrechapitre = """\pagetitretrue
 \hautpagetitre={\\vbox{\obeylines{\\ftitre %s\hfill}
 {\\fauteur %s \hfill Ren\\'e Bastian}
 \medskip\hrule\medskip 
}}""" % (self.gtitre, self.stitre)
        
        R.extend([
            titrechapitre,    
            "\\auteurcourant={\\fstitre " + self.auteur + "}\n",
            "\\titrecourant={\\fstitre " + self.gtitre + "}\n",
            "\n{\\tt\\begincode",        
             texte,
             "\n\endcode}\n",
             "\\vfill\eject\n"
             ])
        return "".join(R)

    def __str__(self):
        """ affiche presque tout."""
        try:
            n = max([len(str(x)) for x in self.__dict__])
            xformat = "%-" + str(n) + "s : %s \n"     
        except ValueError:
            print "Texifieur:__str__ (1)"
        R = [self.__class__.__name__+"\n"]
        try:
            for x in self.__dict__:
                s = xformat % (str(x), self.__dict__[x])
                R.append(s)
        except ValueError:
            print "Texifieur:__str__ (2)"
        return "".join(R)

    def numeropage(self, n):
        "change le numéro de la page"
        self.npage = "\pageno=" + str(n) + "\n"

    def usage(self):
        "usage de la classe"
        print self.__init__.__doc__

    def attributs(self):
        " affiche les attributs "
        print(self.gtitre,
              self.stitre,
              self.auteur,
              self.titreh, 
              self.auteurh,   
              self.copyright, 
              self.npage)      

Essayez de colorier tout ça ...

Ici on peut pomper le code du module py2tex.py

Il faut évidemment adapter ce code aux besoins particuliers : ajouter des caractères, des mots réservés, changer les avis de copyright, etc. car il n'est pas destiné à un public qui ne sait pas pythoner. Le code TeX qui en résulte est monstrueux. Qu'importe. Et selon ma propre expérience, il ne peut pas se texifier lui-même.

Le résultat

Voici un extrait d'un module imprimé :

pageCodePython


Sommaire

Imprimer du code Python
Évolution du projet pythoneon
Utiliser l'Analyse de Fourier
'A Primer on Scientific Programming with Python' de H.P. Langtangen

Valid XHTML 1.0 Transitional

Copyright 2010-11 (c) René Bastian - rbastian (arrobe) free.fr