diff --git a/pyproject.toml b/pyproject.toml index adfd75c..608d83c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ branch = true source = ["template_python", "tests"] [tool.coverage.report] -fail_under = 80 +fail_under = 95 skip_covered = false show_missing = true diff --git a/template_python/constants.py b/template_python/constants.py index dfaac73..36c0b02 100644 --- a/template_python/constants.py +++ b/template_python/constants.py @@ -1,5 +1,10 @@ """Constants for codebases using this template.""" +# Logging constants +LOGGING_FORMAT = "[%(asctime)s] %(levelname)s [%(module)s]: %(message)s" +LOGGING_DATE_FORMAT = "%d/%m/%Y | %H:%M:%S" +LOGGING_LEVEL = "INFO" + # Sphinx SPHINX_EXTENSIONS = [ "sphinx.ext.autodoc", # Auto-generate API docs from docstrings diff --git a/template_python/logging_setup.py b/template_python/logging_setup.py new file mode 100644 index 0000000..d0c115a --- /dev/null +++ b/template_python/logging_setup.py @@ -0,0 +1,46 @@ +"""Logging setup for the server.""" + +import logging +import sys +from logging.handlers import RotatingFileHandler +from pathlib import Path + +from template_python.constants import ( + LOGGING_DATE_FORMAT, + LOGGING_FORMAT, + LOGGING_LEVEL, +) + +FORMATTER = logging.Formatter(LOGGING_FORMAT, datefmt=LOGGING_DATE_FORMAT) +_root_logger = logging.getLogger() + + +def add_console_handler() -> None: + """Add a console handler to the root logger.""" + _console_handler = logging.StreamHandler(sys.stdout) + _console_handler.setLevel(getattr(logging, LOGGING_LEVEL)) + _console_handler.setFormatter(FORMATTER) + _root_logger.addHandler(_console_handler) + + +def add_file_handler(logging_filepath: Path, max_bytes: int, backup_count: int) -> None: + """Configure logging with both console and rotating file handlers. + + :param Path logging_filepath: The path to the log file. + :param int max_bytes: The maximum size of the log file in bytes before rotation. + :param int backup_count: The number of backup log files to keep. + """ + logging_filepath.parent.mkdir(exist_ok=True) + _file_handler = RotatingFileHandler( + filename=logging_filepath, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8" + ) + _file_handler.setLevel(getattr(logging, LOGGING_LEVEL)) + _file_handler.setFormatter(FORMATTER) + _root_logger.addHandler(_file_handler) + + +def setup_default_logging() -> None: + """Configure default logging to console with the specified format and level.""" + _root_logger.setLevel(getattr(logging, LOGGING_LEVEL)) + _root_logger.handlers.clear() + add_console_handler() diff --git a/tests/test_logging_setup.py b/tests/test_logging_setup.py new file mode 100644 index 0000000..5f8ca9c --- /dev/null +++ b/tests/test_logging_setup.py @@ -0,0 +1,36 @@ +"""Unit tests for the python_template_server.logging_setup module.""" + +import logging +from unittest.mock import MagicMock + +from template_python.logging_setup import add_console_handler, add_file_handler, setup_default_logging + + +class TestAddHandlers: + """Tests for the add_console_handler and add_file_handler functions.""" + + def test_add_console_handler(self) -> None: + """Test that add_console_handler adds a StreamHandler to the root logger.""" + add_console_handler() + + root_logger = logging.getLogger() + assert "StreamHandler" in [handler.__class__.__name__ for handler in root_logger.handlers] + + def test_add_file_handler(self, tmp_path: MagicMock) -> None: + """Test that add_file_handler adds a RotatingFileHandler to the root logger.""" + log_filepath = tmp_path / "test.log" + add_file_handler(log_filepath, max_bytes=1024, backup_count=1) + + root_logger = logging.getLogger() + assert "RotatingFileHandler" in [handler.__class__.__name__ for handler in root_logger.handlers] + + +class TestSetupDefaultLogging: + """Tests for the setup_default_logging function.""" + + def test_setup_default_logging(self) -> None: + """Test that setup_default_logging configures the root logger with a StreamHandler.""" + setup_default_logging() + + root_logger = logging.getLogger() + assert "StreamHandler" in [handler.__class__.__name__ for handler in root_logger.handlers] diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 53b7487..9d14230 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -83,6 +83,13 @@ def test_get_rst_prolog(self) -> None: expected_prolog = ".. |key1| replace:: value1\n.. |key2| replace:: value2" assert prolog == expected_prolog + def test_get_rst_prolog_mismatched_keys_values(self) -> None: + """Test generating an RST prolog with mismatched keys and values.""" + keys = ["key1", "key2"] + values = ["value1"] + with pytest.raises(ValueError, match=r"Keys and values must have the same length."): + get_rst_prolog(keys, values) + def test_print_version_pyproject(self) -> None: """Test printing the version from `pyproject.toml`.""" print_version_pyproject()