Augmentant la qualitat de codi Python de l'equip amb pylint (i ChatGPT)
Published: 2023-06-12 08:09:35

## Desenvolupament modern en Python

Python ja no és el que era. Ja fa un temps que el desenvolupament modern amb Python involucra tot d'eines que ens ajuden a augmentar la qualitat del codi.

Ja de temps enrere, projectes de linting com `pylint`, i més recentment `ruff`, analitzen el codi de forma estàtica per a trobar-ne problemes.

Més recentment `mypy` ens ajuda a comprovar que el codi sigui correcte a traves de una declaració explícita dels tipus.

I per a reblar el clau, `pre-commit` ens dóna la forma d'automatitzar aquestes eines per a que s'executin de forma regular.

Totes aquestes eines resulten ser essencials en el dia a dia de l'equip de desenvolupament.

## El problema

Si teniu experiencia amb `mypy`, segur que us haureu trobat amb situacions on no es comporta com esperàveu. La temptació de afegi un `# ignore: typing` és gran, o també de fer servir `typing.cast` per a treure l'error. Al cap i a la fi, si no aconseguim acontentar a `mypy` no ens passarà el `pre-commit` .

```python
import typing

class A:
    def a():
        print('a')

class B(A):
    def b():
        print('b')

things: dict[str,A] = {'a':A(), 'b':B()}

b = things['b']
# Sabem que b és instancia de B, però mypy no

b.b()  # Això no li agrada a mypy

typing.cast(B, b).b()  # mypy accepta el canvi
```

Tot i que això acontenta a mypy, és molt perillós. I si la part del codi canvia i `b` acaba sent de la classe `A`? És possible que ens provoqui errors en temps d'execució, però en llocs poc evidents. Si en lloc de cridar directament `b.b()` ho passéssim a una altra funció, podria passar desapercebut durant molt de temps:

```python
from queue import Queue

cua: Queue[B] = Queue()

def fes_algo_mes_tard(b: B) -> None:
    cua.put(b)

b_b = typing.cast(B, b)  # mypy accepta el canvi
fes_algo_mes_tard(b_b)
```

Llavors no veurem l'error fins que l'element sigui consumit de la cua.

## Alternatives

L'alternativa més senzilla a `typing.cast` és `assert isinstance`:

```python
b = things['b']
# Sabem que b és instancia de B, però mypy no
assert isinstance(b,B)  # ara mypy sí que ho sap
b.b()  # Cap problema
```

Si us hi fixeu, el codi llegeix: "Jo, programador, us asseguro que sé que `b` és instància de `B`, i si no tinc raó, que salti una excepció!". Això ens impedeix que ens passi com en l'exemple anterior on l'error quedava sense detectar durant temps.

A més, en entorn de producció es poden desactivar els `assert`s, eliminant qualsevol penalització en l'eficiència.

Així i tot, això no funciona per tipus com `dict[str,B]`. Per a solucionar-ho podem usar `pydantic` i crear una comanda de substitució:

```python
from typing import cast
from pydantic import parse_obj_as

def assert_cast(expected_type: type[T], value: Any) -> T:
    """Drop-in replacement for typing.cast, but with runtime checks.
    If assert is disabled, this function does nothing.
    Checks that value is of type expected_type and returns it as such.
    this also works with arbitrary types such as Union, Pydanitc models, etc.
    Because of how Pydantic works, we can't prevent the check to use type coercion.
    This should be solved in Pydantic V2, in the future"""

    # If asserts are disabled, we don't want to do anything
    if not __debug__:
        return cast(T, value)

    try:
        parse_obj_as(expected_type, value)
    except ValueError as exc:
        raise AssertionError(
            f"Expected value of type {expected_type} but got {type(value)} instead"
        ) from exc

    return cast(T, value)
```

I ara substituïu `cast` per `assert_cast`, i així tindreu tots els beneficis de cast i assert alhora.

```python
assert_cast(dict[str,B],b_dict)
```

Nota: Recordeu que també podeu demanar a ChatGPT que generi tests unitaris per la funció. I recordeu-vos de revisar-los!

## Implantació

Un cop tenim identificat el problema en la nostra base de codi, cal trobar una forma perquè no torni a passar. Evidentment, se'ls pot explicar als membres de l'equip, però no podem confiar només en la memòria; tornarà a passar.

Caldria trobar la forma de detectar `typing.cast` en el pre-commit per a evitar que se'ns passi, però com? Doncs fem un plugin de `pylint`.

El problema és que fer un plugin de `pylint` té pinta de ser una tasca llarga i avorrida. I, pitjor de tot, ens convertiríem en experts en plugins de `pylint` i ens n'encarregarien constantment. Ningú ho vol, això.

Per tant, la millor alternativa és encarregar-ho a ChatGPT.

## ChatGPT

Perquè ChatGPT ens faci un plugin de `pylint` li ho hem d'encarregar:

> Please write a pylint plugin that warns about using `typing.cast` function anywhere in the code.

Per a tenir uns bons resultats, millor usem `GPT-4`, `GPT-3.5` al·lucina massa.

Al primer intent, ens generarà codi correcte, però que es deixa alguns casos. Per exemple, al principi només ens detectava quan es cridava `typing.cast` però no quan es cridava `cast`després de fer `from typing import cast`. La tècnica és anar encarregant-li al ChatGPT que afegeixi aquests casos.

Ajuda molt tenir diversos fitxers _canaris_ on afegim deliberadament `typing.cast` de diferents formes, per a comprovar que efectivament, el plugin els reconeix.

Un cop funcionant, ajuda molt fer un document en el mateix repositori on explica tot el raonament de per què es prohibeix l'ús de `typing.cast` i referir-s'hi en el missatge del warning. D'aquesta forma tothom podrà entendre de què va el tema quan li salti l'error.

## Recuperant el `pre-commit`

Un cop afegit el plugin, el `pre-commit` ens fallarà, trobant totes les vegades que hi ha `typing.cast` en el codi.

Ens trobem en la situació que hauríem de solucionar tots els errors de pylint abans de poder fer push de la regla, i això pot ser una tasca massa gran, si n'hi ha masses.

En aquest cas, l'estratègia que vam triar va ser la de afegir la regla de pylint com a warning i no com a error, de forma que, al llarg del temps, podem fer el canvi i anar substituint els casos de `typing.cast`de forma progressiva. 


## Conclusions

`pylint` i `pre-commit` són fantàstics per aquest cas d'ús: reforçar una norma que prohibeix l'ús d'una funció. D'aquesta manera no cal vigilar manualment que la gent no se la salti.

Un descobriment és com d'efectiu és ChatGPT per a aquesta tasca.

Si no es pot deixar el pre-commit en verd d'una sola passada, caldrà organitzar sessions per acabar d'eliminar la funció prohibida.