Adding linting as part of python server reload

Posted on 2023-01-23
Tags: python

As a python dev who does a fair amount work with javascript and typescript, I easily get jealous of features JS ecosystem. One of these features is how react-script integrates eslint as part of development server. The idea is whenever you change a file, the webserver warns you if there are problems with the files. So I went looking around how would I could implement this in python.

Django

For Django, you can create and invoke system checks. These checks will run for whenever specific commands like python manage.py runserver are ran or independently via python manage.py check. These can range from very simply checks to actually calling the database.

A small snippet that runs flake8 on the codebase

from django.core.checks import Warning, register
import subprocess

@register()
def flake8(app_configs, **kwargs):
    errors = []
    result = subprocess.run(("flake8", "."),  capture_output=True)
    if(result.returncode):
        errors.append(
            Warning(
                "Found a bunch of flake8 errors.",
                hint="\n" +result.stdout.decode(),
                id="myapp.FLAKE8",
            )
        )
    return errors

You then import it in __init.py__.py of the root of the app

from path.to.check import * # noqa

The output of the python manage.py runserver. It also reruns it whenever there is a file change

WARNINGS:
?: (myapp.FLAKE8) Found a bunch of flake8 errors.
        HINT:
./myapp/broken.py:1:1: F401 'os' imported but unused


System check identified 1 issue (0 silenced).
January 10, 2023 - 10:48:32
Django version 4.1, using settings 'myapp.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Useful links:

  • https://docs.djangoproject.com/en/4.1/topics/checks/
  • https://docs.djangoproject.com/en/4.1/ref/checks/#core-system-checks

Uvicorn/FastAPI

For uvicorn/FastAPI, there isn't a documented guide on how to inject checks. What we can do is extend uvicorn's ChangReload, which handles the reloading, to run a few checks whenever reloading. We can create a runserver.py script that modifies ChangeReload

import uvicorn
from uvicorn.supervisors import ChangeReload
from fastapi import FastAPI
import subprocess
import logging


logger = logging.getLogger("uvicorn.error")


class ChangeReloadWithLinting(ChangeReload):
    def flake_check(self) -> None:
        result = subprocess.run(("flake8", "."), capture_output=True)
        if result.returncode:
            logger.warning(
                "Found the following error running flake8: \n %s",
                result.stdout.decode(),
            )

    def restart(self) -> None:
        # Run check whenever we restart/file change
        self.flake_check()
        super().restart()

    def startup(self) -> None:
        # Run check on 1st load
        self.flake_check()
        super().startup()


app = FastAPI()
if __name__ == "__main__":
    config = uvicorn.Config(
        "startserver:app", # Change to path of app
        host="127.0.0.1",
        port=5000
        log_level="info",
        reload=True,
    )
    server = uvicorn.Server(config)
    sock = config.bind_socket()
    ChangeReloadWithLinting(config, target=server.run, sockets=[sock]).run()

The output of running python runserver.py

WARNING:  StatReload detected changes in 'broken.py'. Reloading...
WARNING:  Found the following error running flake8:
 ./broken.py:1:1: F401 'os' imported but unused

INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [253163]
INFO:     Started server process [253194]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Just a warning that ChangeReload is considered as a private class so compatibility and stability are not guaranteed.

Useful links:

  • https://github.com/encode/uvicorn/pull/1572/files

Conclusion

It is very possible to integrate linting as part of the python webserver. You can also easily extend these to more complex checks.