5 The formatter and the linter
Read the tutorial. Build it yourself is the other way to read this part.
This walk goes through rahuldave/cheesecracker on GitHub. This part starts from the code at step-04. The code before this part
To look at this code on your machine, run these commands. The first one makes a clone, if this folder has none:
[ -d cheesecracker ] || git clone https://github.com/rahuldave/cheesecracker
cd cheesecracker
git fetch --quiet --tags --force origin
git switch --detach step-04just setupCode that works can still be hard to read, and a reader can miss a bug in it. A formatter writes the code in one layout. A linter checks the code against rules, such as "no import that nothing uses". ruff is both, and it has the rules of flake8.
5.1 a change in a hurry: the code still works, and reads worse
A change made in a hurry leaves code that works and reads badly. The change adds an import that nothing uses, and takes out spaces and blank lines. It also makes one line 187 characters long, and adds a function that nothing calls.
± See the change to src/api-service/api/service.py
± See the change to src/api-service/api/utils.py
service.py now imports os, which nothing uses. /health follows / with no blank line. The line
z=power(x,2)+power(y,2) has no spaces, and the return at the end is 187 characters long. utils.py has a missing
space, three blank lines, and unused_function().
just test -qpytest still prints 21 passed. The tests check what the code does, not how it reads.
What changed
src/api-service/api/service.py: an import that nothing uses, missing spaces and blank lines, and a long line. 3 lines added, 5 removed. The file- look at
z=power(x,2)+power(y,2)
- look at
src/api-service/api/utils.py: a missing space, extra blank lines, and a function that nothing calls. 6 lines added, 1 removed. The file- look at
def unused_function():
- look at
The diff of src/api-service/api/service.py
@@ -5,6 +5,7 @@ Cheese App API Service
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
import math
+import os
from api.utils import power
# Setup FastAPI app
@@ -19,13 +20,10 @@ app.add_middleware(
allow_headers=["*"],
)
-
# Routes
@app.get("/")
async def get_index():
return {"message": "Welcome to the Cheese App CI/CD Tutorial!"}
-
-
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@@ -36,6 +34,6 @@ async def calculate_euclidean_distance(x: float = 1, y: float = 2):
"""Calculate the Euclidean distance from origin: sqrt(x^2 + y^2)
Returns the distance of a point (x, y) from the origin (0, 0)
"""
- z = power(x, 2) + power(y, 2)
+ z=power(x,2)+power(y,2)
result = math.sqrt(z)
- return {"x": x, "y": y, "distance": result}
+ return {"x": x, "y": y, "distance": result, "message": "This is a very long line that exceeds 120 characters and Black will complain about it so we need to fix this formatting issue"}The diff of src/api-service/api/utils.py
@@ -4,6 +4,11 @@ Used by the Cheese App API Service
"""
-def power(base, exponent):
+def power(base,exponent):
"""Calculate base raised to the power of exponent"""
return base**exponent
+
+
+
+def unused_function():
+ pass5.2 ruff, the formatter and the linter, in the group lint
ruff is a formatter and a linter in one tool, written in Rust by Astral, the makers
of uv. ruff format writes the code in the layout of black, the formatter of most Python projects. ruff check
checks the rules of flake8 and of many other linters, and can fix some of them. It takes the place of both black and
flake8, and runs much faster.
ruff checks the code, and the server does not need it to run. So ruff goes into a group of its own, lint.
This change came from running uv add --group lint ruff.
uv run ruff --versionuv prints the version of ruff, ruff 0.17.1 on the day of the class.
What changed
pyproject.toml: ruff, in the grouplint. 3 lines added. It comes fromuv add --group lint ruff. The file- look at
"ruff>=0.17.1",
- look at
uv.lock: the exact version of ruff. 29 lines added. It comes fromuv add --group lint ruff. The file
The diff of pyproject.toml
@@ -16,6 +16,9 @@ package = false
default-groups = "all"
[dependency-groups]
+lint = [
+ "ruff>=0.17.1",
+]
test = [
"httpx2>=2.13.1",
"pytest>=9.1.1",5.3 ruff's settings: lines of 120 characters, and flake8's rules
By default, ruff allows lines of up to 88 characters, and this project allows 120. Its default rules also leave out
some rules of flake8. Two tables in pyproject.toml set both.
± See the change to pyproject.toml
[tool.ruff] sets line-length = 120, for the formatter and the linter. [tool.ruff.lint] selects the rules E, W
and F. They are the errors and warnings of pycodestyle, and the rules of pyflakes, which flake8 checks.
uv run ruff checkruff finds 2 errors, and fails. F401 is the import of os, which nothing uses. E501 is the line of 187
characters. ruff says that it can fix the first one.
uv run ruff format --checkruff says 2 files would be reformatted, and fails. --check changes no file.
What changed
pyproject.toml: the settings of ruff, the length of a line and the rules of the linter. 8 lines added. The file- look at
select = ["E", "W", "F"]
- look at
The diff of pyproject.toml
@@ -30,3 +30,11 @@ test = [
# The tests are in tests/, and import the package api from src/api-service/
testpaths = ["tests"]
pythonpath = ["src/api-service"]
+
+[tool.ruff]
+# The longest line, for the formatter and the linter
+line-length = 120
+
+[tool.ruff.lint]
+# The rules that flake8 checks: pycodestyle's errors (E) and warnings (W), and pyflakes (F)
+select = ["E", "W", "F"]5.4 just fmt and just lint run ruff
Two recipes give the formatter and the linter short names. Each passes on to ruff any words that you type after it.
The recipe fmt runs uv run ruff format, and lint runs uv run ruff check. So just fmt --check checks the
layout, and just lint --fix fixes what ruff can fix.
just fmt --checkThe recipe fails as ruff format --check did, with 2 files would be reformatted.
What changed
justfile: the recipesfmtandlint. 8 lines added. The file- look at
uv run ruff check "$@"
- look at
The diff of justfile
@@ -44,3 +44,11 @@ stop:
# Run the tests, and show which lines of the package api they ran. Extra arguments go to pytest: just cov tests/unit
cov *args:
uv run pytest --cov=api --cov-report=term-missing "$@"
+
+# Format the code, one way. Extra arguments go to ruff: just fmt --check, just fmt --diff
+fmt *args:
+ uv run ruff format "$@"
+
+# Check the code against the rules in pyproject.toml. Extra arguments go to ruff: just lint --fix
+lint *args:
+ uv run ruff check "$@"5.5 the formatter fixes the spaces, the blank lines and the line breaks
The formatter writes every file in one layout, so nobody fixes spaces by hand.
This change came from running just fmt.
ruff prints 2 files reformatted. In service.py, z=power(x,2)+power(y,2) now has its spaces, and two blank lines
separate the routes. The long return now spreads its dictionary over six lines, one key on each line. In utils.py, the blank lines are two.
just fmt --checkruff prints 6 files already formatted.
What changed
src/api-service/api/service.py: the spaces, the blank lines, and the longreturnon six lines. 10 lines added, 2 removed. It comes fromjust fmt. The file- look at
z = power(x, 2) + power(y, 2)
- look at
src/api-service/api/utils.py: the space after the comma, and two blank lines beforeunused_function(). 1 line added, 2 removed. It comes fromjust fmt. The file- look at
def power(base, exponent):
- look at
The diff of src/api-service/api/service.py
@@ -20,10 +20,13 @@ app.add_middleware(
allow_headers=["*"],
)
+
# Routes
@app.get("/")
async def get_index():
return {"message": "Welcome to the Cheese App CI/CD Tutorial!"}
+
+
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@@ -34,6 +37,11 @@ async def calculate_euclidean_distance(x: float = 1, y: float = 2):
"""Calculate the Euclidean distance from origin: sqrt(x^2 + y^2)
Returns the distance of a point (x, y) from the origin (0, 0)
"""
- z=power(x,2)+power(y,2)
+ z = power(x, 2) + power(y, 2)
result = math.sqrt(z)
- return {"x": x, "y": y, "distance": result, "message": "This is a very long line that exceeds 120 characters and Black will complain about it so we need to fix this formatting issue"}
+ return {
+ "x": x,
+ "y": y,
+ "distance": result,
+ "message": "This is a very long line that exceeds 120 characters and Black will complain about it so we need to fix this formatting issue",
+ }The diff of src/api-service/api/utils.py
@@ -4,11 +4,10 @@ Used by the Cheese App API Service
"""
-def power(base,exponent):
+def power(base, exponent):
"""Calculate base raised to the power of exponent"""
return base**exponent
-
def unused_function():
pass5.6 the linter removes the import that nothing uses
ruff can remove an import that nothing uses, because the change cannot alter what the code does. The linter still fails
after the fix, because one error is left. || true makes the command count as a success all the same.
This change came from running just lint --fix || true.
ruff removes the line import os from service.py, and still reports the long line.
just lintruff finds 1 error, E501, and fails. The line of the message is 147 characters long, and no tool can shorten words.
What changed
src/api-service/api/service.py: the import ofos, removed. 0 lines added, 1 removed. It comes fromjust lint --fix || true. The file
The diff of src/api-service/api/service.py
@@ -5,7 +5,6 @@ Cheese App API Service
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
import math
-import os
from api.utils import power
# Setup FastAPI app5.7 a shorter line, by hand
A long message is a choice of words, so a person has to shorten it. The new message is short enough for one line.
± See the change to src/api-service/api/service.py
The value of "message" in the return of calculate_euclidean_distance() is now shorter than 120 characters.
just lintruff prints All checks passed!. Note that ruff never reports unused_function(). No rule of E, W or F looks for
a function that nothing calls.
What changed
src/api-service/api/service.py: a shorter message. 1 line added, 1 removed. The file- look at
"message": "This is a very long line that exceeds 120 characters blah",
- look at
The diff of src/api-service/api/service.py
@@ -42,5 +42,5 @@ async def calculate_euclidean_distance(x: float = 1, y: float = 2):
"x": x,
"y": y,
"distance": result,
- "message": "This is a very long line that exceeds 120 characters and Black will complain about it so we need to fix this formatting issue",
+ "message": "This is a very long line that exceeds 120 characters blah",
}5.8 just check runs every check
The layout, the rules and the tests each have a recipe now. The recipe check runs all three, and stops at the
first that fails.
The recipe check, at the end of the justfile, runs just fmt --check, just lint and just test -q, in that
order.
just checkjust prints each command, and the last line is 21 passed.
What changed
justfile: the recipecheck. 6 lines added. The file- look at
just fmt --check
- look at
The diff of justfile
@@ -52,3 +52,9 @@ fmt *args:
# Check the code against the rules in pyproject.toml. Extra arguments go to ruff: just lint --fix
lint *args:
uv run ruff check "$@"
+
+# Everything a commit must pass: the format, the lint and the tests
+check:
+ just fmt --check
+ just lint
+ just test -qThis part ends at the code at step-05. The code after this part