# -*- coding: utf-8 -*-
"""
Native xdoctest interface to the collecting, executing, and summarizing the
results of running doctests. This is an alternative to running through pytest.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from xdoctest import dynamic_analysis as dynamic
from xdoctest import core
from xdoctest import doctest_example
from xdoctest import utils
import time
import warnings
import sys


def doctest_module(modpath_or_name=None, command=None, argv=None, exclude=[],
                   style='auto', verbose=None, config=None):
    """
    Executes requestsed google-style doctests in a package or module.
    Main entry point into the testing framework.

    Args:
        modname (str): name of or path to the module.
        command (str): determines which doctests to run.
            if command is None, this is determined by parsing sys.argv
        argv (list): if None uses sys.argv
        verbose (bool):  verbosity flag
        exclude (list): ignores any modname matching any of these
            glob-like patterns
        config (dict): modifies each examples configuration

    Example:
        >>> modname = 'xdoctest.dynamic_analysis'
        >>> result = doctest_module(modname, 'list', argv=[''])
    """
    print('Start doctest_module({!r})'.format(modpath_or_name))

    # Determine package name via caller if not specified
    if modpath_or_name is None:
        frame_parent = dynamic.get_parent_frame()
        modpath = frame_parent.f_globals['__file__']
    else:
        modpath = core._rectify_to_modpath(modpath_or_name)

    command, style, verbose = _parse_commandline(command, style, verbose, argv)

    if command == 'list':
        print('Listing tests')

    if command is None:
        # Display help if command is not specified
        print('Not testname given. Use `all` to run everything or'
              ' pick from a list of valid choices:')
        command = 'list'

    # TODO: command should not be allowed to be the requested doctest name in
    # case it conflicts with an existing command. This probably requires an API
    # change to this function.
    gather_all = (command == 'all' or command == 'dump')

    tic = time.time()

    # Parse all valid examples
    with warnings.catch_warnings(record=True) as parse_warnlist:
        examples = list(core.parse_doctestables(modpath, exclude=exclude,
                                                style=style))
        # Set each example mode to native to signal that we are using the
        # native xdoctest runner instead of the pytest runner
        for example in examples:
            example.mode = 'native'

    if command == 'list':
        if len(examples) == 0:
            print('... no docstrings with examples found')
        else:
            print('    ' + '\n    '.join([example.cmdline  # + ' @ ' + str(example.lineno)
                                          for example in examples]))
        run_summary = {'action': 'list'}
    else:
        print('gathering tests')
        enabled_examples = []
        for example in examples:
            if gather_all or command in example.valid_testnames:
                if gather_all and example.is_disabled():
                    continue
                enabled_examples.append(example)

        if len(enabled_examples) == 0:
            # Check for zero-arg funcs
            for example in _gather_zero_arg_examples(modpath):
                if command in example.valid_testnames:
                    enabled_examples.append(example)

        if config:
            for example in enabled_examples:
                example.config.update(config)

        if command == 'dump':
            # format the doctests as normal unit tests
            print('dumping tests to stdout')
            _convert_to_test_module(enabled_examples)
            run_summary = {'action': 'dump'}
        else:
            # Run the gathered doctest examples
            run_summary = _run_examples(enabled_examples, verbose)

            toc = time.time()
            n_seconds = toc - tic

            # Print final summary info in a style similar to pytest
            if verbose >= 0 and run_summary:
                _print_summary_report(run_summary, parse_warnlist, n_seconds,
                                      enabled_examples)

    return run_summary


def _convert_to_test_module(enabled_examples):
    """
    Converts all doctests to unit tests that can exist in a standalone module
    """
    module_lines = []
    for example in enabled_examples:
        # Create a unit-testable function for this example
        func_name = 'test_' + example.callname.replace('.', '_')
        body_lines = []
        for part in example._parts:
            body_part = part.format_src(linenos=False, want=False,
                                        prefix=False, colored=False,
                                        partnos=False)
            if part.want:
                want_text = '# doctest want:\n'
                want_text += utils.indent(part.want, '# ')
                body_part += '\n' + want_text
            body_lines.append(body_part)
        body = '\n'.join(body_lines)
        func_text = 'def {}():\n'.format(func_name) + utils.indent(body)
        module_lines.append(func_text)

    module_text = '\n\n\n'.join(module_lines)
    print(module_text)


def _print_summary_report(run_summary, parse_warnlist, n_seconds,
                          enabled_examples):
    """
    Summary report formatting and printing
    """
    def cprint(text, color):
        print(utils.color_text(text, color))

    # report errors
    failed = run_summary.get('failed', [])
    warned = run_summary.get('warned', [])

    # report parse-time warnings
    if parse_warnlist:
        cprint('\n=== Found {} parse-time warnings ==='.format(
            len(parse_warnlist)), 'yellow')

        for warn_idx, warn in enumerate(parse_warnlist, start=1):
            cprint('--- Parse Warning: {} / {} ---'.format(
                warn_idx, len(parse_warnlist)), 'yellow')
            print(utils.indent(
                warnings.formatwarning(warn.message, warn.category,
                                       warn.filename, warn.lineno)))

    # report run-time warnings
    if warned:
        cprint('\n=== Found {} run-time warnings ==='.format(len(warned)), 'yellow')
        for warn_idx, example in enumerate(warned, start=1):
            cprint('--- Runtime Warning: {} / {} ---'.format(warn_idx, len(warned)),
                   'yellow')
            print('example = {!r}'.format(example))
            for warn in example.warn_list:
                print(utils.indent(
                    warnings.formatwarning(warn.message, warn.category,
                                           warn.filename, warn.lineno)))

    if failed and len(enabled_examples) > 1:
        # If there is more than one test being run, print out all the
        # errors that occured so they are consolidated in a single place.
        cprint('\n=== Found {} errors ==='.format(len(failed)), 'red')
        for fail_idx, example in enumerate(failed, start=1):
            cprint('--- Error: {} / {} ---'.format(fail_idx, len(failed)), 'red')
            print(utils.indent('\n'.join(example.repr_failure())))

    # Print command lines to re-run failed tests
    if failed:
        cprint('\n=== Failed tests ===', 'red')
        for example in failed:
            print(example.cmdline)

    # final summary
    n_passed = run_summary.get('n_passed', 0)
    n_failed = run_summary.get('n_failed', 0)
    n_warnings = len(warned) + len(parse_warnlist)
    pairs = zip([n_failed, n_passed, n_warnings],
                ['failed', 'passed', 'warnings'])
    parts = ['{n} {t}'.format(n=n, t=t) for n, t in pairs  if n > 0]
    fmtstr = '=== ' + ' '.join(parts) + ' in {n_seconds:.2f} seconds ==='
    summary_line = fmtstr.format(n_seconds=n_seconds)
    # color text based on worst type of error
    if n_failed > 0:
        summary_line = utils.color_text(summary_line, 'red')
    elif n_warnings > 0:
        summary_line = utils.color_text(summary_line, 'yellow')
    else:
        summary_line = utils.color_text(summary_line, 'green')
    print(summary_line)


def _gather_zero_arg_examples(modpath):
    """
    Find functions in `modpath` args  with no args (so we can automatically
    make a dummy docstring).
    """
    for calldefs, _modpath in core.package_calldefs(modpath):
        for callname, calldef in calldefs.items():
            if calldef.args is not None:
                # The only existing args should have defaults
                n_args = len(calldef.args.args) - len(calldef.args.defaults)
                if n_args == 0:
                    # Create a dummy doctest example for a zero-arg function
                    docsrc = '>>> {}()'.format(callname)
                    example = doctest_example.DocTest(docsrc=docsrc,
                                                      modpath=_modpath,
                                                      callname=callname,
                                                      block_type='zero-arg')
                    example.mode = 'native'
                    yield example


def _run_examples(enabled_examples, verbose):
    """
    Internal helper, loops over each example, runs it, returns a summary
    """
    n_total = len(enabled_examples)
    print('running %d test(s)' % n_total)
    summaries = []
    failed = []
    warned = []
    # It is important to raise immediatly within the test to display errors
    # returned from multiprocessing. Especially in zero-arg mode

    on_error = 'return' if n_total > 1 else 'raise'
    on_error = 'return'
    for example in enabled_examples:
        try:
            summary = example.run(verbose=verbose, on_error=on_error)
        except Exception:
            print('\n'.join(example.repr_failure(with_tb=False)))
            raise

        summaries.append(summary)
        if example.warn_list:
            warned.append(example)
        if summary['passed']:
            if verbose == 0:
                # TODO: should we write anything when verbose=0?
                sys.stdout.write('.')
                sys.stdout.flush()
        else:
            failed.append(example)
            if verbose == 0:
                sys.stdout.write('F')
                sys.stdout.flush()
            if on_error == 'raise':
                # What happens if we don't re-raise here?
                # If it is necessary, write a message explaining why
                print('\n'.join(example.repr_failure()))
                ex_value = example.exc_info[1]
                raise ex_value

        # except Exception:
        #     summary = {'passed': False}
        #     if verbose == 0:
        #         sys.stdout.write('F')
        #         sys.stdout.flush()
    if verbose == 0:
        print('')
    n_passed = sum(s['passed'] for s in summaries)

    print(utils.color_text('============', 'white'))

    if n_total > 1:
        # and verbose > 0:
        print('Finished doctests')
        print('%d / %d passed'  % (n_passed, n_total))

    run_summary = {
        'failed': failed,
        'warned': warned,
        'action': 'run_examples',
        'n_warned': len(warned),
        'n_passed': n_passed,
        'n_failed': n_total - n_passed,
        'n_total': n_total,
    }
    return run_summary


def _parse_commandline(command=None, style='auto', verbose=None, argv=None):
    # Determine command via sys.argv if not specified
    if argv is None:
        argv = sys.argv[1:]
    else:
        print('argv = {!r}'.format(argv))

    if command is None:
        if len(argv) >= 1:
            if argv[0] and not argv[0].startswith('-'):
                command = argv[0]

    # Change how docstrs are found
    if '--freeform' in argv:
        style = 'freeform'
    elif '--google' in argv:
        style = 'google'

    # Parse verbosity flag
    if verbose is None:
        if '--verbose' in argv:
            verbose = 3
        elif '--quiet' in argv:
            verbose = 0
        elif '--silent' in argv:
            verbose = -1
        else:
            verbose = 3
    return command, style, verbose


if __name__ == '__main__':
    r"""
    CommandLine:
        python -m xdoctest.runner
    """
    import xdoctest as xdoc
    xdoc.doctest_module()

# -*- coding: utf-8 -*-
"""
Native xdoctest interface to the collecting, executing, and summarizing the
results of running doctests. This is an alternative to running through pytest.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from xdoctest import dynamic_analysis as dynamic
from xdoctest import core
from xdoctest import doctest_example
from xdoctest import utils
import time
import warnings
import sys


def doctest_module(modpath_or_name=None, command=None, argv=None, exclude=[],
                   style='auto', verbose=None, config=None):
    """
    Executes requestsed google-style doctests in a package or module.
    Main entry point into the testing framework.

    Args:
        modname (str): name of or path to the module.
        command (str): determines which doctests to run.
            if command is None, this is determined by parsing sys.argv
        argv (list): if None uses sys.argv
        verbose (bool):  verbosity flag
        exclude (list): ignores any modname matching any of these
            glob-like patterns
        config (dict): modifies each examples configuration

    Example:
        >>> modname = 'xdoctest.dynamic_analysis'
        >>> result = doctest_module(modname, 'list', argv=[''])
    """
    print('Start doctest_module({!r})'.format(modpath_or_name))

    # Determine package name via caller if not specified
    if modpath_or_name is None:
        frame_parent = dynamic.get_parent_frame()
        modpath = frame_parent.f_globals['__file__']
    else:
        modpath = core._rectify_to_modpath(modpath_or_name)

    command, style, verbose = _parse_commandline(command, style, verbose, argv)

    if command == 'list':
        print('Listing tests')

    if command is None:
        # Display help if command is not specified
        print('Not testname given. Use `all` to run everything or'
              ' pick from a list of valid choices:')
        command = 'list'

    # TODO: command should not be allowed to be the requested doctest name in
    # case it conflicts with an existing command. This probably requires an API
    # change to this function.
    gather_all = (command == 'all' or command == 'dump')

    tic = time.time()

    # Parse all valid examples
    with warnings.catch_warnings(record=True) as parse_warnlist:
        examples = list(core.parse_doctestables(modpath, exclude=exclude,
                                                style=style))
        # Set each example mode to native to signal that we are using the
        # native xdoctest runner instead of the pytest runner
        for example in examples:
            example.mode = 'native'

    if command == 'list':
        if len(examples) == 0:
            print('... no docstrings with examples found')
        else:
            print('    ' + '\n    '.join([example.cmdline  # + ' @ ' + str(example.lineno)
                                          for example in examples]))
        run_summary = {'action': 'list'}
    else:
        print('gathering tests')
        enabled_examples = []
        for example in examples:
            if gather_all or command in example.valid_testnames:
                if gather_all and example.is_disabled():
                    continue
                enabled_examples.append(example)

        if len(enabled_examples) == 0:
            # Check for zero-arg funcs
            for example in _gather_zero_arg_examples(modpath):
                if command in example.valid_testnames:
                    enabled_examples.append(example)

        if config:
            for example in enabled_examples:
                example.config.update(config)

        if command == 'dump':
            # format the doctests as normal unit tests
            print('dumping tests to stdout')
            _convert_to_test_module(enabled_examples)
            run_summary = {'action': 'dump'}
        else:
            # Run the gathered doctest examples
            run_summary = _run_examples(enabled_examples, verbose)

            toc = time.time()
            n_seconds = toc - tic

            # Print final summary info in a style similar to pytest
            if verbose >= 0 and run_summary:
                _print_summary_report(run_summary, parse_warnlist, n_seconds,
                                      enabled_examples)

    return run_summary


def _convert_to_test_module(enabled_examples):
    """
    Converts all doctests to unit tests that can exist in a standalone module
    """
    module_lines = []
    for example in enabled_examples:
        # Create a unit-testable function for this example
        func_name = 'test_' + example.callname.replace('.', '_')
        body_lines = []
        for part in example._parts:
            body_part = part.format_src(linenos=False, want=False,
                                        prefix=False, colored=False,
                                        partnos=False)
            if part.want:
                want_text = '# doctest want:\n'
                want_text += utils.indent(part.want, '# ')
                body_part += '\n' + want_text
            body_lines.append(body_part)
        body = '\n'.join(body_lines)
        func_text = 'def {}():\n'.format(func_name) + utils.indent(body)
        module_lines.append(func_text)

    module_text = '\n\n\n'.join(module_lines)
    print(module_text)


def _print_summary_report(run_summary, parse_warnlist, n_seconds,
                          enabled_examples):
    """
    Summary report formatting and printing
    """
    def cprint(text, color):
        print(utils.color_text(text, color))

    # report errors
    failed = run_summary.get('failed', [])
    warned = run_summary.get('warned', [])

    # report parse-time warnings
    if parse_warnlist:
        cprint('\n=== Found {} parse-time warnings ==='.format(
            len(parse_warnlist)), 'yellow')

        for warn_idx, warn in enumerate(parse_warnlist, start=1):
            cprint('--- Parse Warning: {} / {} ---'.format(
                warn_idx, len(parse_warnlist)), 'yellow')
            print(utils.indent(
                warnings.formatwarning(warn.message, warn.category,
                                       warn.filename, warn.lineno)))

    # report run-time warnings
    if warned:
        cprint('\n=== Found {} run-time warnings ==='.format(len(warned)), 'yellow')
        for warn_idx, example in enumerate(warned, start=1):
            cprint('--- Runtime Warning: {} / {} ---'.format(warn_idx, len(warned)),
                   'yellow')
            print('example = {!r}'.format(example))
            for warn in example.warn_list:
                print(utils.indent(
                    warnings.formatwarning(warn.message, warn.category,
                                           warn.filename, warn.lineno)))

    if failed and len(enabled_examples) > 1:
        # If there is more than one test being run, print out all the
        # errors that occured so they are consolidated in a single place.
        cprint('\n=== Found {} errors ==='.format(len(failed)), 'red')
        for fail_idx, example in enumerate(failed, start=1):
            cprint('--- Error: {} / {} ---'.format(fail_idx, len(failed)), 'red')
            print(utils.indent('\n'.join(example.repr_failure())))

    # Print command lines to re-run failed tests
    if failed:
        cprint('\n=== Failed tests ===', 'red')
        for example in failed:
            print(example.cmdline)

    # final summary
    n_passed = run_summary.get('n_passed', 0)
    n_failed = run_summary.get('n_failed', 0)
    n_warnings = len(warned) + len(parse_warnlist)
    pairs = zip([n_failed, n_passed, n_warnings],
                ['failed', 'passed', 'warnings'])
    parts = ['{n} {t}'.format(n=n, t=t) for n, t in pairs  if n > 0]
    fmtstr = '=== ' + ' '.join(parts) + ' in {n_seconds:.2f} seconds ==='
    summary_line = fmtstr.format(n_seconds=n_seconds)
    # color text based on worst type of error
    if n_failed > 0:
        summary_line = utils.color_text(summary_line, 'red')
    elif n_warnings > 0:
        summary_line = utils.color_text(summary_line, 'yellow')
    else:
        summary_line = utils.color_text(summary_line, 'green')
    print(summary_line)


def _gather_zero_arg_examples(modpath):
    """
    Find functions in `modpath` args  with no args (so we can automatically
    make a dummy docstring).
    """
    for calldefs, _modpath in core.package_calldefs(modpath):
        for callname, calldef in calldefs.items():
            if calldef.args is not None:
                # The only existing args should have defaults
                n_args = len(calldef.args.args) - len(calldef.args.defaults)
                if n_args == 0:
                    # Create a dummy doctest example for a zero-arg function
                    docsrc = '>>> {}()'.format(callname)
                    example = doctest_example.DocTest(docsrc=docsrc,
                                                      modpath=_modpath,
                                                      callname=callname,
                                                      block_type='zero-arg')
                    example.mode = 'native'
                    yield example


def _run_examples(enabled_examples, verbose):
    """
    Internal helper, loops over each example, runs it, returns a summary
    """
    n_total = len(enabled_examples)
    print('running %d test(s)' % n_total)
    summaries = []
    failed = []
    warned = []
    # It is important to raise immediatly within the test to display errors
    # returned from multiprocessing. Especially in zero-arg mode

    on_error = 'return' if n_total > 1 else 'raise'
    on_error = 'return'
    for example in enabled_examples:
        try:
            summary = example.run(verbose=verbose, on_error=on_error)
        except Exception:
            print('\n'.join(example.repr_failure(with_tb=False)))
            raise

        summaries.append(summary)
        if example.warn_list:
            warned.append(example)
        if summary['passed']:
            if verbose == 0:
                # TODO: should we write anything when verbose=0?
                sys.stdout.write('.')
                sys.stdout.flush()
        else:
            failed.append(example)
            if verbose == 0:
                sys.stdout.write('F')
                sys.stdout.flush()
            if on_error == 'raise':
                # What happens if we don't re-raise here?
                # If it is necessary, write a message explaining why
                print('\n'.join(example.repr_failure()))
                ex_value = example.exc_info[1]
                raise ex_value

        # except Exception:
        #     summary = {'passed': False}
        #     if verbose == 0:
        #         sys.stdout.write('F')
        #         sys.stdout.flush()
    if verbose == 0:
        print('')
    n_passed = sum(s['passed'] for s in summaries)

    print(utils.color_text('============', 'white'))

    if n_total > 1:
        # and verbose > 0:
        print('Finished doctests')
        print('%d / %d passed'  % (n_passed, n_total))

    run_summary = {
        'failed': failed,
        'warned': warned,
        'action': 'run_examples',
        'n_warned': len(warned),
        'n_passed': n_passed,
        'n_failed': n_total - n_passed,
        'n_total': n_total,
    }
    return run_summary


def _parse_commandline(command=None, style='auto', verbose=None, argv=None):
    # Determine command via sys.argv if not specified
    if argv is None:
        argv = sys.argv[1:]
    else:
        print('argv = {!r}'.format(argv))

    if command is None:
        if len(argv) >= 1:
            if argv[0] and not argv[0].startswith('-'):
                command = argv[0]

    # Change how docstrs are found
    if '--freeform' in argv:
        style = 'freeform'
    elif '--google' in argv:
        style = 'google'

    # Parse verbosity flag
    if verbose is None:
        if '--verbose' in argv:
            verbose = 3
        elif '--quiet' in argv:
            verbose = 0
        elif '--silent' in argv:
            verbose = -1
        else:
            verbose = 3
    return command, style, verbose


if __name__ == '__main__':
    r"""
    CommandLine:
        python -m xdoctest.runner
    """
    import xdoctest as xdoc
    xdoc.doctest_module()

# -*- coding: utf-8 -*- Native xdoctest interface to the collecting, executing, and summarizing the results of running doctests. This is an alternative to running through pytest. Executes requestsed google-style doctests in a package or module.     Main entry point into the testing framework.      Args:         modname (str): name of or path to the module.         command (str): determines which doctests to run.             if command is None, this is determined by parsing sys.argv         argv (list): if None uses sys.argv         verbose (bool):  verbosity flag         exclude (list): ignores any modname matching any of these             glob-like patterns         config (dict): modifies each examples configuration      Example:         >>> modname = 'xdoctest.dynamic_analysis'         >>> result = doctest_module(modname, 'list', argv=['']) # Determine package name via caller if not specified # Display help if command is not specified # TODO: command should not be allowed to be the requested doctest name in # case it conflicts with an existing command. This probably requires an API # change to this function. # Parse all valid examples # Set each example mode to native to signal that we are using the # native xdoctest runner instead of the pytest runner # + ' @ ' + str(example.lineno) # Check for zero-arg funcs # format the doctests as normal unit tests # Run the gathered doctest examples # Print final summary info in a style similar to pytest Converts all doctests to unit tests that can exist in a standalone module # Create a unit-testable function for this example Summary report formatting and printing # report errors # report parse-time warnings # report run-time warnings # If there is more than one test being run, print out all the # errors that occured so they are consolidated in a single place. # Print command lines to re-run failed tests # final summary # color text based on worst type of error Find functions in `modpath` args  with no args (so we can automatically     make a dummy docstring). # The only existing args should have defaults # Create a dummy doctest example for a zero-arg function Internal helper, loops over each example, runs it, returns a summary # It is important to raise immediatly within the test to display errors # returned from multiprocessing. Especially in zero-arg mode # TODO: should we write anything when verbose=0? # What happens if we don't re-raise here? # If it is necessary, write a message explaining why # except Exception: #     summary = {'passed': False} #     if verbose == 0: #         sys.stdout.write('F') #         sys.stdout.flush() # and verbose > 0: # Determine command via sys.argv if not specified # Change how docstrs are found # Parse verbosity flag CommandLine:         python -m xdoctest.runner