__author__ = "Brian O'Neill"
_version__ = '0.3.0'

from log_calls import log_calls
import doctest


#-------------------------------------------------------------------
# test__omit_property_attr__repr_with_init_active
#-------------------------------------------------------------------

def test__omit_property_attr__repr_with_init_active():
    """
    >>> @log_calls(omit='pair.deleter')
    ... class P():
    ...     def __init__(self, x, y):
    ...         self.x = x
    ...         self.y = y
    ...     @property
    ...     def pair(self):
    ...         return (self.x, self.y)
    ...     @pair.setter
    ...     def pair(self, pr):
    ...         self.x, self.y = pr
    ...     @pair.deleter
    ...     def pair(self):
    ...         print("pair.deleter called -- wouldn't know what to do.")
    ...     def __repr__(self):
    ...         return '<(%r, %r) at 0x%x>' % (self.x, self.y, id(self))
    >>> p = P(0, 0)                         # doctest: +ELLIPSIS
    P.__init__ <== called by <module>
        arguments: self=<__main__.P object at 0x...>, x=0, y=0
    P.__init__ ==> returning to <module>

    >>> print(p.pair)                       # doctest: +ELLIPSIS
    P.pair <== called by <module>
        arguments: self=<(0, 0) at 0x...>
    P.pair ==> returning to <module>
    (0, 0)

    >>> p.pair = (10, 11)                   # doctest: +ELLIPSIS
    P.pair <== called by <module>
        arguments: self=<(0, 0) at 0x...>, pr=(10, 11)
    P.pair ==> returning to <module>

    >>> print(p)                            # doctest: +ELLIPSIS
    <(10, 11) at 0x...>

    >>> del p.pair
    pair.deleter called -- wouldn't know what to do.
    """
    pass

# SURGERY:
test__omit_property_attr__repr_with_init_active.__doc__ = \
    test__omit_property_attr__repr_with_init_active.__doc__.replace("__main__", __name__)


#-------------------------------------------------------------------
# test__repr_with_init_active_2
# __repr__ handling for objects still in construction;
# class inside a function
#-------------------------------------------------------------------
def test__repr_with_init_active_2():
    """
    >>> @log_calls()
    ... def another_fn(y):
    ...     'called by X.__init__'
    ...     return 2 * y


    >>> def globfn():
    ...     class X():
    ...         def __init__(self):
    ...             self.helper(0)
    ...             another_fn(43)
    ...
    ...         @log_calls()
    ...         def helper(self, z):
    ...             'called by __init__ and by users of X objs'
    ...             return z + 1
    ...
    ...         def __repr__(self):
    ...             return "<X() at 0x%x>" % id(self)
    ...
    ...     return X()

`helper` called with __init__ active, so generic `object.__repr__` is used to display `self`,
 rather than the class's `__repr__`.

    >>> x = globfn()            # doctest: +ELLIPSIS
    globfn.<locals>.X.helper <== called by __init__
        arguments: self=<__main__.globfn.<locals>.X object at 0x...>, z=0
    globfn.<locals>.X.helper ==> returning to __init__
    another_fn <== called by __init__
        arguments: y=43
    another_fn ==> returning to __init__

The class's `__repr__` (address `0x...` is the same as above):
    >>> print(repr(x))          # doctest: +ELLIPSIS
    <X() at 0x...>

The instance's __init__ is not active so the class's `__repr__` is used:
    >>> _ = x.helper(100)       # doctest: +ELLIPSIS
    globfn.<locals>.X.helper <== called by <module>
        arguments: self=<X() at 0x...>, z=100
    globfn.<locals>.X.helper ==> returning to <module>
    """
    pass

# SURGERY:
test__repr_with_init_active_2.__doc__ = \
    test__repr_with_init_active_2.__doc__.replace("__main__", __name__)


#-------------------------------------------------------------------
# MORE __repr__ handling for objects still in construction
#-------------------------------------------------------------------
def test__repr_with_init_active_3():
    """
    >>> @log_calls()
    ... def g(obj):
    ...     pass


    >>> class Y():
    ...     def __init__(self, y):
    ...         g(self)
    ...         self.y = y
    ...
    ...     def method(self):
    ...         g(self)
    ...
    ...     def __repr__(self):
    ...         return "Y(%r)" % self.y
    >>> Y('arg').method()                # doctest: +ELLIPSIS
    g <== called by __init__
        arguments: obj=<__main__.Y object at 0x...>
    g ==> returning to __init__
    g <== called by method
        arguments: obj=Y('arg')
    g ==> returning to method
    """
    pass

# SURGERY:
test__repr_with_init_active_3.__doc__ = \
    test__repr_with_init_active_3.__doc__.replace("__main__", __name__)


#-------------------------------------------------------------------
# Tests/examples of `mute`
#-------------------------------------------------------------------
def test__mute():
    """
When a decorated function is not muted (`mute` is `log_calls.MUTE.NOTHING`, the default),
log_calls produces output as do `log_message` and `log_exprs`, which uses `log_message`:
    >>> @log_calls()
    ... def f():
    ...     f.log_message('Hello, world!')
    >>> f()
    f <== called by <module>
        Hello, world!
    f ==> returning to <module>

When `mute` is `log_calls.MUTE.CALLS` ( == `True`),
no extra indent level, and messages are prefixed with function's display name:
    >>> f.log_calls_settings.mute = log_calls.MUTE.CALLS
    >>> f()
    f: Hello, world!

When `mute` is `log_calls.MUTE.ALL`, log_message produces no output:
    >>> f.log_calls_settings.mute = log_calls.MUTE.ALL
    >>> f()     # (no output)
    """
    pass


#-------------------------------------------------------------------
# Tests/examples of log_message writing only if enabled
#-------------------------------------------------------------------
def test__log_message_only_if_enabled():
    """
Basic test
`log_message` writes only if `enabled` is true:

    >>> @log_calls()
    ... def f():
    ...     f.log_message('Hello, cruel world!')
    >>> f()
    f <== called by <module>
        Hello, cruel world!
    f ==> returning to <module>
    >>> f.log_calls_settings.enabled = False
    >>> f()     # (no output)
    >>> f.log_calls_settings.enabled = -1
    >>> f()     # (no output)


Test with recursion and indirect `enabled` values -
each instance of the fn should behave properly,
and should not destroy the behavior of instances further up the call chain.

    >>> @log_calls(enabled='_on=', indent=True)
    ... def rec(level, _on=True):
    ...     if level < 0:
    ...         rec.log_message("Hit bottom")
    ...         return
    ...     rec.log_message("About to call rec(%d, _on=%s)" % (level-1, not _on))
    ...     rec(level-1, _on=(not _on))
    ...     rec.log_message("Returned from rec(%d, _on=%s)" % (level-1, not _on))

    >>> rec(3)
    rec <== called by <module>
        arguments: level=3
        defaults:  _on=True
        About to call rec(2, _on=False)
        rec <== called by rec <== rec
            arguments: level=1, _on=True
            About to call rec(0, _on=False)
            rec <== called by rec <== rec
                arguments: level=-1, _on=True
                Hit bottom
            rec ==> returning to rec ==> rec
            Returned from rec(0, _on=False)
        rec ==> returning to rec ==> rec
        Returned from rec(2, _on=False)
    rec ==> returning to <module>

NOTE: In the call chains `rec <== called by rec <== rec` (and
similarly for `rec ==> returning to rec ==> rec`), the nearest `rec`,
to the left of "<==", is not enabled, and `log_calls` has chased back
further till it found an *enabled* function it decorated (namely, another
invocation of `rec`, with an odd value for `level`).
    """
    pass


#-------------------------------------------------------------------
# Tests/examples of mute setting as an indirect value
#-------------------------------------------------------------------
def test__log_message__indirect_mute():
    """
    settings = {'indent': True, 'mute': 'mute_'}

    @log_calls(settings=settings)
    def f(extra_mute_val=None, **kwargs):
        f.log_message("before g", prefix_with_name=True)
        g(extra_mute_val=extra_mute_val, **kwargs)
        f.log_message("after g", prefix_with_name=True)

    @log_calls(settings=settings)
    def g(extra_mute_val=None, **kwargs):
        g.log_message("before h", prefix_with_name=True)
        if extra_mute_val is not None and 'mute_' in kwargs:
            kwargs['mute_'] = extra_mute_val
        h(**kwargs)
        g.log_message("after h", prefix_with_name=True)

    @log_calls(settings=settings)
    def h(**kwargs):
        h.log_message("greetings", prefix_with_name=True)

    f(mute_=False)
    '''
    f <== called by <module>
        arguments: [**]kwargs={'mute_': False}
        defaults:  extra_mute_val=None
        f: before g
        g <== called by f
            arguments: extra_mute_val=None, [**]kwargs={'mute_': False}
            g: before h
            h <== called by g
                arguments: [**]kwargs={'mute_': False}
                h: greetings
            h ==> returning to g
            g: after h
        g ==> returning to f
        f: after g
    f ==> returning to <module>
    '''
    print('-----------------------')
    f(mute_=True)                   # True == log_calls.MUTE.CALLS
    '''
    f: before g
        g: before h
            h: greetings
        g: after h
    f: after g
    '''
    print('-----------------------')
    f(mute_=True, extra_mute_val=log_calls.MUTE.ALL)    # shut up h
    '''
    f: before g
        g: before h
        g: after h
    f: after g
    '''
    print('-----------------------')
    f(mute_=log_calls.MUTE.ALL)
    # (no output)

    """
    pass


#-------------------------------------------------------------------
# test__global_mute
#-------------------------------------------------------------------
def test__global_mute():
    """
Mute setting applied for a function's log_calls output
and within a function for log_message and log_expr output
is
    `max(`*function's mute setting*`, `*global mute*`)`

### Basic examples/tests

    >>> @log_calls()
    ... def f(): f.log_message("Hi"); g()
    >>> @log_calls()
    ... def g(): g.log_message("Hi")


    >>> assert log_calls.mute == False  # default
    >>> f()
    f <== called by <module>
        Hi
        g <== called by f
            Hi
        g ==> returning to f
    f ==> returning to <module>

    >>> log_calls.mute = log_calls.MUTE.CALLS
    >>> f()
    f: Hi
        g: Hi

    >>> log_calls.mute = log_calls.MUTE.ALL
    >>> f()     # (no output)

    >>> log_calls.mute = False
    >>> g.log_calls_settings.mute = log_calls.MUTE.CALLS
    >>> f()
    f <== called by <module>
        Hi
        g: Hi
    f ==> returning to <module>

    >>> log_calls.mute = log_calls.MUTE.CALLS
    >>> g.log_calls_settings.mute = log_calls.MUTE.ALL
    >>> f()
    f: Hi

### Dynamic examples/tests

Global mute is always checked realtime

    >>> @log_calls()
    ... def f(mute=False):
    ...     f.log_message("before g")
    ...     g(mute=mute)
    ...     f.log_message("after g")

    >>> @log_calls()
    ... def g(mute=False):
    ...     g.log_message("entering g")
    ...     log_calls.mute = mute
    ...     g.log_message("leaving g")

    >>> log_calls.mute = False

Calls to f(), with default arg False, in effect turn off global mute midway through g:

    >>> f()
    f <== called by <module>
        arguments: <none>
        defaults:  mute=False
        before g
        g <== called by f
            arguments: mute=False
            entering g
            leaving g
        g ==> returning to f
        after g
    f ==> returning to <module>

    >>> log_calls.mute = log_calls.MUTE.CALLS
    >>> f()
    f: before g
        g: entering g
            leaving g
        g ==> returning to f
        after g
    f ==> returning to <module>

    >>> log_calls.mute = log_calls.MUTE.ALL
    >>> f()
            leaving g
        g ==> returning to f
        after g
    f ==> returning to <module>

    >>> log_calls.mute = log_calls.MUTE.CALLS
    >>> g.log_calls_settings.mute = log_calls.MUTE.ALL
    >>> f()
    f: before g
        after g
    f ==> returning to <module>

    >>> log_calls.mute = False

`g` is still completely muted
    >>> f(mute=log_calls.MUTE.CALLS)
    f <== called by <module>
        arguments: mute=True
        before g
    f: after g

`g` is still completely muted, and `log_calls.mute == log_calls.MUTE.CALLS`
    >>> f(mute=log_calls.MUTE.ALL)
    f: before g

    >>> log_calls.mute = False  # restore default!
    """
    pass


#-------------------------------------------------------------------
# Tests/examples of log_exprs
#-------------------------------------------------------------------
def test__log_exprs():
    """
    >>> import os
    >>> import math

    >>> @log_calls(mute=True)
    ... def fn(num_files):
    ...     order_of_magnitude = round(math.log10(num_files), 2)
    ...     fn.log_exprs('num_files', 'order_of_magnitude')
    ...     files_per_CPU = math.ceil(num_files/8)
    ...     username = "Joe Doe"
    ...     fn.log_exprs('files_per_CPU', 'username')
    ...     # ...
    ...     # bad exprs:
    ...     z = []
    ...     fn.log_exprs('16-', 'no_such_variable', '"some bum string', 'z[0]',
    ...                  sep='\\n' + (' ' * len('fn: ')))

    >>> fn(10000)
    fn: num_files = 10000, order_of_magnitude = 4.0
    fn: files_per_CPU = 1250, username = 'Joe Doe'
    fn: 16- = '<** unexpected EOF while parsing (<string>, line 1) **>'
        no_such_variable = "<** name 'no_such_variable' is not defined **>"
        "some bum string = '<** EOL while scanning string literal (<string>, line 1) **>'
        z[0] = '<** list index out of range **>'

Another:

    >>> def f(i):
    ...     return 2.5 * i**3 - 5 * i**2 + 17

    >>> @log_calls()        # mute=True
    ... def g(n):
    ...     for i in range(n):
    ...         g.log_exprs('i', 'f(i)')
    ...
    >>> g(5)
    g <== called by <module>
        arguments: n=5
        i = 0, f(i) = 17.0
        i = 1, f(i) = 14.5
        i = 2, f(i) = 17.0
        i = 3, f(i) = 39.5
        i = 4, f(i) = 97.0
    g ==> returning to <module>

0.3.1
    >>> @log_calls()
    ... def gcd(a, b):
    ...     log_calls.print("At bottom of loop:")
    ...     while b:
    ...         a, b = b, (a % b)
    ...         log_calls.print_exprs('a', 'b', prefix="\\t", suffix= '\\t<--')
    ...     return a
    >>> gcd(48, 246)            # doctest: +NORMALIZE_WHITESPACE
    gcd <== called by <module>
        arguments: a=48, b=246
        At bottom of loop:
        	a = 246, b = 48	<--
        	a = 48, b = 6	<--
        	a = 6, b = 0	<--
    gcd ==> returning to <module>
    6

    >>> gcd.log_calls_settings.enabled = False
    >>> gcd(48, 246)
    6
    """
    pass

##############################################################################
# end of tests.
##############################################################################


#-----------------------------------------------------------------------------
# For unittest integration
#-----------------------------------------------------------------------------
def load_tests(loader, tests, ignore):
    tests.addTests(doctest.DocTestSuite())
    return tests


if __name__ == '__main__':

    doctest.testmod()


