Skip to content
24 changes: 15 additions & 9 deletions linkml_runtime/utils/compile_python.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import sys
from logging import warning
Expand All @@ -7,28 +9,32 @@
def file_text(txt_or_fname: str) -> str:
"""
Determine whether text_or_fname is a file name or a string and, if a file name, read it
:param txt_or_fname:
:return:

:param txt_or_fname: Text content or filename to read
:return: File content as string
"""
if len(txt_or_fname) > 4 and "\n" not in txt_or_fname:
with open(txt_or_fname) as ef:
return ef.read()
return txt_or_fname


def compile_python(text_or_fn: str, package_path: str = None) -> ModuleType:
def compile_python(text_or_fn: str, package_path: str | None = None, module_name: str | None = "test") -> ModuleType:
"""
Compile the text or file and return the resulting module
@param text_or_fn: Python text or file name that references python file
@param package_path: Root package path. If omitted and we've got a python file, the package is the containing
directory
@return: Compiled module

:param text_or_fn: Python text or file name that references python file
:param package_path: Root package path. If omitted and we've got a python file, the package is the containing directory
:param module_name: Used in an import statement, default 'test'
:return: Compiled module
"""
if not module_name:
module_name = "test"
python_txt = file_text(text_or_fn)
if package_path is None and python_txt != text_or_fn:
package_path = text_or_fn
spec = compile(python_txt, "test", "exec")
module = ModuleType("test")
spec = compile(python_txt, module_name, "exec")
module = ModuleType(module_name)
if package_path:
package_path_abs = os.path.join(os.getcwd(), package_path)
# We have to calculate the path to expected path relative to the current working directory
Expand Down
63 changes: 63 additions & 0 deletions tests/test_utils/test_compile_python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from types import ModuleType

import pytest

from linkml_runtime.utils.compile_python import compile_python


@pytest.fixture(scope="module")
def base_module() -> str:
return """
x: int = 2

def fun(value: int):
return f'known value {value}'
"""


@pytest.fixture(scope="module")
def importing_module() -> str:
return """
import MODULE_NAME as m

def more_fun(message: str):
return f'got "{message}"'
"""


def check_generated_module(module: ModuleType, module_name: str) -> None:
assert isinstance(module, ModuleType)
assert module.__name__ == module_name
assert module.x == 2
assert module.fun(3) == "known value 3"


@pytest.mark.parametrize(("name_arg", "module_name"), [(None, "test"), ("", "test"), ("base_module", "base_module")])
def test_compile_python_module_name(base_module: str, name_arg: str | None, module_name: str) -> None:
"""Test the compilation of python code to create a module."""
m = compile_python(base_module, module_name=name_arg)
check_generated_module(m, module_name)


@pytest.mark.parametrize(("name_arg", "module_name"), [(None, "test"), ("", "test"), ("base_module", "base_module")])
def test_compile_python_importing_module_local_module(
base_module: str,
importing_module: str,
name_arg: str | None,
module_name: str,
) -> None:
"""Test the compilation of python code to create a local module and then compile a second module that imports the first."""
m = compile_python(base_module, module_name=name_arg)
check_generated_module(m, module_name)

# switch in the appropriate module name
importing_module_text = importing_module.replace("MODULE_NAME", module_name)
m2 = compile_python(importing_module_text, package_path=".", module_name="module_2")
assert isinstance(m2, ModuleType)
assert m2.__name__ == "module_2"
assert m2.more_fun("hello") == 'got "hello"'

# check the imported module, m2.m, has the correct type, name, etc.
check_generated_module(m2.m, module_name)
Loading