From 37be2d283a1b4ce89876dbf393a5d0caec28b7e9 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 27 Feb 2026 18:51:00 +0530 Subject: [PATCH 1/6] first commit --- fancylog/sublog.py | 217 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 fancylog/sublog.py diff --git a/fancylog/sublog.py b/fancylog/sublog.py new file mode 100644 index 0000000..435a1ad --- /dev/null +++ b/fancylog/sublog.py @@ -0,0 +1,217 @@ +"""Sub-logging support for fancylog. + +purpose: to create separate log files for specific +computations or third-party tools, linked from the main log. +""" + +import logging +import os +import subprocess +from contextlib import contextmanager +from datetime import datetime + + +class SubLog: + """A sub-log linked to a parent logger. + + Creates a separate log file for a specific computation or tool, + and logs a reference in the parent logger pointing to the sub-log. + + Parameters + ---------- + name : str + Name of the sub-log . + output_dir : str + directory where the sub-log file will be saved. + parent_logger_name : str or None + name of the parent logger. If None, uses the root logger. + file_log_level : str + Logging level for the sub-log file. Default: 'DEBUG'. + log_to_console : bool + Whether to also print sub-log messages to the console. + Default: False. + timestamp : bool + Whether to add a timestamp to the sub-log filename. + Default: True. + + Attributes + ---------- + logger : logging.Logger + The sub-log's logger instance. + log_file : str + Path to the sub-log file. + """ + + def __init__( + self, + name, + output_dir, + parent_logger_name=None, + file_log_level="DEBUG", + log_to_console=False, + timestamp=True, + ): + self.name = name + self.output_dir = str(output_dir) + self.parent_logger_name = parent_logger_name + self.file_log_level = file_log_level + + filename = name + if timestamp: + filename = datetime.now().strftime(filename + "_%Y-%m-%d_%H-%M-%S") + self.log_file = os.path.join(self.output_dir, filename + ".log") + + # sublogger as a child of the parent + if parent_logger_name: + sub_logger_name = f"{parent_logger_name}.sublog.{name}" + else: + sub_logger_name = f"fancylog.sublog.{name}" + + self.logger = logging.getLogger(sub_logger_name) + self.logger.handlers = [] + self.logger.propagate = False + self.logger.setLevel(getattr(logging, file_log_level)) + + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s" + " - %(processName)s %(filename)s:%(lineno)s" + " - %(message)s" + ) + formatter.datefmt = "%Y-%m-%d %H:%M:%S %p" + + fh = logging.FileHandler(self.log_file, encoding="utf-8") + fh.setLevel(getattr(logging, file_log_level)) + fh.setFormatter(formatter) + self.logger.addHandler(fh) + + if log_to_console: + try: + from rich.logging import RichHandler + + ch = RichHandler() + except ImportError: + ch = logging.StreamHandler() + ch.setLevel(getattr(logging, "DEBUG")) + ch.setFormatter(formatter) + self.logger.addHandler(ch) + + # get the parent logger and log a reference + if parent_logger_name: + self._parent_logger = logging.getLogger(parent_logger_name) + else: + self._parent_logger = logging.getLogger() + + self._parent_logger.info( + "Starting sub-log '%s', see %s for details", name, self.log_file + ) + self.logger.info("Sub-log '%s' started", name) + + def close(self): + """Close the sub-log and log a completion message in the parent.""" + self.logger.info("Sub-log '%s' finished", self.name) + + for handler in self.logger.handlers[:]: + handler.close() + self.logger.removeHandler(handler) + + self._parent_logger.info( + "Sub-log '%s' finished, log saved to %s", + self.name, + self.log_file, + ) + + def run_subprocess(self, command, **kwargs): + """Run a subprocess and capture its output in the sub-log. + + Parameters + ---------- + command : list or str + The command to run (passed to subprocess.run). + **kwargs + Additional keyword arguments passed to subprocess.run. + Note: stdout and stderr will be set to subprocess.PIPE + and cannot be overridden. + + Returns + ------- + subprocess.CompletedProcess + The result of the subprocess execution. + + """ + self.logger.info("Running command: %s", command) + + kwargs.pop("stdout", None) + kwargs.pop("stderr", None) + kwargs.pop("capture_output", None) + + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + **kwargs, + ) + + if result.stdout: + for line in result.stdout.strip().splitlines(): + self.logger.info("[stdout] %s", line) + + if result.stderr: + for line in result.stderr.strip().splitlines(): + self.logger.warning("[stderr] %s", line) + + self.logger.info( + "Command finished with return code %d", result.returncode + ) + + return result + + +@contextmanager +def sub_log( + name, + output_dir, + parent_logger_name=None, + file_log_level="DEBUG", + log_to_console=False, + timestamp=True, +): + """Context manager for creating a sub-log. + + Creates a SubLog on entry and closes it on exit, ensuring + that the sub-log is properly cleaned up even if an exception occurs. + + Parameters + ---------- + name : str + Name of the sub-log. + output_dir : str + Directory where the sub-log file will be saved. + parent_logger_name : str or None + Name of the parent logger. If None, uses the root logger. + file_log_level : str + Logging level for the sub-log file. Default: 'DEBUG'. + log_to_console : bool + Whether to also print sub-log messages to the console. + Default: False. + timestamp : bool + whether to add a timestamp to the sub-log filename. + Default: True. + + outputs: + SubLog + The sub-log instance. + + """ + sl = SubLog( + name, + output_dir, + parent_logger_name=parent_logger_name, + file_log_level=file_log_level, + log_to_console=log_to_console, + timestamp=timestamp, + ) + try: + yield sl + finally: + sl.close() \ No newline at end of file From 30b9bf7d173f52ef98de2fa2eb24fb74adf55154 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 27 Feb 2026 18:52:50 +0530 Subject: [PATCH 2/6] package --- fancylog/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fancylog/__init__.py b/fancylog/__init__.py index 9ce910d..9cd9d52 100644 --- a/fancylog/__init__.py +++ b/fancylog/__init__.py @@ -7,3 +7,4 @@ pass from fancylog.fancylog import start_logging +from fancylog.sublog import SubLog, sub_log \ No newline at end of file From 4e002f0a9c1626dfc027d25b1f1c45fbe5d42a01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:35:59 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- fancylog/__init__.py | 2 +- fancylog/sublog.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fancylog/__init__.py b/fancylog/__init__.py index 9cd9d52..77b2bf5 100644 --- a/fancylog/__init__.py +++ b/fancylog/__init__.py @@ -7,4 +7,4 @@ pass from fancylog.fancylog import start_logging -from fancylog.sublog import SubLog, sub_log \ No newline at end of file +from fancylog.sublog import SubLog, sub_log diff --git a/fancylog/sublog.py b/fancylog/sublog.py index 435a1ad..f4fe262 100644 --- a/fancylog/sublog.py +++ b/fancylog/sublog.py @@ -40,6 +40,7 @@ class SubLog: The sub-log's logger instance. log_file : str Path to the sub-log file. + """ def __init__( @@ -91,7 +92,7 @@ def __init__( ch = RichHandler() except ImportError: ch = logging.StreamHandler() - ch.setLevel(getattr(logging, "DEBUG")) + ch.setLevel(logging.DEBUG) ch.setFormatter(formatter) self.logger.addHandler(ch) @@ -198,7 +199,7 @@ def sub_log( whether to add a timestamp to the sub-log filename. Default: True. - outputs: + outputs: SubLog The sub-log instance. @@ -214,4 +215,4 @@ def sub_log( try: yield sl finally: - sl.close() \ No newline at end of file + sl.close() From 72e7cc7f6c802b4f95b1839c7add8ae24a0653c6 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 28 Feb 2026 14:19:23 +0530 Subject: [PATCH 4/6] tests and example --- example.py | 29 ++- fancylog/sublog.py | 6 +- tests/tests/test_sublog.py | 390 +++++++++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+), 4 deletions(-) create mode 100644 tests/tests/test_sublog.py diff --git a/example.py b/example.py index 1c4fe08..853b9ae 100644 --- a/example.py +++ b/example.py @@ -1,6 +1,8 @@ import logging +import sys import fancylog +from fancylog.sublog import sub_log class MadeUpPaths: @@ -36,6 +38,31 @@ def main(directory): logger.info("This is an info message") logger.debug("This is a debug message") + + logger.info("Starting pipeline...") + + with sub_log( + "preprocessing", + directory, + parent_logger_name="my_logger", + timestamp=True, + ) as sl: + sl.logger.info("Running preprocessing step 1") + sl.logger.debug("Detailed preprocessing debug info") + sl.logger.info("Running preprocessing step 2") + + with sub_log( + "external_tool", + directory, + parent_logger_name="my_logger", + timestamp=True, + ) as sl: + sl.logger.info("About to run external tool") + sl.run_subprocess( + [sys.executable, "-c", "print('Tool output line 1')"] + ) + + logger.info("here comes the completion of the example :(") logger.warning("This fun logging experience is about to end :(") @@ -51,4 +78,4 @@ def main(directory): ) args = parser.parse_args() - main(args.directory[0]) + main(args.directory[0]) \ No newline at end of file diff --git a/fancylog/sublog.py b/fancylog/sublog.py index f4fe262..bbc746b 100644 --- a/fancylog/sublog.py +++ b/fancylog/sublog.py @@ -199,9 +199,9 @@ def sub_log( whether to add a timestamp to the sub-log filename. Default: True. - outputs: - SubLog - The sub-log instance. + outputs: + sublog instance. + """ sl = SubLog( diff --git a/tests/tests/test_sublog.py b/tests/tests/test_sublog.py new file mode 100644 index 0000000..ffaf871 --- /dev/null +++ b/tests/tests/test_sublog.py @@ -0,0 +1,390 @@ +"""Tests for the sub-logging feature.""" + +import logging +import os +import sys + +import pytest + +import fancylog +from fancylog.sublog import SubLog, sub_log + + +class TestSubLogCreation: + """Test that sub-logs are created correctly.""" + + def test_sub_log_creates_file(self, tmp_path): + """A sub-log should create a separate log file.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_test", + log_to_console=False, + ) + + sl = SubLog( + "preprocessing", + tmp_path, + parent_logger_name="main_test", + timestamp=False, + ) + + assert os.path.exists(sl.log_file) + assert sl.log_file == os.path.join(tmp_path, "preprocessing.log") + + sl.close() + + def test_sub_log_creates_file_with_timestamp(self, tmp_path): + """A sub-log with timestamp=True should have a timestamped filename.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_ts", + log_to_console=False, + ) + + sl = SubLog( + "preprocessing", + tmp_path, + parent_logger_name="main_ts", + timestamp=True, + ) + + # Should not be just "preprocessing.log" + assert "preprocessing_" in os.path.basename(sl.log_file) + assert sl.log_file.endswith(".log") + assert os.path.exists(sl.log_file) + + sl.close() + + def test_sub_log_has_own_logger(self, tmp_path): + """The sub-log should have its own named logger.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_logger_test", + log_to_console=False, + ) + + sl = SubLog( + "mysublog", + tmp_path, + parent_logger_name="main_logger_test", + timestamp=False, + ) + + assert sl.logger.name == "main_logger_test.sublog.mysublog" + assert sl.logger.propagate is False + + sl.close() + + def test_sub_log_without_parent_name(self, tmp_path): + """Sub-log without parent_logger_name uses fancylog prefix.""" + sl = SubLog( + "orphan", + tmp_path, + parent_logger_name=None, + timestamp=False, + ) + + assert sl.logger.name == "fancylog.sublog.orphan" + + sl.close() + + +class TestSubLogWriting: + """Test that sub-logs write messages correctly.""" + + def test_sub_log_writes_to_own_file(self, tmp_path): + """Messages logged to sub-log should appear in its file.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_write", + log_to_console=False, + ) + + sl = SubLog( + "step1", + tmp_path, + parent_logger_name="main_write", + timestamp=False, + ) + + sl.logger.info("Sub-log specific message") + sl.close() + + with open(sl.log_file) as f: + content = f.read() + + assert "Sub-log specific message" in content + + def test_sub_log_does_not_write_to_main_log(self, tmp_path): + """Messages logged to sub-log should NOT appear in the main log.""" + main_log = fancylog.start_logging( + tmp_path, fancylog, logger_name="main_isolated", + log_to_console=False, + timestamp=False, + ) + + sl = SubLog( + "isolated_step", + tmp_path, + parent_logger_name="main_isolated", + timestamp=False, + ) + + sl.logger.info("ONLY_IN_SUBLOG_12345") + sl.close() + + with open(main_log) as f: + main_content = f.read() + + # The sub-log message itself should not be in the main log + # (only the reference messages should be) + assert "ONLY_IN_SUBLOG_12345" not in main_content + + def test_main_log_contains_reference(self, tmp_path): + """The main log should contain a reference to the sub-log file.""" + main_log = fancylog.start_logging( + tmp_path, fancylog, logger_name="main_ref", + log_to_console=False, + timestamp=False, + ) + + sl = SubLog( + "referenced_step", + tmp_path, + parent_logger_name="main_ref", + timestamp=False, + ) + + sl.logger.info("doing work") + sl.close() + + with open(main_log) as f: + main_content = f.read() + + assert "Starting sub-log 'referenced_step'" in main_content + assert "referenced_step.log" in main_content + assert "Sub-log 'referenced_step' finished" in main_content + + +class TestSubLogContextManager: + """Test the sub_log context manager.""" + + def test_context_manager_creates_and_closes(self, tmp_path): + """The context manager should create a sub-log and close it.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_ctx", + log_to_console=False, + ) + + with sub_log( + "ctx_step", + tmp_path, + parent_logger_name="main_ctx", + timestamp=False, + ) as sl: + sl.logger.info("Inside context manager") + log_file = sl.log_file + + # After context manager exits, file should exist with content + with open(log_file) as f: + content = f.read() + + assert "Inside context manager" in content + assert "Sub-log 'ctx_step' finished" in content + + def test_context_manager_closes_on_exception(self, tmp_path): + """The context manager should close the sub-log even on exception.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_exc", + log_to_console=False, + ) + + log_file = None + with pytest.raises(ValueError, match="test error"): + with sub_log( + "exc_step", + tmp_path, + parent_logger_name="main_exc", + timestamp=False, + ) as sl: + log_file = sl.log_file + sl.logger.info("Before exception") + raise ValueError("test error") + + # Sub-log should still have been written and closed + assert log_file is not None + with open(log_file) as f: + content = f.read() + + assert "Before exception" in content + assert "Sub-log 'exc_step' finished" in content + + def test_multiple_sub_logs(self, tmp_path): + """Multiple sub-logs can be created sequentially.""" + main_log = fancylog.start_logging( + tmp_path, fancylog, logger_name="main_multi", + log_to_console=False, + timestamp=False, + ) + + with sub_log( + "step1", tmp_path, + parent_logger_name="main_multi", + timestamp=False, + ) as sl1: + sl1.logger.info("Step 1 message") + + with sub_log( + "step2", tmp_path, + parent_logger_name="main_multi", + timestamp=False, + ) as sl2: + sl2.logger.info("Step 2 message") + + # Both sub-log files should exist + assert os.path.exists(os.path.join(tmp_path, "step1.log")) + assert os.path.exists(os.path.join(tmp_path, "step2.log")) + + # Main log should reference both + with open(main_log) as f: + main_content = f.read() + + assert "step1" in main_content + assert "step2" in main_content + + +class TestSubLogSubprocess: + """Test subprocess capture in sub-logs.""" + + def test_run_subprocess_captures_stdout(self, tmp_path): + """Subprocess stdout should be captured in the sub-log.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_proc", + log_to_console=False, + ) + + with sub_log( + "tool_output", + tmp_path, + parent_logger_name="main_proc", + timestamp=False, + ) as sl: + result = sl.run_subprocess( + [sys.executable, "-c", "print('hello from tool')"] + ) + + assert result.returncode == 0 + + with open(sl.log_file) as f: + content = f.read() + + assert "[stdout] hello from tool" in content + + def test_run_subprocess_captures_stderr(self, tmp_path): + """Subprocess stderr should be captured in the sub-log.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_stderr", + log_to_console=False, + ) + + with sub_log( + "tool_errors", + tmp_path, + parent_logger_name="main_stderr", + timestamp=False, + ) as sl: + result = sl.run_subprocess( + [ + sys.executable, "-c", + "import sys; sys.stderr.write('warning msg\\n')" + ] + ) + + with open(sl.log_file) as f: + content = f.read() + + assert "[stderr] warning msg" in content + + def test_run_subprocess_logs_return_code(self, tmp_path): + """Subprocess return code should be logged.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_rc", + log_to_console=False, + ) + + with sub_log( + "tool_rc", + tmp_path, + parent_logger_name="main_rc", + timestamp=False, + ) as sl: + sl.run_subprocess( + [sys.executable, "-c", "exit(0)"] + ) + + with open(sl.log_file) as f: + content = f.read() + + assert "Command finished with return code 0" in content + + def test_run_subprocess_nonzero_exit(self, tmp_path): + """Non-zero return codes from subprocesses should be logged.""" + fancylog.start_logging( + tmp_path, fancylog, logger_name="main_nz", + log_to_console=False, + ) + + with sub_log( + "tool_fail", + tmp_path, + parent_logger_name="main_nz", + timestamp=False, + ) as sl: + result = sl.run_subprocess( + [sys.executable, "-c", "exit(1)"] + ) + + assert result.returncode == 1 + + with open(sl.log_file) as f: + content = f.read() + + assert "Command finished with return code 1" in content + + +class TestSubLogHandlerCleanup: + """Test that handlers are properly cleaned up.""" + + def test_handlers_removed_on_close(self, tmp_path): + """All handlers should be removed when sub-log is closed.""" + sl = SubLog( + "cleanup_test", + tmp_path, + timestamp=False, + ) + + assert len(sl.logger.handlers) > 0 + + sl.close() + + assert len(sl.logger.handlers) == 0 + + def test_handlers_removed_after_context_manager(self, tmp_path): + """Handlers should be removed after context manager exits.""" + with sub_log( + "ctx_cleanup", tmp_path, timestamp=False + ) as sl: + logger = sl.logger + assert len(logger.handlers) > 0 + + assert len(logger.handlers) == 0 + + +class TestSubLogImports: + """Test that sub-log classes are importable from fancylog.""" + + def test_sublog_importable_from_fancylog(self): + """SubLog should be importable from fancylog package.""" + assert hasattr(fancylog, "SubLog") + assert hasattr(fancylog, "sub_log") + + def test_sublog_is_correct_class(self): + """Imported SubLog should be the correct class.""" + assert fancylog.SubLog is SubLog + assert fancylog.sub_log is sub_log \ No newline at end of file From 311420506dacc79bc3a107b17aeb04e578cfddc9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 08:52:02 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- example.py | 2 +- fancylog/sublog.py | 4 +- tests/tests/test_sublog.py | 92 ++++++++++++++++++++++++-------------- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/example.py b/example.py index 853b9ae..e7e42c3 100644 --- a/example.py +++ b/example.py @@ -78,4 +78,4 @@ def main(directory): ) args = parser.parse_args() - main(args.directory[0]) \ No newline at end of file + main(args.directory[0]) diff --git a/fancylog/sublog.py b/fancylog/sublog.py index bbc746b..dbcb16c 100644 --- a/fancylog/sublog.py +++ b/fancylog/sublog.py @@ -199,9 +199,9 @@ def sub_log( whether to add a timestamp to the sub-log filename. Default: True. - outputs: + outputs: sublog instance. - + """ sl = SubLog( diff --git a/tests/tests/test_sublog.py b/tests/tests/test_sublog.py index ffaf871..fc1b431 100644 --- a/tests/tests/test_sublog.py +++ b/tests/tests/test_sublog.py @@ -1,6 +1,5 @@ """Tests for the sub-logging feature.""" -import logging import os import sys @@ -16,7 +15,9 @@ class TestSubLogCreation: def test_sub_log_creates_file(self, tmp_path): """A sub-log should create a separate log file.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_test", + tmp_path, + fancylog, + logger_name="main_test", log_to_console=False, ) @@ -35,7 +36,9 @@ def test_sub_log_creates_file(self, tmp_path): def test_sub_log_creates_file_with_timestamp(self, tmp_path): """A sub-log with timestamp=True should have a timestamped filename.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_ts", + tmp_path, + fancylog, + logger_name="main_ts", log_to_console=False, ) @@ -56,7 +59,9 @@ def test_sub_log_creates_file_with_timestamp(self, tmp_path): def test_sub_log_has_own_logger(self, tmp_path): """The sub-log should have its own named logger.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_logger_test", + tmp_path, + fancylog, + logger_name="main_logger_test", log_to_console=False, ) @@ -92,7 +97,9 @@ class TestSubLogWriting: def test_sub_log_writes_to_own_file(self, tmp_path): """Messages logged to sub-log should appear in its file.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_write", + tmp_path, + fancylog, + logger_name="main_write", log_to_console=False, ) @@ -114,7 +121,9 @@ def test_sub_log_writes_to_own_file(self, tmp_path): def test_sub_log_does_not_write_to_main_log(self, tmp_path): """Messages logged to sub-log should NOT appear in the main log.""" main_log = fancylog.start_logging( - tmp_path, fancylog, logger_name="main_isolated", + tmp_path, + fancylog, + logger_name="main_isolated", log_to_console=False, timestamp=False, ) @@ -139,7 +148,9 @@ def test_sub_log_does_not_write_to_main_log(self, tmp_path): def test_main_log_contains_reference(self, tmp_path): """The main log should contain a reference to the sub-log file.""" main_log = fancylog.start_logging( - tmp_path, fancylog, logger_name="main_ref", + tmp_path, + fancylog, + logger_name="main_ref", log_to_console=False, timestamp=False, ) @@ -168,7 +179,9 @@ class TestSubLogContextManager: def test_context_manager_creates_and_closes(self, tmp_path): """The context manager should create a sub-log and close it.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_ctx", + tmp_path, + fancylog, + logger_name="main_ctx", log_to_console=False, ) @@ -191,21 +204,25 @@ def test_context_manager_creates_and_closes(self, tmp_path): def test_context_manager_closes_on_exception(self, tmp_path): """The context manager should close the sub-log even on exception.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_exc", + tmp_path, + fancylog, + logger_name="main_exc", log_to_console=False, ) log_file = None - with pytest.raises(ValueError, match="test error"): - with sub_log( + with ( + pytest.raises(ValueError, match="test error"), + sub_log( "exc_step", tmp_path, parent_logger_name="main_exc", timestamp=False, - ) as sl: - log_file = sl.log_file - sl.logger.info("Before exception") - raise ValueError("test error") + ) as sl, + ): + log_file = sl.log_file + sl.logger.info("Before exception") + raise ValueError("test error") # Sub-log should still have been written and closed assert log_file is not None @@ -218,20 +235,24 @@ def test_context_manager_closes_on_exception(self, tmp_path): def test_multiple_sub_logs(self, tmp_path): """Multiple sub-logs can be created sequentially.""" main_log = fancylog.start_logging( - tmp_path, fancylog, logger_name="main_multi", + tmp_path, + fancylog, + logger_name="main_multi", log_to_console=False, timestamp=False, ) with sub_log( - "step1", tmp_path, + "step1", + tmp_path, parent_logger_name="main_multi", timestamp=False, ) as sl1: sl1.logger.info("Step 1 message") with sub_log( - "step2", tmp_path, + "step2", + tmp_path, parent_logger_name="main_multi", timestamp=False, ) as sl2: @@ -255,7 +276,9 @@ class TestSubLogSubprocess: def test_run_subprocess_captures_stdout(self, tmp_path): """Subprocess stdout should be captured in the sub-log.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_proc", + tmp_path, + fancylog, + logger_name="main_proc", log_to_console=False, ) @@ -279,7 +302,9 @@ def test_run_subprocess_captures_stdout(self, tmp_path): def test_run_subprocess_captures_stderr(self, tmp_path): """Subprocess stderr should be captured in the sub-log.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_stderr", + tmp_path, + fancylog, + logger_name="main_stderr", log_to_console=False, ) @@ -291,8 +316,9 @@ def test_run_subprocess_captures_stderr(self, tmp_path): ) as sl: result = sl.run_subprocess( [ - sys.executable, "-c", - "import sys; sys.stderr.write('warning msg\\n')" + sys.executable, + "-c", + "import sys; sys.stderr.write('warning msg\\n')", ] ) @@ -304,7 +330,9 @@ def test_run_subprocess_captures_stderr(self, tmp_path): def test_run_subprocess_logs_return_code(self, tmp_path): """Subprocess return code should be logged.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_rc", + tmp_path, + fancylog, + logger_name="main_rc", log_to_console=False, ) @@ -314,9 +342,7 @@ def test_run_subprocess_logs_return_code(self, tmp_path): parent_logger_name="main_rc", timestamp=False, ) as sl: - sl.run_subprocess( - [sys.executable, "-c", "exit(0)"] - ) + sl.run_subprocess([sys.executable, "-c", "exit(0)"]) with open(sl.log_file) as f: content = f.read() @@ -326,7 +352,9 @@ def test_run_subprocess_logs_return_code(self, tmp_path): def test_run_subprocess_nonzero_exit(self, tmp_path): """Non-zero return codes from subprocesses should be logged.""" fancylog.start_logging( - tmp_path, fancylog, logger_name="main_nz", + tmp_path, + fancylog, + logger_name="main_nz", log_to_console=False, ) @@ -336,9 +364,7 @@ def test_run_subprocess_nonzero_exit(self, tmp_path): parent_logger_name="main_nz", timestamp=False, ) as sl: - result = sl.run_subprocess( - [sys.executable, "-c", "exit(1)"] - ) + result = sl.run_subprocess([sys.executable, "-c", "exit(1)"]) assert result.returncode == 1 @@ -367,9 +393,7 @@ def test_handlers_removed_on_close(self, tmp_path): def test_handlers_removed_after_context_manager(self, tmp_path): """Handlers should be removed after context manager exits.""" - with sub_log( - "ctx_cleanup", tmp_path, timestamp=False - ) as sl: + with sub_log("ctx_cleanup", tmp_path, timestamp=False) as sl: logger = sl.logger assert len(logger.handlers) > 0 @@ -387,4 +411,4 @@ def test_sublog_importable_from_fancylog(self): def test_sublog_is_correct_class(self): """Imported SubLog should be the correct class.""" assert fancylog.SubLog is SubLog - assert fancylog.sub_log is sub_log \ No newline at end of file + assert fancylog.sub_log is sub_log From 7d880ffdff2ed416497b33d93b30516dafc70f3c Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 28 Feb 2026 14:24:38 +0530 Subject: [PATCH 6/6] works --- fancylog/sublog.py | 6 +++--- tests/tests/test_sublog.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fancylog/sublog.py b/fancylog/sublog.py index dbcb16c..1521bb2 100644 --- a/fancylog/sublog.py +++ b/fancylog/sublog.py @@ -52,6 +52,7 @@ def __init__( log_to_console=False, timestamp=True, ): + """Initialize a sub-log logger and its output file.""" self.name = name self.output_dir = str(output_dir) self.parent_logger_name = parent_logger_name @@ -130,7 +131,7 @@ def run_subprocess(self, command, **kwargs): The command to run (passed to subprocess.run). **kwargs Additional keyword arguments passed to subprocess.run. - Note: stdout and stderr will be set to subprocess.PIPE + Note: capture_output will be enabled internally and cannot be overridden. Returns @@ -147,8 +148,7 @@ def run_subprocess(self, command, **kwargs): result = subprocess.run( command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, **kwargs, ) diff --git a/tests/tests/test_sublog.py b/tests/tests/test_sublog.py index fc1b431..01ae7e2 100644 --- a/tests/tests/test_sublog.py +++ b/tests/tests/test_sublog.py @@ -314,7 +314,7 @@ def test_run_subprocess_captures_stderr(self, tmp_path): parent_logger_name="main_stderr", timestamp=False, ) as sl: - result = sl.run_subprocess( + sl.run_subprocess( [ sys.executable, "-c",