This track is the reference implementation: bench/expected/logagg.txt is generated from
it and frozen. So the bar is different from the other tracks — it has to be obviously correct
on re-read, not clever. When the Rust version disagrees in eight months, this is the file that
gets read to settle it.
python tracks/30-python/logagg.py bench/data/access.log > bench/expected/logagg.txt
Run once. After that it is a checked-in artefact and changing it requires changing
bench/SPEC.md, reviewed in the same diff. Regenerating it casually to make a failing track
pass defeats the entire apparatus — the golden file's value is exactly that it does not move.
The same discipline as the COBOL lab's differential testing: the legacy output is the specification, and a "better" answer is still a wrong answer.
newline=""with open(path, "r", encoding="ascii", newline="") as fh:
Default Python does universal-newline translation: \r\n and \r both arrive as \n. That
sounds helpful and means the program cannot detect a CRLF file. With newline="" the \r
survives, gets attached to the latency field, and is reported as a LATENCY rejection — which
is a true statement about the input rather than a silent repair of it.
encoding="ascii" is explicit for the same reason Locale.ROOT was needed in Java: without
it the default encoding is locale.getpreferredencoding(), which on Windows is still a
legacy code page (cp1252 here) unless UTF-8 mode is enabled. PEP 686 will make UTF-8 the
default, but it is not landed, and code that has to work on today's interpreters names its
encoding.
sys.stdout.buffer.writesys.stdout.buffer.write(render(source, res).encode("ascii"))
print() on Windows emits \r\n. Third track, third instance of the same problem, third
different fix:
| Track | Fix |
|---|---|
| C | _setmode(_fileno(stdout), _O_BINARY) |
| Java 8 | System.out.write(s.getBytes(US_ASCII)) |
| Python | sys.stdout.buffer.write(s.encode("ascii")) |
Worth keeping that table. It is a cleaner illustration of what "standard library" means than any amount of prose about batteries being included.
The EAFP version of the validator is more Pythonic:
try:
micros = parse_micros(fields[5])
status = int(fields[4])
except (ValueError, IndexError):
res.malformed.append((lineno, "???"))
and it is wrong for this spec, because SPEC 1 requires a specific ordered reason and a
caught ValueError cannot tell you whether the status or the latency failed. So classify()
checks explicitly, in order, LBYL-style, and returns the reason:
if len(fields) != 6:
return "FIELDS"
...
Smallest clear example in the series of a general truth: idiomatic and correct are different axes, and when they disagree the spec wins. Worth flagging in the Q10 of every track where it comes up.
[0-9] and not \dLATENCY_RE = re.compile(r"^[0-9]+\.[0-9]{3}$")
In Python 3, \d on a str pattern matches any Unicode decimal digit — Arabic-Indic
٠١٢, Devanagari, fullwidth 012. SPEC 1 says ASCII digits. re.ASCII would fix it; so
does spelling out the class, which has the advantage of meaning the same thing in the Java
track (where \d is ASCII-only by default) and in every other track after it.
One regex, one meaning, everywhere. Cheap insurance.
First draft had a half-finished row() function left above render() — a placeholder with a
.rjust(0) in it that did nothing. It never ran, because render() defines its own nested
line(). Deleted.
Worth noting only because it compiled, imported, and passed every test. In the C and Java tracks an unfinished function would have been a compile error. This is the cost of dynamic typing in its smallest form: errors wait on the branch that is not taken. A linter catches this one; the general case does not have a linter.
--shard N (SPEC 6) with multiprocessing.Pool, and the threading version,
timed side by side. The expected result — threading is slower than serial because the
GIL serialises the work and you pay for the switching — is the most durable thing this
track teaches, and it needs a number, not a claim.mypy --strict over logagg.py. It is fully annotated; whether it actually passes
is a different question.threading version
overtakes the serial one. That is the future of this section.Lib/dataclasses.py (Q9). Uses exec on a generated source string to build
__init__, with a comment explaining why.