Skip to content
Open
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
13 changes: 11 additions & 2 deletions fromconfig/launcher/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Base class for launchers."""

from abc import ABC
import sys
from typing import Any, Dict, Type
import inspect
import logging
import pkg_resources
try:
from importlib.metadata import entry_points
except ImportError:
from importlib_metadata import entry_points

from fromconfig.core.base import fromconfig, FromConfig, Keys
from fromconfig.utils.libimport import from_import_string
Expand Down Expand Up @@ -136,8 +140,13 @@ def _load():
_CLASSES["parser"] = ParserLauncher
_CLASSES["dry"] = DryLauncher

if sys.version_info >= (3, 10):
eps = entry_points(group=f"fromconfig{__major__}")
else:
eps = entry_points().get(f"fromconfig{__major__}", [])

# Load external classes, use entry point's name for reference
for entry_point in pkg_resources.iter_entry_points(f"fromconfig{__major__}"):
for entry_point in eps:
module = entry_point.load()
for _, cls in inspect.getmembers(module, lambda m: inspect.isclass(m) and issubclass(m, Launcher)):
name = f"{entry_point.name}.{cls.NAME}" if hasattr(cls, "NAME") else entry_point.name
Expand Down
12 changes: 6 additions & 6 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
black==22.6.0
mypy==0.790
pylint==2.6.0
pytest-cov==2.10.1
pytest-xdist==2.1.0
pytest==6.1.2
black==25.11.0
mypy==1.19.0
pylint==4.0.4
pytest-cov==7.0.0
pytest-xdist==3.8.0
pytest==9.0.1
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fire==0.4.0
jsonnet==0.17.0
PyYAML==5.4.1
omegaconf==2.1.1
fire==0.7.1
jsonnet==0.21.0
PyYAML==6.0.3
omegaconf==2.3.0
3 changes: 3 additions & 0 deletions tests/unit/cli/test_cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ def capture(command):
"""Utility to execute and capture the result of a command."""
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
# Normalize line endings for cross-platform compatibility (Windows uses \r\n)
out = out.replace(b'\r\n', b'\n')
err = err.replace(b'\r\n', b'\n')
return out, err, proc.returncode


Expand Down
30 changes: 24 additions & 6 deletions tests/unit/launcher/test_launcher_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""Test launcher.base."""

import pytest
import pkg_resources
import sys
from typing import Any

import fromconfig
Expand Down Expand Up @@ -46,25 +46,43 @@ def test_launcher_classes(name, expected):

def test_launcher_classes_extension(monkeypatch):
"""Test launcher classes extension."""

fromconfig.launcher.base._CLASSES.clear() # Clear internal and external launchers

class DummyModule:
class DummyLauncher(fromconfig.launcher.Launcher):
def __call__(self, config: Any, command: str = ""):
...

class EntryPoint:
class MockEntryPoint:
def __init__(self, name, module):
self.name = name
self.module = module

def load(self):
return self.module

# Test discovery
monkeypatch.setattr(pkg_resources, "iter_entry_points", lambda *_: [EntryPoint("dummy", DummyModule)])
mock_called = {"called": False}

def mock_entry_points(group=None):
mock_called["called"] = True
# For Python 3.10+
if sys.version_info >= (3, 10):
# Return list directly when called with group parameter
return [MockEntryPoint("dummy", DummyModule)]
else:
# For Python 3.8-3.9, return dict-like object
class MockEntryPoints:
def get(self, group_name, default=None):
return [MockEntryPoint("dummy", DummyModule)]
return MockEntryPoints()

monkeypatch.setattr("fromconfig.launcher.base.entry_points", mock_entry_points)

fromconfig.launcher.base._load()

assert mock_called["called"], "Mock entry_points was never called"

fromconfig.utils.testing.assert_launcher_is_discovered("dummy", DummyModule.DummyLauncher)
assert fromconfig.launcher.base._classes()["dummy"] == DummyModule.DummyLauncher

Expand Down Expand Up @@ -110,4 +128,4 @@ def test_launcher_fromconfig(config, expected):
assert_equal_launcher(fromconfig.launcher.Launcher.fromconfig(config), expected)
else:
with pytest.raises(expected):
fromconfig.launcher.Launcher.fromconfig(config)
fromconfig.launcher.Launcher.fromconfig(config)