Example #1
0
def get_metadata() -> OrderedArgsDict:
    '''
    Ordered dictionary synopsizing the current installation of this
    application.
    '''

    # Defer heavyweight imports.
    from betse.util.app.meta import appmetaone
    from betse.util.path import pathnames
    from betse.util.os.command import cmds

    # Return this dictionary.
    return OrderedArgsDict(
        'basename',
        cmds.get_current_basename(),
        'version',
        metadata.VERSION,
        'codename',
        metadata.CODENAME,
        'authors',
        metadata.AUTHORS,
        'license',
        metadata.LICENSE,
        'home directory',
        pathnames.get_home_dirname(),
        'dot directory',
        appmetaone.get_app_meta().dot_dirname,
        'data directory',
        appmetaone.get_app_meta().data_dirname,
    )
Example #2
0
    def expand_help(self, text: str, **kwargs) -> str:
        '''
        Interpolate the passed keyword arguments into the passed help string
        template, stripping all prefixing and suffixing whitespace from this
        template.

        For convenience, the following default keyword arguments are
        unconditionally interpolated into this template:

        * ``{script_basename}``, expanding to the basename of the Python
          wrapper script running the current application (e.g., ``betse``).
        * ``{program_name}``, expanding to the human-readable name of this
          application (e.g., ``BETSE``).
        '''

        # Avoid circular import dependencies.
        from betse.util.app.meta import appmetaone
        from betse.util.os.command import cmds
        from betse.util.type.text.string import strs

        # Expand it like Expander.
        return strs.remove_whitespace_presuffix(text.format(
            program_name=appmetaone.get_app_meta().module_metadata.NAME,
            script_basename=cmds.get_current_basename(),
            **kwargs
        ))
Example #3
0
    def _init_logger_root_handler_std(self) -> None:
        '''
        Initialize root logger handlers redirecting log messages to the
        standard stdout and stderr file handles.
        '''

        # Avoid circular import dependencies.
        from betse.util.io.log.logfilter import (LogFilterThirdPartyDebug,
                                                 LogFilterMoreThanInfo)
        from betse.util.io.log.conf.logconfformat import LogFormatterWrap
        from betse.util.os.command import cmds

        # Initialize the stdout handler to:
        #
        # * Log only informational messages by default.
        # * Unconditionally ignore all warning and error messages, which the
        #   stderr handler already logs.
        #
        # Sadly, the "StreamHandler" constructor does *NOT* accept the
        # customary "level" attribute accepted by its superclass constructor.
        self._logger_root_handler_stdout = StreamHandler(sys.stdout)
        self._logger_root_handler_stdout.setLevel(LogLevel.INFO)
        self._logger_root_handler_stdout.addFilter(LogFilterMoreThanInfo())

        # Initialize the stderr handler to:
        #
        # * Log only warning and error messages by default.
        # * Unconditionally ignore all informational and debug messages, which
        #   the stdout handler already logs.
        self._logger_root_handler_stderr = StreamHandler(sys.stderr)
        self._logger_root_handler_stderr.setLevel(LogLevel.WARNING)

        # Avoid printing third-party debug messages to the terminal.
        self._logger_root_handler_stdout.addFilter(LogFilterThirdPartyDebug())
        self._logger_root_handler_stderr.addFilter(LogFilterThirdPartyDebug())

        #FIXME: Consider colourizing this format string.

        # Format standard output and error in the conventional way. For a list
        # of all available log record attributes, see:
        #
        #     https://docs.python.org/3/library/logging.html#logrecord-attributes
        #
        # Note that "{{" and "}}" substrings in format() strings escape literal
        # "{" and "}" characters, respectively.
        stream_format = '[{}] {{message}}'.format(cmds.get_current_basename())

        # Formatter for this format.
        stream_formatter = LogFormatterWrap(fmt=stream_format, style='{')

        # Assign these formatters to these handlers.
        self._logger_root_handler_stdout.setFormatter(stream_formatter)
        self._logger_root_handler_stderr.setFormatter(stream_formatter)

        # Register these handlers with the root logger.
        self._logger_root.addHandler(self._logger_root_handler_stdout)
        self._logger_root.addHandler(self._logger_root_handler_stderr)
Example #4
0
    def arg_parser_top_kwargs(self) -> MappingType:
        '''
        Dictionary of all keyword arguments to be passed to the
        :meth:`ArgP"arserType.__init__` method of the top-level argument parser
        for this CLI.

        See Also
        ----------
        :meth:`_init_arg_parser_top`
            Method initializing this argument parser with this dictionary *and*
            keyword arguments common to all argument parsers.
        '''

        # Avoid circular import dependencies.
        from betse.util.app.meta import appmetaone
        from betse.util.os.command import cmds

        # Application metadata singleton.
        app_meta = appmetaone.get_app_meta()

        # Dictionary of all keyword arguments to be returned.
        arg_parser_top_kwargs = {
            # Human-readable multi-sentence application description. Since this
            # description is general-purpose rather than CLI-specific, format
            # substrings are *NOT* safely interpolatable into this string.
            'description': app_meta.module_metadata.DESCRIPTION,

            # Human-readable multi-sentence application help suffix.
            'epilog': self.expand_help(self._help_epilog),

            # Basename of the Python wrapper script producing this process.
            'prog': cmds.get_current_basename(),
        }

        # Merge these arguments with all default arguments.
        arg_parser_top_kwargs.update(self.arg_parser_kwargs)

        # Return this dictionary.
        return arg_parser_top_kwargs
Example #5
0
def output(*objs) -> None:
    '''
    Print all passed objects as is to standard output in a format mimicking
    that of standard :mod:`pytest` messages *without* logging these objects.

    This function is intended to be called *only* by :mod:`pytest`-specific
    fixtures, decorators, and helpers.

    This function is intentionally *not* named ``print()`` to avoid conflict
    with the builtin function of the same name.

    Examples
    ----------
        >>> from betse.util.test.pytest import pytests
        >>> pytests.output('Ego, ergo simulare.')
        [py.test] Ego, ergo simulare.
    '''

    # Avoid circular import dependencies.
    from betse.util.os.command import cmds

    # Print these messages in a "py.test"-friendly format.
    print('[{}] {}'.format(cmds.get_current_basename(), ''.join(objs)))
Example #6
0
def expand_help(text: str, **kwargs) -> str:
    '''
    Interpolate the passed keyword arguments into the passed help string
    template, stripping all prefixing and suffixing whitespace from this
    template.

    For convenience, the following default keyword arguments are unconditionally
    interpolated into this template:

    * ``{script_basename}``, expanding to the basename of the current script
        (e.g., ``betse``).
    * ``{program_name}``, expanding to this script's human-readable name
        (e.g., ``BETSE``).
    '''

    from betse import metadata
    from betse.util.os.command import cmds

    return strs.remove_whitespace_presuffix(text.format(
        program_name=metadata.NAME,
        script_basename=cmds.get_current_basename(),
        **kwargs
    ))
Example #7
0
    def _init_logger_root_handler_file(self) -> None:
        '''
        Initialize the root logger handler appending log messages to the
        currently open logfile file handle.

        This method is designed to be called multiple times, permitting the
        filename associated with this handler to be modified at runtime.
        '''

        # Avoid circular import dependencies.
        from betse.util.io.log.logfilter import LogFilterThirdPartyDebug
        from betse.util.io.log.conf.logconfformat import LogFormatterWrap
        from betse.util.io.log.conf.logconfhandle import (
            LogHandlerFileRotateSafe)
        from betse.util.path import pathnames
        from betse.util.os.command import cmds
        from betse.util.type.numeric import ints

        # Absolute or relative path of the directory containing this file.
        file_dirname = pathnames.get_dirname(self._filename)

        # Minimum level of messages to be log to disk, defaulting to "INFO".
        file_level = LogLevel.INFO

        # If this handler has already been created...
        if self._logger_root_handler_file is not None:
            # Preserve the previously set minimum level of messages to log.
            file_level = self._logger_root_handler_file.level

            # If the root logger has also already been created, remove this
            # handler from this root logger.
            if self._logger_root is not None:
                self._logger_root.removeHandler(self._logger_root_handler_file)

        # If the dirname of the directory containing this file is non-empty,
        # create this directory if needed. Note this dirname is empty when this
        # filename is a pure basename (e.g., when the "--log-file=my.log"
        # option is passed).
        #
        # For safety, this directory is created with standard low-level Python
        # functionality rather than our custom higher-level
        # dirs.make_parent_unless_dir() function. The latter logs this
        # creation. Since the root logger is *NOT* fully configured yet,
        # calling that function here would induce subtle errors or exceptions.
        if file_dirname:
            os.makedirs(file_dirname, exist_ok=True)

        # Root logger file handler, preconfigured as documented above.
        self._logger_root_handler_file = LogHandlerFileRotateSafe(
            filename=self._filename,

            # Append rather than overwrite this file.
            mode='a',

            # Defer opening this file in a just-in-time manner (i.e., until the
            # first call to this handler's emit() method is called to write the
            # first log via this handler). Why? Because (in no particular
            # order):
            #
            # * If the end user requests that *NO* messages be logged to disk
            #   (e.g., by passing the "--log-level=none" option), this file
            #   should *NEVER* be opened and hence created. The simplest means
            #   of doing so is simply to indefinitely defer opening this file.
            # * Doing so slightly reduces the likelihood (but *NOT* eliminate
            #   the possibility) of race conditions between multiple BETSE
            #   processes attempting to concurrently rotate the same logfile.
            delay=True,

            # Encode this file's contents as UTF-8.
            encoding='utf-8',

            # Maximum filesize in bytes at which to rotate this file,
            # equivalent to 1MB.
            maxBytes=ints.MiB,

            # Maximum number of rotated logfiles to maintain.
            backupCount=8,
        )

        # Initialize this handler's level to the previously established level.
        self._logger_root_handler_file.setLevel(file_level)

        # Prevent third-party debug messages from being logged to disk.
        self._logger_root_handler_file.addFilter(LogFilterThirdPartyDebug())

        # Linux-style logfile format.
        #
        # Note that the "processName" attribute appears to *ALWAYS* expand to
        # "MainProcess", which is not terribly descriptive. Hence, the name of
        # the current process is manually embedded in this format.
        file_format = ('[{{asctime}}] '
                       '{} {{levelname}} '
                       '({{module}}.py:{{funcName}}():{{lineno}}) '
                       '<PID {{process}}>:\n'
                       '    {{message}}'.format(cmds.get_current_basename()))

        # Format this file according to this format.
        file_formatter = LogFormatterWrap(fmt=file_format, style='{')
        self._logger_root_handler_file.setFormatter(file_formatter)

        # Register this handler with the root logger.
        self._logger_root.addHandler(self._logger_root_handler_file)