From 81d79cdeacf807de649bbe8a79fe9ff741e805b5 Mon Sep 17 00:00:00 2001 From: Nico Matentzoglu Date: Sat, 28 Mar 2026 10:40:13 +0200 Subject: [PATCH 1/2] Fix expr slot derivations resolving to None when source class name contains a space dynamic_object() created Python types using the raw class name (e.g. "my record"), but ObjectIndex.class_name_mappings() CamelCases keys (e.g. "MyRecord"). When ObjectIndex.bless() looked up type(obj).__name__, the mismatch caused a KeyError, silently returning None for all expr-based slot derivations. Apply camelcase() to the type name in dynamic_object() so it matches ObjectIndex's class map. The populated_from path was unaffected because it reads directly from the source dict without going through ObjectIndex. --- src/linkml_map/utils/dynamic_object.py | 3 +- .../test_class_name_spaces.py | 132 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 tests/test_transformer/test_class_name_spaces.py diff --git a/src/linkml_map/utils/dynamic_object.py b/src/linkml_map/utils/dynamic_object.py index f6752a3..467a87c 100644 --- a/src/linkml_map/utils/dynamic_object.py +++ b/src/linkml_map/utils/dynamic_object.py @@ -3,6 +3,7 @@ from typing import Any from linkml_runtime import SchemaView +from linkml_runtime.utils.formatutils import camelcase class DynObj: @@ -47,5 +48,5 @@ def dynamic_object(obj: dict, sv: SchemaView, target: str): else: v = dynamic_object(v, sv, rng) attrs[k] = v - cls = type(target, (DynObj,), {}) + cls = type(camelcase(target), (DynObj,), {}) return cls(**attrs) diff --git a/tests/test_transformer/test_class_name_spaces.py b/tests/test_transformer/test_class_name_spaces.py new file mode 100644 index 0000000..d7c9421 --- /dev/null +++ b/tests/test_transformer/test_class_name_spaces.py @@ -0,0 +1,132 @@ +"""Regression test for expr with spaces in source class names. + +When the source class name contains a space (e.g. "my record"), expr-based +slot derivations should still be able to access source object fields. + +See: https://github.com/linkml/linkml-map/issues/170 +""" + +import textwrap + +import pytest +import yaml +from linkml_runtime import SchemaView + +from linkml_map.transformer.object_transformer import ObjectTransformer + +SOURCE_SCHEMA = textwrap.dedent("""\ + id: https://example.org/source + name: source + prefixes: + linkml: https://w3id.org/linkml/ + imports: + - linkml:types + default_range: string + + classes: + my record: + attributes: + record_id: + identifier: true + label: {} + source_ref: {} +""") + +TARGET_SCHEMA = textwrap.dedent("""\ + id: https://example.org/target + name: target + prefixes: + linkml: https://w3id.org/linkml/ + imports: + - linkml:types + default_range: string + + classes: + Output: + attributes: + id: {} + name: {} + source: {} +""") + +INPUT_OBJ = { + "record_id": "rec-001", + "label": "Hello World", + "source_ref": "db:12345", +} + + +def _make_transformer(transform_spec: dict) -> ObjectTransformer: + """Build an ObjectTransformer from inline schemas and a transform dict.""" + transform_spec["source_schema"] = "https://example.org/source" + transform_spec["target_schema"] = "https://example.org/target" + tr = ObjectTransformer() + tr.source_schemaview = SchemaView(SOURCE_SCHEMA) + tr.target_schemaview = SchemaView(TARGET_SCHEMA) + tr.create_transformer_specification(transform_spec) + return tr + + +def test_expr_with_space_in_class_name_no_index(): + """expr should resolve source fields when class name has a space (no index).""" + transform = { + "class_derivations": { + "Output": { + "populated_from": "my record", + "slot_derivations": { + "id": {"populated_from": "record_id"}, + "name": {"expr": "label"}, + "source": {"expr": "source_ref"}, + }, + } + }, + } + tr = _make_transformer(transform) + result = tr.map_object(INPUT_OBJ, "my record") + + assert result["id"] == "rec-001" + assert result["name"] == "Hello World" + assert result["source"] == "db:12345" + + +def test_expr_with_space_in_class_name_with_index(): + """expr should resolve source fields when class name has a space (with index).""" + transform = { + "class_derivations": { + "Output": { + "populated_from": "my record", + "slot_derivations": { + "id": {"populated_from": "record_id"}, + "name": {"expr": "label"}, + "source": {"expr": "source_ref"}, + }, + } + }, + } + tr = _make_transformer(transform) + tr.index(INPUT_OBJ, "my record") + result = tr.map_object(INPUT_OBJ, "my record") + + assert result["id"] == "rec-001" + assert result["name"] == "Hello World" + assert result["source"] == "db:12345" + + +def test_expr_string_concat_with_space_in_class_name(): + """expr with string operations should work when class name has a space.""" + transform = { + "class_derivations": { + "Output": { + "populated_from": "my record", + "slot_derivations": { + "id": {"populated_from": "record_id"}, + "name": {"expr": "label + ' (copy)'"}, + "source": {"expr": "source_ref"}, + }, + } + }, + } + tr = _make_transformer(transform) + result = tr.map_object(INPUT_OBJ, "my record") + + assert result["name"] == "Hello World (copy)" From 7bab1072e086dfd8dbe45a91de24d27b6d86c738 Mon Sep 17 00:00:00 2001 From: Nico Matentzoglu Date: Sat, 28 Mar 2026 11:01:30 +0200 Subject: [PATCH 2/2] remove unused imports --- tests/test_transformer/test_class_name_spaces.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_transformer/test_class_name_spaces.py b/tests/test_transformer/test_class_name_spaces.py index d7c9421..7efd77e 100644 --- a/tests/test_transformer/test_class_name_spaces.py +++ b/tests/test_transformer/test_class_name_spaces.py @@ -8,8 +8,6 @@ import textwrap -import pytest -import yaml from linkml_runtime import SchemaView from linkml_map.transformer.object_transformer import ObjectTransformer