From 25534085c0ed4bd65ddcbbcb0a40d4dad2cffce8 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Mon, 3 Aug 2026 20:55:01 +0000 Subject: [PATCH 1/2] add_backend_path --- py/src/clippy/__init__.py | 8 ++++++ py/src/clippy/backends/fs/__init__.py | 22 +++++++++------- py/src/clippy/backends/fs/config.py | 2 +- test/test_clippy.py | 38 +++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/py/src/clippy/__init__.py b/py/src/clippy/__init__.py index aae2f34..43dfc1f 100644 --- a/py/src/clippy/__init__.py +++ b/py/src/clippy/__init__.py @@ -48,4 +48,12 @@ def load_classes(): globals()[name] = c +def add_backend_path(path: str) -> None: + """Generate and expose classes found in one filesystem backend path.""" + from .backends import fs # pylint: disable=import-outside-toplevel + + for name, generated_class in fs.classes(paths=[path]).items(): + globals()[name] = generated_class + + load_classes() diff --git a/py/src/clippy/backends/fs/__init__.py b/py/src/clippy/backends/fs/__init__.py index 2cfde7d..0047e95 100644 --- a/py/src/clippy/backends/fs/__init__.py +++ b/py/src/clippy/backends/fs/__init__.py @@ -60,25 +60,27 @@ 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 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..5149172 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) + + clippy.add_backend_path("/foo/bar") + clippy.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 From 9647c8743cbc4b0bb2c4bfdc808d7998322bcb87 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Mon, 3 Aug 2026 20:57:36 +0000 Subject: [PATCH 2/2] moved to backends.fs --- py/src/clippy/__init__.py | 20 ++++++-------------- py/src/clippy/backends/fs/__init__.py | 7 +++++++ test/test_clippy.py | 4 ++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/py/src/clippy/__init__.py b/py/src/clippy/__init__.py index 43dfc1f..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,20 +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 - - -def add_backend_path(path: str) -> None: - """Generate and expose classes found in one filesystem backend path.""" - from .backends import fs # pylint: disable=import-outside-toplevel - - for name, generated_class in fs.classes(paths=[path]).items(): - globals()[name] = generated_class + _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 0047e95..f94c11e 100644 --- a/py/src/clippy/backends/fs/__init__.py +++ b/py/src/clippy/backends/fs/__init__.py @@ -84,6 +84,13 @@ def classes(paths: list[str] | None = None) -> dict[str, Any]: 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/test/test_clippy.py b/test/test_clippy.py index 5149172..2ba1039 100644 --- a/test/test_clippy.py +++ b/test/test_clippy.py @@ -76,8 +76,8 @@ def classes(*, paths=None): monkeypatch.setattr(fs, "classes", classes) monkeypatch.delattr(clippy, "AddedClass", raising=False) - clippy.add_backend_path("/foo/bar") - clippy.add_backend_path("/foo/bar") + fs.add_backend_path("/foo/bar") + fs.add_backend_path("/foo/bar") assert calls == [["/foo/bar"], ["/foo/bar"]] assert clippy.AddedClass is AddedClass