from typing import Any
from typing import Optional
from typing import Iterable
from typing import Dict
import inspect


def get_public_attrs(
    obj: Any,
    *, include_single_underscore: bool = False,
    include_methods: bool = False,
    include_also: Optional[Iterable[str]] = None
) -> Dict[str, Any]:
    """Returns a dict that maps attribute name to attribute value for a Python object.
    Note that it only considers PUBLIC attributes (i.e., those without leading underscores). Refer to options below
    for further customization of output.

    Args:
        obj (Any): Python object whose attributes should be fetched.
        include_single_underscore (:class:`bool`, optional): If ``True``, attributes with a single (but not double)
            leading underscore are also included. Defaults to ``False`` (only include attributes without leading
            underscores in their names).
        include_methods (:class:`bool`, optional): If ``True``, methods are also included in the generated dict.
            Defaults to ``False`` (only include non-method attributes).
        include_also (Optional[Iterable[:class:`str`]], optional): List of extra (private) attributes to include in
            the final dict, IF `obj` has them. Defaults to ``None`` (only include attributes that follow the rules
            above).

    Returns:
        Dict[:class:`str`, Any]: The resulting dict mapping attribute names to attribute values for `obj` 's public
        attributes (following the rules above).

    Examples:
        .. testsetup::

            from serpcord.utils.model import get_public_attrs
        .. doctest::

            >>> class A:
            ...     def __init__(self, a, b, c):
            ...         self.a = a
            ...         self.b = b
            ...         self.c = c
            ...         self._d = a + b
            ...         self._e = b + c
            ...     def sum_a_c(self):
            ...         return self.a + self.c
            >>> obj = A(1, 2, 3)
            >>> get_public_attrs(obj)
            {'a': 1, 'b': 2, 'c': 3}
            >>> get_public_attrs(obj, include_single_underscore=True)
            {'_d': 3, '_e': 5, 'a': 1, 'b': 2, 'c': 3}
            >>> get_public_attrs(obj, include_also=["_e"])
            {'_e': 5, 'a': 1, 'b': 2, 'c': 3}
            >>> get_public_attrs(obj, include_methods=True)
            {'a': 1, 'b': 2, 'c': 3, 'sum_a_c': <bound method A.sum_a_c of <A object at ...>>}
    """
    return {
        k: v
        for k, v in inspect.getmembers(obj)
        if (
            (include_also is not None and hasattr(include_also, "__contains__") and k in include_also)
            or (
                k and isinstance(k, str) and not k.startswith("__") and (
                 include_single_underscore or not k.startswith("_")
                )
                and (include_methods or not inspect.ismethod(v))
            )
        )
    }