Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 20:57
· 280 commits to main since this release

Benchmark

I did not run the benchmark again for this release. Its numbers are still 0.1.0's and are for the
Gemini pack alone: scan precision 100.0% and recall 98.4% over 64 usages in five repositories, and
25 of 277 usages (9.0%) migrated in 20 repositories with no wrong edit, on the
results page. The
scanner and the rewriter have changed since, so read those numbers as dated. The rule for a held
generation config is measured without the tests: a fix dry run over the 23 round-1 repositories at
their pinned commits gives the rows 0.3.0 gave, except 26 that moved from needs_review to auto
(20 in kvcops__Virtual-Herbal-Garden, 6 in
Momen2410__Question-Answering-ChatBot-Using-Gemini1.5). That says what is written, not whether it
passes a repository's tests, which a new round measures, with those two repositories in dev.
openai/openai-0-to-1 and py-pdf/pypdf2-to-pypdf have no benchmark. They rest on fixtures, on
each change measured against both library versions installed side by side, and on a weekly job that
runs their facts on the oldest and newest releases.

Unsupported patterns

docs/KNOWN_ISSUES.md lists
what I know about and have chosen not to fix yet. The gaps listed under 0.3.0 stand, except that
Python 3.15 is supported. The Gemini pack still withholds a model and the chat it starts in one
chain (GenerativeModel(...).start_chat(...)), and a generation config or safety settings held by a
name that is assigned twice, changed after its assignment, passed on, read by anything but the call
or taken as a parameter.

Added

  • fix --apply and verify print one line on stderr before each phase of tests: how many
    commands are about to run and after how many seconds each is stopped, so a long suite no longer
    looks like a hang. It names no command, since one can carry a credential, and stdout is unchanged.
  • The Gemini pack rewrites a generation_config or safety_settings held by a name, where it
    refused as generation_config_not_static or safety_settings_not_static (COVERAGE gap 29).
    The name needs one plain assignment, a mapping, a list of mappings or a GenerationConfig(...),
    and no reader but that argument of a call the rule rewrites, so nothing else sees the value or
    changes it between the assignment and the call. The keys are written into each call. A module's name stays where it
    is, and a GenerationConfig(...) it holds becomes the new SDK's GenerateContentConfig(...);
    a function's own goes with the model, and so does the GenerationConfig(...) it holds. A name
    assigned twice, changed by a stored key or update, passed on, read elsewhere or a parameter is
    still refused. A module's name that another module changes after importing it is not seen.

Changed

  • scan and fix do less work on a large repository and write the same findings. The pass that
    finds a module another one reads (model_object_read_elsewhere) parses only the modules that
    spell both the module's name and a name read, and walks each once, where it walked every parsed
    module again for each model (9.5 s to 3.5 s on 800 synthetic modules). fix no longer parses
    a file that only mentions the library in prose or a comment: 28 s to 4 s on 200 files, 180 of
    them such. A pack the repository does not use, with no code to migrate and no pin to judge, no
    longer parses the files that name a module only its legacy distribution installed, and one that
    does reads imports only where a statement can hold one (11 s to 6 s over 5,000 files).
    End to end, fix takes 4.6 s on a repository of 1,000 files where 200 use the legacy SDK, which
    took 40 s, and 4.4 s on one file of 800 calls, which took 15 s.
  • patch.diff for a file of more than four million line pairs (about 2,000 lines each side) is
    made without the matcher's repeated-line pass, which cost the square of a repeated line's
    count. Below that size the hunks are the ones 0.3.0 wrote; above it a hunk can be larger or
    split differently, and git apply reads the patch the same.

Fixed

  • A second obelize undo of a run exits 0 when every file already holds its original, where it
    exited 4 as if something had gone wrong, and it leaves the first undo.json, which says what
    was put back, instead of replacing it with a row of hash_mismatch skips. undo.json's
    exit_code is now derived from files that were reverted or already original.
  • The note obelize verify appends to REPORT.md names the commands it ran, on one line, so a
    reader of the report is not left with the first run's. A command with a backtick or a newline in
    it stays inside its code span: the fence is longer than any run of backticks, and the newline
    is spelled out.
  • A run folder that an older obelize wrote and this one cannot read now names the release that
    wrote it and the command that undoes the run (uvx obelize@0.2.0 undo --run <id>), where undo
    and verify only said the record was unreadable.
  • The terminal output of scan and fix has less in it that nobody acts on. A planned file shows
    path (N hunk(s)) and not the list of rules that produced the hunks, which REPORT.md and
    plan.json keep. An eligible row has a blank reason, where it printed the code that resolved
    its name. A dry run prints no Verification: line and its REPORT.md no empty phase, since no
    test was attempted, and two packs that suggest the same tests say them once.
  • fix --apply no longer refuses a dirty tree when it has nothing to write. A repository where
    every finding is held back used to exit 5 with "uncommitted changes", which hid the real
    reason; it now exits 4 with the findings. A configured model keeps the gate, since a
    proposal could write.
  • fix --apply --verify "<cmd>" looks for the command's program before it reads a file. A typo, or
    a .venv/bin/python that is not there, is a usage error (exit 2) and nothing is written; it
    used to write the patch and end inconclusive (exit 6) with command_not_executable. A
    command from .obelize.yml or the allowlist is still started, and still ends that way.
  • When the commands you pass to --verify change a tracked file, the refusal of fix --apply
    says they did, where it asked you to commit or stash changes you had not made. The tree was
    clean when the run began, which git_dirty: false already recorded.
  • A verification command that timed out gets a hint naming --timeout <s> and the default of 600
    seconds, which the flag's help and docs/CLI.md now state.

Security

  • fix --apply tries to make a file in each directory it will write into before it writes any,
    and refuses the whole apply as unreadable for a directory that takes none. A read-only
    directory used to stop the write partway and leave a half-migrated tree; a partial write that a
    race still causes now ends with obelize undo in its Next: line.
  • A file name that holds a control or format character, a backtick or a pipe is skipped as
    unusable_name. Printed as it was, an escape sequence in a name moved a terminal's cursor or
    colours, and a backtick or a pipe ended a code span or a table cell in REPORT.md. Its name is
    spelled out (\x1b) in the skip, and the detail of a limitations row is escaped as a pack's
    text is.
  • .obelize/latest is replaced by a rename below a .obelize opened without following a link,
    where a check for a link and then a write could race a swap; a link there is now replaced and
    what it named is not written. .obelize.yml is read without following a link, so a repository
    cannot make a parse error quote another file. A relative XDG_CONFIG_HOME is ignored, as the
    specification says, where it could have put the user's model block, allowlist and pack
    directories under the repository being scanned. Approving a command appends a row and keeps the
    rows already there, with their command text, where it rebuilt the file from the keys.

Python 3.15

  • obelize supports Python 3.15, which CI runs on Linux, macOS and Windows and the classifier names.
    uv.lock moves to pydantic 2.14.1 and pydantic-core 2.50.1, which have 3.15 wheels, where the
    older pair made pip compile pydantic-core with Rust. PyYAML has no 3.15 wheel yet, so pip builds
    it in pure Python, with no compiler. An Intel Mac installs libcst 1.8, which has no 3.15 wheel,
    so it compiles that with Rust there; the other systems install libcst 1.9's wheel.
    tomllib reads TOML 1.1 from 3.15 on, so a pyproject.toml that uses it has its Python floor
    read there, and is still reported as unreadable on 3.12 to 3.14.

Packs

  • gemini/google-generativeai-to-google-genai 0.3.0 writes google-genai>=1.55 where it wrote
    >=1. Releases 1.0 to 1.54 do not hold what the pack claims: 1.54 has no
    enable_enhanced_civic_answers, one of the fifteen generation-config keys the pack moves into a
    config, so code it wrote could fail on an install >=1 admitted. A google-genai line you
    already declare is left as you wrote it. The weekly job now runs the pack's facts on 1.55.0 and
    on the newest google-genai, and the py-pdf/pypdf2-to-pypdf facts on the newest pypdf; the
    Gemini facts also check that every keyword a rewritten call carries is one its method takes.