From b3fe42638e306c13ee916d50725957b12b87323e Mon Sep 17 00:00:00 2001 From: KeyurManiya Date: Mon, 2 Mar 2026 17:20:22 +0100 Subject: [PATCH] fix: use unique logger name per job to prevent log handler conflicts - logging.getLogger(__name__) returned same singleton for all jobs; each new job cleared previous job's file handler silently. Fixed by using log file path as unique logger name. - Propagate MeltingTemp handlers to Solid/Liquid sub-job loggers so all sub-job output also appears in melting_temperature.log. - Fix np.NaN -> np.nan for NumPy 2.0 compatibility. - Add tests for no handler accumulation and no cross-contamination. --- calphy/helpers.py | 4 ++-- calphy/routines.py | 6 ++++++ tests/test_helpers.py | 48 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/calphy/helpers.py b/calphy/helpers.py index a45b8233..fae77123 100644 --- a/calphy/helpers.py +++ b/calphy/helpers.py @@ -232,7 +232,7 @@ def write_data(lmp, file): def prepare_log(file, screen=False): - logger = logging.getLogger(__name__) + logger = logging.getLogger(file) # Remove all existing handlers to prevent duplicate logging for handler in logger.handlers[:]: @@ -240,7 +240,7 @@ def prepare_log(file, screen=False): logger.removeHandler(handler) handler = logging.FileHandler(file) - formatter = logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s") + formatter = logging.Formatter("%(asctime)s calphy.helpers %(levelname)-8s %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.DEBUG) diff --git a/calphy/routines.py b/calphy/routines.py index 0a353d65..8d1de7d0 100644 --- a/calphy/routines.py +++ b/calphy/routines.py @@ -168,6 +168,12 @@ def run_jobs(self): simfolder=self.calculations[1].create_folders(), ) + # Propagate MeltingTemp file handlers to sub-job loggers so that + # all sub-job output also appears in melting_temperature.log + for handler in self.logger.handlers: + self.soljob.logger.addHandler(handler) + self.lqdjob.logger.addHandler(handler) + self.logger.info( "Free energy of %s and %s phases will be calculated" % (self.soljob.calc.lattice, self.lqdjob.calc.lattice) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index bba12e53..0a19c32c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,6 @@ import pytest +import tempfile +import os import calphy.helpers as ch import numpy as np @@ -35,6 +37,50 @@ def test_validate_spring_constants(): e = ch.validate_spring_constants(d) assert e[0] == 1 - d = [1, np.NaN, 4] + d = [1, np.nan, 4] e = ch.validate_spring_constants(d) assert e[1] == 1 + +def test_prepare_log_no_handler_accumulation(): + """ + Calling prepare_log twice for the same file should not accumulate handlers. + Each call should reset to exactly one file handler. + """ + with tempfile.TemporaryDirectory() as tmpdir: + logfile = os.path.join(tmpdir, "test.log") + + logger1 = ch.prepare_log(logfile) + assert len(logger1.handlers) == 1, "Should have exactly 1 handler after first call" + + logger2 = ch.prepare_log(logfile) + assert len(logger2.handlers) == 1, "Should still have exactly 1 handler after second call" + assert logger1 is logger2, "Should return the same logger object" + +def test_prepare_log_no_cross_contamination(): + """ + Two independent calculations logging to different files must not + write into each other's log files. + """ + with tempfile.TemporaryDirectory() as tmpdir: + log1 = os.path.join(tmpdir, "calc1.log") + log2 = os.path.join(tmpdir, "calc2.log") + + logger1 = ch.prepare_log(log1) + logger1.info("message from calc1") + + logger2 = ch.prepare_log(log2) + logger2.info("message from calc2") + + # Flush all handlers + for h in logger1.handlers: + h.flush() + for h in logger2.handlers: + h.flush() + + content1 = open(log1).read() + content2 = open(log2).read() + + assert "message from calc1" in content1, "calc1.log should contain calc1 message" + assert "message from calc2" not in content1, "calc1.log must NOT contain calc2 message" + assert "message from calc2" in content2, "calc2.log should contain calc2 message" + assert "message from calc1" not in content2, "calc2.log must NOT contain calc1 message"