Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
466 changes: 429 additions & 37 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dev = [
"flake8>=7.0.0",
"black>=24.1.1",
"isort>=5.13.2",
"psutil>=5.9.0", # Added for memory profiling in tests
]

[project.scripts]
Expand Down
4 changes: 2 additions & 2 deletions src/pedalboard_pluginary/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def list_yaml():
return bdict(PedalboardPluginary().plugins).to_yaml()


def cli():
def main(): # Renamed from cli
fire.core.Display = lambda lines, out: print(*lines, file=out)
fire.Fire(
{
Expand All @@ -40,4 +40,4 @@ def cli():


if __name__ == "__main__":
cli()
main() # Renamed from cli()
7 changes: 6 additions & 1 deletion src/pedalboard_pluginary/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
class PedalboardScanner:
RE_AUFX = re.compile(r"aufx\s+(\w+)\s+(\w+)\s+-\s+(.*?):\s+(.*?)\s+\((.*?)\)")

import shutil # Added import

def __init__(self):
self.plugins_path = get_cache_path("plugins")
self.plugins = {}
Expand All @@ -34,7 +36,10 @@ def __init__(self):

def ensure_ignores(self):
self.ignores_path = get_cache_path("ignores")
if not self.ignores_path.exists():
if self.ignores_path.is_dir(): # If it exists as a directory
logger.warning(f"'{self.ignores_path}' is a directory, removing it.")
shutil.rmtree(self.ignores_path) # Remove the directory
Comment on lines +39 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider handling potential race conditions when removing the directory.

Another process could modify the directory between the is_dir() check and rmtree(), causing exceptions. Consider wrapping rmtree in a try/except for FileNotFoundError or OSError to handle this safely.

Suggested change
if self.ignores_path.is_dir(): # If it exists as a directory
logger.warning(f"'{self.ignores_path}' is a directory, removing it.")
shutil.rmtree(self.ignores_path) # Remove the directory
if self.ignores_path.is_dir(): # If it exists as a directory
logger.warning(f"'{self.ignores_path}' is a directory, removing it.")
try:
shutil.rmtree(self.ignores_path) # Remove the directory
except (FileNotFoundError, OSError) as e:
logger.warning(f"Failed to remove directory '{self.ignores_path}': {e}")

if not self.ignores_path.exists(): # Now, if it doesn't exist (as file or dir)
copy_default_ignores(self.ignores_path)
self.ignores = load_ignores(self.ignores_path)

Expand Down
2 changes: 1 addition & 1 deletion src/pedalboard_pluginary/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
def ensure_folder(path):
"""Ensure that a folder exists."""
path.parent.mkdir(parents=True, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Changing to path.mkdir may break cases where 'path' is a file path, not a directory.

If ensure_folder receives a file path, this change will create a directory with the file's name, which may not be intended. Confirm that ensure_folder is only called with directory paths to avoid potential bugs.

path.mkdir(parents=True, exist_ok=True)


def from_pb_param(data):
Expand Down
29 changes: 15 additions & 14 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
from unittest.mock import patch, MagicMock
import pytest

from pedalboard_pluginary.data import APP_NAME, PLUGINS_CACHE_FILENAME_BASE, get_cache_path
from pedalboard_pluginary.constants import APP_NAME, PLUGINS_CACHE_FILENAME
from pedalboard_pluginary.data import get_cache_path

# Helper to get the cache file path for plugins
def get_plugins_cache_file():
return get_cache_path(PLUGINS_CACHE_FILENAME_BASE)
return get_cache_path(PLUGINS_CACHE_FILENAME)

@pytest.fixture(autouse=True)
def manage_plugin_cache():
Expand Down Expand Up @@ -65,16 +66,16 @@ def manage_plugin_cache():
}

# This mock will replace the actual PedalboardScanner instance or its methods
@patch('pedalboard_pluginary.scanner.PedalboardScanner.scan_all_plugins')
@patch('pedalboard_pluginary.scanner.PedalboardScanner.update_scan') # Also mock update_scan
@patch('pedalboard_pluginary.scanner.PedalboardScanner.scan') # Patched to 'scan' as it's called by rescan
@patch('pedalboard_pluginary.scanner.PedalboardScanner.update') # Patched to 'update'
@patch('pedalboard_pluginary.scanner.PedalboardScanner.save_plugins') # Mock save_plugins
@patch('pedalboard_pluginary.core.PedalboardPluginary.load_data') # Mock load_data in core
def run_cli_command(
cli_args_list,
mock_core_load_data,
mock_scanner_save_plugins,
mock_scanner_update_scan,
mock_scanner_scan_all,
mock_core_load_data_mock, # Original mock object for core.PedalboardPluginary.load_data
mock_scanner_save_plugins_mock, # Original mock object for scanner.PedalboardScanner.save_plugins
mock_scanner_update_mock, # Original mock object for scanner.PedalboardScanner.update
mock_scanner_scan_mock, # Original mock object for scanner.PedalboardScanner.scan
expected_exit_code=0
):
"""Helper to run CLI commands and capture output."""
Expand All @@ -89,8 +90,8 @@ def side_effect_scan_or_update(*args, **kwargs):
# They modify self.plugins and then call self.save_plugins.
# We've mocked save_plugins separately.

mock_scanner_scan_all.side_effect = side_effect_scan_or_update
mock_scanner_update_scan.side_effect = side_effect_scan_or_update
mock_scanner_scan_mock.side_effect = side_effect_scan_or_update
mock_scanner_update_mock.side_effect = side_effect_scan_or_update

# Mock load_data to set the plugins attribute on an instance if needed,
# or simply prevent it from trying to load a real file during list commands
Expand Down Expand Up @@ -165,10 +166,10 @@ def side_effect_load_json(path_arg):

# Run the 'list' command
# Using direct function call to avoid subprocess complexity with stdout/stderr and fire's display hook
from pedalboard_pluginary.__main__ import list_json_cli
from pedalboard_pluginary.__main__ import list_json # Corrected to list_json

# Fire's Display hook is tricky to test with subprocess.run, so call directly.
# list_json_cli returns a string.
# list_json returns a dict which fire then processes.
# We need to ensure that when `pbpluginary list` is run, this function is called
# and its output (which is JSON string) is printed.
# For simplicity here, just test the function that `fire` would call.
Expand Down Expand Up @@ -254,7 +255,7 @@ def mock_update_scan_writes_cache(*args, **kwargs):
# For this test, assume it behaves like scan if no cache.
with open(cache_file, 'w') as f:
json.dump(MOCK_PLUGIN_DATA, f, indent=4) # For simplicity, same as scan
mock_scanner_instance.update_scan.side_effect = mock_update_scan_writes_cache
mock_scanner_instance.update.side_effect = mock_update_scan_writes_cache # Changed update_scan to update

result = run_cli_command(["update"]) # Uses patches from run_cli_command

Expand All @@ -264,7 +265,7 @@ def mock_update_scan_writes_cache(*args, **kwargs):
data_from_cache = json.load(f)
assert data_from_cache == MOCK_PLUGIN_DATA # Assuming update wrote this

mock_scanner_instance.update_scan.assert_called_once()
mock_scanner_instance.update.assert_called_once() # Changed update_scan to update


# TODO: Test for verbose logging options (--verbose=1, --verbose=2)
Expand Down
13 changes: 6 additions & 7 deletions tests/test_sqlite_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@

def create_test_plugin(plugin_id: str, name: str, manufacturer: str = "TestMfg") -> PluginInfo:
"""Create a test plugin for benchmarking."""
plugin_path_obj = Path(f"/test/path/{name.replace(' ', '_')}.plugin")
return PluginInfo(
id=plugin_id,
name=name,
path=Path(f"/test/path/{name.replace(' ', '_')}.plugin"),
path=str(plugin_path_obj), # Path should be a string
filename=plugin_path_obj.name,
plugin_type="test",
manufacturer=manufacturer,
parameters=[
PluginParameter(
parameters={
f"param_{i}": PluginParameter(
name=f"param_{i}",
default_value=float(i),
min_value=0.0,
max_value=100.0,
value=float(i)
) for i in range(10) # 10 parameters per plugin
]
}
)


Expand Down
Loading