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 setupA 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:
uv add "tabulate>=0.9,<0.10"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 418e7fd88a3fIf 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 fromuv add "tabulate>=0.9,<0.10". The file.- look at
"tabulate>=0.9,<0.10",
- look at
uv.lock: the exact version of tabulate. 13 lines added. It comes fromuv add "tabulate>=0.9,<0.10". The file.
The diff that uv add "tabulate>=0.9,<0.10" makes in pyproject.toml
@@ -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"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__.pyThe 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.txtIt 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 5051c3ff6e71If 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"]))
- look at
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)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:
uv add --group docs mdformatuv 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.
After uv add --group docs mdformat, set the other values that this change sets in pyproject.toml. Run this command from the top of the project. It reads your file with tomlkit, a library for TOML files. It sets each of these values as the project has it, and keeps the rest of your file:
uv run --quiet --no-config --no-project --with tomlkit python - <<'PY'
import pathlib, tomlkit
path = pathlib.Path('pyproject.toml')
doc = tomlkit.parse(path.read_text(encoding='utf-8')) if path.exists() else tomlkit.document()
# Each value that the change sets, by its keys
SET = [
(('tool', 'uv'), {'default-groups': ['dev', 'docs']}),
]
def put(table, keys, value): # make each table on the way; a table goes into the one there, key by key
for key in keys[:-1]:
if key not in table:
table[key] = tomlkit.table(True)
table = table[key]
if isinstance(value, dict) and isinstance(table.get(keys[-1]), dict):
for key, item in value.items():
put(table[keys[-1]], [key], item)
else:
table[keys[-1]] = value
for keys, value in SET:
put(doc, keys, value)
path.write_text(tomlkit.dumps(doc), encoding='utf-8')
PYAfter 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 --versionuv 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 ec6a8aa7f9a8If 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 groupdocs, and the groups thatuv syncinstalls. 6 lines added. It comes fromuv add --group docs mdformat. The file.- look at
default-groups = ["dev", "docs"]
- look at
uv.lock: the exact versions of mdformat and of the packages it needs. 37 lines added. It comes fromuv add --group docs mdformat. The file.
After uv add --group docs mdformat, the patch to pyproject.toml
@@ -25,3 +25,6 @@ docs = [
[tool.ruff]
line-length = 100
+
+[tool.uv]
+default-groups = ["dev", "docs"]