Adding linting as part of python server reload
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.