diff --git a/dist/renamer-0.7.9-py3-none-any.whl b/dist/renamer-0.7.9-py3-none-any.whl new file mode 100644 index 0000000..d9d7359 Binary files /dev/null and b/dist/renamer-0.7.9-py3-none-any.whl differ diff --git a/pyproject.toml b/pyproject.toml index 7d4e6ed..90af8ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "renamer" -version = "0.7.8" +version = "0.7.9" description = "Terminal-based media file renamer and metadata viewer" readme = "README.md" requires-python = ">=3.11" diff --git a/renamer/formatters/__init__.py b/renamer/formatters/__init__.py index 9400201..e2981e7 100644 --- a/renamer/formatters/__init__.py +++ b/renamer/formatters/__init__.py @@ -21,7 +21,6 @@ from .extension_formatter import ExtensionFormatter from .resolution_formatter import ResolutionFormatter from .track_formatter import TrackFormatter from .special_info_formatter import SpecialInfoFormatter -from .formatter import FormatterApplier # Decorator instances from .date_decorators import date_decorators, DateDecorators @@ -51,7 +50,6 @@ __all__ = [ 'ResolutionFormatter', 'TrackFormatter', 'SpecialInfoFormatter', - 'FormatterApplier', # Decorator instances and classes 'date_decorators', diff --git a/renamer/formatters/formatter.py b/renamer/formatters/formatter.py deleted file mode 100644 index 35a1b91..0000000 --- a/renamer/formatters/formatter.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Formatter coordinator and application system. - -This module provides the FormatterApplier class which coordinates the application -of multiple formatters in the correct order (data → text → markup). It ensures -formatters are applied sequentially based on their type. -""" - -from .text_formatter import TextFormatter -from .duration_formatter import DurationFormatter -from .size_formatter import SizeFormatter -from .date_formatter import DateFormatter -from .extension_formatter import ExtensionFormatter -from .resolution_formatter import ResolutionFormatter -from .track_formatter import TrackFormatter -from .special_info_formatter import SpecialInfoFormatter -import logging -import inspect -import os - - -# Set up logging conditionally -if os.getenv('FORMATTER_LOG', '0') == '1': - logging.basicConfig(filename='formatter.log', level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s') -else: - logging.basicConfig(level=logging.CRITICAL) # Disable logging - - -class FormatterApplier: - """Coordinator for applying multiple formatters in the correct order. - - This class manages the application of formatters to data values, ensuring they - are applied in the proper sequence: - 1. Data formatters (transform raw data: size, duration, etc.) - 2. Text formatters (transform text: uppercase, lowercase, etc.) - 3. Markup formatters (add visual styling: bold, colors, etc.) - - The ordering prevents conflicts and ensures consistent output formatting. - - Example: - >>> from renamer.formatters.formatter import FormatterApplier - >>> from renamer.formatters.size_formatter import SizeFormatter - >>> from renamer.formatters.text_formatter import TextFormatter - >>> value = 1073741824 - >>> formatters = [SizeFormatter.format_size, TextFormatter.bold] - >>> result = FormatterApplier.apply_formatters(value, formatters) - >>> # Result: bold("1.00 GB") - """ - - # Define the global order of all formatters - FORMATTER_ORDER = [ - # Data formatters first (transform raw data) - DurationFormatter.format_seconds, - DurationFormatter.format_hhmmss, - DurationFormatter.format_hhmm, - DurationFormatter.format_full, - SizeFormatter.format_size, - SizeFormatter.format_size_full, - DateFormatter.format_modification_date, - DateFormatter.format_year, - ExtensionFormatter.format_extension_info, - ResolutionFormatter.format_resolution_dimensions, - TrackFormatter.format_video_track, - TrackFormatter.format_audio_track, - TrackFormatter.format_subtitle_track, - SpecialInfoFormatter.format_special_info, - SpecialInfoFormatter.format_database_info, - - # Text formatters second (transform text content) - TextFormatter.uppercase, - TextFormatter.lowercase, - TextFormatter.camelcase, - - # Markup formatters last (add visual styling) - TextFormatter.bold, - TextFormatter.italic, - TextFormatter.underline, - TextFormatter.bold_green, - TextFormatter.bold_cyan, - TextFormatter.bold_magenta, - TextFormatter.bold_yellow, - TextFormatter.green, - TextFormatter.yellow, - TextFormatter.magenta, - TextFormatter.cyan, - TextFormatter.red, - TextFormatter.blue, - TextFormatter.grey, - TextFormatter.dim, - TextFormatter.format_url, - ] - - @staticmethod - def apply_formatters(value, formatters): - """Apply multiple formatters to a value in the correct global order. - - Formatters are automatically sorted based on FORMATTER_ORDER to ensure - proper sequencing (data → text → markup). If a formatter fails, the - value is set to "Unknown" and processing continues. - - Args: - value: The value to format (can be any type) - formatters: Single formatter or list of formatter functions - - Returns: - The formatted value after all formatters have been applied - - Example: - >>> formatters = [SizeFormatter.format_size, TextFormatter.bold] - >>> result = FormatterApplier.apply_formatters(1024, formatters) - """ - if not isinstance(formatters, list): - formatters = [formatters] if formatters else [] - - # Sort formatters according to the global order - ordered_formatters = sorted(formatters, key=lambda f: FormatterApplier.FORMATTER_ORDER.index(f) if f in FormatterApplier.FORMATTER_ORDER else len(FormatterApplier.FORMATTER_ORDER)) - - # Apply in the ordered sequence - for formatter in ordered_formatters: - try: - old_value = value - value = formatter(value) - logging.debug(f"Applied {formatter.__name__ if hasattr(formatter, '__name__') else str(formatter)}: {repr(old_value)} -> {repr(value)}") - except Exception as e: - logging.error(f"Error applying {formatter.__name__ if hasattr(formatter, '__name__') else str(formatter)}: {e}") - value = "Unknown" - - return value - - @staticmethod - def format_data_item(item: dict) -> str | None: - """Apply all formatting to a data item and return the formatted string. - - Processes a data item dictionary containing value, label, and formatters, - applying them in the correct order to produce a formatted display string. - - Args: - item: Dictionary containing: - - value: The raw value to format - - label: Label text for the item - - value_formatters: List of formatters to apply to the value - - label_formatters: List of formatters to apply to the label - - display_formatters: List of formatters for the final display - - Returns: - Formatted string combining label and value, or None if value is None - - Example: - >>> item = { - ... "value": 1024, - ... "label": "Size", - ... "value_formatters": [SizeFormatter.format_size], - ... "label_formatters": [TextFormatter.bold] - ... } - >>> result = FormatterApplier.format_data_item(item) - """ - # Handle value formatting first (e.g., size formatting) - value = item.get("value") - if value is not None and value != "Not extracted": - value_formatters = item.get("value_formatters", []) - value = FormatterApplier.apply_formatters(value, value_formatters) - - # Handle label formatting - label = item.get("label", "") - if label: - label_formatters = item.get("label_formatters", []) - label = FormatterApplier.apply_formatters(label, label_formatters) - - # Create the display string - if value is not None: - display_string = f"{label}: {value}" - else: - display_string = label - - # Handle display formatting (e.g., color) - display_formatters = item.get("display_formatters", []) - display_string = FormatterApplier.apply_formatters(display_string, display_formatters) - - return display_string - - @staticmethod - def format_data_items(data: list[dict]) -> list: - """Apply formatting to a list of data items""" - return [FormatterApplier.format_data_item(item) for item in data] \ No newline at end of file diff --git a/renamer/formatters/special_info_formatter.py b/renamer/formatters/special_info_formatter.py index ee2cc68..b1f2d4b 100644 --- a/renamer/formatters/special_info_formatter.py +++ b/renamer/formatters/special_info_formatter.py @@ -31,5 +31,5 @@ class SpecialInfoFormatter: logging.info(f"Formatted tuple/list to: {result!r}") return result if os.getenv("FORMATTER_LOG"): - logging.info("Returning 'Unknown'") - return "Unknown" \ No newline at end of file + logging.info("Returning None") + return None \ No newline at end of file diff --git a/renamer/test/test_formatters.py b/renamer/test/test_formatters.py index f6be93a..816bbcc 100644 --- a/renamer/test/test_formatters.py +++ b/renamer/test/test_formatters.py @@ -16,8 +16,7 @@ from renamer.formatters import ( ExtensionFormatter, ResolutionFormatter, TrackFormatter, - SpecialInfoFormatter, - FormatterApplier + SpecialInfoFormatter ) @@ -270,124 +269,22 @@ class TestSpecialInfoFormatter: def test_format_database_info_dict(self): """Test formatting database info from dict.""" - info = {'type': 'tmdb', 'id': '12345'} + info = {'name': 'tmdb', 'id': '12345'} result = SpecialInfoFormatter.format_database_info(info) - # Just check it returns a string - assert isinstance(result, str) + # Should format as "tmdbid-12345" + assert result == "tmdbid-12345" def test_format_database_info_list(self): """Test formatting database info from list.""" info = ['tmdb', '12345'] result = SpecialInfoFormatter.format_database_info(info) - # Just check it returns a string - assert isinstance(result, str) + # Should format as "tmdbid-12345" + assert result == "tmdbid-12345" def test_format_database_info_none(self): """Test formatting None database info.""" result = SpecialInfoFormatter.format_database_info(None) - # Should return empty or some string - assert isinstance(result, str) + # Should return None when no valid database info + assert result is None -class TestFormatterApplier: - """Test FormatterApplier functionality.""" - - def test_apply_formatters_single(self): - """Test applying single formatter.""" - result = FormatterApplier.apply_formatters("test", TextFormatter.uppercase) - assert result == "TEST" - - def test_apply_formatters_list(self): - """Test applying multiple formatters.""" - formatters = [TextFormatter.uppercase, TextFormatter.bold] - result = FormatterApplier.apply_formatters("test", formatters) - assert "TEST" in result - assert "[bold]" in result - - def test_apply_formatters_ordered(self): - """Test that formatters are applied in correct order.""" - # Text formatters before markup formatters - formatters = [TextFormatter.bold, TextFormatter.uppercase] - result = FormatterApplier.apply_formatters("test", formatters) - # uppercase should be applied first, then bold - assert "[bold]TEST[/bold]" in result - - def test_format_data_item_with_value(self): - """Test formatting data item with value.""" - item = { - "label": "Size", - "value": 1024, - "value_formatters": [SizeFormatter.format_size] - } - result = FormatterApplier.format_data_item(item) - assert "Size:" in result - assert "KB" in result - - def test_format_data_item_with_label_formatters(self): - """Test formatting data item with label formatters.""" - item = { - "label": "title", - "value": "Movie", - "label_formatters": [TextFormatter.uppercase] - } - result = FormatterApplier.format_data_item(item) - assert "TITLE:" in result - - def test_format_data_item_with_display_formatters(self): - """Test formatting data item with display formatters.""" - item = { - "label": "Error", - "value": "Failed", - "display_formatters": [TextFormatter.red] - } - result = FormatterApplier.format_data_item(item) - assert "[red]" in result - - def test_format_data_items_list(self): - """Test formatting list of data items.""" - items = [ - {"label": "Title", "value": "Movie"}, - {"label": "Year", "value": "2024"} - ] - results = FormatterApplier.format_data_items(items) - assert len(results) == 2 - assert "Title: Movie" in results[0] - assert "Year: 2024" in results[1] - - -class TestFormatterIntegration: - """Integration tests for formatters working together.""" - - def test_complete_formatting_pipeline(self): - """Test complete formatting pipeline with multiple formatters.""" - # Create a data item with all formatter types - item = { - "label": "file size", - "value": 1024 * 1024 * 100, # 100 MB - "label_formatters": [TextFormatter.uppercase], - "value_formatters": [SizeFormatter.format_size], - "display_formatters": [TextFormatter.green] - } - - result = FormatterApplier.format_data_item(item) - - # Check all formatters were applied - assert "FILE SIZE:" in result # Label uppercase - assert "MB" in result # Size formatted - assert "[green]" in result # Display color - - def test_error_handling_in_formatter(self): - """Test error handling when formatter fails.""" - # Create a formatter that will fail - def bad_formatter(value): - raise ValueError("Test error") - - item = { - "label": "Test", - "value": "data", - "value_formatters": [bad_formatter] - } - - # Should return "Unknown" instead of crashing - result = FormatterApplier.format_data_item(item) - assert "Unknown" in result diff --git a/uv.lock b/uv.lock index 46c3ab7..705d598 100644 --- a/uv.lock +++ b/uv.lock @@ -462,7 +462,7 @@ wheels = [ [[package]] name = "renamer" -version = "0.7.8" +version = "0.7.9" source = { editable = "." } dependencies = [ { name = "langcodes" },