Skip to content

Python-module extension surface for safe-function namespace #248

@amc-corey-cox

Description

@amc-corey-cox

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions