Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,119 @@ jobs:
set -euo pipefail
python setup.py bdist_wheel

- name: Check wheel layout
run: |
set -euo pipefail
python - <<'PY'
from pathlib import Path
from zipfile import ZipFile

# The wheel ships a kconfiglib package, not fifteen loose modules in
# site-packages. Nothing outside kconfiglib/ but metadata.
#
# That everything the console scripts name is present, importable and
# has the named attribute is checked by resolving them for real in the
# next step, which is strictly stronger than matching paths here.
wheel = next(Path("dist").glob("*.whl"))
with ZipFile(wheel) as archive:
names = set(archive.namelist())

stray = sorted(
name
for name in names
if not name.startswith("kconfiglib/") and ".dist-info/" not in name
)
assert not stray, f"unexpected wheel members: {stray}"
assert "kconfiglib/__init__.py" in names

print(f"{len(names)} members, nothing outside the package")
PY

- name: Check wheel install
run: |
set -euo pipefail
python -m venv /tmp/wheel-env
/tmp/wheel-env/bin/pip install dist/*.whl
# The wheel's own list of console scripts, for the check below
python - <<'PY'
from pathlib import Path
from zipfile import ZipFile

wheel = next(Path("dist").glob("*.whl"))
with ZipFile(wheel) as archive:
name = next(
n for n in archive.namelist()
if n.endswith(".dist-info/entry_points.txt")
)
Path("/tmp/declared.txt").write_bytes(archive.read(name))
PY
export DECLARED=/tmp/declared.txt
# Not from the checkout: the root kconfiglib.py shadows the installed
# package whenever it is the working directory.
cd "$(mktemp -d)"
/tmp/wheel-env/bin/python - <<'PY'
import importlib
import os
from importlib.metadata import entry_points
from pathlib import Path

import kconfiglib

assert kconfiglib.Kconfig

# Resolve every console script the way the generated wrapper does, so
# a module that is present but broken, or a renamed main(), fails here
# rather than the first time a user runs it.
#
# Selected by module prefix rather than asserted over every script in
# the environment, which would also cover pip's, and rather than via
# distribution("kconfiglib"), which picks whichever metadata directory
# comes first when a stray egg-info is on the path.
installed = {
e.name: e
for e in entry_points(group="console_scripts")
if e.module.startswith("kconfiglib.")
}

# Against the wheel's own declaration, so that dropping a script
# still fails here rather than passing because the others survived.
declared = {
line.split("=", 1)[0].strip()
for line in Path(os.environ["DECLARED"]).read_text().splitlines()
if "=" in line and not line.startswith("[")
}
assert declared, "the wheel declares no console scripts"
assert set(installed) == declared, (
f"declared {sorted(declared)}, installed {sorted(installed)}"
)
installed = list(installed.values())

for entry in installed:
if entry.module == "kconfiglib.guiconfig":
try:
import tkinter # noqa: F401
except ImportError:
print(f"no tkinter, skipping {entry.name}")
continue
assert callable(entry.load()), entry.name

# These two ship no script of their own
for module in ("kconfiglib.rawterm", "kconfiglib.uicommon"):
importlib.import_module(module)

print(f"{len(installed)} console scripts resolve")
PY
/tmp/wheel-env/bin/genconfig --help > /dev/null

- name: Check editable install
run: |
set -euo pipefail
python -m pip install --editable .
# The root kconfiglib.py shadows the package whenever the checkout is
# the working directory, so verify the install from somewhere else.
cd "$(mktemp -d)"
python -c "from kconfiglib import Kconfig; import kconfiglib.menuconfig"

- name: List built artifacts
run: |
set -euo pipefail
Expand Down
54 changes: 50 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,17 @@ jobs:
# backstop against a large drop, not a ratchet; raise it as
# coverage improves.
#
# 35 is set against what these runners report, which is 40. Do not
# 38 is set against what these runners report, which is 41. Do not
# calibrate it from a local run on Python 3.14 or newer: coverage
# switches to the sys.monitoring backend there and scores the same
# suite at 51, and a floor taken from that number fails every job
# here. COVERAGE_CORE=ctrace reproduces the runner figure locally.
# suite differently, and a floor taken from that number fails every
# job here. COVERAGE_CORE=ctrace reproduces the runner figure
# locally.
python -m pytest tests/ --ignore=tests/test_conformance.py \
--cov=kconfiglib --cov=menuconfig --cov=guiconfig --cov=rawterm \
--cov=uicommon \
--cov-report=term --cov-report=xml:coverage.xml \
--cov-fail-under=35
--cov-fail-under=38
fi

- name: Store coverage report
Expand Down Expand Up @@ -126,6 +128,50 @@ jobs:
python .ci/validate-rawterm.py
if errorlevel 1 exit /b %errorlevel%

- name: Benchmark the load and UI hot paths
# No pass/fail threshold, and read the numbers with care. Measured on
# this fixture, phase timings move by around 7% run to run on an idle
# machine and around 30% on a busy one; the spread is cross-process
# fixed cost, not the timing loop, so raising -n does not close it. A
# shared runner is the busy case. So this attributes a large regression
# and nothing subtler, and the value is the trend across runs rather
# than any single number. Pointing it at a tree big enough for the
# phases to run in milliseconds would tighten it.
if: ${{ matrix.target.headless-only != true }}
shell: bash
run: |
set -euo pipefail
# Measured once: --json returns before printing the table, so a
# second run would re-measure everything and the table shown here
# would not be the numbers in the artifact. Render the table from
# the captured JSON instead, so the log and the artifact agree.
python scripts/benchmark.py --tree tests --kconfig Kuirender \
--json > benchmark.json
python - <<'PY'
import json

with open("benchmark.json") as f:
phases = json.load(f)

print(f"{'phase':<28} {'best':>10} detail")
print("-" * 72)
for name, data in sorted(phases.items()):
seconds = data.get("seconds")
best = "-" if seconds is None else f"{seconds * 1000:.2f}ms"
detail = ", ".join(
f"{k}={v}" for k, v in sorted(data.items()) if k != "seconds"
)
print(f"{name:<28} {best:>10} {detail}")
PY

- name: Store benchmark timings
if: ${{ !cancelled() && matrix.target.headless-only != true }}
uses: actions/upload-artifact@v6
with:
name: benchmark-${{ matrix.target.os }}-py${{ matrix.target.python }}
path: benchmark.json
if-no-files-found: ignore

- name: Diagnostic dump on failure
if: failure()
shell: bash
Expand Down
6 changes: 6 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# Include the license file in source distributions
include LICENSE

# Ship the whole test suite, not just test_*.py: conftest.py, the helper
# modules and the Kconfig fixtures are what make it runnable for a distro
# packager building from the sdist.
recursive-include tests *
global-exclude __pycache__/* *.pyc
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ pip install git+https://github.com/sysprog21/Kconfiglib

Microsoft Windows is supported.

An installed Kconfiglib is a single `kconfiglib` package, so the interfaces are
imported as `from kconfiglib import menuconfig` rather than `import menuconfig`
(the latter worked before 15.0.0, when every module landed loose in
site-packages). The sources stay flat in a checkout, which means a bare
`kconfiglib.py` in the working directory shadows the installed package there:
run `import kconfiglib.menuconfig` from somewhere other than the source tree.

When installed via `pip`, you get both the core library and the following executables.
All but three (`genconfig`, `setconfig`, and `lint`) mirror functionality available in the C tools.
- [menuconfig](menuconfig.py)
Expand Down Expand Up @@ -177,7 +184,7 @@ This will work even after installing Kconfiglib with `pip`.
Documentation for other modules can be viewed the same way.
For executables, a plain `--help` often suffices:
```shell
pydoc menuconfig/guiconfig/...
pydoc kconfiglib.menuconfig kconfiglib.guiconfig
```

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