Skip to content

Commit 6d9102a

Browse files
authored
Merge pull request #55 from sysprog21/pkg-install
Install as a kconfiglib package, not loose modules
2 parents 7a57bfc + f0fcf8c commit 6d9102a

17 files changed

Lines changed: 1684 additions & 860 deletions

‎.github/workflows/package.yml‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,119 @@ jobs:
5151
set -euo pipefail
5252
python setup.py bdist_wheel
5353
54+
- name: Check wheel layout
55+
run: |
56+
set -euo pipefail
57+
python - <<'PY'
58+
from pathlib import Path
59+
from zipfile import ZipFile
60+
61+
# The wheel ships a kconfiglib package, not fifteen loose modules in
62+
# site-packages. Nothing outside kconfiglib/ but metadata.
63+
#
64+
# That everything the console scripts name is present, importable and
65+
# has the named attribute is checked by resolving them for real in the
66+
# next step, which is strictly stronger than matching paths here.
67+
wheel = next(Path("dist").glob("*.whl"))
68+
with ZipFile(wheel) as archive:
69+
names = set(archive.namelist())
70+
71+
stray = sorted(
72+
name
73+
for name in names
74+
if not name.startswith("kconfiglib/") and ".dist-info/" not in name
75+
)
76+
assert not stray, f"unexpected wheel members: {stray}"
77+
assert "kconfiglib/__init__.py" in names
78+
79+
print(f"{len(names)} members, nothing outside the package")
80+
PY
81+
82+
- name: Check wheel install
83+
run: |
84+
set -euo pipefail
85+
python -m venv /tmp/wheel-env
86+
/tmp/wheel-env/bin/pip install dist/*.whl
87+
# The wheel's own list of console scripts, for the check below
88+
python - <<'PY'
89+
from pathlib import Path
90+
from zipfile import ZipFile
91+
92+
wheel = next(Path("dist").glob("*.whl"))
93+
with ZipFile(wheel) as archive:
94+
name = next(
95+
n for n in archive.namelist()
96+
if n.endswith(".dist-info/entry_points.txt")
97+
)
98+
Path("/tmp/declared.txt").write_bytes(archive.read(name))
99+
PY
100+
export DECLARED=/tmp/declared.txt
101+
# Not from the checkout: the root kconfiglib.py shadows the installed
102+
# package whenever it is the working directory.
103+
cd "$(mktemp -d)"
104+
/tmp/wheel-env/bin/python - <<'PY'
105+
import importlib
106+
import os
107+
from importlib.metadata import entry_points
108+
from pathlib import Path
109+
110+
import kconfiglib
111+
112+
assert kconfiglib.Kconfig
113+
114+
# Resolve every console script the way the generated wrapper does, so
115+
# a module that is present but broken, or a renamed main(), fails here
116+
# rather than the first time a user runs it.
117+
#
118+
# Selected by module prefix rather than asserted over every script in
119+
# the environment, which would also cover pip's, and rather than via
120+
# distribution("kconfiglib"), which picks whichever metadata directory
121+
# comes first when a stray egg-info is on the path.
122+
installed = {
123+
e.name: e
124+
for e in entry_points(group="console_scripts")
125+
if e.module.startswith("kconfiglib.")
126+
}
127+
128+
# Against the wheel's own declaration, so that dropping a script
129+
# still fails here rather than passing because the others survived.
130+
declared = {
131+
line.split("=", 1)[0].strip()
132+
for line in Path(os.environ["DECLARED"]).read_text().splitlines()
133+
if "=" in line and not line.startswith("[")
134+
}
135+
assert declared, "the wheel declares no console scripts"
136+
assert set(installed) == declared, (
137+
f"declared {sorted(declared)}, installed {sorted(installed)}"
138+
)
139+
installed = list(installed.values())
140+
141+
for entry in installed:
142+
if entry.module == "kconfiglib.guiconfig":
143+
try:
144+
import tkinter # noqa: F401
145+
except ImportError:
146+
print(f"no tkinter, skipping {entry.name}")
147+
continue
148+
assert callable(entry.load()), entry.name
149+
150+
# These two ship no script of their own
151+
for module in ("kconfiglib.rawterm", "kconfiglib.uicommon"):
152+
importlib.import_module(module)
153+
154+
print(f"{len(installed)} console scripts resolve")
155+
PY
156+
/tmp/wheel-env/bin/genconfig --help > /dev/null
157+
158+
- name: Check editable install
159+
run: |
160+
set -euo pipefail
161+
python -m pip install --editable .
162+
# The root kconfiglib.py shadows the package whenever the checkout is
163+
# the working directory, so verify the install from somewhere else.
164+
cd "$(mktemp -d)"
165+
python -c "from kconfiglib import Kconfig; import kconfiglib.menuconfig"
166+
54167
- name: List built artifacts
55168
run: |
56169
set -euo pipefail

‎.github/workflows/test.yml‎

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,17 @@ jobs:
8787
# backstop against a large drop, not a ratchet; raise it as
8888
# coverage improves.
8989
#
90-
# 35 is set against what these runners report, which is 40. Do not
90+
# 38 is set against what these runners report, which is 41. Do not
9191
# calibrate it from a local run on Python 3.14 or newer: coverage
9292
# switches to the sys.monitoring backend there and scores the same
93-
# suite at 51, and a floor taken from that number fails every job
94-
# here. COVERAGE_CORE=ctrace reproduces the runner figure locally.
93+
# suite differently, and a floor taken from that number fails every
94+
# job here. COVERAGE_CORE=ctrace reproduces the runner figure
95+
# locally.
9596
python -m pytest tests/ --ignore=tests/test_conformance.py \
9697
--cov=kconfiglib --cov=menuconfig --cov=guiconfig --cov=rawterm \
98+
--cov=uicommon \
9799
--cov-report=term --cov-report=xml:coverage.xml \
98-
--cov-fail-under=35
100+
--cov-fail-under=38
99101
fi
100102
101103
- name: Store coverage report
@@ -126,6 +128,50 @@ jobs:
126128
python .ci/validate-rawterm.py
127129
if errorlevel 1 exit /b %errorlevel%
128130
131+
- name: Benchmark the load and UI hot paths
132+
# No pass/fail threshold, and read the numbers with care. Measured on
133+
# this fixture, phase timings move by around 7% run to run on an idle
134+
# machine and around 30% on a busy one; the spread is cross-process
135+
# fixed cost, not the timing loop, so raising -n does not close it. A
136+
# shared runner is the busy case. So this attributes a large regression
137+
# and nothing subtler, and the value is the trend across runs rather
138+
# than any single number. Pointing it at a tree big enough for the
139+
# phases to run in milliseconds would tighten it.
140+
if: ${{ matrix.target.headless-only != true }}
141+
shell: bash
142+
run: |
143+
set -euo pipefail
144+
# Measured once: --json returns before printing the table, so a
145+
# second run would re-measure everything and the table shown here
146+
# would not be the numbers in the artifact. Render the table from
147+
# the captured JSON instead, so the log and the artifact agree.
148+
python scripts/benchmark.py --tree tests --kconfig Kuirender \
149+
--json > benchmark.json
150+
python - <<'PY'
151+
import json
152+
153+
with open("benchmark.json") as f:
154+
phases = json.load(f)
155+
156+
print(f"{'phase':<28} {'best':>10} detail")
157+
print("-" * 72)
158+
for name, data in sorted(phases.items()):
159+
seconds = data.get("seconds")
160+
best = "-" if seconds is None else f"{seconds * 1000:.2f}ms"
161+
detail = ", ".join(
162+
f"{k}={v}" for k, v in sorted(data.items()) if k != "seconds"
163+
)
164+
print(f"{name:<28} {best:>10} {detail}")
165+
PY
166+
167+
- name: Store benchmark timings
168+
if: ${{ !cancelled() && matrix.target.headless-only != true }}
169+
uses: actions/upload-artifact@v6
170+
with:
171+
name: benchmark-${{ matrix.target.os }}-py${{ matrix.target.python }}
172+
path: benchmark.json
173+
if-no-files-found: ignore
174+
129175
- name: Diagnostic dump on failure
130176
if: failure()
131177
shell: bash

‎MANIFEST.in‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
# Include the license file in source distributions
22
include LICENSE
3+
4+
# Ship the whole test suite, not just test_*.py: conftest.py, the helper
5+
# modules and the Kconfig fixtures are what make it runnable for a distro
6+
# packager building from the sdist.
7+
recursive-include tests *
8+
global-exclude __pycache__/* *.pyc

‎README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ pip install git+https://github.com/sysprog21/Kconfiglib
4242

4343
Microsoft Windows is supported.
4444

45+
An installed Kconfiglib is a single `kconfiglib` package, so the interfaces are
46+
imported as `from kconfiglib import menuconfig` rather than `import menuconfig`
47+
(the latter worked before 15.0.0, when every module landed loose in
48+
site-packages). The sources stay flat in a checkout, which means a bare
49+
`kconfiglib.py` in the working directory shadows the installed package there:
50+
run `import kconfiglib.menuconfig` from somewhere other than the source tree.
51+
4552
When installed via `pip`, you get both the core library and the following executables.
4653
All but three (`genconfig`, `setconfig`, and `lint`) mirror functionality available in the C tools.
4754
- [menuconfig](menuconfig.py)
@@ -177,7 +184,7 @@ This will work even after installing Kconfiglib with `pip`.
177184
Documentation for other modules can be viewed the same way.
178185
For executables, a plain `--help` often suffices:
179186
```shell
180-
pydoc menuconfig/guiconfig/...
187+
pydoc kconfiglib.menuconfig kconfiglib.guiconfig
181188
```
182189

183190
A good place to start is the module docstring, located at the beginning of [kconfiglib.py](kconfiglib.py).

0 commit comments

Comments
 (0)