Keep the join in place until quote, null, and newline bytes match. A passing check on row counts does not protect those bytes. Extract one helper only after that byte lock is green.
Where a clean extract still corrupts files
Manual comma joins look done until a note contains a comma. A reviewer then splits the function and the leak moves with it. The new helper looks cleaner while the file bytes change.
Empty notes and missing quantities also shift during cleanup. A falsy or-empty turns a zero note into a blank field. Record that choice before anyone renames the loop body.
Scope of this pass
This pass records current bytes and does not fix quoting. This pass also refuses a dialect switch inside the same diff. A module rename waits for a later and separate change.
Outputs to freeze
Lock four observable outputs before you extract any helper. Header text and column order form the first locked output. Row quoting, null text, and the final newline complete the set.
Confirm QUOTE_MINIMAL and the excel CRLF ending on the linked csv page. Use that page only when a later commit adopts the csv writer. Those defaults are not the contract of the manual function.
https://docs.python.org/3/library/csv.html
Do not treat the documentation as permission to change bytes. Adopt the csv writer only in a later commit with new expectations. This commit only moves the existing join behind a function name.
Sample under test
The sample below is a labeled teaching artifact, not a field log. It shows a common manual join you can run locally today. No production metric is claimed for this function or its callers.
def export_rows(rows):
lines = ['sku,qty,note']
for row in rows:
sku = '' if row.get('sku') is None else str(row['sku'])
qty = row.get('qty')
qty_text = '' if qty is None else str(qty)
note = row.get('note') or ''
lines.append(','.join([sku, qty_text, note]))
return '\n'.join(lines) + '\n'
The header is a fixed string, so column order cannot drift silently. Quantity None becomes an empty field, while quantity zero stays zero. A note of zero becomes empty because or treats it as false.
A comma inside a note is not quoted by this joiner. That leak is current behavior, so the test must lock it. Fixing the leak is a product decision for a later patch.
Characterization artifact
Each case stores a name, an input row, and the full text. Compare the whole string, including the final newline character. Partial asserts on split columns will hide the comma leak.
import pytest
from export_join import export_rows
CASES = [
('plain', [{'sku': 'A1', 'qty': 2, 'note': 'ok'}], 'sku,qty,note\nA1,2,ok\n'),
('comma_note', [{'sku': 'B2', 'qty': 1, 'note': 'a,b'}], 'sku,qty,note\nB2,1,a,b\n'),
('none_qty', [{'sku': 'C3', 'qty': None, 'note': 'x'}], 'sku,qty,note\nC3,,x\n'),
('zero_qty', [{'sku': 'D4', 'qty': 0, 'note': ''}], 'sku,qty,note\nD4,0,\n'),
('none_note', [{'sku': 'E5', 'qty': 1, 'note': None}], 'sku,qty,note\nE5,1,\n'),
('zero_note', [{'sku': 'F6', 'qty': 1, 'note': 0}], 'sku,qty,note\nF6,1,\n'),
]
@pytest.mark.parametrize('name,rows,expected', CASES, ids=[row[0] for row in CASES])
def test_export_rows_bytes(name, rows, expected):
assert export_rows(rows) == expected
Run the suite from the repository root with a quiet pytest call. A second command prints repr so newline characters stay visible. Save both outputs in the change notes before you edit code.
python -m pytest -q test_export_join.py
python -c "from export_join import export_rows; print(repr(export_rows([{'sku': 'B2', 'qty': 1, 'note': 'a,b'}])))"
'sku,qty,note\nB2,1,a,b\n'
The expected strings are derived by reading the sample, not from a published benchmark. Confirm them with the pytest command before you treat them as truth. This draft does not report a timed run or a server log.
Steps
1. Copy the function unchanged
Place the current function in a module and import it from tests. Do not format, rename, or annotate it in that first commit. The goal is a byte snapshot, not a style cleanup.
2. Add the six row locks
Cover a plain row, a comma note, and a missing quantity. Also cover a zero quantity, a missing note, and a zero note. Those six rows expose order, quoting, nulls, and falsy text.
3. Assert full text, not fragments
Expected strings should include the header and the trailing newline. Use equality against the full return value, not a contains check. A contains check can pass while a terminator character changes.
4. Reject model rows that change the contract
MonkeyCode free model access can propose extra rows from the function. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Accept a proposed row only after you run the current function.
Discard any expected string the model invented without a local run. If the proposal adds quote characters, it is a behavior change. Keep that idea in a follow-up note, not in this patch.
5. Run the same file on a free server
MonkeyCode's free server option can run this same pytest file. Use it as a second environment, not as proof of production parity. No hardware, quota, or duration claim is made for that option.
Pin newlines inside the function so the server OS cannot rewrite them. Do not open a text file in this test, because translation differs. A plain return value keeps this byte contract portable across machines.
6. Extract only the join
Move the comma join into a new function with the same arguments. Leave null handling, header text, and the trailing newline untouched. The characterization file should need zero assertion edits.
def join_fields(sku, qty_text, note):
return ','.join([sku, qty_text, note])
def export_rows(rows):
lines = ['sku,qty,note']
for row in rows:
sku = '' if row.get('sku') is None else str(row['sku'])
qty = row.get('qty')
qty_text = '' if qty is None else str(qty)
note = row.get('note') or ''
lines.append(join_fields(sku, qty_text, note))
return '\n'.join(lines) + '\n'
If a test fails, revert the extract before you edit an expectation. A failing lock means the move was not behavior-preserving. Changing the expected string in the same commit hides that miss.
7. Stop after one green extract
Do not also replace the join with the csv writer in this change. Do not sort columns, add quoting, or drop the trailing newline. One moved call is the whole safe diff for this pass.
Decision table
Use the table when a review comment asks for a wider cleanup. The action column says what belongs in this commit. Everything else is a later patch with its own expected bytes.
| Signal | This commit | Later commit |
|---|---|---|
| Comma inside a note | Lock the unquoted bytes | Add quotes with a new expected string |
| Note value is zero | Lock the blank field | Replace or with an explicit None check |
| Quantity is None | Lock the empty field | Change it only if a spec requires a token |
| Quantity is zero | Lock the character 0 | Do not treat zero as missing |
| Newline differs by OS | Keep the explicit \n return |
Do not move the test to text-mode open |
| Model adds quote marks | Reject that expected string | Store it as a quoting proposal |
A comma in a note stays unquoted until a separate spec commit. A zero note stays blank until you replace the falsy or. A Windows-only failure means the test depended on open translation.
What the lock does not prove
These tests freeze behavior, including known leaks and falsy bugs. They do not prove the file meets an invoice specification. A green run can still be wrong for every downstream consumer.
Float quantities are outside this lock on purpose. Default string conversion of floats can change with formatting choices. Add floats only after you pin a format string in the same test.
Disk encoding, permissions, and atomic rename are not covered here. Add those locks before you extract a function that opens files. This sample returns text, so those risks stay out of scope.
A free model draft is untrusted input, same as a pasted snippet. It can omit the zero-note case and still look complete. Your six-row table is the source of truth, not the draft.
A free server run does not measure speed, cost, or uptime. It only answers whether these assertions pass in that environment. Treat a server failure as an environment clue, not a product score.
Who should not use this pass
Skip this pass if unquoted commas must stop shipping this week. Write the corrected expectation first, then change the joiner. Locking the leak would preserve a defect you already must remove.
Skip this pass when callers of the function are still unknown. A byte lock on a sample row is not a caller inventory. Find the real inputs before you trust six hand-built cases.
Skip this pass if the function sends mail or charges a card. Remote writes are side effects these string tests cannot see. Characterize those effects separately, or do not extract yet.
Skip this pass if you will merge unread model assertions. The method depends on a human comparing repr output to the test. Without that read, the lock is only copied text.
Close
Quote, null, and newline bytes decide whether the extract is safe. Six full-string cases are enough to guard this one join. Leave quoting repairs and dialect swaps for a later diff.
Run the same pytest file on MonkeyCode's free server before you merge. Read the repr line yourself, and keep the assertion diff empty. If that second run is unavailable, a local green lock still stands.
Top comments (0)