Skip to content

Commit c05e250

Browse files
authored
test: add tests of Python consuming JSON metadata from rust (#749)
1 parent 158aa1c commit c05e250

File tree

6 files changed

+205
-0
lines changed

6 files changed

+205
-0
lines changed

.github/workflows/python.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
on:
2+
push:
3+
branches: [main, dev]
4+
pull_request:
5+
6+
name: Python data round trips
7+
8+
jobs:
9+
test-metadata:
10+
name: Test Python metadata round trips
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [ubuntu-24.04]
15+
rust:
16+
- stable
17+
python: [ "3.13" ]
18+
steps:
19+
- name: Cancel Previous Runs
20+
uses: styfle/[email protected]
21+
with:
22+
access_token: ${{ secrets.GITHUB_TOKEN }}
23+
24+
- uses: actions/[email protected]
25+
with:
26+
token: ${{ secrets.GITHUB_TOKEN }}
27+
submodules: recursive
28+
- uses: dtolnay/rust-toolchain@v1
29+
with:
30+
toolchain: ${{ matrix.rust }}
31+
- uses: Swatinem/[email protected]
32+
- name: Install the latest version of uv
33+
uses: astral-sh/setup-uv@v6
34+
with:
35+
activate-environment: true
36+
version: "latest"
37+
python-version: ${{ matrix.python }}
38+
- name: run JSON metadata example
39+
run: |
40+
cargo run --example json_metadata --features derive
41+
- name: setup Python and run tests
42+
run: |
43+
uv venv -p ${{ matrix.python }}
44+
source .venv/bin/activate
45+
uv pip install -r python/requirements_locked_3_13.txt
46+
python -m pytest python
47+

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,7 @@ rustdoc-args = ["--cfg", "doc_cfg"]
5656
# Not run during tests
5757
[[example]]
5858
name = "tree_traversals"
59+
60+
[[example]]
61+
name = "json_metadata"
62+
required-features = ["derive"]

examples/json_metadata.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#[derive(serde::Serialize, serde::Deserialize, tskit::metadata::MutationMetadata)]
2+
#[serializer("serde_json")]
3+
struct MutationMetadata {
4+
effect_size: f64,
5+
dominance: f64,
6+
}
7+
8+
#[derive(serde::Serialize, serde::Deserialize, tskit::metadata::IndividualMetadata)]
9+
#[serializer("serde_json")]
10+
struct IndividualMetadata {
11+
name: String,
12+
phenotypes: Vec<i32>,
13+
}
14+
15+
fn main() {
16+
let ts = make_treeseq().unwrap();
17+
ts.dump("with_json_metadata.trees", 0).unwrap();
18+
}
19+
20+
fn make_tables() -> anyhow::Result<tskit::TableCollection> {
21+
let mut tables = tskit::TableCollection::new(100.0)?;
22+
let pop0 = tables.add_population()?;
23+
let ind0 = tables.add_individual_with_metadata(
24+
0,
25+
None,
26+
None,
27+
&IndividualMetadata {
28+
name: "Jerome".to_string(),
29+
phenotypes: vec![0, 1, 2, 0],
30+
},
31+
)?;
32+
let node0 = tables.add_node(tskit::NodeFlags::new_sample(), 0.0, pop0, ind0)?;
33+
let site0 = tables.add_site(50.0, Some("A".as_bytes()))?;
34+
let _ = tables.add_mutation_with_metadata(
35+
site0,
36+
node0,
37+
tskit::MutationId::NULL,
38+
1.0,
39+
Some("G".as_bytes()),
40+
&MutationMetadata {
41+
effect_size: -1e-3,
42+
dominance: 0.1,
43+
},
44+
)?;
45+
tables.build_index()?;
46+
Ok(tables)
47+
}
48+
49+
fn make_treeseq() -> anyhow::Result<tskit::TreeSequence> {
50+
Ok(make_tables()?.tree_sequence(0)?)
51+
}

python/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tskit>=0.6.3
2+
pytest

python/requirements_locked_3_13.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file was autogenerated by uv via the following command:
2+
# uv pip compile -p 3.13 requirements.txt
3+
attrs==25.3.0
4+
# via
5+
# jsonschema
6+
# referencing
7+
iniconfig==2.1.0
8+
# via pytest
9+
jsonschema==4.23.0
10+
# via tskit
11+
jsonschema-specifications==2025.4.1
12+
# via jsonschema
13+
numpy==2.2.5
14+
# via tskit
15+
packaging==25.0
16+
# via pytest
17+
pluggy==1.6.0
18+
# via pytest
19+
pytest==8.3.5
20+
# via -r requirements.txt
21+
referencing==0.36.2
22+
# via
23+
# jsonschema
24+
# jsonschema-specifications
25+
rpds-py==0.25.0
26+
# via
27+
# jsonschema
28+
# referencing
29+
tskit==0.6.3
30+
# via -r requirements.txt

python/test_json_metadata.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import tskit
2+
import numpy as np
3+
4+
5+
def setup_ts_without_schema():
6+
ts = tskit.TreeSequence.load("with_json_metadata.trees")
7+
return ts
8+
9+
10+
def setup_ts_with_schema():
11+
ts = setup_ts_without_schema()
12+
tables = ts.tables
13+
tables.individuals.metadata_schema = tskit.metadata.MetadataSchema(
14+
{
15+
"codec": "json",
16+
"type": "object",
17+
"name": "Individual metadata",
18+
"properties": {"name": {"type": "string"},
19+
"phenotypes": {"type": "array"}},
20+
"additionalProperties": False,
21+
})
22+
tables.mutations.metadata_schema = tskit.metadata.MetadataSchema(
23+
{
24+
"codec": "json",
25+
"type": "object",
26+
"name": "Individual metadata",
27+
"properties": {"effect_size": {"type": "number"},
28+
"dominance": {"type": "number"}},
29+
"additionalProperties": False,
30+
})
31+
return tables.tree_sequence()
32+
33+
34+
def test_individual_metadata():
35+
# NOTE: the assertions here rely on knowing
36+
# what examples/json_metadata.rs put into the
37+
# metadata!
38+
ts = setup_ts_with_schema()
39+
md = ts.individual(0).metadata
40+
assert md["name"] == "Jerome"
41+
assert md["phenotypes"] == [0, 1, 2, 0]
42+
43+
44+
def test_individual_metadata_without_schema():
45+
# NOTE: the assertions here rely on knowing
46+
# what examples/json_metadata.rs put into the
47+
# metadata!
48+
ts = setup_ts_without_schema()
49+
md = eval(ts.individual(0).metadata)
50+
assert md["name"] == "Jerome"
51+
assert md["phenotypes"] == [0, 1, 2, 0]
52+
53+
54+
def test_mutation_metadata():
55+
# NOTE: the assertions here rely on knowing
56+
# what examples/json_metadata.rs put into the
57+
# metadata!
58+
ts = setup_ts_with_schema()
59+
md = ts.mutation(0).metadata
60+
assert np.isclose(md["effect_size"], -1e-3)
61+
assert np.isclose(md["dominance"], 0.1)
62+
63+
64+
def test_mutation_metadata_without_schema():
65+
# NOTE: the assertions here rely on knowing
66+
# what examples/json_metadata.rs put into the
67+
# metadata!
68+
ts = setup_ts_without_schema()
69+
md = eval(ts.mutation(0).metadata)
70+
assert np.isclose(md["effect_size"], -1e-3)
71+
assert np.isclose(md["dominance"], 0.1)

0 commit comments

Comments
 (0)