diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 44bcdbf..4fa2451 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,13 +17,12 @@ "settings": { // Discover the tests/ package from the Test Explorer. "python.testing.unittestEnabled": true, - "python.testing.unittestArgs": ["-v", "-s", ".", "-p", "test*.py"] + "python.testing.unittestArgs": ["-v", "-s", "tests", "-p", "test*.py"] } } }, - // Compile + install the extension (non-editable: a clean .so in site-packages - // avoids the name clash with the list_reserve/ stub package) and add the - // `build` frontend so `python -m build` works. - "postCreateCommand": "pip install --upgrade pip && pip install . build" + // Compile + install the package in editable mode and add the `build` frontend + // so `python -m build` works. + "postCreateCommand": "pip install --upgrade pip && pip install -e . build" } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55fc16d..08bdc90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,4 +47,4 @@ jobs: pip install . - name: Test run: | - python -m unittest + python tools/run_installed_tests.py diff --git a/MANIFEST.in b/MANIFEST.in index 62c45b5..d4ffb39 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,5 @@ # from a PyPI source download. The tests/ package is not part of the wheel # (packages = ["list_reserve"]), so the sdist is its only distribution channel. recursive-include tests *.py +include tools/run_installed_tests.py include LICENSE diff --git a/README.md b/README.md index 386a3c0..4aa99e0 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,12 @@ supports Dev Containers ("Reopen in Container"), or run it headlessly with the ```bash devcontainer up --workspace-folder . -devcontainer exec --workspace-folder . python -m unittest +devcontainer exec --workspace-folder . python tools/run_installed_tests.py ``` -The container compiles the C extension on creation, so the tests are runnable immediately. -Since `list_reserve` is a C extension, re-run `pip install .` after editing -`src/list_reserve.c`. +The container installs the package in editable mode on creation, so the tests are +runnable immediately. Since `list_reserve` includes a C extension, re-run +`pip install -e .` after editing `src/list_reserve.c`. ## License diff --git a/list_reserve/__init__.py b/list_reserve/__init__.py new file mode 100644 index 0000000..0fb6140 --- /dev/null +++ b/list_reserve/__init__.py @@ -0,0 +1,9 @@ +from ._list_reserve import allocated_bytes, capacity, reserve, shrink_to_fit, stats + +__all__ = [ + "allocated_bytes", + "capacity", + "reserve", + "shrink_to_fit", + "stats", +] diff --git a/pyproject.toml b/pyproject.toml index d7baffa..620713c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,14 +84,9 @@ build = "cp310-* cp311-* cp312-* cp313-* cp314-*" # internals. skip = "pp* *t-*" -# cibuildwheel runs this against each installed wheel from a temp dir, so it -# tests the installed .so. `-s {project}` makes {project} the discovery -# top-level dir, so tests import as `tests.*` and `from tests._support import -# ...` resolves; `-s {project}/tests` would drop the `tests.` prefix and break -# that import. Quote the glob with double quotes (honored by both POSIX sh and -# Windows cmd; single quotes reach unittest literally on Windows and match -# nothing) so unittest, not the shell, expands it. -test-command = "python -m unittest discover -s {project} -p \"test_*.py\"" +# Run tests from outside the source tree so `import list_reserve` resolves to the +# installed wheel instead of the local package directory. +test-command = "python \"{project}/tools/run_installed_tests.py\"" # Windows ARM64 is experimental and its tests cannot run on the build host, so # skip them (the wheel is still built and shipped). diff --git a/setup.py b/setup.py index dab794f..202b60a 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,6 @@ # [tool.setuptools.ext-modules] table cannot yet express stably. setup( ext_modules=[ - Extension("list_reserve", ["src/list_reserve.c"]), + Extension("list_reserve._list_reserve", ["src/list_reserve.c"]), ], ) diff --git a/src/list_reserve.c b/src/list_reserve.c index a9d2cd1..1e298c3 100644 --- a/src/list_reserve.c +++ b/src/list_reserve.c @@ -183,11 +183,11 @@ static PyMethodDef methods[] = { {NULL}}; // module definition struct -static struct PyModuleDef module = {PyModuleDef_HEAD_INIT, "list_reserve", +static struct PyModuleDef module = {PyModuleDef_HEAD_INIT, "list_reserve._list_reserve", "list memory allocation library", -1, methods}; // Initializes module PyMODINIT_FUNC -PyInit_list_reserve(void) { +PyInit__list_reserve(void) { return PyModule_Create(&module); } diff --git a/tools/run_installed_tests.py b/tools/run_installed_tests.py new file mode 100644 index 0000000..b3dfa55 --- /dev/null +++ b/tools/run_installed_tests.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + + +def main() -> int: + project_root = Path(__file__).resolve().parents[1] + source_tests = project_root / "tests" + + with tempfile.TemporaryDirectory(prefix="list-reserve-tests-") as tmp: + tmp_path = Path(tmp) + copied_tests = tmp_path / "tests" + shutil.copytree( + source_tests, + copied_tests, + ignore=shutil.ignore_patterns("__pycache__"), + ) + + sys.path = [ + entry for entry in sys.path if Path(entry or ".").resolve() != project_root + ] + sys.path.insert(0, str(tmp_path)) + + suite = unittest.defaultTestLoader.discover( + str(copied_tests), + pattern="test_*.py", + top_level_dir=str(tmp_path), + ) + result = unittest.TextTestRunner(verbosity=2).run(suite) + return int(not result.wasSuccessful()) + + +if __name__ == "__main__": + raise SystemExit(main())