#!/usr/bin/env python3 # -*- coding: utf-8 -*- # PYTHON_ARGCOMPLETE_OK # # Copyright (C) 1998-2026 Stephane Galland # # This program is free library; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 3 of the # License, or any later version. # # This library is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; see the file COPYING. If not, # write to the Free Software Foundation, Inc., 59 Temple Place - Suite # 330, Boston, MA 02111-1307, USA. """ Abstract implementation for main program. """ import textwrap from abc import ABC, abstractmethod import argparse from collections import defaultdict, deque import logging import os import platform import sys import argcomplete from argparse import Namespace from typing import Any, override import autolatex2.cli.exiters as exiters from autolatex2.cli.autolatexcommands import AutolatexCommand from autolatex2.config.configobj import Config from autolatex2.config.configreader import OldStyleConfigReader from autolatex2.translator.translatorobj import TranslatorLevel from autolatex2.utils.extlogging import LogLevel, DynamicLogLevelFormatter import autolatex2.utils.extprint as eprintpkg import autolatex2.utils.utilfunctions as genutils from autolatex2.utils.i18n import T class AbstractAutoLaTeXMain(ABC): """ Abstract implementation for a main program. """ def __init__(self, read_system_config : bool = True, read_user_config : bool = True, args : list[str] | None = None, exiter : exiters.AutoLaTeXExiter | None = None): """ Constructor. :param read_system_config: Indicates if the system-level configuration must be read. Default is True. :type read_system_config: bool :param read_user_config: Indicates if the user-level configuration must be read. Default is True. :type read_user_config: bool :param args: List of command line arguments. If it is None, the system args are used. :type args: list :param exiter: The instance of the object that is called when the program should stop. :type exiter: exiters.AutoLaTeXExiter | None """ self.__initial_argv = args self.__read_document_configuration = True if exiter: self.__exiter : exiters.AutoLaTeXExiter = exiter else: self.__exiter : exiters.AutoLaTeXExiter = exiters.AutoLaTeXSysExitExiter() # Create the AutoLaTeX configuration object self.__configuration = Config() # Initialization of the logging system (must be after configuration creation) self.__logging_handler = None self._init_logging_system() # Initialization that depends on the script itself script_launch_name = os.path.basename(sys.argv[0]) script_path, script_ext = os.path.splitext(sys.argv[0]) script_name = os.path.basename(script_path) self.__configuration.name = script_name self.__configuration.launch_name = script_launch_name # Read the configuration from the system config_reader = OldStyleConfigReader() if read_system_config: config_reader.read_system_config_safely(self.__configuration) # Read the configuration from the user home if read_user_config: config_reader.read_user_config_safely(self.__configuration) # Create the CLI parser self._cli_parser = self._create_cli_parser(name = self.__configuration.name, version = self.__configuration.version, epilog = self._build_help_epilog()) @property def configuration(self) -> Config: """ Replies the global configuration of the program. :rtype: Config """ return self.__configuration @configuration.setter def configuration(self, config : Config): """ Change the global configuration of AutoLaTeX. :type config: Config """ self.__configuration = config @property def cli_parser(self) -> argparse.ArgumentParser: """ Replies the CLI parser. :rtype: argparse.ArgumentParser """ return self._cli_parser @property def exiter(self) -> exiters.AutoLaTeXExiter: """ Replies the component for stopping the autolatex main program. :rtype: exiters.AutoLaTeXExiter """ return self.__exiter @exiter.setter def exiter(self, exiter : exiters.AutoLaTeXExiter): """ Change the component for stopping the autolatex main program. :type exiter: exiters.AutoLaTeXExiter """ if exiter: self.__exiter = exiter else: self.__exiter = exiters.AutoLaTeXSysExitExiter() def show_configuration(self): """ Show up the configuration of the program. """ eprintpkg.epprint(self.configuration) def _detect_autolatex_configuration_file(self, directory : str | None) -> str | None: """ Search for a configuration file in the given directory or one of its parent directories. :param directory: The start of the search. :type directory: str :return: the path to the configuration file, or None. :rtype: str """ if directory: root = os.path.abspath(os.sep) if os.path.isdir(directory): path = os.path.abspath(directory) else: path = os.path.dirname(os.path.abspath(directory)) while path and path != root and os.path.isdir(path): filename = self.configuration.make_document_config_filename(path) if filename and os.path.isfile(filename): return filename path = os.path.dirname(path) return None def add_cli_options(self, parser : argparse.ArgumentParser): """ Add the definition of the application CLI options. :param parser: the CLI parser :type parser: argparse.ArgumentParser """ pass def add_cli_positional_arguments(self, parser : argparse.ArgumentParser): """ Add the definition of the application CLI positional arguments. :param parser: the CLI parser :type parser: argparse.ArgumentParser """ pass def add_bootstrap_cli_options(self): """ Add the definition of the CLI options for bootstrapping: general ones and those for path management. """ self._add_standard_cli_options_general() self._add_standard_cli_options_path() def add_standard_cli_options(self): """ Add the definition of the standard CLI options, except the general ones and those for path management. """ self._add_standard_cli_options_output() self._add_standard_cli_options_tex() self._add_standard_cli_options_translator() self._add_standard_cli_options_biblio() self._add_standard_cli_options_index() self._add_standard_cli_options_glossary() self._add_standard_cli_options_warning() self._add_standard_cli_options_logging() def _add_standard_cli_options_general(self): """ Add standard CLI options in the "general" category. """ # --version self._cli_parser.add_argument('--version', action = 'version', help = T('Display the version of AutoLaTeX')) def _add_standard_cli_options_path(self): """ Add standard CLI options in the "path configuration" category. """ path_group = self._cli_parser.add_argument_group(T('path optional arguments')) input_method_group = path_group.add_mutually_exclusive_group() outer : AbstractAutoLaTeXMain = self # --directory class DirectoryAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): if os.path.isdir(value): outer.configuration.document_directory = value outer.configuration.document_filename = None else: logging.error(T("Invalid directory: %s") % value) outer._exit_on_failure() input_method_group.add_argument('-d', '--directory', action = DirectoryAction, help = T('Specify a directory in which a LaTeX document to compile is located. You could specify this option for each directory in which you have a LaTeX document to treat')) # --file class FileAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): if os.path.isfile(value): outer.configuration.set_document_directory_and_filename(value) else: logging.error(T("File not found: %s") % value) outer._exit_on_failure() input_method_group.add_argument('-f', '--file', action = FileAction, metavar = 'TEX_FILE', help = T('Specify the main LaTeX file to compile. If this option is not specified, AutoLaTeX will search for a TeX file in the current directory')) # --search-project-from class SearchProjectFromAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): if value: config_file = outer._detect_autolatex_configuration_file(value) else: config_file = outer._detect_autolatex_configuration_file(outer.configuration.document_directory) if config_file: config_reader = OldStyleConfigReader() outer.configuration = config_reader.read_document_config_safely(config_file, outer.configuration) outer.__read_document_configuration = False path_group.add_argument('--search-project-from', action=SearchProjectFromAction, metavar = 'FILE', help = T('When this option is specified, AutoLaTeX is searching a project configuration file (usually \'.autolatex_project.cfg\' on Unix platforms) in the directory of the specified FILE or in one of its ancestors')) def _add_standard_cli_options_output(self): """ Add standard CLI options in the "output configuration" category. """ output_group = self._cli_parser.add_argument_group(T('output optional arguments')) output_type_group = output_group.add_mutually_exclusive_group() outer : AbstractAutoLaTeXMain = self # --pdf class PdfAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.pdf_mode = True output_type_group.add_argument('--pdf', action = PdfAction, nargs = 0, help = T('Do the compilation to produce a PDF document')) # --dvi # --ps class DvipsAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.pdf_mode = False output_type_group.add_argument('--dvi', '--ps', action = DvipsAction, nargs = 0, help = T('Do the compilation to produce a DVI, XDV or Postscript document')) # --stdout # --stderr class StdouterrAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): eprintpkg.IS_STANDARD_OUTPUT = self.const std_output_group = output_group.add_mutually_exclusive_group() std_output_group.add_argument('--stdout', action = StdouterrAction, const = True, nargs = 0, help = T('All the standard messages (no log message) are printed out on the standard output (stdout) of the process')) std_output_group.add_argument('--stderr', action = StdouterrAction, const = False, nargs = 0, help = T('All the standard messages (no log message) are printed out on the standard error output (stderr) of the process')) def _add_standard_cli_options_tex(self): """ Add standard CLI options in the "tex configuration" category. """ tex_group = self._cli_parser.add_argument_group(T('TeX optional arguments')) tex_tool_group = tex_group.add_mutually_exclusive_group() outer : AbstractAutoLaTeXMain = self # --pdflatex class PdflatexCmdAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.latex_compiler = 'pdflatex' tex_tool_group.add_argument('--pdflatex', action = PdflatexCmdAction, nargs = 0, help = T('Use the LaTeX command: \'pdflatex\'')) # --latex class LatexCmdAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.latex_compiler = 'latex' tex_tool_group.add_argument('--latex', action = LatexCmdAction, nargs = 0, help = T('Use the historical LaTeX command: \'latex\'')) # --lualatex class LualatexCmdAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.latex_compiler = 'lualatex' tex_tool_group.add_argument('--lualatex', action = LualatexCmdAction, nargs = 0, help = T('Use the LaTeX command: \'lualatex\'')) # --xelatex class XelatexCmdAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.latex_compiler = 'xelatex' tex_tool_group.add_argument('--xelatex', action = XelatexCmdAction, nargs = 0, help = T('Use the LaTeX command: \'xelatex\'')) # --synctex # --nosynctex synctex_group = tex_group.add_mutually_exclusive_group() class SynctexAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.synctex = self.const synctex_group.add_argument('--synctex', action = SynctexAction, const = True, nargs = 0, help = T('Enable the generation of the output file with SyncTeX')) synctex_group.add_argument('--nosynctex', action = SynctexAction, const = False, nargs = 0, help = T('Disable the generation of the output file with SyncTeX')) # --extramacros # --noextramacros extramacros_group = tex_group.add_mutually_exclusive_group() class ExtramacrosAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.include_extra_macros = self.const extramacros_group.add_argument('--extramacro', action = ExtramacrosAction, const = True, nargs = 0, help = T('Enable the support of the extra TeX and LaTeX macros and environments')) synctex_group.add_argument('--noextramacro', action = ExtramacrosAction, const = False, nargs = 0, help = T('Disable the support of the extra TeX and LaTeX macros and environments')) def _add_standard_cli_options_translator(self): """ Add standard CLI options in the "translator configuration" category. """ translator_group = self._cli_parser.add_argument_group(T('translator optional arguments')) outer : AbstractAutoLaTeXMain = self # --auto # --noauto class AutoAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.translators.is_translator_enable = self.const enable_translator_group = translator_group.add_mutually_exclusive_group() enable_translator_group.add_argument('--auto', action = AutoAction, const = True, nargs = 0, help = T('Enable the auto generation of the figures')) enable_translator_group.add_argument('--noauto', action = AutoAction, const = False, nargs = 0, help = T('Disable the auto generation of the figures')) # --exclude class ExcludeAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.translators.set_included(value, TranslatorLevel.DOCUMENT, False) translator_group.add_argument('-e', '--exclude', action = ExcludeAction, metavar = 'TRANSLATOR', help = T('Avoid AutoLaTeX to load the translator named TRANSLATOR')) # --include class IncludeAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.translators.set_included(value, TranslatorLevel.DOCUMENT, True) translator_group.add_argument('-i', '--include', action = IncludeAction, metavar = 'TRANSLATOR', help = T('Force AutoLaTeX to load the translator named TRANSLATOR')) # --include-path class IncludePathAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): paths = genutils.to_path_list(value, outer.configuration.document_directory) for path in paths: outer.configuration.translators.add_include_path(path) translator_group.add_argument('-I', '--include-path', action = IncludePathAction, metavar = 'PATH', help = T('Notify AutoLaTeX that it could find translator scripts inside the specified directories. The specified PATH could be a list of paths separated by the operating system\'s path separator (\':\' for Unix, \';\' for Windows for example)')) # --imgdirectory class ImgDirectoryAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): paths = genutils.to_path_list(value, outer.configuration.document_directory) for path in paths: outer.configuration.translators.add_image_path(path) translator_group.add_argument('-D', '--imgdirectory', action = ImgDirectoryAction, metavar = 'DIRECTORY', help = T('Specify a directory inside which AutoLaTeX will find the pictures which must be processed by the translators. Each time this option is put on the command line, a directory is added inside the list of the directories to explore')) def _add_standard_cli_options_biblio(self): """ Add standard CLI options in the "bibliography configuration" category. """ biblio_group = self._cli_parser.add_argument_group(T('bibliography optional arguments')) outer : AbstractAutoLaTeXMain = self # --biblio # --nobiblio class BiblioAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.is_biblio_enable = self.const enable_biblio_group = biblio_group.add_mutually_exclusive_group() enable_biblio_group.add_argument('--biblio', action=BiblioAction, const = True, nargs = 0, help = T('Enable the call to the bibliography tool (BibTeX, Biber...)')) enable_biblio_group.add_argument('--nobiblio', action=BiblioAction, const = False, nargs = 0, help = T('Disable the call to the bibliography tool (BibTeX, Biber...)')) def _add_standard_cli_options_index(self): """ Add standard CLI options in the "index configuration" category. """ index_group = self._cli_parser.add_argument_group(T('index optional arguments')) outer : AbstractAutoLaTeXMain = self # --defaultist class DefaultistAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.makeindex_style_filename = outer.configuration.get_system_ist_file() index_group.add_argument('--defaultist', action = DefaultistAction, nargs = 0, help = T('Allow AutoLaTeX to use MakeIndex with the default style (\'.ist\' file)')) # --index index_e_group = index_group.add_mutually_exclusive_group() class IndexAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.is_index_enable = True if value: path = genutils.abs_path(value, outer.configuration.document_directory) if os.path.isfile(path): outer.configuration.generation.makeindex_style_filename = path else: logging.error(T("File not found: %s") % value) outer._exit_on_failure() return index_e_group.add_argument('--index', action = IndexAction, default = None, nargs = '?', metavar = 'FILE', help = T('Allow AutoLaTeX to use MakeIndex. If this option was specified with a value, the FILE value will be assumed to be an \'.ist\' file to pass to MakeIndex')) # --noindex class NoindexAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.is_index_enable = False index_e_group.add_argument('--noindex', action = NoindexAction, nargs = 0, help = T('Avoid AutoLaTeX to use MakeIndex')) def _add_standard_cli_options_glossary(self): """ Add standard CLI options in the "glossary configuration" category. """ glossary_group = self._cli_parser.add_argument_group(T('glossary optional arguments')) outer : AbstractAutoLaTeXMain = self # --glossary # --noglossary # --gloss # --nogloss class GlossaryAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.is_glossary_enable = self.const glossary_e_group = glossary_group.add_mutually_exclusive_group() glossary_e_group.add_argument('--glossary', '--gloss', action=GlossaryAction, const = True, nargs = 0, help = T('Enable the call to the glossary tool (makeglossaries...)')) glossary_e_group.add_argument('--noglossary', '--nogloss', action=GlossaryAction, const = False, nargs = 0, help = T('Disable the call to the glossary tool (makeglossaries...)')) def _add_standard_cli_options_warning(self): """ Add standard CLI options in the "warning configuration" category. """ warning_cfg_group = self._cli_parser.add_argument_group(T('warning optional arguments')) outer : AbstractAutoLaTeXMain = self # --file-line-warning # --nofile-line-warning class FilelinewarningAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): outer.configuration.generation.extended_warnings = self.const warning_group = warning_cfg_group.add_mutually_exclusive_group() warning_group.add_argument('--file-line-warning', action=FilelinewarningAction, const = True, nargs=0, help = T('Enable the extended format for warnings. This format add the filename and the line number where the warning occurs, before the warning message by itself')) warning_group.add_argument('--nofile-line-warning', action=FilelinewarningAction, const = False, nargs=0, help = T('Disable the extended format for warnings. This format add the filename and the line number where the warning occurs, before the warning message by itself')) def _add_standard_cli_options_logging(self): """ Add standard CLI options in the "logging configuration" category. """ logging_group = self._cli_parser.add_argument_group(T('logging optional arguments')) outer : AbstractAutoLaTeXMain = self # --debug class DebugAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() logger.setLevel(LogLevel.DEBUG) for handler in logger.handlers: handler.setLevel(LogLevel.DEBUG) logging_group.add_argument('--debug', action = DebugAction, nargs = 0, help = T('Run AutoLaTeX in debug mode, i.e., the maximum logging level')) # --quiet class QuietAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() logger.setLevel(LogLevel.ERROR) for handler in logger.handlers: handler.setLevel(LogLevel.ERROR) logging_group.add_argument('-q', '--quiet', action = QuietAction, nargs = 0, help = T('Run AutoLaTeX without logging except the errors')) # --silent class SilentAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() logger.setLevel(LogLevel.OFF) for handler in logger.handlers: handler.setLevel(LogLevel.OFF) logging_group.add_argument('--silent', action = SilentAction, nargs = 0, help = T('Run AutoLaTeX without logging, including no error message')) # --info class InfoAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() logger.setLevel(LogLevel.INFO) for handler in logger.handlers: handler.setLevel(LogLevel.INFO) logging_group.add_argument('--info', action = InfoAction, nargs = 0, help = T('Run AutoLaTeX with info logging level')) # --verbose class VerboseAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() level = LogLevel.to_lower_level(logger.level) if level < LogLevel.TRACE: # Specific behavior that shows up the configuration outer.show_configuration() outer._exit_on_success() else: logger.setLevel(level) for handler in logger.handlers: handler.setLevel(level) logging_group.add_argument('-v', '--verbose', action = VerboseAction, nargs = 0, help = T('Each time this option was specified, AutoLaTeX is more verbose')) # --Wall class WallAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() logger.setLevel(LogLevel.FINE_WARNING) for handler in logger.handlers: handler.setLevel(LogLevel.FINE_WARNING) logging_group.add_argument('--Wall', action = WallAction, nargs = 0, help = T('Show all the warnings')) #--Wnone class WnoneAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() logger.setLevel(LogLevel.ERROR) for handler in logger.handlers: handler.setLevel(LogLevel.ERROR) logging_group.add_argument('--Wnone', action = WnoneAction, nargs = 0, help = T('Show no warning')) #--showloglevel class ShowloglevelAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): logger = logging.getLogger() level = logger.level level_name = LogLevel.get_logging_level_name(level) eprintpkg.eprint("%s (%d)" % (level_name, level)) outer._exit_on_success() logging_group.add_argument('--showloglevel', action = ShowloglevelAction, nargs = 0, help = T('Show the current level of logging')) #--testlogs class TestlogsAction(argparse.Action): @override def __call__(self, parser, namespace, value, option_string=None): for level in LogLevel: level_name = LogLevel.get_logging_level_name(level) logging.log(level, T("message for level '%s' (%d)") % (level_name, level)) outer._exit_on_success() logging_group.add_argument('--testlogs', action = TestlogsAction, nargs = 0, help = T('Show a message at each level of logging')) def _init_logging_system(self): """ Configure the logging system. """ if self.__configuration.logging.message: logging.basicConfig(format = self.__configuration.logging.message, level = self.__configuration.logging.level) else: handler = logging.StreamHandler() handler.setFormatter(DynamicLogLevelFormatter()) logger = logging.getLogger() logger.addHandler(handler) logger.setLevel(self.__configuration.logging.level) # noinspection PyMethodMayBeStatic def _build_help_epilog(self) -> str | None: """ Build a string that could serve as help epilog. :return: the epilog text. :rtype: str | None """ return None # noinspection PyMethodMayBeStatic def _create_cli_parser(self, name : str, version : str, default_arg : Any | None = None, description : str | None = None, osname : str | None = None, platform_name : str | None = None, epilog : str | None = None) -> argparse.ArgumentParser: """ Create the instance of the CLI parser. :param name: The name of the program. :type name: str :param version: The version of the program. :type version: str :param default_arg: The default argument for the program. :type default_arg: Any | None :param description: The description of the program. :type description: str | None :param osname: The name of the operating system. :type osname: str | None :param platform_name: The name of the platform. :type platform_name: str | None :param epilog: The epilog of the documentation. :type epilog: str | None :return: the created instance. :rtype: argparse.ArgumentParser """ if not description: description = T('AutoLaTeX is a tool for managing small to large sized LaTeX documents. The user can easily ' 'perform all required steps to do such tasks as: preview the document, or produce a PDF file. ' 'AutoLaTeX will keep track of files that have changed and how to run the various programs that ' 'are needed to produce the output. One of the best feature of AutoLaTeX is to provide translator ' 'rules (aka. translators) to automatically generate the figures which will be included into the PDF.') if not osname: osname = os.name if not platform_name: platform_name = platform.system() parser = argparse.ArgumentParser(prog=name, argument_default=default_arg, description=description, epilog=epilog, formatter_class=argparse.HelpFormatter) parser.version = T("%s %s - %s/%s platform") % (name, version, osname, platform_name) return parser def _pre_run_program(self, strict_arguments : bool) -> tuple[Namespace,list[str],list[str]]: """ Run the general behavior of the main program before the specific behavior related to commands. :param strict_arguments: Indicates if only the arguments from the main script and the associated commands are allowed. If True, the CLI arguments are parsed strictly and if an argument is not known, there is a failure. If False, the function doers not fail if it is encountering an unknown argument. :type strict_arguments: bool :return: the tuple with as first element the parsed CLI arguments (argparse namespace), the actions , and the second element the list of unknown arguments. :rtype: tuple[Namespace,list[str],list[str]] """ # Get the command line value if self.__initial_argv is None or not isinstance(self.__initial_argv, list): script_cli = sys.argv[1:] else: script_cli = list(self.__initial_argv) # Force the document_directory to have a default value if not self.__configuration.document_directory: self.__configuration.document_directory = os.getcwd() # Parse command line options that are the base and may change the behavior of the rest of the tool self.add_bootstrap_cli_options() self.add_standard_cli_options() self.add_cli_options(self._cli_parser) self.add_cli_positional_arguments(self._cli_parser) # Parse the rest of the command line according to the registered CLI definition positional_arguments = [arg for arg in script_cli if not arg.startswith('-')] # Check if a command is provided; and add the default command. if not positional_arguments: default_action = self.configuration.default_cli_action if default_action: positional_arguments.insert(0, default_action) # Enable auto-completion of the command-line arguments on Linux systems argcomplete.autocomplete(self._cli_parser) if strict_arguments: parsed_args = self._cli_parser.parse_args(args=script_cli) other_args = list() else: parsed_args, other_args = self._cli_parser.parse_known_args(args=script_cli) # Read configuration from the document's folder, if it is possible if self.__read_document_configuration: config_file = self._detect_autolatex_configuration_file(self.__configuration.document_directory) if config_file: config_reader = OldStyleConfigReader() self.__configuration = config_reader.read_document_config_safely(config_file, self.__configuration) # Remove positional arguments if they are not recognized (other args) positional_arguments = [arg for arg in positional_arguments if arg not in other_args] return parsed_args, positional_arguments, other_args # noinspection PyUnusedLocal def _post_run_program(self, cli_arguments : Namespace, positional_arguments: list[str], unknown_arguments: list[str]): """ Run the behavior of the main program after the specific behavior. :param cli_arguments: the argparse object that contains the CLI arguments successfully parsed. :type cli_arguments: Namespace :param positional_arguments: the CLI arguments that are not consumed by the argparse library. :type positional_arguments: list[str] :param unknown_arguments: the list of the unsupported arguments. :type unknown_arguments: list[str] """ self._exit_on_success() @abstractmethod def _run_program(self, cli_arguments : Namespace, positional_arguments: list[str], unknown_arguments: list[str]): """ Run the specific behavior. :param cli_arguments: the argparse object that contains the CLI arguments successfully parsed. :type cli_arguments: Namespace :param positional_arguments: the CLI arguments that are not consumed by the argparse library. :type positional_arguments: list[str] :param unknown_arguments: the list of the unsupported arguments. :type unknown_arguments: list[str] """ raise NotImplementedError def run(self): """ Run the program. The program is run according to three steps: initialization (pre-run), command run (run-program) and post-execution (post-run). """ cli_arguments, positional_arguments, unknown_arguments = self._pre_run_program(True) self._run_program(cli_arguments, positional_arguments, unknown_arguments) self._post_run_program(cli_arguments, positional_arguments, unknown_arguments) @staticmethod def build_command_dict(package_name : str) -> dict[str,AutolatexCommand]: """ Build the dictionary that maps the command's names to AutoLaTeX commands. :param package_name: The name of the package to explore. :type package_name: str :return: the dict of the commands. :rtype: dict[str,AutolatexCommand] """ execution_environment : dict[str,Any] = { 'modules': None, } exec("import " + package_name + "\nmodules = " + package_name + ".__all__", None, execution_environment) modules = execution_environment['modules'] ids = dict() for module in modules: execution_environment = { 'id': None, 'alias': None, 'type': None, 'help': None, 'prereq': None, } cmd = textwrap.dedent("""\ from %s.%s import MakerAction type = MakerAction id = MakerAction.id help = MakerAction.help try: alias = MakerAction.alias except: alias = None try: prereq = MakerAction.prerequisites except: prereq = None """) % (package_name, module) exec(cmd, None, execution_environment) cmd_id = execution_environment['id'] cmd_alias = execution_environment['alias'] if cmd_alias is not None: if isinstance(cmd_alias, tuple) or isinstance(cmd_alias, list): cmd_alias = list(cmd_alias) else: cmd_alias = list([str(cmd_alias)]) cmd_prereq = execution_environment['prereq'] if cmd_prereq is not None: if isinstance(cmd_prereq, tuple) or isinstance(cmd_prereq, list): cmd_prereq = list(cmd_prereq) else: cmd_prereq = list([str(cmd_prereq)]) ids[cmd_id] = AutolatexCommand(name=cmd_id, action_type=execution_environment['type'], help_text=execution_environment['help'], aliases=cmd_alias, prerequisites=cmd_prereq) return ids def _create_cli_arguments_for_commands(self, commands : dict[str,AutolatexCommand], title : str, help_text : str, metavar : str = 'COMMAND'): """ Create CLI arguments for the given commands. :param commands: the pairs "command id"-"command instance". :type commands: dict[str,AutolatexCommand] :param title: The title of the command set. :type title: str :param help_text: The help description of the command set. :type help_text: str :param metavar: The name of the command set in the help. Default is: COMMAND. :type metavar: str """ subparsers = self.cli_parser.add_subparsers(title=title, metavar=metavar, help=help_text) for cmd_id, command in commands.items(): command.instance.register_command(sub_parsers=subparsers, command_name=command.name, command_help=command.help, command_aliases=command.aliases, configuration=self.configuration) def build_command_list_from_prerequisites(self, commands_to_run : list[str], all_commands : dict[str,AutolatexCommand]) -> list[str]: """ Build an ordered list of commands depending on the prerequisites of each command. :param commands_to_run: the list of commands to run. :type commands_to_run: list[str] :param all_commands: the definition of all the available commands. :type all_commands: dict[str,AutolatexCommand] :return: the ordered list of commands :rtype: list[str] """ # Adjacency list for the graph graph = defaultdict(set[str]) # In-degree for Kahn's algorithm in_degree = defaultdict(int) queue = deque(commands_to_run) to_be_run = set() while queue: cmd = queue.popleft() if cmd not in to_be_run: if cmd in all_commands: to_be_run.add(cmd) instance = all_commands[cmd] for prereq in instance.prerequisites: queue.append(prereq) else: msg = T('Undefined command %s') % cmd logging.error(msg) ex = Exception(msg) self._exit_on_exception(ex) raise ex for cmd in to_be_run: if cmd in all_commands: instance = all_commands[cmd] for prereq in instance.prerequisites: graph[prereq].add(cmd) in_degree[cmd] += 1 if cmd not in in_degree: in_degree[cmd] = 0 else: msg = T('Undefined command %s') % cmd logging.error(msg) ex = Exception(msg) self._exit_on_exception(ex) raise ex # Return the order in which commands should be executed. Kahn's algorithm for topological sort execution_order = list() queue = deque([cmd for cmd in in_degree if in_degree[cmd] == 0]) while queue: current = queue.popleft() if current in in_degree: in_degree.pop(current) execution_order.append(current) for neighbor in graph[current]: in_degree[neighbor] -= 1 if in_degree[neighbor] == 0: queue.append(neighbor) if in_degree: msg = T('Cycle detected in command prerequisites') logging.error(msg) ex = ValueError(msg) self._exit_on_exception(ex) raise ex return execution_order # noinspection PyMethodMayBeStatic def __unaliases(self, commands_to_run : list[str], all_commands : dict[str,AutolatexCommand]) -> list[str]: """ Replace any alias in the given list of commands to run by their regular names. :param commands_to_run: the list of names or aliases for the commands to be run. :type commands_to_run: list[str] :param all_commands: The definition of all the known commands. :type all_commands: dict[str,AutolatexCommand] :return: the list of commands to run, with their regular names. :rtype: list[str] """ aliases = dict() for name, cmd in all_commands.items(): aliases[name] = name if cmd.aliases: for alias in cmd.aliases: if alias in aliases: raise Exception('multiple commands have the alias \'%s\'' % alias) aliases[alias] = name real_commands = list() for command in commands_to_run: if command in aliases: cmd = aliases[command] if cmd: real_commands.append(cmd) else: real_commands.append(command) else: real_commands.append(command) return real_commands def _execute_commands(self, commands_to_run : list[str], all_commands : dict[str,AutolatexCommand], cli_arguments : Namespace): """ Execute the commands. :param commands_to_run: List of arguments on the command line. :type commands_to_run: list[str] :param all_commands: Dict of all the available commands. :type all_commands: dict[str,AutolatexCommand] :param cli_arguments: the successfully parsed CLI arguments. :type cli_arguments: Namespace """ # Check existing command if not commands_to_run: logging.error(T('Unable to determine the command to run')) self._exit_on_failure() else: # Convert aliases to the real command names commands_to_run = self.__unaliases(commands_to_run, all_commands) # Build the list of commands cmds = self.build_command_list_from_prerequisites(commands_to_run, all_commands) # Run the commands for cmd in cmds: try: cmd_instance = all_commands[cmd] continuation = cmd_instance.instance.run(cli_arguments) if not continuation: self._exit_on_success() break except BaseException as exception: #logging.error(_T('Error when running the command: %s') % str(exception)) self._exit_on_exception(exception) break @staticmethod def _detect_tex_file(directory : str) -> str | None: """ Detect the name of the main TeX file in the project directory. :param directory: the directory to search inside. :type directory: str :return: the filename of the root TeX file, or None if not found. :rtype: str | None """ files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f)) and f.endswith('.tex')] length = len(files) if length <= 0: logging.error(T("Unable to find a TeX file to compile in: %s") % directory) return None else: file = files[0] if length > 1: logging.warning(T("Too much TeX files in: %s") % directory) logging.warning(T("Select the TeX file: %s") % file) return file def _exit_on_failure(self): """ Exit the main program on failure. """ self.exiter.exit_on_failure() def _exit_on_success(self): """ Exit the main program on success. """ self.exiter.exit_on_success() def _exit_on_exception(self, exception : BaseException): """ Exit the main program on exception. :param exception: The exception. :type exception: exception """ self.exiter.exit_on_exception(exception)