"""The `with_doc` decorator: override the docstring that introspection tools
see for a wrapped callable without mutating the wrapped function itself.
Accepts the docstring directly, or a factory that derives it from the
wrapped function at decoration time.

This is the companion of `with_signature`, which overrides the signature in
the same way. Assigning to `__doc__` on an ordinary wrapt wrapper writes
through to the wrapped function, because `__doc__` on every proxy delegates
to the wrapped object, so a wrapper carrying its own docstring is the only
way to change what is reported without touching the wrapped function.
"""

from .__wrapt__ import BoundFunctionWrapper, FunctionWrapper

# Marker for a wrapper which carries no docstring override, so that `__doc__`
# keeps delegating to the wrapped function. Distinct from None, which is a
# legitimate docstring override.

_NO_DOC_OVERRIDE = object()

# The `__doc__` property for a function wrapper which may carry a docstring
# override in its `_self_doc` slot. The wrapper class must assign this to
# `__doc__` in its own class body, rather than inherit it from a mixin, and
# set `_self_doc` in its `__init__()`, to `_NO_DOC_OVERRIDE` when there is
# none.
#
# Both the pure Python metaclass and the C extension attribute hooks
# recognise a `__doc__` descriptor defined by a derived class and consult it
# in place of the default delegation to the wrapped object. It has to be in
# the class dictionary of the wrapper class itself though, because `pydoc`
# reads `__doc__` using `object.__getattribute__()` to avoid inherited
# docstrings, and with the C extension that bypasses the attribute hooks and
# would otherwise find the copy of the wrapped function's docstring which is
# held in the instance dictionary.
#
# Without an override, assignment and deletion write through to the wrapped
# function as they do for any other wrapt wrapper. With one, assignment
# replaces the override and deletion removes it, restoring delegation to the
# wrapped function.


def _get_doc(self):
    doc = self._self_doc
    if doc is _NO_DOC_OVERRIDE:
        return self.__wrapped__.__doc__
    return doc


def _set_doc(self, value):
    if self._self_doc is _NO_DOC_OVERRIDE:
        self.__wrapped__.__doc__ = value
    else:
        self._self_doc = value


def _del_doc(self):
    if self._self_doc is _NO_DOC_OVERRIDE:
        del self.__wrapped__.__doc__
    else:
        self._self_doc = _NO_DOC_OVERRIDE


_DOC_PROPERTY = property(_get_doc, _set_doc, _del_doc)

# The `__doc__` property for the bound wrapper of a function wrapper which
# may carry a docstring override, reading it from the parent. A bound
# wrapper is read only, as `__doc__` of a bound method is.


def _get_bound_doc(self):
    doc = self._self_parent._self_doc
    if doc is _NO_DOC_OVERRIDE:
        return self.__wrapped__.__doc__
    return doc


_BOUND_DOC_PROPERTY = property(_get_bound_doc)


class _BoundDocFunctionWrapper(BoundFunctionWrapper):
    __doc__ = _BOUND_DOC_PROPERTY


class _DocFunctionWrapper(FunctionWrapper):
    __doc__ = _DOC_PROPERTY

    __bound_function_wrapper__ = _BoundDocFunctionWrapper

    def __init__(self, wrapped, wrapper, doc):
        super().__init__(wrapped, wrapper)
        self._self_doc = doc


def with_doc(wrapped=None, /, *, doc=None, factory=None):
    """Override the docstring of a wrapped callable.

    Exactly one of `doc` or `factory` must be supplied:

    - `doc`: the docstring to report.
    - `factory`: a callable `factory(wrapped)` invoked at decoration time
      that returns the docstring to report.

    The resulting wrapper reports the override as its `__doc__`, and so it
    is what `help()` and `pydoc` display. The wrapped function is not
    mutated, and assigning to `__doc__` on the wrapper replaces the override
    rather than writing through to the wrapped function. Calling behaviour
    is unchanged.
    """

    if doc is None and factory is None:
        raise TypeError("with_doc requires one of doc= or factory=")
    if doc is not None and factory is not None:
        raise TypeError("with_doc accepts only one of doc= or factory=")

    def _decorator(wrapped):
        def _wrapper(wrapped, instance, args, kwargs):
            return wrapped(*args, **kwargs)

        if factory is not None:
            resolved = factory(wrapped)
        else:
            resolved = doc

        return _DocFunctionWrapper(wrapped, _wrapper, resolved)

    if wrapped is None:
        return _decorator
    return _decorator(wrapped)
