talks/2023-pyconfr-sphinx-lint.md

9.0 KiB
Raw Permalink Blame History

sphinx-lint

Un linter pour ta doc.


Julien Palard

Julien Palard

  • CPython core dev
  • Sysadmin à lAFPy
  • hackinscience.org

Notes:

  • Je moccupe surtout de docs.python.org
  • Formateur, indépendant, faites passer le mot :)

Pourquoi ?

Notes:

Trouver des fautes de frappe dans du rst, ni plus, ni moins.

Depuis quand ?

Author: Georg Brandl <georg@python.org>
Date:   Sat Jan 3 20:30:15 2009 +0000

Dans cpython dans Doc/tools/rstlint.py !

Notes:

Oui oui, Georg, l'auteur de Sphinx-doc.

La motivation

make suspicious

Bien connu pour ses faux positifs, n'est-ce pas ?

Notes:

Lui il date du portage de la doc de LaTeX à Sphinx, il était là pour repérer les erreurs de portage. Il date donc de 2009 aussi.

La motivation

Remplacer make suspicious par un outil qui naurait aucun faux positif.

Notes:

  • Aucun faux positif == cool pour les contributeurs !
  • Et qui serait rapide !
  • Parler de la lenteur de make suspicious.

La motivation

Sortir rstlint.py de cpython/Doc/tools pour que tout le monde puisse l'utiliser.

Notes: Et pour faciliter la contribution.

La motivation

Parsons du rst !

Le reStructuredText

Le balisage :

*En italique*
**En gras**
`Interprété` (dont le rôle peut changer)
``litéral``
Lien_
`Autre lien`_
`Encore un lien <https://...>`_
`Un lien anonyme <https://...>`__

Notes: Comme en Markdown, comme en Org.

Le reStructuredText

Les textes interprétés :

`while`  C'est du texte interprété.
:keyword:`while`  Du texte interprété avec un « role marker »
`while`:keyword:  Ça lui donne un sens particulier.

Le reStructuredText

Donc oui le balisage et le texte interprété peuvent jouer le même rôle :

*ça* c'est pareil que :emphasis:`ça`.
**ça** c'est pareil que :strong:`ça`.
``ça`` c'est pareil que :literal:`ça`.

Notes:

Mais comme en Python on a plus de méthodes que d'opérateurs…

En rst on a plus de rôles que de balises !

Le reStructuredText

Les directives :

 .. image:: Lenna.jpg

 .. important::
    Elle sappelle *Lena Sjööblom*.

Notes:

Je prends un gros raccourci : les directives ne sont qu'un bout de tout le balisage "explicite" comme les commentaires, les notes de bas de page, ...

Retenez-bien que le corps d'une directive peut contenir du reStructuredText !

Parsons du rst !

Jusque-là, cest facile !

Parsons du rst !

Maintenant, parsez ça, avec vos yeux :

*2 * x  *a **b *.txt*

Notes:

C'est tout en italique !

Parsons du rst !

Vous voyez du balisage, vous, ici ?

2 * x ** 2 + 3 * x ** 3

a || b

"*" '|' (*) [*] {*} <*> *

2*x a**b O(N**2) e**(x*y)

Notes:

Pas le parseur. Mais bon les règles sont bien documentées ♥

Mais on voit que les humains peuvent facilement faire des erreurs ici.

Parsons du rst !

Regardons du côté des directives :

.. code-block:: python

   def fib(n):
       if n in (0, 1):
           return 1
       x = n // 2
       return fib(x-1) * fib(n-x-1) + fib(x) * fib(n-x)

Notes:

Une directive peut avoir un corps, ici du code.

Parsons du rst !

Regardons du côté des directives :

.. image:: Lenna.jpg
   :height: 330px
   :width: 330px
   :scale: 50 %
   :alt: Lenna Sjööblom

Notes:

Une directive peut avoir des options mais pas de corps.

Parsons du rst !

Regardons du côté des directives :

.. csv-table:: Domaines d'organismes publics
   :header: "name", "http_status", "https_status"
   :widths: 20, 10, 10

   cgf.pf, 200, 200
   dun.fr, 200, 200
   caf.pf, 200, 200
   aze.fr, 200, 200

Notes:

Une directive peut avoir des options et un corps.

Quiz !

.. code-block:: python

   items = ["titi", "tata", "toto"]
   print(*items)


.. important::

   items = ["titi", "tata", "toto"]
   print(*items)

Quiz !

.. csv-table:: Domaines d'organismes publics
   :header: "name", "http_status", "https_status"
   :widths: 20, 10, 10

   cgf.pf, 200, 200, OK
   dun.fr, 200, 200, OK

.. csv-table:: Domaines d'organismes publics
   :header: "name", "http_status", "https_status"
   :widths: 20, 10, 10

   caf.pf
   aze.fr

Notes:

Les valeurs manquantes sont moins graves que les valeurs surnuméraires.

Ne parsons pas de rst...

De toutes façons sphinx-lint ne cherche pas du rst valide, il cherche du rst invalide !

Mes amies les regex

SIMPLENAME = r"(?:(?!_)\w)+(?:[-._+:](?:(?!_)\w)+)*"
ROLE_GLUED_WITH_WORD_RE = re.compile(
    fr"(^|\s)(?<!:){SIMPLENAME}:`(?!`)")
ROLE_WITH_NO_BACKTICKS_RE = re.compile(
    fr"(^|\s):{SIMPLENAME}:(?![`:])[^\s`]+(\s|$)")
ROLE_MISSING_RIGHT_COLON_RE = re.compile(
    fr"(^|\s):{SIMPLENAME}`(?!`)")
ROLE_MISSING_CLOSING_BACKTICK_RE = re.compile(
    fr"({ROLE_HEAD}`[^`]+?)[^`]*$")

Notes: Parfois lisibles.

Mes amies les regex

ASCII_BEFORE = r"""[-:/'"<(\[{]"""
UNICODE_BEFORE = r"[\p{Ps}\p{Pi}\p{Pf}\p{Pd}\p{Po}]"
ASCII_AFTER = r"""[-.,:;!?/'")\]}>]"""
UNICODE_AFTER = r"[\p{Pe}\p{Pi}\p{Pf}\p{Pd}\p{Po}]"
re.compile(
    fr"(?<!\x00)(?<=^|\s|{ASCII_BEFORE}|{UNICODE_BEFORE})"
    fr"(?P<inline_markup>{start_string}\S{QUOTE_PAIRS_NLB}"
    fr".*?(?<=\S){end_string})"
    fr"(?=$|\s|\x00|{ASCII_AFTER}|{UNICODE_AFTER})"
)

Notes: Parfois pas. Mais vous connaissez re.VERBOSE ?

Mes amies les regex

re.compile(
    fr"""
(?<!\x00) # Both inline markup start-string and end-string
          # must not be preceded by an unescaped backslash
(?<=             # Inline markup start-strings must:
    ^|           # start a text block
    \s|          # or be immediately preceded by whitespace,
    {ASCII_BEFORE}|  # one of the ASCII characters or a
    {UNICODE_BEFORE} # similar non-ASCII punctuation char.
)
(?P<inline_markup>
    {start_string} # Inline markup start
    \S             # Inline markup start-strings must be
                   # immediately followed by non-whitespace.
                   # The inline markup end-string must be
                   # separated by at least one character
                   # from the start-string.
    {QUOTE_PAIRS_NEGATIVE_LOOKBEHIND}
    .*?
    (?<=\S)       # Inline markup end-strings must be
                  # immediately preceded by non-whitespace.
    {end_string}  # Inline markup end
)

(?=       # Inline markup end-strings must
    $|    # end a text block or
    \s|   # be immediately followed by whitespace,
    \x00|
    {ASCII_AFTER}|  # one of the ASCII characters or a
    {UNICODE_AFTER} # similar non-ASCII punctuation char.
)
""",
    flags=re.VERBOSE | re.DOTALL,
)

Notes: Les commentaires sont un copié-collé de la spec !

Cest utilisé ?

  • CPython (forcément…), devguide, peps, traductions, ...
  • neo4j-python-driver
  • pandas
  • Spyder IDE
  • Sympy
  • Sphinx !!!

Ça trouve des bugs ?

Dans les PEPS

-`vcs <https://pythonhosted.org/vcs/>` library as
+`vcs <https://pythonhosted.org/vcs/>`_ library as

Dans sympy

-... autoclass:: sympy.assumptions.predicates.order
+.. autoclass:: sympy.assumptions.predicates.order
-:data:``sympy.parsing.sympy_parser.transformations``
+:data:`sympy.parsing.sympy_parser.transformations`
-  `KanesMethod`` objects.
+  ``KanesMethod`` objects.

Dans la traduction

I just run the tool against library/ as a whole and it spit out a loooot of messages, so I thought that maybe it didn't work with our po files.

Dans la traduction

After closer inspecting two of these files I found that they were indeed proper errors

-"datetime`. :const:`MINYEAR` es` `1``."
+"datetime`. :const:`MINYEAR` es ``1``."

Notes:

Ils ont réparé plus de 300 erreurs !

Et les perfs ?

$ time make suspicious
real    1m10.416s
user    3m28.918s
sys     0m3.127s
$ time make check  # (c'est sphinx-lint derrière)
real    0m6.351s
user    0m43.478s
sys     0m0.227s

Et les faux positifs ?

  • make suspicious → 400
  • make checks → 0

Et les faux positifs ?

Ils se cachent bien :

  • Aucun dans la doc de Sphinx, cest un bon test (beaucoup de rst dans du rst).
  • Aucun dans la doc de Python, cest un bon test (286k lignes de rst).

Et make suspicious ?

OK, vendu !

Mais comment ça sutilise ?

$ python -m pip install sphinx-lint
$ sphinx-lint

Bouh cest pas packagé pour [distrib]

Bah, fais-le.

Notes: Pour Debian je veux bien le faire ;)

Questions ?