diff --git a/README.md b/README.md index 56b8e811..d68effd1 100644 --- a/README.md +++ b/README.md @@ -657,11 +657,24 @@ ComfyUI_PromptManager/ ### Database Settings -You can customize the database path by modifying the configuration: +You can customize the database path by creating a `config.json` file in the extension root: -```python -# In py/config.py -DATABASE_PATH = "custom_path/prompts.db" +```json +{ + "database": { + "default_path": "/path/to/your/prompts.db" + } +} +``` + +Relative paths are resolved against the extension directory. Absolute paths are used as-is, which is recommended for Docker deployments where the working directory may not persist: + +```json +{ + "database": { + "default_path": "/data/custom_nodes/ComfyUI_PromptManager/prompts.db" + } +} ``` ### Gallery & Monitoring Settings diff --git a/database/models.py b/database/models.py index 0a028d67..cee627a8 100644 --- a/database/models.py +++ b/database/models.py @@ -21,12 +21,12 @@ except ImportError: class PromptModel: """Database model for prompt storage and schema management.""" - def __init__(self, db_path: str = "prompts.db"): + def __init__(self, db_path: str): """ Initialize the database model. Args: - db_path: Path to the SQLite database file + db_path: Absolute path to the SQLite database file """ self.logger = get_logger("prompt_manager.database.models") self.logger.debug(f"Initializing database model with path: {db_path}") diff --git a/database/operations.py b/database/operations.py index 2e04d9a0..588f35b5 100644 --- a/database/operations.py +++ b/database/operations.py @@ -29,17 +29,54 @@ TAG_SUBQUERY = ( ) +def _resolve_db_path(db_path: Optional[str] = None) -> str: + """Resolve the database path from config, falling back to defaults. + + When no explicit path is given, reads PromptManagerConfig.DEFAULT_DB_PATH + (set via config.json). Relative paths are resolved against the extension + root directory so the database location is predictable regardless of the + process working directory (important for Docker deployments). + + Args: + db_path: Explicit database path, or None to use config/default. + + Returns: + Absolute path to the SQLite database file. + """ + extension_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + if db_path is None: + try: + from py.config import PromptManagerConfig + + db_path = PromptManagerConfig.DEFAULT_DB_PATH + except Exception: + db_path = "prompts.db" + + if not os.path.isabs(db_path): + db_path = os.path.join(extension_root, db_path) + + # Ensure parent directory exists for custom paths + parent = os.path.dirname(db_path) + if parent: + os.makedirs(parent, exist_ok=True) + + return db_path + + class PromptDatabase: """Database operations class for managing prompts.""" - def __init__(self, db_path: str = "prompts.db"): + def __init__(self, db_path: Optional[str] = None): """ Initialize the database operations. Args: - db_path: Path to the SQLite database file + db_path: Path to the SQLite database file. If None, resolves from + config.json or defaults to prompts.db in the extension root. """ self.logger = get_logger("prompt_manager.database") + db_path = _resolve_db_path(db_path) self.logger.debug(f"Initializing database operations with path: {db_path}") self.model = PromptModel(db_path) self.logger.debug("Database operations initialized successfully") diff --git a/pyproject.toml b/pyproject.toml index c84f480f..a6b34ae2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "promptmanager" description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite." -version = "3.1.4" +version = "3.1.5" license = {file = "LICENSE"} dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"] diff --git a/tests/test_db_path_config.py b/tests/test_db_path_config.py new file mode 100644 index 00000000..36f49e28 --- /dev/null +++ b/tests/test_db_path_config.py @@ -0,0 +1,121 @@ +""" +Tests for issue #119: Database path configuration. + +Verifies that _resolve_db_path() correctly reads from config, resolves +relative paths against the extension root, and handles absolute paths. +""" + +import os +import sys +import tempfile +import types +import unittest +from unittest.mock import patch + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from database.operations import _resolve_db_path + +EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +class TestResolveDbPath(unittest.TestCase): + """Test _resolve_db_path() database path resolution.""" + + def test_explicit_absolute_path_used_as_is(self): + """An explicit absolute path should be returned unchanged.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "custom.db") + result = _resolve_db_path(path) + self.assertEqual(result, path) + + def test_explicit_relative_path_resolved_to_extension_root(self): + """An explicit relative path should resolve against the extension root.""" + result = _resolve_db_path("data/my.db") + expected = os.path.join(EXTENSION_ROOT, "data", "my.db") + self.assertEqual(result, expected) + + def test_none_falls_back_to_default_without_config(self): + """Without config (outside ComfyUI), should default to prompts.db.""" + # py.config requires ComfyUI's server module, so import fails naturally + result = _resolve_db_path(None) + expected = os.path.join(EXTENSION_ROOT, "prompts.db") + self.assertEqual(result, expected) + + def test_none_reads_config_when_available(self): + """None should read DEFAULT_DB_PATH from config when importable.""" + with tempfile.TemporaryDirectory() as tmpdir: + custom_path = os.path.join(tmpdir, "prompts.db") + fake_config_mod = types.ModuleType("py.config") + fake_pm_config = type( + "PromptManagerConfig", (), {"DEFAULT_DB_PATH": custom_path} + ) + fake_config_mod.PromptManagerConfig = fake_pm_config + + with patch.dict(sys.modules, {"py.config": fake_config_mod}): + result = _resolve_db_path(None) + + self.assertEqual(result, custom_path) + + def test_config_relative_path_resolved_to_extension_root(self): + """A relative path from config should resolve against extension root.""" + fake_config_mod = types.ModuleType("py.config") + fake_pm_config = type( + "PromptManagerConfig", (), {"DEFAULT_DB_PATH": "custom_data/prompts.db"} + ) + fake_config_mod.PromptManagerConfig = fake_pm_config + + with patch.dict(sys.modules, {"py.config": fake_config_mod}): + result = _resolve_db_path(None) + + expected = os.path.join(EXTENSION_ROOT, "custom_data", "prompts.db") + self.assertEqual(result, expected) + + def test_result_is_always_absolute(self): + """Result should always be an absolute path.""" + result = _resolve_db_path("prompts.db") + self.assertTrue(os.path.isabs(result)) + + def test_parent_directory_created(self): + """Parent directories should be created for custom paths.""" + with tempfile.TemporaryDirectory() as tmpdir: + nested = os.path.join(tmpdir, "deep", "nested", "prompts.db") + result = _resolve_db_path(nested) + self.assertEqual(result, nested) + self.assertTrue(os.path.isdir(os.path.join(tmpdir, "deep", "nested"))) + + +class TestPromptDatabaseUsesConfig(unittest.TestCase): + """Test that PromptDatabase() picks up the resolved path.""" + + @patch("database.operations._resolve_db_path") + def test_constructor_calls_resolve_with_none(self, mock_resolve): + """PromptDatabase() should call _resolve_db_path with None.""" + with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f: + mock_resolve.return_value = f.name + + try: + from database.operations import PromptDatabase + + PromptDatabase() + mock_resolve.assert_called_once_with(None) + finally: + os.unlink(f.name) + + @patch("database.operations._resolve_db_path") + def test_constructor_passes_explicit_path(self, mock_resolve): + """PromptDatabase(path) should call _resolve_db_path with that path.""" + with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f: + mock_resolve.return_value = f.name + + try: + from database.operations import PromptDatabase + + PromptDatabase("/explicit/path.db") + mock_resolve.assert_called_once_with("/explicit/path.db") + finally: + os.unlink(f.name) + + +if __name__ == "__main__": + unittest.main()