3 Formatting, and a corpus
Build it yourself. Read the tutorial is the other way to read this part.
If you start at this part, make your clone as Get the project says, and then run this command. It puts in your clone the files that this part starts from. A file that the project deleted before this part stays, so delete it if git check lists it:
git rm -rq --cached --ignore-unmatch -- :/ && git restore --source bfcbe07463b5 --staged --worktree -- :/just setuptally works, and it has tests. Two things help the work ahead. Code in one style is easier to read and to change,
so we install ruff, a formatter and linter. And a count of three words shows little, so just setup makes a
corpus, a text of 2000 words to count.
The code before this part. The code after it.
3.1 ruff, a tool for development
Each person writes code in a style of their own, with other quotes, blank lines and lengths of line. A formatter
writes every file in one style. A linter reads the code, and reports mistakes that the tests do not find. ruff is
both. It goes in the group dev of the project. The group dev is for the people who work on tally, so a user
who installs tally does not get ruff.
One command does it all. uv add writes ruff into pyproject.toml and uv.lock, and installs it in .venv.
The author ran uv add --dev ruff here. It writes pyproject.toml and uv.lock. On a later day, it can write newer versions of packages than the author's, and then git check finds that your files differ. So do not run it. Run this command in its place, from the top of the project. It takes each file as the author has it from the project, and installs exactly the versions that uv.lock names:
git restore --source edb9544c04d2 -- pyproject.toml uv.lock && uv sync --locked --all-groups --all-packagesIf you run uv add on a later day than the author did, it gets a newer ruff than the author's ruff 0.17.0. Your
pyproject.toml and uv.lock then name the newer version, and that is correct. The page can give you a command that
takes the two files from the project in place of uv add. Then they name ruff 0.17.0, and uv sync installs it.
uv run ruff format --checkruff reads every Python file of the project, and prints 5 files already formatted.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check edb9544c04d2If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source bfcbe07463b5 --staged -- :/ && git restore --source edb9544c04d2 --staged --worktree -- :/What changed
pyproject.toml: ruff, in a group of its own. The version is the newest on the day that ruff was added. 5 lines added. It comes fromuv add --dev ruff, and you take it from the project. The file.- look at
"ruff>=0.17.0",
- look at
uv.lock: the exact version of ruff, which every learner gets withjust setup. 35 lines added. It comes fromuv add --dev ruff, and you take it from the project. The file.- look at
name = "ruff"
- look at
The diff that uv add --dev ruff makes in pyproject.toml, which you take from the project
@@ -12,3 +12,8 @@ tally = "tally:main"
[build-system]
requires = ["uv_build>=0.12.13,<0.13.0"]
build-backend = "uv_build"
+
+[dependency-groups]
+dev = [
+ "ruff>=0.17.0",
+]3.2 A line length for ruff
ruff and each editor have their own idea of how long a line can be. A setting in pyproject.toml makes them agree.
It is a table [tool.ruff], with a line length of 100.
Apply the patch to pyproject.toml, from the top of the project:
curl -fsSL https://github.com/rahuldave/tally/commit/0b7b0a80e5804523e11cc5f5d4d334f4a9dc8327.diff | git apply --include=pyproject.tomlThe patch adds the table [tool.ruff] at the end of pyproject.toml, after the group dev.
uv run ruff check --show-settings src | grep line_length
uv run ruff format --checkThe first command prints linter.line_length = 100, so ruff reads the setting. The code still fits, and every file
is already formatted.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check 0b7b0a80e580If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source edb9544c04d2 --staged -- :/ && git restore --source 0b7b0a80e580 --staged --worktree -- :/What changed
pyproject.toml: one setting, for the linter and the formatter both. 3 lines added. The file.- look at
line-length = 100
- look at
The patch to pyproject.toml
@@ -17,3 +17,6 @@ build-backend = "uv_build"
dev = [
"ruff>=0.17.0",
]
+
+[tool.ruff]
+line-length = 1003.3 A corpus, which just setup makes
A count of a b a shows little. tally needs a long text to count, the same for every learner. A script,
scripts/corpus.py, writes data/corpus.txt, 2000 words from a short list, in the same order each time. The
script comes whole, as a patch.
Apply the patch to scripts/corpus.py, from the top of the project:
curl -fsSL https://github.com/rahuldave/tally/commit/1568a522d23312c59c6e42cb027ec25ba3ae8b35.diff | git apply --include=scripts/corpus.pyThe corpus is an artifact, a file that the project makes and git does not hold. So .gitignore names the folder
data/. git leaves an ignored file alone, so the corpus stays when the code changes.
Apply the patch to .gitignore, from the top of the project:
curl -fsSL https://github.com/rahuldave/tally/commit/1568a522d23312c59c6e42cb027ec25ba3ae8b35.diff | git apply --include=.gitignoreThe patch adds the folder data/ at the end of .gitignore, under a comment.
just setup makes the environment, and now it runs the script too. So every learner has the corpus. The script
does not touch a corpus that is already there, so a second just setup writes nothing.
Apply the patch to justfile, from the top of the project:
curl -fsSL https://github.com/rahuldave/tally/commit/1568a522d23312c59c6e42cb027ec25ba3ae8b35.diff | git apply --include=justfileIn the recipe setup, the patch adds the line that runs the script, after the line uv sync.
just setup
uv run tally data/corpus.txt | sort -rn | head -3just setup prints wrote data/corpus.txt: 2000 words. sort -rn puts the lines of tally in order of count, highest
first, and head -3 keeps the first three. They are 127 line, 126 class and 125 word.
Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:
git check 1568a522d233If you are stuck, this command puts the files of this move in place of yours. A file that you made yourself stays:
git restore --source 0b7b0a80e580 --staged -- :/ && git restore --source 1568a522d233 --staged --worktree -- :/What changed
.gitignore: git does not hold the corpus. 3 lines added. The file.- look at
data/
- look at
justfile:just setupnow makes the corpus too. 1 line added. The file.- look at
uv run python scripts/corpus.py data/corpus.txt
- look at
scripts/corpus.py: the script writes the same 2000 words each time, and does not touch a corpus that is already there. New, 25 lines. The file.- look at
if target.exists():
- look at
The patch to .gitignore
@@ -8,3 +8,6 @@ wheels/
# Virtual environments
.venv
+
+# Artifacts that just setup makes: they are not in git
+data/The patch to justfile
@@ -5,6 +5,7 @@ default:
# Make the environment of this step: the first command of every step
setup:
uv sync
+ uv run python scripts/corpus.py data/corpus.txt
# Count the words of a file
run file:The patch to scripts/corpus.py
@@ -0,0 +1,25 @@
+"""Write data/corpus.txt, a text of 2000 words to count, the same every time. Leave it alone if it is there."""
+
+import sys
+from pathlib import Path
+
+WORDS = "the a word count text tally test step move commit tag note slide class shell file line code".split()
+
+
+def main() -> None:
+ target = Path(sys.argv[1] if len(sys.argv) > 1 else "data/corpus.txt")
+ if target.exists():
+ print(f"{target} is there")
+ return
+ seed, words = 7, []
+ for _ in range(2000):
+ seed = (seed * 1103515245 + 12345) % 2**31
+ words.append(WORDS[seed % len(WORDS)])
+ target.parent.mkdir(parents=True, exist_ok=True)
+ lines = (" ".join(words[i : i + 12]) for i in range(0, len(words), 12))
+ target.write_text("\n".join(lines) + "\n")
+ print(f"wrote {target}: {len(words)} words")
+
+
+if __name__ == "__main__":
+ main()