From 25119b7d2dd70d6d97720c64b99a0a9bfd8fb989 Mon Sep 17 00:00:00 2001 From: Jonathan Berthias Date: Sat, 17 Oct 2026 23:32:54 +0200 Subject: [PATCH 1/3] Add runtime docstrings to stubs in release wheels --- CHANGELOG.md | 1 + MANIFEST.in | 3 + pyproject.toml | 22 +++-- requirements/wheel-stub-docstrings.txt | 3 + scripts/add_wheel_stub_docstrings.py | 128 +++++++++++++++++++++++++ 5 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 MANIFEST.in create mode 100644 requirements/wheel-stub-docstrings.txt create mode 100644 scripts/add_wheel_stub_docstrings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index fbec1d025..3b11c0f84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added docstrings to the bundled stubs in release wheels for Python 3.10 and later. - Added more type annotations and deprecation markers to the stubs. - Added tests for `getObjVal()` and `getVal()` ### Fixed diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..638cb39c5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +# Include these helpers in source archives for cibuildwheel. +include requirements/wheel-stub-docstrings.txt +include scripts/add_wheel_stub_docstrings.py diff --git a/pyproject.toml b/pyproject.toml index a9a1bedd8..764927e50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,10 @@ unzip scip.zip mv scip_install scip ''' environment = { SCIPOPTDIR="$(pwd)/scip", LD_LIBRARY_PATH="$(pwd)/scip/lib:$LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} +repair-wheel-command = [ + "auditwheel repair -w {dest_dir} {wheel}", + 'python "{project}/scripts/add_wheel_stub_docstrings.py" "{dest_dir}"', +] [tool.cibuildwheel.macos] @@ -93,13 +97,12 @@ for prefix in /usr/local /opt/homebrew; do done ''' environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH", MACOSX_DEPLOYMENT_TARGET="11.0", RELEASE="true"} -repair-wheel-command = ''' - bash -c ' - export MACOSX_DEPLOYMENT_TARGET=11.0 - delocate-listdeps {wheel} - delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} - ' -''' +repair-wheel-command = [ + "export MACOSX_DEPLOYMENT_TARGET=11.0", + "delocate-listdeps {wheel}", + "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}", + 'python "{project}/scripts/add_wheel_stub_docstrings.py" "{dest_dir}"', +] [tool.cibuildwheel.windows] @@ -113,4 +116,7 @@ before-all = [ ] before-build = "pip install delvewheel" environment = { SCIPOPTDIR='D:\\a\\PySCIPOpt\\PySCIPOpt\\scip', RELEASE="true" } -repair-wheel-command = "delvewheel repair --add-path c:/bin;c:/lib;c:/bin/src;c:/lib/src;D:/a/PySCIPOpt/PySCIPOpt/scip/;D:/a/PySCIPOpt/PySCIPOpt/scip/lib/;D:/a/PySCIPOpt/PySCIPOpt/scip/bin/ -w {dest_dir} {wheel}" +repair-wheel-command = [ + "delvewheel repair --add-path c:/bin;c:/lib;c:/bin/src;c:/lib/src;D:/a/PySCIPOpt/PySCIPOpt/scip/;D:/a/PySCIPOpt/PySCIPOpt/scip/lib/;D:/a/PySCIPOpt/PySCIPOpt/scip/bin/ -w {dest_dir} {wheel}", + 'python "{project}/scripts/add_wheel_stub_docstrings.py" "{dest_dir}"', +] diff --git a/requirements/wheel-stub-docstrings.txt b/requirements/wheel-stub-docstrings.txt new file mode 100644 index 000000000..1793dc5af --- /dev/null +++ b/requirements/wheel-stub-docstrings.txt @@ -0,0 +1,3 @@ +wheel +numpy +docstring-adder @ git+https://github.com/astral-sh/docstring-adder@8377e23ca0c1533bef3e744209709d6d783782b6 diff --git a/scripts/add_wheel_stub_docstrings.py b/scripts/add_wheel_stub_docstrings.py new file mode 100644 index 000000000..cd0e4751d --- /dev/null +++ b/scripts/add_wheel_stub_docstrings.py @@ -0,0 +1,128 @@ +"""Copy runtime docstrings into the type stubs bundled in a release wheel. + +Run after wheel repair has bundled the native libraries. Install the tools and +NumPy (needed to import PySCIPOpt), unpack the wheel, copy docstrings from its +compiled extension into scip.pyi, and repack it with updated file hashes. + +Skip Python <3.10 because docstring-adder requires Python >=3.10. +""" + +import argparse +import ast +import os +import subprocess +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "wheel_dir", + type=Path, + help="Directory containing exactly one wheel with its native libraries bundled", + ) + args = parser.parse_args() + + if sys.version_info < (3, 10): + print("Skipping wheel stub docstrings: docstring-adder requires Python >=3.10") + return + + wheel_dir = args.wheel_dir.resolve() + wheels = list(wheel_dir.glob("*.whl")) + if len(wheels) != 1: + raise RuntimeError( + f"Expected exactly one wheel in {wheel_dir}, found {len(wheels)}" + ) + (wheel_path,) = wheels + + requirements_path = ( + Path(__file__).resolve().parents[1] / "requirements/wheel-stub-docstrings.txt" + ) + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "-r", + str(requirements_path), + ], + check=True, + ) + + with TemporaryDirectory() as temporary_dir: + unpack_dir = Path(temporary_dir) + subprocess.run( + [ + sys.executable, + "-m", + "wheel", + "unpack", + str(wheel_path), + "-d", + str(unpack_dir), + ], + check=True, + ) + (wheel_root,) = unpack_dir.iterdir() + # Put the unpacked wheel first on Python's import path so extraction uses + # its compiled extension and bundled libraries. + extractor_env = dict( + os.environ, + PYTHONPATH=str(wheel_root), + PYTHONDONTWRITEBYTECODE="1", + ) + subprocess.run( + [ + sys.executable, + "-m", + "add_docstrings", + "--packages", + str(wheel_root / "pyscipopt"), + ], + cwd=temporary_dir, + env=extractor_env, + check=True, + ) + # The extractor reports success even when it cannot import the extension. + # Require a known method's docstring before replacing the original wheel. + stub_ast = ast.parse( + (wheel_root / "pyscipopt/scip.pyi").read_text(encoding="utf-8") + ) + model_class = next( + node + for node in stub_ast.body + if isinstance(node, ast.ClassDef) and node.name == "Model" + ) + add_var_method = next( + node + for node in model_class.body + if isinstance(node, ast.FunctionDef) and node.name == "addVar" + ) + if not ast.get_docstring(add_var_method): + raise RuntimeError( + f"Stub docstring extraction failed for {wheel_path.name}: " + "Model.addVar has no docstring; see extractor output above" + ) + + # Repacking updates RECORD, the wheel's list of files and their hashes. + subprocess.run( + [ + sys.executable, + "-m", + "wheel", + "pack", + str(wheel_root), + "-d", + str(wheel_dir), + ], + check=True, + ) + + +if __name__ == "__main__": + main() From d83acc1729d95d570aebf9c365887a7497002453 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sun, 18 Oct 2026 22:37:15 +0100 Subject: [PATCH 2/3] Replace original wheel explicitly, pin wheel, clearer stub check error, drop MANIFEST.in --- MANIFEST.in | 3 --- requirements/wheel-stub-docstrings.txt | 2 +- scripts/add_wheel_stub_docstrings.py | 28 ++++++++++++++++++-------- 3 files changed, 21 insertions(+), 12 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 638cb39c5..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -# Include these helpers in source archives for cibuildwheel. -include requirements/wheel-stub-docstrings.txt -include scripts/add_wheel_stub_docstrings.py diff --git a/requirements/wheel-stub-docstrings.txt b/requirements/wheel-stub-docstrings.txt index 1793dc5af..ad54da93c 100644 --- a/requirements/wheel-stub-docstrings.txt +++ b/requirements/wheel-stub-docstrings.txt @@ -1,3 +1,3 @@ -wheel +wheel==0.48.0 numpy docstring-adder @ git+https://github.com/astral-sh/docstring-adder@8377e23ca0c1533bef3e744209709d6d783782b6 diff --git a/scripts/add_wheel_stub_docstrings.py b/scripts/add_wheel_stub_docstrings.py index cd0e4751d..0af522e0c 100644 --- a/scripts/add_wheel_stub_docstrings.py +++ b/scripts/add_wheel_stub_docstrings.py @@ -10,6 +10,7 @@ import argparse import ast import os +import shutil import subprocess import sys from pathlib import Path @@ -94,22 +95,30 @@ def main(): (wheel_root / "pyscipopt/scip.pyi").read_text(encoding="utf-8") ) model_class = next( - node - for node in stub_ast.body - if isinstance(node, ast.ClassDef) and node.name == "Model" + ( + node + for node in stub_ast.body + if isinstance(node, ast.ClassDef) and node.name == "Model" + ), + None, ) add_var_method = next( - node - for node in model_class.body - if isinstance(node, ast.FunctionDef) and node.name == "addVar" + ( + node + for node in getattr(model_class, "body", []) + if isinstance(node, ast.FunctionDef) and node.name == "addVar" + ), + None, ) - if not ast.get_docstring(add_var_method): + if add_var_method is None or not ast.get_docstring(add_var_method): raise RuntimeError( f"Stub docstring extraction failed for {wheel_path.name}: " "Model.addVar has no docstring; see extractor output above" ) # Repacking updates RECORD, the wheel's list of files and their hashes. + pack_dir = unpack_dir / "packed" + pack_dir.mkdir() subprocess.run( [ sys.executable, @@ -118,10 +127,13 @@ def main(): "pack", str(wheel_root), "-d", - str(wheel_dir), + str(pack_dir), ], check=True, ) + (packed_wheel,) = pack_dir.glob("*.whl") + wheel_path.unlink() + shutil.move(packed_wheel, wheel_dir / packed_wheel.name) if __name__ == "__main__": From 20693e27129dd9b4bdf2ccfb6ef9b3a6215632fe Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 24 Oct 2026 19:42:14 +0100 Subject: [PATCH 3/3] Simplify wheel stub docstring script and inline its requirements --- requirements/wheel-stub-docstrings.txt | 3 - scripts/add_wheel_stub_docstrings.py | 143 +++++-------------------- 2 files changed, 25 insertions(+), 121 deletions(-) delete mode 100644 requirements/wheel-stub-docstrings.txt diff --git a/requirements/wheel-stub-docstrings.txt b/requirements/wheel-stub-docstrings.txt deleted file mode 100644 index ad54da93c..000000000 --- a/requirements/wheel-stub-docstrings.txt +++ /dev/null @@ -1,3 +0,0 @@ -wheel==0.48.0 -numpy -docstring-adder @ git+https://github.com/astral-sh/docstring-adder@8377e23ca0c1533bef3e744209709d6d783782b6 diff --git a/scripts/add_wheel_stub_docstrings.py b/scripts/add_wheel_stub_docstrings.py index 0af522e0c..2e4d438e8 100644 --- a/scripts/add_wheel_stub_docstrings.py +++ b/scripts/add_wheel_stub_docstrings.py @@ -1,14 +1,9 @@ -"""Copy runtime docstrings into the type stubs bundled in a release wheel. +"""Copy runtime docstrings into the type stubs of a repaired wheel. -Run after wheel repair has bundled the native libraries. Install the tools and -NumPy (needed to import PySCIPOpt), unpack the wheel, copy docstrings from its -compiled extension into scip.pyi, and repack it with updated file hashes. - -Skip Python <3.10 because docstring-adder requires Python >=3.10. +Usage: python add_wheel_stub_docstrings.py +Skipped on Python <3.10, which docstring-adder requires. """ -import argparse -import ast import os import shutil import subprocess @@ -16,124 +11,36 @@ from pathlib import Path from tempfile import TemporaryDirectory +DOCSTRING_ADDER = "git+https://github.com/astral-sh/docstring-adder@8377e23ca0c1533bef3e744209709d6d783782b6" -def main(): - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter - ) - parser.add_argument( - "wheel_dir", - type=Path, - help="Directory containing exactly one wheel with its native libraries bundled", - ) - args = parser.parse_args() +def run(*args, **kwargs): + subprocess.run([sys.executable, *args], check=True, **kwargs) + + +def main(): if sys.version_info < (3, 10): - print("Skipping wheel stub docstrings: docstring-adder requires Python >=3.10") + print("Skipping stub docstrings: docstring-adder requires Python >=3.10") return - wheel_dir = args.wheel_dir.resolve() - wheels = list(wheel_dir.glob("*.whl")) - if len(wheels) != 1: - raise RuntimeError( - f"Expected exactly one wheel in {wheel_dir}, found {len(wheels)}" - ) - (wheel_path,) = wheels + wheel_dir = Path(sys.argv[1]).resolve() + (wheel,) = wheel_dir.glob("*.whl") + run("-m", "pip", "install", "wheel==0.48.0", "numpy", f"docstring-adder @ {DOCSTRING_ADDER}") - requirements_path = ( - Path(__file__).resolve().parents[1] / "requirements/wheel-stub-docstrings.txt" - ) - subprocess.run( - [ - sys.executable, - "-m", - "pip", - "install", - "-r", - str(requirements_path), - ], - check=True, - ) + with TemporaryDirectory() as tmp: + tmp = Path(tmp) + run("-m", "wheel", "unpack", str(wheel), "-d", str(tmp / "unpacked")) + (root,) = (tmp / "unpacked").iterdir() - with TemporaryDirectory() as temporary_dir: - unpack_dir = Path(temporary_dir) - subprocess.run( - [ - sys.executable, - "-m", - "wheel", - "unpack", - str(wheel_path), - "-d", - str(unpack_dir), - ], - check=True, - ) - (wheel_root,) = unpack_dir.iterdir() - # Put the unpacked wheel first on Python's import path so extraction uses - # its compiled extension and bundled libraries. - extractor_env = dict( - os.environ, - PYTHONPATH=str(wheel_root), - PYTHONDONTWRITEBYTECODE="1", - ) - subprocess.run( - [ - sys.executable, - "-m", - "add_docstrings", - "--packages", - str(wheel_root / "pyscipopt"), - ], - cwd=temporary_dir, - env=extractor_env, - check=True, - ) - # The extractor reports success even when it cannot import the extension. - # Require a known method's docstring before replacing the original wheel. - stub_ast = ast.parse( - (wheel_root / "pyscipopt/scip.pyi").read_text(encoding="utf-8") - ) - model_class = next( - ( - node - for node in stub_ast.body - if isinstance(node, ast.ClassDef) and node.name == "Model" - ), - None, - ) - add_var_method = next( - ( - node - for node in getattr(model_class, "body", []) - if isinstance(node, ast.FunctionDef) and node.name == "addVar" - ), - None, - ) - if add_var_method is None or not ast.get_docstring(add_var_method): - raise RuntimeError( - f"Stub docstring extraction failed for {wheel_path.name}: " - "Model.addVar has no docstring; see extractor output above" - ) + env = dict(os.environ, PYTHONPATH=str(root), PYTHONDONTWRITEBYTECODE="1") + run("-c", "import pyscipopt.scip", env=env, cwd=tmp) + run("-m", "add_docstrings", "--packages", str(root / "pyscipopt"), env=env, cwd=tmp) - # Repacking updates RECORD, the wheel's list of files and their hashes. - pack_dir = unpack_dir / "packed" - pack_dir.mkdir() - subprocess.run( - [ - sys.executable, - "-m", - "wheel", - "pack", - str(wheel_root), - "-d", - str(pack_dir), - ], - check=True, - ) - (packed_wheel,) = pack_dir.glob("*.whl") - wheel_path.unlink() - shutil.move(packed_wheel, wheel_dir / packed_wheel.name) + (tmp / "packed").mkdir() + run("-m", "wheel", "pack", str(root), "-d", str(tmp / "packed")) + (packed,) = (tmp / "packed").glob("*.whl") + wheel.unlink() + shutil.move(packed, wheel_dir / packed.name) if __name__ == "__main__":