Source

pygame / test / util / gen_stubs.py

#################################### IMPORTS ###################################

from __future__ import with_statement
from optparse import OptionParser
from inspect import isclass, ismodule, getdoc
from unittest import TestCase

import pygame, sys, datetime, re, types
import relative_indentation

################################ TESTS DIRECTORY ###############################

from os.path import normpath, join, dirname, abspath

sys.path.append(
    abspath(normpath(
        join(dirname(__file__), '../')
    ))
)

#################################### IGNORES ###################################

# pygame.sprite.Sprite.__module__ = 'pygame.sprite' 
# pygame.sprite.Rect.__module__   = 'pygame'

# In [7]: pygame.overlay.Overlay.__name__
# Out[7]: 'overlay'

# Can not then filter out nonsensical classes or functions automatically

IGNORE = (
    'pygame.sprite.Rect',
    'pygame.mixer.Channel',
    'pygame.sprite.from_surface',
    'pygame.sprite.get_ticks',
    'pygame.fastevent.Event',
    'pygame.fastevent.event_name',
    'pygame.sysfont.SysFont',
    'pygame.sysfont.get_fonts',
    'pygame.sysfont.match_font',
)

# Types that need instantiating before inspection
INSTANTIATE = (
    'pygame.event.Event',
    'pygame.cdrom.CD',
    'pygame.joystick.Joystick',
    'pygame.time.Clock',
    'pygame.mixer.Channel',
    'pygame.movie.Movie',
    'pygame.mask.Mask',
    'pygame.display.Info',
)

##################################### TODO #####################################

"""

1)
    IGNORE. Find a better solution.


2)

    Test

3)

    Properties
    Helper functions that return objects, ie time.Clock() etc

"""

################################ STUB TEMPLATES ################################

date = datetime.datetime.now().date()

STUB_TEMPLATE = relative_indentation.Template ( '''
    def ${test_name}(self):

        # __doc__ (as of %s) for ${unitname}:

          ${comments}

        self.assert_(test_not_implemented()) ''' % date, 

        strip_common = 0, strip_excess = 0
)

############################## REGULAR EXPRESSIONS #############################

module_re = re.compile(r"pygame\.([^.]*)\.?")

#################################### OPTIONS ###################################

opt_parser = OptionParser()

opt_parser.add_option(

     "-l",  "--list",
     dest   = "list",
     action = 'store_true',
     help   = "list only test names not stubs" )

opt_parser.set_usage(
"""
$ %prog ROOT

eg. 

$ %prog sprite.Sprite

def test_add(self):

    # Doc string for pygame.sprite.Sprite:

    ...
"""
)

################################### FUNCTIONS ##################################

def module_in_package(module, pkg):
    return ("%s." % pkg.__name__) in module.__name__

def get_package_modules(pkg):
    modules = (getattr(pkg, x) for x in dir(pkg) if is_public(x))
    return [m for m in modules if ismodule(m) and module_in_package(m, pkg)]
                                                 # Don't want to pick up 
                                                 # string module for example
def py_comment(input_str):
    return '\n'.join (
        [('# ' + l) for l in input_str.split('\n')]
    )

def is_public(obj_name):
    return not obj_name.startswith(('__','_'))

def is_test(f):
    return f.__name__.startswith('test_')

def get_callables(obj, if_of = None):
    publics = (getattr(obj, x) for x in dir(obj) if is_public(x))
    callables = [x for x in publics if callable(x)]
    
    if type(obj) is types.ModuleType: 
        callables = [x for x in callables if "pygame" in x.__module__]
                                            # XXXX, find something better
    if if_of:
        callables = [x for x in callables if if_of(x)] # isclass, ismethod etc
    
    return set(callables)

def get_class_from_test_case(TC, default = ''):
    TC = TC.__name__
    if 'Type' in TC:
        return '.' + TC[:TC.index('Type')]
    else:
        return default

################################################################################

def test_stub(f, module, parent_class = None):
    test_name = 'test_%s' % f.__name__
    unit_name = '%s.' % module.__name__

    if parent_class:
        unit_name += '%s.' % parent_class

    unit_name += f.__name__

    stub = STUB_TEMPLATE.render (

        test_name = test_name,
        comments = py_comment(getdoc(f) or ''),
        unitname = unit_name,
    )

    return test_name, stub

def names_of(*args):
    return tuple(map(lambda o: o.__name__, args))

def module_stubs(module):
    stubs = {}

    classes = get_callables(module, isclass)
    functions = get_callables(module) - classes

    for class_ in classes:
        if ("%s.%s" % names_of(module, class_)).startswith(IGNORE):
            continue

        for method in get_callables(class_):
            stub = test_stub(method, module, class_.__name__ )
            stubs['%s.%s.%s' % names_of(module, class_, method) ] = stub

    for function in functions:
        fname = '%s.%s' % names_of(module, function)
        if fname.startswith(IGNORE): continue

        stub = test_stub(function, module)
        stubs[fname] = stub
    
    return stubs

def package_stubs(package):
    stubs = dict()

    for module in get_package_modules(package):
        stubs.update(module_stubs(module))
    
    return stubs

def already_tested_in_module(module):
    already = []

    mod_name =  module.__name__
    
    test_name = "%s_test" % mod_name[7:]
    
    try: test_file = __import__(test_name)
    except ImportError:
        return
    
    classes = get_callables(test_file, isclass)
    test_cases = (t for t in classes if TestCase in t.__bases__)
    
    for class_ in test_cases:
        class_tested = get_class_from_test_case(class_, default = '')
    
        for test in get_callables(class_, is_test):
            fname = test.__name__[5:].split('__')[0]
            already.append("%s%s.%s" % (mod_name, class_tested, fname))
    
    return already

def already_tested_in_package(package):
    already = []

    for module in get_package_modules(package):
        already.append(already_tested_in_module(module))

    return already

def get_stubs(root):
    module_root = module_re.search(root)
    if module_root:
        module = getattr(pygame, module_root.group(1))
        stubs = module_stubs(module)
        tested = already_tested_in_module(module)
    else:
        stubs = package_stubs(pygame)
        tested = already_tested_in_package(pygame)

    return stubs, tested

if __name__ == "__main__":
    options, args = opt_parser.parse_args()
    if not sys.argv[1:]: 
        sys.exit(opt_parser.print_help())
    
    root = args and args[0] or 'pygame'
    if root != 'pygame' and not root.startswith('pygame'):
        root = 'pygame.' + root

    stubs, tested = get_stubs(root)

    for fname in sorted(s for s in stubs.iterkeys() if s not in tested):
        if not fname.startswith(root): continue  # eg. module.Class

        test_name, stub = stubs[fname]

        if options.list:
            print ('%13s: %s\n%13s: %s\n' %
                  ('Callable Name', fname, 'Test Name', test_name))
        else:
            print stub

################################################################################