Coverage for moptipy / api / improvement_logger.py: 85%
61 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 10:36 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-29 10:36 +0000
1"""A base class for logging improvements."""
2from os import remove
3from typing import Callable, Final
5from pycommons.io.path import Path
6from pycommons.types import check_int_range
8from moptipy.api.logging import FILE_SUFFIX
9from moptipy.utils.logger import FileLogger, Logger
11#: the default base directory for logging
12_DEFAULT_DIR_NAME: Final[str] = "improvements"
13#: the default base directory for logging
14_DEFAULT_BASE_DIR: Final[str] = f"./{_DEFAULT_DIR_NAME}"
15#: the default base name for log files
16_DEFAULT_BASE_NAME: Final[str] = "improvement"
19class ImprovementLogger:
20 """A improvement logger."""
22 def log_improvement(self, call: Callable[[Logger], None]) -> None:
23 """
24 Provide a logger to the callable to log an improvement.
26 This method is invoked whenever an improvement was discovered during
27 the optimization process. The method needs to create and provide a
28 :class:`~moptipy.utils.logger.Logger` to the callable it receives as
29 input.
31 :param call: A :class:`Callable` to invoke with a logger to write the
32 improvement information to.
33 """
34 raise NotImplementedError
37class ImprovementLoggerFactory:
38 """A factory for file improvement loggers."""
40 def create(self, reference_path: str | None,
41 reference_name: str | None) -> ImprovementLogger:
42 """
43 Create a new file improvement logger.
45 :param reference_path: a path to a file used as reference for creating
46 the logging base directory and file, or `None` if none is
47 available
48 :param reference_name: a name that can be used as reference for file
49 names, or `None` if none is available
50 :returns: the improvement logger
51 """
52 raise NotImplementedError
55class FileImprovementLogger(ImprovementLogger):
56 """
57 A file-based improvement logger.
59 This logger creates a new text file for each improvement.
60 The improved solution is stored in the text file.
61 The logger allows you to limit the number of retained files.
62 If more improvements than the provided limit are created, then it will
63 delete the oldest log file.
64 """
66 def __init__(self, log_dir: str | None = None,
67 log_base_name: str | None = None,
68 max_files: int | None = None) -> None:
69 """
70 Create the file improvement logger.
72 :param log_dir: the directory to store the improvement logs in
73 :param log_base_name: the base name of the improvement logs
74 :param max_files: a limit for the number of log files to be retained,
75 or `None` for unlimited
76 """
77 #: the logging directory
78 self.__log_dir: Final[Path] = Path(
79 _DEFAULT_BASE_DIR if log_dir is None else log_dir)
80 self.__log_dir.ensure_dir_exists()
82 log_base_name = _DEFAULT_BASE_NAME if log_base_name is None else (
83 str.removesuffix(str.strip(log_base_name), "_"))
84 if str.__len__(log_base_name) <= 0:
85 raise ValueError("Log base name cannot be empty or just "
86 "consist of whitespace.")
87 #: the log file base name
88 self.__log_base_name: Final[str] = log_base_name
89 #: the maximum number of files to keep alive
90 self.__max_files: Final[int | None] = None if max_files is None \
91 else check_int_range(max_files, "max_files", 1, 1_000_000_000_000)
92 #: the file history
93 self.__file_history: Final[list[Path] | None] = \
94 None if max_files is None else []
96 #: the file index
97 self.__index: int = 0
99 def log_improvement(self, call: Callable[[Logger], None]) -> None:
100 """
101 Log an improvement.
103 :param call: the callable to do the logging
104 """
105 file: Path | None = None
106 while True:
107 self.__index += 1
108 file = self.__log_dir.resolve_inside(
109 f"{self.__log_base_name}_{self.__index}{FILE_SUFFIX}")
110 if not file.ensure_file_exists():
111 break
113 call(FileLogger(file))
114 if (self.__file_history is not None) and (
115 self.__max_files is not None):
116 self.__file_history.append(file)
117 if list.__len__(self.__file_history) > self.__max_files:
118 remove(self.__file_history.pop(0))
121class FileImprovementLoggerFactory(ImprovementLoggerFactory):
122 """A file-based improvement logger factory."""
124 def __init__(self, base_dir: str | None = None,
125 log_base_name: str | None = None,
126 max_files: int | None = None) -> None:
127 """
128 Create the file improvement logger factory.
130 :param base_dir: the base directory to store the improvement
131 logs in, `None` for default
132 :param log_base_name: the default base name of the improvement logs,
133 `None` for default
134 :param max_files: a limit for the number of log files to be retained,
135 or `None` for unlimited
136 """
137 #: the logging directory
138 self.__base_dir: Final[Path] = Path(
139 _DEFAULT_BASE_DIR if base_dir is None else base_dir)
141 log_base_name = _DEFAULT_BASE_NAME if log_base_name is None \
142 else str.strip(log_base_name)
143 if str.__len__(log_base_name) <= 0:
144 raise ValueError("Log base name cannot be empty or just "
145 "consist of whitespace.")
146 #: the log file base name
147 self.__log_base_name: Final[str] = log_base_name
148 #: the maximum number of files to keep alive
149 self.__max_files: Final[int | None] = None if max_files is None \
150 else check_int_range(max_files, "max_files", 1, 1_000_000_000_000)
152 def create(self, reference_path: str | None,
153 reference_name: str | None) -> ImprovementLogger:
154 """
155 Create a new file improvement logger.
157 :param reference_path: a path to a file used as reference for creating
158 the logging base directory and file, or `None` if none is
159 available
160 :param reference_name: a name that can be used as reference for file
161 names, or `None` if none is available
162 :returns: the improvement logger
163 """
164 log_dir: Path | None = None
165 log_name: str | None = reference_name
167 if reference_path is not None:
168 pt: Path = Path(reference_path)
169 log_name = (pt.basename().removesuffix(FILE_SUFFIX)
170 .removesuffix("_") + f"_{_DEFAULT_BASE_NAME}")
171 log_dir = Path(str.removesuffix(
172 pt, FILE_SUFFIX).removesuffix("_") + f"_{_DEFAULT_DIR_NAME}")
174 if log_dir is None:
175 log_dir = self.__base_dir
176 log_dir.ensure_dir_exists()
177 if log_name is not None:
178 log_dir = log_dir.resolve_inside(str.removesuffix(
179 log_name, "_"))
180 if log_name is None:
181 log_name = self.__log_base_name
183 return FileImprovementLogger(log_dir, log_name, self.__max_files)