diff --git a/.git_archival.txt b/.git_archival.txt deleted file mode 100644 index 89efee1bb..000000000 --- a/.git_archival.txt +++ /dev/null @@ -1,7 +0,0 @@ -SPDX-FileCopyrightText: Copyright DB InfraGO AG -SPDX-License-Identifier: CC0-1.0 - -node: $Format:%H$ -node-date: $Format:%cI$ -describe-name: $Format:%(describe:tags=true)$ -ref-names: $Format:%D$ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9d7fea6b0..e49d66bf9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,7 +28,7 @@ updates: commit-message: prefix: build(python) - package-ecosystem: cargo - directory: / + directory: /rust schedule: interval: weekly day: wednesday diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 95651e604..9b8196754 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -11,17 +11,19 @@ on: tags: ["v*.*.*"] workflow_dispatch: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref_type == 'tag' && github.sha || '0' }} cancel-in-progress: true jobs: - pre-commit: # {{{1 + pre-commit: + # {{{1 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - with: - fetch-depth: 0 - uses: astral-sh/setup-uv@v7 with: enable-cache: true @@ -30,20 +32,17 @@ jobs: - name: Run Pre-Commit run: make lint - sdist: # {{{1 + sdist: + # {{{1 name: Build source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - name: Build sdist + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 with: - fetch-depth: 0 - - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "**/pyproject.toml" - python-version: "3.13" - - name: Package the sources - run: make sdist + command: sdist + args: --out dist - name: Upload artifacts uses: actions/upload-artifact@v7 with: @@ -51,121 +50,245 @@ jobs: path: dist/ if-no-files-found: error - build: # {{{1 - name: Build wheel for ${{matrix.os}} / ${{matrix.arch}}${{ matrix.manylinux && ' / ' || '' }}${{ matrix.manylinux || '' }} - runs-on: ${{matrix.os}} + build: + # {{{1 + name: Build wheels for ${{ matrix.platform.tag }} ${{ matrix.platform.target }} + runs-on: ${{ matrix.platform.runner }} strategy: fail-fast: false matrix: - include: + platform: # Linux manylinux {{{ - - os: ubuntu-latest - arch: x86_64 - manylinux: manylinux - - os: ubuntu-24.04-arm - arch: aarch64 - manylinux: manylinux + - runner: ubuntu-22.04 + target: x86_64 + tag: manylinux + manylinux: auto + unittests: true + - runner: ubuntu-22.04 + target: x86 + tag: manylinux + manylinux: auto + - runner: ubuntu-22.04 + target: aarch64 + tag: manylinux + manylinux: auto + - runner: ubuntu-22.04 + target: armv7 + tag: manylinux + manylinux: auto + - runner: ubuntu-22.04 + target: s390x + tag: manylinux + manylinux: auto + - runner: ubuntu-22.04 + target: ppc64le + tag: manylinux + manylinux: auto # }}} # Linux musllinux {{{ - - os: ubuntu-latest - arch: x86_64 - manylinux: musllinux - - os: ubuntu-24.04-arm - arch: aarch64 - manylinux: musllinux + - runner: ubuntu-22.04 + target: x86_64 + tag: musllinux + manylinux: musllinux_1_2 + - runner: ubuntu-22.04 + target: x86 + tag: musllinux + manylinux: musllinux_1_2 + - runner: ubuntu-22.04 + target: aarch64 + tag: musllinux + manylinux: musllinux_1_2 + - runner: ubuntu-22.04 + target: armv7 + tag: musllinux + manylinux: musllinux_1_2 # }}} # macOS {{{ - - os: macos-15-intel - arch: x86_64 - - os: macos-latest - arch: arm64 + - runner: macos-15-intel + target: x86_64 + tag: macos + - runner: macos-latest + target: aarch64 + tag: macos + unittests: true # }}} - # Windows {{{ - - os: windows-latest - arch: x86 - - os: windows-latest - arch: AMD64 - # }}} - env: - CIBW_ARCHS: ${{matrix.arch}} - CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH"' - CIBW_ENVIRONMENT_WINDOWS: 'PATH="$UserProfile\.cargo\bin;$PATH"' - MACOSX_DEPLOYMENT_TARGET: "11.0" steps: - uses: actions/checkout@v6 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: - fetch-depth: 0 - - name: Determine which Python versions to test against + python-version: "3.12" + - uses: astral-sh/setup-uv@v7 + if: matrix.platform.unittests + with: + python-version: "3.12" + cache-python: "true" + ignore-nothing-to-cache: "true" + - name: Build abi3 wheel + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist -i python3.12 + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: ${{ matrix.platform.manylinux }} + - name: Run unittests with Python 3.12 + if: matrix.platform.unittests + env: + UV_PYTHON: cp312 shell: bash + run: |- + echo "::group::Prepare test environment" + uv sync --group test --no-install-project + . .venv/bin/activate + uv pip install dist/*-"$UV_PYTHON"-*.whl + echo "::endgroup::" + + make test + - name: Build 3.13t wheel + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist -i python3.13t + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: ${{ matrix.platform.manylinux }} + - name: Build 3.14t wheel + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist -i python3.14t + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: ${{ matrix.platform.manylinux }} + - name: Run unittests with Python 3.14t + if: matrix.platform.unittests env: - ENV_TAG: ${{runner.os}}-${{matrix.arch}}-${{matrix.manylinux}} - MANYLINUX: ${{matrix.manylinux}} + UV_PYTHON: cp314t + shell: bash run: |- - if [[ "$ENV_TAG" == "Linux-x86_64-manylinux" || "$ENV_TAG" == "macOS-arm64-" ]]; then - echo "CIBW_BUILD=cp*-$MANYLINUX*" >> "$GITHUB_ENV" - echo "CIBW_SKIP=cp*t-*" >> "$GITHUB_ENV" + echo "::group::Prepare test environment" + uv sync --group test --no-install-project + . .venv/bin/activate + uv pip install dist/*-"$UV_PYTHON"-*.whl + echo "::endgroup::" + + make test + - name: Assert that exactly one abi3 wheel exists + shell: bash + run: |- + wheels=(dist/*-abi3-*.whl) + if [[ "${#wheels[@]}" -eq 1 ]] && [[ -e "${wheels[0]}" ]]; then + echo "OK" else - echo "CIBW_BUILD=cp311-$MANYLINUX* cp314-$MANYLINUX*" >> "$GITHUB_ENV" + echo "Expected exactly one abi3 wheel, found ${#wheels[@]}:" + printf ' - %s\n' "${wheels[@]}" + exit 1 fi - - name: Build wheels - uses: pypa/cibuildwheel@v4.1.0 - name: Upload artifacts uses: actions/upload-artifact@v7 with: - name: wheel-${{runner.os}}-${{matrix.arch}}${{ matrix.manylinux && '-' || '' }}${{ matrix.manylinux || '' }} - path: wheelhouse/ + name: wheels-${{ matrix.platform.tag }}-${{ matrix.platform.target }} + path: dist/ if-no-files-found: error - build-pure: # {{{1 - name: Build pure-Python wheel - runs-on: ubuntu-latest + build-windows: + # {{{1 + name: Build wheels for windows ${{ matrix.platform.target }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + platform: + - target: x64 + unittests: true + - target: x86 steps: - uses: actions/checkout@v6 + - name: Set up Python 3.12 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: - fetch-depth: 0 + python-version: "3.12" + architecture: ${{ matrix.platform.target }} + - name: Set up Python 3.13t + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.13t" + architecture: ${{ matrix.platform.target }} + - name: Set up Python 3.14t + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.14t" + architecture: ${{ matrix.platform.target }} - uses: astral-sh/setup-uv@v7 + if: matrix.platform.unittests with: - enable-cache: true - cache-dependency-glob: "**/pyproject.toml" - python-version: "3.13" - - name: Build the wheel + python-version: "3.12" + cache-python: "true" + ignore-nothing-to-cache: "true" + - name: Build abi3 wheel + uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist -i python3.12 + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Run unittests with Python 3.12 + if: matrix.platform.unittests + env: + UV_PYTHON: cp312 + shell: bash run: |- - SETUPTOOLS_SCM_PRETEND_VERSION="$( - uvx --with setuptools_scm python -c \ - "import setuptools_scm as scm; print(scm.get_version())" - )" - export SETUPTOOLS_SCM_PRETEND_VERSION - - uvx --with tomli_w python <> "$GITHUB_ENV" - - run: uv add --dev ./dist/*.whl pytest pytest-xdist && uv sync + - run: uv add --dev ./dist/*-abi3-*.whl pytest pytest-xdist && uv sync - run: uv run pytest -n auto diff --git a/Makefile b/Makefile index fb84469f9..7e93a6a00 100644 --- a/Makefile +++ b/Makefile @@ -19,8 +19,11 @@ help: #: Show this help .PHONY: dev dev: .venv #: Set up development environment .venv: pyproject.toml + find src -type f \( -name '*.so' -o -name '*.pyd' \) -delete || true uv sync --inexact + uv run --no-sync python -m maturin_import_hook site install touch -c .venv + if [[ -d .jj ]]; then touch .jj/.maturin_hook_ignore; fi .PHONY: install-hooks install-hooks: dev .venv #: Install pre-commit hooks @@ -28,13 +31,16 @@ install-hooks: dev .venv #: Install pre-commit hooks uv run --no-sync pre-commit install-hooks .PHONY: rebuild -rebuild: #: Rebuild native Rust module +rebuild: clean dev #: Rebuild native Rust module uv sync --inexact --reinstall-package capellambse .PHONY: clean -clean: docs-clean #: Clean all build artifacts +clean: #: Clean built library files + find src -type f \( -name '*.so' -o -name '*.pyd' \) -delete || true + +.PHONY: dist-clean +dist-clean: clean docs-clean #: Clean all build artifacts, tools and data find . -type d \( -name __pycache__ -o -name target \) -execdir rm -rf {} \; 2>/dev/null || true - find src \( -type f -name '*.so' -o -name '*.pyd' \) -delete || true rm -rf .*cache .coverage .venv build dist htmlcov src/*.egg-info # Testing {{{1 diff --git a/pyproject.toml b/pyproject.toml index 0371df681..a8651b663 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,23 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 [build-system] -requires = [ - "setuptools>=77", - "setuptools-rust", - "setuptools-scm[toml]>=3.4", - "wheel", -] -build-backend = "setuptools.build_meta" +requires = ["maturin>=1.9,<2.0"] +build-backend = "maturin" [project] -dynamic = ["version"] - name = "capellambse" +version = "0.7.12" description = "Provides access to Capella MBSE projects in Python" readme = "README.md" requires-python = ">=3.11, <3.15" license = "Apache-2.0 AND OFL-1.1" -license-files = ["LICENSES/*.txt"] authors = [{ name = "DB InfraGO AG" }] keywords = ["arcadia", "capella", "mbse", "model-based systems engineering"] classifiers = [ @@ -31,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Free Threading :: 2 - Beta", "Topic :: Other/Nonlisted Topic", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules", @@ -137,6 +131,8 @@ dev = [ "docformatter[tomli]==1.7.7", "jupyterlab==4.5.2", "mypy==1.19.1", + "maturin>=1.9.6", + "maturin-import-hook>=0.3.0", "pandas>=2.3.0", "pre-commit==4.5.1", "pylsp-mypy==0.7.0", @@ -221,6 +217,10 @@ py_limited_api = "cp311" wrap-descriptions = 72 wrap-summaries = 79 +[tool.maturin] +module-name = "capellambse._compiled" +include = ["capellambse/known_models/*.json"] + [tool.mypy] check_untyped_defs = true disallow_incomplete_defs = true @@ -368,23 +368,5 @@ extend-immutable-calls = ["capellambse.diagram.Vector2D"] convention = "numpy" ignore-decorators = ["typing.overload"] -[tool.setuptools] -platforms = ["any"] -zip-safe = false - -[tool.setuptools.package-data] -"*" = ["py.typed"] -"capellambse" = ["OpenSans-Regular.ttf"] - -[tool.setuptools.packages.find] -where = ["src"] - -[[tool.setuptools-rust.ext-modules]] -target = "capellambse._compiled" -optional = true - -[tool.setuptools_scm] -# This section must exist for setuptools_scm to work - [tool.uv] default-groups = ["dev", "test", "typecheck"] diff --git a/Cargo.toml b/rust/Cargo.toml similarity index 100% rename from Cargo.toml rename to rust/Cargo.toml diff --git a/src/exs.rs b/rust/src/exs.rs similarity index 100% rename from src/exs.rs rename to rust/src/exs.rs diff --git a/src/lib.rs b/rust/src/lib.rs similarity index 83% rename from src/lib.rs rename to rust/src/lib.rs index 2066f4ad2..45e1bf2d2 100644 --- a/src/lib.rs +++ b/rust/src/lib.rs @@ -5,7 +5,7 @@ use pyo3::prelude::*; mod exs; -#[pymodule(name = "_compiled")] +#[pymodule(name = "_compiled", gil_used = false)] fn setup_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(exs::serialize, m)?)?; diff --git a/scripts/release.py b/scripts/release.py index a6bad9819..68671e3df 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -29,6 +29,7 @@ MAILUSER = re.compile( r"^(?:\d+\+)?(?P[^@]+)@users\.noreply\.github\.com$" ) +RELEASE_COMMIT_MESSAGE = "chore: Release v{version}" logger = logging.getLogger(__name__) @@ -108,11 +109,17 @@ def _validate_version_tag( "based on the '--prev' tag and '--head' commit." ), ) +@click.option( + "--update-pyproject/--no-update-pyproject", + default=True, + help="Commit the new version number to pyproject.toml before tagging.", +) def _main( *, prev: awesomeversion.AwesomeVersion | None, head: str | None, version: awesomeversion.AwesomeVersion | None, + update_pyproject: bool, ) -> None: logging.basicConfig() @@ -143,6 +150,9 @@ def _main( version = _bump_version(prev, bump) logger.info("%s version bump from %s to %s", bump.name, prev, version) + if update_pyproject: + head = _update_pyproject(head, version, jj=jj) + changelog = _format_changelog(commit_log) _create_git_tag(head, version, changelog) _update_release_branch(head, version, jj=jj) @@ -362,6 +372,33 @@ def _copy_to_clipboard(text: str) -> None: return +def _update_pyproject( + head: str, + version: awesomeversion.AwesomeVersion, + *, + jj: bool, +) -> str: + msg = RELEASE_COMMIT_MESSAGE.format(version=version) + + if jj: + _exec("jj", "new", "-m", msg, head) + _exec("uv", "version", version) + _exec("jj", "sign", "-r", "::@ & ~signed() & ~immutable()") + return _exec("jj", "log", "--no-graph", "-r@", "-Tcommit_id") + + try: + _exec("git", "diff", "--quiet") + except subprocess.CalledProcessError: + raise SystemExit( + "Worktree is dirty, commit or stash all changes and try again" + ) from None + _exec("git", "switch", "-d", head) + _exec("uv", "version", version) + _exec("git", "add", "pyproject.toml") + _exec("git", "commit", "-m", msg) + return _exec("git", "rev-parse", "HEAD") + + def _exec(exe: str, /, *args: str, **kw: t.Any) -> str: """Execute a command and return its stdout as string.""" cmd = (exe, *args)