Source code for utool.util_tests

# -*- coding: utf-8 -*-
"""
Helpers for tests

NOTE:
    DEPRICATE THIS IN FAVOR OF pytest and xdoctest

This module contains a more sane reimplementation of doctest functionality.
(I.E.  asserts work and you don't have to worry about stdout mucking things up)
The code isn't super clean though due to time constriaints.  Many functions
probably belong elsewhere and the parsers need a big cleanup.

TODO:
    * report the line of the doctest in the file when reporting errors as well as
     the relative line

    * restructure so there is a test collection step, a filtering step, and an
      execution step

    * Fix finding tests when running with @profile
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import six
import inspect
import types
import sys
from collections import namedtuple
from os.path import basename
from utool import util_arg
from utool import util_time
from utool import util_inject
from utool import util_dbg
from utool import util_dev
from utool._internal.meta_util_six import get_funcname

print, rrr, profile = util_inject.inject2(__name__)


VERBOSE_TEST = util_arg.get_module_verbosity_flags('test')[0]

DEBUG_SRC = not util_arg.get_argflag('--nodbgsrc')
PRINT_SRC = util_arg.get_argflag(
    ('--printsrc', '--src', '--show-src', '--showsrc'),
    help_='show docstring source when running tests',
)
PRINT_FACE = util_arg.get_argflag(('--printface', '--face'))
BIGFACE = util_arg.get_argflag('--bigface')
SYSEXIT_ON_FAIL = util_arg.get_argflag(
    ('--sysexitonfail', '--fastfail'),
    help_='Force testing harness to exit on first test failure',
)
VERBOSE_TIMER = not util_arg.get_argflag('--no-time-tests')
INDENT_TEST = False

ModuleDoctestTup = namedtuple(
    'ModuleDoctestTup', ('enabled_testtup_list', 'frame_fpath', 'all_testflags', 'module')
)


[docs]class TestTuple(util_dev.NiceRepr): """ Simple container for test objects to replace old tuple format exec mode specifies if the test is being run as a script """ def __init__( self, name, num, src, want, flag, tags=None, frame_fpath=None, mode=None, nametup=None, test_namespace=None, shortname=None, total=None, ): self._name = name # function / class / testable name self.num = num # doctest index self.src = src # doctest src self.want = want # doctest required result (optional) self.flag = flag # doctest commandline flags self.frame_fpath = frame_fpath # parent file fpath self.mode = mode # flags if running as script self.nametup = nametup self.total = total self.test_namespace = test_namespace self.shortname = shortname if tags is None and src is not None: # hack to parse tag from source tagline = src.split('\n')[0].strip() if tagline.startswith('#'): tagline = tagline[1:].strip() tagline = tagline.replace('_DOCTEST', '') tags = tagline.split(',') self.tags = tags @property def name(self): # Hack for namespaced name return self.nametup[0] @property def full_name(self): return self.modname + '.' + self.name @property def modname(self): import utool as ut return ut.get_modname_from_modpath(self.frame_fpath) @property def namespace_levels(self): if self.test_namespace: return [self.modename, self.test_namespace] else: return [self.modename] @property def namespace(self): return '.'.join(self.namespace_levels) @property def exec_mode(self): return self.mode == 'exec' def __nice__(self): tagstr = ' ' + ','.join(self.tags) if self.tags is not None else '' return ( ' ' + self.name + ':' + str(self.num) + ' /' + str(self.total) + ' ' + tagstr + ' in ' + self.modname )
HAPPY_FACE_BIG = r''' .-""""""-. .' '. / O O \ : : | | ' , ,' : \ '-......-' / '. .' '-......-' ''' SAD_FACE_BIG = r''' .-""""""-. .' '. / O O \ : ` : | | : .------. : \ ' ' / '. .' '-......-' ''' HAPPY_FACE_SMALL = r''' .""". | o o | | \_/ | ' = ' ''' SAD_FACE_SMALL = r''' .""". | . . | | ~ | ' = ' ''' if BIGFACE: HAPPY_FACE = HAPPY_FACE_BIG SAD_FACE = SAD_FACE_BIG else: HAPPY_FACE = HAPPY_FACE_SMALL # SAD_FACE = SAD_FACE_BIG SAD_FACE = SAD_FACE_SMALL
[docs]def get_package_testables(module=None, **tagkw): r""" New command that should eventually be used intead of old stuff? Args: module_list (list): (default = None) test_flags (None): (default = None) CommandLine: python -m utool get_package_testables --show --mod wbia python -m utool get_package_testables --show --mod utool --tags SCRIPT python -m utool get_package_testables --show --mod utool --tags ENABLE Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> has_any = ut.get_argval('--tags', type_=str, default=None) >>> module = ut.get_argval('--mod', default='utool') >>> test_tuples = get_package_testables(module, has_any=has_any) >>> result = ut.repr3(test_tuples) >>> print(result) >>> #print(ut.repr3(ut.list_getattr(test_tuples, 'tags'))) """ import utool as ut if isinstance(module, six.string_types): module = ut.import_modname(module) modname_list = ut.package_contents(module, ignore_prefix=[], ignore_suffix=[]) module_list = [] for modname in modname_list: try: module_list.append(ut.import_modname(modname)) except Exception: pass test_tuples = [] for module in module_list: old_testables = ut.get_module_doctest_tup( module=module, needs_enable=False, allexamples=True, verbose=False ) test_tuples.extend(old_testables.enabled_testtup_list) if tagkw: tags_list = ut.list_getattr(test_tuples, 'tags') flags = ut.filterflags_general_tags(tags_list, **tagkw) test_tuples = ut.compress(test_tuples, flags) return test_tuples
[docs]def doctest_module_list(module_list): """ Runs many module tests Entry point for batch run Depth 0) Ignore: :'<,'>!sort -n -k 2 """ import utool as ut nPass_list = [] nTotal_list = [] failed_cmds_list = [] error_reports_list = [] print('[util_test] Running doctests on module list') try: ut.write_to('timeings.txt', '\n\n --- begining doctest_module_list\n', mode='a') except IOError as ex: ut.printex(ex, '[util_test] IOWarning', iswarning=True) failed_doctest_fname = 'failed_doctests.txt' seen_ = set([]) with open(failed_doctest_fname, 'a') as file_: file_.write('\n-------\n\n') file_.write(ut.get_timestamp(format_='printable') + '\n') file_.write( 'logfile (only present if logging) = %r\n' % (ut.util_logging.get_current_log_fpath(),) ) testkw = dict(allexamples=True) with ut.Timer(verbose=False) as t: for module in module_list: (nPass, nTotal, failed_list, error_report_list) = ut.doctest_funcs( module=module, seen_=seen_, **testkw ) nPass_list.append(nPass) nTotal_list.append(nTotal) failed_cmds_list.append(failed_list) error_reports_list.append(error_report_list) # Write failed tests to disk for cmd in failed_list: file_.write(cmd + '\n') total_time = t.ellapsed nPass = sum(nPass_list) nTotal = sum(nTotal_list) file_.write('PASSED %d / %d' % (nPass, nTotal)) failed_cmd_list = ut.flatten(failed_cmds_list) error_report_list = ut.filter_Nones(ut.flatten(error_reports_list)) if len(error_report_list) > 0: print('\nPrinting %d error reports' % (len(error_report_list),)) for count, error_report in enumerate(error_report_list): print('\n=== Error Report %d / %d' % (count, len(error_report_list))) print(error_report) print('--- Done printing error reports ----') try: ut.write_to( 'timeings.txt', '\n\n --- finished doctest_module_list total_time=%.3fs\n' % (total_time), mode='a', ) except IOError as ex: ut.printex(ex, '[util_test] IOWarning', iswarning=True) print('') print('+========') print('| FINISHED TESTING %d MODULES' % (len(module_list),)) print('| PASSED %d / %d' % (nPass, nTotal)) print('L========') if len(failed_cmd_list) > 0: print('FAILED TESTS:') print('\n'.join(failed_cmd_list)) return nPass, nTotal, failed_cmd_list
[docs]def doctest_funcs( testable_list=None, check_flags=True, module=None, allexamples=None, needs_enable=None, strict=False, verbose=True, return_error_report=True, seen_=None, ): r""" Main entry point into utools main module doctest harness Imports a module and checks flags for the function to run Depth 1) Args: testable_list (list): check_flags (bool): Force checking of the --test- and --exec- flags module (None): allexamples (None): needs_enable (None): Returns: tuple: (nPass, nTotal, failed_cmd_list) CommandLine: python -m wbia.algo.preproc.preproc_chip --all-examples References: http://legacy.python.org/dev/peps/pep-0338/ https://docs.python.org/2/library/runpy.html Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA >>> testable_list = [] >>> check_flags = True >>> module = None >>> allexamples = None >>> needs_enable = None >>> # careful might infinitely recurse >>> (nPass, nTotal) = doctest_funcs(testable_list, check_flags, module, ... allexamples, needs_enable) >>> print((nPass, nTotal)) """ import multiprocessing import utool as ut # NOQA # ut.start_logging() multiprocessing.freeze_support() # just in case if ut.VERBOSE: print('[util_test] doctest_funcs') ut.inject_colored_exceptions() if (verbose or VERBOSE_TEST) and ut.NOT_QUIET: if VERBOSE_TEST: print('[util_test.doctest_funcs][DEPTH 1] doctest_funcs()') print('[util_test.doctest_funcs] Running doctest_funcs') if ut.is_developer(): ut.change_term_title('DocTest ' + ' '.join(sys.argv)) # PARSE OUT TESTABLE DOCTESTTUPS mod_doctest_tup = get_module_doctest_tup( testable_list, check_flags, module, allexamples, needs_enable, N=1, verbose=verbose, ) enabled_testtup_list, frame_fpath, all_testflags, module = mod_doctest_tup nPass = 0 nFail = 0 nTotal = len(enabled_testtup_list) # flags = [(tup.name, tup.num) in seen_ for tup in enabled_testtup_list] if seen_ is not None: flags = [tup.src not in seen_ for tup in enabled_testtup_list] enabled_testtup_list = ut.compress(enabled_testtup_list, flags) # Remove duplicate tests from previous parts of the batch run # print(sum(flags)) EARLYEXIT = False if seen_ is not None: for tup in enabled_testtup_list: # seen_.add((tup.name, tup.num)) seen_.add(tup.src) if EARLYEXIT: nPass = nTotal - sum(flags) if return_error_report: return (nPass, nTotal, [], []) else: return (nPass, nTotal, []) modname = ut.get_modname_from_modpath(frame_fpath) nTotal = len(enabled_testtup_list) # Run enabled examles failed_flag_list = [] error_report_list = [] if ut.get_argflag(('--edit-test-file', '--etf')): ut.editfile(frame_fpath) exec_mode = all([testtup.exec_mode for testtup in enabled_testtup_list]) for testtup in enabled_testtup_list: name = testtup.name num = testtup.num src = testtup.src want = testtup.want flag = testtup.flag # if ut.is_developer(): # ut.change_term_title('DocTest ' + modname + ' ' + name) print('\n') fmtdict = dict(modname=modname, name=name, num=num) # 1 v12 v20 v30 v40 v50 v62 print('+------------------------------------------------------------+') print('* DOCTEST {modname:<20} {name:>26}:{num:d} '.format(**fmtdict)) print('+------------------------------------------------------------+') if PRINT_SRC or VERBOSE_TEST: if ut.is_developer(): print(ut.msgblock('EXEC SRC', ut.highlight_code(src), side='>>>')) else: print(ut.msgblock('EXEC SRC', src, side='>>>')) # Commented because it caused differences between # individual test runs and large test runs with ut # being imported # test_globals = module.__dict__.copy() test_globals = {} error_report = None try: testkw = dict(globals=test_globals, want=want) # HACK assert testtup.frame_fpath == frame_fpath test_locals, error_report = ut.run_test(testtup, **testkw) pass_flag = test_locals is not False if pass_flag: if VERBOSE_TEST: print('seems to pass') nPass += 1 else: if VERBOSE_TEST: print('raising failed exception') raise Exception('failed') except Exception: if VERBOSE_TEST: print('Seems to fail. ') nFail += 1 failed_flag_list.append(flag) error_report_list.append(error_report) if strict or util_arg.SUPER_STRICT: raise else: if VERBOSE_TEST: print( 'Silently Failing: ' 'maybe adding the --super-strict flag would help debug?' ) pass except KeyboardInterrupt: print('[util_test] caught Ctrl+C') print('L_____________________________________________________________') # L__________________ # +------------------- # Print Results if nTotal == 0 and not allexamples: valid_test_argflags = ['--allexamples'] + all_testflags modname = ut.get_modname_from_modpath(frame_fpath) # TODO: ensure that exename is in the PATH exename = basename(sys.executable) warning_msg = ( ut.codeblock( r""" [ut.doctest_funcs] No test flags specified Please choose one of the following flags or specify --enableall Valid test argflags: """ ) + ut.indentjoin(valid_test_argflags, '\n %s -m %s ' % (exename, modname)) ) # warning_msg = ut.indent(warning_msg, '[util_test.doctest_funcs]') ut.colorprint(warning_msg, 'yellow') if not exec_mode: print('+-------') print('| finished testing fpath=%r' % (frame_fpath,)) print('| passed %d / %d' % (nPass, nTotal)) print('L-------') failed_cmd_list = [] if nFail > 0: # modname = module.__name__ modname = ut.get_modname_from_modpath(frame_fpath) # TODO: ensure that exename is in the PATH exename = basename(sys.executable) failed_cmd_list = [ '%s -m %s %s' % (exename, modname, flag_) for flag_ in failed_flag_list ] # failed_cmd_list = ['python %s %s' % (frame_fpath, flag_) # for flag_ in failed_flag_list] print('Failed sys.argv = %r' % (' '.join(sys.argv),)) print('Failed Tests:') print('\n'.join(failed_cmd_list)) # L__________________ if ut.util_inject.PROFILING: ut.dump_profile_text() if return_error_report: return (nPass, nTotal, failed_cmd_list, error_report_list) else: return (nPass, nTotal, failed_cmd_list)
[docs]def run_test(func_or_testtup, *args, **kwargs): r""" Runs the test function. Prints a success / failure report. Args: func_or_testtup (func or tuple): function or doctest tuple args*: args to be forwarded to `func_or_testtup` kwargs*: keyword args to be forwarded to `func_or_testtup` """ import utool as ut # func_is_testtup = isinstance(func_or_testtup, tuple) # NOTE: isinstance might not work here if ut.rrrr has been called func_is_testtup = isinstance(func_or_testtup, TestTuple) exec_mode = False dump_mode = False write_times = True if func_is_testtup: testtup = func_or_testtup src = testtup.src funcname = testtup.name frame_fpath = testtup.frame_fpath # (funcname, src, frame_fpath) = func_or_testtup exec_mode = testtup.exec_mode dump_mode = testtup.mode == 'dump' else: func_ = func_or_testtup funcname = get_funcname(func_) frame_fpath = ut.get_funcfpath(func_) upper_funcname = funcname.upper() if ut.VERBOSE: print('\n=============================') print('**[TEST.BEGIN] %s ' % (sys.executable)) print('**[TEST.BEGIN] %s ' % (funcname,)) verbose_timer = not exec_mode and VERBOSE_TIMER nocheckwant = True if exec_mode else None print_face = not exec_mode and PRINT_FACE error_report = None if dump_mode: print('testtup = %r' % (testtup,)) print(ut.highlight_code(src)) return None, None try: # RUN THE TEST WITH A TIMER with util_time.Timer(upper_funcname, verbose=verbose_timer) as timer: if func_is_testtup: test_locals = _exec_doctest(src, kwargs, nocheckwant) else: # TEST INPUT IS A LIVE PYTHON FUNCTION test_locals = func_(*args, **kwargs) except Exception as ex: import utool as ut # Get locals in the wrapped function ut.printex(ex, tb=True) error_report_lines = [ '**[TEST.ERROR] %s -- FAILED:\n type(ex)=%s' % (funcname, type(ex)) ] error_report_lines.append(ut.formatex(ex, tb=True)) def print_report(msg): error_report_lines.append(msg) print(msg) print_report('\n=============================') print_report( '**[TEST.FINISH] %s -- FAILED:\n type(ex)=%s' % (funcname, type(ex)) ) exc_type, exc_value, tb = sys.exc_info() if PRINT_FACE: print_report(SAD_FACE) if func_is_testtup: print_report('Failed in module: %r' % frame_fpath) src_with_lineno = ut.number_text_lines(src) print_report( ut.msgblock('FAILED DOCTEST IN %s' % (funcname,), src_with_lineno) ) if util_arg.SUPER_STRICT: exc_type, exc_value, exc_traceback = sys.exc_info() if not func_is_testtup: # Remove this function from stack strace # dont do this for execed code exc_traceback = exc_traceback.tb_next pass # FIXME: make a _internal/py2_syntax_funcs version of this # function as well as a python3 syntax version due to the # reraise syntax issue to avoid an extra frame in the stack six.reraise(exc_type, exc_value, exc_traceback) if SYSEXIT_ON_FAIL: print('[util_test] SYSEXIT_ON_FAIL = True') print('[util_test] exiting with sys.exit(1)') sys.exit(ut.EXIT_FAILURE) # raise error_report = '\n'.join(error_report_lines) return False, error_report else: # LOG PASSING TEST if not exec_mode: print('\n=============================') print('**[TEST.FINISH] %s -- SUCCESS' % (funcname,)) if print_face: print(HAPPY_FACE) if write_times: timemsg = '%.4fs in %s %s\n' % (timer.ellapsed, funcname, frame_fpath) try: ut.write_to('timeings.txt', timemsg, mode='a') except IOError as ex: ut.printex(ex, '[util_test] IOWarning', iswarning=True) # RETURN VALID TEST LOCALS return test_locals, error_report
def _exec_doctest(src, kwargs, nocheckwant=None): """ Helper for run_test block of code that runs doctest and was too big to be in run_test """ # TEST INPUT IS PYTHON CODE TEXT # test_locals = {} test_globals = kwargs.get('globals', {}).copy() want = kwargs.get('want', None) # test_globals['print'] = doctest_print # EXEC FUNC # six.exec_(src, test_globals, test_locals) # adds stack to debug trace import utool as ut # TODO RECTIFY WITH TF if ut.get_argflag(('--cmd', '--embed')): src = _cmd_modify_src(src) code = compile(src, '<string>', 'exec') try: # IN EXEC CONTEXT THERE IS NO DIFF BETWEEN LOCAL / GLOBALS. ONLY PASS # IN ONE DICT. OTHERWISE TREATED ODDLY # References: https://bugs.python.org/issue13557 # exec(code, test_globals, test_locals) test_locals = test_globals exec(code, test_globals) except ExitTestException: print('Test exited before show') pass if nocheckwant is None: nocheckwant = util_arg.get_argflag( '--no-checkwant', help_='Turns off checking for results' ) if nocheckwant or want is None or want == '': if not nocheckwant: print('warning test does not want anything') else: if want.endswith('\n'): want = want[:-1] result = six.text_type(test_locals.get('result', 'NO VARIABLE NAMED result')) if result != want: from utool import util_str msglines = [] # Even if they arent exactly the same, the difference might just be # some whitespace characters. Ignore in this case. difftext = util_str.get_textdiff(want, result, ignore_whitespace=True) if difftext: if util_dbg.COLORED_EXCEPTIONS: difftext = ut.color_diff_text(difftext) msglines += [ 'DIFF/GOT/EXPECTED', difftext, ] if VERBOSE_TEST: msglines += [ 'REPR_GOT: result=', repr(result), 'REPR_EXPECTED: want=', repr(want), ] msglines += [ 'STR_GOT: result=', str(result), 'STR_EXPECTED: want=', str(want), ] errmsg1 = '\n'.join(msglines) raise AssertionError('result != want\n' + errmsg1) return test_locals
[docs]def get_module_testlines(module_list, remove_pyc=True, verbose=True, pythoncmd=None): """ Builds test commands for autogen tests called by autogen test scripts """ import utool as ut # NOQA if pythoncmd is None: pythoncmd = sys.executable #'python' testcmd_list = [] for module in module_list: mod_doctest_tup = get_module_doctest_tup( module=module, allexamples=True, verbose=verbose ) (enabled_testtup_list, frame_fpath, all_testflags, module_) = mod_doctest_tup for testtup in enabled_testtup_list: # testflag = testtup[-1] testflag = testtup.flag if remove_pyc: # FIXME python 3 __pycache__/*.pyc frame_fpath = frame_fpath.replace('.pyc', '.py') frame_rel_fpath = ut.get_relative_modpath(frame_fpath) testcmd = ' '.join((pythoncmd, frame_rel_fpath, testflag)) testcmd_list.append(testcmd) return testcmd_list
def _docblock_tester1(): """testting docblocks foo # Depth 5) call: foo the next call is: bar called by parse_doctest_from_docstr TODO: move to util_inspect test what happens with weird indentation bla Args: docstr (str): a documentation string formatted in google style. Remember indentation matters. whoo this .. params new (Optional[bool]): does a new style of parsing **kwargs: Returns: list: docstr_blocks tuples [(blockname, blockstr, offset)] Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> print('example1') Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> print('dummy') """ def _docblock_tester2(): """ Args: Returns: Example: Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA """ def _docblock_tester3(): """ foo Args: Returns: Example: Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA """ def _docblock_tester4(): """foobar Args: *args: **kwargs: Returns: bool Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA """ def _test_docblock_parser(): import utool as ut basis = { 'n_args': [None, 0, 1], 'n_return': [None, 0, 1, 2, 3], 'spacing': [1, 2], # 'example_lines': [0, 1, 2], 'doc_lines': [0, 1, 2], 'doc_indent': [0, 1, 4], } for config in ut.all_dict_combinations(basis): print('---') print(config) print('=====') docstr = _make_test_docstr(config) docparts = ut.parse_docblocks_from_docstr(docstr) print(docstr) print('=====') if config['n_args']: assert 'Args:' in ut.take_column(docparts, 0) if config['n_return']: assert 'Returns:' in ut.take_column(docparts, 0) def _make_test_docstr(config): import utool as ut blocks = [] docpart = '\n'.join(ut.lorium_ipsum().strip().split('\n')[0 : config['doc_lines']]) docpart = (' ' * config['doc_indent']) + docpart blocks.append(docpart) if config['n_args'] is not None: argheader = 'Args' argname_list = ut.chr_range(config['n_args'], base='a') argtype_list = ['bool'] * config['n_args'] argdesc_list = [''] * config['n_args'] arg_docstr = ut.make_args_docstr( argname_list, argtype_list, argdesc_list, ismethod=False ) argsblock = ut.make_docstr_block(argheader, arg_docstr) else: argsblock = '' if config['n_return'] is not None: if config['n_return'] == 0: returnblock = 'Returns:' else: return_name = 'outvar' return_type = 'int' return_desc = '\n'.join( ut.lorium_ipsum().strip().split('\n')[0 : config['n_return']] ) return_doctr = ut.make_returns_or_yeilds_docstr( return_type, return_name, return_desc ) returnblock = ut.make_docstr_block('Returns', return_doctr) blocks.append(returnblock) blocks.append(argsblock) docstr = ('\n' * config['spacing']).join(blocks) return docstr
[docs]def parse_docblocks_from_docstr(docstr, offsets=False): """ # Depth 5) called by parse_doctest_from_docstr TODO: move to util_inspect Args: docstr (str): a documentation string formatted in google style. Returns: list: docstr_blocks tuples [(blockname, blockstr)] CommandLine: python -m utool.util_tests parse_docblocks_from_docstr Doctest: >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> #func_or_class = ut.flatten2 >>> #func_or_class = ut.list_depth >>> func_or_class = ut.iter_module_doctestable >>> func_or_class = ut.all_multi_paths >>> docstr = ut.get_docstr(func_or_class) >>> docstr_blocks = parse_docblocks_from_docstr(docstr) >>> result = str(ut.repr4(docstr_blocks)) >>> print(result) """ if docstr is None: return [] # import parse import utool as ut import re # import itertools as it docstr = ut.ensure_unicode(docstr) docstr = ut.unindent(docstr) # Parse out initial documentation lines # Then parse out the blocked lines. docstr_lines = docstr.split('\n') line_indent = [ut.get_indentation(line) for line in docstr_lines] line_len = [len(line) for line in docstr_lines] # The first line may not have the correct indentation if it starts # right after the triple quotes. Adjust it in this case to ensure that # base indent is always 0 adjusted = False is_nonzero = [len_ > 0 for len_ in line_len] if len(line_indent) >= 2: if line_len[0] != 0: indents = ut.compress(line_indent, is_nonzero) if len(indents) >= 2: indent_adjust = min(indents[1:]) line_indent[0] += indent_adjust line_len[0] += indent_adjust docstr_lines[0] = (' ' * indent_adjust) + docstr_lines[0] adjusted = True if adjusted: # Redo prepreocessing, but this time on a rectified input docstr = ut.unindent('\n'.join(docstr_lines)) docstr_lines = docstr.split('\n') line_indent = [ut.get_indentation(line) for line in docstr_lines] line_len = [len(line) for line in docstr_lines] indents = ut.compress(line_indent, is_nonzero) if len(indents) >= 1: if indents[0] != 0: print('ERROR IN PARSING') print('adjusted = %r' % (adjusted,)) print(docstr) raise ValueError('Google Style Docstring Missformat') base_indent = 0 # We will group lines by their indentation. # Rectify empty lines by giving them their parent's indentation. true_indent = [] prev_indent = None for indent_, len_ in zip(line_indent, line_len): if len_ == 0: # Empty lines take on their parents indentation indent_ = prev_indent true_indent.append(indent_) prev_indent = indent_ # tag_pattern = ut.regex_or(['Args:', 'Return:', 'CommandLine':]) tag_pattern = '[^\s]+: *$' group_id = 0 prev_indent = 0 group_list = [] in_tag = False for line_num, (line, indent_) in enumerate(zip(docstr_lines, true_indent)): if re.match(tag_pattern, line): # Check if we can look ahead if line_num + 1 < len(docstr_lines): # A tag is only valid if its next line is properly indented, # empty, or is a tag itself. if ( true_indent[line_num + 1] > base_indent or line_len[line_num + 1] == 0 or re.match(tag_pattern, docstr_lines[line_num + 1]) ): group_id += 1 in_tag = True else: group_id += 1 in_tag = True # Check if this line belongs to a new group elif in_tag and indent_ != prev_indent and indent_ == base_indent: group_id += 1 in_tag = False group_list.append(group_id) prev_indent = indent_ groups_ = ut.group_items(docstr_lines, group_list) groups = [] line_offset = 0 for k, lines in groups_.items(): if len(lines) == 0 or (len(lines) == 1 and len(lines[0]) == 0): continue elif len(lines) >= 1 and re.match(tag_pattern, lines[0]): # An encoded google sub-block key = lines[0] val = lines[1:] subblock = ut.unindent('\n'.join(val)) else: # A top level text documentation block key = '__DOC__' val = lines[:] subblock = '\n'.join(val) if offsets: groups.append((key, subblock, line_offset)) else: groups.append((key, subblock)) line_offset += len(lines) # Ensure that no keys are duplicated # try: # assert len(ut.find_duplicate_items(ut.take_column(groups, 0))) == 0, ( # 'Duplicate google docblock keys are not allowed') # except Exception as ex: # ut.printex(ex, iswarning=True) return groups
[docs]def read_exampleblock(docblock): # if False: # no longer needed with new parser # import utool as ut # nonheader_src = ut.unindent('\n'.join(docblock.splitlines()[1:])) nonheader_src = docblock nonheader_lines = nonheader_src.splitlines() reversed_src_lines = [] reversed_want_lines = [] finished_want = False # Read the example block backwards to get the want string # and then the rest should all be source for line in reversed(nonheader_lines): if not finished_want: if line.startswith('>>> ') or line.startswith('... '): finished_want = True else: reversed_want_lines.append(line) if len(line.strip()) == 0: reversed_want_lines = [] continue reversed_src_lines.append(line[4:]) test_src = '\n'.join(reversed_src_lines[::-1]) test_want = '\n'.join(reversed_want_lines[::-1]) return test_src, test_want
[docs]def parse_doctest_from_docstr(docstr): r""" because doctest itself doesnt do what I want it to do CAREFUL, IF YOU GET BACK WRONG RESULTS MAKE SURE YOUR DOCSTR IS PREFFIXED WITH R CommandLine: python -m utool.util_tests --exec-parse_doctest_from_docstr Doctest: >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> func_or_class = parse_doctest_from_docstr >>> func_or_class = ut.list_depth >>> #func_or_class = ut.util_depricated.cartesian >>> func_or_class = ut.iter_module_doctestable >>> docstr = ut.get_docstr(func_or_class) >>> testsrc_list, testwant_list, testlinenum_list, func_lineno, docstr = get_doctest_examples(func_or_class) >>> print('\n\n'.join(testsrc_list)) >>> assert len(testsrc_list) == len(testwant_list) """ import utool as ut docstr_blocks = parse_docblocks_from_docstr(docstr, offsets=True) # print('docstr_blocks = %r' % (docstr_blocks,)) example_docblocks = [] example_setups = [] grid_example_docblock = [] grid_setups = [] param_grids = None for header, docblock, line_offset in docstr_blocks: if header.startswith('Example') or header.startswith('Doctest'): example_docblocks.append((header, docblock, line_offset)) if header.startswith('Setup'): setup_src = read_exampleblock(docblock)[0] example_setups.append(setup_src) if header.startswith('GridParams'): paramgrid_src = read_exampleblock(docblock)[0] globals_ = {} six.exec_('import utool as ut\n' + paramgrid_src, globals_) assert 'combos' in globals_, 'param grid must define combos' combos = globals_['combos'] param_grids = [ut.execstr_dict(combo, explicit=True) for combo in combos] if header.startswith('GridExample'): grid_example_docblock.append((header, docblock, line_offset)) if header.startswith('GridSetup'): setup_src = read_exampleblock(docblock)[0] grid_setups.append(setup_src) assert len(example_setups) <= 1, 'cant have more than 1 setup. %d' % ( len(example_setups) ) if example_setups and not grid_setups: grid_setups = example_setups testheader_list = [] testsrc_list = [] testwant_list = [] testlineoffset_list = [] # Place grid tests first for header, docblock, line_offset in grid_example_docblock: test_src, test_want = read_exampleblock(docblock) assert len(grid_setups) <= 1, 'need one grid setup' if len(grid_setups): grid_setup = grid_setups[0] else: grid_setup = '' hack_show_request = False if 'ut.show_if_requested()' in test_src: hack_show_request = True test_src = test_src.replace('ut.show_if_requested()', '') # Megahack full_grid_testsrc = '\n'.join( [grid_setup] + [ '\n'.join( [ pgrid, "print('Grid %d')" % (count,), test_src if count < (len(param_grids) - 1) else test_src, ] ) for count, pgrid in enumerate(param_grids) ] ) if hack_show_request: full_grid_testsrc += '\n' + 'ut.show_if_requested()' testsrc_list.append(full_grid_testsrc) testheader_list.append(header) testwant_list.append(test_want) testlineoffset_list.append(line_offset) for header, docblock, line_offset in example_docblocks: test_src, test_want = read_exampleblock(docblock) if header.startswith('Doctest'): # Enable anything labeled as a doctest test_src = '# ENABLE_DOCTEST\n' + test_src if len(example_setups) == 0: full_testsrc = test_src elif len(example_setups) == 1: # Hack: append setups to all sources full_testsrc = '\n'.join([example_setups[0], test_src]) else: assert False, 'more than 1 setup' testheader_list.append(header) testsrc_list.append(full_testsrc) testwant_list.append(test_want) testlineoffset_list.append(line_offset) return testheader_list, testsrc_list, testwant_list, testlineoffset_list
# @debug_decor
[docs]def get_doctest_examples(func_or_class, modpath=None): """ get_doctest_examples Depth 3) called by get_module_doctest_tup Args: func_or_class (function) Returns: tuple (list, list): example_list, want_list CommandLine: python -m utool.util_tests --test-get_doctest_examples Doctest: >>> from utool.util_tests import * # NOQA >>> func_or_class = get_doctest_examples >>> tup = get_doctest_examples(func_or_class) >>> testsrc_list, testwant_list, testlinenum_list, func_lineno, docstr = tup >>> result = str(len(testsrc_list) + len(testwant_list)) >>> print(testsrc_list) >>> print(testlinenum_list) >>> print(func_lineno) >>> print(testwant_list) >>> print(result) 6 Doctest: >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> func_or_class = ut.tryimport >>> tup = get_doctest_examples(func_or_class) >>> testsrc_list, testwant_list, testlinenum_list, func_lineno, docstr = tup >>> result = str(len(testsrc_list) + len(testwant_list)) >>> print(testsrc_list) >>> print(testlinenum_list) >>> print(func_lineno) >>> print(testwant_list) >>> print(result) 4 Example2: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA >>> import wbia >>> func_or_class = wbia.control.manual_annot_funcs.add_annots >>> tup = get_doctest_examples(func_or_class) >>> testsrc_list, testwant_list, testlinenum_list, func_lineno, docstr = tup >>> result = str(len(testsrc_list) + len(testwant_list)) >>> print(testsrc_list) >>> print(testlinenum_list) >>> print(func_lineno) >>> print(testwant_list) >>> print(result) 2 """ if isinstance(func_or_class, staticmethod): func_or_class = func_or_class.__func__ import utool as ut if VERBOSE_TEST: print('[util_test][DEPTH 3] get_doctest_examples()') print('[util_test] + parsing %r for doctest' % (func_or_class)) print('[util_test] - name = %r' % (func_or_class.__name__,)) if hasattr(func_or_class, '__ut_parent_class__'): print( '[util_test] - __ut_parent_class__ = %r' % (func_or_class.__ut_parent_class__,) ) try: raise NotImplementedError('FIXME') # func_or_class._utinfo['orig_func'] func_lineno = func_or_class.func_code.co_firstlineno # FIXME: doesn't handle decorators well # # ~~FIXME doesn't account for multiline function definitions # actually parse this out~~ # TODO: rectify with util_insepct get_funcsource with stip def line sourcecode = inspect.getsource(func_or_class) match = ut.regex_get_match('def [^)]*\\):\n', sourcecode) if match is not None: num_funcdef_lines = match.group().count('\n') else: num_funcdef_lines = 1 except Exception as ex: func_lineno = 0 num_funcdef_lines = 1 if ut.DEBUG2: ut.printex(ex, '[util-test] error getting function line number') docstr = ut.get_docstr(func_or_class) ( testheader_list, testsrc_list, testwant_list, testlineoffset_list, ) = parse_doctest_from_docstr(docstr) testlinenum_list = [ func_lineno + num_funcdef_lines + offset for offset in testlineoffset_list ] if VERBOSE_TEST: print('[util_test] L found %d doctests' % (len(testsrc_list),)) examptup = testsrc_list, testwant_list, testlinenum_list, func_lineno, docstr return examptup
[docs]def get_module_doctest_tup( testable_list=None, check_flags=True, module=None, allexamples=None, needs_enable=None, N=0, verbose=True, testslow=False, ): """ Parses module for testable doctesttups Depth 2) Args: testable_list (list): a list of functions (default = None) check_flags (bool): (default = True) module (None): (default = None) allexamples (None): (default = None) needs_enable (None): (default = None) N (int): (default = 0) verbose (bool): verbosity flag(default = True) testslow (bool): (default = False) Returns: ModuleDoctestTup : (enabled_testtup_list, frame_fpath, all_testflags, module) enabled_testtup_list (list): a list of testtup testtup (tuple): (name, num, src, want, flag) describes a valid doctest in the module name (str): test name num (str): test number of the module / function / class / method src (str): test source code want (str): expected test result flag (str): a valid commandline flag to enable this test frame_fpath (str): module fpath that will be tested module (module): the actual module that will be tested all_testflags (list): the command line arguments that will enable different tests exclude_inherited (bool): does not included tests defined in other modules CommandLine: python -m utool.util_tests --exec-get_module_doctest_tup Doctest: >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> #testable_list = [ut.util_import.package_contents] >>> testable_list = None >>> check_flags = False >>> module = ut.util_cplat >>> allexamples = False >>> needs_enable = None >>> N = 0 >>> verbose = True >>> testslow = False >>> mod_doctest_tup = get_module_doctest_tup(testable_list, check_flags, >>> module, allexamples, >>> needs_enable, N, verbose, >>> testslow) >>> result = ('mod_doctest_tup = %s' % (ut.repr4(mod_doctest_tup, nl=4),)) >>> print(result) """ # +------------------------ if VERBOSE_TEST: print('[util_test.get_module_doctest tup][DEPTH 2] get_module_doctest tup()') import utool as ut # NOQA if needs_enable is None: needs_enable = not ut.get_argflag('--enableall') # needs_enable = True TEST_ALL_EXAMPLES = allexamples or ut.get_argflag(('--allexamples', '--all-examples')) parse_testables = True if isinstance(testable_list, types.ModuleType): # hack module = testable_list testable_list = [] testable_name_list = [] elif testable_list is None: testable_list = [] testable_name_list = [] else: testable_name_list = [ut.get_funcname(func) for func in testable_list] parse_testables = False # L________________________ # +------------------------ # GET_MODULE_DOCTEST_TUP Step 1: # Inspect caller module for testable names if module is None: frame_fpath = '???' try: # This is a bit finky. Need to be exactly N frames under the main # module frame = ut.get_parent_frame(N=N) main_modname = '__main__' frame_name = frame.f_globals['__name__'] frame_fpath = frame.f_globals['__file__'] if frame_name == main_modname: module = sys.modules[main_modname] entry_modname = ut.get_modname_from_modpath(module.__file__) if entry_modname in ['kernprof', 'kernprof-script']: # kernprof clobbers the __main__ variable. # workaround by reimporting the module name import importlib modname = ut.get_modname_from_modpath(frame_fpath) module = importlib.import_module(modname) except Exception as ex: print(frame.f_globals) ut.printex(ex, keys=['frame', 'module']) raise allexamples = False else: frame_fpath = module.__file__ allexamples = True # L________________________ # +------------------------ # GET_MODULE_DOCTEST_TUP Step 2: # --- PARSE TESTABLE FUNCTIONS --- # Get testable functions if parse_testables: try: if verbose or VERBOSE_TEST and ut.NOT_QUIET: print('[ut.test] Iterating over module funcs') print('[ut.test] module =%r' % (module,)) _testableiter = ut.iter_module_doctestable(module, include_inherited=False) for key, val in _testableiter: if isinstance(val, staticmethod): docstr = inspect.getdoc(val.__func__) else: docstr = inspect.getdoc(val) docstr = ut.ensure_unicode(docstr) if docstr is not None and ( docstr.find('Example') >= 0 or docstr.find('Doctest') >= 0 ): testable_name_list.append(key) testable_list.append(val) if VERBOSE_TEST and ut.NOT_QUIET: print('[ut.test] Testable: %s' % (key,)) else: if VERBOSE_TEST and ut.NOT_QUIET: if docstr.find('Example') >= 0 or docstr.find('Doctest') >= 0: print('[ut.test] Ignoring (disabled) : %s' % key) else: print('[ut.test] Ignoring (no Example) : %s' % key) except Exception as ex: print('FAILED') print(docstr) ut.printex(ex, keys=['frame']) raise # OUTPUTS: testable_list # L________________________ # +------------------------ # GET_MODULE_DOCTEST_TUP Step 3: # --- FILTER TESTABLES_--- # Get testable function examples test_sentinals = [ 'ENABLE_DOCTEST', 'ENABLE_GRID_DOCTEST', ] if testslow or ut.get_argflag(('--testall', '--testslow', '--test-slow')): test_sentinals.append('SLOW_DOCTEST') if testslow or ut.get_argflag(('--testall', '--testunstable')): test_sentinals.append('UNSTABLE_DOCTEST') # FIND THE TEST NAMES REQUESTED # Grab sys.argv enabled tests cmdline_varargs = ut.get_cmdline_varargs() force_enable_testnames_ = cmdline_varargs[:] valid_prefix_list = ['--test-', '--exec-', '--dump-'] # if False: for arg in sys.argv: for prefix in valid_prefix_list: if arg.startswith(prefix): testname = arg[len(prefix) :] # testname = testname.split(':')[0].replace('-', '_') force_enable_testnames_.append(testname) # break # PartA: Fixup names # TODO: parse out requested test number here # instead of later in the code. See PartB force_enable_testnames = [] for testname in force_enable_testnames_: testname = testname.split(':')[0].replace('-', '_') testname.split(':')[0].replace('-', '_') force_enable_testnames.append(testname) def _get_testable_name(testable): import utool as ut if isinstance(testable, staticmethod): testable = testable.__func__ try: testable_name = testable.func_name except AttributeError as ex1: try: testable_name = testable.__name__ except AttributeError as ex2: ut.printex(ex1, ut.repr4(dir(testable))) ut.printex(ex2, ut.repr4(dir(testable))) raise return testable_name sorted_testable = sorted(list(set(testable_list)), key=_get_testable_name) # Append each testable example if VERBOSE_TEST: print('Vars:') print(' * needs_enable = %r' % (needs_enable,)) print(' * force_enable_testnames = %r' % (force_enable_testnames,)) print(' * len(sorted_testable) = %r' % (len(sorted_testable),)) print(' * cmdline_varargs = %r' % (cmdline_varargs,)) indenter = ut.Indenter('[FIND_AVAIL]') indenter.start() # PARSE OUT THE AVAILABLE TESTS FOR EACH REQUEST local_testtup_list = [] for testable in sorted_testable: short_testname = _get_testable_name(testable) full_testname = None # Namespaced classname (within module) if isinstance(testable, staticmethod): testable = testable.__func__ if hasattr(testable, '__ut_parent_class__'): # HACK for getting classname.funcname test_namespace = testable.__ut_parent_class__.__name__ full_testname = test_namespace + '.' + short_testname else: test_namespace = None full_testname = short_testname nametup = tuple(ut.unique([full_testname, short_testname])) # modpath = ut.get_modpath(module) examptup = get_doctest_examples(testable) examples, wants, linenums, func_lineno, docstr = examptup total_examples = len(examples) if total_examples > 0: for testno, srcwant_tup in enumerate(zip(examples, wants)): src, want = srcwant_tup src_ = ut.regex_replace('from __future__ import.*$', '', src) test_disabled = not any([src_.find(s) >= 0 for s in test_sentinals]) skip = ( needs_enable and test_disabled and ut.isdisjoint(nametup, force_enable_testnames) ) if not skip: if VERBOSE_TEST: print( ' * HACK adding testname=%r to local_testtup_list' % (full_testname,) ) local_testtup = ( nametup, testno, src_, want, test_namespace, short_testname, total_examples, ) local_testtup_list.append(local_testtup) else: if VERBOSE_TEST: # print('force_enable_testnames = %r' % (force_enable_testnames,)) # print('nametup = %r' % (nametup,)) # print('needs_enable = %r' % (needs_enable,)) # print('test_disabled = %r' % (test_disabled,)) print(' * skipping: %r / %r' % (short_testname, full_testname)) else: print( 'WARNING: no examples in %r for testname=%r' % (frame_fpath, full_testname) ) if verbose: print(testable) print(examples) print(wants) print(docstr) if VERBOSE_TEST: print(' --') if VERBOSE_TEST: indenter.stop() # L________________________ # +------------------------ # Get enabled (requested) examples if VERBOSE_TEST: print('\n-----\n') indenter = ut.Indenter('[IS_ENABLED]') indenter.start() print('Finished parsing available doctests.') print('Now we need to find which examples are enabled') print('len(local_testtup_list) = %r' % (len(local_testtup_list),)) print( 'local_testtup_list.T[0:2].T = %s' % ut.repr4(ut.take_column(local_testtup_list, [0, 1])) ) print('sys.argv = %r' % (sys.argv,)) all_testflags = [] enabled_testtup_list = [] distabled_testflags = [] subx = ut.get_argval( '--subx', type_=int, default=None, help_='Only tests the subxth example' ) def make_valid_testnames(name, num, total): return [ name + ':' + str(num), name, name + ':' + str(num - total), # allow negative indices # prefix + name.replace('_', '-') + ':' + str(num), # prefix + name.replace('_', '-') ] def make_valid_test_argflags(prefix, name, num, total): valid_testnames = make_valid_testnames(name, num, total) return [prefix + testname for testname in valid_testnames] def check_if_test_requested(nametup, num, total, valid_prefix_list): # cmdline_varargs if VERBOSE_TEST: print('Checking cmdline for %r %r' % (nametup, num)) valid_argflags = [] # FIXME: PartB # should parse out test number above instead of here # See PartA mode = None veryverb = 0 # First check positional args testflag = None for name in nametup: valid_testnames = make_valid_test_argflags('', name, num, total) if veryverb: print('Checking if positional* %r' % (valid_testnames[0:1],)) print('name = %r' % (name,)) if any([x in cmdline_varargs for x in valid_testnames]): # hack mode = 'exec' testflag = name flag1 = '--exec-' + name + ':' + str(num) if testflag is not None: if veryverb: print('FOUND POSARG') print(' * testflag = %r' % (testflag,)) print(' * num = %r' % (num,)) break # Then check keyword-ish args if mode is None: for prefix, name in reversed(list(ut.iprod(valid_prefix_list, nametup))): valid_argflags = make_valid_test_argflags(prefix, name, num, total) if veryverb: print('Checking for flags*: %r' % (valid_argflags[0],)) flag1 = valid_argflags[0] testflag = ut.get_argflag(valid_argflags) mode = prefix.replace('-', '') if testflag: if veryverb: print('FOUND VARARG') break else: # print('WARNING NO TEST IS ENABLED %r ' % (nametup,)) pass checktup = flag1, mode, name, testflag return checktup for local_testtup in local_testtup_list: (nametup, num, src, want, shortname, test_namespace, total) = local_testtup checktup = check_if_test_requested(nametup, num, total, valid_prefix_list) flag1, mode, name, testflag = checktup testenabled = TEST_ALL_EXAMPLES or not check_flags or testflag if subx is not None and subx != num: continue all_testflags.append(flag1) if testenabled: if VERBOSE_TEST: print('... enabling test') testtup = TestTuple( name, num, src, want, flag1, frame_fpath=frame_fpath, mode=mode, total=total, nametup=nametup, shortname=shortname, test_namespace=test_namespace, ) if VERBOSE_TEST: print('... ' + str(testtup)) enabled_testtup_list.append(testtup) else: if VERBOSE_TEST: print('... disabling test') distabled_testflags.append(flag1) # Attempt to run test without any context # This will only work if the function exist and is self contained if len(force_enable_testnames_) > 0 and len(enabled_testtup_list) == 0: if VERBOSE_TEST: print('Forced test did not have a doctest example') print('Maybe it can be run without any context') import utool as ut # assert len(force_enable_testnames) == 1 test_funcname_ = force_enable_testnames[0] if test_funcname_.find('.') != -1: test_classname, test_funcname = test_funcname_.split('.') class_ = getattr(module, test_classname, None) assert class_ is not None func_ = getattr(class_, test_funcname, None) else: test_funcname = test_funcname_ func_ = getattr(module, test_funcname, None) if VERBOSE_TEST: print('test_funcname = %r' % (test_funcname,)) print('func_ = %r' % (func_,)) if func_ is not None: testno = 0 modname = ut.get_modname_from_modpath(module.__file__) want = None try: if VERBOSE_TEST: print('attempting xdoctest hack') # hack to get classmethods to read their example using # the xdoctest port from xdoctest import docscrape_google from xdoctest import core as xdoc_core from xdoctest import static_analysis as static if func_.__doc__ is None: raise TypeError blocks = docscrape_google.split_google_docblocks(func_.__doc__) example_blocks = [] for type_, block in blocks: if type_.startswith('Example') or type_.startswith('Doctest'): example_blocks.append((type_, block)) if len(example_blocks) == 0: if VERBOSE_TEST: print('xdoctest found no blocks') raise KeyError callname = test_funcname_ hack_testtups = [] for num, (type_, block) in enumerate(example_blocks): # print('modname = %r' % (modname,)) # print('callname = %r' % (callname,)) # print('num = %r' % (num,)) modpath = static.modname_to_modpath(modname) example = xdoc_core.DocTest( modpath=modpath, callname=callname, docsrc=block, num=num ) src = example.format_src(colored=False, want=False, linenos=False) want = '\n'.join(list(example.wants())) testtup = TestTuple( test_funcname_, num, src, want=want, flag='--exec-' + test_funcname_, frame_fpath=frame_fpath, mode='exec', total=len(example_blocks), nametup=[test_funcname_], ) hack_testtups.append(testtup) if VERBOSE_TEST: print('hack_testtups = %r' % (hack_testtups,)) enabled_testtup_list.extend(hack_testtups) # src = '\n'.join([line[4:] for line in src.split('\n')]) except (ImportError, KeyError, TypeError): if VERBOSE_TEST: print('xdoctest hack failed') # varargs = ut.get_cmdline_varargs() varargs = force_enable_testnames[1:] # Create dummy doctest src = ut.codeblock( """ # DUMMY_DOCTEST from {modname} import * # NOQA args = {varargs} result = {test_funcname_}(*args) print(result) """ ).format( modname=modname, test_funcname_=test_funcname_, varargs=repr(varargs) ) testtup = TestTuple( test_funcname_, testno, src, want=want, flag='--exec-' + test_funcname_, frame_fpath=frame_fpath, mode='exec', total=1, nametup=[test_funcname_], ) enabled_testtup_list.append(testtup) else: print('function %r was not found in %r' % (test_funcname_, module)) if VERBOSE_TEST: indenter.stop() if ut.get_argflag('--list'): # HACK: Should probably just return a richer structure print('testable_name_list = %s' % (ut.repr4(testable_name_list),)) mod_doctest_tup = ModuleDoctestTup( enabled_testtup_list, frame_fpath, all_testflags, module ) # L________________________ return mod_doctest_tup
[docs]def doctest_was_requested(): """ lets a __main__ codeblock know that util_test should do its thing """ # FIXME; does not handle positinal doctest requests valid_prefix_list = ['--exec-', '--test-'] return '--tf' in sys.argv or any( [ any([arg.startswith(prefix) for prefix in valid_prefix_list]) for arg in sys.argv ] )
[docs]def find_doctestable_modnames( dpath_list=None, exclude_doctests_fnames=[], exclude_dirs=[], allow_nonpackages=False ): """ Tries to find files with a call to ut.doctest_funcs in the __main__ part Implementation is very hacky. Should find a better heuristic Args: dpath_list (list): list of python package directories exclude_doctests_fnames (list): (default = []) exclude_dirs (list): (default = []) allow_nonpackages (bool): (default = False) Returns: list: list of filepaths CommandLine: python -m utool.util_tests find_doctestable_modnames --show Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA >>> import utool as ut >>> from os.path import dirname >>> dpath_list = [ut.get_module_dir(ut)] >>> exclude_doctests_fnames = [] >>> exclude_dirs = [] >>> allow_nonpackages = False >>> result = find_doctestable_modnames(dpath_list, exclude_doctests_fnames, exclude_dirs, allow_nonpackages) >>> print(result) """ import utool as ut from os.path import dirname, exists, join fpath_list = ut.grep( r'doctest_funcs\(', dpath_list=dpath_list, include_patterns=['*.py'], exclude_dirs=exclude_dirs, recursive=True, )[0] exclude_doctests_fnames = set(exclude_doctests_fnames) def is_not_excluded(fpath): return basename(fpath) not in exclude_doctests_fnames def is_in_package(fpath): return exists(join(dirname(fpath), '__init__.py')) fpath_list = list(filter(is_in_package, fpath_list)) fpath_list = list(filter(is_not_excluded, fpath_list)) doctest_modname_list = list(map(ut.get_modname_from_modpath, fpath_list)) doctest_modname_list = ut.unique(doctest_modname_list) return doctest_modname_list
[docs]def find_untested_modpaths(dpath_list=None, exclude_doctests_fnames=[], exclude_dirs=[]): import utool as ut fpath_list, lines_list, lxs_list = ut.grep( '>>> # ENABLE_DOCTEST', dpath_list=dpath_list, include_patterns=['*.py'], exclude_dirs=exclude_dirs, recursive=True, inverse=True, ) exclude_doctests_fnames = set(list(exclude_doctests_fnames) + ['__init__.py']) def is_not_excluded(fpath): fname = basename(fpath) return (not fname.startswith('_')) and fname not in exclude_doctests_fnames doctest_modpath_list = list(filter(is_not_excluded, fpath_list)) # doctest_modname_list = list(map(ut.get_modname_from_modpath, doctest_modpath_list)) return doctest_modpath_list
[docs]def show_was_requested(): """ returns True if --show is specified on the commandline or you are in IPython (and presumably want some sort of interaction """ try: import wbia.plottool as pt except ImportError: import wbia.plottool as pt return pt.show_was_requested()
# import utool as ut # return ut.get_argflag('--show') or ut.inIPython() try: # Use ExitTestException from xdoctest if possible from xdoctest import ExitTestException except ImportError: try: from xdoctest.core import ExitTestException except ImportError: class ExitTestException(Exception): pass
[docs]def qt4ensure(): try: import wbia.plottool as pt except ImportError: import wbia.plottool as pt pt.qtensure()
[docs]def qtensure(): try: import wbia.plottool as pt except ImportError: import wbia.plottool as pt pt.qtensure()
[docs]def quit_if_noshow(): import utool as ut saverequest = ut.get_argval('--save', default=None) if not (saverequest or ut.get_argflag(('--show', '--save')) or ut.inIPython()): raise ExitTestException('This should be caught gracefully by ut.run_test')
[docs]def show_if_requested(): try: import wbia.plottool as pt except ImportError: import wbia.plottool as pt pt.show_if_requested(N=2)
[docs]def find_testfunc( module, test_funcname, ignore_prefix=[], ignore_suffix=[], func_to_module_dict={}, return_mod=False, ): import utool as ut if isinstance(module, six.string_types): module = ut.import_modname(module) modname_list = ut.package_contents( module, ignore_prefix=ignore_prefix, ignore_suffix=ignore_suffix ) # Get only the modules already imported have_modnames = [modname_ for modname_ in modname_list if modname_ in sys.modules] # missing_modnames = [modname for modname in modname_list # if modname not in sys.modules] module_list = ut.dict_take(sys.modules, have_modnames) # Search for the module containing the function test_func = None test_module = None test_classname = None if test_funcname.find('.') != -1: test_classname, test_funcname = test_funcname.split('.') if test_funcname.find(':') != -1: test_funcname, testno = test_funcname.split(':') testno = int(testno) else: testno = 0 if test_classname is None: for module_ in module_list: # test_funcname = 'find_installed_tomcat' if test_funcname in module_.__dict__: test_module = module_ test_func = test_module.__dict__[test_funcname] break else: for module_ in module_list: # test_funcname = 'find_installed_tomcat' if test_classname in module_.__dict__: test_module = module_ test_class = test_module.__dict__[test_classname] test_func = getattr(test_class, test_funcname) # test_class.__dict__[test_funcname] if test_func is None: print('Did not find any function named %r ' % (test_funcname,)) print('Searched ' + ut.repr4([mod.__name__ for mod in module_list])) if return_mod: return test_func, testno, test_module else: return test_func, testno
[docs]def get_module_completions(module): import utool as ut test_tuples = ut.get_package_testables(module) testnames = ut.instancelist(test_tuples).name return testnames
# def autocomplete_hook(module): # """ # # https://argcomplete.readthedocs.io/en/latest/#activating-global-completion%20argcomplete # pip install argcomplete # Need to put # PYTHON_ARGCOMPLETE_OK at begining of file # pip install argcomplete # activate-global-python-argcomplete # eval "$(register-python-argcomplete your_script)" # register-python-argcomplete wbia # eval "$(register-python-argcomplete wbia)" # """ # #if len(sys.argv) < 3: # # sys.exit(1) # try: # # hook # import argparse as ap # import argcomplete # except ImportError: # pass # else: # parser = ap.ArgumentParser() # testnames = get_module_completions(module) # testnames = ['foo', 'foo2'] # parser.add_argument('position1', choices=[testnames]) # argcomplete.autocomplete(parser) # args = parser.parse_args()
[docs]def main_function_tester( module, ignore_prefix=[], ignore_suffix=[], test_funcname=None, func_to_module_dict={} ): """ Allows a shorthand for __main__ packages of modules to run tests with unique function names """ import utool as ut ut.colorprint('[utool] main_function_tester', 'yellow') if ut.get_argflag('--list-testfuncs'): print('Listing testfuncs') test_tuples = ut.get_package_testables(module) result = ut.repr3(test_tuples) print(result) # autocomplete_hook(module) if ut.get_argflag('--update-bashcomplete'): # http://stackoverflow.com/questions/427472/line-completion-with-custom-commands print('Listing testfuncs') testnames = get_module_completions(module) modname = module if isinstance(module, six.string_types) else module.__name__ line = 'complete -W "%s" "%s"' % (' '.join(testnames), modname) bash_completer = ut.unixjoin( ut.ensure_app_resource_dir('wbia'), 'wbia_bash_complete.sh' ) ut.writeto(bash_completer, line) print('ADD TO BASHRC\nsource %s' % (bash_completer,)) # print(line) sys.exit(ut.EXIT_SUCCESS) if ut.get_argflag('--make-bashcomplete'): # http://stackoverflow.com/questions/427472/line-completion-with-custom-commands print('Listing testfuncs') testnames = get_module_completions(module) modname = module if isinstance(module, six.string_types) else module.__name__ line = 'complete -W "%s" "%s"' % (' '.join(testnames), modname) print('add the following line to your bashrc') print(line) sys.exit(ut.EXIT_SUCCESS) test_funcname = ut.get_argval( ('--test-func', '--tfunc', '--tf', '--testfunc'), type_=str, default=test_funcname, help_='specify a function to doctest', ) if test_funcname is None: cmdline_varags = ut.get_cmdline_varargs() if VERBOSE_TEST: print('Checking varargs') print('cmdline_varags = %r' % (cmdline_varags,)) if len(cmdline_varags) > 0: test_funcname = cmdline_varags[0] print('test_funcname = %r' % (test_funcname,)) if test_funcname in func_to_module_dict: modname = func_to_module_dict[test_funcname] ut.import_modname(modname) if test_funcname is not None: # locals_ = {} ut.inject_colored_exceptions() # print('[utool] __main__ Begin Function Test') print('[utool] __main__ Begin Function Test') test_func, testno, test_mod = find_testfunc( module, test_funcname, ignore_prefix, ignore_suffix, func_to_module_dict, return_mod=True, ) if test_func is not None: globals_ = {} try: func_globals = ut.get_funcglobals(test_func) globals_.update(func_globals) except AttributeError: pass tests = ut.get_doctest_examples(test_func)[0] if len(tests) > 0: testsrc = tests[testno] else: # Create dummy doctest modname = ut.get_modname_from_modpath(ut.get_modpath(test_mod)) varargs = cmdline_varags[1:] testsrc = ut.codeblock( """ # DUMMY_DOCTEST from {modname} import * # NOQA args = {varargs} result = {test_funcname_}(*args) print(result) """ ).format( modname=modname, test_funcname_=test_funcname, varargs=repr(varargs) ) # testtup = TestTuple(test_funcname_, testno, src, want=want, # flag='--exec-' + test_funcname_, # frame_fpath=frame_fpath, mode='exec', # total=1, nametup=[test_funcname_]) if ut.get_argflag(('--cmd', '--embed')): # TODO RECTIFY WITH EXEC DOCTEST testsrc = _cmd_modify_src(testsrc) # _exec_doctest(doctest_src) # doctest_src = ut.indent(testsrc, '>>> ') # Add line numbers doctest_src = ut.number_text_lines(testsrc) colored_src = ut.highlight_code(doctest_src) print('testsrc = \n%s' % (colored_src,)) try: code = compile(testsrc, '<string>', 'exec') exec(code, globals_) # , locals_) except ExitTestException: print('Test exited before show') pass retcode = ut.EXIT_SUCCESS print('Finished function test.') else: retcode = ut.EXIT_FAILURE print('Did not find any function named %r ' % (test_funcname,)) if ut.util_inject.PROFILING: ut.dump_profile_text() print('...exiting') sys.exit(retcode)
def _cmd_modify_src(testsrc): # testsrc = 'import IPython; import utool as ut; ut.qtensure()\n' + testsrc # testsrc += '\nimport IPython; IPython.embed()' # testsrc += '\nimport utool as ut; ut.embed()' import utool as ut pline = 'print(%r)' % ut.highlight_code(ut.indent(testsrc, '>>> ')) testsrc += '\nimport utool as ut; ' + pline + '; ut.qtensure(); ut.embed()' # testsrc += '\nimport utool as ut; ut.qtensure(); ut.embed()' return testsrc
[docs]def execute_doctest(func, testnum=0, module=None): """ Execute a function doctest. Can optionaly specify func name and module to run from ipython notebooks. Example: >>> # DISABLE_DOCTEST >>> from utool.util_tests import * # NOQA IPython: import utool as ut ut.execute_doctest(func='dummy_example_depcacahe', module='dtool.example_depcache') """ import utool as ut if isinstance(func, six.string_types): funcname = func if isinstance(module, six.string_types): modname = module module = ut.import_modname(modname) func, _testno = find_testfunc( module, funcname, ignore_prefix=[], ignore_suffix=[] ) # TODO RECTIFY WITH EXEC DOCTEST globals_ = {} testsrc = ut.get_doctest_examples(func)[0][testnum] # colored_src = ut.highlight_code(ut.indent(testsrc, '>>> ')) doctest_src = ut.indent(testsrc, '>>> ') doctest_src = '\n'.join( [ '%3d %s' % (count, line) for count, line in enumerate(doctest_src.splitlines(), start=1) ] ) colored_src = ut.highlight_code(doctest_src) print('testsrc = \n%s' % (colored_src,)) try: code = compile(testsrc, '<string>', 'exec') exec(code, globals_) except ExitTestException: print('Test exited before show')
if __name__ == '__main__': """ CommandLine: python -c "import utool, utool.util_tests; utool.doctest_funcs(utool.util_tests)" python -m utool.util_tests python -m utool.util_tests --allexamples python -m utool.util_tests python -c "import utool; utool.doctest_funcs(module=utool.util_tests, needs_enable=False)" /model/preproc/preproc_chip.py --allexamples """ import multiprocessing import utool as ut # NOQA multiprocessing.freeze_support() # doctest_funcs() ut.doctest_funcs()