Skip to content

Commit 6079c12

Browse files
Use constructor in TargetSpec, allow union in builder (#325)
A normal constructor can now be used with `TargetSpec`. Also, the builder config dict `target` accepts either a str or a prebuilt `Target Spec`. The stubtest uses the right `_core` module now. Since the `TargetSpec` is now part of the `EncoderfileConfig`, it needs to derive `JsonSchema`. Let me know if that would be an issue. From the top of my head I don't know if we currently test schema generation and check something on it. The `version` param takes `None` as default to make it consistent with the Rust-side option. However, it might be more appropriate to signal `'0.1.0'` as default, as it is the real value that will be set up when the default is used. A discussion for another day 😅
1 parent 3667104 commit 6079c12

7 files changed

Lines changed: 70 additions & 29 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ build-py:
1616
.PHONY: stubtest
1717
stubtest:
1818
@echo "Running stubtest..."
19-
@uv run --dev stubtest --allowlist=allowlist.txt encoderfile.encoderfile
19+
@uv run --dev stubtest --allowlist=allowlist.txt encoderfile._core
2020

2121
.PHONY: format
2222
format:

allowlist.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# ignore missing __all__ in encoderfile
2-
encoderfile.encoderfile.__all__
2+
encoderfile._core.__all__
33
# encoderfile.encoderfile.EncoderfileBuilder.from_config

encoderfile-py/python/encoderfile/encoderfile.pyi renamed to encoderfile-py/python/encoderfile/_core.pyi

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ class TargetSpec:
66
arch: str
77
os: str
88
abi: str
9-
10-
@staticmethod
11-
def parse(spec: str) -> "TargetSpec": ...
9+
def __new__(cls, spec: str): ...
1210

1311
@final
1412
class EncoderfileBuilder:
@@ -20,7 +18,7 @@ class EncoderfileBuilder:
2018
def from_dict(
2119
*,
2220
name: str,
23-
version: Optional[str] = "0.1.0",
21+
version: Optional[str] = None,
2422
model_type: ModelType,
2523
path: str,
2624
output_path: Optional[str] = None,
@@ -30,7 +28,7 @@ class EncoderfileBuilder:
3028
lua_libs: Optional[list[str]] = None,
3129
tokenizer: Optional[TokenizerBuildConfig] = None,
3230
validate_transform: bool = True,
33-
target: Optional[str] = None,
31+
target: Optional[str | TargetSpec] = None,
3432
) -> "EncoderfileBuilder": ...
3533
def build(
3634
self,

encoderfile-py/python/tests/test_encoderfile.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,37 @@ def test_encoderfilebuilder_build_all_from_dict(tmp_path):
197197
assert result.encoderfile_config.lua_libs == config.get("lua_libs")
198198

199199

200+
def test_encoderfilebuilder_with_target_spec_object(tmp_path):
201+
name = "model-built-from-config"
202+
target_spec = TargetSpec("x86_64-unknown-linux-musl")
203+
config = {
204+
"name": name,
205+
"path": "models/token_classification",
206+
"model_type": ModelType.TokenClassification,
207+
"output_path": str(tmp_path / f"{name}.encoderfile"),
208+
"tokenizer": TokenizerBuildConfig.new(
209+
pad_strategy="batch_longest",
210+
),
211+
"transform": """
212+
--- No docs
213+
---
214+
--- Args:
215+
--- arr (Tensor): A tensor of shape [batch_size, n_tokens, n_labels].
216+
--- The softmax is applied along the third axis (n_labels).
217+
---
218+
--- Returns:
219+
--- Tensor: The input tensor with softmax-normalized embeddings.
220+
---@param arr Tensor
221+
---@return Tensor
222+
function Postprocess(arr)
223+
return arr:softmax(3)
224+
end
225+
""",
226+
"target": target_spec,
227+
}
228+
EncoderfileBuilder.from_dict(**config)
229+
230+
200231
def test_encoderfilebuilder_wrong_arch(tmp_path):
201232
name = "model-built-from-config"
202233
config = {
@@ -224,11 +255,10 @@ def test_encoderfilebuilder_wrong_arch(tmp_path):
224255
""",
225256
"target": "nonsense!",
226257
}
227-
builder = EncoderfileBuilder.from_dict(**config)
228-
with pytest.raises(RuntimeError) as exc_info:
229-
builder.build(workdir=None, version=None, no_download=True)
258+
with pytest.raises(ValueError) as exc_info:
259+
EncoderfileBuilder.from_dict(**config)
230260
assert (
231-
f"Error building encoderfile: invalid or unsupported target triple `{config['target']}`"
261+
f"Failed to parse target spec: invalid or unsupported target triple `{config['target']}`"
232262
in str(exc_info.value)
233263
)
234264

@@ -383,23 +413,23 @@ def test_tokenizer_config_partial_optional_fields():
383413

384414
def test_parse_target_spec_valid_1():
385415
spec_str = "x86_64-unknown-linux-musl"
386-
target_spec = TargetSpec.parse(spec_str)
416+
target_spec = TargetSpec(spec_str)
387417
assert target_spec.arch == "x86_64"
388418
assert target_spec.os == "linux"
389419
assert target_spec.abi == "musl"
390420

391421

392422
def test_parse_target_spec_valid_2():
393423
spec_str = "aarch64-apple-darwin"
394-
target_spec = TargetSpec.parse(spec_str)
424+
target_spec = TargetSpec(spec_str)
395425
assert target_spec.arch == "aarch64"
396426
assert target_spec.os == "darwin"
397427
assert target_spec.abi == "gnu"
398428

399429

400430
def test_parse_target_spec_valid_3():
401431
spec_str = "x86_64-pc-windows-msvc"
402-
target_spec = TargetSpec.parse(spec_str)
432+
target_spec = TargetSpec(spec_str)
403433
assert target_spec.arch == "x86_64"
404434
assert target_spec.os == "windows"
405435
assert target_spec.abi == "msvc"
@@ -408,7 +438,7 @@ def test_parse_target_spec_valid_3():
408438
def test_parse_target_spec_invalid_format():
409439
spec_str = "nonsense!"
410440
with pytest.raises(ValueError) as exc_info:
411-
TargetSpec.parse(spec_str)
441+
TargetSpec(spec_str)
412442
assert (
413443
"Failed to parse target spec: invalid or unsupported target triple `nonsense!`"
414444
in str(exc_info.value)
@@ -418,7 +448,7 @@ def test_parse_target_spec_invalid_format():
418448
def test_parse_target_spec_unsupported_arch():
419449
spec_str = "riscv64-unknown-linux-musl"
420450
with pytest.raises(ValueError) as exc_info:
421-
TargetSpec.parse(spec_str)
451+
TargetSpec(spec_str)
422452
assert "Failed to parse target spec: unsupported architecture `riscv64`" in str(
423453
exc_info.value
424454
)

encoderfile-py/src/builder.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use encoderfile::builder::{
1313
},
1414
};
1515
use encoderfile::common::{Config, ModelConfig};
16+
use pyo3::types::PyString;
1617
use pyo3::{
1718
exceptions::{PyIOError, PyRuntimeError, PyValueError},
1819
prelude::*,
@@ -129,7 +130,7 @@ impl PyEncoderfileBuilder {
129130

130131
#[allow(clippy::too_many_arguments)]
131132
#[staticmethod]
132-
#[pyo3(signature = (*, name, version = Some("0.1.0"), model_type, path, output_path = None, cache_dir = None, base_binary_path = None, transform = None, lua_libs = None, tokenizer = None, validate_transform = true, target = None))]
133+
#[pyo3(signature = (*, name, version = None, model_type, path, output_path = None, cache_dir = None, base_binary_path = None, transform = None, lua_libs = None, tokenizer = None, validate_transform = true, target = None))]
133134
fn from_dict(
134135
name: String,
135136
version: Option<&str>,
@@ -142,7 +143,7 @@ impl PyEncoderfileBuilder {
142143
lua_libs: Option<Vec<String>>,
143144
tokenizer: Option<Bound<'_, PyTokenizerBuildConfig>>,
144145
validate_transform: bool,
145-
target: Option<String>,
146+
target: Option<&Bound<PyAny>>,
146147
) -> PyResult<Self> {
147148
let encoderfile = EncoderfileConfig {
148149
name,
@@ -158,7 +159,20 @@ impl PyEncoderfileBuilder {
158159
lua_libs: lua_libs.map(|libs| libs.into_iter().collect()),
159160
tokenizer: tokenizer.map(|t| t.borrow().0.clone()),
160161
validate_transform,
161-
target,
162+
target: target
163+
.map(|t| {
164+
if let Ok(spec) = t.cast::<PyString>() {
165+
PyTargetSpec::parse(spec.to_str()?)
166+
} else if let Ok(spec) = t.cast::<PyTargetSpec>() {
167+
Ok(PyTargetSpec(spec.get().0.clone()))
168+
} else {
169+
Err(PyRuntimeError::new_err(
170+
"Failed to parse target spec: expected either a string or a TargetSpec",
171+
))
172+
}
173+
})
174+
.transpose()?
175+
.map(|t| t.0),
162176
};
163177
Ok(PyEncoderfileBuilder(EncoderfileBuilder {
164178
config: BuildConfig { encoderfile },
@@ -264,11 +278,12 @@ impl PyEncoderfileConfig {
264278
}
265279

266280
#[pyclass(name = "TargetSpec", frozen)]
281+
#[derive(Clone)]
267282
pub struct PyTargetSpec(TargetSpec);
268283

269284
#[pymethods]
270285
impl PyTargetSpec {
271-
#[staticmethod]
286+
#[new]
272287
#[pyo3(signature = (spec))]
273288
fn parse(spec: &str) -> PyResult<Self> {
274289
spec.parse()

encoderfile/src/builder/base_binary/target_spec.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use anyhow::Result;
2+
use schemars::JsonSchema;
23
use serde::{Deserialize, Serialize};
34
use std::str::FromStr;
45

5-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6+
#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
67
pub struct TargetSpec {
78
pub arch: Architecture,
89
pub os: OperatingSystem,
@@ -125,7 +126,7 @@ impl std::fmt::Display for TargetSpec {
125126
}
126127
}
127128

128-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
129130
pub enum Architecture {
130131
X86_64,
131132
Aarch64,
@@ -140,7 +141,7 @@ impl std::fmt::Display for Architecture {
140141
}
141142
}
142143

143-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
144145
pub enum OperatingSystem {
145146
Linux,
146147
MacOS,
@@ -157,7 +158,7 @@ impl std::fmt::Display for OperatingSystem {
157158
}
158159
}
159160

160-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
161162
pub enum Abi {
162163
Gnu,
163164
Musl,

encoderfile/src/builder/config.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,12 @@ pub struct EncoderfileConfig {
5454
pub tokenizer: Option<TokenizerBuildConfig>,
5555
#[serde(default = "default_validate_transform")]
5656
pub validate_transform: bool,
57-
pub target: Option<String>,
57+
pub target: Option<TargetSpec>,
5858
}
5959

6060
impl EncoderfileConfig {
6161
pub fn target(&self) -> Result<Option<TargetSpec>> {
62-
self.target
63-
.as_ref()
64-
.map(|s| TargetSpec::from_str(s.as_str()))
65-
.transpose()
62+
Ok(self.target.clone())
6663
}
6764

6865
pub fn embedded_config(&self) -> Result<EmbeddedConfig> {

0 commit comments

Comments
 (0)