Skip to content

Commit c32c1d4

Browse files
committed
Refactor into standalone function
1 parent 42c8007 commit c32c1d4

File tree

2 files changed

+61
-121
lines changed

2 files changed

+61
-121
lines changed

packages/jupyter-ai/jupyter_ai/personas/persona_manager.py

Lines changed: 52 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ def _init_persona_classes(self) -> None:
145145
+ "Please verify your server configuration and open a new issue on our GitHub repo if this warning persists."
146146
)
147147

148-
# TODO: check whether in the end we need a full class for this, or if we can just use a simple function that returns a list of persona classes from the local filesystem.
149-
persona_classes.extend(LocalPersonaLoader(self.root_dir).load_persona_classes())
148+
# Load persona classes from local filesystem
149+
persona_classes.extend(load_persona_classes_from_directory(self.root_dir))
150150

151151
PersonaManager._persona_classes = persona_classes
152152

@@ -297,114 +297,66 @@ def get_mcp_config(self) -> dict[str, Any]:
297297
return self._mcp_config_loader.get_config(jdir)
298298

299299

300-
class LocalPersonaLoader(LoggingConfigurable):
300+
def load_persona_classes_from_directory(root_dir: str) -> list[type[BasePersona]]:
301301
"""
302-
Load _persona class declarations_ from the local filesystem.
302+
Load _persona class declarations_ from Python files in the local filesystem.
303303
304-
Those class declarations are then used to instantiate personas by the `PersonaManager`.
304+
Those class declarations are then used to instantiate personas by the
305+
`PersonaManager`.
305306
306-
TODO: wire to local .jupyter directory system, for now just use the current
307-
directory.
308-
"""
309-
310-
def __init__(
311-
self,
312-
*args,
313-
root_dir: str | None = None,
314-
**kwargs,
315-
):
316-
# Forward other arguments to parent class
317-
super().__init__(*args, **kwargs)
307+
Scans the root_dir for .py files containing "persona" in their name,
308+
dynamically imports them, and extracts any class declarations that are
309+
subclasses of BasePersona.
318310
319-
# Set default root directory to current working directory if not provided
320-
self.root_dir = root_dir or os.getcwd()
311+
Args:
312+
root_dir: Directory to scan for persona Python files
321313
322-
self.log.info(
323-
f"LocalPersonaLoader initialized with root directory: {self.root_dir}"
324-
)
325-
326-
def load_persona_classes(self) -> list[type[BasePersona]]:
327-
"""
328-
Loads persona classes from Python files in the local filesystem.
329-
330-
Scans the root_dir for .py files, dynamically imports them, and extracts
331-
any class declarations that are subclasses of BasePersona.
332-
"""
333-
persona_classes: list[type[BasePersona]] = []
334-
335-
# Check if root directory exists
336-
if not os.path.exists(self.root_dir):
337-
self.log.info(f"Root directory does not exist: {self.root_dir}")
338-
return persona_classes
314+
Returns:
315+
List of BasePersona subclasses found in the directory
316+
"""
317+
persona_classes: list[type[BasePersona]] = []
339318

340-
# Find all .py files in the root directory that contain "persona" in the name
341-
all_py_files = glob(os.path.join(self.root_dir, "*.py"))
342-
py_files = [f for f in all_py_files if "persona" in Path(f).stem.lower()]
319+
# Check if root directory exists
320+
if not os.path.exists(root_dir):
321+
return persona_classes
343322

344-
if not py_files:
345-
self.log.info(
346-
f"No Python files with 'persona' in the name found in directory: {self.root_dir}"
347-
)
348-
return persona_classes
323+
# Find all .py files in the root directory that contain "persona" in the name
324+
all_py_files = glob(os.path.join(root_dir, "*.py"))
325+
py_files = [f for f in all_py_files if "persona" in Path(f).stem.lower()]
349326

350-
self.log.info(
351-
f"Found {len(py_files)} Python files with 'persona' in the name in {self.root_dir}"
352-
)
353-
self.log.info("PENDING: Loading persona classes from local Python files...")
354-
start_time_ns = time_ns()
327+
if not py_files:
328+
return persona_classes
355329

356-
for py_file in py_files:
357-
try:
358-
# Get module name from file path
359-
module_name = Path(py_file).stem
360-
361-
# Skip if module name starts with underscore (private modules)
362-
if module_name.startswith("_"):
363-
continue
364-
365-
# Create module spec and load the module
366-
spec = importlib.util.spec_from_file_location(module_name, py_file)
367-
if spec is None or spec.loader is None:
368-
self.log.warning(f" - Unable to create module spec for {py_file}")
369-
continue
370-
371-
module = importlib.util.module_from_spec(spec)
372-
spec.loader.exec_module(module)
373-
374-
# Find all classes in the module that are BasePersona subclasses
375-
module_persona_classes = []
376-
for name, obj in inspect.getmembers(module, inspect.isclass):
377-
# Check if it's a subclass of BasePersona but not BasePersona itself
378-
if (
379-
issubclass(obj, BasePersona)
380-
and obj is not BasePersona
381-
and obj.__module__ == module_name
382-
):
383-
module_persona_classes.append(obj)
384-
385-
if module_persona_classes:
386-
persona_classes.extend(module_persona_classes)
387-
class_names = [cls.__name__ for cls in module_persona_classes]
388-
self.log.info(
389-
f" - Loaded {len(module_persona_classes)} persona class(es) from '{py_file}': {class_names}"
390-
)
391-
else:
392-
self.log.debug(f" - No persona classes found in '{py_file}'")
330+
for py_file in py_files:
331+
try:
332+
# Get module name from file path
333+
module_name = Path(py_file).stem
393334

394-
except Exception:
395-
# On exception, log an error and continue
396-
# This mirrors the error handling pattern from entry point loading
397-
self.log.exception(
398-
f" - Unable to load persona classes from '{py_file}' due to an exception printed below."
399-
)
335+
# Skip if module name starts with underscore (private modules)
336+
if module_name.startswith("_"):
400337
continue
401338

402-
if len(persona_classes) > 0:
403-
elapsed_time_ms = (time_ns() - start_time_ns) // 1_000_000
404-
self.log.info(
405-
f"SUCCESS: Loaded {len(persona_classes)} persona classes from local filesystem. Time elapsed: {elapsed_time_ms}ms."
406-
)
407-
else:
408-
self.log.info("No persona classes found in local filesystem.")
339+
# Create module spec and load the module
340+
spec = importlib.util.spec_from_file_location(module_name, py_file)
341+
if spec is None or spec.loader is None:
342+
continue
409343

410-
return persona_classes
344+
module = importlib.util.module_from_spec(spec)
345+
spec.loader.exec_module(module)
346+
347+
# Find all classes in the module that are BasePersona subclasses
348+
for name, obj in inspect.getmembers(module, inspect.isclass):
349+
# Check if it's a subclass of BasePersona but not BasePersona itself
350+
if (
351+
issubclass(obj, BasePersona)
352+
and obj is not BasePersona
353+
and obj.__module__ == module_name
354+
):
355+
persona_classes.append(obj)
356+
357+
except Exception:
358+
# On exception, continue to next file
359+
# This mirrors the error handling pattern from entry point loading
360+
continue
361+
362+
return persona_classes

packages/jupyter-ai/jupyter_ai/tests/test_personas.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pytest
99
from jupyter_ai.personas.base_persona import BasePersona, PersonaDefaults
10-
from jupyter_ai.personas.persona_manager import LocalPersonaLoader
10+
from jupyter_ai.personas.persona_manager import load_persona_classes_from_directory
1111

1212

1313
@pytest.fixture
@@ -17,13 +17,12 @@ def tmp_persona_dir():
1717
yield Path(temp_dir)
1818

1919

20-
class TestLocalPersonaLoader:
21-
"""Test cases for LocalPersonaLoader class."""
20+
class TestLoadPersonaClassesFromDirectory:
21+
"""Test cases for load_persona_classes_from_directory function."""
2222

2323
def test_empty_directory_returns_empty_list(self, tmp_persona_dir):
2424
"""Test that an empty directory returns an empty list of persona classes."""
25-
loader = LocalPersonaLoader(root_dir=str(tmp_persona_dir))
26-
result = loader.load_persona_classes()
25+
result = load_persona_classes_from_directory(str(tmp_persona_dir))
2726
assert result == []
2827

2928
def test_non_persona_file_returns_empty_list(self, tmp_persona_dir):
@@ -32,8 +31,7 @@ def test_non_persona_file_returns_empty_list(self, tmp_persona_dir):
3231
non_persona_file = tmp_persona_dir / "no_personas.py"
3332
non_persona_file.write_text("pass")
3433

35-
loader = LocalPersonaLoader(root_dir=str(tmp_persona_dir))
36-
result = loader.load_persona_classes()
34+
result = load_persona_classes_from_directory(str(tmp_persona_dir))
3735
assert result == []
3836

3937
def test_simple_persona_file_returns_persona_class(self, tmp_persona_dir):
@@ -53,28 +51,18 @@ def process_message(self, message):
5351
"""
5452
persona_file.write_text(persona_content)
5553

56-
loader = LocalPersonaLoader(root_dir=str(tmp_persona_dir))
57-
result = loader.load_persona_classes()
54+
result = load_persona_classes_from_directory(str(tmp_persona_dir))
5855

5956
assert len(result) == 1
6057
assert result[0].__name__ == "TestPersona"
6158
assert issubclass(result[0], BasePersona)
6259

63-
def test_bad_persona_file_logs_exception_returns_empty_list(
64-
self, tmp_persona_dir, caplog
65-
):
66-
"""Test that a file with syntax errors logs an exception and returns empty list."""
60+
def test_bad_persona_file_returns_empty_list(self, tmp_persona_dir):
61+
"""Test that a file with syntax errors returns empty list."""
6762
# Create a file with invalid Python code
6863
bad_persona_file = tmp_persona_dir / "bad_persona.py"
6964
bad_persona_file.write_text("1/0")
7065

71-
loader = LocalPersonaLoader(root_dir=str(tmp_persona_dir))
72-
result = loader.load_persona_classes()
66+
result = load_persona_classes_from_directory(str(tmp_persona_dir))
7367

7468
assert result == []
75-
# Check that an error was logged
76-
assert any(
77-
"Unable to load persona classes from" in record.message
78-
for record in caplog.records
79-
if record.levelname in ["ERROR", "EXCEPTION"]
80-
)

0 commit comments

Comments
 (0)