1 A table, and a tool for the docs

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 974d653080b0 --staged --worktree -- :/
just setup

A list of counts is hard to read at a glance. A table lines up the words and the counts. A small package, tabulate, draws tables, so tally depends on it. The docs come later, and their tool comes now, in a group of its own.

The code before this part. The code after it.

1.1 tabulate, in a range of versions

tally needs tabulate to draw a table. A new version 1.0 of tabulate could change how it draws one, so tally takes any 0.9 version, and no later one. uv add with a range of versions puts the range in pyproject.toml, and the exact version in uv.lock:

The author ran uv add "tabulate>=0.9,<0.10" 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 418e7fd88a3f -- pyproject.toml uv.lock && uv sync --locked --all-groups --all-packages
uv run python -c "import tabulate; print(tabulate.__version__)"

It prints 0.9.0.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check 418e7fd88a3f

If 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 974d653080b0 --staged -- :/ && git restore --source 418e7fd88a3f --staged --worktree -- :/

What changed

  • pyproject.toml: tabulate, with the range of its versions. 3 lines added, 1 removed. It comes from uv add "tabulate>=0.9,<0.10", and you take it from the project. The file.
    • look at "tabulate>=0.9,<0.10",
  • uv.lock: the exact version of tabulate. 13 lines added. It comes from uv add "tabulate>=0.9,<0.10", and you take it from the project. The file.
The diff that uv add "tabulate>=0.9,<0.10" makes in pyproject.toml, which you take from the project
@@ -4,7 +4,9 @@ version = "0.1.0"
 description = "Count the words in a text"
 readme = "README.md"
 requires-python = ">=3.12"
-dependencies = []
+dependencies = [
+    "tabulate>=0.9,<0.10",
+]
 
 [project.scripts]
 tally = "tally:main"

This change on GitHub. The code after it.

1.2 tally --table

tally prints one line for each word, with the count first. A new option, --table, prints the same counts as a table, with a header. main takes the option out of the arguments, and passes the rows to tabulate.

Apply the patch to src/tally/__init__.py, from the top of the project:

curl -fsSL https://github.com/rahuldave/tally/commit/5051c3ff6e71020d5458c29a2e4a1f358d361b16.diff | git apply --include=src/tally/__init__.py

The patch adds from tabulate import tabulate under import sys, after a blank line. In main, it adds the flag as_table, and prints the rows with tabulate when the flag is set.

uv run tally --top 3 --table data/corpus.txt

It prints a header, word and times, a line of dashes, and the three words with their counts in a column on the right.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check 5051c3ff6e71

If 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 418e7fd88a3f --staged -- :/ && git restore --source 5051c3ff6e71 --staged --worktree -- :/

What changed

  • src/tally/__init__.py: the option --table, which prints the rows with tabulate. 11 lines added, 3 removed. The file.
    • look at print(tabulate(rows, headers=["word", "times"]))
The patch to src/tally/__init__.py
@@ -2,6 +2,8 @@
 
 import sys
 
+from tabulate import tabulate
+
 
 def count(text: str) -> dict[str, int]:
     "How many times each word of `text` appears, ignoring case."
@@ -17,11 +19,17 @@ def top(counts: dict[str, int], n: int) -> list[tuple[str, int]]:
 
 
 def main() -> None:
-    "Count the words of a file, or of the standard input. With --top N, only the N most common."
-    args, n = sys.argv[1:], None
+    "Count the words of a file, or of the standard input. Options: --top N, and --table."
+    args = sys.argv[1:]
+    as_table = "--table" in args
+    args, n = [arg for arg in args if arg != "--table"], None
     if args[:1] == ["--top"]:
         n, args = int(args[1]), args[2:]
     text = open(args[0]).read() if args else sys.stdin.read()
     counts = count(text)
-    for word, times in top(counts, n) if n else sorted(counts.items()):
+    rows = top(counts, n) if n else sorted(counts.items())
+    if as_table:
+        print(tabulate(rows, headers=["word", "times"]))
+        return
+    for word, times in rows:
         print(times, word)

This change on GitHub. The code after it.

1.3 mdformat, in the group docs

The docs will be Markdown files. mdformat formats Markdown, and its option --check says whether a file is formatted. Only the people who write the docs need it, so it goes in a group of its own, docs:

The author ran uv add --group docs mdformat 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 ec6a8aa7f9a8 -- pyproject.toml uv.lock && uv sync --locked --all-groups --all-packages

uv sync installs the group dev and no other group, unless the project names more. One more line in the table [tool.uv] of pyproject.toml names both groups, so that just setup installs mdformat too.

The command above took pyproject.toml from the project with this change in it already, so there is nothing to apply.

After uv add, the patch adds a table [tool.uv] at the end of pyproject.toml, with the line default-groups.

uv sync -q && uv run --no-sync mdformat --version

uv sync installs mdformat. Then uv run --no-sync runs it without another sync, and it prints mdformat 1.0.0, or a newer version.

Check your files against this move. The command prints nothing when they match, and otherwise shows what differs:

git check ec6a8aa7f9a8

If 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 5051c3ff6e71 --staged -- :/ && git restore --source ec6a8aa7f9a8 --staged --worktree -- :/

What changed

  • pyproject.toml: the group docs, and the groups that uv sync installs. 6 lines added. It comes from uv add --group docs mdformat, and you take it from the project. The file.
    • look at default-groups = ["dev", "docs"]
  • uv.lock: the exact versions of mdformat and of the packages it needs. 37 lines added. It comes from uv add --group docs mdformat, and you take it from the project. The file.
The diff that uv add --group docs mdformat makes in pyproject.toml, which you take from the project
@@ -19,6 +19,12 @@ build-backend = "uv_build"
 dev = [
     "ruff>=0.17.0",
 ]
+docs = [
+    "mdformat>=1.0.0",
+]
 
 [tool.ruff]
 line-length = 100
+
+[tool.uv]
+default-groups = ["dev", "docs"]

This change on GitHub. The code after it.