Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions recipes/safetensors/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package:
name: safetensors
version: "0.8.0"

build:
number: 1
script_env:
_PYTHON_SYSCONFIGDATA_NAME: '{sysconfigdata_name}'

test:
# numpy is an upstream extra (safetensors[numpy]), not a Requires-Dist, but
# the numpy/safe_open path is what downstream consumers (model2vec) use — so
# install it for the on-device test.
requires:
- numpy
69 changes: 69 additions & 0 deletions recipes/safetensors/tests/test_safetensors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import ctypes
import struct


def _spec(TensorSpec, data, shape):
# serialize() takes raw pointers (TensorSpec); a ctypes buffer keeps the
# memory alive for the duration of the call. Constructor takes
# python-style dtype names ("float32"); headers/output use safetensors
# format codes ("F32").
buf = ctypes.create_string_buffer(data, len(data))
spec = TensorSpec(
dtype="float32", shape=shape, data_ptr=ctypes.addressof(buf), data_len=len(data)
)
return spec, buf


def test_serialize_deserialize_roundtrip():
"""safetensors is a PyO3 wrapper around the Rust core. Round-trip a
tensor through the bytes-level API — no numpy/torch needed."""
from safetensors import TensorSpec, deserialize, serialize

data = struct.pack("<2f", 1.0, 2.0)
spec, buf = _spec(TensorSpec, data, [2])
blob = serialize({"emb": spec})
out = dict(deserialize(bytes(blob)))

assert out["emb"]["dtype"] == "F32"
assert list(out["emb"]["shape"]) == [2]
assert bytes(out["emb"]["data"]) == data


def test_serialize_file_roundtrip(tmp_path):
"""serialize_file writes through the Rust file path; numpy-free (the
recipe-tester app only ships the recipe's own hard deps, and safetensors
has none — safe_open needs a framework module, so it lives in the
numpy-gated test below)."""
from safetensors import TensorSpec, deserialize, serialize_file

data = struct.pack("<4f", 1.0, 2.0, 3.0, 4.0)
spec, buf = _spec(TensorSpec, data, [2, 2])
path = str(tmp_path / "weights.safetensors")
serialize_file({"w": spec}, path)

with open(path, "rb") as f:
out = dict(deserialize(f.read()))
assert list(out["w"]["shape"]) == [2, 2]
assert bytes(out["w"]["data"]) == data


def test_numpy_safe_open_roundtrip(tmp_path):
"""The numpy integration + mmap safe_open path is what downstream
consumers (model2vec) use."""
import numpy as np

from safetensors import safe_open
from safetensors.numpy import load, save_file

arr = np.arange(6, dtype=np.float32).reshape(2, 3)
path = str(tmp_path / "weights.safetensors")
save_file({"emb": arr}, path)

with safe_open(path, framework="numpy") as f:
assert f.keys() == ["emb"]
assert np.array_equal(f.get_tensor("emb"), arr)

with open(path, "rb") as fh:
import safetensors.numpy as st_np

assert np.array_equal(st_np.load(fh.read())["emb"], arr)
6 changes: 3 additions & 3 deletions tests/recipe-tester/pyproject.toml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Generic in-app pytest runner for mobile-forge recipe wheels."
requires-python = ">=3.10"

dependencies = [
"flet >=0.86.0",
"flet",
"pytest",
# `stage_recipe.sh` rewrites the line below to pin the recipe under test (e.g. `"numpy==2.2.2"`),
# and replaces the token line after it with any test-only deps declared in
Expand All @@ -16,13 +16,13 @@ dependencies = [

[dependency-groups]
dev = [
"flet[all] >=0.86.0",
"flet[all]",
]

[tool.flet]
artifact = "recipe-tester"

# Flet >=0.86 compiles the app to .pyc and strips the source by default, but pytest
# Flet 0.86 compiles the app to .pyc and strips the source by default, but pytest
# only collects .py test files — so the bundled recipe_tests would report "0 items"
# (EXIT 5). Keep the app source (main.py + recipe_tests/*.py) as .py. This only
# affects the app; bundled/package dependencies still get compiled.
Expand Down
Loading