5 Still green
just setupMuch later, top comes with a test of its own, and the old tests still pass. The types and the docstring that came
in between did not change a test. The tests check what the code does, and not how it says it.
uv run python -m unittest discover -s tests -v
uv run tally --top 3 data/corpus.txtThe four tests pass, and the command prints the three most common words of the corpus.
What changed
.gitignore: git does not hold the corpus. 3 lines added. The file.justfile:just setupmakes the corpus and the report. 7 lines added. The file.pyproject.toml: ruff, and its line length. 8 lines added. The file.scripts/corpus.py: the script that writes the corpus. New, 25 lines. The file.src/tally/__init__.py: types, a docstring,topand the option--top. 15 lines added, 4 removed. The file.tests/test_top.py: the new test, oftop. New, 8 lines. The file.uv.lock: the exact version of ruff. 35 lines added. The file.
The diff of .gitignore
@@ -8,3 +8,6 @@ wheels/
# Virtual environments
.venv
+
+# Artifacts that just setup makes: they are not in git
+data/The diff of justfile
@@ -5,6 +5,8 @@ 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
+ test -f build/top.txt || just report
# Count the words of a file
run file:
@@ -13,3 +15,8 @@ run file:
# Run the tests
test:
uv run python -m unittest discover -s tests -q
+
+# Write the ten most common words of the corpus to build/top.txt
+report:
+ mkdir -p build
+ uv run tally --top 10 data/corpus.txt > build/top.txtThe diff of pyproject.toml
@@ -12,3 +12,11 @@ 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",
+]
+
+[tool.ruff]
+line-length = 100The diff of 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()The diff of src/tally/__init__.py
@@ -3,14 +3,25 @@
import sys
-def count(text):
+def count(text: str) -> dict[str, int]:
+ "How many times each word of `text` appears, ignoring case."
counts = {}
for word in text.lower().split():
counts[word] = counts.get(word, 0) + 1
return counts
+def top(counts: dict[str, int], n: int) -> list[tuple[str, int]]:
+ "The n most common words, most common first."
+ return sorted(counts.items(), key=lambda pair: (-pair[1], pair[0]))[:n]
+
+
def main() -> None:
- text = open(sys.argv[1]).read() if len(sys.argv) > 1 else sys.stdin.read()
- for word, n in sorted(count(text).items()):
- print(n, word)
+ "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
+ 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()):
+ print(times, word)The diff of tests/test_top.py
@@ -0,0 +1,8 @@
+import unittest
+
+from tally import top
+
+
+class Top(unittest.TestCase):
+ def test_most_common_first(self):
+ self.assertEqual(top({"a": 1, "b": 3, "c": 2}, 2), [("b", 3), ("c", 2)])