Source code for et_micc.project

# -*- coding: utf-8 -*-

"""
Module et_micc.project
======================

An OO interface to *micc* projects.

"""
import os
import shutil
import json
from pathlib import Path
import subprocess
from operator import xor

import click
import semantic_version
from sphinx.cmd.build import main as sphinx_build

from et_micc.tomlfile import TomlFile
import et_micc.utils
import et_micc.expand
import et_micc.logger
from et_micc import __version__

CURRENT_ET_MICC_BUILD_VERSION = __version__


def micc_version():
    return __version__


[docs]class Project: """ An OO interface to *micc* projects. :param types.SimpleNameSpace options: all options from the ``micc`` CLI. """ def __init__(self, options): self.exit_code = 0 self.logger = None self.options = options project_path = options.project_path if hasattr(options, 'template_parameters'): # only needed for expanding templates. template_parameters_json = project_path / 'micc.json' if template_parameters_json.exists(): options.template_parameters.update( et_micc.expand.get_template_parameters(template_parameters_json) ) else: options.template_parameters.update( et_micc.expand.get_template_parameters( et_micc.expand.get_preferences(Path('.')) ) ) if et_micc.utils.is_project_directory(project_path, self): # existing project self.get_logger() self.version = self.pyproject_toml['tool']['poetry']['version'] else: # not a project directory or not a directory at all if getattr(options, 'create', False): if project_path.exists() and os.listdir(str(project_path)): self.error(f"Cannot create project in ({project_path}):\n" f" Directory must be empty." ) else: self.create() else: # all other micc commands require a project directory. self.error(f"Not a project directory ({project_path}).") @property def project_path(self): return self.options.project_path
[docs] def error(self, msg): """Print an error message :py:obj:`msg` and set the project's :py:obj:`exit_code`.""" click.secho("[ERROR]\n" + msg, fg='bright_red') self.exit_code = 1
[docs] def warning(self, msg): """Print an warning message :py:obj:`msg` and set the project's :py:obj:`exit_code`.""" click.secho("[WARNING]\n" + msg, fg='green')
[docs] def create(self): """Create a new project skeleton.""" self.project_path.mkdir(parents=True, exist_ok=True) if not self.options.allow_nesting: # Prevent the creation of a project inside another project p = self.project_path.parent.resolve() while not p.samefile('/'): if et_micc.utils.is_project_directory(p): self.error(f"Cannot create project in ({project_path}):\n" f" Specify '--allow-nesting' to create a et_micc project inside another et_micc project ({p})." ) return p = p.parent project_name = self.project_path.name if not et_micc.utils.verify_project_name(project_name): self.error(f"Invalid project name ({project_name}):\n" f" project name must start with char, and contain only chars, digits, hyphens and underscores." ) return self.project_name = project_name self.package_name = et_micc.utils.pep8_module_name(project_name) try: relative_project_path = self.project_path.relative_to(Path.cwd()) except ValueError: # project_path was specified relative to cwd using ../ # use full path instead of relative path relative_project_path = self.project_path if self.options.structure == 'module': structure = f"({relative_project_path}{os.sep}{self.package_name}.py)" elif self.options.structure == 'package': structure = f"({relative_project_path}{os.sep}{self.package_name}{os.sep}__init__.py)" else: structure = '' self.options.verbosity = max(1, self.options.verbosity) self.get_logger() with et_micc.logger.logtime(self): with et_micc.logger.log(self.logger.info , f"Creating project ({self.project_name}):" ): self.logger.info(f"Python {self.options.structure} ({self.package_name}): structure = {structure}") template_parameters = {'project_name': self.project_name , 'package_name': self.package_name } template_parameters.update(self.options.template_parameters) self.options.template_parameters = template_parameters self.options.overwrite = False self.exit_code = et_micc.expand.expand_templates(self.options) if self.exit_code: self.logger.critical(f"Exiting ({self.exit_code}) ...") return my_micc_file = self.project_path / 'micc.json' with my_micc_file.open('w') as f: json.dump(template_parameters, f) self.logger.debug(f" . Wrote project template parameters to {my_micc_file}.") with et_micc.logger.log(self.logger.info, "Creating git repository"): with et_micc.utils.in_directory(self.project_path): cmds = [['git', 'init'] , ['git', 'add', '*'] , ['git', 'add', '.gitignore'] , ['git', 'commit', '-m', '"first commit"'] ] if template_parameters['github_username']: cmds.extend( [['git', 'remote', 'add', 'origin', f"https://github.com/{template_parameters['github_username']}/{self.project_name}"] , ['git', 'push', '-u', 'origin', 'master'] ] ) et_micc.utils.execute(cmds, self.logger.debug, stop_on_error=False) self.logger.warning( "Run 'poetry install' in the project directory to create a virtual " "environment and install its dependencies." )
[docs] def module_to_package_cmd(self): """Convert a module project (:file:`module.py`) to a package project (:file:`package/__init__.py`).""" if self.package: self.warning(f"Project ({self.project_name}) is already a package ({self.package}).") return self.logger.info( f"Converting Python module project {self.project_name} to Python package project." ) # add documentation files for general Python project self.options.templates = "package-general-docs" self.options.template_parameters.update( {'project_short_description': self.pyproject_toml['tool']['poetry']['description']} ) self.exit_code = et_micc.expand.expand_templates(self.options) if self.exit_code: self.logger.critical( f"Expand failed during Project.module_to_package_cmd for project ({self.project_name})." ) return # move <package_name>.py to <package_name>/__init__.py package_path = self.project_path / self.package_name package_path.mkdir(exist_ok=True) src = self.project_path / (self.package_name + '.py') dst = self.project_path / self.package_name / '__init__.py' shutil.move(src, dst)
[docs] def info_cmd(self): """Output info on the project.""" if self.options.verbosity >= 0: self.options.verbosity = 10 if self.options.verbosity >= 1: click.echo("Project " + click.style(str(self.project_name), fg='green') + " located at " + click.style(str(self.project_path), fg='green') + "\n package: " + click.style(str(self.package_name), fg='green') + "\n version: " + click.style(self.version, fg='green') ) if self.options.verbosity >= 2: if self.module: kind = " (Python module)" source = str(self.module) else: kind = " (Python package)" source = str(self.package) click.echo(" structure: " + click.style(source, fg='green') + kind) if self.options.verbosity >= 3 and self.package: package_path = self.project_path / self.package_name files = [] files.extend(package_path.glob('**/*.py')) files.extend(package_path.glob('**/cpp_*/')) files.extend(package_path.glob('**/f2py_*')) if len(files) > 1: # __init__.py is always there. click.echo(" contents:") for f in files: # filters if '{' in str(f): continue if 'package-' in str(f): # very ad hoc solution, only relevant to the et_micc project itself continue if f.name == "__init__.py" and f.parent.samefile(package_path): # ignore the top-level __init__.py continue if 'build_' in str(f): continue fg = 'green' extra = '' if f.name.startswith('cli'): kind = "application " fg = 'blue' elif f.name.startswith('cpp_'): kind = "C++ module " extra = f"{os.sep}{f.name.split('_', 1)[1]}.cpp" elif f.name.startswith('f2py_'): kind = "f2py module " extra = f"{os.sep}{f.name.split('_', 1)[1]}.f90" elif f.name == '__init__.py': kind = "package " else: kind = "module " click.echo(" " + kind + click.style(str(f.relative_to(package_path)) + extra, fg=fg))
[docs] def version_cmd(self): """Bump the version according to :py:obj:`self.options.rule` or show the current version if no rule is specified. The version is stored in pyproject.toml in the project directory, and in :py:obj:`__version__` variable of the top-level package, which is either in :file:`<package_name>.py`, :file:`<package_name>/__init__.py`, or in :file:`<package_name>/__version__.py`. """ self.options.verbosity = max(1, self.options.verbosity) if not self.options.rule: if self.options.short: print(self.version) else: click.echo("Project " + click.style(f"({self.project_name}) ", fg='cyan') + "version " + click.style(f"({self.version}) ", fg='cyan') ) else: r = f"--{self.options.rule}" current_semver = semantic_version.Version(self.version) if self.options.rule == 'patch': new_semver = current_semver.next_patch() elif self.options.rule == 'minor': new_semver = current_semver.next_minor() elif self.options.rule == 'major': new_semver = current_semver.next_major() else: r = f"--rule {self.options.rule}" new_semver = semantic_version.Version(self.options.rule) # update pyproject.toml if not self.options.dry_run: self.pyproject_toml['tool']['poetry']['version'] = str(new_semver) self.pyproject_toml.save() # update __version__ look_for = f'__version__ = "{current_semver}"' replace_with = f'__version__ = "{new_semver}"' if self.module: # update in <package_name>.py et_micc.utils.replace_in_file(self.project_path / self.module, look_for, replace_with) else: # update in <package_name>/__init__.py p = self.project_path / self.package_name / "__version__.py" if p.exists(): et_micc.utils.replace_in_file(p, look_for, replace_with) else: p = self.project_path / self.package et_micc.utils.replace_in_file(p, look_for, replace_with) self.logger.info(f"({self.project_name})> micc version ({current_semver}) -> ({new_semver})") else: click.echo(f"({self.project_name})> micc version {r} --dry-run : " + click.style(f"({current_semver} ", fg='cyan') + "-> " + click.style(f"({new_semver})", fg='cyan') ) self.version = str(new_semver) # even if dry run!
[docs] def tag_cmd(self): """Create and push a version tag ``v<Major>.<minor>.<patch>`` for the current version.""" tag = f"v{self.version}" with et_micc.utils.in_directory(self.project_path): self.logger.info(f"Creating git tag {tag} for project {self.project_name}") cmd = ['git', 'tag', '-a', tag, '-m', f'"tag version {self.version}"'] self.logger.debug(f"Running '{' '.join(cmd)}'") completed_process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) self.logger.debug(completed_process.stdout.decode('utf-8')) if completed_process.stderr: self.logger.critical(completed_process.stderr.decode('utf-8')) self.logger.debug(f"Pushing tag {tag} for project {self.project_name}") cmd = ['git', 'push', 'origin', tag] self.logger.debug(f"Running '{' '.join(cmd)}'") completed_process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) if completed_process.returncode == 0: if completed_process.stdout: self.logger.debug(completed_process.stdout.decode('utf-8')) else: if completed_process.stdout: self.logger.warning(completed_process.stdout.decode('utf-8')) if completed_process.stderr: self.logger.warning(completed_process.stderr.decode('utf-8')) self.logger.warning(f"Failed '{' '.join(cmd)}'\nRerun the command later (you must be online).") self.logger.info('Done.')
[docs] def add_cmd(self): """Add some source file to the project. This method dispatches to * :py:meth:`add_app`, * :py:meth:`add_python_module`, * :py:meth:`add_f2py_module`, * :py:meth:`add_cpp_module` """ if self.module: self.error(f"Cannot add to a module project ({self.module}).\n" f" Use `micc convert-to-package' on this project to convert it to a package project." ) return # set implied flags: if self.options.group: app_implied = f" [implied by --group ({int(self.options.group)})]" self.options.app = True else: app_implied = "" if self.options.package: py_implied = f" [implied by --package ({int(self.options.package)})]" self.options.py = True else: py_implied = "" if (not (self.options.app or self.options.py or self.options.f2py or self.options.cpp) or not xor(xor(self.options.app, self.options.py), xor(self.options.f2py, self.options.cpp))): # Do not log, as the state of the project is not changed. self.error(f"Specify one and only one of \n" f" --app ({int(self.options.app)}){app_implied}\n" f" --py ({int(self.options.py)}){py_implied}\n" f" --f2py ({int(self.options.f2py)})\n" f" --cpp ({int(self.options.cpp)})\n", fg='bright_red' ) return if self.options.app: app_name = self.options.add_name if self.app_exists(app_name): self.error(f"Project {self.project_name} has already an app named {app_name}.") return if not et_micc.utils.verify_project_name(app_name): self.error( f"Not a valid app name ({app_name}_. Valid names:\n" f" * start with a letter [a-zA-Z]\n" f" * contain only [a-zA-Z], digits, hyphens, and underscores\n" ) return if self.options.group: if not self.options.templates: self.options.templates = 'app-sub-commands' else: if not self.options.templates: self.options.templates = 'app-simple' self.add_app() else: module_name = self.options.add_name if self.module_exists(module_name): self.error(f"Project {self.project_name} has already a module named {module_name}.") return if (not et_micc.utils.verify_project_name(module_name) or module_name != et_micc.utils.pep8_module_name(module_name)): self.error( f"Not a valid module name ({module_name}). Valid names:\n" f" * start with a letter [a-zA-Z]\n" f" * contain only [a-zA-Z], digits, and underscores\n" ) return # self.options.template_parameters['path_to_cmake'] = et_micc.utils.path_to_cmake() if self.options.py: self.options.structure = 'package' if self.options.package else 'module' if not self.options.templates: self.options.templates = 'module-py' self.add_python_module() elif self.options.f2py: if not self.options.templates: self.options.templates = 'module-f2py' self.add_f2py_module() elif self.options.cpp: if not self.options.templates: self.options.templates = 'module-cpp' self.add_cpp_module()
[docs] def add_app(self): """Add a console script (app) to the package.""" project_path = self.project_path app_name = self.options.add_name cli_app_name = 'cli_' + et_micc.utils.pep8_module_name(app_name) w = 'with' if self.options.group else 'without' with et_micc.logger.log(self.logger.info, f"Adding CLI {app_name} {w} sub-commands to project {project_path.name}."): self.options.template_parameters.update( {'app_name': app_name, 'cli_app_name': cli_app_name} ) self.exit_code = et_micc.expand.expand_templates(self.options) if self.exit_code: self.logger.critical( f"Expand failed during Project.add_app for project ({self.project_name})." ) return package_name = self.options.template_parameters['package_name'] src_file = os.path.join(project_path.name, package_name, f"cli_{app_name}.py") tst_file = os.path.join(project_path.name, 'tests', f"test_cli_{app_name}.py") self.logger.info(f"- Python source file {src_file}.") self.logger.info(f"- Python test code {tst_file}.") with et_micc.utils.in_directory(project_path): # docs with open('docs/index.rst', "r") as f: lines = f.readlines() has_already_apps = False api_line = -1 for l, line in enumerate(lines): has_already_apps = has_already_apps or line.startswith(" apps") if line.startswith(' api'): api_line = l if not has_already_apps: lines.insert(api_line, ' apps\n') with open('docs/index.rst', "w") as f: for line in lines: f.write(line) txt = '' if not Path('APPS.rst').exists(): title = "Command Line Interfaces (apps)" line = len(title) * '*' + '\n' txt += (line + title + '\n' + line + '\n' ) else: txt += (f".. click:: {package_name}.{cli_app_name}:main\n" f" :prog: {app_name}\n" f" :show-nested:\n\n" ) with open("APPS.rst", "a") as f: f.write(txt) # pyproject.toml self.add_dependencies({'click': '^7.0'}) self.pyproject_toml['tool']['poetry']['scripts'][app_name] = f"{package_name}:{cli_app_name}.main" self.pyproject_toml.save() # TODO: add 'import <package_name>.cli_<app_name> to __init__.py line = f"import {package_name}.cli_{app_name}\n" file = project_path / self.package et_micc.utils.insert_in_file(file, [line], before=True, startswith="__version__")
[docs] def add_python_module(self): """Add a python sub-module or sub-package to this project.""" project_path = self.project_path module_name = self.options.add_name if not module_name == et_micc.utils.pep8_module_name(module_name): self.error(f"Not a valid module_name: {module_name}") return source_file = f"{module_name}.py" if self.options.structure == 'module' else f"{module_name}{os.sep}__init__.py" with et_micc.logger.log(self.logger.info, f"Adding python module {source_file} to project {project_path.name}." ): self.options.template_parameters.update({'module_name': module_name}) self.exit_code = et_micc.expand.expand_templates(self.options) if self.exit_code: self.logger.critical( f"Expand failed during Project.add_python_module for project ({self.project_name})." ) return package_name = self.options.template_parameters['package_name'] if self.options.structure == 'package': self.module_to_package(project_path / package_name / (module_name + '.py')) package_name = self.options.template_parameters['package_name'] src_file = os.path.join(project_path.name, package_name, source_file) tst_file = os.path.join(project_path.name, 'tests', 'test_' + module_name + '.py') self.logger.info(f"- python source in {src_file}.") self.logger.info(f"- Python test code in {tst_file}.") with et_micc.utils.in_directory(project_path): # docs with open("API.rst", "a") as f: f.write(f"\n.. automodule:: {package_name}.{module_name}" "\n :members:\n\n" )
[docs] def add_f2py_module(self): """Add a f2py module to this project.""" project_path = self.project_path module_name = self.options.add_name with et_micc.logger.log(self.logger.info, f"Adding f2py module {module_name} to project {project_path.name}." ): self.options.template_parameters.update({'module_name': module_name}) self.exit_code = et_micc.expand.expand_templates(self.options) if self.exit_code: self.logger.critical( f"Expand failed during Project.add_f2py_module for project ({self.project_name})." ) package_name = self.options.template_parameters['package_name'] src_file = os.path.join(project_path.name , package_name , 'f2py_' + module_name , module_name + '.f90' ) tst_file = os.path.join(project_path.name , 'tests' , 'test_f2py_' + module_name + '.py' ) rst_file = os.path.join(project_path.name , package_name , 'f2py_' + module_name , module_name + '.rst' ) self.logger.info(f"- Fortran source in {src_file}.") self.logger.info(f"- Python test code in {tst_file}.") self.logger.info(f"- module documentation in {rst_file} (restructuredText format).") with et_micc.utils.in_directory(project_path): self.add_dependencies({'et-micc-build': f"^{CURRENT_ET_MICC_BUILD_VERSION}"}) # docs with open("API.rst", "a") as f: f.write(f"\n.. include:: ../{package_name}/f2py_{module_name}/{module_name}.rst\n") self.add_auto_build_code()
[docs] def add_auto_build_code(self): """Add auto build code for binary extension modules in :file:`__init__.py` of the package.""" module_name = self.options.add_name text_to_insert = [ "", "try:", f" import {self.package_name}.{module_name}", "except ModuleNotFoundError as e:", " # Try to build this binary extension:", " from pathlib import Path", " import click", " from et_micc_build.cli_micc_build import auto_build_binary_extension", f" msg = auto_build_binary_extension(Path(__file__).parent, '{module_name}')", " if not msg:", f" import {self.package_name}.{module_name}", " else:", f" click.secho(msg, fg='bright_red')", ] et_micc.utils.insert_in_file( self.project_path / self.package_name / "__init__.py", text_to_insert, startswith="__version__ = ", )
[docs] def add_cpp_module(self): """Add a cpp module to this project.""" project_path = self.project_path module_name = self.options.add_name with et_micc.logger.log(self.logger.info, f"Adding cpp module cpp_{module_name} to project {project_path.name}." ): self.options.template_parameters.update({'module_name': module_name}) self.exit_code = et_micc.expand.expand_templates(self.options) if self.exit_code: self.logger.critical( f"Expand failed during Project.add_cpp_module for project ({self.project_name})." ) return package_name = self.options.template_parameters['package_name'] src_file = os.path.join(project_path.name , package_name , 'cpp_' + module_name , module_name + '.cpp' ) tst_file = os.path.join(project_path.name , 'tests' , 'test_cpp_' + module_name + '.py' ) rst_file = os.path.join(project_path.name , package_name , 'cpp_' + module_name , module_name + '.rst' ) self.logger.info(f"- C++ source in {src_file}.") self.logger.info(f"- Python test code in {tst_file}.") self.logger.info(f"- module documentation in {rst_file} (restructuredText format).") with et_micc.utils.in_directory(project_path): self.add_dependencies({'et-micc-build': f"^{CURRENT_ET_MICC_BUILD_VERSION}"}) # docs with open("API.rst", "a") as f: f.write(f"\n.. include:: ../{package_name}/cpp_{module_name}/{module_name}.rst\n") self.add_auto_build_code()
[docs] def app_exists(self, app_name): """Test if there is already an app with name ``app_name`` in this project. * :file:`<package_name>/cli_<app_name>.py` :param str app_name: app name :returns: bool """ return (self.project_path / self.package_name / f"cli_{app_name}.py").is_file()
[docs] def module_exists(self, module_name): """Test if there is already a module with name py:obj:`module_name` in this project. This can be either a Python module, package, or a binary extension module. :param str module_name: module name :returns: bool """ return (self.py_module_exists(module_name) or self.py_package_exists(module_name) or self.cpp_module_exists(module_name) or self.f2py_module_exists(module_name) )
[docs] def py_module_exists(self, module_name): """Test if there is already a python module with name :py:obj:`module_name` in the project at :file:`project_path`. :param str module_name: module name :returns: bool """ file = self.project_path / self.package_name / f'{module_name}.py' return file.is_file()
[docs] def py_package_exists(self, module_name): """Test if there is already a python package with name :py:obj:`module_name` in the project at :file:`project_path`. :param str module_name: module name :returns: bool """ return (self.project_path / self.package_name / module_name / '__init__.py').is_file()
[docs] def f2py_module_exists(self, module_name): """Test if there is already a f2py module with name py:obj:`module_name` in this project. :param str module_name: module name :returns: bool """ return (self.project_path / self.package_name / ('f2py_' + module_name) / f"{module_name}.f90").is_file()
[docs] def cpp_module_exists(self, module_name): """Test if there is already a cpp module with name py:obj:`module_name` in this project. :param str module_name: module name :returns: bool """ return (self.project_path / self.package_name / ('cpp_' + module_name) / f"{module_name}.cpp").is_file()
[docs] def add_dependencies(self, deps): """Add dependencies to the :file:`pyproject.toml` file. :param dict deps: (package,version_constraint) pairs. """ tool_poetry_dependencies = self.pyproject_toml['tool']['poetry']['dependencies'] modified = False for pkg, version_constraint in deps.items(): if pkg in tool_poetry_dependencies: # project was already depending on this package range1 = et_micc.utils.version_range(version_constraint) range2 = et_micc.utils.version_range(tool_poetry_dependencies[pkg]) if range1 == range2: # nothing to do: new and old version specifcation are the same continue intersection = et_micc.utils.intersect(range1, range2) if et_micc.utils.validate_intersection(intersection): range = intersection else: range = et_micc.utils.most_recent(version_constraint, tool_poetry_dependencies[pkg]) tool_poetry_dependencies[pkg] = et_micc.utils.version_constraint(range) modified = True else: # an entirely new dependency tool_poetry_dependencies[pkg] = version_constraint modified = True if modified: self.pyproject_toml.save() self.logger.warning("Dependencies added. Run `poetry update` to update the project's virtual environment.")
[docs] def module_to_package(self, module_py): """Move file :file:`module.py` to :file:`module/__init__.py`. :param str|Path module_py: path to module.py """ module_py = Path(module_py).resolve() if not module_py.is_file(): raise FileNotFoundError(module_py) src = str(module_py) package_name = str(module_py.name).replace('.py', '') package = module_py.parent / package_name package.mkdir() dst = str(package / '__init__.py') shutil.move(src, dst) et_micc.logger.log(self.logger.debug, f" . Module {module_py} converted to package {package_name}{os.sep}__init__.py." )
# removed in favor of docs/Makefile # see https://github.com/etijskens/et-micc/issues/24 # def docs_cmd(self): # """Build documentation.""" # docs = self.project_path / 'docs' # open_cmds = [] # # for format_ in self.options.documentation_formats: # args = ['-M', format_, str(docs), str(docs / '_build')] # self.exit_code = sphinx_build(args) # if self.options.open: # if format_ == 'html': # open_cmds.append(['open', str(docs / '_build' / 'html' / 'index.html')]) # elif format_ == 'latexpdf': # open_cmds.append(['open', str(docs / '_build' / 'latex' / (self.project_name + ".pdf"))]) # # if open_cmds: # my_env = os.environ.copy() # et_micc.utils.execute(open_cmds, logfun=print, cwd=str(docs), env=my_env) def get_logger(self, log_file_path=None): """""" if self.logger: return if log_file_path: log_file_name = log_file_path.name log_file_dir = log_file_path.parent else: log_file_name = f"{self.options.project_path.name}.micc.log" log_file_dir = self.options.project_path log_file_path = log_file_dir / log_file_name self.log_file = log_file_path if getattr(self.options, 'clear_log', False): if log_file_path.exists(): log_file_path.unlink() # create a new logger object that will write to the log file and to the console self.logger = et_micc.logger.create_logger(log_file_path) # set the log level from the verbosity self.logger.console_handler.setLevel(et_micc.logger.verbosity_to_loglevel(self.options.verbosity)) if self.options.verbosity > 2: print(f"Current logfile = {log_file_path}") if getattr(self.options, 'clear_log', False): self.logger.info(f"The log file was cleared: {log_file_path}") self.options.clear_log = False self.options.logger = self.logger
# eof