Source code for xotl.tools.names

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

"""A protocol to obtain or manage object names."""

# FIX: These imports must be local
from xotl.tools.symbols import Undefined as _undef

# TODO: This module must be reviewed and deprecate most of it.


def _get_mappings(source):
    """Return a sequence of mappings from `source`.

    Source could be a stack frame, a single dictionary, or any sequence of
    dictionaries.

    """
    from collections.abc import Mapping

    if isinstance(source, Mapping):
        return (source,)
    else:
        from xotl.tools.future.inspect import get_attr_value

        l = get_attr_value(source, "f_locals", _undef)
        g = get_attr_value(source, "f_globals", _undef)
        if isinstance(l, Mapping) and isinstance(g, Mapping):
            return (l,) if l is g else (l, g)
        else:
            return tuple(source)


def _key_for_value(source, value, strict=True):
    """Returns the tuple (key, mapping) where the "value" is found.

    if strict is True, then look first for the same object::
        >>> x = [1]
        >>> y = [1]  #  equal to `x` but not the same
        >>> src = {'x': x, 'y': y}
        >>> search = lambda o, strict=True: _key_for_value(src, o, strict)
        >>> search(x) == search(y)
        False
        >>> search(x, strict=False) == search(y, strict=False)
        True

    This is mainly intended to find object names in stack frame variables.

    Source could be a stack frame, a single dictionary, or any sequence of
    dictionaries.

    """
    source = _get_mappings(source)
    found, equal = _undef, (None, {})
    i, mapping_count = 0, len(source)
    while found is _undef and (i < mapping_count):
        mapping = source[i]
        keys = list(mapping)
        j, key_count = 0, len(keys)
        while found is _undef and (j < key_count):
            key = keys[j]
            item = mapping[key]
            if item is value:
                found = key, mapping
            elif value == item:
                if strict:
                    equal = key, mapping
                else:
                    found = key, mapping
            j += 1
        i += 1
    return found if found is not _undef else equal


def _get_value(source, key, default=None):
    """Returns the value for the given `key` in `source` mappings.

    This is mainly intended to obtain object values in stack frame variables.

    Source could be a stack frame, a single dictionary, or any sequence of
    dictionaries.

    """
    source = _get_mappings(source)
    res = _undef
    i, mapping_count = 0, len(source)
    while res is _undef and (i < mapping_count):
        mapping = source[i]
        res = mapping.get(key, _undef)
        i += 1
    return res if res is not _undef else default


def _get_best_name(names, safe=False, full=False):
    """Get the best name in the give list of `names`.

    Best names are chosen in the following order (from worst to best):

    - Any string
    - A valid slug
    - A valid protected identifier
    - A valid public identifier
    - A valid full identifier

    If a string in the list `names` contains the substring "%(next)s", then
    the algorithm recurses to find the best name of the remaining names first
    and substitutes the substring with the result, the remaining names are
    then pruned from the search.

    If `safe` is True, returned name must be a valid identifier.  If `full` is
    True (halted if `safe` is not True) then the returned name must a valid
    full identifier.

    """
    from xotl.tools.validators import (
        is_valid_full_identifier,
        is_valid_identifier,
        is_valid_public_identifier,
        is_valid_slug,
    )

    names = list(names)

    def inner(start=0):
        ok, best_idx, best_qlty = start, -1, 0
        i, count = start, len(names)
        assert start < count, 'start is "%s", max is "%s".' % (start, count)
        while i < count:
            name = names[i]
            if "%(next)s" in name:
                next = inner(i + 1)
                names[i] = name % {"next": next}
                count = i + 1
            else:
                if is_valid_slug(name):
                    qlty = 25
                if is_valid_identifier(name):
                    qlty = 75 if is_valid_public_identifier(name) else 50
                elif is_valid_full_identifier(name):
                    qlty = 100
                else:
                    qlty = -25
                if best_qlty <= qlty:
                    best_idx = i
                    best_qlty = qlty
                ok = i
                i += 1
        idx = best_idx if best_idx >= 0 else ok
        return names[idx]

    res = inner()
    if safe:
        # TODO: Improve these methods to return False of reserved identifiers
        is_valid = is_valid_full_identifier if full else is_valid_identifier
        if not is_valid(res):
            from xotl.tools.string import slugify

            _mark = "dot_dot_dot"
            full = full and "." in res
            if full:
                res = res.replace(".", _mark)
            res = slugify(res, "_")
            if full:
                res = res.replace(_mark, ".")
            if not is_valid(res):
                res = "_" + res
    return str(res)


def module_name(item):
    """Returns the full module name where the given object is declared.

    Examples::

       >>> module_name(module_name)
       'xotl.tools.names'

       >>> from xotl.tools.symbols import Unset
       >>> module_name(Unset)
       'xotl.tools.symbols'

    """
    from xotl.tools.future.inspect import get_attr_value

    if item is None:
        res = ""
    elif isinstance(item, str):
        res = item
    else:
        res = get_attr_value(item, "__module__", None)
        if res is None:
            res = get_attr_value(type(item), "__module__", "")
    if res.startswith("__") or res in ("builtins", "exceptions", "<module>"):
        res = ""
    return str(res)


def simple_name(item, join=True):
    """Returns the simple name for the given object.

    :param join: If False, only the object inner name is returned; if it is a
           callable is used similar to a string join receiving a tuple of
           (module-name, inner-name) as argument; True means (is equivalent
           to)::

             join = lambda arg: '.'.join(arg).strip('.')

           For example, use ``lambda arg: arg`` to return the 2-tuple itself.

           See `module_name`:func: for more information when a not False value
           is used.

    Examples::

       >>> simple_name(simple_name)
       'xotl.tools.names.simple_name'

       >>> from xotl.tools.symbols import Unset
       >>> simple_name(Unset)
       'xotl.tools.symbols.Unset'

    This function is intended for named objects (those with the `__name__`
    attribute), if an object without standard name is used, the type name is
    returned instead; for example::

        >>> simple_name(0)
        'int'

    To get a name in a more precise way, use `nameof`:func:.

    """
    # TODO: deprecate `join` argument
    from xotl.tools.future.inspect import safe_name

    singletons = (None, True, False, Ellipsis, NotImplemented)
    res = next((str(s) for s in singletons if s is item), None)
    if res is None:
        res = safe_name(item)
        if res is None:
            item = type(item)
            res = safe_name(item)
        if join:
            if join is True:

                def join(arg):
                    return str(".".join(arg).strip("."))

            res = join((module_name(item), res))
    return res


[docs]def nameof(*args, **kwargs): """Obtain the name of each one of a set of objects. .. versionadded:: 1.4.0 .. versionchanged:: 1.6.0 - Keyword arguments are now keyword-only arguments. - Support for several objects - Improved the semantics of parameter `full`. - Added the `safe` keyword argument. If no object is given, None is returned; if only one object is given, a single string is returned; otherwise a list of strings is returned. The name of an object is normally the variable name in the calling stack. If the object is not present calling frame, up to five frame levels are searched. Use the `depth` keyword argument to specify a different starting point and the search will proceed five levels from this frame up. If the same object has several good names a single one is arbitrarily chosen. Good names candidates are retrieved based on the keywords arguments `full`, `inner`, `safe` and `typed`. If `typed` is True and the object is not a type name or a callable (see `xotl.tools.future.inspect.safe_name`:func:), then the `type` of the object is used instead. If `inner` is True we try to extract the name by introspection instead of looking for the object in the frame stack. If `full` is True the full identifier of the object is preferred. In this case if `inner` is False the local-name for the object is found. If `inner` is True, find the import-name. If `safe` is True, returned value is converted -if it is not- into a valid Python identifier, though you should not trust this identifier resolves to the value. See `the examples in the documentation <name-of-narrative>`:ref:. """ # XXX: The examples are stripped from here. Go the documentation page. from numbers import Number from xotl.tools.future.inspect import safe_name arg_count = len(args) names = [[] for i in range(arg_count)] params = kwargs idx = 0 def grant(name=None, **again): nonlocal params nonlocal idx if name: names[idx].append(name) assert len(names[idx]) < 5 if again: params = dict(kwargs, **again) else: params = kwargs idx += 1 def param(name, default=False): nonlocal params return params.get(name, default) while idx < arg_count: item = args[idx] if param("typed") and not safe_name(item): item = type(item) if param("inner"): res = safe_name(item) if res: if param("full"): head = module_name(item) if head: res = ".".join((head, res)) grant(res) elif isinstance(item, (str, Number)): grant(str(item)) else: grant("@".join(("%(next)s", hex(id(item)))), typed=True) else: import sys sf = sys._getframe(param("depth", 1)) try: i, LIMIT, res = 0, 5, _undef _full = param("full") while not res and sf and (i < LIMIT): key, mapping = _key_for_value(sf, item) if key and _full: head = module_name(_get_value(mapping, "__name__")) if not head: head = module_name(sf.f_code.co_name) if not head: head = module_name(item) or None else: head = None if key: res = key else: sf = sf.f_back i += 1 finally: # TODO: on "del sf" Python says "SyntaxError: can not delete # variable 'sf' referenced in nested scope". sf = None if res: grant(".".join((head, res)) if head else res) else: res = safe_name(item) if res: grant(res) else: grant(None, inner=True) for i in range(arg_count): names[i] = _get_best_name(names[i], safe=param("safe")) if arg_count == 0: return None elif arg_count == 1: return names[0] else: return names
[docs]def identifier_from(*args): """Build an valid identifier from the name extracted from an object. .. versionadded:: 1.5.6 First, check if argument is a type and then returns the name of the type prefixed with `_` if valid; otherwise calls `nameof` function repeatedly until a valid identifier is found using the following order logic: ``inner=True``, without arguments looking-up a variable in the calling stack, and ``typed=True``. Returns None if no valid value is found. Examples:: >>> identifier_from({}) 'dict' """ if len(args) == 1: from xotl.tools.future.inspect import get_attr_value from xotl.tools.validators.identifiers import is_valid_identifier as valid res = None if isinstance(args[0], type): aux = get_attr_value(args[0], "__name__", None) if valid(aux): res = str("_%s" % aux) if res is None: tests = ({"inner": True}, {}, {"typed": True}) names = (nameof(args[0], depth=2, **test) for test in tests) res = next((name for name in names if valid(name)), None) return res else: msg = "identifier_from() takes exactly 1 argument (%s given)" raise TypeError(msg % len(args))
class namelist(list): """Similar to list, but only intended for storing object names. Constructors: * namelist() -> new empty list * namelist(collection) -> new list initialized from collection's items * namelist(item, ...) -> new list initialized from severals items Instances can be used as decorators to store names of module items (functions or classes):: >>> __all__ = namelist() >>> @__all__ ... def foobar(*args, **kwargs): ... 'Automatically added to this module "__all__" names.' >>> 'foobar' in __all__ True """ def __init__(self, *args): if len(args) == 1: from types import GeneratorType as gtype if isinstance(args[0], (tuple, list, set, frozenset, gtype)): args = args[0] super().__init__(nameof(arg, depth=2) for arg in args) def __add__(self, other): other = [nameof(item, depth=2) for item in other] return super().__add__(other) __iadd__ = __add__ def __contains__(self, item): return super().__contains__(nameof(item, inner=True)) def append(self, value): """l.append(value) -- append a name object to end""" super().append(nameof(value, depth=2)) return value # What allow to use its instances as a decorator __call__ = append def extend(self, items): """l.extend(items) -- extend list by appending items from the iterable""" items = (nameof(item, depth=2) for item in items) return super().extend(items) def index(self, value, *args): """l.index(value, [start, [stop]]) -> int -- return first index of name Raises ValueError if the name is not present. """ return super().index(nameof(value, depth=2), *args) def insert(self, index, value): """l.insert(index, value) -- insert object before index""" return super().insert(index, nameof(value, depth=2)) def remove(self, value): """l.remove(value) -- remove first occurrence of value Raises ValueError if the value is not present. """ return list.remove(self, nameof(value, depth=2)) class strlist(list): """Similar to list, but only intended for storing ``str`` instances. Constructors: * strlist() -> new empty list * strlist(collection) -> new list initialized from collection's items * strlist(item, ...) -> new list initialized from severals items Last versions of Python 2.x has a feature to use unicode as standard strings, but some object names can be only ``str``. To be compatible with Python 3.x in an easy way, use this list. """ def __init__(self, *args): if len(args) == 1: from types import GeneratorType as gtype if isinstance(args[0], (tuple, list, set, frozenset, gtype)): args = args[0] super().__init__(str(arg) for arg in args) def __add__(self, other): other = [str(item) for item in other] return super().__add__(other) __iadd__ = __add__ def __contains__(self, item): return super().__contains__(str(item)) def append(self, value): """l.append(value) -- append a name object to end""" super().append(str(value)) return value # What allow to use its instances as a decorator __call__ = append def extend(self, items): """l.extend(items) -- extend list by appending items from the iterable""" items = (str(item) for item in items) return super().extend(items) def index(self, value, *args): """l.index(value, [start, [stop]]) -> int -- return first index of name Raises ValueError if the name is not present. """ return super().index(str(value), *args) def insert(self, index, value): """l.insert(index, value) -- insert object before index""" return super().insert(index, str(value)) def remove(self, value): """l.remove(value) -- remove first occurrence of value Raises ValueError if the value is not present. """ return list.remove(self, str(value)) # Theses tests need to be defined in this module to test relative imports. # Otherwise the `tests/` directory would need to be a proper package. import unittest as _utest from xotl.tools.symbols import Unset as _Unset # Use a tier 0 module! class TestRelativeImports(_utest.TestCase): RelativeUnset = _Unset AbsoluteUndefined = _undef def test_relative_imports(self): self.assertEqual(nameof(self.RelativeUnset), "_Unset") self.assertEqual(nameof(self.RelativeUnset, inner=True), "Unset") # Even relative imports are resolved properly with `full=True` self.assertEqual(nameof(self.RelativeUnset, full=True), "xotl.tools.names._Unset") self.assertEqual(nameof(self.AbsoluteUndefined, full=True), "xotl.tools.names._undef") # Don't delete the _Unset name, so that the nameof inside the test could find # them in the module. del _utest