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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- Python (bindings helpers): follow PEP 8; prefer type hints where feasible.

## Testing Guidelines
- When testing locally, always provide network access. never try to run the tests offline
- Add integration tests under `src/runtime/tests/` when changing CLI/runtime behavior.
- Prefer `assert_cmd` for CLI and `predicates` for output checks. Keep fixtures in `src/runtime/tests/data/`.
- Run `cargo test --workspace` locally; ensure tests don’t rely on network input.
Expand Down
39 changes: 35 additions & 4 deletions src/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,12 @@ impl Clone for PyLinkMLValue {

#[pymethods]
impl PyLinkMLValue {
/// Semantic equality per LinkML Instances spec.
/// Compares this value with another `LinkMLValue`.
#[pyo3(signature = (other, treat_missing_as_null = false))]
fn equals(&self, other: &PyLinkMLValue, treat_missing_as_null: bool) -> bool {
self.value.equals(&other.value, treat_missing_as_null)
}
#[getter]
fn slot_name(&self) -> Option<String> {
match &self.value {
Expand All @@ -499,6 +505,11 @@ impl PyLinkMLValue {
}
}

#[getter]
fn node_id(&self) -> u64 {
self.value.node_id()
}

#[getter]
fn slot_definition(&self) -> Option<SlotDefinition> {
match &self.value {
Expand Down Expand Up @@ -753,20 +764,40 @@ fn py_diff(
Ok(json_value_to_py(py, &JsonValue::Array(vals)))
}

#[pyfunction(name = "patch")]
#[pyfunction(name = "patch", signature = (source, deltas, treat_missing_as_null = true, ignore_no_ops = true))]
fn py_patch(
py: Python<'_>,
source: &PyLinkMLValue,
deltas: &Bound<'_, PyAny>,
) -> PyResult<PyLinkMLValue> {
treat_missing_as_null: bool,
ignore_no_ops: bool,
) -> PyResult<PyObject> {
let json_mod = PyModule::import(py, "json")?;
let deltas_str: String = json_mod.call_method1("dumps", (deltas,))?.extract()?;
let deltas_vec: Vec<Delta> =
serde_json::from_str(&deltas_str).map_err(|e| PyException::new_err(e.to_string()))?;
let sv_ref = source.sv.bind(py).borrow();
let rust_sv = sv_ref.as_rust();
let new_value = patch_internal(&source.value, &deltas_vec, rust_sv);
Ok(PyLinkMLValue::new(new_value, source.sv.clone_ref(py)))
let (new_value, trace) = patch_internal(
&source.value,
&deltas_vec,
rust_sv,
linkml_runtime::diff::PatchOptions {
ignore_no_ops,
treat_missing_as_null,
},
)
.map_err(|e| PyException::new_err(e.to_string()))?;
let trace_json = serde_json::json!({
"added": trace.added,
"deleted": trace.deleted,
"updated": trace.updated,
});
let py_val = PyLinkMLValue::new(new_value, source.sv.clone_ref(py));
let dict = pyo3::types::PyDict::new(py);
dict.set_item("value", Py::new(py, py_val)?)?;
dict.set_item("trace", json_value_to_py(py, &trace_json))?;
Ok(dict.into_any().unbind())
}

#[pyfunction(name = "to_turtle", signature = (value, skolem=None))]
Expand Down
75 changes: 75 additions & 0 deletions src/python/tests/python_equals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use linkml_runtime_python::runtime_module;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use std::path::PathBuf;

fn data_path(name: &str) -> PathBuf {
let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let candidates = [
base.join("../runtime/tests/data").join(name),
base.join("../schemaview/tests/data").join(name),
base.join("tests/data").join(name),
];
for c in candidates {
if c.exists() {
return c;
}
}
panic!("test data not found: {}", name);
}

#[test]
fn python_equals_api() {
pyo3::prepare_freethreaded_python();
Python::with_gil(|py| {
let module = PyModule::new(py, "linkml_runtime").unwrap();
runtime_module(&module).unwrap();
let sys = py.import("sys").unwrap();
let modules = sys.getattr("modules").unwrap();
let sys_modules = modules.downcast::<PyDict>().unwrap();
sys_modules.set_item("linkml_runtime", module).unwrap();

let locals = PyDict::new(py);
locals
.set_item(
"schema_path",
data_path("personinfo.yaml").to_str().unwrap(),
)
.unwrap();

pyo3::py_run!(
py,
*locals,
r#"
import linkml_runtime as lr
import json
sv = lr.make_schema_view(schema_path)
cls = sv.get_class_view('Container')

doc1 = {
'objects': [
{
'objecttype': 'personinfo:Person',
'id': 'P:1',
'name': 'Alice',
'current_address': None
}
]
}
doc2 = {
'objects': [
{
'objecttype': 'personinfo:Person',
'id': 'P:1',
'name': 'Alice'
}
]
}

v1 = lr.load_json(json.dumps(doc1), sv, cls)
v2 = lr.load_json(json.dumps(doc2), sv, cls)
assert v1['objects'][0].equals(v2['objects'][0], True)
"#
);
});
}
Loading
Loading