Detecting attributes, inherits and relations

    :param items: list of objects to describe
    :param show_methods: do detection of methods
    :param show_properties: do detection of properties

    Return tuple (objects, relations, inherits)


    Where objects is list::

        [{
            'name': '<Mapper class name or table name>',
            'cols': [
                ('<Column type class name>', '<Column name>'),
                ...
            ],
            'props': ['<Property name>'],
            'methods': ['<Method name>', ...],
        }, ...]


    Relations is::

        [{
            'from': '<From mapper class name>',
            'by': '<By mapper foreign key column name>',
            'to': '<To mapper class name>',
        }, ...]


    Example usage::

        import sadisplay
        from app import models

        desc = sadisplay.describe([
            getattr(model, attr) for attr in dir(model)
        ])

        desc = sadisplay.describe([models.User, models.Group])

def describe(items, show_methods=True, show_properties=True):
    """Detecting attributes, inherits and relations

    :param items: list of objects to describe
    :param show_methods: do detection of methods
    :param show_properties: do detection of properties

    Return tuple (objects, relations, inherits)


    Where objects is list::

        [{
            'name': '<Mapper class name or table name>',
            'cols': [
                ('<Column type class name>', '<Column name>'),
                ...
            ],
            'props': ['<Property name>'],
            'methods': ['<Method name>', ...],
        }, ...]


    Relations is::

        [{
            'from': '<From mapper class name>',
            'by': '<By mapper foreign key column name>',
            'to': '<To mapper class name>',
        }, ...]


    Example usage::

        import sadisplay
        from app import models

        desc = sadisplay.describe([
            getattr(model, attr) for attr in dir(model)
        ])

        desc = sadisplay.describe([models.User, models.Group])
    """

    class EntryItem(object):
        """Class adaptor for mapped classes and tables"""
        name = None
        methods = []
        columns = []
        inherits = None
        properties = []
        bases = tuple()

        def __init__(self, mapper=None, table=None):

            if mapper is not None:
                self.name = mapper.class_.__name__
                self.columns = mapper.columns
                self.methods = mapper.class_.__dict__.items()
                self.inherits = mapper.inherits
                self.properties = mapper.iterate_properties
                self.bases = mapper.class_.__bases__
                self.class_ = mapper.class_
                self.table_name = str(mapper.mapped_table)

            elif table is not None:
                self.name = table.name
                self.table_name = table.name
                # prepend schema if exists for foreign key matching
                if hasattr(table, "schema") and table.schema:
                    self.table_name = table.schema + "." + self.table_name
                self.columns = table.columns
            else:
                pass

        def __repr__(self):
            return '<{s.__class__.__name__} {s.name}>'.format(s=self)

        def __eq__(self, other):
            if other.inherits or self.inherits:
                return self.name == other.name
            return self.table_name == other.table_name

    objects = []
    relations = []
    inherits = []

    entries = []
    for item in items:
        try:
            mapper = class_mapper(item)
        except (exc.ArgumentError, orm.exc.UnmappedClassError):
            if isinstance(item, Table):
                entity = EntryItem(table=item)
            else:
                continue
        else:
            entity = EntryItem(mapper=mapper)

        if entity not in entries:
            entries.append(entity)

    for entry in entries:

        result_item = {
            'name': entry.name,
            'cols': [
                (c.type.__class__.__name__, c.name) for c in entry.columns
            ],
            'props': [],
            'methods': [],
        }

        if show_methods and entry.methods:

            if entry.inherits:
                base_methods = entry.inherits.class_.__dict__.keys()
            else:
                # Create the DummyClass subclass of mapper bases
                # for detecting mapper own methods
                suffix = '%s' % str(uuid.uuid4())
                params = {
                    '__tablename__': 'dummy_table_%s' % suffix,
                    'dummy_id_col': Column(Integer, primary_key=True)
                }

                DummyClass = type('Dummy%s' % suffix, entry.bases, params)

                base_methods = DummyClass.__dict__.keys()

            # Filter mapper methods
            for name, func in entry.methods:
                if name[0] != '_' and name not in base_methods:
                    if isinstance(func, types.FunctionType):
                        result_item['methods'].append(name)

        if show_properties and entry.properties:
            for item in entry.properties:
                if not isinstance(item, ColumnProperty):
                    result_item['props'].append(item.key)

        # ordering
        for key in ('methods', 'props'):
            result_item[key].sort()

        objects.append(result_item)

        # Detect relations by ForeignKey
        for col in entry.columns:
            for fk in col.foreign_keys:
                table = fk.column.table
                for m in entries:
                    try:
                        if str(table) == str(m.table_name):
                            relations.append({
                                'from': entry.name,
                                'by': col.name,
                                'to': m.name,
                            })
                    except AttributeError:
                        pass

        if entry.inherits:

            inh = {
                'child': entry.name,
                'parent': EntryItem(mapper=entry.inherits).name,
            }

            inherits.append(inh)

            # Delete relation by inherits
            for i, rel in enumerate(relations):
                if inh['child'] == rel['from'] and inh['parent'] == rel['to']:
                    relations.pop(i)

    return objects, relations, inherits