diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 15ecfd8b..29e211ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,7 +78,7 @@ jobs: steps: - - name: Checkout Pyqasm + - name: Checkout PyQASM uses: actions/checkout@v4 - name: Install Python @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout Pyqasm + - name: Checkout PyQASM uses: actions/checkout@v4 - name: Install Python @@ -138,7 +138,7 @@ jobs: environment: release steps: - - name: Checkout Pyqasm + - name: Checkout PyQASM uses: actions/checkout@v4 - name: Download artifacts diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 00000000..21020d09 --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,162 @@ +name: Publish to TestPyPI + +on: + workflow_dispatch: + +jobs: + build_wheels: + runs-on: ${{ matrix.os }} + strategy: + + # Ensure that a wheel builder finishes even if another fails + fail-fast: false + + matrix: + include: + # Windows 64 bit + - os: windows-latest + python: 310 + platform_id: win_amd64 + - os: windows-latest + python: 311 + platform_id: win_amd64 + - os: windows-latest + python: 312 + platform_id: win_amd64 + - os: windows-latest + python: 313 + platform_id: win_amd64 + + # Linux 64 bit manylinux2014 + - os: ubuntu-latest + python: 310 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2014 + - os: ubuntu-latest + python: 311 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2014 + - os: ubuntu-latest + python: 312 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2014 + - os: ubuntu-latest + python: 313 + platform_id: manylinux_x86_64 + manylinux_image: manylinux2014 + + # MacOS x86_64 + - os: macos-13 + python: 310 + platform_id: macosx_x86_64 + - os: macos-13 + python: 311 + platform_id: macosx_x86_64 + - os: macos-13 + python: 312 + platform_id: macosx_x86_64 + - os: macos-13 + python: 313 + platform_id: macosx_x86_64 + + # MacOS arm64 + - os: macos-14 + python: 310 + platform_id: macosx_arm64 + - os: macos-14 + python: 311 + platform_id: macosx_arm64 + - os: macos-14 + python: 312 + platform_id: macosx_arm64 + - os: macos-14 + python: 313 + platform_id: macosx_arm64 + + + steps: + - name: Checkout PyQASM + uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Write version file + run: python bin/write_version_file.py + + - name: Build and Test Wheels + env: + CIBW_BUILD: "cp${{ matrix.python }}-${{ matrix.platform_id }}" + CIBW_ARCHS_LINUX: x86_64 + CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_image }} + CIBW_BEFORE_BUILD: bash {project}/bin/cibw/pre_build.sh {project} + CIBW_TEST_EXTRAS: "test,cli" + CIBW_TEST_COMMAND: bash {project}/bin/cibw/test_wheel.sh {project} + CIBW_BUILD_VERBOSITY: 1 + + run: bash bin/cibw/build_wheels.sh + + - name: Upload built wheels + uses: actions/upload-artifact@v4 + with: + name: package-wheel-cp${{ matrix.python }}-${{ matrix.os }} + path: dist/*.whl + + build_sdist: + name: Source distribution + runs-on: ubuntu-latest + + steps: + - name: Checkout PyQASM + uses: actions/checkout@v4 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Write version file + run: python bin/write_version_file.py + + - name: Build source distribution + run: bash bin/build_sdist.sh + + - name: Test source distribution + env: + RELEASE_BUILD: "true" + run: bash bin/test_sdist.sh + + - name: Store artifacts + uses: actions/upload-artifact@v4 + with: + name: package-sdist + path: dist/*.tar.gz + + pypi-publish: + name: Build dist & upload to PyPI + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: release + + steps: + - name: Checkout PyQASM + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + # pattern should match the upload artifact naming convention + # of the previous jobs + pattern: package-* + path: dist + # put all files in single directory + merge-multiple: true + + - name: Publish package to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} \ No newline at end of file diff --git a/bin/write_version_file.py b/bin/write_version_file.py new file mode 100644 index 00000000..b0446168 --- /dev/null +++ b/bin/write_version_file.py @@ -0,0 +1,94 @@ +# Copyright (C) 2025 qBraid +# +# This file is part of PyQASM +# +# PyQASM is free software released under the GNU General Public License v3 +# or later. You can redistribute and/or modify it under the terms of the GPL v3. +# See the LICENSE file in the project root or . +# +# THERE IS NO WARRANTY for PyQASM, as per Section 15 of the GPL v3. + +""" +This script is used to write the version to the version file. +It is used to ensure that the version file is always up to date +with the version in pyproject.toml. +""" + +import pathlib +import sys +import tomllib + + +def get_version_from_pyproject(pyproject_path: pathlib.Path) -> str: + """Extract the version from pyproject.toml file.""" + try: + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + return data["project"]["version"] + except FileNotFoundError: + print(f"Error: pyproject.toml not found at {pyproject_path}") + sys.exit(1) + except KeyError: + print("Error: Version not found in pyproject.toml") + sys.exit(1) + + +def parse_version_tuple(version_string: str) -> tuple[int | str, ...]: + """Parse a semantic version string into a tuple.""" + parts = version_string.split(".") + + version_tuple = [] + for part in parts[:3]: + try: + version_tuple.append(int(part)) + except ValueError: + version_tuple.append(part) + + if len(parts) > 3: + extra = ".".join(parts[3:]) + if "-" in extra: + pre_release, build = extra.split("-", 1) + version_tuple.extend(pre_release.split(".")) + if "+" in build: + build = build.split("+", 1)[0] + version_tuple.extend(build.split(".")) + elif "+" in extra: + pre_release, build = extra.split("+", 1) + version_tuple.extend(pre_release.split(".")) + else: + version_tuple.extend(extra.split(".")) + + return tuple(version_tuple) + + +def write_version_file(version_file_path: pathlib.Path, version: str) -> None: + """Write the version to a file.""" + version_file_path.parent.mkdir(parents=True, exist_ok=True) + version_tuple = parse_version_tuple(version) + content = f"""# file generated during build +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + VERSION_TUPLE = tuple[int | str, ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '{version}' +__version_tuple__ = version_tuple = {version_tuple} +""" + version_file_path.write_text(content) + print(f"Version file written: {version_file_path}") + + +if __name__ == "__main__": + root = pathlib.Path(__file__).parent.parent.resolve() + pyproject_toml = root / "pyproject.toml" + version_file = "src" / "pyqasm" / "_version.py" + + version_str = get_version_from_pyproject(pyproject_toml) + write_version_file(version_file, version_str) diff --git a/pyproject.toml b/pyproject.toml index a5a840c9..76d0db31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=74.1", "setuptools_scm", "Cython>=3.0", "numpy"] +requires = ["setuptools>=74.1", "Cython>=3.0", "numpy"] build-backend = "setuptools.build_meta" [project] @@ -12,7 +12,7 @@ requires-python = ">=3.10" keywords = ["quantum", "openqasm", "symantic-analyzer", "compiler", "qbraid"] license = {text = "GNU General Public License v3.0"} classifiers = [ - "Development Status :: 2 - Pre-Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Natural Language :: English", @@ -41,13 +41,9 @@ dependencies = ["numpy", "openqasm3[parser]>=1.0.0,<2.0.0"] [project.optional-dependencies] cli = ["typer>=0.12.1", "rich>=10.11.0", "typing-extensions"] test = ["pytest", "pytest-cov"] -lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.8.5"] +lint = ["black", "isort>=6.0.0", "pylint", "mypy", "qbraid-cli>=0.10.2"] docs = ["sphinx>=7.3.7,<8.2.0", "sphinx-autodoc-typehints>=1.24,<3.1", "sphinx-rtd-theme>=2.0.0,<4.0.0", "docutils<0.22", "sphinx-copybutton"] -[tool.setuptools_scm] -version_scheme = "no-guess-dev" -write_to = "src/pyqasm/_version.py" - [tool.setuptools.package-data] pyqasm = ["py.typed", "*.pyx"] diff --git a/src/pyqasm/__init__.py b/src/pyqasm/__init__.py index 990e65b6..72cc5e4a 100644 --- a/src/pyqasm/__init__.py +++ b/src/pyqasm/__init__.py @@ -46,13 +46,14 @@ """ import warnings +from importlib.metadata import version try: # Injected in _version.py during the build process. from ._version import __version__ # type: ignore -except ImportError: # pragma: no cover - warnings.warn("Importing 'pyqasm' outside a proper installation.") - __version__ = "dev" +except (ImportError, ModuleNotFoundError): # pragma: no cover + warnings.warn("Importing 'pyqasm' outside a proper installation.", UserWarning) + __version__ = version("pyqasm") from .entrypoint import dump, dumps, load, loads from .exceptions import PyQasmError, QasmParsingError, ValidationError diff --git a/src/pyqasm/cli/main.py b/src/pyqasm/cli/main.py index a4ea7c76..2dc9cb8c 100644 --- a/src/pyqasm/cli/main.py +++ b/src/pyqasm/cli/main.py @@ -36,7 +36,7 @@ def version_callback(value: bool): """Show the version and exit.""" if value: # pylint: disable-next=import-outside-toplevel - from pyqasm._version import __version__ # type: ignore + from pyqasm import __version__ # type: ignore typer.echo(f"pyqasm/{__version__}") raise typer.Exit(0) diff --git a/tests/cli/test_cli_commands.py b/tests/cli/test_cli_commands.py index bfe67a1f..0ca05ea1 100644 --- a/tests/cli/test_cli_commands.py +++ b/tests/cli/test_cli_commands.py @@ -16,11 +16,16 @@ import os import re import shutil +import warnings import pytest import typer from typer.testing import CliRunner +warnings.filterwarnings( + "ignore", "Importing 'pyqasm' outside a proper installation.", category=UserWarning +) + from pyqasm.cli.main import app from pyqasm.cli.validate import validate_qasm diff --git a/tox.ini b/tox.ini index be4f5d53..e98057c0 100644 --- a/tox.ini +++ b/tox.ini @@ -57,7 +57,7 @@ commands = [testenv:headers] envdir = .tox/linters skip_install = true -deps = qbraid-cli>=0.9.9 +deps = qbraid-cli>=0.10.2 commands = qbraid admin headers src tests bin examples --skip=src/pyqasm/_version.py --type=gpl -p PyQASM {posargs}