Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/linkml_reference_validator/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""LinkML validation plugins for reference validation."""

from linkml_reference_validator.plugins.reference_validation_plugin import (
ReferenceValidationPlugin,
)
from importlib.util import find_spec

__all__ = ["ReferenceValidationPlugin"]
# `linkml` is an optional dependency. Expose the LinkML plugin only when LinkML is available.
__all__: list[str]
if find_spec("linkml") is not None and find_spec("linkml.validator") is not None:
from linkml_reference_validator.plugins.reference_validation_plugin import ( # noqa: F401
ReferenceValidationPlugin,
)

__all__ = ["ReferenceValidationPlugin"]
else:
__all__ = []
51 changes: 51 additions & 0 deletions tests/test_optional_linkml_dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Tests for running without optional `linkml` dependency installed."""

import importlib
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module 'importlib' is imported with both 'import' and 'import from'.

Copilot uses AI. Check for mistakes.
import sys
from importlib import util as importlib_util


def test_cli_imports_without_linkml(monkeypatch):
"""Importing the CLI should not require `linkml` to be installed."""

real_find_spec = importlib_util.find_spec

def fake_find_spec(name: str, *args, **kwargs): # type: ignore[no-untyped-def]
if name == "linkml" or name.startswith("linkml."):
return None
return real_find_spec(name, *args, **kwargs)

monkeypatch.setattr(importlib_util, "find_spec", fake_find_spec)

# Force a clean import of our package modules under the "no linkml" condition
for mod in list(sys.modules):
if mod.startswith("linkml_reference_validator"):
del sys.modules[mod]

cli = importlib.import_module("linkml_reference_validator.cli")
assert getattr(cli, "app", None) is not None


def test_plugins_package_imports_without_linkml(monkeypatch):
"""Importing `linkml_reference_validator.plugins` should not require `linkml`."""

real_find_spec = importlib_util.find_spec

def fake_find_spec(name: str, *args, **kwargs): # type: ignore[no-untyped-def]
if name == "linkml" or name.startswith("linkml."):
return None
return real_find_spec(name, *args, **kwargs)

monkeypatch.setattr(importlib_util, "find_spec", fake_find_spec)

for mod in list(sys.modules):
if mod.startswith("linkml_reference_validator"):
del sys.modules[mod]
Comment on lines +41 to +43
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this test might work correctly for testing the conditional import in plugins/init.py (since that code explicitly calls find_spec which is monkeypatched), the test could be more robust by also removing linkml modules from sys.modules. This would ensure the test works even if linkml was previously imported and cached, making the test results more reliable and less dependent on test execution order.

Copilot uses AI. Check for mistakes.

Comment on lines +7 to +44
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test appears to have multiple issues that prevent it from correctly verifying CLI imports work without linkml:

  1. The test only removes linkml_reference_validator modules from sys.modules (line 22-23) but doesn't remove linkml modules. If linkml was already imported by other tests (which it likely is, given conftest.py or other tests), the cached import will be used and the test will pass even though it shouldn't.

  2. More fundamentally, src/linkml_reference_validator/cli/validate.py has unconditional imports at the module level: from linkml.validator import Validator (line 8) and from linkml_reference_validator.plugins.reference_validation_plugin import ReferenceValidationPlugin (line 14-16). When linkml_reference_validator.cli.__init__.py imports from .validate, these imports will execute immediately and fail with ModuleNotFoundError if linkml is not available.

The test assertion that "Importing the CLI should not require linkml to be installed" cannot be true without also making the imports in validate.py conditional. To fix this test, validate.py would need to use conditional imports similar to what was done in plugins/init.py.

Suggested change
def test_cli_imports_without_linkml(monkeypatch):
"""Importing the CLI should not require `linkml` to be installed."""
real_find_spec = importlib_util.find_spec
def fake_find_spec(name: str, *args, **kwargs): # type: ignore[no-untyped-def]
if name == "linkml" or name.startswith("linkml."):
return None
return real_find_spec(name, *args, **kwargs)
monkeypatch.setattr(importlib_util, "find_spec", fake_find_spec)
# Force a clean import of our package modules under the "no linkml" condition
for mod in list(sys.modules):
if mod.startswith("linkml_reference_validator"):
del sys.modules[mod]
cli = importlib.import_module("linkml_reference_validator.cli")
assert getattr(cli, "app", None) is not None
def test_plugins_package_imports_without_linkml(monkeypatch):
"""Importing `linkml_reference_validator.plugins` should not require `linkml`."""
real_find_spec = importlib_util.find_spec
def fake_find_spec(name: str, *args, **kwargs): # type: ignore[no-untyped-def]
if name == "linkml" or name.startswith("linkml."):
return None
return real_find_spec(name, *args, **kwargs)
monkeypatch.setattr(importlib_util, "find_spec", fake_find_spec)
for mod in list(sys.modules):
if mod.startswith("linkml_reference_validator"):
del sys.modules[mod]
import pytest
def test_cli_imports_without_linkml():
"""Importing the CLI should not require `linkml` to be installed in environments where it is absent."""
# If `linkml` is installed in this test environment, we cannot reliably
# assert behavior "without linkml", so skip instead of trying to fake it.
if importlib_util.find_spec("linkml") is not None:
pytest.skip("`linkml` is installed; cannot verify behavior without it.")
# In an environment where `linkml` is truly absent, importing the CLI
# module should still succeed.
cli = importlib.import_module("linkml_reference_validator.cli")
assert cli is not None
def test_plugins_package_imports_without_linkml():
"""Importing `linkml_reference_validator.plugins` should not require `linkml` when it is not installed."""
if importlib_util.find_spec("linkml") is not None:
pytest.skip("`linkml` is installed; cannot verify behavior without it.")

Copilot uses AI. Check for mistakes.
plugins = importlib.import_module("linkml_reference_validator.plugins")
assert getattr(plugins, "__all__", None) == []





Comment on lines +47 to +51
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file has 4 trailing blank lines (lines 48-51). Python style guides typically recommend ending files with a single blank line.

Suggested change

Copilot uses AI. Check for mistakes.