Repository navigation
feat(edit): add edit.set_cell, a one-cell edit that keeps padding - #277
Merged
Merged
Conversation
The editing API reserved edit.set_cell(text, row_key, column, value) for a text-level cell edit. This states its contract before the code exists, so the cases for it can be written from the text alone: - one changed line, the row, and only the cell's span within it; - the cell keeps its width when it was padded or the table is, and places its new text as the old text was placed, or as the column's alignment cell says when the old cell gives no side; - every argument is a str, any stored column can be set, a computed column takes only "", and a value that is already there is a no-op; - the API's existing refusals apply, a value with outer space or tab is not-representable, and the row is read back before returning. Measured at d368364, rowspec.set_cell + render turns the padded row `| r_20 | nut | 10 |` into `| r_20 | nut | 11 |` for qty=11, and writes " nut" as `| nut |`, which parse reads back as "nut". Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
set_cell(text, row_key, column, value, /) rewrites one stored data cell in place, under the name and rules docs/editing-api.md gives it. Only the cell's span of the row's line changes. A padded cell, or any cell of a padded table, keeps its width where the new text fits, and the text is placed as the old text was, or as the alignment cell says. Refusals are the editing API's own: no-key, no-such-row, no-such-column, line-break, and not-representable for a value with leading or trailing space or tab. A value in a computed column is left to the result's evaluation, which refuses it as value-in-computed-cell, and the row is read back before the text is returned. A value equal to the cell's text, compared in NFC, returns the input unchanged. The row key is type-checked up front. Without that, a table with no rows reports no-such-row for a key that is not a str, which a test now pins. tests/test_edit_merge.py gains three set_cell pairings: two edits of one row conflict, and edits two rows apart merge, padded or not. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
tests/test_edit_set_cell.py was written from docs/editing-api.md and SPEC.md at b34b957, the commit that specified set_cell, by an author who saw no implementation. The author worked in a directory holding only SPEC.md, docs/editing-api.md, CONTRIBUTING.md and tests/test_edit.py, the last for harness style, with no .git and no reference/. The file is as written: sha256 09f8e72ae5d0c962... here and in that directory. Its first run against 96fab16 passed 145 of 145. The author listed what the docs left open: the space count on the far side of a placed cell, the split of an odd spare in a centred cell, whether an empty cell of two spaces is padded, and whether a cell that is not NFC reads as its NFC form. Those cases are left out here, and the docs now answer each of them in their own commit. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
The cases written from the first draft found four outcomes it did not determine. Each now has the answer the implementation gives, and a test in tests/test_edit.py: - text placed right or left has exactly one space on its other side, so `| 3|` set to `4` becomes `| 4 |`; - centred text with an odd spare space has the extra one on its right; - a cell is padded when it is more than two characters wider than its text, so three spaces are padded and two are not; - the no-op comparison is in NFC, so setting a cell that is not NFC to its NFC form keeps the cell's bytes. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Rule 3 said "padded" two ways: more than one space beside the text, for header cells and for a padded table, and more than two characters wider than the text, for set_cell. The code has one test for both, _padded, and it is the second. So `| id | qq|` renamed to q gives `| q|`, and in a padded table set_cell places `| 33|` set to 4 by the alignment cell, as `| 4 |`, not by its own two spaces. Rule 3 now gives the one definition first and uses it for header cells, data cells and the padded table. It also says widths count code points, with a tab as one. A test pins both of the cases above, and it fails with the threshold moved from 2 to 1. The set_cell bullet now says a `|` in the value is written as `\|`. The CHANGELOG now says that "" in a computed column is accepted. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
set_cell(None, ...) raised AttributeError from the parser, and a column of 3 raised a TypeError from unicodedata that did not name the argument. text, row_key and column are now checked first, and each raises "<name> must be a str"; the value already was. A test covers all four, and three of them fail with the check removed. Two tests pin refusal rule 2 for set_cell, which refuses an outer space before any text is built. Without that check, " 1" in a computed column is refused as value-in-computed-cell, and " r_02" in the key column as duplicate-row-id. Both must stay not-representable. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
Through real merges with stock git, on the canonical table: - set_cell of one cell to two different values conflicts; - set_cell of one cell to the same value on both branches merges cleanly, as the same change; - set_cell of a row against a delete of that row conflicts. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
- The CHANGELOG said the cell keeps its width where the new text fits. It does that only when the cell or the table is padded, and keeps its own spaces otherwise. It now says so. - Rule 3 said an unpadded header cell is re-rendered to its new text. It keeps the spaces it had beside its text, as `| qq|` renamed to q giving `| q|` shows, so the rule now says that. - The set_cell bullet says that any argument that is not a str raises TypeError, and that setting the key column changes the row's identity (§6): an edit that still names the old key no longer finds the row. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
CameronBrooks11
added a commit
that referenced
this pull request
Oct 23, 2026
Prepare 0.6.0. - Version 0.6.0 in pyproject.toml, reference/rowspec/__init__.py, action.yml's default pin (which release.yml checks against the build), the README's @v0.6.0 example and rowspec's line in uv.lock. Every other 0.5.0 mention is historical. - CHANGELOG: Unreleased becomes [0.6.0] - 2026-10-23, with rowspec.edit.set_cell (#277) under Added and a Known issues section for #278 (a data cell holding := is refused as malformed-declaration) and #279 (a row ending in an escaped | is accepted where section 9.18 says malformed-table-line). Both were already in 0.5.0; rowspec_alt reads each line as SPEC.md says. - ROADMAP names v0.6.0 as the latest release and adds a Shipped in 0.6.0 section; of its figures only just test moved (1650 to 1818). just check, conform (631), conform-alt (631), conform-csv (281), mutants (114 killed, 2 equivalent), test (1818 passed), test-xlsx and test-libreoffice all pass. The sdist and wheel built from a git archive install in fresh venvs at 0.6.0, edit.set_cell works from each, and release.yml's tag and pin checks pass on that build. Signed-off-by: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
rowspec.edit.set_cell(text, row_key, column, value, /), the one-cell edit that docs/editing-api.md reserved under that name for a later release. It changes one stored data cell in the text, and only that cell's span of the row's line.Why
Measured at d368364, the released
rowspec.set_cellplusrenderrewrites the whole row unpadded, and it writes a value it cannot read back:edit.set_cellgives| r_20 | nut | 11 |for the first. It refuses the second asnot-representable.rowspec.set_cellis unchanged.Contract (docs/editing-api.md)
text(rule 4, never reflow).|3|becomes|30|.str, positional-only, and each of the four raisesTypeErrornaming it otherwise. There are no column types in.mdtbl, so the value is the cell's text, read as any cell is (§4.1.6). The API edits.mdtbltext only, like the other edits, so a CSV file and its sidecar are not covered."", which is a no-op. Anything else is refused asvalue-in-computed-cell.no-key,no-such-row,no-such-column,line-break, andnot-representablefor a value with leading or trailing space or tab.duplicate-row-id,bad-identifierfor a blank key, and §9.10 for an order column.not-representable.There is no CLI command;
cli-missing-subcommandsin #271 kept new subcommands out, and this does not change that.Commits
docs(edit): the contract, written before any code.feat(edit): the function, the CHANGELOG entry, tests intests/test_edit.py, and three real-merge pairings intests/test_edit_merge.py.test(edit):tests/test_edit_set_cell.py. It was written from SPEC.md and docs/editing-api.md at b34b957 by an author who saw no implementation. The author worked in agit archiveof SPEC.md, docs/editing-api.md, CONTRIBUTING.md and tests/test_edit.py, with no.gitand noreference/. It lands as written, with the same sha256 (09f8e72ae5d0c962…) in the export and here. Its first run against 96fab16: 145 passed, 145 cases.docs(edit): four outcomes the first draft left undetermined, as the author of those cases listed them. Right- or left-placed text gets one space on the far side. The odd spare space of centred text goes on the right. "Padded" means more than two characters wider than the text. The no-op comparison is in NFC. Each has a test intests/test_edit.py, so these four are not blind cases.docs(edit): one definition of "padded" across rule 3. The code has always used one test,_padded, for header and data cells, so the header-cell behaviour is 0.5.0's; only its description changes. A test pins| id | qq|renamed toqgiving| q|, and a padded table's| 33|set to4giving| 4 |.fix(edit):text,row_keyandcolumnare type-checked before parsing. Before,text=NoneraisedAttributeError, andcolumn=3raised aTypeErrorthat did not name the argument. Two tests pin refusal rule 2's order:" 1"in a computed column and" r_02"in the key column are bothnot-representable. Without the up-front check they becomevalue-in-computed-cellandduplicate-row-id.test(edit): three more merge pairings. Two edits setting one cell to different values conflict. Setting one cell to the same value on both branches merges cleanly. Aset_cellagainst a delete of the same row conflicts.docs(edit): the CHANGELOG states when the width is kept, rule 3 says an unpadded header cell keeps its own spaces, and the bullet states theTypeErrorand the key's identity.Mutation sweep
just mutantsdoes not coveredit.py; its targets aretable.pyand the CSV modules. So this is a separate sweep.reference/andtests/into a fresh directory per mutant and applies exact-anchor replacements toedit.py.PYTHONPYCACHEPREFIX.tests/test_edit_set_cell.py tests/test_edit.py tests/test_edit_line_endings.py tests/test_edit_merge.pyat 9b664e7.The verdict lines are as printed, without the per-test failure lines.
One mutant survives.
read-back-offremoves the last check, which compares the written row with the request. No input I tried reaches it: every byteset_cellwrites isescape_cell(value)plus spaces,split_rowundoes the escape, and a trailing\gets a space. It stays as a guard, as the read-back inrename_columndoes, so that a gap in the rules is refused rather than written.trim-refusal-offis not equivalent: the two rule-2 tests in 2e39136 kill it.Gate
At each commit, the tree was gated exactly as committed, with the rest stashed.
just checkexited 0 every time.just testexited 0 with 2 skipped each time:just conform,conform-alt,conform-csvandjust mutantswere not run, because nothing they read changed: SPEC.md, the case trees,table.py,csvmode.pyandsidecar.py.rowspec_althas no editing API.