Provide a path for downstream trans-spec authors to register custom functions in the safe-function namespace without writing a Python harness around linkml-map.
Design
Decorator-based discovery. Users write a plain Python module and tag functions:
from linkml_map.utils.extensions import safe_function
@safe_function
def slugify(s, separator="_"):
...
@safe_function(override=True) # explicit shadowing of a built-in
def lower(s):
...
@safe_function(distributes=False) # list-style function, opts out of scalar distribution
def my_aggregator(items):
...
The decorator is the membership marker. @safe_function is a declaration by the author that the function is bounded-time, pure, and free of I/O — linkml-map does not verify this. Same posture as typing.final.
Loader. load_extensions(paths) reads file paths, imports each via importlib.util.spec_from_file_location, walks the module for tagged functions, applies the _distributing wrapper to scalar ones, and returns the merged dict.
CLI. -F/--functions PATH repeatable on map-data. File paths only — same shape as --schema. Module-name discovery (e.g. pkg.mod) deferred until demand emerges.
Semantics.
- Collision between two extensions: error.
- Collision with a built-in: error unless the extension declared
override=True.
override=True declared but no matching built-in exists: warn (typo catcher; doesn't block).
- Missing function (typo or extension not loaded): error differentiates the two cases.
- No
-F flags + unknown name → Unknown function 'foo'. Did you forget --functions?
- With
-F flags + unknown name → Unknown function 'foo'. Not in built-ins or loaded extensions [a.py, b.py]. Did you mean: 'bar', 'baz'?
- Extension function raises at runtime: wrap with attribution (file + function name).
- Extension referenced in spec but not loaded at runtime: fail with clear exception (same wrapping pattern). Convention: trans-specs note required extensions in a header comment. A declarative `required_extensions:` YAML key is a likely follow-up but out of scope here.
Scope. CLI flag + Python kwarg on `ObjectTransformer`. Python users replicate the CLI's loader call.
Where it lives. New module `src/linkml_map/utils/extensions.py`. Keeps `eval_utils.py` from accreting.
Docs (ship with the PR)
- Extension contract — pure, bounded-time, deterministic, no I/O, author-declared safe.
- When NOT to use extensions — explicit anti-pattern: don't escape the declarative model. Extensions are for named atomic operations that read cleaner as a name than as an expression chain. Not for putting transformation logic in Python.
- Convention — specs requiring extensions document it in a top-level header comment.
Out of scope (deferred)
- `required_extensions:` YAML key for declarative dependency on extensions. Want this eventually; not yet.
- Module-name path syntax (`pkg.module`) in addition to file paths.
- `--allow-override` CLI flag — the decorator's `override=True` is sufficient declaration.
Origin: discussion during #242. Python API already accepts `functions=` at `src/linkml_map/utils/eval_utils.py:558`; this exposes that cleanly to non-Python trans-spec authors while preserving the safety boundary.
Provide a path for downstream trans-spec authors to register custom functions in the safe-function namespace without writing a Python harness around linkml-map.
Design
Decorator-based discovery. Users write a plain Python module and tag functions:
The decorator is the membership marker.
@safe_functionis a declaration by the author that the function is bounded-time, pure, and free of I/O — linkml-map does not verify this. Same posture astyping.final.Loader.
load_extensions(paths)reads file paths, imports each viaimportlib.util.spec_from_file_location, walks the module for tagged functions, applies the_distributingwrapper to scalar ones, and returns the merged dict.CLI.
-F/--functions PATHrepeatable onmap-data. File paths only — same shape as--schema. Module-name discovery (e.g.pkg.mod) deferred until demand emerges.Semantics.
override=True.override=Truedeclared but no matching built-in exists: warn (typo catcher; doesn't block).-Fflags + unknown name →Unknown function 'foo'. Did you forget --functions?-Fflags + unknown name →Unknown function 'foo'. Not in built-ins or loaded extensions [a.py, b.py]. Did you mean: 'bar', 'baz'?Scope. CLI flag + Python kwarg on `ObjectTransformer`. Python users replicate the CLI's loader call.
Where it lives. New module `src/linkml_map/utils/extensions.py`. Keeps `eval_utils.py` from accreting.
Docs (ship with the PR)
Out of scope (deferred)
Origin: discussion during #242. Python API already accepts `functions=` at `src/linkml_map/utils/eval_utils.py:558`; this exposes that cleanly to non-Python trans-spec authors while preserving the safety boundary.