diff --git a/.github/workflows/test-python-bindings.yml b/.github/workflows/test-python-bindings.yml index 1fe7fc0d239..77b2e2443c7 100644 --- a/.github/workflows/test-python-bindings.yml +++ b/.github/workflows/test-python-bindings.yml @@ -13,6 +13,7 @@ on: paths: - 'bindings/python/src/**' - 'bindings/python/tests/**' + - 'bindings/python/deprecation/**' - 'bindings/python/examples/**' - 'bindings/python/scripts/**' - 'bindings/python/pyproject.toml' @@ -25,6 +26,7 @@ on: paths: - 'bindings/python/src/**' - 'bindings/python/tests/**' + - 'bindings/python/deprecation/**' - 'bindings/python/examples/**' - 'bindings/python/scripts/**' - 'bindings/python/pyproject.toml' diff --git a/.github/workflows/test-python-examples.yml b/.github/workflows/test-python-examples.yml index 9e6b7d83801..ed158051c89 100644 --- a/.github/workflows/test-python-examples.yml +++ b/.github/workflows/test-python-examples.yml @@ -12,6 +12,7 @@ on: paths: - 'bindings/python/src/**' - 'bindings/python/tests/**' + - 'bindings/python/deprecation/**' - 'bindings/python/examples/**' - 'bindings/python/scripts/**' - 'bindings/python/pyproject.toml' @@ -24,6 +25,7 @@ on: paths: - 'bindings/python/src/**' - 'bindings/python/tests/**' + - 'bindings/python/deprecation/**' - 'bindings/python/examples/**' - 'bindings/python/scripts/**' - 'bindings/python/pyproject.toml' diff --git a/bindings/python/AGENTS.md b/bindings/python/AGENTS.md index 35f595d7804..81bd8d44522 100644 --- a/bindings/python/AGENTS.md +++ b/bindings/python/AGENTS.md @@ -17,6 +17,7 @@ Paths in this file are relative to the repository root (the directory that conta | reporting an engine bug upstream, sending a low-hanging engine fix, or checking an upstream fix to one of our issues | `bindings/python/docs/development/upstream-issues.md` | | the periodic `bindings/python` update to ArcadeData/arcadedb (retired 2026-10-25; the bindings continue in ArcadeData/arcadedb-python) | `bindings/python/docs/development/upstream-bindings-pr.md` | | releasing | `bindings/python/docs/development/release.md` | +| publishing the last `arcadedb-embedded` release (the deprecation shim), or telling users how to move | `bindings/python/deprecation/README.md`, `bindings/python/deprecation/MIGRATION.md` | | debugging a build, JVM or test failure | `bindings/python/docs/development/troubleshooting.md` | | making a performance claim, or working around an engine bug | `bindings/python/docs/guide/performance.md`, `bindings/python/docs/guide/known-issues.md`, `bindings/python/docs/benchmarks/protocol.md` | diff --git a/bindings/python/deprecation/MIGRATION.md b/bindings/python/deprecation/MIGRATION.md new file mode 100644 index 00000000000..c5cee5bf133 --- /dev/null +++ b/bindings/python/deprecation/MIGRATION.md @@ -0,0 +1,45 @@ +# Migrating from arcadedb-embedded + +The ArcadeDB Python bindings moved from `humemai/arcadedb-embedded-python` to [ArcadeData/arcadedb-python](https://github.com/ArcadeData/arcadedb-python), under new PyPI names: `arcadedb` (the native build) and `arcadedb-jvm` (the JVM build). `arcadedb-embedded` was the JVM package. Its last release is a shim that installs `arcadedb`, the native build. If you need the JVM build, install `arcadedb-jvm` yourself. This repository is archived after the move. Documentation: [docs.arcadedb.com/arcadedb-python](https://docs.arcadedb.com/arcadedb-python/). To decide between the two packages, read [Which package?](https://docs.arcadedb.com/arcadedb-python/which-package.html). + +`arcadedb-embedded-native` (the native preview package) is archived: no further releases and no shim. No files of it remain on PyPI today (its simple index lists none). Install `arcadedb` instead: it is the native build, and its import name is `arcadedb`, not `arcadedb_native`. + +## Install and import + +| Before | After (native build) | After (JVM build) | +|---|---|---| +| `uv add arcadedb-embedded` | `uv add arcadedb` | `uv add arcadedb-jvm` | +| `uv add 'arcadedb-embedded[arrow,pandas]'` | `uv add 'arcadedb[arrow,pandas]'` | `uv add 'arcadedb-jvm[arrow,pandas]'` | +| `uv add arcadedb-embedded` | `uv add arcadedb` | `uv add arcadedb-jvm` | +| `import arcadedb_embedded` | `import arcadedb` | `import arcadedb` | +| `from arcadedb_embedded import open_database` | `from arcadedb import open_database` | `from arcadedb import open_database` | +| `arcadedb_embedded.core`, `arcadedb_embedded.jvm`, ... | `arcadedb.core`, ... | `arcadedb.core`, `arcadedb.jvm`, ... | + +The package for `arcadedb-embedded` users is now `arcadedb`, which is the native build: the engine runs as a native library, without a JVM. It is a different runtime from the one you had. If your code needs the JVM build, for example because it uses Java classes through JPype, install `arcadedb-jvm`: it is the same JVM build as `arcadedb-embedded`, so nothing changes at run time. The extra `vector` of `arcadedb-embedded` (numpy) is the extra `numpy` of `arcadedb`; `arrow`, `pandas` and `polars` keep their names. `arcadedb` defines no other extras: the old `test`, `dev` and `examples` do not exist there. + +Both new packages provide the module `arcadedb`, so an environment holds `arcadedb` or `arcadedb-jvm`, not both. Remove the old package first: `uv remove arcadedb-embedded`. + +## Version mapping + +From 26.11.1 the package version is the ArcadeDB engine version, as before. + +| Old | Last release | New | +|---|---|---| +| `arcadedb-embedded` (JVM, bundled JRE) | 26.10.2.post1 on PyPI, then the shim 26.11.1 | `arcadedb` 26.11.1 and later (native), or `arcadedb-jvm` 26.11.1 and later (JVM) | +| `arcadedb-embedded-native` (preview, archived) | no further releases, no shim | `arcadedb` 26.11.1 and later | + +The 26.11.1 release of `arcadedb-embedded` is a shim and the last one. It installs `arcadedb` at exactly 26.11.1. Importing `arcadedb_embedded` then emits one `FutureWarning` and re-exports `arcadedb`, submodules included, so unchanged code keeps running until you edit the import. The warning says that the package is now `arcadedb`, that this is the native build, and that `arcadedb-jvm` is the JVM build. The shim never upgrades: for newer versions install `arcadedb` or `arcadedb-jvm`. Older releases of the old name stay on PyPI for pinned installs. The shim is published only after the native `arcadedb` 26.11.1 final release exists. + +## What stays the same + +- The Python API: the module is renamed, its classes and functions are the ones of the 26.11.1 bindings. A few features can differ between the two builds; Which package? lists them. +- Python 3.11 to 3.14, and the Apache-2.0 license. +- The extras `arrow`, `pandas` and `polars`, under the same names. +- Where to look: the user guide and API reference move with the code to the new docs site, and the old `docs.humem.ai/arcadedb` pages redirect there. + +## What changes + +- The distribution and import names, as above. +- The default runtime: `uv add arcadedb` installs the native build, not the JVM build. +- Issues and pull requests go to [ArcadeData/arcadedb-python](https://github.com/ArcadeData/arcadedb-python/issues). Benchmarks stay with HumemAI, in [humemai/dbbench](https://github.com/humemai/dbbench). +- Engine changes between 26.10.2 and 26.11.1: read the 26.11.1 release notes, as for any upgrade. diff --git a/bindings/python/deprecation/README.md b/bindings/python/deprecation/README.md new file mode 100644 index 00000000000..17c765d1329 --- /dev/null +++ b/bindings/python/deprecation/README.md @@ -0,0 +1,75 @@ +# Deprecation shim for the old PyPI name arcadedb-embedded + +The Python bindings move to [ArcadeData/arcadedb-python](https://github.com/ArcadeData/arcadedb-python) as `arcadedb` (the native build) and `arcadedb-jvm` (the JVM build), both importing as `arcadedb`. This folder holds the last release of the old name `arcadedb-embedded`: a pure-Python shim that depends on `arcadedb` at the same version, warns once, and re-exports it. The old JVM package therefore moves to the native build, on purpose (decided 2026-10-25). The warning and the README say so in plain words, name `arcadedb-jvm` as the way to keep the JVM build, and link the Which package? page of the new documentation. Users: see [MIGRATION.md](MIGRATION.md). + +`arcadedb-embedded-native` (the native preview package) is archived: no further releases and no shim. No files of it remain on PyPI today, so there is nothing to redirect; the shim README and MIGRATION.md tell people to install `arcadedb` (import name `arcadedb`, not `arcadedb_native`). + +| Folder | PyPI project | Depends on | Version | +|---|---|---|---| +| `arcadedb-embedded/` | `arcadedb-embedded` (old JVM package) | `arcadedb==26.11.1` (the native build) | 26.11.1 | + +The shim module is `arcadedb_embedded`. The mapping above lives in one place, `TARGETS` in `build_shims.py`, together with the one assumption behind it: `arcadedb` provides the import name `arcadedb` (the layout says so, but it has no real release yet). `EXTRAS` there maps the extras of the old package to the extras of `arcadedb` (`vector`, the numpy extra, becomes `numpy`; `arcadedb` has no `vector`), and `NATIVE_EXTRAS` is the set the native `arcadedb` wheel defines (`numpy`, `arrow`, `pandas`, `polars`; source: ArcadeData/arcadedb-python, branch `native-two-dist`, `native/wheel/pyproject.toml`). `build_shims.py` fails when the shim forwards anything else, so re-read that file when the native wheel's extras change. `build_shims.py` refuses to build when the shim's `pyproject.toml` (name, description, dependencies, extras, `Replaced by` URL) or its `_NEW_DIST` and `_NEW_MODULE` constants disagree with `TARGETS`. The real package in `bindings/python/src` is not changed and does not warn: the benchmark harness imports it until the split. + +**This shim publishes only after the native `arcadedb` 26.11.1 FINAL exists.** It pins `arcadedb==26.11.1`, and a `26.11.1.devN` release does not satisfy that pin. Until the final release is out, do not publish this shim: `uv add arcadedb-embedded` would fail to resolve. + +## Build and test (never uploads) + +```bash +python3 bindings/python/deprecation/build_shims.py --out /tmp/shims # uv build, wheel + sdist +taskset -c 0-3 uv run pytest bindings/python/tests/test_deprecation_shims.py # needs the real wheel installed, like the rest of the suite +``` + +`build_shims.py` checks each version with `scripts/check_version.py` first; add `--check-pypi` on cutover day to check it is the next version on PyPI (it fails once that version is published). + +## Cutover day + +1. Release the native `arcadedb` 26.11.1 FINAL from ArcadeData/arcadedb-python first (and `arcadedb-jvm` 26.11.1, which the warning names). Check that PyPI serves it (`curl -sf https://pypi.org/pypi/arcadedb/26.11.1/json` succeeds): the shim pins this exact version. If the first final `arcadedb` is not out (only `26.11.1.devN`), do not publish the shim. +2. Never push a `26.11.1` tag in this repository: `release-python-packages.yml` would build and publish the real wheels under `arcadedb-embedded`. +3. Build and check: `python3 bindings/python/deprecation/build_shims.py --out dist --check-pypi && uvx twine check dist/*`. +4. Publish the shim. Trusted publishing works only from a workflow, so either (a) register a trusted publisher on the PyPI project `arcadedb-embedded` (repository `humemai/arcadedb-embedded-python`, workflow `publish-deprecation-shims.yml`, environment `pypi`), add the workflow below to `.github/workflows/` (and to `FORK_GITHUB_ALLOWLIST` in `sync-upstream.sh` if another sync will still run), and dispatch it with `publish` on; or (b) upload from a laptop with `uvx twine upload dist/*` and a project-scoped API token. +5. In a clean environment, `uv run --no-project --with arcadedb-embedded==26.11.1 python -W default -c "import arcadedb_embedded, importlib.metadata as m; print(m.version('arcadedb'))"` installs `arcadedb` (the native build, not `arcadedb-jvm`: it prints `26.11.1`) and shows one FutureWarning that names `arcadedb-jvm` for the JVM build. + +```yaml +name: Publish deprecation shims +on: + workflow_dispatch: + inputs: + publish: + description: "Upload to PyPI (off: build and check only)" + type: boolean + default: false +permissions: + contents: read +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup UV package manager + run: | + installer="$(mktemp)" + curl -LsSf --retry 5 --retry-all-errors --retry-delay 5 -o "$installer" https://astral.sh/uv/0.12.0/install.sh + sh "$installer" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - run: python3 bindings/python/deprecation/build_shims.py --out dist --check-pypi + - run: uvx twine check dist/*.whl dist/*.tar.gz + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: shims + path: dist/ + publish: + needs: build + if: inputs.publish + runs-on: ubuntu-24.04 + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: shims + path: dist/ + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/ +``` diff --git a/bindings/python/deprecation/arcadedb-embedded/README.md b/bindings/python/deprecation/arcadedb-embedded/README.md new file mode 100644 index 00000000000..9b80aa23171 --- /dev/null +++ b/bindings/python/deprecation/arcadedb-embedded/README.md @@ -0,0 +1,40 @@ +# arcadedb-embedded has moved to arcadedb + +**This package is deprecated. 26.11.1 is its last release.** The ArcadeDB Python bindings now live in the ArcadeData organization, under a new package name. The package is now [`arcadedb`](https://pypi.org/project/arcadedb/), which is the **native build**: the engine runs as a native library, without a JVM. If you need the JVM build, install [`arcadedb-jvm`](https://pypi.org/project/arcadedb-jvm/) instead. + +**The runtime changes for users of the old name.** `arcadedb-embedded` was the JVM build; `arcadedb` is the native build. Both new packages are imported as `arcadedb`. + +| | Old | New | +|---|---|---| +| PyPI package | `arcadedb-embedded` | [`arcadedb`](https://pypi.org/project/arcadedb/) (native build) or [`arcadedb-jvm`](https://pypi.org/project/arcadedb-jvm/) (JVM build) | +| Import | `import arcadedb_embedded` | `import arcadedb` | +| Source and issues | `humemai/arcadedb-embedded-python` (archived) | [ArcadeData/arcadedb-python](https://github.com/ArcadeData/arcadedb-python) | +| Documentation | `docs.humem.ai/arcadedb` | [docs.arcadedb.com/arcadedb-python](https://docs.arcadedb.com/arcadedb-python/) | + +## What to do + +```bash +uv remove arcadedb-embedded +uv add arcadedb +``` + +Then change `import arcadedb_embedded` to `import arcadedb` (and `from arcadedb_embedded import X` to `from arcadedb import X`). + +`arcadedb-embedded` was the JVM build, so `arcadedb` is a different runtime. If your code needs the JVM build, for example because it uses Java classes through JPype or a long-running load that suits the JVM, install it instead: + +```bash +uv remove arcadedb-embedded +uv add arcadedb-jvm +``` + +[Which package?](https://docs.arcadedb.com/arcadedb-python/which-package.html) explains the difference. `arcadedb` and `arcadedb-jvm` both provide the module `arcadedb`: install one of them in an environment, not both. + +## What this release does + +It is a shim. It installs `arcadedb==26.11.1`, and `import arcadedb_embedded` emits one `FutureWarning` and then gives you the `arcadedb` module, submodules included, so existing code keeps running. It is pinned to `arcadedb==26.11.1` and gets no updates: to receive newer ArcadeDB versions, switch to `arcadedb` (or `arcadedb-jvm`). + +The extras `vector`, `arrow`, `pandas` and `polars` are forwarded to `arcadedb`. The old `vector` extra (numpy) maps to the `numpy` extra of `arcadedb`, which defines `numpy`, `arrow`, `pandas` and `polars`; the old `test`, `dev` and `examples` extras do not exist there. + +The sibling package `arcadedb-embedded-native` (the native preview) is archived: no further releases, no shim. Install `arcadedb` instead; its import name is `arcadedb`, not `arcadedb_native`. + +Older `arcadedb-embedded` releases stay on PyPI for pinned installs. For the full old-to-new table, see the [migration guide](https://github.com/humemai/arcadedb-embedded-python/blob/main/bindings/python/deprecation/MIGRATION.md). diff --git a/bindings/python/deprecation/arcadedb-embedded/pyproject.toml b/bindings/python/deprecation/arcadedb-embedded/pyproject.toml new file mode 100644 index 00000000000..819b54eff4e --- /dev/null +++ b/bindings/python/deprecation/arcadedb-embedded/pyproject.toml @@ -0,0 +1,47 @@ +# Deprecation shim for arcadedb-embedded (the last release). Built by ../build_shims.py; see ../README.md. +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "arcadedb-embedded" +version = "26.11.1" +description = "Deprecated: arcadedb-embedded has moved to arcadedb (import arcadedb), now the native build, not the JVM build; for the JVM build use arcadedb-jvm. Last release, a thin shim." +readme = "README.md" +requires-python = ">=3.11" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [{ name = "Taewoon Kim", email = "taewoon@humem.ai" }] +keywords = ["arcadedb", "deprecated", "moved", "database"] +classifiers = [ + "Development Status :: 7 - Inactive", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Database", +] +# The shim installs exactly the package it points to, at the same version. +dependencies = ["arcadedb==26.11.1"] + +# The extras people installed from the old name, forwarded to the extras of `arcadedb` (the native wheel defines numpy, arrow, pandas and polars; the old `vector` extra was numpy). build_shims.py checks this block. +[project.optional-dependencies] +vector = ["arcadedb[numpy]==26.11.1"] +arrow = ["arcadedb[arrow]==26.11.1"] +pandas = ["arcadedb[pandas]==26.11.1"] +polars = ["arcadedb[polars]==26.11.1"] + +[project.urls] +Homepage = "https://github.com/ArcadeData/arcadedb-python" +Documentation = "https://docs.arcadedb.com/arcadedb-python/" +Source = "https://github.com/ArcadeData/arcadedb-python" +Issues = "https://github.com/ArcadeData/arcadedb-python/issues" +"Replaced by" = "https://pypi.org/project/arcadedb/" + +[tool.hatch.build.targets.wheel] +packages = ["src/arcadedb_embedded"] + +[tool.hatch.build.targets.sdist] +include = ["src", "README.md", "LICENSE", "pyproject.toml"] diff --git a/bindings/python/deprecation/arcadedb-embedded/src/arcadedb_embedded/__init__.py b/bindings/python/deprecation/arcadedb-embedded/src/arcadedb_embedded/__init__.py new file mode 100644 index 00000000000..4f143dae00b --- /dev/null +++ b/bindings/python/deprecation/arcadedb-embedded/src/arcadedb_embedded/__init__.py @@ -0,0 +1,106 @@ +# Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) +# SPDX-License-Identifier: Apache-2.0 +"""Deprecated: ``arcadedb-embedded`` has moved to ``arcadedb``, the native build. + +This is the last release of ``arcadedb-embedded``. It is a thin shim: it warns once at import, then hands every name and submodule over to ``arcadedb``, which provides the module ``arcadedb``. For the JVM build, install ``arcadedb-jvm``. +""" + +# ---- constants of this shim ---- +_OLD_DIST = "arcadedb-embedded" +_NEW_DIST = "arcadedb" +_NEW_MODULE = "arcadedb" +_NOTE = ( + "`arcadedb` is the native build, which runs without a JVM. " + "This package was the JVM build, so for you the runtime changes. " + "If you need the JVM build, install `arcadedb-jvm` (`uv add arcadedb-jvm`; it is also `import arcadedb`). " + "Which package to pick: https://docs.arcadedb.com/arcadedb-python/which-package.html." +) +# ---- body (tests/test_deprecation_shims.py checks that it is there) ---- + +import importlib +import importlib.abc +import importlib.machinery +import importlib.util +import sys +import warnings + +_MESSAGE = ( + f"The {_OLD_DIST!r} package has moved and this is its last release. " + f"The package is now {_NEW_DIST!r}: install it with `uv add {_NEW_DIST}`. {_NOTE} " + f"Write `import {_NEW_MODULE}` where you wrote `import {__name__}`. " + "New home: https://github.com/ArcadeData/arcadedb-python, " + "docs: https://docs.arcadedb.com/arcadedb-python/" +) +# FutureWarning, not DeprecationWarning: Python shows it to end users by default. +warnings.warn(_MESSAGE, FutureWarning, stacklevel=2) + +_new = importlib.import_module(_NEW_MODULE) +__all__ = list( + getattr(_new, "__all__", None) or [n for n in dir(_new) if not n.startswith("_")] +) +# the same names `from import *` would bind +globals().update({_n: getattr(_new, _n) for _n in __all__}) + +_OLD = __name__ +_NEW = _new.__name__ + + +class _AliasLoader(importlib.abc.Loader): + """Hand out the real ``arcadedb`` submodule under the old name, not a second copy.""" + + def __init__(self, target): + self._target = target + self._spec = None + + def create_module(self, spec): + module = importlib.import_module(self._target) + self._spec = module.__spec__ + return module + + def exec_module(self, module): + # the import machinery stamped the alias spec on the shared module: put its own back + module.__spec__ = self._spec + + +class _AliasFinder(importlib.abc.MetaPathFinder): + """Resolve ``arcadedb_embedded.x.y`` to ``arcadedb.x.y``, also for modules not yet imported.""" + + alias_of = _NEW + old = _OLD + + def find_spec(self, fullname, path=None, target=None): + if not fullname.startswith(_OLD + "."): + return None + real = _NEW + fullname[len(_OLD) :] + try: + found = importlib.util.find_spec(real) + except (ImportError, ValueError): + return None + if found is None: + return None + return importlib.machinery.ModuleSpec(fullname, _AliasLoader(real)) + + +# first in line, so a package alias is not also found on disk and loaded a second time +if not any( + getattr(f, "alias_of", None) == _NEW and getattr(f, "old", None) == _OLD + for f in sys.meta_path +): + sys.meta_path.insert(0, _AliasFinder()) + +# submodules the new package has already imported answer to the old name right away +for _name, _module in list(sys.modules.items()): + if _name.startswith(_NEW + ".") and _module is not None: + sys.modules.setdefault(_OLD + _name[len(_NEW) :], _module) + + +def __getattr__(name): + """Anything not star-imported (a private name, a submodule) comes from ``arcadedb``.""" + try: + return getattr(_new, name) + except AttributeError: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None + + +def __dir__(): + return sorted(set(globals()) | set(dir(_new))) diff --git a/bindings/python/deprecation/build_shims.py b/bindings/python/deprecation/build_shims.py new file mode 100644 index 00000000000..e534e374485 --- /dev/null +++ b/bindings/python/deprecation/build_shims.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) +# SPDX-License-Identifier: Apache-2.0 +"""Build the deprecation shim of ``arcadedb-embedded`` with ``uv build``. + +This builds only. It never uploads: publishing is a separate, deliberate step (README.md here). + +Where each old name points is set in ``TARGETS`` below, and nowhere else. Before it builds, this script checks that each shim's ``pyproject.toml`` and ``__init__.py`` files agree with ``TARGETS``, so a changed target cannot ship half-changed. + +Each shim is copied to a scratch directory first, together with the repository's LICENSE, so the +license file ships in the wheel and the sdist and the source tree stays clean. The version in the +shim's pyproject.toml must pass ``scripts/check_version.py`` (syntax and counting rule). + +usage: + build_shims.py [--out DIR] [--shim NAME ...] [--check-pypi] + + --out DIR where the wheels and sdists go (default: dist/ next to this script, git-ignored) + --shim NAME build only this shim (repeatable; default: all of them, which is one) + --check-pypi also run check_version.py against the live PyPI project (needs the network, and + fails once that version has been published, as the counting rule says) +""" + +from __future__ import annotations + +import argparse +import importlib.util +import os +import re +import shutil +import subprocess # nosec B404 - runs `uv build` with a fixed argument list +import sys +import tempfile +import tomllib +from pathlib import Path +from typing import NamedTuple + +HERE = Path(__file__).resolve().parent +REPO_ROOT = HERE.parents[2] +# There is one shim. `arcadedb-embedded-native` (the native preview package) gets none: its PyPI project has no files and is archived. +SHIMS = ("arcadedb-embedded",) + + +class Target(NamedTuple): + """The package an old name points to.""" + + dist: str # the PyPI distribution the shim depends on and tells people to install + module: str # the import name that distribution provides: what the shim re-exports + + +# Where each old name points (the packaging layout v3 decided on 2026-10-25, see ArcadeData/arcadedb-python .notes/MIGRATION.md). +# arcadedb-embedded (the old JVM package) -> `arcadedb`, which is the NATIVE build. The runtime therefore changes for users of the old name, and the warning, the README and MIGRATION.md say so in plain words and name `arcadedb-jvm` as the way to keep the JVM build. +# This shim publishes only after the native `arcadedb` 26.11.1 FINAL exists, because it pins `arcadedb==26.11.1` and a `26.11.1.devN` release does not satisfy that pin. +# ASSUMPTION: `arcadedb` provides the import name `arcadedb`. The native wheel does (packages.find and package-data in native/wheel/pyproject.toml on the branch named below), and `arcadedb-jvm` imports as `arcadedb` too, but `arcadedb` has no final release on PyPI yet (0.0.1 is a placeholder), so the published wheel cannot be checked here. +# If the package gets another import name, change it here and in the `_NEW_MODULE` constant of the shim's `__init__.py`; check_target() fails until both agree. +IMPORT_NAME = "arcadedb" +TARGETS = { + "arcadedb-embedded": Target(dist="arcadedb", module=IMPORT_NAME), +} +# The JVM build the warning and the README name for people who need it (not a dependency of the shim). +JVM_DIST = "arcadedb-jvm" +# The extras of the real `arcadedb` (native) distribution, and nothing else may be forwarded to. Source: ArcadeData/arcadedb-python, branch native-two-dist (commit 56aab39), file native/wheel/pyproject.toml, [project.optional-dependencies]. The root pyproject.toml of that repo builds `arcadedb-jvm` instead (extras test, dev, vector, arrow, pandas, polars, examples) and is NOT the source. +NATIVE_EXTRAS = frozenset({"numpy", "arrow", "pandas", "polars"}) +# The extras people installed from the old name -> the extra of `arcadedb` they are forwarded to (the old `vector` extra was numpy; `arcadedb` has no `vector`). The old test, dev and examples extras are not forwarded: `arcadedb` does not define them. +EXTRAS = {"vector": "numpy", "arrow": "arrow", "pandas": "pandas", "polars": "polars"} + + +def _load_check_version(): + path = HERE.parent / "scripts" / "check_version.py" + spec = importlib.util.spec_from_file_location("check_version", path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules["check_version"] = module + spec.loader.exec_module(module) + return module + + +def shim_version(shim: str) -> str: + """The version in the shim's pyproject.toml.""" + with open(HERE / shim / "pyproject.toml", "rb") as handle: + return tomllib.load(handle)["project"]["version"] + + +def check_target(shim: str) -> None: + """Fail unless the shim's metadata and import constants all point at TARGETS[shim].""" + target = TARGETS[shim] + version = shim_version(shim) + with open(HERE / shim / "pyproject.toml", "rb") as handle: + project = tomllib.load(handle)["project"] + expected = { + "name": shim, + "description": f"Deprecated: {shim} has moved to {target.dist} (import {target.module}), now the native build, not the JVM build; for the JVM build use {JVM_DIST}. Last release, a thin shim.", + "dependencies": [f"{target.dist}=={version}"], + "optional-dependencies": { + old: [f"{target.dist}[{new}]=={version}"] for old, new in EXTRAS.items() + }, + } + problems = [ + f"project.{key} is {project.get(key)!r}, expected {want!r}" + for key, want in expected.items() + if project.get(key) != want + ] + forwarded = { + match.group(1) + for requirements in project.get("optional-dependencies", {}).values() + for requirement in requirements + if ( + match := re.fullmatch( + rf"{re.escape(target.dist)}\[(\w+)\]==.+", requirement + ) + ) + } + if forwarded != NATIVE_EXTRAS or set(EXTRAS.values()) != NATIVE_EXTRAS: + problems.append( + f"the shim forwards the extras {sorted(forwarded)} (EXTRAS: {sorted(set(EXTRAS.values()))}), but {target.dist} defines {sorted(NATIVE_EXTRAS)} (NATIVE_EXTRAS)" + ) + replaced_by = project["urls"].get("Replaced by") + if replaced_by != f"https://pypi.org/project/{target.dist}/": + problems.append(f"project.urls['Replaced by'] is {replaced_by!r}") + for path in sorted((HERE / shim / "src").glob("*/__init__.py")): + text = path.read_text(encoding="utf-8") + for name, want in ( + ("_OLD_DIST", shim), + ("_NEW_DIST", target.dist), + ("_NEW_MODULE", target.module), + ): + if f'{name} = "{want}"\n' not in text: + problems.append(f'{path.relative_to(HERE)}: {name} is not "{want}"') + if f"`uv add {JVM_DIST}`" not in text: + problems.append( + f"{path.relative_to(HERE)}: the warning does not name `uv add {JVM_DIST}`" + ) + if problems: + raise SystemExit( + f"{shim} does not point at {target.dist}:\n " + "\n ".join(problems) + ) + + +def check_version(shim: str, check_pypi: bool) -> None: + """Fail unless the shim's version follows the counting rule (and is the next one on PyPI).""" + cv = _load_check_version() + version = shim_version(shim) + argv = [version] + if check_pypi: + argv += ["--pypi", shim] + if cv.main(argv) != 0: + raise SystemExit(f"{shim} {version} does not pass check_version.py") + + +def build(shim: str, out_dir: Path) -> list[Path]: + """Build one shim into out_dir and return the files it produced.""" + # `uv run` exports its own path as UV; a uv-made test environment (Windows CI) may not have uv on PATH. + uv = os.environ.get("UV") or shutil.which("uv") + if not uv: + raise SystemExit("uv is required (https://docs.astral.sh/uv/)") + out_dir.mkdir(parents=True, exist_ok=True) + before = set(out_dir.iterdir()) + with tempfile.TemporaryDirectory(prefix=f"{shim}-") as scratch: + stage = Path(scratch) / shim + shutil.copytree( + HERE / shim, stage, ignore=shutil.ignore_patterns("__pycache__", "dist") + ) + shutil.copyfile(REPO_ROOT / "LICENSE", stage / "LICENSE") + subprocess.run( # nosec B603 - fixed argument list, no shell + [uv, "build", "--out-dir", str(out_dir), str(stage)], check=True + ) + made = set(out_dir.iterdir()) - before + return sorted(p for p in made if p.suffix == ".whl" or p.name.endswith(".tar.gz")) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--out", type=Path, default=HERE / "dist") + parser.add_argument("--shim", action="append", choices=SHIMS) + parser.add_argument("--check-pypi", action="store_true") + args = parser.parse_args(argv) + + shims = args.shim or list(SHIMS) + for shim in shims: + check_target(shim) + check_version(shim, args.check_pypi) + for shim in shims: + for artifact in build(shim, args.out): + print(f"built {artifact}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bindings/python/docs/development/release.md b/bindings/python/docs/development/release.md index 2453830074a..b7fd90ae754 100644 --- a/bindings/python/docs/development/release.md +++ b/bindings/python/docs/development/release.md @@ -381,6 +381,10 @@ uv run --project .. --group docs mike alias --update-aliases \ 3. Check "Set as a pre-release" 4. Or delete the release entirely +## The Last Releases: Deprecation Shims + +The bindings move to ArcadeData/arcadedb-python as `arcadedb` (the native build) and `arcadedb-jvm` (the JVM build). The old name `arcadedb-embedded` ends with one shim release, 26.11.1, from `bindings/python/deprecation/` (a pure-Python wheel that warns once and depends on `arcadedb`, the native build, at the same version; the warning names `arcadedb-jvm` for the JVM build). It publishes only after the native `arcadedb` 26.11.1 FINAL exists. `arcadedb-embedded-native` is archived: no further releases and no shim, and its PyPI project has no files. They are not released with the steps above: never push a `26.11.1` tag in this repository, because the release workflow would build and publish the real wheels. The cutover order and the publish options are in `bindings/python/deprecation/README.md`. + ## Troubleshooting ### PyPI upload fails diff --git a/bindings/python/docs/development/testing.md b/bindings/python/docs/development/testing.md index f81b5cc1e8b..aa7372658b6 100644 --- a/bindings/python/docs/development/testing.md +++ b/bindings/python/docs/development/testing.md @@ -143,6 +143,7 @@ asserts, so the list below stops at the file. | `test_wheel_platform_tag.py` | The wheel's manylinux platform tag, and `__version__` equals the installed distribution version | | `test_wheel_size.py` | The wheel size gate: a wheel at or over 100 decimal megabytes fails the build | | `test_check_test_skips.py` | The skip gate described below | +| `test_deprecation_shims.py` | The shim for the old PyPI name `arcadedb-embedded` (`deprecation/`): it builds with `uv build` as a pure `py3-none-any` wheel, pins `arcadedb` (the native build) at the same version and never `arcadedb-jvm` (`build_shims.py` refuses a shim that points elsewhere), there is no shim for `arcadedb-embedded-native`, the warning names `arcadedb-jvm` for the JVM build and links the Which package? page, the version follows the counting rule, and against a stub `arcadedb` the import warns once with a `FutureWarning` and names, submodules (also not-yet-imported and nested ones) and private attributes resolve to the new module. Not collected where `deprecation/` is absent (the upstream pull request branch) | ### Docs and open engine findings diff --git a/bindings/python/tests/conftest.py b/bindings/python/tests/conftest.py index 8198f3305be..0f19dbe88ba 100644 --- a/bindings/python/tests/conftest.py +++ b/bindings/python/tests/conftest.py @@ -28,6 +28,11 @@ if not os.path.isdir(os.path.join(os.path.dirname(os.path.dirname(__file__)), "docs")): # the upstream pull request branch has no docs/ directory collect_ignore.append("test_docs_examples.py") +if not os.path.isdir( + os.path.join(os.path.dirname(os.path.dirname(__file__)), "deprecation") +): + # the upstream pull request branch has no deprecation/ directory (the shims for the old PyPI names) + collect_ignore.append("test_deprecation_shims.py") @pytest.hookimpl(trylast=True) diff --git a/bindings/python/tests/test_deprecation_shims.py b/bindings/python/tests/test_deprecation_shims.py new file mode 100644 index 00000000000..b05ee37e10b --- /dev/null +++ b/bindings/python/tests/test_deprecation_shims.py @@ -0,0 +1,432 @@ +"""The deprecation shim for the old PyPI name (deprecation/): built here, checked against a stub. + +`arcadedb-embedded` gets one last release, 26.11.1: a pure-Python shim that installs `arcadedb` (the native +build, the new home of the bindings) at the same version, warns once at import, and re-exports it. The old JVM +package therefore moves to the native build; the warning says so in plain words and names `arcadedb-jvm` for +people who need the JVM build. `arcadedb-embedded-native` gets no shim: its PyPI project has no files and is +archived. The tests build the shim with `uv build` (no upload), and check the wheel, the dependency pin, the +warning, and attribute and submodule access against a stub `arcadedb` module: the real package is not needed. +""" + +from __future__ import annotations + +import importlib.util +import json +import re +import subprocess # nosec B404 - test-controlled child processes, no untrusted input +import sys +import textwrap +import zipfile +from pathlib import Path + +import pytest + +BINDINGS = Path(__file__).resolve().parents[1] +DEPRECATION = BINDINGS / "deprecation" +VERSION = "26.11.1" +# old distribution -> the package it points to (written out here on purpose, apart from TARGETS in build_shims.py, +# so a wrong edit of that table fails a test). The old JVM package points to the NATIVE build. +SHIMS = {"arcadedb-embedded": "arcadedb"} +# the import name of the replacement (an assumption, see build_shims.IMPORT_NAME) +IMPORT_NAME = "arcadedb" +WHICH_PACKAGE_URL = "https://docs.arcadedb.com/arcadedb-python/which-package.html" +# What PyPI listed before the cutover (checked 2026-10-25): the counting rule is applied against this +# snapshot, because once the shims are published the live list contains 26.11.1 itself. +PUBLISHED_BEFORE_CUTOVER = { + "arcadedb-embedded": ["26.9.1", "26.10.1", "26.10.2", "26.10.2.post1"], +} +# old distribution -> the import names its shim ships +MODULES = {"arcadedb-embedded": ["arcadedb_embedded"]} +CASES = [(dist, module) for dist, modules in MODULES.items() for module in modules] +BODY_MARKER = "# ---- body" +# the extras of the old package -> the extra of `arcadedb` they are forwarded to (`vector` was numpy; `arcadedb` has no `vector`) +EXTRAS = {"arrow": "arrow", "pandas": "pandas", "polars": "polars", "vector": "numpy"} +# the extras the native `arcadedb` wheel defines (ArcadeData/arcadedb-python, branch native-two-dist, native/wheel/pyproject.toml); written out here on purpose, apart from build_shims.NATIVE_EXTRAS +NATIVE_EXTRAS = {"numpy", "arrow", "pandas", "polars"} + +STUB_FILES = { + "arcadedb/__init__.py": """ + __version__ = "26.11.1" + __all__ = ["Database", "open_database"] + + class Database: + pass + + def open_database(path): + return Database() + + _private = "p" + from . import core # the real package imports its submodules like this + """, + "arcadedb/core.py": "class Core:\n pass\n\nVALUE = 1\n", + # not imported by __init__: only a lazy import reaches it + "arcadedb/extra.py": "VALUE = 42\n", + "arcadedb/sub/__init__.py": "", + "arcadedb/sub/deep.py": "NAME = 'deep'\n", +} + +# written for arcadedb_embedded; _probe() swaps in the module name under test +PROBE = """ +import json, sys, warnings +sys.path.insert(0, sys.argv[1]) +with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + import arcadedb_embedded + import arcadedb_embedded.core + import arcadedb_embedded.extra + import arcadedb_embedded.sub.deep + from arcadedb_embedded.sub import deep + from arcadedb_embedded.extra import VALUE +import arcadedb +out = { + "warnings": [[w.category.__name__, str(w.message), w.filename] for w in caught], + "same_class": arcadedb_embedded.Database is arcadedb.Database, + "same_function": arcadedb_embedded.open_database is arcadedb.open_database, + "version": arcadedb_embedded.__version__, + "all": sorted(arcadedb_embedded.__all__), + "private": arcadedb_embedded._private, + "core_alias": arcadedb_embedded.core is arcadedb.core, + "core_in_modules": sys.modules["arcadedb_embedded.core"] is arcadedb.core, + "lazy_alias": arcadedb_embedded.extra is arcadedb.extra, + "lazy_value": VALUE, + "nested_alias": arcadedb_embedded.sub.deep is arcadedb.sub.deep and deep is arcadedb.sub.deep, + "no_second_copy": sorted(n for n in sys.modules if n.startswith("arcadedb_embedded.") + and sys.modules[n].__name__.startswith("arcadedb_embedded.")), + "spec_kept": arcadedb.extra.__spec__.name, + "in_dir": all(n in dir(arcadedb_embedded) for n in ("Database", "core", "_private")), +} +try: + arcadedb_embedded.missing_name +except AttributeError as err: + out["missing_attr"] = str(err) +try: + import arcadedb_embedded.no_such_module +except ModuleNotFoundError: + out["missing_module"] = True +print(json.dumps(out)) +""" + + +def _load_build_shims(): + spec = importlib.util.spec_from_file_location( + "build_shims", DEPRECATION / "build_shims.py" + ) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules["build_shims"] = module + spec.loader.exec_module(module) + return module + + +def _load_check_version(): + spec = importlib.util.spec_from_file_location( + "check_version", BINDINGS / "scripts" / "check_version.py" + ) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules["check_version"] = module + spec.loader.exec_module(module) + return module + + +@pytest.fixture(scope="module") +def wheels(tmp_path_factory): + """Build the shim once (`uv build`, no upload) and return {old distribution: wheel path}.""" + out = tmp_path_factory.mktemp("shim-dist") + assert _load_build_shims().main(["--out", str(out)]) == 0 + built = {} + for dist in SHIMS: + found = list(out.glob(f"{dist.replace('-', '_')}-{VERSION}-*.whl")) + assert len(found) == 1, f"{dist}: expected one wheel in {out}, found {found}" + built[dist] = found[0] + return built + + +@pytest.fixture(scope="module") +def sites(wheels, tmp_path_factory): + """Per shim, a site directory holding the unpacked wheel next to the stub `arcadedb`.""" + result = {} + for dist, wheel in wheels.items(): + site = tmp_path_factory.mktemp(f"site-{dist}") + with zipfile.ZipFile(wheel) as archive: + archive.extractall(site) + for name, body in STUB_FILES.items(): + target = site / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(textwrap.dedent(body).lstrip("\n"), encoding="utf-8") + result[dist] = site + return result + + +def _metadata(wheel: Path) -> str: + with zipfile.ZipFile(wheel) as archive: + name = next(n for n in archive.namelist() if n.endswith(".dist-info/METADATA")) + return archive.read(name).decode("utf-8") + + +def _required_names(metadata: str) -> set[str]: + """The distribution names in the Requires-Dist lines, without extras, version or marker.""" + requires = re.findall(r"^Requires-Dist: (.+)$", metadata, flags=re.MULTILINE) + return {re.match(r"[A-Za-z0-9][A-Za-z0-9._-]*", r).group(0) for r in requires} + + +def _probe(site: Path, module: str = "arcadedb_embedded") -> dict: + done = subprocess.run( # nosec B603 - fixed argument list, no shell + [ + sys.executable, + "-I", + "-c", + PROBE.replace("arcadedb_embedded", module), + str(site), + ], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + assert done.returncode == 0, done.stderr + return json.loads(done.stdout.strip().splitlines()[-1]) + + +@pytest.mark.parametrize("dist", SHIMS) +def test_wheel_is_pure_python(dist, wheels): + wheel = wheels[dist] + assert wheel.name == f"{dist.replace('-', '_')}-{VERSION}-py3-none-any.whl" + with zipfile.ZipFile(wheel) as archive: + names = archive.namelist() + wheel_file = archive.read( + next(n for n in names if n.endswith(".dist-info/WHEEL")) + ).decode("utf-8") + assert "Root-Is-Purelib: true" in wheel_file + assert "Tag: py3-none-any" in wheel_file + code = [ + n + for n in names + if not n.startswith(f"{dist.replace('-', '_')}-{VERSION}.dist-info/") + ] + assert sorted(code) == [f"{m}/__init__.py" for m in MODULES[dist]], code + assert any(n.endswith("licenses/LICENSE") for n in names) + + +@pytest.mark.parametrize("dist", SHIMS) +def test_dependency_pins_the_new_package_at_the_same_version(dist, wheels): + metadata = _metadata(wheels[dist]) + new = SHIMS[dist] + assert f"Name: {dist}\n" in metadata + assert f"Version: {VERSION}\n" in metadata + requires = re.findall(r"^Requires-Dist: (.+)$", metadata, flags=re.MULTILINE) + assert f"{new}=={VERSION}" in requires + # every requirement is an exact pin on the new package, extras forwarded, nothing else + assert all( + re.fullmatch( + rf"{re.escape(new)}(\[\w+\])?=={re.escape(VERSION)}(; extra == '\w+')?", r + ) + for r in requires + ), requires + + +def test_the_shim_requires_arcadedb_the_native_build_and_not_arcadedb_jvm(wheels): + """The decided layout: the old JVM package moves to `arcadedb`, which is the native build. It never pulls `arcadedb-jvm`.""" + metadata = _metadata(wheels["arcadedb-embedded"]) + assert f"Requires-Dist: arcadedb=={VERSION}\n" in metadata + assert _required_names(metadata) == {"arcadedb"} + assert "arcadedb-jvm" not in _required_names(metadata) + assert "arcadedb-native" not in _required_names(metadata) + assert "arcadedb-embedded-native" not in _required_names(metadata) + # the extras of the old package are forwarded to the matching extras of arcadedb + assert sorted( + re.findall(r"^Provides-Extra: (.+)$", metadata, flags=re.MULTILINE) + ) == sorted(EXTRAS) + for old, new in EXTRAS.items(): + assert ( + f"Requires-Dist: arcadedb[{new}]=={VERSION}; extra == '{old}'\n" in metadata + ) + # only extras the native arcadedb defines are forwarded to: no vector, test, dev or examples + forwarded = set( + re.findall(r"^Requires-Dist: arcadedb\[(\w+)\]==", metadata, flags=re.MULTILINE) + ) + assert forwarded == NATIVE_EXTRAS + assert "Project-URL: Replaced by, https://pypi.org/project/arcadedb/\n" in metadata + assert "has moved to arcadedb (import arcadedb), now the native build" in metadata + assert "for the JVM build use arcadedb-jvm" in metadata + + +def test_the_pypi_readme_says_arcadedb_is_the_native_build_and_names_the_jvm_package( + wheels, +): + metadata = _metadata(wheels["arcadedb-embedded"]) + assert "uv add arcadedb\n" in metadata + assert "uv remove arcadedb-embedded\n" in metadata + assert "native build" in metadata + # the JVM build is named as the other choice, with the install line, and the Which package? page is linked + assert "uv add arcadedb-jvm\n" in metadata + assert WHICH_PACKAGE_URL in metadata + # the runtime change is said plainly, and the archived sibling is named with its import name + assert "The runtime changes for users of the old name." in metadata + assert "`arcadedb-embedded-native` (the native preview) is archived" in metadata + assert "not `arcadedb_native`" in metadata + + +def test_the_migration_texts_say_arcadedb_embedded_native_is_archived(): + for text in ("MIGRATION.md", "README.md"): + doc = (DEPRECATION / text).read_text(encoding="utf-8") + assert "`arcadedb-embedded-native`" in doc and "archived" in doc, text + assert "no further releases" in doc and "no shim" in doc, text + assert "not `arcadedb_native`" in doc, text + + +def test_there_is_no_shim_for_the_native_preview_package(): + """`arcadedb-embedded-native` has no files on PyPI and is archived: nothing here builds, ships or mentions a shim for it.""" + build_shims = _load_build_shims() + assert build_shims.SHIMS == ("arcadedb-embedded",) + assert sorted(build_shims.TARGETS) == ["arcadedb-embedded"] + folders = sorted( + p.name for p in DEPRECATION.iterdir() if p.is_dir() and p.name != "__pycache__" + ) + assert folders == ["arcadedb-embedded"] + assert not list(DEPRECATION.glob("**/arcadedb_native")) + + +def test_build_shims_has_one_table_with_the_decided_mapping(): + build_shims = _load_build_shims() + assert {k: v.dist for k, v in build_shims.TARGETS.items()} == SHIMS + assert {v.module for v in build_shims.TARGETS.values()} == {IMPORT_NAME} + assert dict(build_shims.EXTRAS) == EXTRAS + assert set(build_shims.NATIVE_EXTRAS) == NATIVE_EXTRAS + assert set(build_shims.EXTRAS.values()) == NATIVE_EXTRAS + assert build_shims.JVM_DIST == "arcadedb-jvm" + for dist in SHIMS: + build_shims.check_target( + dist + ) # the files under deprecation/ agree with the table + + +@pytest.mark.parametrize("dist", SHIMS) +def test_build_shims_refuses_a_shim_that_points_elsewhere(dist, monkeypatch): + """Changing the table without the shim files (or the other way round) must stop the build.""" + build_shims = _load_build_shims() + other = "arcadedb-jvm" # the other new package: the shim must not drift onto the JVM build unnoticed + monkeypatch.setitem( + build_shims.TARGETS, dist, build_shims.Target(dist=other, module=IMPORT_NAME) + ) + with pytest.raises(SystemExit) as refused: + build_shims.check_target(dist) + message = str(refused.value) + assert f"does not point at {other}" in message + assert "project.dependencies" in message and "_NEW_DIST" in message + monkeypatch.setitem( + build_shims.TARGETS, + dist, + build_shims.Target(dist=SHIMS[dist], module="arcadedb_other"), + ) + with pytest.raises(SystemExit) as refused: + build_shims.check_target(dist) + assert "_NEW_MODULE" in str(refused.value) + + +@pytest.mark.parametrize("dist", SHIMS) +def test_build_shims_refuses_extras_that_the_native_arcadedb_does_not_define( + dist, monkeypatch +): + """The shim may forward only to extras of the native `arcadedb` wheel: a drift either way stops the build.""" + build_shims = _load_build_shims() + # (a) the table forwards `vector` to an extra that `arcadedb` does not have + monkeypatch.setitem(build_shims.EXTRAS, "vector", "vector") + with pytest.raises(SystemExit) as refused: + build_shims.check_target(dist) + assert "NATIVE_EXTRAS" in str(refused.value) + monkeypatch.setitem(build_shims.EXTRAS, "vector", "numpy") + build_shims.check_target(dist) + # (b) the native wheel gains an extra that the shim does not forward yet + monkeypatch.setattr( + build_shims, "NATIVE_EXTRAS", frozenset(NATIVE_EXTRAS | {"parquet"}) + ) + with pytest.raises(SystemExit) as refused: + build_shims.check_target(dist) + assert "defines" in str(refused.value) and "parquet" in str(refused.value) + + +@pytest.mark.parametrize("dist", SHIMS) +def test_version_follows_the_counting_rule(dist): + cv = _load_check_version() + assert str(cv.check_next(VERSION, PUBLISHED_BEFORE_CUTOVER[dist])) == VERSION + with pytest.raises(cv.VersionError): + cv.check_next(VERSION, PUBLISHED_BEFORE_CUTOVER[dist] + [VERSION]) + # the build script reads the same version from pyproject.toml, and the rule's script accepts it + build_shims = _load_build_shims() + assert build_shims.shim_version(dist) == VERSION + build_shims.check_version(dist, check_pypi=False) + + +def test_the_shim_module_has_its_constants_and_the_body(): + text = ( + DEPRECATION / "arcadedb-embedded" / "src" / "arcadedb_embedded" / "__init__.py" + ).read_text(encoding="utf-8") + assert BODY_MARKER in text + assert '_OLD_DIST = "arcadedb-embedded"\n' in text + assert '_NEW_DIST = "arcadedb"\n' in text + assert '_NEW_MODULE = "arcadedb"\n' in text + + +@pytest.mark.parametrize(("dist", "module"), CASES) +def test_warning_fires_once_and_is_visible_by_default(dist, module, sites): + result = _probe(sites[dist], module) + assert len(result["warnings"]) == 1, result["warnings"] + category, message, filename = result["warnings"][0] + assert category == "FutureWarning" + assert filename == "" # attributed to the importing code, not to the shim + # plain words: the package is now arcadedb, that is the native build, the JVM build is arcadedb-jvm + assert ( + "The package is now 'arcadedb': install it with `uv add arcadedb`." in message + ) + assert "`arcadedb` is the native build, which runs without a JVM." in message + assert "This package was the JVM build, so for you the runtime changes." in message + assert "install `arcadedb-jvm` (`uv add arcadedb-jvm`;" in message + assert WHICH_PACKAGE_URL in message + assert f"`import {IMPORT_NAME}` where you wrote `import {module}`" in message + assert "https://github.com/ArcadeData/arcadedb-python" in message + assert "https://docs.arcadedb.com/arcadedb-python/" in message + + # with Python's default filters, with no -W option: shown once on stderr + shown = subprocess.run( # nosec B603 - fixed argument list, no shell + [ + sys.executable, + "-I", + "-c", + f"import sys; sys.path.insert(0, sys.argv[1]); import {module}; import {module}.core", + str(sites[dist]), + ], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + assert shown.returncode == 0, shown.stderr + assert shown.stderr.count("FutureWarning") == 1, shown.stderr + + +@pytest.mark.parametrize(("dist", "module"), CASES) +def test_attributes_and_submodules_come_from_the_new_module(dist, module, sites): + result = _probe(sites[dist], module) + assert result["same_class"] and result["same_function"] + assert result["version"] == VERSION + assert result["all"] == ["Database", "open_database"] + assert result["private"] == "p" + assert result["in_dir"] + # a submodule the new package already imported, one it has not, and a nested one: the same + # module objects as under `arcadedb`, never a second copy loaded from disk + assert result["core_alias"] and result["core_in_modules"] + assert result["lazy_alias"] and result["lazy_value"] == 42 + assert result["nested_alias"] + assert result["no_second_copy"] == [] + assert result["spec_kept"] == "arcadedb.extra" + assert "no attribute 'missing_name'" in result["missing_attr"] + assert result["missing_module"] is True + + +def test_the_real_package_has_no_warning(): + """The benchmark harness imports `arcadedb_embedded` from the real package until the split: it must stay quiet.""" + source = (BINDINGS / "src" / "arcadedb_embedded" / "__init__.py").read_text( + encoding="utf-8" + ) + assert "warnings" not in source and "FutureWarning" not in source