Source code for uiclasses.collections

# Copyright (c) 2020 NewStore GmbH

# 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.

import itertools
from types import GeneratorType
from .base import Model, UserFriendlyObject
from .base import COLLECTION_TYPES
from typing import Iterable
from typing import Callable
from ordered_set import OrderedSet
from humanfriendly.tables import format_robust_table, format_pretty_table
from . import typing as internal_typing

ITERABLES = (list, tuple, itertools.chain, set, map, filter, GeneratorType)


def is_iterable(values) -> bool:
    return any(
        [
            isinstance(values, ITERABLES + (IterableCollection,)),
            callable(getattr(values, "__iter__", None)),
        ]
    )


[docs]class IterableCollection(UserFriendlyObject): """Base mixin for ModelList and ModelSet, provides methods to manipulate iterable collections in ways take advantage of the behavior of models. For example it supports filtering by instance attributes through a cal to the :py:meth:`~uiclasses.base.Model.attribute_matches` method of each children. **Features:** - :py:meth:`~uiclasses.collections.IterableCollection.sorted_by` - sort by a single attribute - :py:meth:`~uiclasses.collections.IterableCollection.filter_by` - to filter by a single attribute - :py:meth:`~uiclasses.collections.IterableCollection.sorted` - alias to ``MyModel.List(sorted(my_model_collection))`` or ``.Set()`` - :py:meth:`~uiclasses.collections.IterableCollection.filter` - alias to ``MyModel.List(filter(callback, my_model_collection))`` - :py:meth:`~uiclasses.collections.IterableCollection.format_robust_table` - :py:meth:`~uiclasses.collections.IterableCollection.format_pretty_table` """ __visible_attributes__ = ["model_class"] def __repr__(self): return f"<{self.__ui_name__()} {list(self)}>" def __str__(self): return f"{self.__ui_name__()}[length={len(self)}]" def sorted(self, **kw): """returns a new ``ModelList`` with this collections' children sorted. Example: .. code:: x = MyModel.List([MyModel({"id": 2}), MyModel({"id": 3})]) result = x.sorted(key=lambda model: model.id) """ items = sorted(self, **kw) return self.__class__(items) def sorted_by(self, attribute: str, **kw): """sort by a single attribute of the model children. Example: .. code:: x = MyModel.List([MyModel({"id": 2}), MyModel({"id": 3})]) result = x.sorted_by('id') """ return self.sorted( key=lambda model: getattr(model, attribute, model.get(attribute)) or "", **kw, ) def filter_by( self, attribute_name: str, fnmatch_pattern: str ) -> internal_typing.IterableCollection[Model]: """filter by a single attribute of the model children. Example: .. code:: x = MyModel.List([MyModel({"name": 'chucknorris'}), MyModel({"name": 'foobar'})]) result = x.filter_by('name', '*norris*') """ return self.filter( lambda model: model.attribute_matches( attribute_name, fnmatch_pattern ) ) def filter(self, check: Callable[[Model], bool]) -> Iterable[Model]: """returns a new ``ModelList`` with this collections' children filter. Example: .. code:: x = MyModel.List([MyModel({"id": 2}), MyModel({"id": 3})]) result = x.filter(key=lambda model: model.id) """ results = filter(check, self) return self.__class__(results) def get_table_columns(self, columns: Iterable[str] = None): """proxy to :py:meth:`~uiclasses.base.Model.get_table_columns` """ available_columns = self.__of_model__.__visible_attributes__ if not isinstance(columns, list): return available_columns return self.validate_columns(columns) def get_table_rows(self, columns: Iterable[str] = None): """returns a list of values from the __ui_attributes__ of each child of this collection. Used by :py:meth:`~uiclasses.collections.IterableCollection.format_robust_table` and :py:meth:`~uiclasses.collections.IterableCollection.format_pretty_table`. """ columns = self.get_table_columns(columns) return [ [model.__ui_attributes__().get(key) for key in columns] for model in self ] def get_table_columns_and_rows(self, columns: Iterable[str] = None): """returns a 2-item tuple with columns names and row values of each child of this collection. Used by :py:meth:`~uiclasses.collections.IterableCollection.format_robust_table` and :py:meth:`~uiclasses.collections.IterableCollection.format_pretty_table`. """ columns = self.get_table_columns(columns) rows = self.get_table_rows(columns) return columns, rows def format_robust_table(self, columns: Iterable[str] = None): """returns a string with a robust table ready to be printed on a terminal. powered by :py:func:`humanfriendly.tables.format_robust_table` """ columns, rows = self.get_table_columns_and_rows(columns) return format_robust_table(rows, columns) def format_pretty_table(self, columns: Iterable[str] = None): """returns a string with a pretty table ready to be printed on a terminal. powered by :py:func:`humanfriendly.tables.format_pretty_table` """ columns, rows = self.get_table_columns_and_rows(columns) return format_pretty_table(rows, columns) def validate_columns(self, columns): mismatched_columns = set(columns).difference( self.__of_model__.__visible_attributes__ ) if mismatched_columns: raise ValueError( f"the following columns are not available " f"for {self.__of_model__}: {mismatched_columns}" ) return columns def to_dict(self, only_visible: bool = False) -> Iterable[dict]: """calls ``.to_dict()`` in each children of this collection.""" return [m.to_dict(only_visible=only_visible) for m in self] def serialize(self, only_visible: bool = False) -> Iterable[dict]: """calls ``.serialize()`` in each children of this collection.""" return [m.serialize(only_visible=only_visible) for m in self] def serialize_visible(self) -> Iterable[dict]: """calls ``.serialize_visible()`` in each children of this collection.""" return [m.serialize_visible() for m in self] def serialize_all(self) -> Iterable[dict]: """calls ``.serialize_all()`` in each children of this collection.""" return [m.serialize_all() for m in self]
[docs]class ModelList(list, IterableCollection): """Implementation of :py:class:`~uiclasses.collections.IterableCollection` for the :py:class:`list` type. """ def __init__(self, children: Iterable[Model]): model_class = self.__of_model__ if not is_iterable(children): raise TypeError( f"{self.__class__.__name__} requires the 'children' attribute to be " f"a valid iterable, got {children!r} {type(children)} instead" ) items = [] for index, child in enumerate(children): if isinstance(child, dict): child = self.__of_model__(child) if not isinstance(child, model_class): raise TypeError( f"cannot create {self.__class__.__name__} because value at index [{index}] is not a {model_class}: {child!r} {type(child)}" ) items.append(child) super().__init__(map(model_class, items)) def unique(self) -> "ModelSet": """returns a :py:class:`~uiclasses.collections.ModelSet` of all unique items in this :py:class:`~uiclasses.collections.ModelList`""" return self.__of_model__.Set(self)
[docs]class ModelSet(OrderedSet, IterableCollection): """Implementation of :py:class:`~uiclasses.collections.IterableCollection` for the `OrderedSet <https://pypi.org/project/ordered-set/>`_ type. """ def __init__(self, children: Iterable[Model]): model_class = getattr(self, "__of_model__", None) if not is_iterable(children): raise TypeError( f"{self.__class__.__name__} requires the 'children' attribute to be " f"a valid iterable, got {children!r} {type(children)} instead" ) items = [] for index, child in enumerate(children): if isinstance(child, dict): child = self.__of_model__(child) if not isinstance(child, model_class): raise TypeError( f"cannot create {self.__class__.__name__} because value at index [{index}] is not a {model_class}: {child!r} {type(child)}" ) items.append(child) super().__init__(map(model_class, items))
COLLECTION_TYPES[iter] = IterableCollection COLLECTION_TYPES[list] = ModelList COLLECTION_TYPES[set] = ModelSet COLLECTION_TYPES[OrderedSet] = ModelSet