diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 87594a172..183301911 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,7 +19,7 @@ jobs: # execute the actual make build process - name: execute the make build run: make -# remove mopitpy +# remove moptipy - name: purge local moptipy installation run: | pip uninstall -y moptipy diff --git a/moptipy/api/_process_base.py b/moptipy/api/_process_base.py index 0c4aadafa..dcc2e6d5c 100644 --- a/moptipy/api/_process_base.py +++ b/moptipy/api/_process_base.py @@ -1,5 +1,4 @@ """An internal module with the base class for implementing Processes.""" -import os from io import StringIO from math import inf, isfinite from threading import Lock, Timer @@ -83,37 +82,26 @@ def _error_1(logger: Logger, title: str, exception_type, """ if exception_type or exception_value or traceback: with logger.text(title=title) as ts: + wt: Final[Callable[[str], None]] = ts.write if exception_type: - ts.write(KEY_EXCEPTION_TYPE) - ts.write(": ") if isinstance(exception_type, str): if exception_type.startswith(" None: @@ -125,29 +113,24 @@ def _error_2(logger: Logger, title: str, exception: Exception) -> None: created :param exception: the exception - >>> from moptipy.utils.logger import InMemoryLogger - >>> ime = InMemoryLogger() + >>> from moptipy.utils.logger import PrintLogger + >>> ime = PrintLogger() >>> def k(): ... 1 / 0 >>> try: ... k() ... except Exception as be: ... _error_2(ime, "ERROR", be) - >>> the_log = ime.get_log() - >>> print(the_log[0]) BEGIN_ERROR - >>> print(the_log[1]) exceptionType: ZeroDivisionError - >>> print(the_log[2]) exceptionValue: division by zero - >>> print(the_log[3]) exceptionStackTrace: - >>> print(the_log[-1]) + File "", line 2, in \ + + k() + File "", line 2, in k + 1 / 0 END_ERROR - >>> all(ss == ss.strip() for ss in the_log) - True - >>> all(len(ss) > 0 for ss in the_log) - True """ _error_1(logger, title, exception_type=exception, exception_value=str(exception), diff --git a/moptipy/evaluation/end_results.py b/moptipy/evaluation/end_results.py index 831dcfea2..be9d78361 100644 --- a/moptipy/evaluation/end_results.py +++ b/moptipy/evaluation/end_results.py @@ -60,7 +60,7 @@ PerRunData, ) from moptipy.evaluation.log_parser import SetupAndStateParser -from moptipy.utils.help import argparser +from moptipy.utils.help import moptipy_argparser from moptipy.utils.logger import CSV_SEPARATOR from moptipy.utils.math import try_float_div, try_int from moptipy.utils.strings import ( @@ -913,7 +913,7 @@ def lines(self, lines: list[str]) -> bool: # Run log files to end results if executed as script if __name__ == "__main__": - parser: Final[argparse.ArgumentParser] = argparser( + parser: Final[argparse.ArgumentParser] = moptipy_argparser( __file__, "Convert log files obtained with moptipy to the end results CSV " "format that can be post-processed or exported to other tools.", diff --git a/moptipy/evaluation/end_statistics.py b/moptipy/evaluation/end_statistics.py index 0d2f1a89f..bf3a96d68 100644 --- a/moptipy/evaluation/end_statistics.py +++ b/moptipy/evaluation/end_statistics.py @@ -49,7 +49,7 @@ KEY_STDDEV, Statistics, ) -from moptipy.utils.help import argparser +from moptipy.utils.help import moptipy_argparser from moptipy.utils.logger import CSV_SEPARATOR, SCOPE_SEPARATOR from moptipy.utils.math import try_int, try_int_div from moptipy.utils.strings import sanitize_name @@ -1276,7 +1276,7 @@ def __inner_sd(s: EndStatistics, ll1=l1) -> int | float | None: # Run end-results to stat file if executed as script if __name__ == "__main__": - parser: Final[argparse.ArgumentParser] = argparser( + parser: Final[argparse.ArgumentParser] = moptipy_argparser( __file__, "Build an end-results statistics CSV file.", "This program creates a CSV file with basic statistics on the " "end-of-run state of experiments conducted with moptipy. It " diff --git a/moptipy/evaluation/frequency.py b/moptipy/evaluation/frequency.py index 52267316b..1d7246d8d 100644 --- a/moptipy/evaluation/frequency.py +++ b/moptipy/evaluation/frequency.py @@ -36,7 +36,7 @@ PerRunData, ) from moptipy.evaluation.log_parser import SetupAndStateParser -from moptipy.utils.help import argparser +from moptipy.utils.help import moptipy_argparser from moptipy.utils.logger import CSV_SEPARATOR, SCOPE_SEPARATOR #: the lower bound of the objective function @@ -469,7 +469,7 @@ def lines(self, lines: list[str]) -> bool: # Print a CSV file if __name__ == "__main__": - parser: Final[argparse.ArgumentParser] = argparser( + parser: Final[argparse.ArgumentParser] = moptipy_argparser( __file__, "Collecting the Number of Existing Objective Values.", "Gather all the existing objective values and store them in a " "CSV-formatted file.") diff --git a/moptipy/evaluation/ioh_analyzer.py b/moptipy/evaluation/ioh_analyzer.py index 6c739a4c4..6a59f2ea5 100644 --- a/moptipy/evaluation/ioh_analyzer.py +++ b/moptipy/evaluation/ioh_analyzer.py @@ -51,7 +51,7 @@ from moptipy.evaluation.base import F_NAME_RAW, TIME_UNIT_FES, check_f_name from moptipy.evaluation.progress import Progress -from moptipy.utils.help import argparser +from moptipy.utils.help import moptipy_argparser def __prefix(s: str) -> str: @@ -241,7 +241,7 @@ def __consume(progress: Progress) -> None: # Run conversion if executed as script if __name__ == "__main__": - parser: Final[argparse.ArgumentParser] = argparser( + parser: Final[argparse.ArgumentParser] = moptipy_argparser( __file__, "Convert experimental results from the moptipy to the " "IOHanalyzer format.", diff --git a/moptipy/examples/__init__.py b/moptipy/examples/__init__.py index 4379393ac..a495c3032 100644 --- a/moptipy/examples/__init__.py +++ b/moptipy/examples/__init__.py @@ -4,6 +4,6 @@ You can find many more applications and examples at in our `moptipyapps` package building on `moptipy` at . Here, we mainly include examples that are useful for verifying the algorithms -and modules implemented in `mopitpy` and those useful for our "Optimization +and modules implemented in `moptipy` and those useful for our "Optimization Algorithms" book (see ). """ diff --git a/moptipy/examples/jssp/evaluation.py b/moptipy/examples/jssp/evaluation.py index 1993aaca1..e0ee0075a 100644 --- a/moptipy/examples/jssp/evaluation.py +++ b/moptipy/examples/jssp/evaluation.py @@ -32,7 +32,7 @@ plot_stat_gantt_charts, ) from moptipy.spaces.permutations import Permutations -from moptipy.utils.help import argparser +from moptipy.utils.help import moptipy_argparser from moptipy.utils.lang import EN from moptipy.utils.logger import sanitize_name from moptipy.utils.strings import ( @@ -594,7 +594,7 @@ def evaluate_experiment(results_dir: str = pp.join(".", "results"), # Evaluate experiment if run as script if __name__ == "__main__": - parser: Final[argparse.ArgumentParser] = argparser( + parser: Final[argparse.ArgumentParser] = moptipy_argparser( __file__, "Evaluate the results of the JSSP example experiment", "This experiment evaluates all the results of the JSSP example" " experiment and creates the figures and tables of the " diff --git a/moptipy/examples/jssp/experiment.py b/moptipy/examples/jssp/experiment.py index 3e8e88e27..b1ea2d95b 100644 --- a/moptipy/examples/jssp/experiment.py +++ b/moptipy/examples/jssp/experiment.py @@ -23,7 +23,7 @@ from moptipy.operators.permutations.op1_swap2 import Op1Swap2 from moptipy.operators.permutations.op1_swapn import Op1SwapN from moptipy.spaces.permutations import Permutations -from moptipy.utils.help import argparser +from moptipy.utils.help import moptipy_argparser #: The default instances to be used in our experiment. These have been #: computed via instance_selector.propose_instances. @@ -173,7 +173,7 @@ def creator(inst: Instance, algor: Callable = algo) -> Execution: # Execute experiment if run as script if __name__ == "__main__": - parser: Final[argparse.ArgumentParser] = argparser( + parser: Final[argparse.ArgumentParser] = moptipy_argparser( __file__, "Execute the JSSP example experiment.", "Execute an example experiment on the Job " "Shop Scheduling Problem (JSSP).") diff --git a/moptipy/examples/jssp/instance.py b/moptipy/examples/jssp/instance.py index be7853552..2db0c5555 100644 --- a/moptipy/examples/jssp/instance.py +++ b/moptipy/examples/jssp/instance.py @@ -81,7 +81,7 @@ deServlet/dbt_derivate_00001373/Dissertation.pdf """ from importlib import resources # nosem -from typing import Final, cast +from typing import Final, Iterable, cast import numpy as np from pycommons.types import check_int_range, type_error @@ -416,7 +416,7 @@ def from_text(name: str, rows: list[str]) -> "Instance": makespan_lower_bound=makespan_lower_bound) @staticmethod - def from_stream(name: str, stream) -> "Instance": + def from_stream(name: str, stream: Iterable[str]) -> "Instance": """ Load an instance from a text stream. @@ -427,7 +427,7 @@ def from_stream(name: str, stream) -> "Instance": state = 0 rows: list[str] | None = None for linestr in stream: - line = str(linestr).strip() + line = str.strip(linestr) if len(line) <= 0: continue if state == 0: diff --git a/moptipy/utils/help.py b/moptipy/utils/help.py index 81bb380b9..4c01c753b 100644 --- a/moptipy/utils/help.py +++ b/moptipy/utils/help.py @@ -1,84 +1,15 @@ """Print a help screen.""" + import argparse -import os.path -import sys -from typing import Final -from pycommons.io.path import Path -from pycommons.types import type_error +from pycommons.io.arguments import make_argparser, make_epilog from moptipy.version import __version__ -#: The default argument parser for moptipy executables. -__DEFAULT_ARGUMENTS: Final[argparse.ArgumentParser] = argparse.ArgumentParser( - epilog="Copyright\u00a0\u00a9\u00a02022\u00a0Thomas\u00a0WEISE, " - "GNU\u00a0GENERAL\u00a0PUBLIC\u00a0LICENSE\u00a0Version\u00a03," - "\u00a029\u00a0June\u00a02007, " - "https://thomasweise.github.io/moptipy, " - "tweise@hfuu.edu.cn,\u00a0tweise@ustc.edu.cn", - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, -) -__DEFAULT_ARGUMENTS.add_argument( - "--version", action="version", version=__version__) - - -def __get_python_interpreter_short() -> str: - """ - Get the python interpreter. - - :returns: the fully-qualified path - """ - inter: Final[str] = Path(sys.executable) - bn = os.path.basename(inter) - if bn.startswith("python3."): - bn2 = bn[:7] - interp2 = os.path.join(os.path.dirname(inter), bn2) - if os.path.exists(interp2) and os.path.isfile(interp2) \ - and (Path(interp2) == inter): - return bn2 - return bn - - -#: The python interpreter in short form. -__INTERPRETER_SHORT: Final[str] = __get_python_interpreter_short() -del __get_python_interpreter_short - -#: the base path of the moptipy package -__BASE_PATH: Final[str] = Path(os.path.dirname( - os.path.dirname(os.path.dirname(Path(__file__))))) + os.sep - - -def __get_prog(file: str) -> str: - """ - Get the program as to be displayed by the help screen. - - The result of this function applied to the `__file__` special - variable should be put into the `prog` argument of the constructor - of :class:`argparse.ArgumentParser`. - - :param file: the calling python script - :return: the program string - """ - if not isinstance(file, str): - raise type_error(file, "file", str) - - # get the module minus the base path and extension - module: str = Path(file) - end: int = len(module) - start: int = 0 - if module.endswith(".py"): - end -= 3 - if module.startswith(__BASE_PATH): - start += len(__BASE_PATH) - module = module[start:end].replace(os.sep, ".") - - return f"{__INTERPRETER_SHORT} -m {module}" - -def argparser(file: str, description: str, - epilog: str) -> argparse.ArgumentParser: +def moptipy_argparser(file: str, description: str, + epilog: str) -> argparse.ArgumentParser: """ Create an argument parser with default settings. @@ -87,26 +18,16 @@ def argparser(file: str, description: str, :param epilog: the epilogue string :returns: the argument parser - >>> ap = argparser(__file__, "This is a test program.", "This is a test.") + >>> ap = moptipy_argparser( + ... __file__, "This is a test program.", "This is a test.") >>> isinstance(ap, argparse.ArgumentParser) True >>> "Copyright" in ap.epilog True """ - if not isinstance(file, str): - raise type_error(file, "file", str) - if len(file) <= 3: - raise ValueError(f"invalid file={file!r}.") - if not isinstance(description, str): - raise type_error(description, "description", str) - if len(description) <= 12: - raise ValueError(f"invalid description={description!r}.") - if not isinstance(epilog, str): - raise type_error(epilog, "epilog", str) - if len(epilog) <= 10: - raise ValueError(f"invalid epilog={epilog!r}.") - return argparse.ArgumentParser( - parents=[__DEFAULT_ARGUMENTS], prog=__get_prog(file), - description=description.strip(), - epilog=f"{epilog.strip()} {__DEFAULT_ARGUMENTS.epilog}", - formatter_class=__DEFAULT_ARGUMENTS.formatter_class) + return make_argparser( + file, description, + make_epilog(epilog, 2022, 2024, "Thomas Weise", + url="https://thomasweise.github.io/moptipy", + email="tweise@hfuu.edu.cn, tweise@ustc.edu.cn"), + __version__) diff --git a/moptipy/utils/logger.py b/moptipy/utils/logger.py index 10f67a769..59cb11eee 100644 --- a/moptipy/utils/logger.py +++ b/moptipy/utils/logger.py @@ -33,12 +33,11 @@ from contextlib import AbstractContextManager from io import StringIO, TextIOBase from math import isfinite -from os.path import realpath from re import sub -from typing import Callable, Final, Iterable, cast +from typing import Any, Callable, Final, Iterable, cast from pycommons.ds.cache import str_is_new -from pycommons.io.path import Path +from pycommons.io.path import Path, line_writer from pycommons.strings.string_conv import bool_to_str, float_to_str from pycommons.types import type_error @@ -75,25 +74,26 @@ class Logger(AbstractContextManager): experiments with `moptipy`. """ - def __init__(self, stream: TextIOBase, name: str) -> None: + def __init__(self, name: str, writer: Callable[[str], Any], + closer: Callable[[], Any] = lambda: None) -> None: """ Create a new logger. - :param stream: the stream to which we will log, will be closed when - the logger is closed + :param writer: the string writing function + :param closer: the function to be invoked when the stream is closed :param name: the name of the logger """ if not isinstance(name, str): raise type_error(name, "name", str) - if stream is None: - raise ValueError("stream must be valid stream but is None.") - if not isinstance(stream, TextIOBase): - raise type_error(stream, "stream", TextIOBase) + if not callable(writer): + raise type_error(writer, "writer", call=True) + if not callable(closer): + raise type_error(closer, "closer", call=True) #: The internal stream - self._stream: TextIOBase = stream + self._writer: Callable[[str], Any] | None = writer + self._closer: Callable[[], Any] | None = closer self.__section: str | None = None - self.__starts_new_line: bool = True self.__log_name: str = name self.__sections: Callable = str_is_new() self.__closer: str | None = None @@ -129,11 +129,10 @@ def __exit__(self, exception_type, exception_value, traceback) -> None: """ if self.__section is not None: self._error("Cannot close logger, because section still open") - if self._stream is not None: - if not self.__starts_new_line: - self._stream.write("\n") - self._stream.close() - self._stream = None + if self._closer is not None: + self._closer() + self._closer = None + self._writer = None def _open_section(self, title: str) -> None: """ @@ -141,7 +140,7 @@ def _open_section(self, title: str) -> None: :param title: the section title """ - if self._stream is None: + if self._writer is None: self._error(f"Cannot open section {title!r} " "because logger already closed") @@ -157,9 +156,8 @@ def _open_section(self, title: str) -> None: if not self.__sections(title): self._error(f"Section {title!r} already done") - self._stream.write(f"{SECTION_START}{title}\n") - self.__closer = f"{SECTION_END}{title}\n" - self.__starts_new_line = True + self._writer(f"{SECTION_START}{title}") + self.__closer = f"{SECTION_END}{title}" self.__section = title def _close_section(self, title: str) -> None: @@ -170,18 +168,13 @@ def _close_section(self, title: str) -> None: """ if (self.__section is None) or (self.__section != title): self._error(f"Cannot open section {title!r} since it is not open") - printer = self.__closer - if not self.__starts_new_line: - printer = "\n" + printer - - self._stream.write(printer) + self._writer(self.__closer) self.__closer = None - self.__starts_new_line = True self.__section = None def _comment(self, comment: str) -> None: """ - Write a comment. + Write a comment line. :param comment: the comment """ @@ -190,8 +183,7 @@ def _comment(self, comment: str) -> None: if len(comment) <= 0: return comment = sub(r"\s+", " ", comment.strip()) - self._stream.write(f"{COMMENT_CHAR} {comment}\n") - self.__starts_new_line = True + self._writer(f"{COMMENT_CHAR} {comment}") def _write(self, text: str) -> None: """ @@ -209,9 +201,10 @@ def _write(self, text: str) -> None: self._error(f"String {self.__closer!r} " "must not be contained in output") - text = text.replace("#", "") # omit all # characters - self._stream.write(text) - self.__starts_new_line = text.endswith("\n") + if COMMENT_CHAR in text: + raise ValueError( + f"{COMMENT_CHAR!r} not permitted in text {text!r}.") + self._writer(text) def key_values(self, title: str) -> "KeyValueLogSection": r""" @@ -292,7 +285,7 @@ def text(self, title: str) -> "TextLogSection": ... tx.write("\n") ... tx.write("ccccc") ... print(l.get_log()) - ['BEGIN_C', 'aaaaaabbbbb', 'ccccc', 'END_C'] + ['BEGIN_C', 'aaaaaa', 'bbbbb', '', 'ccccc', 'END_C'] """ return TextLogSection(title=title, logger=self) @@ -309,8 +302,16 @@ def __init__(self, path: str) -> None: if not isinstance(path, str): raise type_error(path, "path", str) name = path - path = realpath(path) - super().__init__(stream=Path(path).open_for_write(), name=name) + tio: TextIOBase = Path(path).open_for_write() + super().__init__(name, line_writer(tio), tio.close) + + +class PrintLogger(Logger): + """A logger logging to stdout.""" + + def __init__(self) -> None: + """Initialize the logger.""" + super().__init__("printer", print) class InMemoryLogger(Logger): @@ -318,8 +319,9 @@ class InMemoryLogger(Logger): def __init__(self) -> None: """Initialize the logger.""" - super().__init__(stream=StringIO(), - name="in-memory-logger") + #: the internal stream + self.__stream: Final[StringIO] = StringIO() + super().__init__("in-memory-logger", line_writer(self.__stream)) def get_log(self) -> list[str]: """ @@ -327,7 +329,7 @@ def get_log(self) -> list[str]: :return: a list of strings with the logged lines """ - return cast(StringIO, self._stream).getvalue().splitlines() + return cast(StringIO, self.__stream).getvalue().splitlines() class LogSection(AbstractContextManager): @@ -383,7 +385,7 @@ def comment(self, comment: str) -> None: ... tx.write("aaaaaa") ... tx.comment("hello") ... print(l.get_log()) - ['BEGIN_A', 'aaaaaa# hello', 'END_A'] + ['BEGIN_A', 'aaaaaa', '# hello', 'END_A'] """ # noinspection PyProtectedMember self._logger._comment(comment) @@ -430,8 +432,7 @@ def __init__(self, title: str, logger: Logger, header: list[str]) -> None: logger._error(f"Invalid column {c}") # noinspection PyProtectedMember - logger._write(CSV_SEPARATOR.join( - [c.strip() for c in header]) + "\n") + logger._writer(CSV_SEPARATOR.join(c.strip() for c in header)) def row(self, row: tuple[int | float | bool, ...] | list[int | float | bool]) -> None: @@ -455,7 +456,7 @@ def row(self, row: tuple[int | float | bool, ...] for c in row) # noinspection PyProtectedMember - self._logger._write(f"{CSV_SEPARATOR.join(txt)}\n") + self._logger._write(CSV_SEPARATOR.join(txt)) class KeyValueLogSection(LogSection): @@ -545,15 +546,15 @@ def key_value(self, key: str, value, the_hex = hex(value) txt = KEY_VALUE_SEPARATOR.join([key, txt]) - txt = f"{txt}\n" + txt = f"{txt}" if the_hex: tmp = KEY_VALUE_SEPARATOR.join( [key + KEY_HEX_VALUE, the_hex]) - txt = f"{txt}{tmp}\n" + txt = f"{txt}\n{tmp}" # noinspection PyProtectedMember - self._logger._write(txt) + self._logger._writer(txt) def scope(self, prefix: str) -> "KeyValueLogSection": """ @@ -626,7 +627,7 @@ def __init__(self, title: str, logger: Logger) -> None: """ super().__init__(title, logger) # noinspection PyProtectedMember - self.write = self._logger._write # type: ignore + self.write = self._logger._writer # type: ignore def parse_key_values(lines: Iterable[str]) -> dict[str, str]: diff --git a/moptipy/utils/plot_utils.py b/moptipy/utils/plot_utils.py index 6fc83a1ac..b81582811 100644 --- a/moptipy/utils/plot_utils.py +++ b/moptipy/utils/plot_utils.py @@ -20,7 +20,7 @@ # Ensure that matplotlib uses Type 1 fonts. # Some scientific conferences, such as GECCO organized by ACM, require this. -# In the language utilities :meth:`~mopitpy.utils.lang.Lang.font`, we do +# In the language utilities :meth:`~moptipy.utils.lang.Lang.font`, we do # return acceptable fonts anyway, but it may be better to set this here # explicitly to avoid any problem. rcParams["pdf.fonttype"] = 42