Get a dictionary of elt properties.

    Such dictionary is filled related to the elt properties, and from all base
        elts properties if not local.

    :param elt: properties elt. Not None methods.
    :param ctx: elt ctx from where get properties. Equals elt if None. It
        allows to get function properties related to a class or instance if
        related function is defined in base class.
    :param keys: keys of properties to get from elt. If None, get all
        properties.
    :param bool local: if False, get properties from bases classes and type
        as well.
    :param set exclude: elts from where not get properties.
    :param bool first: get only first property values.
    :param _found_keys: used such as a cache value for result.

    :return: dict of properties by name and ...:

        - if local or first: value.
        - other cases: [(elt, value)] in order of property definition even if
            multi inheritance do not ensure to respect inheritance order in a
            list.
    :rtype: dict

def _get_properties(
    elt, keys=None, local=False, exclude=None, first=False, ctx=None,
    _found_keys=None
):
    """Get a dictionary of elt properties.

    Such dictionary is filled related to the elt properties, and from all base
        elts properties if not local.

    :param elt: properties elt. Not None methods.
    :param ctx: elt ctx from where get properties. Equals elt if None. It
        allows to get function properties related to a class or instance if
        related function is defined in base class.
    :param keys: keys of properties to get from elt. If None, get all
        properties.
    :param bool local: if False, get properties from bases classes and type
        as well.
    :param set exclude: elts from where not get properties.
    :param bool first: get only first property values.
    :param _found_keys: used such as a cache value for result.

    :return: dict of properties by name and ...:

        - if local or first: value.
        - other cases: [(elt, value)] in order of property definition even if
            multi inheritance do not ensure to respect inheritance order in a
            list.
    :rtype: dict
    """

    result = {}

    # get the best context
    if ctx is None:
        ctx = find_ctx(elt=elt)

    # initialize found keys
    if first and _found_keys is None:
        _found_keys = set()

    # get elt properties if exist
    elt_properties = _ctx_elt_properties(elt=elt, ctx=ctx, create=False)

    # if elt_properties exists
    if elt_properties is not None:
        # if elt properties is not empty
        # try to add property values in result related to input keys
        properties_to_keep = elt_properties.copy() if keys is None else {}
        # get key values
        if keys is not None:
            for key in keys:
                if key in elt_properties:
                    if not first or key not in _found_keys:
                        properties_to_keep[key] = elt_properties[key]
                    if first and key not in _found_keys:
                        _found_keys.add(key)

        if local:  # in case of local, use properties_to_keep
            result = properties_to_keep

        # if not local and not first and properties to keep
        elif not first and properties_to_keep:
            for name in properties_to_keep:
                property_to_keep = properties_to_keep[name]
                # add (elt, properties_to_keep) in result
                if name not in result:
                    result[name] = [(elt, property_to_keep)]
                else:
                    result[name].append((elt, property_to_keep))

        # in case of first, result is properties_to_keep
        elif properties_to_keep:
            result = properties_to_keep
    # check if all keys have been found if first
    keys_left = not first or keys is None or len(keys) != len(_found_keys)

    # if not local or keys left
    if not local and keys_left:
        # initialize exclude
        if exclude is None:
            exclude = set()

        # add elt in exclude in order to avoid to get elt properties twice
        if isinstance(elt, Hashable):
            exclude.add(elt)

        # and search among bases ctx elements
        if ctx is not elt:
            # go to base elts
            try:
                elt_name = elt.__name__
            except AttributeError:
                pass
            else:
                # if elt is a sub attr of ctx
                if hasattr(ctx, elt_name):
                    if hasattr(ctx, __BASES__):
                        ctx_bases = ctx.__bases__
                    elif hasattr(ctx, __CLASS__):
                        ctx_bases = (ctx.__class__,)
                    else:
                        ctx_bases = ()
                    for base_ctx in ctx_bases:
                        # get base_elt properties
                        base_elt = getattr(base_ctx, elt_name, None)
                        if base_elt is not None:
                            base_properties = _get_properties(
                                elt=base_elt,
                                keys=keys,
                                local=local,
                                exclude=exclude,
                                ctx=base_ctx,
                                _found_keys=_found_keys
                            )
                            # update result with base_properties
                            for name in base_properties:
                                properties = base_properties[name]
                                if name not in result:
                                    result[name] = properties
                                else:
                                    result[name] += properties

        else:
            # bases classes
            if hasattr(elt, __BASES__):
                for base in elt.__bases__:
                    if base not in exclude:
                        base_result = _get_properties(
                            elt=base,
                            keys=keys,
                            local=local,
                            exclude=exclude,
                            ctx=base,
                            _found_keys=_found_keys
                        )
                        # update result with base_result
                        for name in base_result:
                            properties = base_result[name]
                            if name not in result:
                                result[name] = properties
                            else:
                                result[name] += properties

            # search among type definition

            # class
            if hasattr(elt, __CLASS__):
                elt_class = elt.__class__
                # get class properties only if elt is not its own class
                if elt_class is not elt and elt_class not in exclude:
                    elt_class_properties = _get_properties(
                        elt=elt_class,
                        keys=keys,
                        local=local,
                        exclude=exclude,
                        ctx=elt_class,
                        _found_keys=_found_keys
                    )
                    # update result with class result
                    for name in elt_class_properties:
                        properties = elt_class_properties[name]
                        if name not in result:
                            result[name] = properties
                        else:
                            result[name] += properties

            # type
            elt_type = type(elt)
            # get elt type properties only if elt is not its type
            if elt_type is not elt and elt_type not in exclude:
                elt_type_properties = _get_properties(
                    elt=elt_type,
                    keys=keys,
                    local=local,
                    exclude=exclude,
                    ctx=elt_type,
                    _found_keys=_found_keys
                )
                # update result with type properties
                for name in elt_type_properties:
                    properties = elt_type_properties[name]
                    if name not in result:
                        result[name] = properties
                    else:
                        result[name] += properties

    return result