From 7966b90d6bec3798c1f49571dedf25eb33574de6 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 20 Jan 2026 13:34:54 +0000 Subject: [PATCH 01/25] Modernize build system with pyproject.toml and custom build backend - Add build_ddbc package for native extension compilation - python -m build_ddbc: standalone compilation CLI - Custom PEP 517 backend: auto-compiles on python -m build - Consolidate project config into pyproject.toml - Moved pytest config from pytest.ini - Added optional dependencies [dev], [lint], [all] - Added coverage, mypy, black, autopep8, pylint configs - Simplified setup.py to platform-specific wheel logic only - Delete pytest.ini (moved to pyproject.toml) --- build_ddbc/__init__.py | 18 ++++ build_ddbc/__main__.py | 82 +++++++++++++++++ build_ddbc/build_backend.py | 129 +++++++++++++++++++++++++++ build_ddbc/compiler.py | 170 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 162 +++++++++++++++++++++++++++++++++- pytest.ini | 10 --- setup.py | 166 ++++++++++++----------------------- 7 files changed, 618 insertions(+), 119 deletions(-) create mode 100644 build_ddbc/__init__.py create mode 100644 build_ddbc/__main__.py create mode 100644 build_ddbc/build_backend.py create mode 100644 build_ddbc/compiler.py delete mode 100644 pytest.ini diff --git a/build_ddbc/__init__.py b/build_ddbc/__init__.py new file mode 100644 index 000000000..2b10c6fce --- /dev/null +++ b/build_ddbc/__init__.py @@ -0,0 +1,18 @@ +""" +build_ddbc - Build system for mssql-python native extensions. + +This package provides: +1. A CLI tool: `python -m build_ddbc` +2. A PEP 517 build backend that auto-compiles ddbc_bindings + +Usage: + python -m build_ddbc # Compile ddbc_bindings only + python -m build_ddbc --arch arm64 # Specify architecture (Windows) + python -m build_ddbc --coverage # Enable coverage (Linux) + python -m build # Compile + create wheel (automatic) +""" + +from .compiler import compile_ddbc, get_platform_info + +__all__ = ["compile_ddbc", "get_platform_info"] +__version__ = "1.0.0" diff --git a/build_ddbc/__main__.py b/build_ddbc/__main__.py new file mode 100644 index 000000000..72c35d493 --- /dev/null +++ b/build_ddbc/__main__.py @@ -0,0 +1,82 @@ +""" +CLI entry point for build_ddbc. + +Usage: + python -m build_ddbc # Compile ddbc_bindings + python -m build_ddbc --arch arm64 # Specify architecture (Windows) + python -m build_ddbc --coverage # Enable coverage (Linux) + python -m build_ddbc --help # Show help +""" + +import argparse +import sys + +from .compiler import compile_ddbc, get_platform_info + + +def main() -> int: + """Main entry point for the CLI.""" + parser = argparse.ArgumentParser( + prog="python -m build_ddbc", + description="Compile ddbc_bindings native extension for mssql-python", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python -m build_ddbc # Build for current platform + python -m build_ddbc --arch arm64 # Build for ARM64 (Windows) + python -m build_ddbc --coverage # Build with coverage (Linux) + python -m build_ddbc --quiet # Build without output + """, + ) + + parser.add_argument( + "--arch", "-a", + choices=["x64", "x86", "arm64", "x86_64", "aarch64", "universal2"], + help="Target architecture (Windows: x64, x86, arm64)", + ) + + parser.add_argument( + "--coverage", "-c", + action="store_true", + help="Enable coverage instrumentation (Linux only)", + ) + + parser.add_argument( + "--quiet", "-q", + action="store_true", + help="Suppress build output", + ) + + parser.add_argument( + "--version", "-V", + action="version", + version="%(prog)s 1.0.0", + ) + + args = parser.parse_args() + + # Show platform info + if not args.quiet: + arch, platform_tag = get_platform_info() + print(f"[build_ddbc] Platform: {sys.platform}") + print(f"[build_ddbc] Architecture: {arch}") + print(f"[build_ddbc] Platform tag: {platform_tag}") + print() + + try: + compile_ddbc( + arch=args.arch, + coverage=args.coverage, + verbose=not args.quiet, + ) + return 0 + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + except RuntimeError as e: + print(f"Build failed: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/build_ddbc/build_backend.py b/build_ddbc/build_backend.py new file mode 100644 index 000000000..6d95b8756 --- /dev/null +++ b/build_ddbc/build_backend.py @@ -0,0 +1,129 @@ +""" +PEP 517 Build Backend for mssql-python. + +This module wraps setuptools' build backend and adds automatic +ddbc_bindings compilation before building wheels. + +Usage in pyproject.toml: + [build-system] + requires = ["setuptools>=61.0", "wheel", "pybind11"] + build-backend = "build_ddbc.build_backend" + backend-path = ["."] +""" + +import sys +from pathlib import Path + +# Import setuptools build backend - we'll wrap its functions +from setuptools.build_meta import ( + build_wheel as _setuptools_build_wheel, + build_sdist as _setuptools_build_sdist, + get_requires_for_build_wheel as _get_requires_for_build_wheel, + get_requires_for_build_sdist as _get_requires_for_build_sdist, + prepare_metadata_for_build_wheel as _prepare_metadata_for_build_wheel, +) + +from .compiler import compile_ddbc + + +# ============================================================================= +# PEP 517 Required Hooks +# ============================================================================= + +def get_requires_for_build_wheel(config_settings=None): + """Return build requirements for wheel.""" + return _get_requires_for_build_wheel(config_settings) + + +def get_requires_for_build_sdist(config_settings=None): + """Return build requirements for sdist.""" + return _get_requires_for_build_sdist(config_settings) + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + """Prepare wheel metadata.""" + return _prepare_metadata_for_build_wheel(metadata_directory, config_settings) + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + """ + Build a wheel, compiling ddbc_bindings first. + + This is the main hook - it compiles the native extension before + delegating to setuptools to create the wheel. + """ + print("[build_backend] Starting wheel build...") + + # Check if we should skip compilation (e.g., for sdist-only builds) + skip_compile = False + if config_settings: + skip_compile = config_settings.get("--skip-ddbc-compile", False) + + if not skip_compile: + # Extract build options from config_settings + arch = None + coverage = False + + if config_settings: + arch = config_settings.get("--arch") + coverage = config_settings.get("--coverage", False) + + print("[build_backend] Compiling ddbc_bindings...") + try: + compile_ddbc(arch=arch, coverage=coverage, verbose=True) + print("[build_backend] Compilation successful!") + except FileNotFoundError: + # If build scripts don't exist, assume pre-compiled binaries + print("[build_backend] Build scripts not found, assuming pre-compiled binaries") + except RuntimeError as e: + print(f"[build_backend] Compilation failed: {e}") + raise + else: + print("[build_backend] Skipping ddbc compilation (--skip-ddbc-compile)") + + # Now build the wheel using setuptools + print("[build_backend] Creating wheel...") + return _setuptools_build_wheel(wheel_directory, config_settings, metadata_directory) + + +def build_sdist(sdist_directory, config_settings=None): + """ + Build a source distribution. + + For sdist, we don't compile - just package the source including build scripts. + """ + print("[build_backend] Building source distribution...") + return _setuptools_build_sdist(sdist_directory, config_settings) + + +# ============================================================================= +# Optional PEP 660 Hooks (Editable Installs) +# ============================================================================= + +def get_requires_for_build_editable(config_settings=None): + """Return build requirements for editable install.""" + return get_requires_for_build_wheel(config_settings) + + +def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + """ + Build an editable wheel, compiling ddbc_bindings first. + + This enables `pip install -e .` to automatically compile. + """ + print("[build_backend] Starting editable install...") + + # Compile ddbc_bindings for editable installs too + print("[build_backend] Compiling ddbc_bindings for editable install...") + try: + compile_ddbc(verbose=True) + print("[build_backend] Compilation successful!") + except FileNotFoundError: + print("[build_backend] Build scripts not found, assuming pre-compiled binaries") + except RuntimeError as e: + print(f"[build_backend] Compilation failed: {e}") + raise + + # Import here to avoid issues if not available + from setuptools.build_meta import build_editable as _setuptools_build_editable + return _setuptools_build_editable(wheel_directory, config_settings, metadata_directory) diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py new file mode 100644 index 000000000..3164eefdb --- /dev/null +++ b/build_ddbc/compiler.py @@ -0,0 +1,170 @@ +""" +Core compiler logic for ddbc_bindings. + +This module contains the platform detection and build script execution logic. +""" + +import os +import sys +import subprocess +from pathlib import Path +from typing import Tuple, Optional + + +def get_platform_info() -> Tuple[str, str]: + """ + Get platform-specific architecture and platform tag information. + + Returns: + Tuple of (architecture, platform_tag) + + Raises: + OSError: If the platform or architecture is not supported + """ + if sys.platform.startswith("win"): + arch = os.environ.get("ARCHITECTURE", "x64") + if isinstance(arch, str): + arch = arch.strip("\"'") + + if arch in ["x86", "win32"]: + return "x86", "win32" + elif arch == "arm64": + return "arm64", "win_arm64" + else: + return "x64", "win_amd64" + + elif sys.platform.startswith("darwin"): + return "universal2", "macosx_15_0_universal2" + + elif sys.platform.startswith("linux"): + import platform + target_arch = os.environ.get("targetArch", platform.machine()) + libc_name, _ = platform.libc_ver() + is_musl = libc_name == "" or "musl" in libc_name.lower() + + if target_arch == "x86_64": + return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" + elif target_arch in ["aarch64", "arm64"]: + return "aarch64", "musllinux_1_2_aarch64" if is_musl else "manylinux_2_28_aarch64" + else: + raise OSError( + f"Unsupported architecture '{target_arch}' for Linux; " + "expected 'x86_64' or 'aarch64'." + ) + + raise OSError(f"Unsupported platform: {sys.platform}") + + +def find_pybind_dir() -> Path: + """Find the pybind directory containing build scripts.""" + # Try relative to this file first (for installed package) + possible_paths = [ + Path(__file__).parent.parent / "mssql_python" / "pybind", + Path.cwd() / "mssql_python" / "pybind", + ] + + for path in possible_paths: + if path.exists() and (path / "build.sh").exists(): + return path + + raise FileNotFoundError( + "Could not find mssql_python/pybind directory. " + "Make sure you're running from the project root." + ) + + +def compile_ddbc( + arch: Optional[str] = None, + coverage: bool = False, + verbose: bool = True, +) -> bool: + """ + Compile ddbc_bindings using the platform-specific build script. + + Args: + arch: Target architecture (Windows only: x64, x86, arm64) + coverage: Enable coverage instrumentation (Linux only) + verbose: Print build output + + Returns: + True if build succeeded, False otherwise + + Raises: + FileNotFoundError: If build script is not found + RuntimeError: If build fails + """ + pybind_dir = find_pybind_dir() + + if arch is None: + arch, _ = get_platform_info() + + if sys.platform.startswith("win"): + return _run_windows_build(pybind_dir, arch, verbose) + else: + return _run_unix_build(pybind_dir, coverage, verbose) + + +def _run_windows_build(pybind_dir: Path, arch: str, verbose: bool) -> bool: + """Run build.bat on Windows.""" + build_script = pybind_dir / "build.bat" + if not build_script.exists(): + raise FileNotFoundError(f"Build script not found: {build_script}") + + cmd = [str(build_script), arch] + + if verbose: + print(f"[build_ddbc] Running: {' '.join(cmd)}") + print(f"[build_ddbc] Working directory: {pybind_dir}") + + result = subprocess.run( + cmd, + cwd=pybind_dir, + shell=True, + check=False, + capture_output=not verbose, + ) + + if result.returncode != 0: + if not verbose and result.stderr: + print(result.stderr.decode(), file=sys.stderr) + raise RuntimeError(f"build.bat failed with exit code {result.returncode}") + + if verbose: + print("[build_ddbc] Windows build completed successfully!") + + return True + + +def _run_unix_build(pybind_dir: Path, coverage: bool, verbose: bool) -> bool: + """Run build.sh on macOS/Linux.""" + build_script = pybind_dir / "build.sh" + if not build_script.exists(): + raise FileNotFoundError(f"Build script not found: {build_script}") + + # Make sure the script is executable + build_script.chmod(0o755) + + cmd = ["bash", str(build_script)] + if coverage: + cmd.append("--coverage") + + if verbose: + print(f"[build_ddbc] Running: {' '.join(cmd)}") + print(f"[build_ddbc] Working directory: {pybind_dir}") + + result = subprocess.run( + cmd, + cwd=pybind_dir, + check=False, + capture_output=not verbose, + ) + + if result.returncode != 0: + if not verbose and result.stderr: + print(result.stderr.decode(), file=sys.stderr) + raise RuntimeError(f"build.sh failed with exit code {result.returncode}") + + if verbose: + print("[build_ddbc] Unix build completed successfully!") + + return True diff --git a/pyproject.toml b/pyproject.toml index 538a4a992..3e4a749f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,142 @@ +# ============================================================================= +# Build System Configuration +# ============================================================================= +[build-system] +requires = ["setuptools>=61.0", "wheel", "pybind11"] +build-backend = "build_ddbc.build_backend" +backend-path = ["."] + +# ============================================================================= +# Project Metadata +# ============================================================================= +[project] +name = "mssql-python" +version = "1.2.0" +description = "A Python library for interacting with Microsoft SQL Server" +readme = "PyPI_Description.md" +license = "MIT" +requires-python = ">=3.10" +authors = [ + { name = "Microsoft Corporation", email = "mssql-python@microsoft.com" } +] +keywords = ["mssql", "sql-server", "database", "odbc", "microsoft"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +dependencies = [ + "azure-identity>=1.12.0", +] + +[project.urls] +Homepage = "https://github.com/microsoft/mssql-python" +Documentation = "https://github.com/microsoft/mssql-python#readme" +Repository = "https://github.com/microsoft/mssql-python" +Issues = "https://github.com/microsoft/mssql-python/issues" +Changelog = "https://github.com/microsoft/mssql-python/blob/main/CHANGELOG.md" + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "coverage", + "unittest-xml-reporting", + "psutil", + "pybind11", + "setuptools", + "wheel", + "build", +] +lint = [ + "black", + "autopep8", + "flake8", + "pylint", + "cpplint", + "mypy", + "types-setuptools", +] +all = [ + "mssql-python[dev,lint]", +] + +# ============================================================================= +# Setuptools Configuration +# ============================================================================= +[tool.setuptools] +zip-safe = false +include-package-data = true + +[tool.setuptools.packages.find] +where = ["."] +include = ["mssql_python*"] + +[tool.setuptools.package-data] +mssql_python = [ + "ddbc_bindings.cp*.pyd", + "ddbc_bindings.cp*.so", + "libs/*", + "libs/**/*", + "*.dll", + "*.pyi", +] + +[tool.setuptools.exclude-package-data] +"*" = ["*.yml", "*.yaml"] +mssql_python = [ + "libs/*/vcredist/*", + "libs/*/vcredist/**/*", +] + +# ============================================================================= +# Pytest Configuration +# ============================================================================= +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +markers = [ + "stress: marks tests as stress tests (long-running, resource-intensive)", +] +addopts = "-m 'not stress'" + +# ============================================================================= +# Coverage Configuration +# ============================================================================= +[tool.coverage.run] +source = ["mssql_python"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/pybind/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise NotImplementedError", + "if __name__ == .__main__.:", +] + +# ============================================================================= +# Code Formatting - Black +# ============================================================================= [tool.black] line-length = 100 -target-version = ['py38', 'py39', 'py310', 'py311'] +target-version = ['py310', 'py311', 'py312', 'py313'] include = '\.pyi?$' extend-exclude = ''' /( @@ -14,6 +150,9 @@ extend-exclude = ''' )/ ''' +# ============================================================================= +# Code Formatting - Autopep8 +# ============================================================================= [tool.autopep8] max_line_length = 100 ignore = "E203,W503" @@ -21,6 +160,9 @@ in-place = true recursive = true aggressive = 3 +# ============================================================================= +# Linting - Pylint +# ============================================================================= [tool.pylint.messages_control] disable = [ "fixme", @@ -34,6 +176,10 @@ disable = [ [tool.pylint.format] max-line-length = 100 +# ============================================================================= +# Linting - Flake8 (Note: flake8 doesn't support pyproject.toml natively, +# but some plugins like Flake8-pyproject do) +# ============================================================================= [tool.flake8] max-line-length = 100 extend-ignore = ["E203", "W503"] @@ -45,3 +191,17 @@ exclude = [ ".venv", "htmlcov" ] + +# ============================================================================= +# Type Checking - MyPy +# ============================================================================= +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true +exclude = [ + "build", + "dist", + "tests", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index dc94ab9e1..000000000 --- a/pytest.ini +++ /dev/null @@ -1,10 +0,0 @@ -[pytest] -# Register custom markers -markers = - stress: marks tests as stress tests (long-running, resource-intensive) - -# Default options applied to all pytest runs -# Default: pytest -v → Skips stress tests (fast) -# To run ONLY stress tests: pytest -m stress -# To run ALL tests: pytest -v -m "" -addopts = -m "not stress" diff --git a/setup.py b/setup.py index 32d109bb2..675da43ef 100644 --- a/setup.py +++ b/setup.py @@ -1,141 +1,91 @@ +""" +Setup script for mssql-python. + +This script handles platform-specific wheel building with correct platform tags. +The native extension compilation is handled by the build_ddbc package. + +Note: This file is still needed for: +1. Platform-specific package discovery (libs/windows, libs/linux, libs/macos) +2. Custom wheel platform tags (BinaryDistribution, CustomBdistWheel) + +For building: + python -m build_ddbc # Compile ddbc_bindings only + python -m build # Compile + create wheel (recommended) + pip install -e . # Editable install with auto-compile +""" + import os import sys + from setuptools import setup, find_packages from setuptools.dist import Distribution from wheel.bdist_wheel import bdist_wheel +from build_ddbc import get_platform_info -# Custom distribution to force platform-specific wheel -class BinaryDistribution(Distribution): - def has_ext_modules(self): - return True +# ============================================================================= +# Platform-Specific Package Discovery +# ============================================================================= -def get_platform_info(): - """Get platform-specific architecture and platform tag information.""" - if sys.platform.startswith("win"): - # Get architecture from environment variable or default to x64 - arch = os.environ.get("ARCHITECTURE", "x64") - # Strip quotes if present - if isinstance(arch, str): - arch = arch.strip("\"'") - - # Normalize architecture values - if arch in ["x86", "win32"]: - return "x86", "win32" - elif arch == "arm64": - return "arm64", "win_arm64" - else: # Default to x64/amd64 - return "x64", "win_amd64" +def get_platform_packages(): + """Get platform-specific package list.""" + packages = find_packages() + arch, _ = get_platform_info() + if sys.platform.startswith("win"): + packages.extend([ + f"mssql_python.libs.windows.{arch}", + f"mssql_python.libs.windows.{arch}.1033", + f"mssql_python.libs.windows.{arch}.vcredist", + ]) elif sys.platform.startswith("darwin"): - # macOS platform - always use universal2 - return "universal2", "macosx_15_0_universal2" - + packages.append("mssql_python.libs.macos") elif sys.platform.startswith("linux"): - # Linux platform - use musllinux or manylinux tags based on architecture - # Get target architecture from environment variable or default to platform machine type - import platform + packages.append("mssql_python.libs.linux") + + return packages - target_arch = os.environ.get("targetArch", platform.machine()) - # Detect libc type - libc_name, _ = platform.libc_ver() - is_musl = libc_name == "" or "musl" in libc_name.lower() +# ============================================================================= +# Custom Distribution (Force Platform-Specific Wheel) +# ============================================================================= - if target_arch == "x86_64": - return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" - elif target_arch in ["aarch64", "arm64"]: - return "aarch64", "musllinux_1_2_aarch64" if is_musl else "manylinux_2_28_aarch64" - else: - raise OSError( - f"Unsupported architecture '{target_arch}' for Linux; expected 'x86_64' or 'aarch64'." - ) +class BinaryDistribution(Distribution): + """Distribution that forces platform-specific wheel creation.""" + + def has_ext_modules(self): + return True -# Custom bdist_wheel command to override platform tag +# ============================================================================= +# Custom bdist_wheel Command +# ============================================================================= + class CustomBdistWheel(bdist_wheel): + """Custom wheel builder with platform-specific tags.""" + def finalize_options(self): - # Call the original finalize_options first to initialize self.bdist_dir bdist_wheel.finalize_options(self) - - # Get platform info using consolidated function arch, platform_tag = get_platform_info() self.plat_name = platform_tag - print(f"Setting wheel platform tag to: {self.plat_name} (arch: {arch})") + print(f"[setup.py] Setting wheel platform tag to: {self.plat_name} (arch: {arch})") -# Find all packages in the current directory -packages = find_packages() +# ============================================================================= +# Setup Configuration +# ============================================================================= -# Get platform info using consolidated function arch, platform_tag = get_platform_info() -print(f"Detected architecture: {arch} (platform tag: {platform_tag})") - -# Add platform-specific packages -if sys.platform.startswith("win"): - packages.extend( - [ - f"mssql_python.libs.windows.{arch}", - f"mssql_python.libs.windows.{arch}.1033", - f"mssql_python.libs.windows.{arch}.vcredist", - ] - ) -elif sys.platform.startswith("darwin"): - packages.extend( - [ - f"mssql_python.libs.macos", - ] - ) -elif sys.platform.startswith("linux"): - packages.extend( - [ - f"mssql_python.libs.linux", - ] - ) +print(f"[setup.py] Detected architecture: {arch} (platform tag: {platform_tag})") setup( - name="mssql-python", - version="1.2.0", - description="A Python library for interacting with Microsoft SQL Server", - long_description=open("PyPI_Description.md", encoding="utf-8").read(), - long_description_content_type="text/markdown", - author="Microsoft Corporation", - author_email="mssql-python@microsoft.com", - url="https://github.com/microsoft/mssql-python", - packages=packages, - package_data={ - # Include PYD and DLL files inside mssql_python, exclude YML files - "mssql_python": [ - "ddbc_bindings.cp*.pyd", # Include all PYD files - "ddbc_bindings.cp*.so", # Include all SO files - "libs/*", - "libs/**/*", - "*.dll", - ] - }, - include_package_data=True, - # Requires >= Python 3.10 - python_requires=">=3.10", - # Add dependencies - install_requires=[ - "azure-identity>=1.12.0", # Azure authentication library - ], - classifiers=[ - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - ], - zip_safe=False, + # Package discovery + packages=get_platform_packages(), + # Force binary distribution distclass=BinaryDistribution, - exclude_package_data={ - "": ["*.yml", "*.yaml"], # Exclude YML files - "mssql_python": [ - "libs/*/vcredist/*", - "libs/*/vcredist/**/*", # Exclude vcredist directories, added here since `'libs/*' is already included` - ], - }, + # Register custom commands cmdclass={ "bdist_wheel": CustomBdistWheel, From 2fefac9d632badeeec6a70539a37987f263f0e3a Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Tue, 27 Jan 2026 05:48:12 +0000 Subject: [PATCH 02/25] CHORE: Clean up unused imports and fix cross-platform build script detection - Remove unused 'sys' and 'Path' imports from build_backend.py - Remove unused 'os' import from setup.py - Fix find_pybind_dir() to check for platform-appropriate build script (build.bat on Windows, build.sh on Unix) --- build_ddbc/build_backend.py | 3 --- build_ddbc/compiler.py | 7 +++++-- setup.py | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/build_ddbc/build_backend.py b/build_ddbc/build_backend.py index 6d95b8756..27045c24c 100644 --- a/build_ddbc/build_backend.py +++ b/build_ddbc/build_backend.py @@ -11,9 +11,6 @@ backend-path = ["."] """ -import sys -from pathlib import Path - # Import setuptools build backend - we'll wrap its functions from setuptools.build_meta import ( build_wheel as _setuptools_build_wheel, diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py index 3164eefdb..526969454 100644 --- a/build_ddbc/compiler.py +++ b/build_ddbc/compiler.py @@ -63,12 +63,15 @@ def find_pybind_dir() -> Path: Path.cwd() / "mssql_python" / "pybind", ] + # Check for platform-appropriate build script + build_script = "build.bat" if sys.platform.startswith("win") else "build.sh" + for path in possible_paths: - if path.exists() and (path / "build.sh").exists(): + if path.exists() and (path / build_script).exists(): return path raise FileNotFoundError( - "Could not find mssql_python/pybind directory. " + f"Could not find mssql_python/pybind directory with {build_script}. " "Make sure you're running from the project root." ) diff --git a/setup.py b/setup.py index 675da43ef..409530053 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ pip install -e . # Editable install with auto-compile """ -import os import sys from setuptools import setup, find_packages From 3409a70e2b8d22994447d90ff1dd2b7f243749de Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 29 Jan 2026 06:15:25 +0000 Subject: [PATCH 03/25] CHORE: Update pyproject.toml - GA status and Python 3.14 support - Change Development Status from Beta to Production/Stable (GA release) - Add Python 3.14 to classifiers and Black target versions - Remove unused [tool.flake8] section (flake8 doesn't read pyproject.toml natively) --- pyproject.toml | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e4a749f2..46a734e60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ authors = [ ] keywords = ["mssql", "sql-server", "database", "odbc", "microsoft"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS", @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Database", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -136,7 +137,7 @@ exclude_lines = [ # ============================================================================= [tool.black] line-length = 100 -target-version = ['py310', 'py311', 'py312', 'py313'] +target-version = ['py310', 'py311', 'py312', 'py313', 'py314'] include = '\.pyi?$' extend-exclude = ''' /( @@ -176,22 +177,6 @@ disable = [ [tool.pylint.format] max-line-length = 100 -# ============================================================================= -# Linting - Flake8 (Note: flake8 doesn't support pyproject.toml natively, -# but some plugins like Flake8-pyproject do) -# ============================================================================= -[tool.flake8] -max-line-length = 100 -extend-ignore = ["E203", "W503"] -exclude = [ - ".git", - "__pycache__", - "build", - "dist", - ".venv", - "htmlcov" -] - # ============================================================================= # Type Checking - MyPy # ============================================================================= From 56186bde7fb53f39f2066d50b2f92bdbd2744fc0 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 29 Jan 2026 06:17:19 +0000 Subject: [PATCH 04/25] CHORE: Remove flake8 - not enforced, only Black is required - Remove flake8 from pyproject.toml lint dependencies - Remove flake8 from requirements.txt - Remove flake8 step from lint-check.yml workflow - Remove .flake8 trigger from workflow paths - Delete .flake8 config file Flake8 was never enforced (continue-on-error: true), only Black is blocking. --- .flake8 | 19 ------------------- .github/workflows/lint-check.yml | 14 ++------------ pyproject.toml | 1 - requirements.txt | 1 - 4 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 2c329a529..000000000 --- a/.flake8 +++ /dev/null @@ -1,19 +0,0 @@ -[flake8] -max-line-length = 100 -# Ignore codes: E203 (whitespace before ':'), W503 (line break before binary operator), -# E501 (line too long), E722 (bare except), F401 (unused imports), F841 (unused variables), -# W293 (blank line contains whitespace), W291 (trailing whitespace), -# F541 (f-string missing placeholders), F811 (redefinition of unused), -# E402 (module level import not at top), E711/E712 (comparison to None/True/False), -# E721 (type comparison), F821 (undefined name) -extend-ignore = E203, W503, E501, E722, F401, F841, W293, W291, F541, F811, E402, E711, E712, E721, F821 -exclude = - .git, - __pycache__, - build, - dist, - .venv, - htmlcov, - *.egg-info -per-file-ignores = - __init__.py:F401 diff --git a/.github/workflows/lint-check.yml b/.github/workflows/lint-check.yml index 761620d10..0fd950e6a 100644 --- a/.github/workflows/lint-check.yml +++ b/.github/workflows/lint-check.yml @@ -12,7 +12,6 @@ on: - '**.hpp' - '.github/workflows/lint-check.yml' - 'pyproject.toml' - - '.flake8' - '.clang-format' push: branches: @@ -39,7 +38,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black flake8 pylint autopep8 + pip install black pylint autopep8 if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Check Python formatting with Black @@ -51,15 +50,6 @@ jobs: } echo "::endgroup::" - - name: Lint with Flake8 - run: | - echo "::group::Flake8 Linting" - flake8 mssql_python/ tests/ --max-line-length=100 --extend-ignore=E203,W503,E501,E722,F401,F841,W293,W291,F541,F811,E402,E711,E712,E721,F821 --count --statistics || { - echo "::warning::Flake8 found linting issues (informational only, not blocking)" - } - echo "::endgroup::" - continue-on-error: true - - name: Lint with Pylint run: | echo "::group::Pylint Analysis" @@ -159,7 +149,7 @@ jobs: echo "❌ **Python Formatting (Black):** FAILED - Please run Black formatter" >> $GITHUB_STEP_SUMMARY fi - echo "ℹ️ **Python Linting (Flake8, Pylint):** Informational only" >> $GITHUB_STEP_SUMMARY + echo "ℹ️ **Python Linting (Pylint):** Informational only" >> $GITHUB_STEP_SUMMARY echo "ℹ️ **C++ Linting (clang-format, cpplint):** Informational only" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/pyproject.toml b/pyproject.toml index 46a734e60..06381b85a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ dev = [ lint = [ "black", "autopep8", - "flake8", "pylint", "cpplint", "mypy", diff --git a/requirements.txt b/requirements.txt index 0951f7d04..519e5ef20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ setuptools # Code formatting and linting black autopep8 -flake8 pylint cpplint mypy From a1667e66dc52a2487b13053cf6ab9b12f83610b7 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 29 Jan 2026 06:18:03 +0000 Subject: [PATCH 05/25] CHORE: Update documentation URL to point to wiki --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 06381b85a..c3bcf57f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ [project.urls] Homepage = "https://github.com/microsoft/mssql-python" -Documentation = "https://github.com/microsoft/mssql-python#readme" +Documentation = "https://github.com/microsoft/mssql-python/wiki" Repository = "https://github.com/microsoft/mssql-python" Issues = "https://github.com/microsoft/mssql-python/issues" Changelog = "https://github.com/microsoft/mssql-python/blob/main/CHANGELOG.md" From c42f819bce8b6ebec54c2cb7df452cb2bf31c3d6 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 29 Jan 2026 06:39:42 +0000 Subject: [PATCH 06/25] fix: Address all Copilot review comments - compiler.py: Move platform import to module level - compiler.py: Fix libc detection logic (default to glibc when detection fails) - compiler.py: Lower macOS platform tag from 15.0 to 11.0 (Big Sur) - compiler.py: Update coverage param docs to say Linux/macOS - compiler.py: Print stdout on build failure (not just stderr) - build_backend.py: Handle build_editable import gracefully with better error - pyproject.toml: Fix license field to SPDX string format with license-files - pyproject.toml: Add explicit build_ddbc/tests/benchmarks exclusion - pyproject.toml: Expand self-reference in optional deps for older pip - setup.py: Fix circular dependency by duplicating get_platform_info --- build_ddbc/build_backend.py | 12 +++++++-- build_ddbc/compiler.py | 31 ++++++++++++++++------ pyproject.toml | 20 ++++++++++++++- setup.py | 51 +++++++++++++++++++++++++++++++------ 4 files changed, 95 insertions(+), 19 deletions(-) diff --git a/build_ddbc/build_backend.py b/build_ddbc/build_backend.py index 27045c24c..4eaf8ad05 100644 --- a/build_ddbc/build_backend.py +++ b/build_ddbc/build_backend.py @@ -27,6 +27,7 @@ # PEP 517 Required Hooks # ============================================================================= + def get_requires_for_build_wheel(config_settings=None): """Return build requirements for wheel.""" return _get_requires_for_build_wheel(config_settings) @@ -97,6 +98,7 @@ def build_sdist(sdist_directory, config_settings=None): # Optional PEP 660 Hooks (Editable Installs) # ============================================================================= + def get_requires_for_build_editable(config_settings=None): """Return build requirements for editable install.""" return get_requires_for_build_wheel(config_settings) @@ -121,6 +123,12 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non print(f"[build_backend] Compilation failed: {e}") raise - # Import here to avoid issues if not available - from setuptools.build_meta import build_editable as _setuptools_build_editable + # Import here and handle absence gracefully for older setuptools versions + try: + from setuptools.build_meta import build_editable as _setuptools_build_editable + except ImportError as exc: + raise RuntimeError( + "Editable installs are not supported with this setuptools version. " + "Please install setuptools>=64.0.0 to use PEP 660 editable installs." + ) from exc return _setuptools_build_editable(wheel_directory, config_settings, metadata_directory) diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py index 526969454..8097cfdfa 100644 --- a/build_ddbc/compiler.py +++ b/build_ddbc/compiler.py @@ -5,6 +5,7 @@ """ import os +import platform import sys import subprocess from pathlib import Path @@ -34,13 +35,21 @@ def get_platform_info() -> Tuple[str, str]: return "x64", "win_amd64" elif sys.platform.startswith("darwin"): - return "universal2", "macosx_15_0_universal2" + return "universal2", "macosx_11_0_universal2" elif sys.platform.startswith("linux"): - import platform target_arch = os.environ.get("targetArch", platform.machine()) libc_name, _ = platform.libc_ver() - is_musl = libc_name == "" or "musl" in libc_name.lower() + # Empty libc_name could indicate detection failure; default to glibc (manylinux) + if not libc_name: + print( + "[build_ddbc] Warning: libc detection failed (platform.libc_ver() " + "returned an empty name); defaulting to glibc (manylinux) tags.", + file=sys.stderr, + ) + is_musl = False + else: + is_musl = "musl" in libc_name.lower() if target_arch == "x86_64": return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" @@ -86,7 +95,7 @@ def compile_ddbc( Args: arch: Target architecture (Windows only: x64, x86, arm64) - coverage: Enable coverage instrumentation (Linux only) + coverage: Enable coverage instrumentation (Linux/macOS only) verbose: Print build output Returns: @@ -128,8 +137,11 @@ def _run_windows_build(pybind_dir: Path, arch: str, verbose: bool) -> bool: ) if result.returncode != 0: - if not verbose and result.stderr: - print(result.stderr.decode(), file=sys.stderr) + if not verbose: + if result.stdout: + print(result.stdout.decode(), file=sys.stderr) + if result.stderr: + print(result.stderr.decode(), file=sys.stderr) raise RuntimeError(f"build.bat failed with exit code {result.returncode}") if verbose: @@ -163,8 +175,11 @@ def _run_unix_build(pybind_dir: Path, coverage: bool, verbose: bool) -> bool: ) if result.returncode != 0: - if not verbose and result.stderr: - print(result.stderr.decode(), file=sys.stderr) + if not verbose: + if result.stdout: + print(result.stdout.decode(), file=sys.stderr) + if result.stderr: + print(result.stderr.decode(), file=sys.stderr) raise RuntimeError(f"build.sh failed with exit code {result.returncode}") if verbose: diff --git a/pyproject.toml b/pyproject.toml index c3bcf57f3..ee1f99a61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ version = "1.2.0" description = "A Python library for interacting with Microsoft SQL Server" readme = "PyPI_Description.md" license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.10" authors = [ { name = "Microsoft Corporation", email = "mssql-python@microsoft.com" } @@ -68,7 +69,23 @@ lint = [ "types-setuptools", ] all = [ - "mssql-python[dev,lint]", + # Dev dependencies + "pytest", + "pytest-cov", + "coverage", + "unittest-xml-reporting", + "psutil", + "pybind11", + "setuptools", + "wheel", + "build", + # Lint dependencies + "black", + "autopep8", + "pylint", + "cpplint", + "mypy", + "types-setuptools", ] # ============================================================================= @@ -81,6 +98,7 @@ include-package-data = true [tool.setuptools.packages.find] where = ["."] include = ["mssql_python*"] +exclude = ["build_ddbc*", "tests*", "benchmarks*"] [tool.setuptools.package-data] mssql_python = [ diff --git a/setup.py b/setup.py index 409530053..e379a89aa 100644 --- a/setup.py +++ b/setup.py @@ -14,30 +14,65 @@ pip install -e . # Editable install with auto-compile """ +import os +import platform import sys from setuptools import setup, find_packages from setuptools.dist import Distribution from wheel.bdist_wheel import bdist_wheel -from build_ddbc import get_platform_info + +def get_platform_info(): + """ + Get platform-specific architecture and platform tag information. + + Note: This is duplicated from build_ddbc.compiler to avoid circular imports + during fresh installs where build_ddbc isn't available yet. + """ + if sys.platform.startswith("win"): + arch = os.environ.get("ARCHITECTURE", "x64") + if isinstance(arch, str): + arch = arch.strip("\"'") + if arch in ["x86", "win32"]: + return "x86", "win32" + elif arch == "arm64": + return "arm64", "win_arm64" + else: + return "x64", "win_amd64" + elif sys.platform.startswith("darwin"): + return "universal2", "macosx_11_0_universal2" + elif sys.platform.startswith("linux"): + target_arch = os.environ.get("targetArch", platform.machine()) + libc_name, _ = platform.libc_ver() + is_musl = libc_name and "musl" in libc_name.lower() + if target_arch == "x86_64": + return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" + elif target_arch in ["aarch64", "arm64"]: + return "aarch64", "musllinux_1_2_aarch64" if is_musl else "manylinux_2_28_aarch64" + else: + raise OSError(f"Unsupported architecture '{target_arch}' for Linux") + raise OSError(f"Unsupported platform: {sys.platform}") # ============================================================================= # Platform-Specific Package Discovery # ============================================================================= + def get_platform_packages(): """Get platform-specific package list.""" packages = find_packages() arch, _ = get_platform_info() if sys.platform.startswith("win"): - packages.extend([ - f"mssql_python.libs.windows.{arch}", - f"mssql_python.libs.windows.{arch}.1033", - f"mssql_python.libs.windows.{arch}.vcredist", - ]) + packages.extend( + [ + f"mssql_python.libs.windows.{arch}", + f"mssql_python.libs.windows.{arch}.1033", + f"mssql_python.libs.windows.{arch}.vcredist", + ] + ) elif sys.platform.startswith("darwin"): packages.append("mssql_python.libs.macos") elif sys.platform.startswith("linux"): @@ -50,6 +85,7 @@ def get_platform_packages(): # Custom Distribution (Force Platform-Specific Wheel) # ============================================================================= + class BinaryDistribution(Distribution): """Distribution that forces platform-specific wheel creation.""" @@ -61,6 +97,7 @@ def has_ext_modules(self): # Custom bdist_wheel Command # ============================================================================= + class CustomBdistWheel(bdist_wheel): """Custom wheel builder with platform-specific tags.""" @@ -81,10 +118,8 @@ def finalize_options(self): setup( # Package discovery packages=get_platform_packages(), - # Force binary distribution distclass=BinaryDistribution, - # Register custom commands cmdclass={ "bdist_wheel": CustomBdistWheel, From 2084bb1be87b1972266375a9e223539446ead4dc Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 29 Jan 2026 07:03:30 +0000 Subject: [PATCH 07/25] refactor: Use shared get_platform_info from build_ddbc.compiler - Removed duplicate get_platform_info() from setup.py - Import directly from build_ddbc.compiler instead - Single source of truth for platform detection logic --- build_ddbc/compiler.py | 2 +- setup.py | 34 +--------------------------------- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py index 8097cfdfa..2a874d3ec 100644 --- a/build_ddbc/compiler.py +++ b/build_ddbc/compiler.py @@ -35,7 +35,7 @@ def get_platform_info() -> Tuple[str, str]: return "x64", "win_amd64" elif sys.platform.startswith("darwin"): - return "universal2", "macosx_11_0_universal2" + return "universal2", "macosx_15_0_universal2" elif sys.platform.startswith("linux"): target_arch = os.environ.get("targetArch", platform.machine()) diff --git a/setup.py b/setup.py index e379a89aa..0b5b3f0c0 100644 --- a/setup.py +++ b/setup.py @@ -14,45 +14,13 @@ pip install -e . # Editable install with auto-compile """ -import os -import platform import sys from setuptools import setup, find_packages from setuptools.dist import Distribution from wheel.bdist_wheel import bdist_wheel - -def get_platform_info(): - """ - Get platform-specific architecture and platform tag information. - - Note: This is duplicated from build_ddbc.compiler to avoid circular imports - during fresh installs where build_ddbc isn't available yet. - """ - if sys.platform.startswith("win"): - arch = os.environ.get("ARCHITECTURE", "x64") - if isinstance(arch, str): - arch = arch.strip("\"'") - if arch in ["x86", "win32"]: - return "x86", "win32" - elif arch == "arm64": - return "arm64", "win_arm64" - else: - return "x64", "win_amd64" - elif sys.platform.startswith("darwin"): - return "universal2", "macosx_11_0_universal2" - elif sys.platform.startswith("linux"): - target_arch = os.environ.get("targetArch", platform.machine()) - libc_name, _ = platform.libc_ver() - is_musl = libc_name and "musl" in libc_name.lower() - if target_arch == "x86_64": - return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" - elif target_arch in ["aarch64", "arm64"]: - return "aarch64", "musllinux_1_2_aarch64" if is_musl else "manylinux_2_28_aarch64" - else: - raise OSError(f"Unsupported architecture '{target_arch}' for Linux") - raise OSError(f"Unsupported platform: {sys.platform}") +from build_ddbc.compiler import get_platform_info # ============================================================================= From f797db3fe3145ddf3280a75eafa413f41c497975 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Fri, 30 Jan 2026 05:17:22 +0000 Subject: [PATCH 08/25] fix: Address David's code review feedback - Fix Alpine musl detection with glob fallback for /lib/ld-musl* - Remove shell=True from Windows subprocess call (security) - Fix version duplication - import __version__ from __init__.py - Add OSError handling in build_wheel and build_editable - Sync build_ddbc version to 1.2.0 (matches package) --- build_ddbc/__init__.py | 2 +- build_ddbc/__main__.py | 3 ++- build_ddbc/build_backend.py | 4 ++-- build_ddbc/compiler.py | 18 ++++++++++-------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/build_ddbc/__init__.py b/build_ddbc/__init__.py index 2b10c6fce..2e2e8e148 100644 --- a/build_ddbc/__init__.py +++ b/build_ddbc/__init__.py @@ -15,4 +15,4 @@ from .compiler import compile_ddbc, get_platform_info __all__ = ["compile_ddbc", "get_platform_info"] -__version__ = "1.0.0" +__version__ = "1.2.0" diff --git a/build_ddbc/__main__.py b/build_ddbc/__main__.py index 72c35d493..e767f8f5e 100644 --- a/build_ddbc/__main__.py +++ b/build_ddbc/__main__.py @@ -11,6 +11,7 @@ import argparse import sys +from . import __version__ from .compiler import compile_ddbc, get_platform_info @@ -50,7 +51,7 @@ def main() -> int: parser.add_argument( "--version", "-V", action="version", - version="%(prog)s 1.0.0", + version=f"%(prog)s {__version__}", ) args = parser.parse_args() diff --git a/build_ddbc/build_backend.py b/build_ddbc/build_backend.py index 4eaf8ad05..8eda5af15 100644 --- a/build_ddbc/build_backend.py +++ b/build_ddbc/build_backend.py @@ -73,7 +73,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): except FileNotFoundError: # If build scripts don't exist, assume pre-compiled binaries print("[build_backend] Build scripts not found, assuming pre-compiled binaries") - except RuntimeError as e: + except (RuntimeError, OSError) as e: print(f"[build_backend] Compilation failed: {e}") raise else: @@ -119,7 +119,7 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non print("[build_backend] Compilation successful!") except FileNotFoundError: print("[build_backend] Build scripts not found, assuming pre-compiled binaries") - except RuntimeError as e: + except (RuntimeError, OSError) as e: print(f"[build_backend] Compilation failed: {e}") raise diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py index 2a874d3ec..b40a2832d 100644 --- a/build_ddbc/compiler.py +++ b/build_ddbc/compiler.py @@ -4,6 +4,7 @@ This module contains the platform detection and build script execution logic. """ +import glob import os import platform import sys @@ -40,14 +41,16 @@ def get_platform_info() -> Tuple[str, str]: elif sys.platform.startswith("linux"): target_arch = os.environ.get("targetArch", platform.machine()) libc_name, _ = platform.libc_ver() - # Empty libc_name could indicate detection failure; default to glibc (manylinux) + if not libc_name: - print( - "[build_ddbc] Warning: libc detection failed (platform.libc_ver() " - "returned an empty name); defaulting to glibc (manylinux) tags.", - file=sys.stderr, - ) - is_musl = False + # Fallback: check for musl linker (Alpine Linux) + # platform.libc_ver() returns empty string on Alpine + is_musl = bool(glob.glob("/lib/ld-musl*")) + if not is_musl: + print( + "[build_ddbc] Warning: libc detection failed; defaulting to glibc.", + file=sys.stderr, + ) else: is_musl = "musl" in libc_name.lower() @@ -131,7 +134,6 @@ def _run_windows_build(pybind_dir: Path, arch: str, verbose: bool) -> bool: result = subprocess.run( cmd, cwd=pybind_dir, - shell=True, check=False, capture_output=not verbose, ) From 6a50152016abf7aa09383979904164c3d628d220 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Fri, 30 Jan 2026 05:32:10 +0000 Subject: [PATCH 09/25] fix: Update setuptools requirement and remove duplicate coverage config - Bump setuptools>=64.0.0 (required for PEP 660 editable installs) - Remove [tool.coverage] section - .coveragerc is single source of truth --- pyproject.toml | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ee1f99a61..6e86d4527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ # Build System Configuration # ============================================================================= [build-system] -requires = ["setuptools>=61.0", "wheel", "pybind11"] +requires = ["setuptools>=64.0.0", "wheel", "pybind11"] build-backend = "build_ddbc.build_backend" backend-path = ["."] @@ -130,25 +130,6 @@ markers = [ ] addopts = "-m 'not stress'" -# ============================================================================= -# Coverage Configuration -# ============================================================================= -[tool.coverage.run] -source = ["mssql_python"] -omit = [ - "*/tests/*", - "*/__pycache__/*", - "*/pybind/*", -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "raise NotImplementedError", - "if __name__ == .__main__.:", -] - # ============================================================================= # Code Formatting - Black # ============================================================================= From af72b632650d2e9e9b7557ef3d309bceb8790ed5 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 9 Feb 2026 05:36:54 +0000 Subject: [PATCH 10/25] Refactor: Move get_platform_info to mssql_python/platform_utils.py - Single source of truth for platform detection - Clean imports in setup.py (no sys.path hacks) - build_ddbc re-exports for backwards compatibility --- build_ddbc/__init__.py | 3 +- build_ddbc/compiler.py | 62 ++----------------------------- mssql_python/platform_utils.py | 67 ++++++++++++++++++++++++++++++++++ setup.py | 2 +- 4 files changed, 74 insertions(+), 60 deletions(-) create mode 100644 mssql_python/platform_utils.py diff --git a/build_ddbc/__init__.py b/build_ddbc/__init__.py index 2e2e8e148..796cd1884 100644 --- a/build_ddbc/__init__.py +++ b/build_ddbc/__init__.py @@ -12,7 +12,8 @@ python -m build # Compile + create wheel (automatic) """ -from .compiler import compile_ddbc, get_platform_info +from .compiler import compile_ddbc +from mssql_python.platform_utils import get_platform_info __all__ = ["compile_ddbc", "get_platform_info"] __version__ = "1.2.0" diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py index b40a2832d..1362fbfaa 100644 --- a/build_ddbc/compiler.py +++ b/build_ddbc/compiler.py @@ -1,70 +1,16 @@ """ Core compiler logic for ddbc_bindings. -This module contains the platform detection and build script execution logic. +This module contains the build script execution logic. +Platform detection is provided by mssql_python.platform_utils. """ -import glob -import os -import platform import sys import subprocess from pathlib import Path -from typing import Tuple, Optional +from typing import Optional - -def get_platform_info() -> Tuple[str, str]: - """ - Get platform-specific architecture and platform tag information. - - Returns: - Tuple of (architecture, platform_tag) - - Raises: - OSError: If the platform or architecture is not supported - """ - if sys.platform.startswith("win"): - arch = os.environ.get("ARCHITECTURE", "x64") - if isinstance(arch, str): - arch = arch.strip("\"'") - - if arch in ["x86", "win32"]: - return "x86", "win32" - elif arch == "arm64": - return "arm64", "win_arm64" - else: - return "x64", "win_amd64" - - elif sys.platform.startswith("darwin"): - return "universal2", "macosx_15_0_universal2" - - elif sys.platform.startswith("linux"): - target_arch = os.environ.get("targetArch", platform.machine()) - libc_name, _ = platform.libc_ver() - - if not libc_name: - # Fallback: check for musl linker (Alpine Linux) - # platform.libc_ver() returns empty string on Alpine - is_musl = bool(glob.glob("/lib/ld-musl*")) - if not is_musl: - print( - "[build_ddbc] Warning: libc detection failed; defaulting to glibc.", - file=sys.stderr, - ) - else: - is_musl = "musl" in libc_name.lower() - - if target_arch == "x86_64": - return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" - elif target_arch in ["aarch64", "arm64"]: - return "aarch64", "musllinux_1_2_aarch64" if is_musl else "manylinux_2_28_aarch64" - else: - raise OSError( - f"Unsupported architecture '{target_arch}' for Linux; " - "expected 'x86_64' or 'aarch64'." - ) - - raise OSError(f"Unsupported platform: {sys.platform}") +from mssql_python.platform_utils import get_platform_info def find_pybind_dir() -> Path: diff --git a/mssql_python/platform_utils.py b/mssql_python/platform_utils.py new file mode 100644 index 000000000..f324f390c --- /dev/null +++ b/mssql_python/platform_utils.py @@ -0,0 +1,67 @@ +""" +Platform detection utilities for mssql-python. + +This module provides platform and architecture detection used by both +the build system (setup.py, build_ddbc) and runtime code. +""" + +import glob +import os +import platform +import sys +from typing import Tuple + + +def get_platform_info() -> Tuple[str, str]: + """ + Get platform-specific architecture and platform tag information. + + Returns: + Tuple of (architecture, platform_tag) where platform_tag is a + PEP 425 compatible wheel platform tag. + + Raises: + OSError: If the platform or architecture is not supported + """ + if sys.platform.startswith("win"): + arch = os.environ.get("ARCHITECTURE", "x64") + if isinstance(arch, str): + arch = arch.strip("\"'") + + if arch in ["x86", "win32"]: + return "x86", "win32" + elif arch == "arm64": + return "arm64", "win_arm64" + else: + return "x64", "win_amd64" + + elif sys.platform.startswith("darwin"): + return "universal2", "macosx_15_0_universal2" + + elif sys.platform.startswith("linux"): + target_arch = os.environ.get("targetArch", platform.machine()) + libc_name, _ = platform.libc_ver() + + if not libc_name: + # Fallback: check for musl linker (Alpine Linux) + # platform.libc_ver() returns empty string on Alpine + is_musl = bool(glob.glob("/lib/ld-musl*")) + if not is_musl: + print( + "[mssql_python] Warning: libc detection failed; defaulting to glibc.", + file=sys.stderr, + ) + else: + is_musl = "musl" in libc_name.lower() + + if target_arch == "x86_64": + return "x86_64", "musllinux_1_2_x86_64" if is_musl else "manylinux_2_28_x86_64" + elif target_arch in ["aarch64", "arm64"]: + return "aarch64", "musllinux_1_2_aarch64" if is_musl else "manylinux_2_28_aarch64" + else: + raise OSError( + f"Unsupported architecture '{target_arch}' for Linux; " + "expected 'x86_64' or 'aarch64'." + ) + + raise OSError(f"Unsupported platform: {sys.platform}") diff --git a/setup.py b/setup.py index 0b5b3f0c0..e9bfb3e0d 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from setuptools.dist import Distribution from wheel.bdist_wheel import bdist_wheel -from build_ddbc.compiler import get_platform_info +from mssql_python.platform_utils import get_platform_info # ============================================================================= From 9aa716bfa08b520d03c526a1c65222b6456ff991 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 9 Feb 2026 05:44:50 +0000 Subject: [PATCH 11/25] Fix version to 1.3.0 and add py.typed to package-data --- build_ddbc/__init__.py | 2 +- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build_ddbc/__init__.py b/build_ddbc/__init__.py index 796cd1884..44e7f7162 100644 --- a/build_ddbc/__init__.py +++ b/build_ddbc/__init__.py @@ -16,4 +16,4 @@ from mssql_python.platform_utils import get_platform_info __all__ = ["compile_ddbc", "get_platform_info"] -__version__ = "1.2.0" +__version__ = "1.3.0" diff --git a/pyproject.toml b/pyproject.toml index 6e86d4527..8da1437ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ backend-path = ["."] # ============================================================================= [project] name = "mssql-python" -version = "1.2.0" +version = "1.3.0" description = "A Python library for interacting with Microsoft SQL Server" readme = "PyPI_Description.md" license = "MIT" @@ -102,6 +102,7 @@ exclude = ["build_ddbc*", "tests*", "benchmarks*"] [tool.setuptools.package-data] mssql_python = [ + "py.typed", "ddbc_bindings.cp*.pyd", "ddbc_bindings.cp*.so", "libs/*", From da350dee3574102749faf25f81d2729b8adeb84f Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 9 Feb 2026 05:49:41 +0000 Subject: [PATCH 12/25] Add tests for platform_utils (100% coverage) --- tests/test_001_globals.py | 234 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 7c004a136..79973fdb3 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -740,3 +740,237 @@ def separator_reader_worker(): # Always make sure to clean up stop_event.set() setDecimalSeparator(original_separator) + + +# ============================================================================= +# Platform Utils Tests +# ============================================================================= + + +class TestPlatformUtils: + """Tests for mssql_python.platform_utils module.""" + + def test_get_platform_info_returns_tuple(self): + """Test that get_platform_info returns a tuple of two strings.""" + from mssql_python.platform_utils import get_platform_info + + result = get_platform_info() + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], str) # architecture + assert isinstance(result[1], str) # platform_tag + + def test_get_platform_info_current_platform(self): + """Test get_platform_info on current platform returns valid values.""" + from mssql_python.platform_utils import get_platform_info + import sys + + arch, platform_tag = get_platform_info() + + # Architecture should be non-empty + assert arch + + # Platform tag should match current platform + if sys.platform.startswith("win"): + assert "win" in platform_tag + elif sys.platform.startswith("darwin"): + assert "macos" in platform_tag + elif sys.platform.startswith("linux"): + assert "linux" in platform_tag + + def test_windows_x64_detection(self): + """Test Windows x64 platform detection.""" + from unittest.mock import patch + + with patch("mssql_python.platform_utils.sys") as mock_sys, \ + patch("mssql_python.platform_utils.os") as mock_os: + mock_sys.platform = "win32" + mock_os.environ.get.return_value = "x64" + + from mssql_python import platform_utils + # Force reimport to pick up mocked values + import importlib + importlib.reload(platform_utils) + + # Restore for actual test + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="x64"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x64" + assert tag == "win_amd64" + + def test_windows_x86_detection(self): + """Test Windows x86 platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="x86"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86" + assert tag == "win32" + + def test_windows_arm64_detection(self): + """Test Windows ARM64 platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="arm64"): + arch, tag = platform_utils.get_platform_info() + assert arch == "arm64" + assert tag == "win_arm64" + + def test_macos_detection(self): + """Test macOS platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "darwin"): + arch, tag = platform_utils.get_platform_info() + assert arch == "universal2" + assert "macosx" in tag + assert "universal2" in tag + + def test_linux_x86_64_glibc_detection(self): + """Test Linux x86_64 glibc platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): + with patch.object(platform_utils.platform, "machine", return_value="x86_64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "manylinux_2_28_x86_64" + + def test_linux_x86_64_musl_detection(self): + """Test Linux x86_64 musl platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): + with patch.object(platform_utils.platform, "machine", return_value="x86_64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "musllinux_1_2_x86_64" + + def test_linux_aarch64_glibc_detection(self): + """Test Linux aarch64 glibc platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="aarch64"): + with patch.object(platform_utils.platform, "machine", return_value="aarch64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "manylinux_2_28_aarch64" + + def test_linux_aarch64_musl_detection(self): + """Test Linux aarch64 musl platform detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="aarch64"): + with patch.object(platform_utils.platform, "machine", return_value="aarch64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "musllinux_1_2_aarch64" + + def test_linux_arm64_alias(self): + """Test Linux arm64 is treated as aarch64.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="arm64"): + with patch.object(platform_utils.platform, "machine", return_value="arm64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "manylinux_2_28_aarch64" + + def test_linux_empty_libc_with_musl_glob(self): + """Test Linux with empty libc_ver falls back to glob for musl detection.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): + with patch.object(platform_utils.platform, "machine", return_value="x86_64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("", "")): + with patch.object(platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"]): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "musllinux_1_2_x86_64" + + def test_linux_empty_libc_no_musl_glob(self, capsys): + """Test Linux with empty libc_ver and no musl glob defaults to glibc.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): + with patch.object(platform_utils.platform, "machine", return_value="x86_64"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("", "")): + with patch.object(platform_utils.glob, "glob", return_value=[]): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "manylinux_2_28_x86_64" + # Check warning was printed + captured = capsys.readouterr() + assert "Warning" in captured.err or "warning" in captured.err.lower() + + def test_linux_unsupported_architecture(self): + """Test Linux with unsupported architecture raises OSError.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "linux"): + with patch.object(platform_utils.os.environ, "get", return_value="ppc64le"): + with patch.object(platform_utils.platform, "machine", return_value="ppc64le"): + with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + with pytest.raises(OSError) as exc_info: + platform_utils.get_platform_info() + assert "ppc64le" in str(exc_info.value) + assert "Unsupported architecture" in str(exc_info.value) + + def test_unsupported_platform(self): + """Test unsupported platform raises OSError.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "freebsd"): + with pytest.raises(OSError) as exc_info: + platform_utils.get_platform_info() + assert "freebsd" in str(exc_info.value) + assert "Unsupported platform" in str(exc_info.value) + + def test_windows_strips_quotes_from_arch(self): + """Test Windows architecture strips surrounding quotes.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value='"x64"'): + arch, tag = platform_utils.get_platform_info() + assert arch == "x64" + assert tag == "win_amd64" + + def test_windows_win32_alias(self): + """Test Windows win32 is treated as x86.""" + from unittest.mock import patch + from mssql_python import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="win32"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86" + assert tag == "win32" From aa360788264191a3e164efbba23fc1a433023dc8 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 9 Feb 2026 05:55:50 +0000 Subject: [PATCH 13/25] Fix Black formatting in platform_utils tests --- tests/test_001_globals.py | 48 ++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 79973fdb3..e4b9ca995 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -388,7 +388,8 @@ def test_decimal_separator_with_db_operations(db_connection): try: # Create a test table with decimal values cursor = db_connection.cursor() - cursor.execute(""" + cursor.execute( + """ DROP TABLE IF EXISTS #decimal_separator_test; CREATE TABLE #decimal_separator_test ( id INT, @@ -399,7 +400,8 @@ def test_decimal_separator_with_db_operations(db_connection): (2, 678.90), (3, 0.01), (4, 999.99); - """) + """ + ) cursor.close() # Test 1: Fetch with default separator @@ -467,7 +469,8 @@ def test_decimal_separator_batch_operations(db_connection): try: # Create test data cursor = db_connection.cursor() - cursor.execute(""" + cursor.execute( + """ DROP TABLE IF EXISTS #decimal_batch_test; CREATE TABLE #decimal_batch_test ( id INT, @@ -478,7 +481,8 @@ def test_decimal_separator_batch_operations(db_connection): (1, 123.456, 12345.67890), (2, 0.001, 0.00001), (3, 999.999, 9999.99999); - """) + """ + ) cursor.close() # Test 1: Fetch results with default separator @@ -782,14 +786,18 @@ def test_windows_x64_detection(self): """Test Windows x64 platform detection.""" from unittest.mock import patch - with patch("mssql_python.platform_utils.sys") as mock_sys, \ - patch("mssql_python.platform_utils.os") as mock_os: + with ( + patch("mssql_python.platform_utils.sys") as mock_sys, + patch("mssql_python.platform_utils.os") as mock_os, + ): mock_sys.platform = "win32" mock_os.environ.get.return_value = "x64" from mssql_python import platform_utils + # Force reimport to pick up mocked values import importlib + importlib.reload(platform_utils) # Restore for actual test @@ -840,7 +848,9 @@ def test_linux_x86_64_glibc_detection(self): with patch.object(platform_utils.sys, "platform", "linux"): with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): with patch.object(platform_utils.platform, "machine", return_value="x86_64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + with patch.object( + platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") + ): arch, tag = platform_utils.get_platform_info() assert arch == "x86_64" assert tag == "manylinux_2_28_x86_64" @@ -853,7 +863,9 @@ def test_linux_x86_64_musl_detection(self): with patch.object(platform_utils.sys, "platform", "linux"): with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): with patch.object(platform_utils.platform, "machine", return_value="x86_64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")): + with patch.object( + platform_utils.platform, "libc_ver", return_value=("musl", "1.2") + ): arch, tag = platform_utils.get_platform_info() assert arch == "x86_64" assert tag == "musllinux_1_2_x86_64" @@ -866,7 +878,9 @@ def test_linux_aarch64_glibc_detection(self): with patch.object(platform_utils.sys, "platform", "linux"): with patch.object(platform_utils.os.environ, "get", return_value="aarch64"): with patch.object(platform_utils.platform, "machine", return_value="aarch64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + with patch.object( + platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") + ): arch, tag = platform_utils.get_platform_info() assert arch == "aarch64" assert tag == "manylinux_2_28_aarch64" @@ -879,7 +893,9 @@ def test_linux_aarch64_musl_detection(self): with patch.object(platform_utils.sys, "platform", "linux"): with patch.object(platform_utils.os.environ, "get", return_value="aarch64"): with patch.object(platform_utils.platform, "machine", return_value="aarch64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")): + with patch.object( + platform_utils.platform, "libc_ver", return_value=("musl", "1.2") + ): arch, tag = platform_utils.get_platform_info() assert arch == "aarch64" assert tag == "musllinux_1_2_aarch64" @@ -892,7 +908,9 @@ def test_linux_arm64_alias(self): with patch.object(platform_utils.sys, "platform", "linux"): with patch.object(platform_utils.os.environ, "get", return_value="arm64"): with patch.object(platform_utils.platform, "machine", return_value="arm64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + with patch.object( + platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") + ): arch, tag = platform_utils.get_platform_info() assert arch == "aarch64" assert tag == "manylinux_2_28_aarch64" @@ -906,7 +924,9 @@ def test_linux_empty_libc_with_musl_glob(self): with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): with patch.object(platform_utils.platform, "machine", return_value="x86_64"): with patch.object(platform_utils.platform, "libc_ver", return_value=("", "")): - with patch.object(platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"]): + with patch.object( + platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"] + ): arch, tag = platform_utils.get_platform_info() assert arch == "x86_64" assert tag == "musllinux_1_2_x86_64" @@ -936,7 +956,9 @@ def test_linux_unsupported_architecture(self): with patch.object(platform_utils.sys, "platform", "linux"): with patch.object(platform_utils.os.environ, "get", return_value="ppc64le"): with patch.object(platform_utils.platform, "machine", return_value="ppc64le"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")): + with patch.object( + platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") + ): with pytest.raises(OSError) as exc_info: platform_utils.get_platform_info() assert "ppc64le" in str(exc_info.value) From ee794b5fd538f45a24a75429bce12ed33a3cfaa6 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 9 Feb 2026 06:02:51 +0000 Subject: [PATCH 14/25] Fix Black 26.1.0 formatting (multi-line string style change) --- tests/test_001_globals.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index e4b9ca995..72e74bc6e 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -388,8 +388,7 @@ def test_decimal_separator_with_db_operations(db_connection): try: # Create a test table with decimal values cursor = db_connection.cursor() - cursor.execute( - """ + cursor.execute(""" DROP TABLE IF EXISTS #decimal_separator_test; CREATE TABLE #decimal_separator_test ( id INT, @@ -400,8 +399,7 @@ def test_decimal_separator_with_db_operations(db_connection): (2, 678.90), (3, 0.01), (4, 999.99); - """ - ) + """) cursor.close() # Test 1: Fetch with default separator @@ -469,8 +467,7 @@ def test_decimal_separator_batch_operations(db_connection): try: # Create test data cursor = db_connection.cursor() - cursor.execute( - """ + cursor.execute(""" DROP TABLE IF EXISTS #decimal_batch_test; CREATE TABLE #decimal_batch_test ( id INT, @@ -481,8 +478,7 @@ def test_decimal_separator_batch_operations(db_connection): (1, 123.456, 12345.67890), (2, 0.001, 0.00001), (3, 999.999, 9999.99999); - """ - ) + """) cursor.close() # Test 1: Fetch results with default separator From 515b673c5bc4027c1722c7e5cf099eb7e6f7de87 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 12 Feb 2026 12:12:24 +0000 Subject: [PATCH 15/25] fix: address Copilot review comments - setup.py: filter find_packages to exclude build_ddbc from wheel - build_backend.py: add _is_truthy() for PEP 517 config_settings boolean coercion - test_001_globals.py: clean up test_windows_x64_detection (remove dead reload/patch) --- build_ddbc/build_backend.py | 17 +++++++++++++++-- setup.py | 3 +-- tests/test_001_globals.py | 26 ++++++-------------------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/build_ddbc/build_backend.py b/build_ddbc/build_backend.py index 8eda5af15..e602263c9 100644 --- a/build_ddbc/build_backend.py +++ b/build_ddbc/build_backend.py @@ -23,6 +23,19 @@ from .compiler import compile_ddbc +def _is_truthy(value): + """Convert a config_settings value to a boolean. + + PEP 517 frontends pass config_settings values as strings (e.g., "true"), + not Python booleans. This helper normalises them. + """ + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.lower() in ("true", "1", "yes") + return bool(value) + + # ============================================================================= # PEP 517 Required Hooks # ============================================================================= @@ -55,7 +68,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): # Check if we should skip compilation (e.g., for sdist-only builds) skip_compile = False if config_settings: - skip_compile = config_settings.get("--skip-ddbc-compile", False) + skip_compile = _is_truthy(config_settings.get("--skip-ddbc-compile", False)) if not skip_compile: # Extract build options from config_settings @@ -64,7 +77,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): if config_settings: arch = config_settings.get("--arch") - coverage = config_settings.get("--coverage", False) + coverage = _is_truthy(config_settings.get("--coverage", False)) print("[build_backend] Compiling ddbc_bindings...") try: diff --git a/setup.py b/setup.py index e9bfb3e0d..d19b03066 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ from mssql_python.platform_utils import get_platform_info - # ============================================================================= # Platform-Specific Package Discovery # ============================================================================= @@ -30,7 +29,7 @@ def get_platform_packages(): """Get platform-specific package list.""" - packages = find_packages() + packages = find_packages(include=["mssql_python", "mssql_python.*"]) arch, _ = get_platform_info() if sys.platform.startswith("win"): diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 72e74bc6e..dd0dcd752 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -781,27 +781,13 @@ def test_get_platform_info_current_platform(self): def test_windows_x64_detection(self): """Test Windows x64 platform detection.""" from unittest.mock import patch + from mssql_python import platform_utils - with ( - patch("mssql_python.platform_utils.sys") as mock_sys, - patch("mssql_python.platform_utils.os") as mock_os, - ): - mock_sys.platform = "win32" - mock_os.environ.get.return_value = "x64" - - from mssql_python import platform_utils - - # Force reimport to pick up mocked values - import importlib - - importlib.reload(platform_utils) - - # Restore for actual test - with patch.object(platform_utils.sys, "platform", "win32"): - with patch.object(platform_utils.os.environ, "get", return_value="x64"): - arch, tag = platform_utils.get_platform_info() - assert arch == "x64" - assert tag == "win_amd64" + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="x64"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x64" + assert tag == "win_amd64" def test_windows_x86_detection(self): """Test Windows x86 platform detection.""" From a27b023fe68cd6f1b829507d834cd137ac7dd6c7 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 12 Feb 2026 12:39:27 +0000 Subject: [PATCH 16/25] fix: avoid importing mssql_python in build_ddbc to prevent segfault on shutdown build_ddbc.compiler imported from mssql_python.platform_utils, which triggered mssql_python/__init__.py. That init loads the native ddbc_bindings .so and registers atexit handlers whose ODBC handle teardown segfaults when the interpreter exits after a build-only run (python -m build_ddbc). Fix: insert mssql_python/ into sys.path temporarily and import platform_utils directly, bypassing the package __init__. --- build_ddbc/__init__.py | 3 +-- build_ddbc/compiler.py | 16 +++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/build_ddbc/__init__.py b/build_ddbc/__init__.py index 44e7f7162..86d9ab38a 100644 --- a/build_ddbc/__init__.py +++ b/build_ddbc/__init__.py @@ -12,8 +12,7 @@ python -m build # Compile + create wheel (automatic) """ -from .compiler import compile_ddbc -from mssql_python.platform_utils import get_platform_info +from .compiler import compile_ddbc, get_platform_info __all__ = ["compile_ddbc", "get_platform_info"] __version__ = "1.3.0" diff --git a/build_ddbc/compiler.py b/build_ddbc/compiler.py index 1362fbfaa..e2b891fe1 100644 --- a/build_ddbc/compiler.py +++ b/build_ddbc/compiler.py @@ -1,8 +1,7 @@ -""" -Core compiler logic for ddbc_bindings. +"""Core compiler logic for ddbc_bindings. -This module contains the build script execution logic. -Platform detection is provided by mssql_python.platform_utils. +Locates and runs the platform-specific build script +(``build.sh`` / ``build.bat``) in ``mssql_python/pybind/``. """ import sys @@ -10,7 +9,14 @@ from pathlib import Path from typing import Optional -from mssql_python.platform_utils import get_platform_info +# Import platform_utils directly without going through mssql_python.__init__ +# (which loads the native ddbc_bindings .so). +_mssql_dir = str(Path(__file__).resolve().parent.parent / "mssql_python") +sys.path.insert(0, _mssql_dir) +import platform_utils as _platform_utils # noqa: E402 +sys.path.remove(_mssql_dir) + +get_platform_info = _platform_utils.get_platform_info def find_pybind_dir() -> Path: From e363b72cfce33c6d1c2fe8e5c9143315ec568e8d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 06:30:58 +0000 Subject: [PATCH 17/25] =?UTF-8?q?refactor:=20rename=20build=5Fddbc=20?= =?UTF-8?q?=E2=86=92=20build=5Fbackend,=20move=20platform=5Futils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename build_ddbc/ to build_backend/ for clearer naming - Rename build_backend.py to hooks.py (PEP 517 hook module) - Move platform_utils.py from mssql_python/ to build_backend/ (it was never imported by mssql_python modules, only by build tools) - Add [project.scripts] entry: build-ddbc = build_backend.__main__:main - Update pyproject.toml build-backend path to build_backend.hooks - Update all test imports to use build_backend.platform_utils - Update setup.py import to use build_backend.platform_utils --- build_backend/__init__.py | 19 ++ {build_ddbc => build_backend}/__main__.py | 29 +-- {build_ddbc => build_backend}/compiler.py | 21 +- .../hooks.py | 36 +++- .../platform_utils.py | 4 +- build_ddbc/__init__.py | 18 -- pyproject.toml | 26 +-- setup.py | 6 +- tests/test_001_globals.py | 204 +++++++++--------- 9 files changed, 181 insertions(+), 182 deletions(-) create mode 100644 build_backend/__init__.py rename {build_ddbc => build_backend}/__main__.py (63%) rename {build_ddbc => build_backend}/compiler.py (82%) rename build_ddbc/build_backend.py => build_backend/hooks.py (82%) rename {mssql_python => build_backend}/platform_utils.py (97%) delete mode 100644 build_ddbc/__init__.py diff --git a/build_backend/__init__.py b/build_backend/__init__.py new file mode 100644 index 000000000..ae1775104 --- /dev/null +++ b/build_backend/__init__.py @@ -0,0 +1,19 @@ +""" +build_backend - Build system for mssql-python native extensions. + +This package provides: +1. A CLI tool: `python -m build_backend` +2. A PEP 517 build backend that auto-compiles ddbc_bindings + +Usage: + python -m build_backend # Compile ddbc_bindings only + python -m build_backend --arch arm64 # Specify architecture (Windows) + python -m build_backend --coverage # Enable coverage (Linux) + python -m build # Compile + create wheel (automatic) +""" + +from .compiler import compile_ddbc +from .platform_utils import get_platform_info + +__all__ = ["compile_ddbc", "get_platform_info"] +__version__ = "1.3.0" diff --git a/build_ddbc/__main__.py b/build_backend/__main__.py similarity index 63% rename from build_ddbc/__main__.py rename to build_backend/__main__.py index e767f8f5e..a6b6b76ef 100644 --- a/build_ddbc/__main__.py +++ b/build_backend/__main__.py @@ -1,32 +1,33 @@ """ -CLI entry point for build_ddbc. +CLI entry point for build_backend. Usage: - python -m build_ddbc # Compile ddbc_bindings - python -m build_ddbc --arch arm64 # Specify architecture (Windows) - python -m build_ddbc --coverage # Enable coverage (Linux) - python -m build_ddbc --help # Show help + python -m build_backend # Compile ddbc_bindings + python -m build_backend --arch arm64 # Specify architecture (Windows) + python -m build_backend --coverage # Enable coverage (Linux) + python -m build_backend --help # Show help """ import argparse import sys from . import __version__ -from .compiler import compile_ddbc, get_platform_info +from .compiler import compile_ddbc +from .platform_utils import get_platform_info def main() -> int: """Main entry point for the CLI.""" parser = argparse.ArgumentParser( - prog="python -m build_ddbc", + prog="python -m build_backend", description="Compile ddbc_bindings native extension for mssql-python", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - python -m build_ddbc # Build for current platform - python -m build_ddbc --arch arm64 # Build for ARM64 (Windows) - python -m build_ddbc --coverage # Build with coverage (Linux) - python -m build_ddbc --quiet # Build without output + python -m build_backend # Build for current platform + python -m build_backend --arch arm64 # Build for ARM64 (Windows) + python -m build_backend --coverage # Build with coverage (Linux) + python -m build_backend --quiet # Build without output """, ) @@ -59,9 +60,9 @@ def main() -> int: # Show platform info if not args.quiet: arch, platform_tag = get_platform_info() - print(f"[build_ddbc] Platform: {sys.platform}") - print(f"[build_ddbc] Architecture: {arch}") - print(f"[build_ddbc] Platform tag: {platform_tag}") + print(f"[build_backend] Platform: {sys.platform}") + print(f"[build_backend] Architecture: {arch}") + print(f"[build_backend] Platform tag: {platform_tag}") print() try: diff --git a/build_ddbc/compiler.py b/build_backend/compiler.py similarity index 82% rename from build_ddbc/compiler.py rename to build_backend/compiler.py index e2b891fe1..824f8486d 100644 --- a/build_ddbc/compiler.py +++ b/build_backend/compiler.py @@ -9,14 +9,7 @@ from pathlib import Path from typing import Optional -# Import platform_utils directly without going through mssql_python.__init__ -# (which loads the native ddbc_bindings .so). -_mssql_dir = str(Path(__file__).resolve().parent.parent / "mssql_python") -sys.path.insert(0, _mssql_dir) -import platform_utils as _platform_utils # noqa: E402 -sys.path.remove(_mssql_dir) - -get_platform_info = _platform_utils.get_platform_info +from .platform_utils import get_platform_info def find_pybind_dir() -> Path: @@ -80,8 +73,8 @@ def _run_windows_build(pybind_dir: Path, arch: str, verbose: bool) -> bool: cmd = [str(build_script), arch] if verbose: - print(f"[build_ddbc] Running: {' '.join(cmd)}") - print(f"[build_ddbc] Working directory: {pybind_dir}") + print(f"[build_backend] Running: {' '.join(cmd)}") + print(f"[build_backend] Working directory: {pybind_dir}") result = subprocess.run( cmd, @@ -99,7 +92,7 @@ def _run_windows_build(pybind_dir: Path, arch: str, verbose: bool) -> bool: raise RuntimeError(f"build.bat failed with exit code {result.returncode}") if verbose: - print("[build_ddbc] Windows build completed successfully!") + print("[build_backend] Windows build completed successfully!") return True @@ -118,8 +111,8 @@ def _run_unix_build(pybind_dir: Path, coverage: bool, verbose: bool) -> bool: cmd.append("--coverage") if verbose: - print(f"[build_ddbc] Running: {' '.join(cmd)}") - print(f"[build_ddbc] Working directory: {pybind_dir}") + print(f"[build_backend] Running: {' '.join(cmd)}") + print(f"[build_backend] Working directory: {pybind_dir}") result = subprocess.run( cmd, @@ -137,6 +130,6 @@ def _run_unix_build(pybind_dir: Path, coverage: bool, verbose: bool) -> bool: raise RuntimeError(f"build.sh failed with exit code {result.returncode}") if verbose: - print("[build_ddbc] Unix build completed successfully!") + print("[build_backend] Unix build completed successfully!") return True diff --git a/build_ddbc/build_backend.py b/build_backend/hooks.py similarity index 82% rename from build_ddbc/build_backend.py rename to build_backend/hooks.py index e602263c9..c08594465 100644 --- a/build_ddbc/build_backend.py +++ b/build_backend/hooks.py @@ -7,7 +7,7 @@ Usage in pyproject.toml: [build-system] requires = ["setuptools>=61.0", "wheel", "pybind11"] - build-backend = "build_ddbc.build_backend" + build-backend = "build_backend.hooks" backend-path = ["."] """ @@ -125,16 +125,30 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non """ print("[build_backend] Starting editable install...") - # Compile ddbc_bindings for editable installs too - print("[build_backend] Compiling ddbc_bindings for editable install...") - try: - compile_ddbc(verbose=True) - print("[build_backend] Compilation successful!") - except FileNotFoundError: - print("[build_backend] Build scripts not found, assuming pre-compiled binaries") - except (RuntimeError, OSError) as e: - print(f"[build_backend] Compilation failed: {e}") - raise + # Check if we should skip compilation + skip_compile = False + if config_settings: + skip_compile = _is_truthy(config_settings.get("--skip-ddbc-compile", False)) + + if not skip_compile: + arch = None + coverage = False + + if config_settings: + arch = config_settings.get("--arch") + coverage = _is_truthy(config_settings.get("--coverage", False)) + + print("[build_backend] Compiling ddbc_bindings for editable install...") + try: + compile_ddbc(arch=arch, coverage=coverage, verbose=True) + print("[build_backend] Compilation successful!") + except FileNotFoundError: + print("[build_backend] Build scripts not found, assuming pre-compiled binaries") + except (RuntimeError, OSError) as e: + print(f"[build_backend] Compilation failed: {e}") + raise + else: + print("[build_backend] Skipping ddbc compilation (--skip-ddbc-compile)") # Import here and handle absence gracefully for older setuptools versions try: diff --git a/mssql_python/platform_utils.py b/build_backend/platform_utils.py similarity index 97% rename from mssql_python/platform_utils.py rename to build_backend/platform_utils.py index f324f390c..9c7bde200 100644 --- a/mssql_python/platform_utils.py +++ b/build_backend/platform_utils.py @@ -1,8 +1,8 @@ """ Platform detection utilities for mssql-python. -This module provides platform and architecture detection used by both -the build system (setup.py, build_ddbc) and runtime code. +This module provides platform and architecture detection used by +the build system (setup.py, build_backend). """ import glob diff --git a/build_ddbc/__init__.py b/build_ddbc/__init__.py deleted file mode 100644 index 86d9ab38a..000000000 --- a/build_ddbc/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -build_ddbc - Build system for mssql-python native extensions. - -This package provides: -1. A CLI tool: `python -m build_ddbc` -2. A PEP 517 build backend that auto-compiles ddbc_bindings - -Usage: - python -m build_ddbc # Compile ddbc_bindings only - python -m build_ddbc --arch arm64 # Specify architecture (Windows) - python -m build_ddbc --coverage # Enable coverage (Linux) - python -m build # Compile + create wheel (automatic) -""" - -from .compiler import compile_ddbc, get_platform_info - -__all__ = ["compile_ddbc", "get_platform_info"] -__version__ = "1.3.0" diff --git a/pyproject.toml b/pyproject.toml index 8da1437ce..d6187fbd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ # ============================================================================= [build-system] requires = ["setuptools>=64.0.0", "wheel", "pybind11"] -build-backend = "build_ddbc.build_backend" +build-backend = "build_backend.hooks" backend-path = ["."] # ============================================================================= @@ -48,6 +48,9 @@ Repository = "https://github.com/microsoft/mssql-python" Issues = "https://github.com/microsoft/mssql-python/issues" Changelog = "https://github.com/microsoft/mssql-python/blob/main/CHANGELOG.md" +[project.scripts] +build-ddbc = "build_backend.__main__:main" + [project.optional-dependencies] dev = [ "pytest", @@ -69,23 +72,8 @@ lint = [ "types-setuptools", ] all = [ - # Dev dependencies - "pytest", - "pytest-cov", - "coverage", - "unittest-xml-reporting", - "psutil", - "pybind11", - "setuptools", - "wheel", - "build", - # Lint dependencies - "black", - "autopep8", - "pylint", - "cpplint", - "mypy", - "types-setuptools", + "mssql-python[dev]", + "mssql-python[lint]", ] # ============================================================================= @@ -98,7 +86,7 @@ include-package-data = true [tool.setuptools.packages.find] where = ["."] include = ["mssql_python*"] -exclude = ["build_ddbc*", "tests*", "benchmarks*"] +exclude = ["build_backend*", "tests*", "benchmarks*"] [tool.setuptools.package-data] mssql_python = [ diff --git a/setup.py b/setup.py index d19b03066..e7e0c0138 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,14 @@ Setup script for mssql-python. This script handles platform-specific wheel building with correct platform tags. -The native extension compilation is handled by the build_ddbc package. +The native extension compilation is handled by the build_backend package. Note: This file is still needed for: 1. Platform-specific package discovery (libs/windows, libs/linux, libs/macos) 2. Custom wheel platform tags (BinaryDistribution, CustomBdistWheel) For building: - python -m build_ddbc # Compile ddbc_bindings only + python -m build_backend # Compile ddbc_bindings only python -m build # Compile + create wheel (recommended) pip install -e . # Editable install with auto-compile """ @@ -20,7 +20,7 @@ from setuptools.dist import Distribution from wheel.bdist_wheel import bdist_wheel -from mssql_python.platform_utils import get_platform_info +from build_backend.platform_utils import get_platform_info # ============================================================================= # Platform-Specific Package Discovery diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index dd0dcd752..80fdb068c 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -748,11 +748,11 @@ def separator_reader_worker(): class TestPlatformUtils: - """Tests for mssql_python.platform_utils module.""" + """Tests for build_backend.platform_utils module.""" def test_get_platform_info_returns_tuple(self): """Test that get_platform_info returns a tuple of two strings.""" - from mssql_python.platform_utils import get_platform_info + from build_backend.platform_utils import get_platform_info result = get_platform_info() assert isinstance(result, tuple) @@ -762,7 +762,7 @@ def test_get_platform_info_returns_tuple(self): def test_get_platform_info_current_platform(self): """Test get_platform_info on current platform returns valid values.""" - from mssql_python.platform_utils import get_platform_info + from build_backend.platform_utils import get_platform_info import sys arch, platform_tag = get_platform_info() @@ -781,7 +781,7 @@ def test_get_platform_info_current_platform(self): def test_windows_x64_detection(self): """Test Windows x64 platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "win32"): with patch.object(platform_utils.os.environ, "get", return_value="x64"): @@ -792,7 +792,7 @@ def test_windows_x64_detection(self): def test_windows_x86_detection(self): """Test Windows x86 platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "win32"): with patch.object(platform_utils.os.environ, "get", return_value="x86"): @@ -803,7 +803,7 @@ def test_windows_x86_detection(self): def test_windows_arm64_detection(self): """Test Windows ARM64 platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "win32"): with patch.object(platform_utils.os.environ, "get", return_value="arm64"): @@ -814,7 +814,7 @@ def test_windows_arm64_detection(self): def test_macos_detection(self): """Test macOS platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "darwin"): arch, tag = platform_utils.get_platform_info() @@ -825,131 +825,133 @@ def test_macos_detection(self): def test_linux_x86_64_glibc_detection(self): """Test Linux x86_64 glibc platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): - with patch.object(platform_utils.platform, "machine", return_value="x86_64"): - with patch.object( - platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "manylinux_2_28_x86_64" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "manylinux_2_28_x86_64" def test_linux_x86_64_musl_detection(self): """Test Linux x86_64 musl platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): - with patch.object(platform_utils.platform, "machine", return_value="x86_64"): - with patch.object( - platform_utils.platform, "libc_ver", return_value=("musl", "1.2") - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "musllinux_1_2_x86_64" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "musllinux_1_2_x86_64" def test_linux_aarch64_glibc_detection(self): """Test Linux aarch64 glibc platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="aarch64"): - with patch.object(platform_utils.platform, "machine", return_value="aarch64"): - with patch.object( - platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "aarch64" - assert tag == "manylinux_2_28_aarch64" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="aarch64"), + patch.object(platform_utils.platform, "machine", return_value="aarch64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "manylinux_2_28_aarch64" def test_linux_aarch64_musl_detection(self): """Test Linux aarch64 musl platform detection.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="aarch64"): - with patch.object(platform_utils.platform, "machine", return_value="aarch64"): - with patch.object( - platform_utils.platform, "libc_ver", return_value=("musl", "1.2") - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "aarch64" - assert tag == "musllinux_1_2_aarch64" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="aarch64"), + patch.object(platform_utils.platform, "machine", return_value="aarch64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "musllinux_1_2_aarch64" def test_linux_arm64_alias(self): """Test Linux arm64 is treated as aarch64.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="arm64"): - with patch.object(platform_utils.platform, "machine", return_value="arm64"): - with patch.object( - platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "aarch64" - assert tag == "manylinux_2_28_aarch64" + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="arm64"), + patch.object(platform_utils.platform, "machine", return_value="arm64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "manylinux_2_28_aarch64" def test_linux_empty_libc_with_musl_glob(self): """Test Linux with empty libc_ver falls back to glob for musl detection.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): - with patch.object(platform_utils.platform, "machine", return_value="x86_64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("", "")): - with patch.object( - platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"] - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "musllinux_1_2_x86_64" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("", "")), + patch.object(platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"]), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "musllinux_1_2_x86_64" def test_linux_empty_libc_no_musl_glob(self, capsys): """Test Linux with empty libc_ver and no musl glob defaults to glibc.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="x86_64"): - with patch.object(platform_utils.platform, "machine", return_value="x86_64"): - with patch.object(platform_utils.platform, "libc_ver", return_value=("", "")): - with patch.object(platform_utils.glob, "glob", return_value=[]): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "manylinux_2_28_x86_64" - # Check warning was printed - captured = capsys.readouterr() - assert "Warning" in captured.err or "warning" in captured.err.lower() + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("", "")), + patch.object(platform_utils.glob, "glob", return_value=[]), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "manylinux_2_28_x86_64" + # Check warning was printed + captured = capsys.readouterr() + assert "Warning" in captured.err or "warning" in captured.err.lower() def test_linux_unsupported_architecture(self): """Test Linux with unsupported architecture raises OSError.""" from unittest.mock import patch - from mssql_python import platform_utils - - with patch.object(platform_utils.sys, "platform", "linux"): - with patch.object(platform_utils.os.environ, "get", return_value="ppc64le"): - with patch.object(platform_utils.platform, "machine", return_value="ppc64le"): - with patch.object( - platform_utils.platform, "libc_ver", return_value=("glibc", "2.28") - ): - with pytest.raises(OSError) as exc_info: - platform_utils.get_platform_info() - assert "ppc64le" in str(exc_info.value) - assert "Unsupported architecture" in str(exc_info.value) + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="ppc64le"), + patch.object(platform_utils.platform, "machine", return_value="ppc64le"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + with pytest.raises(OSError) as exc_info: + platform_utils.get_platform_info() + assert "ppc64le" in str(exc_info.value) + assert "Unsupported architecture" in str(exc_info.value) def test_unsupported_platform(self): """Test unsupported platform raises OSError.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "freebsd"): with pytest.raises(OSError) as exc_info: @@ -960,7 +962,7 @@ def test_unsupported_platform(self): def test_windows_strips_quotes_from_arch(self): """Test Windows architecture strips surrounding quotes.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "win32"): with patch.object(platform_utils.os.environ, "get", return_value='"x64"'): @@ -971,7 +973,7 @@ def test_windows_strips_quotes_from_arch(self): def test_windows_win32_alias(self): """Test Windows win32 is treated as x86.""" from unittest.mock import patch - from mssql_python import platform_utils + from build_backend import platform_utils with patch.object(platform_utils.sys, "platform", "win32"): with patch.object(platform_utils.os.environ, "get", return_value="win32"): From 1e01c8114448f90ab79281198de0da2848f00d9d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 06:41:26 +0000 Subject: [PATCH 18/25] chore: remove [project.scripts] entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build-ddbc CLI script is unnecessary — users should invoke python -m build_backend directly. Removes the entry to avoid polluting the user's environment. --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d6187fbd8..5b212afca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,6 @@ Repository = "https://github.com/microsoft/mssql-python" Issues = "https://github.com/microsoft/mssql-python/issues" Changelog = "https://github.com/microsoft/mssql-python/blob/main/CHANGELOG.md" -[project.scripts] -build-ddbc = "build_backend.__main__:main" - [project.optional-dependencies] dev = [ "pytest", From 74ba9ff8ab1d12af3bcc9bc9420470ed428bbac4 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 06:44:59 +0000 Subject: [PATCH 19/25] fix: fail build on FileNotFoundError instead of silently continuing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both build_wheel and build_editable now treat FileNotFoundError the same as RuntimeError/OSError — the build fails immediately if compilation fails for any reason. No silent fallback to assumed pre-compiled binaries. --- build_backend/hooks.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/build_backend/hooks.py b/build_backend/hooks.py index c08594465..08e7dc46c 100644 --- a/build_backend/hooks.py +++ b/build_backend/hooks.py @@ -83,10 +83,7 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): try: compile_ddbc(arch=arch, coverage=coverage, verbose=True) print("[build_backend] Compilation successful!") - except FileNotFoundError: - # If build scripts don't exist, assume pre-compiled binaries - print("[build_backend] Build scripts not found, assuming pre-compiled binaries") - except (RuntimeError, OSError) as e: + except (FileNotFoundError, RuntimeError, OSError) as e: print(f"[build_backend] Compilation failed: {e}") raise else: @@ -142,9 +139,7 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non try: compile_ddbc(arch=arch, coverage=coverage, verbose=True) print("[build_backend] Compilation successful!") - except FileNotFoundError: - print("[build_backend] Build scripts not found, assuming pre-compiled binaries") - except (RuntimeError, OSError) as e: + except (FileNotFoundError, RuntimeError, OSError) as e: print(f"[build_backend] Compilation failed: {e}") raise else: From f3c6ce11d52d83fb8765a09b76690a8d419d2bb6 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 06:54:48 +0000 Subject: [PATCH 20/25] test: add test_017_build_backend.py with 100% coverage - Move TestPlatformUtils from test_001_globals.py to new file - Add tests for hooks.py: _is_truthy, build_wheel, build_editable, build_sdist, get_requires_for_build_*, ImportError fallback - Add tests for compiler.py: find_pybind_dir, compile_ddbc, _run_unix_build, _run_windows_build - Add tests for __main__.py: CLI success, errors, --quiet, --arch, --coverage flags - 60 tests, 100% coverage of all build_backend modules - Add .coverage to .gitignore --- .gitignore | 1 + tests/test_001_globals.py | 238 ---------- tests/test_017_build_backend.py | 755 ++++++++++++++++++++++++++++++++ 3 files changed, 756 insertions(+), 238 deletions(-) create mode 100644 tests/test_017_build_backend.py diff --git a/.gitignore b/.gitignore index 3069e19d4..285eff3ae 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ build/ # learning files learnings/ +.coverage diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 80fdb068c..0d9bd4bb6 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -742,241 +742,3 @@ def separator_reader_worker(): setDecimalSeparator(original_separator) -# ============================================================================= -# Platform Utils Tests -# ============================================================================= - - -class TestPlatformUtils: - """Tests for build_backend.platform_utils module.""" - - def test_get_platform_info_returns_tuple(self): - """Test that get_platform_info returns a tuple of two strings.""" - from build_backend.platform_utils import get_platform_info - - result = get_platform_info() - assert isinstance(result, tuple) - assert len(result) == 2 - assert isinstance(result[0], str) # architecture - assert isinstance(result[1], str) # platform_tag - - def test_get_platform_info_current_platform(self): - """Test get_platform_info on current platform returns valid values.""" - from build_backend.platform_utils import get_platform_info - import sys - - arch, platform_tag = get_platform_info() - - # Architecture should be non-empty - assert arch - - # Platform tag should match current platform - if sys.platform.startswith("win"): - assert "win" in platform_tag - elif sys.platform.startswith("darwin"): - assert "macos" in platform_tag - elif sys.platform.startswith("linux"): - assert "linux" in platform_tag - - def test_windows_x64_detection(self): - """Test Windows x64 platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "win32"): - with patch.object(platform_utils.os.environ, "get", return_value="x64"): - arch, tag = platform_utils.get_platform_info() - assert arch == "x64" - assert tag == "win_amd64" - - def test_windows_x86_detection(self): - """Test Windows x86 platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "win32"): - with patch.object(platform_utils.os.environ, "get", return_value="x86"): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86" - assert tag == "win32" - - def test_windows_arm64_detection(self): - """Test Windows ARM64 platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "win32"): - with patch.object(platform_utils.os.environ, "get", return_value="arm64"): - arch, tag = platform_utils.get_platform_info() - assert arch == "arm64" - assert tag == "win_arm64" - - def test_macos_detection(self): - """Test macOS platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "darwin"): - arch, tag = platform_utils.get_platform_info() - assert arch == "universal2" - assert "macosx" in tag - assert "universal2" in tag - - def test_linux_x86_64_glibc_detection(self): - """Test Linux x86_64 glibc platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="x86_64"), - patch.object(platform_utils.platform, "machine", return_value="x86_64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "manylinux_2_28_x86_64" - - def test_linux_x86_64_musl_detection(self): - """Test Linux x86_64 musl platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="x86_64"), - patch.object(platform_utils.platform, "machine", return_value="x86_64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "musllinux_1_2_x86_64" - - def test_linux_aarch64_glibc_detection(self): - """Test Linux aarch64 glibc platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="aarch64"), - patch.object(platform_utils.platform, "machine", return_value="aarch64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "aarch64" - assert tag == "manylinux_2_28_aarch64" - - def test_linux_aarch64_musl_detection(self): - """Test Linux aarch64 musl platform detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="aarch64"), - patch.object(platform_utils.platform, "machine", return_value="aarch64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "aarch64" - assert tag == "musllinux_1_2_aarch64" - - def test_linux_arm64_alias(self): - """Test Linux arm64 is treated as aarch64.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="arm64"), - patch.object(platform_utils.platform, "machine", return_value="arm64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "aarch64" - assert tag == "manylinux_2_28_aarch64" - - def test_linux_empty_libc_with_musl_glob(self): - """Test Linux with empty libc_ver falls back to glob for musl detection.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="x86_64"), - patch.object(platform_utils.platform, "machine", return_value="x86_64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("", "")), - patch.object(platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"]), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "musllinux_1_2_x86_64" - - def test_linux_empty_libc_no_musl_glob(self, capsys): - """Test Linux with empty libc_ver and no musl glob defaults to glibc.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="x86_64"), - patch.object(platform_utils.platform, "machine", return_value="x86_64"), - patch.object(platform_utils.platform, "libc_ver", return_value=("", "")), - patch.object(platform_utils.glob, "glob", return_value=[]), - ): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86_64" - assert tag == "manylinux_2_28_x86_64" - # Check warning was printed - captured = capsys.readouterr() - assert "Warning" in captured.err or "warning" in captured.err.lower() - - def test_linux_unsupported_architecture(self): - """Test Linux with unsupported architecture raises OSError.""" - from unittest.mock import patch - from build_backend import platform_utils - - with ( - patch.object(platform_utils.sys, "platform", "linux"), - patch.object(platform_utils.os.environ, "get", return_value="ppc64le"), - patch.object(platform_utils.platform, "machine", return_value="ppc64le"), - patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), - ): - with pytest.raises(OSError) as exc_info: - platform_utils.get_platform_info() - assert "ppc64le" in str(exc_info.value) - assert "Unsupported architecture" in str(exc_info.value) - - def test_unsupported_platform(self): - """Test unsupported platform raises OSError.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "freebsd"): - with pytest.raises(OSError) as exc_info: - platform_utils.get_platform_info() - assert "freebsd" in str(exc_info.value) - assert "Unsupported platform" in str(exc_info.value) - - def test_windows_strips_quotes_from_arch(self): - """Test Windows architecture strips surrounding quotes.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "win32"): - with patch.object(platform_utils.os.environ, "get", return_value='"x64"'): - arch, tag = platform_utils.get_platform_info() - assert arch == "x64" - assert tag == "win_amd64" - - def test_windows_win32_alias(self): - """Test Windows win32 is treated as x86.""" - from unittest.mock import patch - from build_backend import platform_utils - - with patch.object(platform_utils.sys, "platform", "win32"): - with patch.object(platform_utils.os.environ, "get", return_value="win32"): - arch, tag = platform_utils.get_platform_info() - assert arch == "x86" - assert tag == "win32" diff --git a/tests/test_017_build_backend.py b/tests/test_017_build_backend.py new file mode 100644 index 000000000..03f292b6c --- /dev/null +++ b/tests/test_017_build_backend.py @@ -0,0 +1,755 @@ +""" +Tests for the build_backend package. + +Covers: +- platform_utils: platform detection logic +- hooks: PEP 517/660 build hooks (_is_truthy, build_wheel, build_editable, etc.) +- compiler: find_pybind_dir, compile_ddbc, _run_windows_build, _run_unix_build +- __main__: CLI entry point +""" + +import pytest +from unittest.mock import patch, MagicMock +from pathlib import Path + + +# ============================================================================= +# platform_utils tests +# ============================================================================= + + +class TestPlatformUtils: + """Tests for build_backend.platform_utils module.""" + + def test_get_platform_info_returns_tuple(self): + """Test that get_platform_info returns a tuple of two strings.""" + from build_backend.platform_utils import get_platform_info + + result = get_platform_info() + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], str) # architecture + assert isinstance(result[1], str) # platform_tag + + def test_get_platform_info_current_platform(self): + """Test get_platform_info on current platform returns valid values.""" + from build_backend.platform_utils import get_platform_info + import sys + + arch, platform_tag = get_platform_info() + + # Architecture should be non-empty + assert arch + + # Platform tag should match current platform + if sys.platform.startswith("win"): + assert "win" in platform_tag + elif sys.platform.startswith("darwin"): + assert "macos" in platform_tag + elif sys.platform.startswith("linux"): + assert "linux" in platform_tag + + def test_windows_x64_detection(self): + """Test Windows x64 platform detection.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="x64"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x64" + assert tag == "win_amd64" + + def test_windows_x86_detection(self): + """Test Windows x86 platform detection.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="x86"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86" + assert tag == "win32" + + def test_windows_arm64_detection(self): + """Test Windows ARM64 platform detection.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="arm64"): + arch, tag = platform_utils.get_platform_info() + assert arch == "arm64" + assert tag == "win_arm64" + + def test_macos_detection(self): + """Test macOS platform detection.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "darwin"): + arch, tag = platform_utils.get_platform_info() + assert arch == "universal2" + assert "macosx" in tag + assert "universal2" in tag + + def test_linux_x86_64_glibc_detection(self): + """Test Linux x86_64 glibc platform detection.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "manylinux_2_28_x86_64" + + def test_linux_x86_64_musl_detection(self): + """Test Linux x86_64 musl platform detection.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "musllinux_1_2_x86_64" + + def test_linux_aarch64_glibc_detection(self): + """Test Linux aarch64 glibc platform detection.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="aarch64"), + patch.object(platform_utils.platform, "machine", return_value="aarch64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "manylinux_2_28_aarch64" + + def test_linux_aarch64_musl_detection(self): + """Test Linux aarch64 musl platform detection.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="aarch64"), + patch.object(platform_utils.platform, "machine", return_value="aarch64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("musl", "1.2")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "musllinux_1_2_aarch64" + + def test_linux_arm64_alias(self): + """Test Linux arm64 is treated as aarch64.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="arm64"), + patch.object(platform_utils.platform, "machine", return_value="arm64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "aarch64" + assert tag == "manylinux_2_28_aarch64" + + def test_linux_empty_libc_with_musl_glob(self): + """Test Linux with empty libc_ver falls back to glob for musl detection.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("", "")), + patch.object(platform_utils.glob, "glob", return_value=["/lib/ld-musl-x86_64.so.1"]), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "musllinux_1_2_x86_64" + + def test_linux_empty_libc_no_musl_glob(self, capsys): + """Test Linux with empty libc_ver and no musl glob defaults to glibc.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="x86_64"), + patch.object(platform_utils.platform, "machine", return_value="x86_64"), + patch.object(platform_utils.platform, "libc_ver", return_value=("", "")), + patch.object(platform_utils.glob, "glob", return_value=[]), + ): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86_64" + assert tag == "manylinux_2_28_x86_64" + # Check warning was printed + captured = capsys.readouterr() + assert "Warning" in captured.err or "warning" in captured.err.lower() + + def test_linux_unsupported_architecture(self): + """Test Linux with unsupported architecture raises OSError.""" + from build_backend import platform_utils + + with ( + patch.object(platform_utils.sys, "platform", "linux"), + patch.object(platform_utils.os.environ, "get", return_value="ppc64le"), + patch.object(platform_utils.platform, "machine", return_value="ppc64le"), + patch.object(platform_utils.platform, "libc_ver", return_value=("glibc", "2.28")), + ): + with pytest.raises(OSError) as exc_info: + platform_utils.get_platform_info() + assert "ppc64le" in str(exc_info.value) + assert "Unsupported architecture" in str(exc_info.value) + + def test_unsupported_platform(self): + """Test unsupported platform raises OSError.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "freebsd"): + with pytest.raises(OSError) as exc_info: + platform_utils.get_platform_info() + assert "freebsd" in str(exc_info.value) + assert "Unsupported platform" in str(exc_info.value) + + def test_windows_strips_quotes_from_arch(self): + """Test Windows architecture strips surrounding quotes.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value='"x64"'): + arch, tag = platform_utils.get_platform_info() + assert arch == "x64" + assert tag == "win_amd64" + + def test_windows_win32_alias(self): + """Test Windows win32 is treated as x86.""" + from build_backend import platform_utils + + with patch.object(platform_utils.sys, "platform", "win32"): + with patch.object(platform_utils.os.environ, "get", return_value="win32"): + arch, tag = platform_utils.get_platform_info() + assert arch == "x86" + assert tag == "win32" + + +# ============================================================================= +# _is_truthy tests +# ============================================================================= + + +class TestIsTruthy: + """Tests for the _is_truthy config_settings helper.""" + + def test_bool_true(self): + from build_backend.hooks import _is_truthy + assert _is_truthy(True) is True + + def test_bool_false(self): + from build_backend.hooks import _is_truthy + assert _is_truthy(False) is False + + def test_string_true_variants(self): + from build_backend.hooks import _is_truthy + for val in ("true", "True", "TRUE", "1", "yes", "Yes", "YES"): + assert _is_truthy(val) is True, f"Expected True for {val!r}" + + def test_string_false_variants(self): + from build_backend.hooks import _is_truthy + for val in ("false", "False", "0", "no", "No", "", "anything"): + assert _is_truthy(val) is False, f"Expected False for {val!r}" + + def test_non_string_non_bool(self): + from build_backend.hooks import _is_truthy + assert _is_truthy(1) is True + assert _is_truthy(0) is False + assert _is_truthy([]) is False + assert _is_truthy([1]) is True + + +# ============================================================================= +# PEP 517 hooks tests +# ============================================================================= + + +class TestBuildWheel: + """Tests for build_wheel hook.""" + + @patch("build_backend.hooks._setuptools_build_wheel", return_value="fake.whl") + @patch("build_backend.hooks.compile_ddbc") + def test_build_wheel_compiles_and_delegates(self, mock_compile, mock_st_wheel): + from build_backend.hooks import build_wheel + + result = build_wheel("/tmp/out") + mock_compile.assert_called_once_with(arch=None, coverage=False, verbose=True) + mock_st_wheel.assert_called_once_with("/tmp/out", None, None) + assert result == "fake.whl" + + @patch("build_backend.hooks._setuptools_build_wheel", return_value="fake.whl") + @patch("build_backend.hooks.compile_ddbc") + def test_build_wheel_skip_compile(self, mock_compile, mock_st_wheel): + from build_backend.hooks import build_wheel + + result = build_wheel("/tmp/out", config_settings={"--skip-ddbc-compile": "true"}) + mock_compile.assert_not_called() + assert result == "fake.whl" + + @patch("build_backend.hooks._setuptools_build_wheel", return_value="fake.whl") + @patch("build_backend.hooks.compile_ddbc") + def test_build_wheel_passes_arch_and_coverage(self, mock_compile, mock_st_wheel): + from build_backend.hooks import build_wheel + + build_wheel("/tmp/out", config_settings={"--arch": "arm64", "--coverage": "true"}) + mock_compile.assert_called_once_with(arch="arm64", coverage=True, verbose=True) + + @patch("build_backend.hooks.compile_ddbc", side_effect=FileNotFoundError("build.sh not found")) + def test_build_wheel_file_not_found_raises(self, mock_compile): + from build_backend.hooks import build_wheel + + with pytest.raises(FileNotFoundError, match="build.sh not found"): + build_wheel("/tmp/out") + + @patch("build_backend.hooks.compile_ddbc", side_effect=RuntimeError("cmake failed")) + def test_build_wheel_runtime_error_raises(self, mock_compile): + from build_backend.hooks import build_wheel + + with pytest.raises(RuntimeError, match="cmake failed"): + build_wheel("/tmp/out") + + @patch("build_backend.hooks.compile_ddbc", side_effect=OSError("permission denied")) + def test_build_wheel_os_error_raises(self, mock_compile): + from build_backend.hooks import build_wheel + + with pytest.raises(OSError, match="permission denied"): + build_wheel("/tmp/out") + + +class TestBuildEditable: + """Tests for build_editable hook (PEP 660).""" + + @patch("build_backend.hooks.compile_ddbc") + def test_build_editable_compiles_and_delegates(self, mock_compile): + from build_backend.hooks import build_editable + + mock_st_editable = MagicMock(return_value="fake-editable.whl") + with patch( + "build_backend.hooks._setuptools_build_editable", + mock_st_editable, + create=True, + ): + # Patch the import inside build_editable + with patch( + "setuptools.build_meta.build_editable", + mock_st_editable, + ): + result = build_editable("/tmp/out") + + mock_compile.assert_called_once_with(arch=None, coverage=False, verbose=True) + assert result == "fake-editable.whl" + + @patch("build_backend.hooks.compile_ddbc") + def test_build_editable_skip_compile(self, mock_compile): + from build_backend.hooks import build_editable + + mock_st_editable = MagicMock(return_value="fake-editable.whl") + with patch("setuptools.build_meta.build_editable", mock_st_editable): + result = build_editable( + "/tmp/out", config_settings={"--skip-ddbc-compile": "true"} + ) + + mock_compile.assert_not_called() + assert result == "fake-editable.whl" + + @patch("build_backend.hooks.compile_ddbc") + def test_build_editable_passes_arch_and_coverage(self, mock_compile): + from build_backend.hooks import build_editable + + mock_st_editable = MagicMock(return_value="fake-editable.whl") + with patch("setuptools.build_meta.build_editable", mock_st_editable): + build_editable( + "/tmp/out", + config_settings={"--arch": "x64", "--coverage": "1"}, + ) + + mock_compile.assert_called_once_with(arch="x64", coverage=True, verbose=True) + + @patch("build_backend.hooks.compile_ddbc", side_effect=FileNotFoundError("build.sh missing")) + def test_build_editable_file_not_found_raises(self, mock_compile): + from build_backend.hooks import build_editable + + with pytest.raises(FileNotFoundError, match="build.sh missing"): + build_editable("/tmp/out") + + @patch("build_backend.hooks.compile_ddbc", side_effect=RuntimeError("compile error")) + def test_build_editable_runtime_error_raises(self, mock_compile): + from build_backend.hooks import build_editable + + with pytest.raises(RuntimeError, match="compile error"): + build_editable("/tmp/out") + + @patch("build_backend.hooks.compile_ddbc", side_effect=OSError("disk full")) + def test_build_editable_os_error_raises(self, mock_compile): + from build_backend.hooks import build_editable + + with pytest.raises(OSError, match="disk full"): + build_editable("/tmp/out") + + +class TestBuildSdist: + """Tests for build_sdist hook.""" + + @patch("build_backend.hooks._setuptools_build_sdist", return_value="fake.tar.gz") + def test_build_sdist_delegates(self, mock_st_sdist): + from build_backend.hooks import build_sdist + + result = build_sdist("/tmp/out") + mock_st_sdist.assert_called_once_with("/tmp/out", None) + assert result == "fake.tar.gz" + + +class TestRequiresHooks: + """Tests for get_requires_for_build_* hooks.""" + + @patch("build_backend.hooks._get_requires_for_build_wheel", return_value=["setuptools"]) + def test_get_requires_for_build_wheel(self, mock_req): + from build_backend.hooks import get_requires_for_build_wheel + + result = get_requires_for_build_wheel() + assert result == ["setuptools"] + + @patch("build_backend.hooks._get_requires_for_build_sdist", return_value=["setuptools"]) + def test_get_requires_for_build_sdist(self, mock_req): + from build_backend.hooks import get_requires_for_build_sdist + + result = get_requires_for_build_sdist() + assert result == ["setuptools"] + + @patch("build_backend.hooks._get_requires_for_build_wheel", return_value=["setuptools"]) + def test_get_requires_for_build_editable(self, mock_req): + from build_backend.hooks import get_requires_for_build_editable + + result = get_requires_for_build_editable() + assert result == ["setuptools"] + + @patch( + "build_backend.hooks._prepare_metadata_for_build_wheel", + return_value="metadata-dir", + ) + def test_prepare_metadata_for_build_wheel(self, mock_prep): + from build_backend.hooks import prepare_metadata_for_build_wheel + + result = prepare_metadata_for_build_wheel("/tmp/meta") + mock_prep.assert_called_once_with("/tmp/meta", None) + assert result == "metadata-dir" + + +# ============================================================================= +# CLI (__main__) tests +# ============================================================================= + + +class TestCLI: + """Tests for build_backend CLI entry point.""" + + @patch("build_backend.__main__.compile_ddbc") + @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_main_success(self, mock_plat, mock_compile): + from build_backend.__main__ import main + + with patch("sys.argv", ["build_backend"]): + assert main() == 0 + mock_compile.assert_called_once() + + @patch("build_backend.__main__.compile_ddbc", side_effect=FileNotFoundError("no build.sh")) + @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_main_file_not_found(self, mock_plat, mock_compile): + from build_backend.__main__ import main + + with patch("sys.argv", ["build_backend"]): + assert main() == 1 + + @patch("build_backend.__main__.compile_ddbc", side_effect=RuntimeError("failed")) + @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_main_runtime_error(self, mock_plat, mock_compile): + from build_backend.__main__ import main + + with patch("sys.argv", ["build_backend"]): + assert main() == 1 + + @patch("build_backend.__main__.compile_ddbc") + @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_main_quiet_suppresses_output(self, mock_plat, mock_compile, capsys): + from build_backend.__main__ import main + + with patch("sys.argv", ["build_backend", "--quiet"]): + assert main() == 0 + captured = capsys.readouterr() + assert "[build_backend]" not in captured.out + + @patch("build_backend.__main__.compile_ddbc") + @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_main_passes_arch(self, mock_plat, mock_compile): + from build_backend.__main__ import main + + with patch("sys.argv", ["build_backend", "--arch", "arm64", "--quiet"]): + main() + mock_compile.assert_called_once_with(arch="arm64", coverage=False, verbose=False) + + @patch("build_backend.__main__.compile_ddbc") + @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_main_passes_coverage(self, mock_plat, mock_compile): + from build_backend.__main__ import main + + with patch("sys.argv", ["build_backend", "--coverage", "--quiet"]): + main() + mock_compile.assert_called_once_with(arch=None, coverage=True, verbose=False) + + +# ============================================================================= +# build_editable ImportError branch (hooks.py L148-153) +# ============================================================================= + + +class TestBuildEditableImportError: + """Test the ImportError fallback when setuptools lacks build_editable.""" + + @patch("build_backend.hooks.compile_ddbc") + def test_build_editable_old_setuptools_raises(self, mock_compile): + """Older setuptools without build_editable raises RuntimeError.""" + from build_backend.hooks import build_editable + + with patch.dict("sys.modules", {"setuptools.build_meta": MagicMock(spec=[])}): + # Remove build_editable from the module so the import fails + with patch( + "builtins.__import__", + side_effect=_make_import_blocker("setuptools.build_meta", "build_editable"), + ): + with pytest.raises(RuntimeError, match="Editable installs are not supported"): + build_editable( + "/tmp/out", + config_settings={"--skip-ddbc-compile": "true"}, + ) + + +def _make_import_blocker(module_name, attr_name): + """Create an __import__ side_effect that blocks a specific from-import.""" + real_import = __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__ + + def blocker(name, *args, **kwargs): + if name == module_name: + mod = MagicMock(spec=[]) # spec=[] means no attributes + # Accessing attr_name will raise AttributeError, + # which 'from X import Y' turns into ImportError + return mod + return real_import(name, *args, **kwargs) + + return blocker + + +# ============================================================================= +# compiler tests +# ============================================================================= + + +class TestFindPybindDir: + """Tests for find_pybind_dir.""" + + def test_find_pybind_dir_relative_to_file(self, tmp_path): + """Test finding pybind dir relative to compiler.py.""" + from build_backend.compiler import find_pybind_dir + + # The real pybind dir should be found from the repo root + pybind_dir = find_pybind_dir() + assert pybind_dir.exists() + assert (pybind_dir / "build.sh").exists() or (pybind_dir / "build.bat").exists() + + def test_find_pybind_dir_not_found(self, tmp_path): + """Test FileNotFoundError when pybind dir doesn't exist.""" + from build_backend import compiler + + # Point both search paths to a tmp dir with no pybind content + fake_parent = tmp_path / "fake" + fake_parent.mkdir() + + with ( + patch.object(compiler, "__file__", str(fake_parent / "compiler.py")), + patch.object(compiler.Path, "cwd", return_value=tmp_path), + ): + with pytest.raises(FileNotFoundError, match="Could not find"): + compiler.find_pybind_dir() + + +class TestCompileDdbc: + """Tests for compile_ddbc.""" + + @patch("build_backend.compiler._run_unix_build", return_value=True) + @patch("build_backend.compiler.find_pybind_dir", return_value=Path("/fake/pybind")) + @patch("build_backend.compiler.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + def test_compile_ddbc_linux_default_arch(self, mock_plat, mock_find, mock_unix): + """compile_ddbc uses get_platform_info when arch is None.""" + from build_backend.compiler import compile_ddbc + + with patch("build_backend.compiler.sys") as mock_sys: + mock_sys.platform = "linux" + result = compile_ddbc(arch=None, coverage=False, verbose=True) + + mock_plat.assert_called_once() + mock_unix.assert_called_once_with(Path("/fake/pybind"), False, True) + assert result is True + + @patch("build_backend.compiler._run_unix_build", return_value=True) + @patch("build_backend.compiler.find_pybind_dir", return_value=Path("/fake/pybind")) + def test_compile_ddbc_linux_explicit_arch(self, mock_find, mock_unix): + """compile_ddbc skips get_platform_info when arch is provided.""" + from build_backend.compiler import compile_ddbc + + with patch("build_backend.compiler.sys") as mock_sys: + mock_sys.platform = "linux" + result = compile_ddbc(arch="aarch64", coverage=True, verbose=False) + + mock_unix.assert_called_once_with(Path("/fake/pybind"), True, False) + assert result is True + + @patch("build_backend.compiler._run_windows_build", return_value=True) + @patch("build_backend.compiler.find_pybind_dir", return_value=Path("/fake/pybind")) + def test_compile_ddbc_windows(self, mock_find, mock_win): + """compile_ddbc dispatches to _run_windows_build on Windows.""" + from build_backend.compiler import compile_ddbc + + with patch("build_backend.compiler.sys") as mock_sys: + mock_sys.platform = "win32" + result = compile_ddbc(arch="x64", coverage=False, verbose=True) + + mock_win.assert_called_once_with(Path("/fake/pybind"), "x64", True) + assert result is True + + +class TestRunUnixBuild: + """Tests for _run_unix_build.""" + + def test_unix_build_success(self, tmp_path): + """Successful build.sh execution.""" + from build_backend.compiler import _run_unix_build + + build_script = tmp_path / "build.sh" + build_script.write_text("#!/bin/bash\nexit 0\n") + build_script.chmod(0o755) + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + result = _run_unix_build(tmp_path, coverage=False, verbose=True) + + assert result is True + args = mock_run.call_args + assert "bash" in args[0][0][0] + + def test_unix_build_with_coverage(self, tmp_path): + """build.sh receives --coverage flag.""" + from build_backend.compiler import _run_unix_build + + build_script = tmp_path / "build.sh" + build_script.write_text("#!/bin/bash\nexit 0\n") + build_script.chmod(0o755) + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + _run_unix_build(tmp_path, coverage=True, verbose=True) + + cmd = mock_run.call_args[0][0] + assert "--coverage" in cmd + + def test_unix_build_failure_verbose(self, tmp_path): + """build.sh failure raises RuntimeError (verbose mode).""" + from build_backend.compiler import _run_unix_build + + build_script = tmp_path / "build.sh" + build_script.write_text("#!/bin/bash\nexit 1\n") + build_script.chmod(0o755) + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=1) + with pytest.raises(RuntimeError, match="build.sh failed"): + _run_unix_build(tmp_path, coverage=False, verbose=True) + + def test_unix_build_failure_quiet_prints_output(self, tmp_path): + """build.sh failure in quiet mode prints stdout/stderr.""" + from build_backend.compiler import _run_unix_build + + build_script = tmp_path / "build.sh" + build_script.write_text("#!/bin/bash\nexit 1\n") + build_script.chmod(0o755) + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=1, + stdout=b"some output", + stderr=b"some error", + ) + with pytest.raises(RuntimeError, match="build.sh failed"): + _run_unix_build(tmp_path, coverage=False, verbose=False) + + def test_unix_build_script_not_found(self, tmp_path): + """Missing build.sh raises FileNotFoundError.""" + from build_backend.compiler import _run_unix_build + + with pytest.raises(FileNotFoundError, match="Build script not found"): + _run_unix_build(tmp_path, coverage=False, verbose=True) + + +class TestRunWindowsBuild: + """Tests for _run_windows_build.""" + + def test_windows_build_success(self, tmp_path): + """Successful build.bat execution.""" + from build_backend.compiler import _run_windows_build + + build_script = tmp_path / "build.bat" + build_script.write_text("@echo off\nexit /b 0\n") + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=0) + result = _run_windows_build(tmp_path, arch="x64", verbose=True) + + assert result is True + cmd = mock_run.call_args[0][0] + assert "x64" in cmd + + def test_windows_build_failure_verbose(self, tmp_path): + """build.bat failure raises RuntimeError (verbose mode).""" + from build_backend.compiler import _run_windows_build + + build_script = tmp_path / "build.bat" + build_script.write_text("@echo off\nexit /b 1\n") + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock(returncode=1) + with pytest.raises(RuntimeError, match="build.bat failed"): + _run_windows_build(tmp_path, arch="x64", verbose=True) + + def test_windows_build_failure_quiet_prints_output(self, tmp_path): + """build.bat failure in quiet mode prints stdout/stderr.""" + from build_backend.compiler import _run_windows_build + + build_script = tmp_path / "build.bat" + build_script.write_text("@echo off\nexit /b 1\n") + + with patch("build_backend.compiler.subprocess.run") as mock_run: + mock_run.return_value = MagicMock( + returncode=1, + stdout=b"build output", + stderr=b"build error", + ) + with pytest.raises(RuntimeError, match="build.bat failed"): + _run_windows_build(tmp_path, arch="x86", verbose=False) + + def test_windows_build_script_not_found(self, tmp_path): + """Missing build.bat raises FileNotFoundError.""" + from build_backend.compiler import _run_windows_build + + with pytest.raises(FileNotFoundError, match="Build script not found"): + _run_windows_build(tmp_path, arch="x64", verbose=True) From e2549956201e69c57f3a12d7ab9a1095a6d4bc9d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 06:57:27 +0000 Subject: [PATCH 21/25] style: fix black formatting in test files --- tests/test_001_globals.py | 2 -- tests/test_017_build_backend.py | 38 +++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/tests/test_001_globals.py b/tests/test_001_globals.py index 0d9bd4bb6..7c004a136 100644 --- a/tests/test_001_globals.py +++ b/tests/test_001_globals.py @@ -740,5 +740,3 @@ def separator_reader_worker(): # Always make sure to clean up stop_event.set() setDecimalSeparator(original_separator) - - diff --git a/tests/test_017_build_backend.py b/tests/test_017_build_backend.py index 03f292b6c..aae49ba56 100644 --- a/tests/test_017_build_backend.py +++ b/tests/test_017_build_backend.py @@ -12,7 +12,6 @@ from unittest.mock import patch, MagicMock from pathlib import Path - # ============================================================================= # platform_utils tests # ============================================================================= @@ -248,24 +247,29 @@ class TestIsTruthy: def test_bool_true(self): from build_backend.hooks import _is_truthy + assert _is_truthy(True) is True def test_bool_false(self): from build_backend.hooks import _is_truthy + assert _is_truthy(False) is False def test_string_true_variants(self): from build_backend.hooks import _is_truthy + for val in ("true", "True", "TRUE", "1", "yes", "Yes", "YES"): assert _is_truthy(val) is True, f"Expected True for {val!r}" def test_string_false_variants(self): from build_backend.hooks import _is_truthy + for val in ("false", "False", "0", "no", "No", "", "anything"): assert _is_truthy(val) is False, f"Expected False for {val!r}" def test_non_string_non_bool(self): from build_backend.hooks import _is_truthy + assert _is_truthy(1) is True assert _is_truthy(0) is False assert _is_truthy([]) is False @@ -358,9 +362,7 @@ def test_build_editable_skip_compile(self, mock_compile): mock_st_editable = MagicMock(return_value="fake-editable.whl") with patch("setuptools.build_meta.build_editable", mock_st_editable): - result = build_editable( - "/tmp/out", config_settings={"--skip-ddbc-compile": "true"} - ) + result = build_editable("/tmp/out", config_settings={"--skip-ddbc-compile": "true"}) mock_compile.assert_not_called() assert result == "fake-editable.whl" @@ -457,7 +459,9 @@ class TestCLI: """Tests for build_backend CLI entry point.""" @patch("build_backend.__main__.compile_ddbc") - @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_main_success(self, mock_plat, mock_compile): from build_backend.__main__ import main @@ -466,7 +470,9 @@ def test_main_success(self, mock_plat, mock_compile): mock_compile.assert_called_once() @patch("build_backend.__main__.compile_ddbc", side_effect=FileNotFoundError("no build.sh")) - @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_main_file_not_found(self, mock_plat, mock_compile): from build_backend.__main__ import main @@ -474,7 +480,9 @@ def test_main_file_not_found(self, mock_plat, mock_compile): assert main() == 1 @patch("build_backend.__main__.compile_ddbc", side_effect=RuntimeError("failed")) - @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_main_runtime_error(self, mock_plat, mock_compile): from build_backend.__main__ import main @@ -482,7 +490,9 @@ def test_main_runtime_error(self, mock_plat, mock_compile): assert main() == 1 @patch("build_backend.__main__.compile_ddbc") - @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_main_quiet_suppresses_output(self, mock_plat, mock_compile, capsys): from build_backend.__main__ import main @@ -492,7 +502,9 @@ def test_main_quiet_suppresses_output(self, mock_plat, mock_compile, capsys): assert "[build_backend]" not in captured.out @patch("build_backend.__main__.compile_ddbc") - @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_main_passes_arch(self, mock_plat, mock_compile): from build_backend.__main__ import main @@ -501,7 +513,9 @@ def test_main_passes_arch(self, mock_plat, mock_compile): mock_compile.assert_called_once_with(arch="arm64", coverage=False, verbose=False) @patch("build_backend.__main__.compile_ddbc") - @patch("build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.__main__.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_main_passes_coverage(self, mock_plat, mock_compile): from build_backend.__main__ import main @@ -589,7 +603,9 @@ class TestCompileDdbc: @patch("build_backend.compiler._run_unix_build", return_value=True) @patch("build_backend.compiler.find_pybind_dir", return_value=Path("/fake/pybind")) - @patch("build_backend.compiler.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64")) + @patch( + "build_backend.compiler.get_platform_info", return_value=("x86_64", "manylinux_2_28_x86_64") + ) def test_compile_ddbc_linux_default_arch(self, mock_plat, mock_find, mock_unix): """compile_ddbc uses get_platform_info when arch is None.""" from build_backend.compiler import compile_ddbc From aaff54625c7fac731a794ba7a00ef8e3658bfffb Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 07:16:41 +0000 Subject: [PATCH 22/25] fix: add create=True to build_editable patches for older setuptools --- tests/test_017_build_backend.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_017_build_backend.py b/tests/test_017_build_backend.py index aae49ba56..1232e3ae1 100644 --- a/tests/test_017_build_backend.py +++ b/tests/test_017_build_backend.py @@ -341,17 +341,14 @@ def test_build_editable_compiles_and_delegates(self, mock_compile): from build_backend.hooks import build_editable mock_st_editable = MagicMock(return_value="fake-editable.whl") + # Patch the runtime import inside build_editable; create=True is + # needed because older setuptools (<64) lack build_editable. with patch( - "build_backend.hooks._setuptools_build_editable", + "setuptools.build_meta.build_editable", mock_st_editable, create=True, ): - # Patch the import inside build_editable - with patch( - "setuptools.build_meta.build_editable", - mock_st_editable, - ): - result = build_editable("/tmp/out") + result = build_editable("/tmp/out") mock_compile.assert_called_once_with(arch=None, coverage=False, verbose=True) assert result == "fake-editable.whl" @@ -361,7 +358,9 @@ def test_build_editable_skip_compile(self, mock_compile): from build_backend.hooks import build_editable mock_st_editable = MagicMock(return_value="fake-editable.whl") - with patch("setuptools.build_meta.build_editable", mock_st_editable): + with patch( + "setuptools.build_meta.build_editable", mock_st_editable, create=True + ): result = build_editable("/tmp/out", config_settings={"--skip-ddbc-compile": "true"}) mock_compile.assert_not_called() @@ -372,7 +371,9 @@ def test_build_editable_passes_arch_and_coverage(self, mock_compile): from build_backend.hooks import build_editable mock_st_editable = MagicMock(return_value="fake-editable.whl") - with patch("setuptools.build_meta.build_editable", mock_st_editable): + with patch( + "setuptools.build_meta.build_editable", mock_st_editable, create=True + ): build_editable( "/tmp/out", config_settings={"--arch": "x64", "--coverage": "1"}, From d1b96011039fac4c2c8d0d81cbab8d68add198ae Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 07:19:44 +0000 Subject: [PATCH 23/25] fix: restore missing stress_threading and slow markers from pytest.ini --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5b212afca..f49925a39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,8 +113,12 @@ python_classes = ["Test*"] python_functions = ["test_*"] markers = [ "stress: marks tests as stress tests (long-running, resource-intensive)", + "stress_threading: marks tests as multi-threaded stress tests (concurrent execution)", + "slow: marks tests as slow-running (may take several minutes)", ] -addopts = "-m 'not stress'" +# Default: skips stress tests. To run stress: pytest -m stress +# To run ALL tests including stress: pytest -v -m "" +addopts = "-m 'not stress and not stress_threading'" # ============================================================================= # Code Formatting - Black From 9afe5d54abf0bbbad79a4312195f2f37f55ce9d6 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 07:21:35 +0000 Subject: [PATCH 24/25] fix: keep only stress marker (stress_threading/slow are from unmerged branch) --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f49925a39..1ad0433a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,12 +113,10 @@ python_classes = ["Test*"] python_functions = ["test_*"] markers = [ "stress: marks tests as stress tests (long-running, resource-intensive)", - "stress_threading: marks tests as multi-threaded stress tests (concurrent execution)", - "slow: marks tests as slow-running (may take several minutes)", ] # Default: skips stress tests. To run stress: pytest -m stress # To run ALL tests including stress: pytest -v -m "" -addopts = "-m 'not stress and not stress_threading'" +addopts = "-m 'not stress'" # ============================================================================= # Code Formatting - Black From 66c7808a7df5aba9c7cfac2d7444eede77b54963 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 16 Feb 2026 07:23:22 +0000 Subject: [PATCH 25/25] style: fix black formatting in build_editable tests --- tests/test_017_build_backend.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_017_build_backend.py b/tests/test_017_build_backend.py index 1232e3ae1..4b079581d 100644 --- a/tests/test_017_build_backend.py +++ b/tests/test_017_build_backend.py @@ -358,9 +358,7 @@ def test_build_editable_skip_compile(self, mock_compile): from build_backend.hooks import build_editable mock_st_editable = MagicMock(return_value="fake-editable.whl") - with patch( - "setuptools.build_meta.build_editable", mock_st_editable, create=True - ): + with patch("setuptools.build_meta.build_editable", mock_st_editable, create=True): result = build_editable("/tmp/out", config_settings={"--skip-ddbc-compile": "true"}) mock_compile.assert_not_called() @@ -371,9 +369,7 @@ def test_build_editable_passes_arch_and_coverage(self, mock_compile): from build_backend.hooks import build_editable mock_st_editable = MagicMock(return_value="fake-editable.whl") - with patch( - "setuptools.build_meta.build_editable", mock_st_editable, create=True - ): + with patch("setuptools.build_meta.build_editable", mock_st_editable, create=True): build_editable( "/tmp/out", config_settings={"--arch": "x64", "--coverage": "1"},