Skip to content

Install as a kconfiglib package, not loose modules - #55

Merged
jserv merged 5 commits into
mainfrom
pkg-install
Sep 20, 2026
Merged

jserv merged 5 commits into
mainfrom
pkg-install

Conversation

@jserv

@jserv jserv commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Installing Kconfiglib dropped fifteen unprefixed modules straight into site-packages, so a shared site-packages ended up holding this project's defconfig.py and setconfig.py under names any other distribution is free to want for itself. A FreeBSD port maintainer packaging this hit it and filed #53. The sources stay flat in the checkout, so python menuconfig.py keeps working from a clone and the README links still point at files that exist; build_py maps them into a kconfiglib package at build time instead. That breaks import menuconfig against an installed copy, so this goes out as 15.0.0, with from kconfiglib import menuconfig replacing it. Nothing changes for code that only imports kconfiglib itself.

Four adjacent changes ride along. The presentation helpers that menuconfig.py and guiconfig.py held byte-identical copies of move into uicommon.py. The terminal interface's twenty module globals become one _State built fresh per run, which is what finally made the cursor and scrolling logic reachable from a test, and which also stops a second run in one process inheriting the first one's scroll position. A help, prompt, type or visible if written under a menu or comment used to escape as an AttributeError traceback rather than a parse error, so a typo produced a Python stack trace with no file or line in it; five properties were affected and they share one guard now. The benchmark script that had been sitting in the tree unused now runs in CI.

Verified on Python 3.14 on macOS: 470 tests pass, up from 306. Each commit was exported with git archive and tested standalone, so the series bisects. The two refactors are behavior-identical rather than merely test-passing, with _node_str, _shown_nodes and _info_str over five test Kconfigs in four display modes producing output byte-identical to the base branch. The parser guards came out of the new fuzz tests, which found all five crashes. One further crash turned up that is not this branch's: a config listed by two separate choice blocks sends visibility evaluation around a cycle until the Python stack runs out, which reproduces unchanged on main, so it is recorded with a minimized reproducer and left for a separate change rather than fixed here. The wheel builds with 16 package members and no loose files, installs into a clean venv, and all 13 console scripts resolve.

Left out deliberately: the kernel conformance suite, which needs a Linux tree and the C kconfig tools and is covered by its own CI lane, and guiconfig.py's own module globals, which want the same treatment as menuconfig.py's and are now tracked separately.

Closes #53

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

Installing dropped fifteen unprefixed modules straight into site-
packages: kconfiglib.py next to menuconfig.py, rawterm.py, and a
defconfig.py and setconfig.py that any other distribution is free to
want for itself. A port maintainer packaging this for FreeBSD hit it.

The sources stay flat in the checkout, so running python menuconfig.py
from a clone keeps working and the README links still point at files
that exist. build_py maps them into the package at build time instead.
Globbing the repository root would have swept up setup.py and lint.py,
so the module list is explicit. Editable installs default to strict
mode, because the lenient one maps the package name at the root where
there is no __init__.py to find, but an explicit editable_mode is left
alone.

This breaks importing menuconfig from an installed Kconfiglib, so the
version goes to 15.0.0. Importing it from the kconfiglib package
replaces that. Nothing changes for code that only imports kconfiglib
itself.

One wart comes with keeping the layout: the root kconfiglib.py shadows
the installed package whenever the checkout is the working directory,
which the README now says and the CI check works around by testing from
elsewhere.

The sdist also grows the whole tests tree. It shipped the test modules
but not conftest.py, the helpers or the Kconfig fixtures, so the suite
could not run from the tarball at all, which is what a packager building
from it needs.
menuconfig.py and guiconfig.py share 38 function names, and 17 of those
had byte-identical bodies: the whole expression-formatting chain that
renders every expression either tool prints, plus the range, include
path and choice symbol blurbs from the info dialog. Two copies means a
fix applied to one silently leaves the other wrong, and the 21 names
that are not identical show that has already been happening.

Eleven are pure functions of a MenuNode or an expression, with no
terminal and no Tk in them, so they move to uicommon.py unchanged. The
remaining six read module globals belonging to one tool or the other and
stay put for now. Both tools import the module rather than aliasing each
name, so adding a helper is a one-place edit and the call site says
where the answer comes from. Together the two shrink from 6653 lines to
6334.

Putting them in kconfiglib.py instead would have avoided the import
dance that guiconfig.py now needs, but the core library is already the
densest file here and these are presentation helpers, not configuration
ones. rawterm.py set the precedent for extracting a module the
interfaces share.

Three tests were parametrized over both tools to check the two agreed,
which is a tautology once there is one copy, and the guiconfig half
dragged in tkinter to re-run the same function. They call uicommon
directly now, as do two of the range tests. The checks that validate an
entered value keep their pair, being still a real copy in each tool.
Twenty module globals, assigned through 64 global statements scattered
across the file, with the selected index and the scroll offset written
from a dozen places each. Nothing could be called before the main loop
had set them up, so the cursor and scrolling logic had no tests at all:
reaching it meant starting a terminal. That is most of why menuconfig.py
sits at 11 percent coverage while the library it drives is at 75.

They become fields on a _State that the entry point builds fresh per
run. The rewrite was driven off the syntax tree rather than the text,
after checking that no function binds any of the twenty names locally,
so every one of the 314 references was unambiguously the global it
looked like. Behavior is unchanged: the row renderer, the shown-node
walk and the info dialog over five test Kconfigs in four display modes
produce output identical to before, byte for byte.

Building it fresh also fixes something. The old globals survived between
runs, so a second run in one process inherited the first one's scroll
position, dialog state and show-all mode. The search caches needed an
explicit clear at entry for the same reason, so that goes away.

tests/test_uinav.py is what this was for. It builds a _State, hands it a
window with a height, and drives entering and leaving menus, the four
selection commands and the scroll offset directly. One case pins the
invariant the whole offset dance exists for, that the selection never
leaves the window; another pins the clamp that catches a terminal
shrinking while the user is inside a submenu.

Building the fixture's node list has to come after installing the state,
not before, because the walk reads show-all off the module rather than
off anything passed in. Getting that backwards silently dropped the
promptless node the fixture exists to show.
cubic-dev-ai[bot]

This comment was marked as resolved.

A help, prompt, type, def_bool, transitional, option or visible if
written under a menu or a comment escaped as an AttributeError traceback
instead of a parse error. Menu and comment nodes carry a plain constant
as their item rather than a symbol or a choice, and they leave the help
and visibility slots unset, so the handlers reached for attributes that
were not there. A typo in a Kconfig file produced a Python stack trace
with no file or line in it. Alongside those, the modules property raised
TypeError on any symbol that is not MODULES, because its warning passed
a filename and a line number to a helper that takes a single location.

Which node kinds may carry which property is a table now, checked once
at the top of the property loop, rather than nine hand-written kind
tests scattered through the dispatch. The table is complete because it
is a table: the chain only ever covered the cases somebody remembered.
It also reuses the symbol-or-choice frozenset that already existed.
Visibility stays a separate check, since menu and comment items are both
plain constants and telling them apart is an identity test, not a class
one.

Nothing that parsed before parses differently. Every input newly
rejected raised AttributeError or TypeError before, which is not
acceptance.

tests/test_fuzz.py found all of them. Generating token soup was tried
first and is useless here, because every case dies on line one and the
block and property parsers are never reached. So it builds a
structurally valid Kconfig tree and then corrupts it, which puts about
half of each run past the end of the parser. The contract it asserts is
the only one worth asserting on random input: the parser either accepts
the file or raises its own error, never anything else.

A blown Python stack is not acceptance either, so it is only forgiven
for the one shape known to cause it, where a config listed by two
separate choice blocks sends visibility evaluation around a cycle. That
defect reproduces on the base branch and is left for its own change.
Matching the shape rather than a seed number means changing the
generator cannot quietly widen the exemption.
scripts/benchmark.py has been sitting in the tree unused. The collector
and probe cache work that went in recently came with specific numbers
attached, twenty small parses dropping from 0.81s to 0.01s among them,
and nothing was watching whether they held. The selftest job now runs it
and keeps the JSON as an artifact, so a later regression in the parse or
redraw paths can be attributed instead of merely noticed.

Once per job, not twice: asking for JSON returns before the table is
printed, so a second run would re-measure everything and the table in
the log would not be the numbers in the artifact.

Still no pass/fail threshold, and the numbers want reading with care.
Phases on this fixture move by around 7 percent run to run on an idle
machine and around 30 on a busy one, and the spread is cross-process
fixed cost rather than the timing loop, so a shared runner is the busy
case. That attributes a large regression and nothing subtler. Pointing
it at a tree big enough for the phases to run in milliseconds would
tighten it.

The coverage floor goes from 35 to 38 against a total that is now 41,
and uicommon.py joins the measured set. The comment explaining how to
reproduce the runner figure locally stays, because it is easy to take a
number from a plain local run and set a floor that fails every job here.
@jserv
jserv merged commit 6d9102a into main Sep 20, 2026
20 checks passed
@jserv
jserv deleted the pkg-install branch September 20, 2026 17:30
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.

package installs modules directly into sitelibdir

1 participant