Skip to content

features and fixes - #204

Merged
KotlinIsland merged 7 commits into
mainfrom
features-and-fixes
Sep 7, 2026
Merged

KotlinIsland merged 7 commits into
mainfrom
features-and-fixes

Conversation

@KotlinIsland

Copy link
Copy Markdown
Owner

No description provided.

`by compile` wrote the generated C, the extensions and `by.h`, and nothing
else. a compiled module reads its data files relative to itself, so a tree
holding artefacts alone failed on the first `open`, naming a path in a
directory the author never wrote anything to.

carrying the rest of the project over is not enough on its own. the tree has
to be one command's whole answer, because `finish` deletes what the last run
wrote and this one did not — and that is only correct when this run authored
the tree. so `compile` now writes everything `build` writes, the transpiled
python and the sourcemap and the build record included, and the extensions
besides. python's finder prefers an extension to source, so the modules that
were named load natively and the rest are interpreted.

three things follow, each of which was broken while the tree was artefacts
alone:

- `by compile one.by` produced a tree that could import `one` and nothing
  else written in basedpython, since the other modules arrived as `.by`
- `by compile a.by` then `by compile b.by` left neither compiled: the second
  run pruned the first's artefact and put no python in its place
- `by compile` into a directory `by build` had written deleted the sourcemap
  and the build record, and `by restage` then refused the tree — which took
  the language server's single-file re-stage down with it

`Artifact` now reports what was written rather than what would have been.
`--emit-c-only` used to name the extension it did not build, at a path no
real build uses, so the caller found nothing there and pruned the extension a
previous run had left.

a build output is recognised by the `.by-manifest` it carries rather than by
its name, so a tree written to any `--out` is turned away as a source. the
directory is no longer created before the command knows it has anything to
put in it, so a run that fails early leaves nothing behind.

the default output directory is `build/` for both commands, which the
standard python .gitignore already covers; `out/` never was. the editable
install backend stages into the same constant and moves with it.
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

ecosystem check

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

by ecosystem round-trip

base: 606cdb3758fa340b8a9f80243cd3079b102690ff (merge base) → head: 204/merge

regressions: 0, changed: 63, improvements: 132, error changes: 0 (across 24927 files in 148 projects)

⚠️ 15 project(s) fail to round-trip on both base and head, so this check says nothing about them.

ℹ️ changed round-trip output

pytest-autoprofile — .ci-helper-funcs.py
(only produced on head)
pytest-autoprofile — _by_sourcemap.py
(only produced on head)
pytest-autoprofile — lib/pytest_autoprofile/__init__.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_doctest.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_json.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_multiprocessing.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_patches.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_test_util_capture_warnings.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_test_utils.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_typing.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_warnings.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/_xdoctest.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/importers.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/option_hooks.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/plugin.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/profiler.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/rewriting.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/setup.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/startup_hook.py
(only produced on base)
pytest-autoprofile — lib/pytest_autoprofile/utils.py
(only produced on base)
pytest-autoprofile — pytest_autoprofile/__init__.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/_json.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/_patches.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/_test_util_capture_warnings.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/_typing.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/_warnings.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/_xdoctest.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/plugin.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/rewriting.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/setup.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/startup_hook.py
(only produced on head)
pytest-autoprofile — pytest_autoprofile/utils.py
(only produced on head)
pytest-autoprofile — tests/__init__.py
(only produced on head)
pytest-autoprofile — tests/conftest.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/math_pkg/__init__.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/math_pkg/arithmatics.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/math_pkg/taylor_series.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/my_test_package/__init__.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/my_test_package/test_math.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/my_test_package/test_string.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/string_pkg/__init__.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_autoprof/string_pkg/utils.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_doctest/packages/my_pkg/__init__.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_doctest/packages/my_pkg/classes.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_doctest/packages/my_pkg/funcs.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_doctest/tests/test_misc.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_global_prof/complex_env/script.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_global_prof/complex_env/test_toggling.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_global_prof/simple_env/modules/mymod.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_global_prof/simple_env/tests/test_func.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_profiler/profiler_test_module.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_subprocess/packages/myfork.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_subprocess/packages/mymodule.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_subprocess/packages/myprocess.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_subprocess/tests/conftest.py
(only produced on head)
pytest-autoprofile — tests/test-modules-and-tests/test_subprocess/tests/test_func.py
(only produced on head)
pytest-autoprofile — tests/test_ast.py
(only produced on head)
pytest-autoprofile — tests/test_doctest.py
(only produced on head)
pytest-autoprofile — tests/test_global_prof.py
(only produced on head)
pytest-autoprofile — tests/test_misc.py
(only produced on head)
pytest-autoprofile — tests/test_patches.py
(only produced on head)
pytest-autoprofile — tests/test_profiler.py
(only produced on head)
pytest-autoprofile — tests/test_subprocess.py
(only produced on head)

💥 fails to round-trip (unchanged from base)

  • apprise: build: killed: exceeded 11.7365GB memory budget
  • cryptography: build: killed: exceeded 11.7365GB memory budget
  • dedupe: build: killed: timed out after 900s
  • jax: build: killed: exceeded 11.7365GB memory budget
  • manticore: reverse: killed: exceeded 11.7365GB memory budget
  • meson: build: killed: exceeded 11.7365GB memory budget
  • openlibrary: reverse: killed: timed out after 900s
  • pandas: build: killed: timed out after 900s
  • pandas-stubs: build: killed: timed out after 900s
  • schemathesis: build: killed: exceeded 11.7365GB memory budget
  • scikit-learn: build: killed: exceeded 11.7365GB memory budget
  • scipy: build: killed: exceeded 11.7365GB memory budget
  • setuptools: build: killed: exceeded 11.7365GB memory budget
  • sympy: reverse: killed: exceeded 11.7365GB memory budget
  • xarray: build: killed: exceeded 11.7365GB memory budget

✅ improvements (failed on base, now builds)

  • DateType <build>
  • Expression <build>
  • PyGithub <build>
  • PyWinCtl <build>
  • Tanjun <build>
  • aiohttp <build>
  • aiohttp-devtools <build>
  • aioredis <build>
  • aiortc <build>
  • alectryon <build>
  • alerta <build>
  • altair <build>
  • antidote <build>
  • anyio <build>
  • archinstall <build>
  • artigraph <build>
  • arviz <build>
  • async-utils <build>
  • asynq <build>
  • attrs <build>
  • bandersnatch <build>
  • beartype <build>
  • bidict <build>
  • black <build>
  • bokeh <build>
  • boostedblob <build>
  • build <build>
  • check-jsonschema <build>
  • cibuildwheel <build>
  • cki-lib <build>
  • cloud-init <build>
  • colour <build>
  • com2ann <build>
  • comtypes <build>
  • cwltool <build>
  • dacite <build>
  • discord.py <build>
  • django-modern-rest <build>
  • django-stubs <build>
  • django-test-migrations <build>
  • docstring-adder <build>
  • downforeveryone <build>
  • dragonchain <build>
  • dulwich <build>
  • egglog-python <build>
  • flake8 <build>
  • flake8-pyi <build>
  • freqtrade <build>
  • git-revise <build>
  • graphql-core <build>
  • httpx-caching <build>
  • hydpy <build>
  • hydra-zen <build>
  • ibis <build>
  • ignite <build>
  • imagehash <build>
  • isort <build>
  • itsdangerous <build>
  • janus <build>
  • jinja <build>
  • koda-validate <build>
  • kopf <build>
  • kornia <build>
  • materialize <build>
  • mitmproxy <build>
  • mkdocs <build>
  • mkosi <build>
  • mongo-python-driver <build>
  • more-itertools <build>
  • mypy <build>
  • mypy-protobuf <build>
  • mypy_primer <build>
  • nionutils <build>
  • nox <build>
  • numpy-stl <build>
  • operator <build>
  • optuna <build>
  • paasta <build>
  • packaging <build>
  • pandera <build>
  • paroxython <build>
  • parso <build>
  • pegen <build>
  • pip <build>
  • poetry <build>
  • porcupine <build>
  • ppb-vector <build>
  • prefect <build>
  • psycopg <build>
  • pwndbg <build>
  • pybind11 <build>
  • pycryptodome <build>
  • pydantic <build>
  • pyinstrument <build>
  • pyjwt <build>
  • pylox <build>
  • pyodide <build>
  • pyp <build>
  • pyppeteer <build>
  • pyproject-metadata <build>
  • pytest <build>
  • pytest-robotframework <build>
  • python-chess <build>
  • python-htmlgen <build>
  • python-sop <build>
  • pywin32 <build>
  • rclip <build>
  • rich <build>
  • schema_salad <build>
  • scikit-build-core <build>
  • scipy-stubs <build>
  • scrapy <build>
  • sockeye <build>
  • speedrun.com_global_scoreboard_webapp <build>
  • sphinx <build>
  • starlette <build>
  • static-frame <build>
  • stone <build>
  • strawberry <build>
  • svcs <build>
  • tornado <build>
  • trio <build>
  • twine <build>
  • typeshed-stats <build>
  • urllib3 <build>
  • vision <build>
  • websockets <build>
  • werkzeug <build>
  • xarray-dataclasses <build>
  • yarl <build>
  • zipp <build>
  • zope.interface <build>

⏭️ skipped

  • AutoSplit: setup failed: setup_primer_project.py failed for 'AutoSplit': Cloning into '/tmp/tmpacboz30t/AutoSplit'... Using CPython 3.12.3 interpreter at: /home/runner/work/_temp/setup-uv-cache/environments-v2/…
  • CPython (Argument Clinic): skipped: 2330 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • CPython (cases_generator): skipped: 2330 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • CPython (peg_generator): skipped: 2330 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • core: skipped: 18638 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • dd-trace-py: skipped: 2979 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • ecosystem-analyzer: setup failed: setup_primer_project.py failed for 'ecosystem-analyzer': Cloning into '/tmp/tmpwsmcxbzk/ecosystem-analyzer'... Using CPython 3.12.3 interpreter at: /home/runner/work/_temp/setup-uv-cach…
  • pylint: skipped: 2433 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • rotki: skipped: 2598 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory
  • spack: skipped: known to spike past the runner's memory during by build (faster than the watchdog can react)
  • streamlit: setup failed: setup_primer_project.py failed for 'streamlit': Cloning into '/tmp/tmp1448p8av/streamlit'... Updating files: 45% (4476/9902) Updating files: 46% (4555/9902) Updating files: 47% (4654/99…
  • zulip: skipped: 2028 .py files exceeds --max-project-py-files (2000); by build would not fit in the runner's memory

a `by check` builds a project database, resolves an environment, parses and
infers every file, and then exits and throws all of it away. a language server
running for that project has that work done and kept current, so `by check` now
asks it first and says "using project server information" when it does. on an
11,850-file project that is 3.8s down to 0.8s

a server publishes a loopback port and a secret into a per-user directory as it
starts, and takes the record away as it exits. a caller reads the directory,
sends the project root and the configuration it resolved, and gets the
diagnostics back already rendered — rendering needs the database, and relative
paths need the caller's own directory, so both cross the wire

the answer only crosses when it is the answer a cold check would have given, so
what the two sides compare is what each actually ended up with: the merged
options across every layer, the resolved python version, platform and search
paths, and the two database inputs that are not options at all — `force-exclude`
and whether the diagnostics explain where each rule was turned on. an
environment is discovered as much as configured, out of `VIRTUAL_ENV`, a `.venv`
beside the project or uv's answer, and a server started by an editor discovers
it from the editor's environment: two processes that agree about every option
can still be checking against different site-packages. the caller therefore asks
after building its database rather than before, since resolving all of that is
the database's job, and building one is cheap next to checking with it

the server also refuses a different build, an open buffer the editor has not
saved — every open document, notebooks included, and buffers outside the project
root this database has read — and a session diagnosing only its open files; that
last one is the default, so this needs `diagnosticMode: workspace`. it re-reads
the file system first, because its own picture of the project is whatever the
editor's watcher reported and the caller can see the disk directly, and it does
so between the two passes over a request so a check that was going to be refused
does not make the editor re-pull its diagnostics. `--no-server` and
`BY_NO_PROJECT_SERVER` switch it off, the second on both halves

the caller rules itself out for anything that is not a whole-project check:
`--watch`, a fix mode, explicit paths and a memory report. a failure writing the
answer is a failure rather than "no server answered", so a broken pipe no longer
prints half the diagnostics and then checks the whole project again

connections are served one thread each, both directions are bounded in size and
time, the token is compared in constant time, and the answer repeats the token
back: a record outlives a killed server, and anything that binds the freed port
could otherwise have answered "All checks passed!". a build is identified by its
executable as well as its version, since the version string does not change when
a dirty tree is rebuilt

the tests build their requests from a real cold database the way the command
line does, and assert that the rendering matches that database's own — the claim
the whole design rests on. `scripts/check_project_server.py` drives a real
server over stdin and stdout and asserts the same thing across two processes,
plus that every flag which changes a check refuses
`or` and `and` inside a type expression spell union and intersection, so the
boolean rules have nothing to say about them: `type A = "foo" or "bar"` was
SIM222 "use `"foo"` instead of `"foo" or ...`". A bare string in a type position
is the literal type, not a forward reference, so UP037 offered to strip the
quotes off `c: "c"` and leave a name. A match type's `case` arms are types, so
their `:` opened no suite for E701 to report.

`is` is a parametric type test rather than identity, and `===` is the spelling
that always compares identity — F632 read the two as one, panicked on `1 === 1`
because it could not find that operator's token, and offered to rewrite the type
test `1 is T` as `1 == T`. E721 told a `.by` file to reach for python's two
operators rather than basedpython's.

A destructuring binder always binds, so `for Point(x, y) in points` and `def
f(Point(x, y): Point)` now bind their captures the way the equivalent python
unpacking does instead of as plain assignments F841 reports.

A type parameter is not a constant, however the one-letter convention spells it,
so `assert T == int` is no longer a yoda condition.

ty spelled a `.by` file's types in python syntax whenever a `.py` file that
imports it was checked first: the diagnostic that inference cached said
`Literal["b"]` rather than `"b"`. The spelling now comes from the file being
inferred rather than from whoever asked for the inference, which is what keeps it
out of the salsa cache's blind spot.
`TypeVar("T", bound=A & B)` type checked clean and transpiled to itself, so the
emitted python evaluated `A.__and__(B)` at import. `NewType`, `TypeAliasType`,
`ParamSpec`, `TypeVarTuple` and the functional `NamedTuple` and `TypedDict` were
all silent the same way, for `?`, `not`, `or`/`and` and the rest of the type
syntax as much as for `&`: the transpiler's type-expression walker recognised
twelve positions, and none of them was an argument.

Which arguments those are is now the type checker's answer rather than a list the
transpiler keeps. `CallTypeForm` names the constructs and their type-expression
arguments in one place, inference dispatches on it, and the walker asks it, so a
form ty learns to check is one the transpiler lowers without further change.

The mdtest that covers this is one the divergence harness runs: with the walker's
hook removed it fails with `TypeError: unsupported operand type(s) for &`, which
is what the miscompilation did to anyone who wrote it.
the right-hand side of `is` in a `.by` file is a type position, but it was
inferred as a value expression — so none of the type-expression validation ran
on it. an annotation and a type test rejected different things: of 23 targets,
annotation position and `cast` position rejected the same 7 and `is` rejected
none, lowering each to a program that raises.

the target is now inferred as a type expression, and the type it names is
classified for a runtime form. what the runtime can check exactly is lowered to
that check; what it can only partly check is rejected rather than approximated,
since a test narrows and has to earn its `True`:

- a class, `type[C]`, a union, `None`, a literal, an enum member, a template
    literal type and a conformance interface all lower to the check that decides
    membership of the type. a literal pins the class too, since `1 == True`
    would otherwise let a `bool` satisfy `Literal[1]`
- `Any`, a callable type, a `TypedDict`, an intersection and a protocol with an
    unnameable member are rejected by `erased-type-check`, each with its own
    reason. `float`, `complex`, `LiteralString` and `type[Any]` each report the
    true reason rather than "has no runtime form", and a bare `Callable` lowers
    to `callable()`
- a tuple target now means the tuple *type*. `v is (int, str)` answered `True`
    for an `int`, because it lowered to `isinstance`'s classinfo tuple

the emitted check needs a *value*, and the type the source names parts company
with one more often than it looks like it does: `type AL = int` evaluates to a
`TypeAliasType`, `Literal[Color.RED]` and `Annotated[int, "x"]` to special
forms, `list[Any]` to a subscripted generic — none of which `isinstance` will
take. so the spelling is rebuilt from the type rather than passed through, and
the source is used only where rebuilding cannot reach: a class the emitting
module cannot name as a global, and only when the source wrote a plain dotted
name.

a sealed hierarchy now declares each variant in the enum body —
`Circle: ClassVar[type[_Shape_Circle]]`, `Point: ClassVar[_Shape_Point]` —
which is what lets the checker see that one is a class and the other a value,
and the lowering asks it rather than matching the written name. matching the
name both over-fired (a local binding shadowing the enum was rewritten anyway)
and under-fired (`S = Shape; s is S.Point` was missed, emitting an `isinstance`
against a singleton that raises). the declarations also clear the
`unresolved-attribute` a lowered enum used to report on its own variants.

`basedpython_is_keeps_identity` goes away with the value position it existed to
serve: `None`, an enum member and a literal all fall out of their own types.

the spelling now rides on the ast. `===` / `!==` and the `is` keyword parse to
one `CmpOp`, and four places told them apart by reading the source text between
the operands — so a parenthesised operand, a line continuation or a comment
silently turned a type test back into python identity, and `is` and `is not`
disagreed about the same spelling. `ExprCompare::identity_ops` records which
operators were written `===` / `!==`; the checker, the narrower, the formatter
and the transpiler all read it, and the parser builds the side table lazily so
an ordinary comparison allocates nothing. a chained type test is now a syntax
error, but only where a type test is something that gets handed on: python
chains `a is int is str` into `a is int and int is str`, whose second half asks
whether the class `int` has the type `str`, while `a < b is None` hands nothing
on. narrowing skips a chain the parser rejected instead of making its body
unreachable.

`generate_comparison` printed `is` for source that wrote `===`, so E713/E714
rewrote an identity comparison into a type test through a *safe* fix, and
E711/E712 destroyed every `===` in a chain; it takes the spelling now.
`assertIs` writes `===`, and BY003 rewrites an `isinstance` only when its
argument has the shape of a type expression — the predicate the reverse
direction already used. F632 reads the recorded spelling as well, rather than
scanning the tokens for it a second time; the scan is left to place the fix,
which is what lets `1 === 1 !== 2` offer `==` for one operator and `!=` for the
other. `identity_swap` no longer decides on ast shape, so an
f-string, a call or a unary-minus target takes the same path as a name; its
replacement range accounts for parentheses around an operand, which produced
invalid python for `(a) is not str`, and an unspaced `a===b` brings its own
spaces.

a settled test types as its answer rather than `bool`, which is what the
editor's data-flow view reads. the reverse direction writes an `isinstance`
back out only when its argument is something a type expression can say, mapping
a classinfo tuple to a union.

also: a recursive alias overflowed the stack on `v is A` — `type A = int | B`
with `type B = str | A` — so the alias recursion carries what it has already
opened; the template-literal pattern regex and the static matcher disagreed on
`-0`, where `str(-0)` is `"0"` so the sign belongs to the non-zero alternative;
and control characters are escaped rather than passed through, since CPython
refuses a source containing a NUL.

the runtime contract each target kind lowers to is executed end to end in
`parametric_is_runtime`, rather than only type-checked.
the entry is a folded scalar, which only folds the lines indented to match its
first one — the deeper-indented continuations keep their newlines, so bash was
handed `&& RUSTDOCFLAGS=...` at the start of a line and refused the whole
command. every commit failed the hook with a syntax error rather than a doc
error. the continuations line up with the first line again.
@KotlinIsland
KotlinIsland merged commit d31f3bb into main Sep 7, 2026
52 of 53 checks passed
@KotlinIsland
KotlinIsland deleted the features-and-fixes branch September 7, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant