Source code for aglyph

# -*- coding: UTF-8 -*-

# Copyright (c) 2006, 2011, 2013-2018 Matthew Zipay.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

"""This module defines a custom error type and several utility functions
used by Aglyph.

.. note::
   Aglyph uses the standard :mod:`logging` module, but by default
   registers a :class:`logging.NullHandler` to suppress messages.

   To enable Aglyph logging, configure a logger and handler for the
   *"aglyph"* log channel (see :mod:`logging.config`).

.. note::
   .. versionadded:: 3.0.0

   Aglyph framework functions and methods are fully traced using
   `Autologging <http://ninthtest.info/python-autologging/>`_. However,
   all tracing is **deactivated** by default.

   To activate tracing:

   1. Configure a logger and handler for the *"aglyph"* log channel and
      set the logging level to :attr:`autologging.TRACE`.
   2. Run Aglyph with the *AGLYPH_TRACED* environment variable set to a
      **non-empty** value.

"""

from collections import namedtuple

#: .. deprecated:: 3.0.0.post1
#:    Reference :attr:`__version__` **only** as the canonical public
#:    version identifier.
version_info = namedtuple(
    "version_info", ["major", "minor", "patch", "pre_release", "metadata"])(
        3, 0, 0, "", ".post1")

__author__ = "Matthew Zipay <mattz@ninthtest.info>"
# PEP 440 canonical public version identifier:
#         "[N!]N(.N)*[{a|b|rc}N][.postN][.devN]"
# (see https://www.python.org/dev/peps/pep-0440/, specifically section #id48,
# and https://semver.org/)
__version__ = "3.0.0.post1"

from inspect import ismodule
import logging
import os
import sys

import autologging
if not os.getenv("AGLYPH_TRACED"):
    autologging.install_traced_noop()

from autologging import traced

__all__ = [
    "AglyphError",
    "format_dotted_name",
    "resolve_dotted_name",
]

# configure a logging for the "aglyph" channel to see log output
_log = logging.getLogger(__name__)

# prevent messages to the console when there's no logging configuration
# (see https://docs.python.org/3/howto/logging.html#library-config)
if not _log.handlers:
    _log.addHandler(logging.NullHandler())

# log the Aglyph and Python versions, the platform, and compatibility details
from aglyph._compat import is_string, name_of, platform_detail
_log.info("Aglyph %s on %s", __version__, platform_detail)


class AglyphDeprecationWarning(DeprecationWarning):
    """Issued when deprecated Aglyph functions, classes, or methods are
    used.

    """

    def __init__(self, name, replacement=None):
        """
        :arg str name:
           the name of the deprecated function, class, or method
        :keyword str replacement:
           the name of the replacement function, class, or method

        """
        message = (
            "%s is deprecated and will be removed in release %d.0.0." %
                (name, MAJOR + 1))
        if replacement is not None:
            message = "%s Use %s instead." % (message, replacement)
        #PYVER: arguments to super() are implicit under Python 3
        super(AglyphDeprecationWarning, self).__init__(message)


[docs]class AglyphError(Exception): """Raised when Aglyph operations fail with a condition that is not sufficiently described by a built-in exception. """ def __init__(self, message, cause=None): #PYVER: arguments to super() are implicit under Python 3 super(AglyphError, self).__init__(message) self.cause = cause
[docs]@traced def format_dotted_name(obj): """Return the importable dotted-name string for *obj*. :param obj: an **importable** class, function, or module :return: a dotted name representing *obj* :rtype: :obj:`str` :raise AglyphError: if *obj* does not have a resolvable (importable) dotted name The dotted name returned by this function is a *"dotted_name.NAME"* or *"dotted_name"* string for *obj* that represents a valid absolute import statement according to the following productions: .. productionlist:: absolute_import_stmt: "from" dotted_name "import" NAME : | "import" dotted_name dotted_name: NAME ('.' NAME)* .. note:: This function is the inverse of :func:`resolve_dotted_name`. .. warning:: This function will attempt to use the ``__qualname__`` attribute, which is only available in Python 3.3+. When ``__qualname__`` is **not** available, ``__name__`` is used instead. .. seealso:: :pep:`3155`, :func:`aglyph._compat.name_of` """ if not _importable(obj): raise AglyphError("%r does not have an importable dotted name" % obj) if not ismodule(obj): return "%s.%s" % (obj.__module__, name_of(obj)) else: return obj.__name__
@traced def _importable(obj): """Tell whether or not *obj* is directly importable. :arg obj: any object :rtype: :obj:`bool` If *obj* is importable, then: >>> resolve_dotted_name(format_dotted_name(obj)) is obj True """ if ismodule(obj): return True elif hasattr(obj, "__module__") and hasattr(obj, "__name__"): return obj.__name__ in sys.modules[obj.__module__].__dict__ else: return False
[docs]@traced def resolve_dotted_name(dotted_name): """Return the class, function, or module identified by *dotted_name*. :param str dotted_name: a string representing an **importable** class, function, or module :return: a class, function, or module *dotted_name* must be a "dotted_name.NAME" or "dotted_name" string that represents a valid absolute import statement according to the following productions: .. productionlist:: absolute_import_stmt: "from" dotted_name "import" NAME : | "import" dotted_name dotted_name: NAME ('.' NAME)* .. note:: This function is the inverse of :func:`format_dotted_name`. """ if '.' in dotted_name: (module_name, name) = dotted_name.rsplit('.', 1) module = __import__(module_name, fromlist=[name], level=0) obj = getattr(module, name) else: obj = __import__(dotted_name, level=0) return obj
@traced def _identify(spec): """Determine the unique ID for *spec*. :arg spec: an **importable** class, function, or module; or a :obj:`str` :return: *spec* unchanged (if it is a :obj:`str`), else *spec*'s importable dotted name :rtype: :obj:`str` If *spec* is a string, it is assumed to already represent a unique ID and is returned unchanged. Otherwise, *spec* is assumed to be an **importable** class, function, or module, and its dotted name is returned (see :func:`format_dotted_name`). """ return spec if is_string(spec) else format_dotted_name(spec)