refactor: Remove FormatterApplier class and update related imports and tests
This commit is contained in:
BIN
dist/renamer-0.7.9-py3-none-any.whl
vendored
Normal file
BIN
dist/renamer-0.7.9-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "renamer"
|
name = "renamer"
|
||||||
version = "0.7.8"
|
version = "0.7.9"
|
||||||
description = "Terminal-based media file renamer and metadata viewer"
|
description = "Terminal-based media file renamer and metadata viewer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from .extension_formatter import ExtensionFormatter
|
|||||||
from .resolution_formatter import ResolutionFormatter
|
from .resolution_formatter import ResolutionFormatter
|
||||||
from .track_formatter import TrackFormatter
|
from .track_formatter import TrackFormatter
|
||||||
from .special_info_formatter import SpecialInfoFormatter
|
from .special_info_formatter import SpecialInfoFormatter
|
||||||
from .formatter import FormatterApplier
|
|
||||||
|
|
||||||
# Decorator instances
|
# Decorator instances
|
||||||
from .date_decorators import date_decorators, DateDecorators
|
from .date_decorators import date_decorators, DateDecorators
|
||||||
@@ -51,7 +50,6 @@ __all__ = [
|
|||||||
'ResolutionFormatter',
|
'ResolutionFormatter',
|
||||||
'TrackFormatter',
|
'TrackFormatter',
|
||||||
'SpecialInfoFormatter',
|
'SpecialInfoFormatter',
|
||||||
'FormatterApplier',
|
|
||||||
|
|
||||||
# Decorator instances and classes
|
# Decorator instances and classes
|
||||||
'date_decorators',
|
'date_decorators',
|
||||||
|
|||||||
@@ -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]
|
|
||||||
@@ -31,5 +31,5 @@ class SpecialInfoFormatter:
|
|||||||
logging.info(f"Formatted tuple/list to: {result!r}")
|
logging.info(f"Formatted tuple/list to: {result!r}")
|
||||||
return result
|
return result
|
||||||
if os.getenv("FORMATTER_LOG"):
|
if os.getenv("FORMATTER_LOG"):
|
||||||
logging.info("Returning 'Unknown'")
|
logging.info("Returning None")
|
||||||
return "Unknown"
|
return None
|
||||||
@@ -16,8 +16,7 @@ from renamer.formatters import (
|
|||||||
ExtensionFormatter,
|
ExtensionFormatter,
|
||||||
ResolutionFormatter,
|
ResolutionFormatter,
|
||||||
TrackFormatter,
|
TrackFormatter,
|
||||||
SpecialInfoFormatter,
|
SpecialInfoFormatter
|
||||||
FormatterApplier
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -270,124 +269,22 @@ class TestSpecialInfoFormatter:
|
|||||||
|
|
||||||
def test_format_database_info_dict(self):
|
def test_format_database_info_dict(self):
|
||||||
"""Test formatting database info from dict."""
|
"""Test formatting database info from dict."""
|
||||||
info = {'type': 'tmdb', 'id': '12345'}
|
info = {'name': 'tmdb', 'id': '12345'}
|
||||||
result = SpecialInfoFormatter.format_database_info(info)
|
result = SpecialInfoFormatter.format_database_info(info)
|
||||||
# Just check it returns a string
|
# Should format as "tmdbid-12345"
|
||||||
assert isinstance(result, str)
|
assert result == "tmdbid-12345"
|
||||||
|
|
||||||
def test_format_database_info_list(self):
|
def test_format_database_info_list(self):
|
||||||
"""Test formatting database info from list."""
|
"""Test formatting database info from list."""
|
||||||
info = ['tmdb', '12345']
|
info = ['tmdb', '12345']
|
||||||
result = SpecialInfoFormatter.format_database_info(info)
|
result = SpecialInfoFormatter.format_database_info(info)
|
||||||
# Just check it returns a string
|
# Should format as "tmdbid-12345"
|
||||||
assert isinstance(result, str)
|
assert result == "tmdbid-12345"
|
||||||
|
|
||||||
def test_format_database_info_none(self):
|
def test_format_database_info_none(self):
|
||||||
"""Test formatting None database info."""
|
"""Test formatting None database info."""
|
||||||
result = SpecialInfoFormatter.format_database_info(None)
|
result = SpecialInfoFormatter.format_database_info(None)
|
||||||
# Should return empty or some string
|
# Should return None when no valid database info
|
||||||
assert isinstance(result, str)
|
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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user