diff --git a/py/src/clippy/__init__.py b/py/src/clippy/__init__.py index aae2f34..b3dabcc 100644 --- a/py/src/clippy/__init__.py +++ b/py/src/clippy/__init__.py @@ -29,6 +29,11 @@ logger.setLevel(cfg.get("loglevel")) +def _register_classes(generated_classes: AnyDict) -> None: + """Expose backend-generated classes in the clippy namespace.""" + globals().update(generated_classes) + + def load_classes(): """For each listed backend, import the module of the same name. The backend should expose two functions: a classes() function that returns @@ -40,12 +45,7 @@ def load_classes(): for backend in cfg.get("backends"): b = importlib.import_module(f".backends.{backend}", package=__name__) setattr(cfg, backend, b.get_cfg()) - for name, c in b.classes().items(): - # backend_config = importlib.import_module(f".backends.{name}.config.{name}_config") - # for k, v in backend_config.items(): - # cfg._set(k, v) - - globals()[name] = c + _register_classes(b.classes()) load_classes() diff --git a/py/src/clippy/backends/fs/__init__.py b/py/src/clippy/backends/fs/__init__.py index 2cfde7d..f94c11e 100644 --- a/py/src/clippy/backends/fs/__init__.py +++ b/py/src/clippy/backends/fs/__init__.py @@ -60,28 +60,37 @@ def get_cfg() -> CLIPPY_CONFIG: return cfg -def classes() -> dict[str, Any]: +def classes(paths: list[str] | None = None) -> dict[str, Any]: """This is a mandatory function for all backends. It returns a dictionary of class name to the actual Class for all classes supported by the backend.""" from ... import cfg as topcfg # pylint: disable=import-outside-toplevel - paths = cfg.get("fs_backend_paths") + if paths is None: + paths = cfg.get("fs_backend_paths") + _classes = {} # iterate over all (filesystem) paths and for those that aren't explicitly excluded, # create the class based on the directory name and add methods based on the executables # within the directory. for path in paths: - files = os.scandir(path) - for f in files: - if f.name in cfg.get("fs_exclude_paths"): - continue - p = pathlib.Path(path, f) - if os.path.isdir(p): - _cls = _create_class(f.name, path, topcfg) - _classes[f.name] = _cls + with os.scandir(path) as files: + for f in files: + if f.name in cfg.get("fs_exclude_paths"): + continue + p = pathlib.Path(path, f) + if os.path.isdir(p): + _cls = _create_class(f.name, path, topcfg) + _classes[f.name] = _cls return _classes +def add_backend_path(path: str) -> None: + """Generate and expose classes found in one filesystem backend path.""" + from ... import _register_classes # pylint: disable=import-outside-toplevel + + _register_classes(classes(paths=[path])) + + def _create_class(name: str, path: str, topcfg: CLIPPY_CONFIG): """Given a name, a path, and a master configuration, create a class with the given name, and add methods based on the diff --git a/py/src/clippy/backends/fs/config.py b/py/src/clippy/backends/fs/config.py index ef3ceec..9e8d7af 100644 --- a/py/src/clippy/backends/fs/config.py +++ b/py/src/clippy/backends/fs/config.py @@ -5,7 +5,7 @@ # backend path for executables, in addition to the CLIPPY_BACKEND_PATH environment variable. Add to it here. "fs_backend_paths": ( None, - [x.strip() for x in os.environ.get("CLIPPY_BACKEND_PATH", "").split(":")], + [path for entry in os.environ.get("CLIPPY_BACKEND_PATH", "").split(":") if (path := entry.strip())], ), # exclude these directories from being made as classes. "fs_exclude_paths": ("CLIPPY_FS_EXCLUDE_PATHS", ["CMakeFiles"]), diff --git a/test/test_clippy.py b/test/test_clippy.py index a274032..2ba1039 100644 --- a/test/test_clippy.py +++ b/test/test_clippy.py @@ -1,5 +1,7 @@ import pytest import sys +import os +import subprocess sys.path.append("src") @@ -8,6 +10,7 @@ from clippy.error import ClippyValidationError, ClippyInvalidSelectorError from clippy.backends.fs.execution import NonZeroReturnCodeError import logging +from clippy.backends import fs clippy.logger.setLevel(logging.WARN) logging.getLogger().setLevel(logging.WARN) @@ -45,6 +48,41 @@ def test_imports(): assert "ExampleBag" in clippy.__dict__ +def test_import_without_backend_path(): + env = os.environ.copy() + env.pop("CLIPPY_BACKEND_PATH", None) + + result = subprocess.run( + [sys.executable, "-c", "import clippy"], + check=False, + capture_output=True, + env=env, + text=True, + ) + + assert result.returncode == 0, result.stderr + + +def test_add_backend_path_scans_only_explicit_path(monkeypatch): + class AddedClass: + pass + + calls = [] + + def classes(*, paths=None): + calls.append(paths) + return {"AddedClass": AddedClass} + + monkeypatch.setattr(fs, "classes", classes) + monkeypatch.delattr(clippy, "AddedClass", raising=False) + + fs.add_backend_path("/foo/bar") + fs.add_backend_path("/foo/bar") + + assert calls == [["/foo/bar"], ["/foo/bar"]] + assert clippy.AddedClass is AddedClass + + def test_bag(examplebag): examplebag.insert(41) assert examplebag.size() == 1