def get_mpl_hist_data(self, update=None, skip=None): """ Returns a dictionary containing information on *bins*, *range*, *label*, and *log*, that can be passed to `matplotlib histograms <https://matplotlib.org/api/_as_gen/matplotlib.pyplot.hist.html>`_. When *update* is set, the returned dict is updated with *update*. When *skip* is set, it can be a single key or a sequence of keys that will not be added to the returned dictionary. """ data = { "bins": self.n_bins, "range": (self.x_min, self.x_max), "label": self.name, } if self.log_x: data["log"] = True # update? if update: data.update(update) # skip some values? if skip: for key in make_list(skip): if key in data: del data[key] return data
def keys(self, keys): # keys parser _keys = [] for key in make_list(keys): if not isinstance(key, six.string_types): raise TypeError("invalid key type: {}".format(key)) _keys.append(str(key)) return _keys
def has_tag(self, tag, mode=any, **kwargs): """ has_tag(tag, mode=any, **kwargs) Returns *True* when this object is tagged with *tag*, *False* otherwise. When *tag* is a sequence of tags, the behavior is defined by *mode*. For *any*, the object is considered *tagged* when at least one of the provided tags matches. When *all*, all provided tags have to match. Each *tag* can be a *fnmatch* or *re* pattern. All *kwargs* are passed to :py:func:`util.multi_match`. """ match = lambda tag: any(multi_match(t, [tag], **kwargs) for t in self.tags) return mode(match(tag) for tag in make_list(tag))
def extend(self, objs): """ Adds multiple new objects to the index. All elements of the sequence *objs* are forwarded to :py:meth:`add` and the list of return values is returned. When an object is a dictionary or a tuple, it is expanded for the invocation of :py:meth:`add`. """ results = [] objs = objs.values() if isinstance( objs, UniqueObjectIndex) else make_list(objs) for obj in objs: if isinstance(obj, dict): obj = self.add(**obj) elif isinstance(obj, tuple): obj = self.add(*obj) else: obj = self.add(obj) results.append(obj) return results
def decorator(unique_cls): if not issubclass(unique_cls, UniqueObject): raise TypeError( "decorated class must inherit from UniqueObject: {}".format( unique_cls)) # determine configuration defaults cls = kwargs.get("cls", unique_cls) singular = kwargs.get("singular", cls.__name__).lower() plural = kwargs.get("plural", singular + "s").lower() parents = kwargs.get("parents", True) deep_children = kwargs.get("deep_children", False) deep_parents = kwargs.get("deep_parents", False) skip = make_list(kwargs.get("skip", None) or []) # decorator for registering new instance methods with proper name and doc string # functionality is almost similar to functools.wraps, except for the customized function # naming and automatic transfer to the unique_class to extend def patch(name=None, prop=False, **kwargs): def decorator(f): _name = name if _name is not None and hasattr(f, "__name__"): f.__name__ = _name elif _name is None and hasattr(f, "__name__"): _name = f.__name__ if f.__doc__: f.__doc__ = f.__doc__.format(name=_name, singular=singular, plural=plural, **kwargs) if prop: f = property(f) # only patch when there is not attribute with that name if not hasattr(unique_cls, _name) and _name not in skip: setattr(unique_cls, _name, f) return f return decorator # patch the init method orig_init = unique_cls.__init__ def __init__(self, *args, **kwargs): # register the child and parent indexes setattr(self, "_" + plural, UniqueObjectIndex(cls=cls)) if parents: setattr(self, "_parent_" + plural, UniqueObjectIndex(cls=cls)) orig_init(self, *args, **kwargs) unique_cls.__init__ = __init__ # add attribute docs if unique_cls.__doc__: unique_cls.__doc__ += """ .. py:attribute:: {plural} type: UniqueObjectIndex read-only The :py:class:`~order.unique.UniqueObjectIndex` of child {plural}. """.format(plural=plural) if parents: unique_cls.__doc__ += """ .. py:attribute:: parent_{plural} type: UniqueObjectIndex read-only The :py:class:`~order.unique.UniqueObjectIndex` of parent {plural}. """.format(plural=plural) # # child methods, independent of parents # # direct child index access @patch() @typed(setter=False, name=plural) def get_index(self): pass if not deep_children: # has child method @patch("has_" + singular) def has(self, obj, context=None): """ Checks if the :py:attr:`{plural}` index for *context* contains an *obj* which might be a *name*, *id*, or an instance. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. """ return getattr(self, plural).has(obj, context=context) # get child method @patch("get_" + singular) def get(self, obj, default=_no_default, context=None): """ Returns a child {singular} given by *obj*, which might be a *name*, *id*, or an instance from the :py:attr:`{plural}` index for *context*. When no {singular} is found, *default* is returned when set. Otherwise, an error is raised. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. """ return getattr(self, plural).get(obj, default=default, context=context) else: # has child method @patch("has_" + singular) def has(self, obj, deep=True, context=None): """ Checks if the :py:attr:`{plural}` index for *context* contains an *obj* which might be a *name*, *id*, or an instance. If *deep* is *True*, the lookup is recursive. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. """ return getattr(self, "get_" + singular)( obj, default=_not_found, deep=deep, context=context) != _not_found # get child method @patch("get_" + singular) def get(self, obj, default=_no_default, deep=True, context=None): """ Returns a child {singular} given by *obj*, which might be a *name*, *id*, or an instance from the :py:attr:`{plural}` index for *context*. If *deep* is *True*, the lookup is recursive. When no {singular} is found, *default* is returned when set. Otherwise, an error is raised. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. """ indexes = [getattr(self, plural)] while len(indexes) > 0: index = indexes.pop(0) _obj = index.get(obj, default=_not_found, context=context) if _obj != _not_found: return _obj elif deep: indexes.extend(getattr(_obj, plural) for _obj in index) else: if default != _no_default: return default else: raise ValueError("unknown {}: {}".format( singular, obj)) # walk children method @patch("walk_" + plural) def walk(self, context=None): """ Walks through the :py:attr:`{plural}` index for *context* and per iteration, yields a child {singular}, its depth relative to *this* {singular}, and its child {plural} in a list that can be modified to alter the walking. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. When *context* is *all*, all indices are traversed. """ lookup = [ (obj, 1) for obj in getattr(self, plural).values(context=context) ] while lookup: obj, depth = lookup.pop(0) objs = list(getattr(obj, plural).values(context=context)) yield (obj, depth, objs) lookup.extend((obj, depth + 1) for obj in objs) if unique_cls == cls: # is leaf method @patch("is_leaf_" + singular, prop=True) def is_leaf(self): """ Returns *True* when this {singular} has no child {plural}, *False* otherwise. """ return len(getattr(self, plural)) == 0 # # child methods, disabled parents # if not parents: # add child method @patch("add_" + singular) def add(self, *args, **kwargs): """ Adds a child {singular}. See :py:meth:`UniqueObjectIndex.add` for more info. """ return getattr(self, plural).add(*args, **kwargs) # remove child method @patch("remove_" + singular) def remove(self, obj, context=None, silent=False): """ Removes a child {singular} given by *obj*, which might be a *name*, *id*, or an instance from the :py:attr:`{plural}` index for *context* and returns the removed object. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. Unless *silent* is *True*, an error is raised if the object was not found. See :py:meth:`UniqueObjectIndex.remove` for more info. """ return getattr(self, plural).remove(obj, context=context, silent=silent) # # child methods, enabled parents # else: # remove child method with limited number of parents @patch("remove_" + singular) def remove(self, obj, context=None, silent=False): """ Removes a child {singular} given by *obj*, which might be a *name*, *id*, or an instance from the :py:attr:`{plural}` index for *context* and returns the removed object. Also removes *this* {singular} from the :py:attr:`parent_{plural}` index of the removed {singular}. When *context* is *None*, the *default_context* of the :py:attr:`{plural}` index is used. Unless *silent* is *True*, an error is raised if the object was not found. See :py:meth:`UniqueObjectIndex.remove` for more info. """ obj = getattr(self, plural).remove(obj, context=context, silent=silent) if obj is not None: getattr(obj, "parent_" + plural).remove(self, context=obj.context, silent=silent) return obj # # child methods, enabled and unlimited parents # if isinstance(parents, six.integer_types): # add child method with infinite number of parents @patch("add_" + singular) def add(self, *args, **kwargs): """ Adds a child {singular}. Also adds *this* {singular} to the parent index of the added {singular}. See :py:meth:`UniqueObjectIndex.add` for more info. """ obj = getattr(self, plural).add(*args, **kwargs) getattr(obj, "parent_" + plural).add(self) return obj # # child methods, enabled but limited parents # else: # add child method with limited number of parents @patch("add_" + singular) def add(self, *args, **kwargs): """ Adds a child {singular}. Also adds *this* {singular} to the parent index of the added {singular}. An exception is raised when the number of allowed parents is exceeded. See :py:meth:`UniqueObjectIndex.add` for more info. """ index = getattr(self, plural) obj = index.add(*args, **kwargs) parent_index = getattr(obj, "parent_" + plural) if len(parent_index) >= parents: index.remove(obj) raise Exception( "number of parents exceeded: {}".format(parents)) parent_index.add(self) return obj # # parent methods, independent of number # # direct parent index access @patch() # noqa: F811 @typed(setter=False, name="parent_" + plural) def get_index(self): pass # remove parent method @patch("remove_parent_" + singular) # noqa: F811 def remove(self, obj, context=None, silent=False): """ Removes a parent {singular} *obj* which might be a *name*, *id*, or an instance from the :py:attr:`parent_{plural}` index for *context*. Also removes *this* instance from the child index of the removed {singular}. Returns the removed object. When *context* is *None*, the *default_context* of the :py:attr:`parent_{plural}` index is used. Unless *silent* is *True*, an error is raised if the object was not found. See :py:meth:`UniqueObjectIndex.remove` for more info. """ obj = getattr(self, "parent_" + plural).remove(obj, context=context, silent=silent) if obj is not None: getattr(obj, plural).remove(self, context=obj.context, silent=silent) return obj if not deep_parents: @patch("has_parent_" + singular) # noqa: F811 def has(self, obj, context=None): """ Checks if the :py:attr:`parent_{plural}` index for *context* contains an *obj* which might be a *name*, *id*, or an instance. When *context* is *None*, the *default_context* of the :py:attr:`parent_{plural}` index is used. """ return getattr(self, "parent_" + plural).has(obj, context=context) # get child method @patch("get_parent_" + singular) # noqa: F811 def get(self, obj, default=_no_default, context=None): """ Returns a parent {singular} given by *obj*, which might be a *name*, *id*, or an instance from the :py:attr:`parent_{plural}` index for *context*. When no {singular} is found, *default* is returned when set. Otherwise, an error is raised. When *context* is *None*, the *default_context* of the :py:attr:`parent_{plural}` index is used. """ return getattr(self, "parent_" + plural).get(obj, default=default, context=context) else: # has child method @patch("has_parent_" + singular) def has(self, obj, deep=True, context=None): """ Checks if the :py:attr:`parent_{plural}` index for *context* contains an *obj*, which might be a *name*, *id*, or an instance. If *deep* is *True*, the lookup is recursive. When *context* is *None*, the *default_context* of the :py:attr:`parent_{plural}` index is used. """ return getattr(self, "get_parent_" + singular)( obj, default=_not_found, deep=deep, context=context) != _not_found # get parent method @patch("get_parent_" + singular) def get(self, obj, default=_no_default, deep=True, context=None): """ Returns a parent {singular} given by *obj*, which might be a *name*, *id*, or an instance from the :py:attr:`parent_{plural}` index for *context*. If *deep* is *True*, the lookup is recursive. When no {singular} is found, *default* is returned when set. Otherwise, an error is raised. When *context* is *None*, the *default_context* of the :py:attr:`parent_{plural}` index is used. """ indexes = [getattr(self, "parent_" + plural)] while len(indexes) > 0: index = indexes.pop(0) _obj = index.get(obj, default=_not_found, context=context) if _obj != _not_found: return _obj elif deep: indexes.extend( getattr(_obj, "parent_" + plural) for _obj in index) else: if default != _no_default: return default else: raise ValueError("unknown {}: {}".format( singular, obj)) # walk parents method @patch("walk_parent_" + plural) # noqa: F811 def walk(self, context=None): """ Walks through the :py:attr:`parent_{plural}` index for *context* and per iteration, yields a parent {singular}, its depth relative to *this* {singular}, and its parent {plural} in a list that can be modified to alter the walking. When *context* is *None*, the *default_context* of the :py:attr:`parent_{plural}` index is used. When *context* is *all*, all indices are traversed. """ lookup = [ (obj, 1) for obj in getattr(self, "parent_" + plural).values() ] while lookup: obj, depth = lookup.pop(0) objs = list(getattr(obj, "parent_" + plural).values()) yield (obj, depth, objs) lookup.extend((obj, depth + 1) for obj in objs) if unique_cls == cls: # is_root method @patch("is_root_" + singular, prop=True) def is_root(self): """ Returns *True* when this {singular} has no parent {plural}, *False* otherwise. """ return len(getattr(self, "parent_" + plural)) == 0 # # parent methods, unlimited number # if not isinstance(parents, six.integer_types): # add parent method with inf number of parents @patch("add_parent_" + singular) # noqa: F811 def add(self, *args, **kwargs): """ Adds a parent {singular}. Also adds *this* {singular} to the child index of the added {singular}. See :py:meth:`UniqueObjectIndex.add` for more info. """ obj = getattr(self, "parent_" + plural).add(*args, **kwargs) getattr(obj, plural).add(self) return obj # # parent methods, limited number # else: # add parent method with inf number of parents @patch("add_parent_" + singular) def add(self, *args, **kwargs): """ Adds a parent {singular}. Also adds *this* {singular} to the child index of the added {singular}. See :py:meth:`UniqueObjectIndex.add` for more info. """ parent_index = getattr(self, "parent_" + plural) if len(parent_index) >= parents: raise Exception( "number of parents exceeded: {}".format(parents)) obj = parent_index.add(*args, **kwargs) getattr(obj, plural).add(self) return obj # # convenient parent methods, exactly 1 parent # if not isinstance(parents, bool) and parents == 1: # direct parent access @patch(name="parent_" + singular) @property def parent(self): index = getattr(self, "parent_" + plural) return None if len(index) != 1 else list( index.values(context=index.ALL))[0] return unique_cls