Repository: rhayes777/PyAutoFit (python)

Issue:
Updates to root.log
The following config file creates root.log:

https://github.com/rhayes777/PyAutoFit/blob/main/autofit/config/logging.yaml

This contains useful informtion, putting the console output to the file.

However, the file `root.log` is always made in the current working directory of the use, which makes it fairly useless to track.

Could we have it so that `root.log` is always written to the output folder of the search being run?

If a user does not supply a search name (so not hard-disk output) the `root.log` file should not be created.

It should also be customizeable in `output.yaml` if root.log is created.



Pull request description:
feature/search log handler
Resolves #985


Patch:
diff --git a/autofit/config/output.yaml b/autofit/config/output.yaml
index 821354e98..b0679036d 100644
--- a/autofit/config/output.yaml
+++ b/autofit/config/output.yaml
@@ -88,3 +88,6 @@ covariance: true # `covariance.csv`: The [free parameters x free parameters] cov
 data: true # `data.json`: The value of every data point in the data.
 noise_map: true # `noise_map.json`: The value of every RMS noise map value.
 
+search_log: true # `search.log`: logging produced whilst running the fit or fit_sequential method
+
+default: false
\ No newline at end of file
diff --git a/autofit/non_linear/search/abstract_search.py b/autofit/non_linear/search/abstract_search.py
index ece3fb100..06b653c4e 100644
--- a/autofit/non_linear/search/abstract_search.py
+++ b/autofit/non_linear/search/abstract_search.py
@@ -15,6 +15,10 @@ if TYPE_CHECKING:
     from autofit.non_linear.result import Result
 
 from autoconf import conf, cached_property
+
+from autoconf.dictable import to_dict
+from autoconf.output import should_output
+
 from autofit import exc, jax_wrapper
 from autofit.database.sqlalchemy_ import sa
 from autofit.graphical import (
@@ -74,6 +78,45 @@ def check_cores(func):
     return wrapper
 
 
+def configure_handler(func):
+    """
+    Add a file handler for logging during the course of the search.
+
+    Optionally outputs 'search.log' to the search's output directory. Can be
+    turned on or off in the output.yaml file.
+
+    Parameters
+    ----------
+    func
+        Some function for which logging should be output to file
+
+    Returns
+    -------
+    A decorated version of the function
+    """
+    root_logger = logging.getLogger()
+
+    def decorated(self, *args, **kwargs):
+        if not should_output("search_log"):
+            return func(self, *args, **kwargs)
+        try:
+            os.makedirs(
+                self.paths.output_path,
+                exist_ok=True,
+            )
+            handler = logging.FileHandler(self.paths.output_path / "search.log")
+            root_logger.addHandler(handler)
+        except AttributeError:
+            return func(self, *args, **kwargs)
+
+        try:
+            return func(self, *args, **kwargs)
+        finally:
+            root_logger.removeHandler(handler)
+
+    return decorated
+
+
 class NonLinearSearch(AbstractFactorOptimiser, ABC):
     def __init__(
         self,
@@ -628,6 +671,7 @@ class NonLinearSearch(AbstractFactorOptimiser, ABC):
                     f"turned on and set to {timeout_seconds} seconds.***\n"
                 )
 
+    @configure_handler
     def start_resume_fit(self, analysis: Analysis, model: AbstractPriorModel) -> Result:
         """
         Start a non-linear search from scratch, or resumes one which was previously terminated mid-way through.


Test patch:
diff --git a/test_autofit/non_linear/search/test_logging.py b/test_autofit/non_linear/search/test_logging.py
new file mode 100644
index 000000000..0743f9441
--- /dev/null
+++ b/test_autofit/non_linear/search/test_logging.py
@@ -0,0 +1,38 @@
+import logging
+import shutil
+
+import pytest
+
+import autofit as af
+from autoconf.conf import with_config
+
+
+class Analysis(af.mock.MockAnalysis):
+    def log_likelihood_function(self, instance):
+        logging.warning("Loud logging")
+        return super().log_likelihood_function(instance)
+
+
+@pytest.fixture(name="run_search")
+def make_run_search():
+    def run_search():
+        search = af.mock.MockSearch("name", fit_fast=False)
+        search.paths.remove_files = False
+        analysis = Analysis()
+        model = af.Model(af.Gaussian)
+        search.fit(model, analysis)
+        return search
+
+    return run_search
+
+
+def test_logging(run_search):
+    search = run_search()
+    assert (search.paths.output_path / "search.log").exists()
+
+
+@with_config("output", "search_log", value=False)
+def test_logging_off(run_search, output_directory):
+    shutil.rmtree(output_directory)
+    search = run_search()
+    assert not (search.paths.output_path / "search.log").exists()