def smart_getter(obj, strict=False): '''Returns a smart getter for `obj`. If `obj` is a mapping, it returns the ``.get()`` method bound to the object `obj`, otherwise it returns a partial of ``getattr`` on `obj`. :param strict: Set this to True so that the returned getter checks that keys/attrs exists. If `strict` is True the getter may raise a KeyError or an AttributeError. .. versionchanged:: 1.5.3 Added the parameter `strict`. ''' from .types import is_mapping if is_mapping(obj): if not strict: return obj.get else: def _get(key, default=Unset): try: return obj[key] except KeyError: if default is Unset: raise else: return default return _get else: if not strict: return lambda attr, default=None: getattr(obj, attr, default) else: def _partial(attr, default=Unset): try: return getattr(obj, attr) except AttributeError: if default is Unset: raise else: return default return _partial
def smart_copy(*args, **kwargs): '''Copies the first apparition of attributes (or keys) from `sources` to `target`. :param sources: The objects from which to extract keys or attributes. :param target: The object to fill. :param defaults: Default values for the attributes to be copied as explained below. Defaults to False. :type defaults: Either a bool, a dictionary, an iterable or a callable. Every `sources` and `target` are always positional arguments. There should be at least one source. `target` will always be the last positional argument. If `defaults` is a dictionary or an iterable then only the names provided by itering over `defaults` will be copied. If `defaults` is a dictionary, and one of its key is not found in any of the `sources`, then the value of the key in the dictionary is copied to `target` unless: - It's the value :class:`xoutil.types.Required` or an instance of Required. - An exception object - A sequence with is first value being a subclass of Exception. In which case :class:`xoutil.data.adapt_exception` is used. In these cases a KeyError is raised if the key is not found in the sources. If `default` is an iterable and a key is not found in any of the sources, None is copied to `target`. If `defaults` is a callable then it should receive one positional arguments for the current `attribute name` and several keyword arguments (we pass ``source``) and return either True or False if the attribute should be copied. If `defaults` is False (or None) only the attributes that do not start with a "_" are copied, if it's True all attributes are copied. When `target` is not a mapping only valid Python identifiers will be copied. Each `source` is considered a mapping if it's an instance of `collections.Mapping` or a `MappingProxyType`. The `target` is considered a mapping if it's an instance of `collections.MutableMapping`. :returns: `target`. .. versionchanged:: 1.7.0 `defaults` is now keyword only. ''' from collections import MutableMapping from xoutil.types import is_collection, is_mapping, Required from xoutil.data import adapt_exception from xoutil.validators.identifiers import is_valid_identifier defaults = kwargs.pop('defaults', False) if kwargs: raise TypeError('smart_copy does not accept a "%s" keyword argument' % kwargs.keys()[0]) sources, target = args[:-1], args[-1] if not sources: raise TypeError('smart_copy requires at least one source') if isinstance(target, (bool, type(None), int, float, str_base)): raise TypeError('target should be a mutable object, not %s' % type(target)) if isinstance(target, MutableMapping): def setter(key, val): target[key] = val else: def setter(key, val): if is_valid_identifier(key): setattr(target, key, val) _mapping = is_mapping(defaults) if _mapping or is_collection(defaults): for key, val in ((key, get_first_of(sources, key, default=Unset)) for key in defaults): if val is Unset: if _mapping: val = defaults.get(key, None) else: val = None exc = adapt_exception(val, key=key) if exc or val is Required or isinstance(val, Required): raise KeyError(key) setter(key, val) else: keys = [] for source in sources: get = smart_getter(source) if is_mapping(source): items = (name for name in source) else: items = dir(source) for key in items: private = isinstance(key, str_base) and key.startswith('_') if (defaults is False or defaults is None) and private: copy = False elif callable(defaults): copy = defaults(key, source=source) else: copy = True if key not in keys: keys.append(key) if copy: setter(key, get(key)) return target