Skip to content

Commit 853693e

Browse files
committed
Implement unified serialization layer and enhance type safety
- Created a unified serialization layer with PluginSerializer to centralize JSON operations and improve code organization. - Added cache versioning and metadata support to enhance plugin data management. - Improved type safety with TypedDict definitions for better data validation. - Updated CHANGELOG.md to reflect these new features and enhancements. - Marked tasks as complete in TODO.md related to serialization and type safety improvements.
1 parent 12119b5 commit 853693e

9 files changed

Lines changed: 278 additions & 166 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4343
- Added Protocol definitions for scanner interfaces
4444
- Implemented type safety improvements with types.py
4545
- Refactored scanners to use common base class functionality
46+
- Created unified serialization layer with PluginSerializer
47+
- Added cache versioning and metadata support
48+
- Improved type safety with TypedDict definitions
49+
- Centralized all JSON operations in serialization module
4650

4751
## [1.1.0] - Previous Release
4852

TODO.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
- [x] Implement type guards for runtime validation
1818

1919
### Unified Serialization
20-
- [ ] Create serialization.py module
21-
- [ ] Implement PluginSerializer class
22-
- [ ] Replace duplicate serialization code in scanner.py
23-
- [ ] Add proper error handling to serialization
24-
- [ ] Add validation for loaded data
20+
- [x] Create serialization.py module
21+
- [x] Implement PluginSerializer class
22+
- [x] Replace duplicate serialization code in scanner.py
23+
- [x] Add proper error handling to serialization
24+
- [x] Add validation for loaded data
2525

2626
## Phase 2: Performance and Architecture
2727

@@ -143,12 +143,18 @@
143143
✅ Implemented basic plugin parameter extraction
144144
✅ Added progress bars for scanning
145145
✅ Created initial planning documents
146+
✅ Created protocols.py with Protocol definitions
147+
✅ Implemented BaseScanner abstraction
148+
✅ Refactored scanners to use inheritance
149+
✅ Created unified serialization layer
150+
✅ Added type safety with types.py and TypedDict
146151

147152
🚧 In Progress:
148-
- Type safety improvements
149-
- Scanner abstraction
153+
- Removing remaining type: ignore comments
154+
- Implementing proper error handling
150155

151156
📋 Next Priority:
152-
- Create protocols.py
153-
- Implement BaseScanner
154-
- Fix serialization duplication
157+
- Create exceptions.py with custom exception hierarchy
158+
- Add type stubs for pedalboard
159+
- Implement async scanner support
160+
- Add comprehensive test coverage
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Constants and configuration values for pedalboard_pluginary.
3+
"""
4+
5+
from typing import Final
6+
7+
# Application metadata
8+
APP_NAME: Final[str] = "com.twardoch.pedalboard-pluginary"
9+
APP_VERSION: Final[str] = "0.1.0" # TODO: Get from package metadata
10+
11+
# Cache configuration
12+
CACHE_VERSION: Final[str] = "2.0.0"
13+
PLUGINS_CACHE_FILENAME: Final[str] = "plugins"
14+
IGNORES_CACHE_FILENAME: Final[str] = "ignores"
15+
16+
# Scanner configuration
17+
DEFAULT_SCAN_TIMEOUT: Final[int] = 10 # seconds
18+
MAX_SCAN_RETRIES: Final[int] = 3
19+
SCAN_RETRY_DELAY: Final[float] = 1.0 # seconds
20+
21+
# Plugin types
22+
PLUGIN_TYPE_VST3: Final[str] = "vst3"
23+
PLUGIN_TYPE_AU: Final[str] = "aufx"
24+
SUPPORTED_PLUGIN_TYPES: Final[list[str]] = [PLUGIN_TYPE_VST3, PLUGIN_TYPE_AU]
25+
26+
# File extensions
27+
VST3_EXTENSION: Final[str] = ".vst3"
28+
AU_EXTENSION: Final[str] = ".component"
29+
30+
# Platform names
31+
PLATFORM_WINDOWS: Final[str] = "Windows"
32+
PLATFORM_MACOS: Final[str] = "Darwin"
33+
PLATFORM_LINUX: Final[str] = "Linux"
34+
35+
# Progress reporting
36+
DEFAULT_PROGRESS_BAR_WIDTH: Final[int] = 80
37+
PROGRESS_UPDATE_INTERVAL: Final[float] = 0.1 # seconds
38+
39+
# Logging configuration
40+
LOG_FORMAT: Final[str] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
41+
LOG_DATE_FORMAT: Final[str] = "%Y-%m-%d %H:%M:%S"
42+
43+
# CLI configuration
44+
DEFAULT_OUTPUT_FORMAT: Final[str] = "json"
45+
SUPPORTED_OUTPUT_FORMATS: Final[list[str]] = ["json", "yaml", "table", "csv"]
46+
47+
# Resource paths
48+
RESOURCES_PACKAGE: Final[str] = "pedalboard_pluginary.resources"
49+
DEFAULT_IGNORES_FILENAME: Final[str] = "default_ignores.json"

src/pedalboard_pluginary/core.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
import json
22
from pathlib import Path
3-
from typing import Any, Dict
3+
from typing import Dict
44

5-
from .data import get_cache_path, load_json_file
5+
from .data import get_cache_path
6+
from .models import PluginInfo
67
from .scanner import PedalboardScanner
8+
from .serialization import PluginSerializer
79

810

911
class PedalboardPluginary:
12+
"""Main class for the Pedalboard Pluginary application."""
13+
1014
plugins_path: Path
11-
plugins: Dict[str, Any] # Assuming plugin names (keys) are strings
15+
plugins: Dict[str, PluginInfo]
1216

1317
def __init__(self) -> None:
18+
"""Initialize the Pedalboard Pluginary instance."""
1419
self.plugins_path = get_cache_path("plugins")
15-
self.plugins = {} # Initialize to empty dict
20+
self.plugins = {}
1621
self.load_data()
1722

1823
def load_data(self) -> None:
24+
"""Load plugin data from cache or perform a scan if cache doesn't exist."""
1925
if not self.plugins_path.exists():
2026
scanner = PedalboardScanner()
21-
scanner.full_scan() # Updated to use full_scan instead of scan
27+
scanner.full_scan()
2228

23-
# Ensure plugins are loaded even if scan wasn't needed or if it just ran
24-
# load_json_file returns Dict[Any, Any], but we expect Dict[str, Any] for plugins
25-
loaded_plugins = load_json_file(self.plugins_path)
26-
if isinstance(loaded_plugins, dict):
27-
self.plugins = loaded_plugins
28-
else:
29-
# This case should ideally not happen if save_json_file and load_json_file are robust
30-
self.plugins = {}
29+
# Load plugins using the serializer
30+
self.plugins = PluginSerializer.load_plugins(self.plugins_path)
3131

3232
def list_plugins(self) -> str:
3333
"""Returns a JSON string representation of the plugins."""
34-
return json.dumps(self.plugins, indent=4)
34+
# Convert PluginInfo objects to dictionaries for JSON serialization
35+
plugins_dict = {}
36+
for plugin_id, plugin in self.plugins.items():
37+
plugins_dict[plugin_id] = PluginSerializer.plugin_to_dict(plugin)
38+
39+
return json.dumps(plugins_dict, indent=4)

src/pedalboard_pluginary/data.py

Lines changed: 9 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import json
22
import os
3-
import platform # Added
3+
import platform
44
import shutil
55
from importlib import resources
66
from pathlib import Path
7-
from typing import Any, Dict, List, Optional, Set, Union
7+
from typing import Any, Dict, List, Set, Union
88

9-
from .models import PluginInfo, PluginParameter # Added
10-
11-
# pkg_resources import removed, will rely on importlib.resources primarily.
12-
# Fallback block for pkg_resources in copy_default_ignores will try to use it
13-
# and handle NameError if it's not available.
149
from .utils import ensure_folder
1510

1611
APP_NAME: str = "com.twardoch.pedalboard-pluginary"
@@ -41,8 +36,8 @@ def get_cache_path(cache_name: str) -> Path:
4136
return app_data_dir / f"{cache_name}.json"
4237

4338

44-
def load_json_file(file_path: Path) -> Dict[str, Any]:
45-
"""Load JSON data from a file. If it's the plugins cache, reconstruct PluginInfo objects."""
39+
def load_json_file(file_path: Path) -> Any:
40+
"""Load JSON data from a file."""
4641
if not file_path.exists():
4742
return {}
4843

@@ -52,63 +47,12 @@ def load_json_file(file_path: Path) -> Dict[str, Any]:
5247
except json.JSONDecodeError:
5348
return {} # Return empty dict if JSON is corrupted
5449

55-
# Check if this is the plugins cache file by its name
56-
if file_path.name == f"{PLUGINS_CACHE_FILENAME_BASE}.json":
57-
if not isinstance(raw_data, dict):
58-
return {} # Corrupted plugin cache
59-
60-
reconstructed_plugins: Dict[str, PluginInfo] = {}
61-
for plugin_id, plugin_data_dict in raw_data.items():
62-
if not isinstance(plugin_data_dict, dict):
63-
continue # Skip malformed entries
64-
65-
param_dicts = plugin_data_dict.get("parameters", {})
66-
reconstructed_params: Dict[str, PluginParameter] = {}
67-
if isinstance(param_dicts, dict):
68-
for param_name, param_data_dict in param_dicts.items():
69-
if isinstance(param_data_dict, dict):
70-
try:
71-
reconstructed_params[param_name] = PluginParameter(
72-
name=str(param_data_dict.get("name", param_name)),
73-
value=param_data_dict.get("value", 0.0),
74-
)
75-
except TypeError as e:
76-
print(
77-
f"Warning: Could not reconstruct parameter {param_name} for {plugin_id}: {e}"
78-
)
79-
continue
80-
81-
try:
82-
reconstructed_plugins[plugin_id] = PluginInfo(
83-
id=plugin_data_dict.get("id", plugin_id),
84-
name=plugin_data_dict.get("name", "Unknown Plugin Name"),
85-
path=plugin_data_dict.get("path", ""),
86-
filename=plugin_data_dict.get("filename", ""),
87-
plugin_type=plugin_data_dict.get("plugin_type", "unknown"),
88-
parameters=reconstructed_params,
89-
manufacturer=plugin_data_dict.get("manufacturer"),
90-
name_in_file=plugin_data_dict.get("name_in_file"),
91-
)
92-
except TypeError as e:
93-
print(
94-
f"Warning: Could not reconstruct plugin info for {plugin_id}: {e}"
95-
)
96-
continue
97-
98-
return reconstructed_plugins
99-
100-
# Handle non-plugin cache files
101-
result: Dict[str, Any] = {}
102-
if isinstance(raw_data, dict):
103-
result = {str(k): v for k, v in raw_data.items()}
104-
elif isinstance(raw_data, list):
105-
result = {str(i): item for i, item in enumerate(raw_data)}
106-
return result
50+
return raw_data
10751

10852

10953
def save_json_file(data: Union[Dict[Any, Any], List[Any]], file_path: Path) -> None:
11054
"""Save JSON data to a file."""
111-
ensure_folder(file_path)
55+
ensure_folder(file_path.parent)
11256
with open(file_path, "w", encoding="utf-8") as file:
11357
json.dump(data, file, indent=4)
11458

@@ -136,7 +80,7 @@ def copy_default_ignores(destination_path: Path) -> None:
13680
).joinpath("default_ignores.json")
13781

13882
if not destination_path.exists():
139-
ensure_folder(destination_path)
83+
ensure_folder(destination_path.parent)
14084
with importlib.resources.as_file(
14185
default_ignores_src_path
14286
) as src_file_on_fs:
@@ -149,5 +93,5 @@ def copy_default_ignores(destination_path: Path) -> None:
14993
f"Warning: Could not copy default ignores using importlib.resources: {e}. Creating empty ignores file."
15094
)
15195
if not destination_path.exists():
152-
ensure_folder(destination_path)
153-
save_json_file([], destination_path)
96+
ensure_folder(destination_path.parent)
97+
save_json_file([], destination_path)

0 commit comments

Comments
 (0)