diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml index ba94996..c3b7f1c 100644 --- a/.github/workflows/distribute.yml +++ b/.github/workflows/distribute.yml @@ -27,8 +27,7 @@ jobs: run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - name: Update version in __init__.py - run: | - echo "__version__ = '${{ steps.tag.outputs.TAG_NAME }}'" > src/fastapi_fastkit/__init__.py + run: echo "__version__ = '${{ steps.tag.outputs.TAG_NAME }}'" > src/fastapi_fastkit/__init__.py - name: Build package run: pdm build diff --git a/CHANGELOG.md b/CHANGELOG.md index c175be2..0bc3983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## v1.1.5 (2025-09-14) + +### Improvements + +- **Adaptive Console Sizing**: Enhanced terminal output display + - Console width is 80% of terminal width, capped at 120 characters + - Console height is terminal height minus buffer (5 lines) + - Automatic terminal size detection with fallback to default sizes (80x24) + - Dynamic sizing based on actual terminal dimensions + +### Fixes + +- **Text Truncation Prevention**: Completely eliminated text truncation in CLI output + - Template names and descriptions are now fully displayed without "..." truncation + - Table columns automatically adjust to content length to prevent text cutting + - Added `overflow="fold"` and `no_wrap=False` settings to Rich tables + - Template listing now shows complete template names (e.g., `fastapi-custom-response` instead of `fastapi-custom-respo...`) +- **Fixing the object `console` not found error** + - this critical error was occurred every version before this version. + - this error was occurred because of the mismatched logic between distribute github actions workflow and the top `__init__.py` file of fastkit project package. + - This issue was discovered during the development of version 1.1.2, and I spent a lot of time troubleshooting it. I believe this was due to my lack of development experience. I sincerely apologize. + +## v1.1.4 (deprecated) + +this version was hotfix build, but it is deprecated. + +The issues that were being addressed during the development of this version remained unresolved and were fixed in version v1.1.5. + +For more details, please refer to the CHANGELOG.md file for v1.1.5. + ## v1.1.3 (2025-09-13) ### Templates diff --git a/src/fastapi_fastkit/__init__.py b/src/fastapi_fastkit/__init__.py index 91dbcb8..9b102be 100644 --- a/src/fastapi_fastkit/__init__.py +++ b/src/fastapi_fastkit/__init__.py @@ -1,10 +1 @@ -__version__ = "1.1.3" - -import os - -from rich.console import Console - -if "PYTEST_CURRENT_TEST" in os.environ: - console = Console(no_color=True) -else: - console = Console() +__version__ = "1.1.5" diff --git a/src/fastapi_fastkit/backend/package_managers/pdm_manager.py b/src/fastapi_fastkit/backend/package_managers/pdm_manager.py index 602564b..69867c1 100644 --- a/src/fastapi_fastkit/backend/package_managers/pdm_manager.py +++ b/src/fastapi_fastkit/backend/package_managers/pdm_manager.py @@ -8,10 +8,14 @@ import sys from typing import List -from fastapi_fastkit import console from fastapi_fastkit.core.exceptions import BackendExceptions from fastapi_fastkit.utils.logging import debug_log, get_logger -from fastapi_fastkit.utils.main import handle_exception, print_error, print_success +from fastapi_fastkit.utils.main import ( + console, + handle_exception, + print_error, + print_success, +) from .base import BasePackageManager diff --git a/src/fastapi_fastkit/backend/package_managers/pip_manager.py b/src/fastapi_fastkit/backend/package_managers/pip_manager.py index 0516bf4..2edc943 100644 --- a/src/fastapi_fastkit/backend/package_managers/pip_manager.py +++ b/src/fastapi_fastkit/backend/package_managers/pip_manager.py @@ -8,10 +8,14 @@ import sys from typing import List -from fastapi_fastkit import console from fastapi_fastkit.core.exceptions import BackendExceptions from fastapi_fastkit.utils.logging import debug_log, get_logger -from fastapi_fastkit.utils.main import handle_exception, print_error, print_success +from fastapi_fastkit.utils.main import ( + console, + handle_exception, + print_error, + print_success, +) from .base import BasePackageManager diff --git a/src/fastapi_fastkit/backend/package_managers/poetry_manager.py b/src/fastapi_fastkit/backend/package_managers/poetry_manager.py index 275e256..14a79dc 100644 --- a/src/fastapi_fastkit/backend/package_managers/poetry_manager.py +++ b/src/fastapi_fastkit/backend/package_managers/poetry_manager.py @@ -6,10 +6,14 @@ import subprocess from typing import List -from fastapi_fastkit import console from fastapi_fastkit.core.exceptions import BackendExceptions from fastapi_fastkit.utils.logging import debug_log, get_logger -from fastapi_fastkit.utils.main import handle_exception, print_error, print_success +from fastapi_fastkit.utils.main import ( + console, + handle_exception, + print_error, + print_success, +) from .base import BasePackageManager diff --git a/src/fastapi_fastkit/backend/package_managers/uv_manager.py b/src/fastapi_fastkit/backend/package_managers/uv_manager.py index 7e93170..ddf4b3b 100644 --- a/src/fastapi_fastkit/backend/package_managers/uv_manager.py +++ b/src/fastapi_fastkit/backend/package_managers/uv_manager.py @@ -7,10 +7,14 @@ import subprocess from typing import List -from fastapi_fastkit import console from fastapi_fastkit.core.exceptions import BackendExceptions from fastapi_fastkit.utils.logging import debug_log, get_logger -from fastapi_fastkit.utils.main import handle_exception, print_error, print_success +from fastapi_fastkit.utils.main import ( + console, + handle_exception, + print_error, + print_success, +) from .base import BasePackageManager diff --git a/src/fastapi_fastkit/cli.py b/src/fastapi_fastkit/cli.py index 1293b0f..14dd731 100644 --- a/src/fastapi_fastkit/cli.py +++ b/src/fastapi_fastkit/cli.py @@ -1,4 +1,3 @@ -# TODO : add a feature to automatically fix appropriate fastkit output console size # -------------------------------------------------------------------------- # The Module defines main and core CLI operations for FastAPI-fastkit. # @@ -31,6 +30,7 @@ from fastapi_fastkit.core.exceptions import CLIExceptions from fastapi_fastkit.core.settings import FastkitConfig from fastapi_fastkit.utils.logging import get_logger, setup_logging +from fastapi_fastkit.utils.main import console as utils_console from fastapi_fastkit.utils.main import ( create_info_table, is_fastkit_project, @@ -41,7 +41,9 @@ validate_email, ) -from . import __version__, console +console = utils_console + +from . import __version__ @click.group() @@ -97,6 +99,7 @@ def echo(ctx: Context) -> None: Deploy FastAPI app foundation instantly at your local! --- + - Project Maintainer : [link=mailto:bbbong9@gmail.com]bnbong(JunHyeok Lee)[/link] - Current Version : {__version__} - Github : [link]https://github.com/bnbong/FastAPI-fastkit[/link] @@ -135,8 +138,7 @@ def list_templates(ctx: Context) -> None: print_warning("No available templates.") return - table = create_info_table("Available Templates") - + template_data = {} for template in templates: template_path = os.path.join(template_dir, template) readme_path = os.path.join(template_path, "README.md-tpl") @@ -148,8 +150,9 @@ def list_templates(ctx: Context) -> None: if first_line.startswith("# "): description = first_line[2:] - table.add_row(template, description) + template_data[template] = description + table = create_info_table("Available Templates", template_data) console.print(table) diff --git a/src/fastapi_fastkit/utils/main.py b/src/fastapi_fastkit/utils/main.py index f244fd7..ead48c5 100644 --- a/src/fastapi_fastkit/utils/main.py +++ b/src/fastapi_fastkit/utils/main.py @@ -15,7 +15,6 @@ from rich.table import Table from rich.text import Text -from fastapi_fastkit import console from fastapi_fastkit.core.settings import settings from fastapi_fastkit.utils.logging import debug_log, get_logger @@ -25,6 +24,66 @@ REGEX = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b" +def get_optimal_console_size() -> tuple[int, int]: + """ + Get optimal console size based on terminal dimensions. + + Returns: + tuple: (width, height) - optimal console dimensions + """ + try: + # Get terminal size + terminal_size = os.get_terminal_size() + width = terminal_size.columns + height = terminal_size.lines + + # Set minimum and maximum constraints + min_width = 80 + max_width = 120 + min_height = 24 + + # Calculate optimal width (80% of terminal width, but within constraints) + optimal_width = max(min_width, min(max_width, int(width * 0.8))) + # Calculate optimal height (leave some space for prompt and buffer) + optimal_height = max(min_height, height - 5) + + return optimal_width, optimal_height + except (OSError, ValueError): + # Fallback to default size if terminal size detection fails + return 80, 24 + + +def create_adaptive_console() -> Console: + """ + Create a console instance with adaptive sizing based on terminal dimensions. + + Returns: + Console: Rich console instance with optimal sizing + """ + if "PYTEST_CURRENT_TEST" in os.environ: + return Console(no_color=True) + + width, height = get_optimal_console_size() + return Console(width=width, height=height) + + +# Initialize console with adaptive sizing +console = create_adaptive_console() + + +def _get_adaptive_panel_width(message: str) -> int: + """ + Calculate optimal panel width based on message length and terminal size. + + :param message: Message content + :return: Optimal panel width + """ + optimal_width, _ = get_optimal_console_size() + # Use message length + padding, but constrain to reasonable bounds + min_width = min(len(message) + 10, optimal_width - 4) + return max(40, min_width) # Minimum 40 chars, leave margin for borders + + def print_error( message: str, title: str = "Error", @@ -42,7 +101,9 @@ def print_error( error_text = Text() error_text.append("❌ ", style="bold red") error_text.append(message) - console.print(Panel(error_text, border_style="red", title=title)) + + panel_width = _get_adaptive_panel_width(message) + console.print(Panel(error_text, border_style="red", title=title, width=panel_width)) # Log error for debugging purposes (internal logging) debug_log(f"Error: {message}", "error") @@ -81,7 +142,11 @@ def print_success( success_text = Text() success_text.append("✨ ", style="bold yellow") success_text.append(message, style="bold green") - console.print(Panel(success_text, border_style="green", title=title)) + + panel_width = _get_adaptive_panel_width(message) + console.print( + Panel(success_text, border_style="green", title=title, width=panel_width) + ) def print_warning( @@ -97,7 +162,11 @@ def print_warning( warning_text = Text() warning_text.append("⚠️ ", style="bold yellow") warning_text.append(message) - console.print(Panel(warning_text, border_style="yellow", title=title)) + + panel_width = _get_adaptive_panel_width(message) + console.print( + Panel(warning_text, border_style="yellow", title=title, width=panel_width) + ) def print_info(message: str, title: str = "Info", console: Console = console) -> None: @@ -111,7 +180,9 @@ def print_info(message: str, title: str = "Info", console: Console = console) -> info_text = Text() info_text.append("ℹ ", style="bold blue") info_text.append(message) - console.print(Panel(info_text, border_style="blue", title=title)) + + panel_width = _get_adaptive_panel_width(message) + console.print(Panel(info_text, border_style="blue", title=title, width=panel_width)) def create_info_table( @@ -121,7 +192,7 @@ def create_info_table( console: Console = console, ) -> Table: """ - Create a table for displaying information. + Create a table for displaying information that never truncates text. :param title: Title for the table :param data: Dictionary of data to populate the table @@ -129,13 +200,53 @@ def create_info_table( :param console: Rich console instance :return: Configured Rich Table instance """ - table = Table(title=title, show_header=show_header, title_style="bold magenta") - table.add_column("Field", style="cyan") - table.add_column("Value", style="green") + # Calculate exact content lengths if data exists + if data: + max_field_length = max(len(str(key)) for key in data.keys()) + max_value_length = max(len(str(value)) for value in data.values()) + + # Set column widths to exactly match the longest content + # Add small padding to ensure content fits comfortably + field_width = max_field_length + 2 + value_width = max_value_length + 2 + else: + # Default widths for empty tables + field_width = 15 + value_width = 30 + + # Create table that prioritizes full text display over terminal fitting + table = Table( + title=title, + show_header=show_header, + title_style="bold magenta", + expand=False, # Never expand to terminal width + width=None, # Let table size itself based on content + pad_edge=False, # Reduce padding to save space + ) + + # Add columns with settings that prevent any truncation + table.add_column( + "Field", + style="cyan", + no_wrap=False, # Allow wrapping instead of truncating + width=field_width, # Exact width for content + min_width=field_width, # Minimum width to prevent shrinking + max_width=None, # No maximum width limit + overflow="fold", # Fold text instead of truncating + ) + table.add_column( + "Value", + style="green", + no_wrap=False, # Allow wrapping instead of truncating + width=value_width, # Exact width for content + min_width=value_width, # Minimum width to prevent shrinking + max_width=None, # No maximum width limit + overflow="fold", # Fold text instead of truncating + ) if data: for key, value in data.items(): - table.add_row(key, value) + table.add_row(str(key), str(value)) return table diff --git a/tests/test_backends/test_console_sizing.py b/tests/test_backends/test_console_sizing.py new file mode 100644 index 0000000..d0763b9 --- /dev/null +++ b/tests/test_backends/test_console_sizing.py @@ -0,0 +1,199 @@ +# -------------------------------------------------------------------------- +# Tests for console sizing functionality in FastAPI-fastkit CLI. +# +# @author bnbong bbbong9@gmail.com +# -------------------------------------------------------------------------- +import os +from unittest.mock import MagicMock, patch + +import pytest +from rich.console import Console + +from fastapi_fastkit.utils.main import ( + create_adaptive_console, + create_info_table, + get_optimal_console_size, + print_error, + print_info, + print_success, + print_warning, +) + + +class TestConsoleSizing: + """Test cases for console sizing functionality.""" + + def test_get_optimal_console_size_normal_terminal(self) -> None: + """Test optimal console size calculation with normal terminal dimensions.""" + with patch("os.get_terminal_size") as mock_get_size: + # Mock terminal size: 100 columns, 30 lines + mock_get_size.return_value = os.terminal_size((100, 30)) + + width, height = get_optimal_console_size() + + # Width should be 80% of terminal width (80), but within constraints + assert width == 80 # min(120, max(80, int(100 * 0.8))) + # Height should be terminal height minus buffer (25) + assert height == 25 # max(24, 30 - 5) + + def test_get_optimal_console_size_large_terminal(self) -> None: + """Test optimal console size calculation with large terminal dimensions.""" + with patch("os.get_terminal_size") as mock_get_size: + # Mock large terminal: 200 columns, 50 lines + mock_get_size.return_value = os.terminal_size((200, 50)) + + width, height = get_optimal_console_size() + + # Width should be capped at max_width (120) + assert width == 120 # min(120, max(80, int(200 * 0.8))) + # Height should be terminal height minus buffer (45) + assert height == 45 # max(24, 50 - 5) + + def test_get_optimal_console_size_small_terminal(self) -> None: + """Test optimal console size calculation with small terminal dimensions.""" + with patch("os.get_terminal_size") as mock_get_size: + # Mock small terminal: 60 columns, 20 lines + mock_get_size.return_value = os.terminal_size((60, 20)) + + width, height = get_optimal_console_size() + + # Width should be minimum width (80) + assert width == 80 # min(120, max(80, int(60 * 0.8))) + # Height should be minimum height (24) + assert height == 24 # max(24, 20 - 5) + + def test_get_optimal_console_size_fallback(self) -> None: + """Test fallback behavior when terminal size detection fails.""" + with patch("os.get_terminal_size") as mock_get_size: + # Mock OSError (terminal size detection failure) + mock_get_size.side_effect = OSError("Terminal size not available") + + width, height = get_optimal_console_size() + + # Should fallback to default size + assert width == 80 + assert height == 24 + + def test_create_adaptive_console_normal(self) -> None: + """Test adaptive console creation in normal environment.""" + with patch("os.get_terminal_size") as mock_get_size: + mock_get_size.return_value = os.terminal_size((100, 30)) + + console = create_adaptive_console() + + assert isinstance(console, Console) + # Console should have adaptive sizing + assert console.width == 80 + # Rich Console may use different height handling + assert console.height >= 25 or console.height == 30 + + def test_create_adaptive_console_pytest_env(self) -> None: + """Test adaptive console creation in pytest environment.""" + with patch.dict( + os.environ, {"PYTEST_CURRENT_TEST": "test_module::test_function"} + ): + console = create_adaptive_console() + + assert isinstance(console, Console) + # Should create no_color console for testing + assert console._color_system is None or console._force_terminal is False + + def test_create_info_table_adaptive_sizing(self) -> None: + """Test that create_info_table uses adaptive sizing.""" + with patch("os.get_terminal_size") as mock_get_size: + mock_get_size.return_value = os.terminal_size((100, 30)) + + data = { + "Project Name": "test-project", + "Author": "Test Author", + "Description": "A test project description", + } + + table = create_info_table("Test Table", data) + + # Table should have columns with proper width settings + assert len(table.columns) == 2 + assert ( + table.columns[0].width is not None and table.columns[0].width >= 14 + ) # Length of "Project Name" + 2 + assert ( + table.columns[1].width is not None and table.columns[1].width >= 27 + ) # Length of "A test project description" + 2 + + def test_create_info_table_long_content(self) -> None: + """Test table creation with long content that needs scaling.""" + with patch("os.get_terminal_size") as mock_get_size: + mock_get_size.return_value = os.terminal_size((80, 30)) # Small terminal + + data = { + "Very Long Project Name Field": "This is a very long project description that might exceed normal terminal width", + "Another Long Field Name": "Another very long value that should be handled properly", + } + + table = create_info_table("Test Table", data) + + # Table should have appropriate column widths based on content + assert len(table.columns) == 2 + assert ( + table.columns[0].width is not None + and table.columns[0].width >= len("Very Long Project Name Field") + 2 + ) + assert ( + table.columns[1].width is not None + and table.columns[1].width + >= len( + "This is a very long project description that might exceed normal terminal width" + ) + + 2 + ) + + @patch("fastapi_fastkit.utils.main.console") + def test_print_functions_use_adaptive_sizing(self, mock_console: MagicMock) -> None: + """Test that print functions use adaptive panel sizing.""" + with patch("os.get_terminal_size") as mock_get_size: + mock_get_size.return_value = os.terminal_size((100, 30)) + + test_message = "Test message" + + # Test each print function + print_error(test_message, console=mock_console) + print_success(test_message, console=mock_console) + print_warning(test_message, console=mock_console) + print_info(test_message, console=mock_console) + + # Each function should have been called once + assert mock_console.print.call_count == 4 + + # Check that panels were created with width parameter + for call in mock_console.print.call_args_list: + panel = call[0][0] # First positional argument should be the Panel + assert hasattr(panel, "width") + assert panel.width is not None + + def test_empty_data_table_creation(self) -> None: + """Test table creation with empty data.""" + with patch("os.get_terminal_size") as mock_get_size: + mock_get_size.return_value = os.terminal_size((100, 30)) + + table = create_info_table("Empty Table") + + # Should create table with default column setup + assert len(table.columns) == 2 + assert table.columns[0].width == 15 # Default field width + assert table.columns[1].width == 30 # Default value width + + def test_console_sizing_edge_cases(self) -> None: + """Test edge cases in console sizing.""" + with patch("os.get_terminal_size") as mock_get_size: + # Test with zero dimensions + mock_get_size.return_value = os.terminal_size((0, 0)) + + width, height = get_optimal_console_size() + + # Should use minimum constraints + assert width == 80 + assert height == 24 + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/test_utils.py b/tests/test_utils.py index 59a8aaf..48797e7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -40,7 +40,7 @@ def test_print_functions(self) -> None: test_message = "Test message" # when & then - with patch("fastapi_fastkit.console.print") as mock_console_print: + with patch("fastapi_fastkit.utils.main.console.print") as mock_console_print: print_error(test_message) print_info(test_message) print_success(test_message) @@ -55,7 +55,7 @@ def test_print_error_formatting(self) -> None: error_message = "Critical error occurred" # when - with patch("fastapi_fastkit.console.print") as mock_console_print: + with patch("fastapi_fastkit.utils.main.console.print") as mock_console_print: print_error(error_message) # then @@ -70,7 +70,7 @@ def test_print_success_formatting(self) -> None: success_message = "Operation completed successfully" # when - with patch("fastapi_fastkit.console.print") as mock_console_print: + with patch("fastapi_fastkit.utils.main.console.print") as mock_console_print: print_success(success_message) # then @@ -85,7 +85,7 @@ def test_print_warning_formatting(self) -> None: warning_message = "This is a warning" # when - with patch("fastapi_fastkit.console.print") as mock_console_print: + with patch("fastapi_fastkit.utils.main.console.print") as mock_console_print: print_warning(warning_message) # then @@ -100,7 +100,7 @@ def test_print_info_formatting(self) -> None: info_message = "Information message" # when - with patch("fastapi_fastkit.console.print") as mock_console_print: + with patch("fastapi_fastkit.utils.main.console.print") as mock_console_print: print_info(info_message) # then