# -*- 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
@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)