Index: /trunk/cheesecake/subprocess.py =================================================================== --- /trunk/cheesecake/subprocess.py (revision 10) +++ /trunk/cheesecake/subprocess.py (revision 150) @@ -394,5 +394,5 @@ import pickle -__all__ = ["Popen", "PIPE", "STDOUT", "call"] +__all__ = ["Popen", "PIPE", "STDOUT", "call", "ProcessError"] try: @@ -495,4 +495,8 @@ return ''.join(result) +class ProcessError(Exception): + """This exception is raised when there is an error calling + a subprocess.""" + pass class Popen(object): @@ -984,5 +988,5 @@ if data != "": child_exception = pickle.loads(data) - raise child_exception + raise ProcessError, child_exception Index: /trunk/cheesecake/util.py =================================================================== --- /trunk/cheesecake/util.py (revision 150) +++ /trunk/cheesecake/util.py (revision 150) @@ -0,0 +1,212 @@ +"""Utility functions for Cheesecake project. +""" + +import os +import shutil +import sys +import tarfile +import time +import zipfile + +from subprocess import call, ProcessError, Popen, PIPE, STDOUT + +PAD_TEXT = 40 +PAD_VALUE = 4 + +def run_cmd(cmd, env=None): + """Run command and return its return code and its output. + + >>> run_cmd('/bin/true') + (0, '') + """ + arglist = cmd.split() + try: + p = Popen(arglist, stdout=PIPE, stderr=STDOUT, env=env) + except ProcessError, e: + return 1, e + output = p.communicate()[0] + return p.returncode, output + +def command_successful(cmd): + """Returns True if command exited normally, False otherwise. + + >>> command_successful('/bin/true') + True + >>> command_successful('this-command-doesnt-exist') + False + """ + rc, output = run_cmd(cmd) + if rc: + return False + return True + +class StdoutRedirector(object): + """Redirect stdout to a temporary file. + """ + def __init__(self, filename=None): + if filename: + self.fh = open(filename, 'w') + else: + self.fh = os.tmpfile() + + def write(self, buf): + self.fh.write(buf) + + def flush(self): + self.fh.flush() + + def read_buffer(self): + """Return contents of the temporary file. + """ + self.fh.seek(0) + return self.fh.read() + +def pad_with_dots(msg, length=PAD_TEXT): + """Pad text with dots up to given length. + + >>> pad_with_dots("Hello world", 20) + 'Hello world ........' + >>> pad_with_dots("Exceeding length", 10) + 'Exceeding length' + """ + msg_length = len(msg) + + if msg_length >= length: + return msg + + msg = msg + " " + for i in range(msg_length+1, length): + msg += "." + return msg + +def pad_left_spaces(value, length=PAD_VALUE): + """Pad value with spaces at left up to given length. + + >>> pad_left_spaces(15, 4) + ' 15' + >>> pad_left_spaces(123456, 2) + '123456' + >>> len(pad_left_spaces("")) == PAD_VALUE + True + """ + if not isinstance(value, basestring): + value = str(value) + diff = length - len(value) + return " " * diff + value + +def pad_right_spaces(value, length=PAD_VALUE): + """Pad value with spaces at left up to given length. + + >>> pad_right_spaces(123, 5) + '123 ' + >>> pad_right_spaces(12.1, 5) + '12.1 ' + """ + if not isinstance(value, basestring): + value = str(value) + diff = length - len(value) + return value + " " * diff + +def pad_msg(msg, value, msg_length=PAD_TEXT, value_length=PAD_VALUE): + """Pad message with dots and pad value with spaces. + + >>> pad_msg("123456", 77, msg_length=10, value_length=4) + '123456 ... 77' + >>> pad_msg("123", u"45", msg_length=5, value_length=3) + u'123 . 45' + """ + return msg + " " +"." * (msg_length-len(msg)-1) + pad_left_spaces(value, value_length) + +def pad_line(char="=", length=(PAD_TEXT+PAD_VALUE+1)): + """Return line consisting of 'char' characters. + + >>> pad_line('*', 3) + '***' + >>> pad_line(length=10) + '==========' + """ + return char * length + +def unzip_package(package, destination): + """Unzip given `package` to the `destination` directory. + + Return name of unpacked directory or None on error. + """ + try: + z = zipfile.ZipFile(package) + except zipfile.error: + return None + + # Get directory structure from zip and create it in destination directory. + for name in z.namelist(): + (dir, file) = os.path.split(name) + unpack_dir = dir + target_dir = os.path.join(destination, dir) + if not os.path.exists(target_dir): + os.makedirs(target_dir) + + # Extract files to directory structure + for i, name in enumerate(z.namelist()): + if not name.endswith('/'): + outfile = open(os.path.join(destination, name), 'wb') + outfile.write(z.read(name)) + outfile.flush() + outfile.close() + + return unpack_dir.split(os.sep)[0] + +def untar_package(package, destination): + """Untar given `package` to the `destination` directory. + + Return name of unpacked directory or None on error. + """ + try: + t = tarfile.open(package) + except tarfile.ReadError, e: + return None + + for member in t.getmembers(): + t.extract(member, destination) + + tarinfo = t.members[0] + return tarinfo.name.split(os.sep)[0] + +def unegg_package(package, destination): + """Unpack given egg to the `destination` directory. + + Return name of unpacked directory or None on error. + """ + if os.path.isdir(package): + package_name = os.path.basename(package) + destination = os.path.join(destination, package_name) + shutil.copytree(package, destination, symlinks=True) + return package_name + else: + return unzip_package(package, destination) + +def mkdirs(dir): + """Make directory with parent directories as needed. + + Don't throw an exception if directory exists. + """ + parts = dir.split(os.path.sep) + for length in xrange(1, len(parts)+1): + path = os.path.sep.join([''] + parts[:length]) + if not os.path.exists(path): + os.mkdir(path) + +def time_function(function): + """Measure function execution time. + + Return (return value, time taken) tuple. + + >>> def fun(x): + ... return x*2 + >>> ret, time_taken = time_function(lambda: fun(5)) + >>> ret + 10 + """ + start = time.time() + ret = function() + end = time.time() + return ret, end-start Index: /trunk/cheesecake/__init__.py =================================================================== --- /trunk/cheesecake/__init__.py (revision 2) +++ /trunk/cheesecake/__init__.py (revision 150) @@ -0,0 +1,1 @@ +__version__ = '0.6' Index: /trunk/cheesecake/model.py =================================================================== --- /trunk/cheesecake/model.py (revision 11) +++ /trunk/cheesecake/model.py (revision 150) @@ -1,6 +1,13 @@ """ --Code borrowed from Michael Hudson's docextractor package with the author's permission. --The original code is available at +Code borrowed from Michael Hudson's docextractor package with the author's +permission. + +The original code is available at http://codespeak.net/svn/user/mwh/docextractor/. + +Changes: + * do not print warnings to stdout (in System.warning) + * collect all function calls """ + from compiler import ast @@ -11,7 +18,44 @@ import sets -import compiler from compiler.transformer import parse, parseFile from compiler.visitor import walk + +import ast_pp + + +def get_call_name(node): + assert isinstance(node, ast.CallFunc) + + def get_name(node): + if isinstance(node, ast.CallFunc): + return None + elif isinstance(node, ast.Name): + return node.name + elif isinstance(node, str): + return node + elif isinstance(node, tuple): + if len(node) == 1: + return node[0] + else: + return "%s.%s" % (get_name(node[:-1][0]), node[-1]) + elif isinstance(node, ast.Getattr): + return get_name(node.asList()) + else: + return None + + return get_name(node.node) + +def get_function_calls(node, fc): + if not isinstance(node, ast.Node): + return + + for child in node.getChildren(): + if isinstance(child, ast.CallFunc): + func_called = get_call_name(child) + if func_called: + fc[func_called] = 1 + + get_function_calls(child, fc) + class Documentable(object): @@ -44,10 +88,99 @@ return self.parent.name2fullname(name) + def resolveDottedName(self, dottedname, verbose=False): + parts = dottedname.split('.') + obj = self + system = self.system + while parts[0] not in obj._name2fullname: + obj = obj.parent + if obj is None: + if parts[0] in system.allobjects: + obj = system.allobjects[parts[0]] + break + for othersys in system.moresystems: + if parts[0] in othersys.allobjects: + obj = othersys.allobjects[parts[0]] + break + else: + if verbose: + print "1 didn't find %r from %r"%(dottedname, + self.fullName()) + return None + break + else: + fn = obj._name2fullname[parts[0]] + if fn in system.allobjects: + obj = system.allobjects[fn] + else: + if verbose: + print "1.5 didn't find %r from %r"%(dottedname, + self.fullName()) + return None + for p in parts[1:]: + if p not in obj.contents: + if verbose: + print "2 didn't find %r from %r"%(dottedname, + self.fullName()) + return None + obj = obj.contents[p] + if verbose: + print dottedname, '->', obj.fullName(), 'in', self.fullName() + return obj + + def dottedNameToFullName(self, dottedname): + if '.' not in dottedname: + start, rest = dottedname, '' + else: + start, rest = dottedname.split('.', 1) + rest = '.' + rest + obj = self + while start not in obj._name2fullname: + obj = obj.parent + if obj is None: + return dottedname + return obj._name2fullname[start] + rest + + def __getstate__(self): + # this is so very, very evil. + # see doc/extreme-pickling-pain.txt for more. + r = {} + for k, v in self.__dict__.iteritems(): + if isinstance(v, Documentable): + r['$'+k] = v.fullName() + elif isinstance(v, list) and v: + for vv in v: + if vv is not None and not isinstance(vv, Documentable): + r[k] = v + break + else: + rr = [] + for vv in v: + if vv is None: + rr.append(vv) + else: + rr.append(vv.fullName()) + r['@'+k] = rr + elif isinstance(v, dict) and v: + for vv in v.itervalues(): + if not isinstance(vv, Documentable): + r[k] = v + break + else: + rr = {} + for kk, vv in v.iteritems(): + rr[kk] = vv.fullName() + r['!'+k] = rr + else: + r[k] = v + return r class Package(Documentable): + kind = "Package" def name2fullname(self, name): raise NameError + class Module(Documentable): + kind = "Module" def name2fullname(self, name): if name in self._name2fullname: @@ -59,15 +192,153 @@ return name + class Class(Documentable): + kind = "Class" def setup(self): super(Class, self).setup() self.bases = [] - def __repr__(self): - return "%s(%r, %r) # %r"%(self.__class__.__name__, - self.name, self.shortdocstring(), - self.bases) + self.rawbases = [] + self.baseobjects = [] + self.subclasses = [] + class Function(Documentable): - pass + kind = "Function" + + +class ModuleVistor(object): + def __init__(self, system, modname): + self.system = system + self.modname = modname + self.morenodes = [] + + def default(self, node): + for child in node.getChildNodes(): + self.visit(child) + + def postpone(self, docable, node): + self.morenodes.append((docable, node)) + + def visitModule(self, node): + if self.system.current and self.modname in self.system.current.contents: + m = self.system.current.contents[self.modname] + assert m.docstring is None + m.docstring = node.doc + self.system.push(m, node) + self.default(node) + self.system.pop(m) + else: + if not self.system.current: + roots = [x for x in self.system.rootobjects if x.name == self.modname] + if roots: + mod, = roots + self.system.push(mod, node) + self.default(node) + self.system.pop(mod) + return + self.system.pushModule(self.modname, node.doc) + self.default(node) + self.system.popModule() + + def visitClass(self, node): + cls = self.system.pushClass(node.name, node.doc) + if node.lineno is not None: + cls.linenumber = node.lineno + for n in node.bases: + str_base = ast_pp.pp(n) + cls.rawbases.append(str_base) + base = cls.dottedNameToFullName(str_base) + cls.bases.append(base) + self.default(node) + self.system.popClass() + + def visitFrom(self, node): + modname = expandModname(self.system, node.modname) + name2fullname = self.system.current._name2fullname + for fromname, asname in node.names: + if fromname == '*': + self.system.warning("import *", modname) + if modname not in self.system.allobjects: + return + mod = self.system.allobjects[modname] + # this might fail if you have an import-* cycle, or if + # you're just not running the import star finder to + # save time (not that this is possibly without + # commenting stuff out yet, but...) + if isinstance(mod, Package): + self.system.warning("import * from a package", modname) + return + if mod.processed: + for n in mod.contents: + name2fullname[n] = modname + '.' + n + else: + self.system.warning("unresolvable import *", modname) + return + if asname is None: + asname = fromname + name2fullname[asname] = modname + '.' + fromname + + def visitImport(self, node): + name2fullname = self.system.current._name2fullname + for fromname, asname in node.names: + fullname = expandModname(self.system, fromname) + if asname is None: + asname = fromname.split('.', 1)[0] + # aaaaargh! python sucks. + parts = fullname.split('.') + for i, part in enumerate(fullname.split('.')[::-1]): + if part == asname: + fullname = '.'.join(parts[:len(parts)-i]) + name2fullname[asname] = fullname + break + else: + name2fullname[asname] = '.'.join(parts) + else: + name2fullname[asname] = fullname + + def visitFunction(self, node): + fc = {} + get_function_calls(node, fc) + func = self.system.pushFunction(node.name, node.doc, fc) + if node.lineno is not None: + func.linenumber = node.lineno + # ast.Function has a pretty lame representation of + # arguments. Let's convert it to a nice concise format + # somewhat like what inspect.getargspec returns + argnames = node.argnames[:] + kwname = starargname = None + if node.kwargs: + kwname = argnames.pop(-1) + if node.varargs: + starargname = argnames.pop(-1) + defaults = [] + for default in node.defaults: + try: + defaults.append(ast_pp.pp(default)) + except (KeyboardInterrupt, SystemExit): + raise + except Exception, e: + self.system.warning("unparseable default", "%s: %s %r"%(e.__class__.__name__, + e, default)) + defaults.append('???') + # argh, convert unpacked-arguments from tuples to lists, + # because that's what getargspec uses and the unit test + # compares it + argnames2 = [] + for argname in argnames: + if isinstance(argname, tuple): + argname = list(argname) + argnames2.append(argname) + func.argspec = (argnames2, starargname, kwname, tuple(defaults)) + self.postpone(func, node.code) + self.system.popFunction() + +states = [ + 'blank', + 'preparse', + 'importstarred', + 'parsed', + 'finalized', + ] @@ -77,4 +348,5 @@ Package = Package Function = Function + ModuleVistor = ModuleVistor def __init__(self): @@ -85,8 +357,12 @@ self.rootobjects = [] self.warnings = {} - # importstargraph contains edges {importedby:[imports]} but only + # importstargraph contains edges {importer:[imported]} but only # for import * statements self.importstargraph = {} self.func_called = {} + self.state = 'blank' + self.packages = [] + self.moresystems = [] + self.urlprefix = '' def _push(self, cls, name, docstring): @@ -128,4 +404,11 @@ The default is that the second definition "wins". ''' + i = 0 + fn = obj.fullName() + while (fn + ' ' + str(i)) in self.allobjects: + i += 1 + prev = self.allobjects[obj.fullName()] + prev.name = obj.name + ' ' + str(i) + self.allobjects[prev.fullName()] = prev self.warning("duplicate", self.allobjects[obj.fullName()]) self.allobjects[obj.fullName()] = obj @@ -204,6 +487,8 @@ def warning(self, type, detail): - fn = self.current.fullName() - #print fn, type, detail + if self.current is not None: + fn = self.current.fullName() + else: + fn = '' self.warnings.setdefault(type, []).append((fn, detail)) @@ -212,4 +497,47 @@ if isinstance(o, cls): yield o + + def finalStateComputations(self): + self.recordBasesAndSubclasses() + + def recordBasesAndSubclasses(self): + for cls in self.objectsOfType(Class): + for n in cls.bases: + o = cls.parent.resolveDottedName(n) + cls.baseobjects.append(o) + if o: + o.subclasses.append(cls) + + def __getstate__(self): + state = self.__dict__.copy() + del state['moresystems'] + return state + + def __setstate__(self, state): + self.moresystems = [] + # this is so very, very evil. + # see doc/extreme-pickling-pain.txt for more. + self.__dict__.update(state) + for obj in self.orderedallobjects: + for k, v in obj.__dict__.copy().iteritems(): + if k.startswith('$'): + del obj.__dict__[k] + obj.__dict__[k[1:]] = self.allobjects[v] + elif k.startswith('@'): + n = [] + for vv in v: + if vv is None: + n.append(None) + else: + n.append(self.allobjects[vv]) + del obj.__dict__[k] + obj.__dict__[k[1:]] = n + elif k.startswith('!'): + n = {} + for kk, vv in v.iteritems(): + n[kk] = self.allobjects[vv] + del obj.__dict__[k] + obj.__dict__[k[1:]] = n + def expandModname(system, modname, givewarning=True): @@ -242,157 +570,8 @@ modname = expandModname(self.system, node.modname, False) self.system.importstargraph.setdefault( - modname, []).append(self.modfullname) - -class ModuleVistor(object): - def __init__(self, system, modname): - self.system = system - self.modname = modname - self.morenodes = [] - - def default(self, node): - for child in node.getChildNodes(): - self.visit(child) - - def postpone(self, docable, node): - self.morenodes.append((docable, node)) - - def visitModule(self, node): - if self.system.current and self.modname in self.system.current.contents: - m = self.system.current.contents[self.modname] - assert m.docstring is None - m.docstring = node.doc - self.system.push(m, node) - self.default(node) - self.system.pop(m) - else: - self.system.pushModule(self.modname, node.doc) - self.default(node) - self.system.popModule() - - def visitClass(self, node): - cls = self.system.pushClass(node.name, node.doc) - for n in node.bases: - if isinstance(n, ast.Name): - cls.bases.append(cls.parent.name2fullname(n.name)) - elif isinstance(n, ast.Getattr): - p = [] - while isinstance(n, ast.Getattr): - p.append(n.attrname) - n = n.expr - assert isinstance(n, ast.Name) - p.append(cls.parent.name2fullname(n.name)) - p.reverse() - assert None not in p, n - cls.bases.append('.'.join(p)) - else: - assert not n - self.default(node) - self.system.popClass() - - def visitFrom(self, node): - modname = expandModname(self.system, node.modname) - name2fullname = self.system.current._name2fullname - for fromname, asname in node.names: - if fromname == '*': - self.system.warning("import *", modname) - if modname not in self.system.allobjects: - return - mod = self.system.allobjects[modname] - #snarl (see below) - #assert mod.processed - self.system.warning("mwh is an idiot", "") - for n in mod.contents: - name2fullname[n] = modname + '.' + n - return - if asname is None: - asname = fromname - name2fullname[asname] = modname + '.' + fromname - - def visitImport(self, node): - name2fullname = self.system.current._name2fullname - for fromname, asname in node.names: - fullname = expandModname(self.system, fromname) - if asname is None: - asname = fromname.split('.', 1)[0] - # aaaaargh! python sucks. - parts = fullname.split('.') - for i, part in enumerate(fullname.split('.')[::-1]): - if part == asname: - fullname = '.'.join(parts[:len(parts)-i]) - name2fullname[asname] = fullname - break - else: - name2fullname[asname] = '.'.join(parts) - else: - name2fullname[asname] = fullname - - - def visitFunction(self, node): - fc = {} - get_function_calls(node, fc) - #print fc.keys() - func = self.system.pushFunction(node.name, node.doc, fc) - # ast.Function has a pretty lame representation of - # arguments. Let's convert it to a nice concise - # getargspec-like format and include it in the Function - # object. - argnames = node.argnames[:] - kwname = starargname = None - if node.kwargs: - kwname = argnames.pop(-1) - if node.varargs: - starargname = argnames.pop(-1) - defaults = [] - for default in node.defaults: - if isinstance(default, ast.Const): - defaults.append(default.value) - elif isinstance(default, ast.Name): - defaults.append(default.name) - else: - self.system.warning("unparseable default", repr(default)) - defaults.append('???') - #assert False, "don't know how to handle default %r"%(default,) - # argh, convert unpacked-arguments from tuples to lists, - # because that's what getargspec uses and the unit test - # compares it - argnames2 = [] - for argname in argnames: - if isinstance(argname, tuple): - argname = list(argname) - argnames2.append(argname) - func.argspec = (argnames2, starargname, kwname, tuple(defaults)) - #for child in node.getChildren(): - # if isinstance(child, compiler.ast.Stmt): - # for c in child.getChildren(): - # print c.__class__ - # print c - self.postpone(func, node.code) - self.system.popFunction() - -def get_function_calls(node, fc): - if not isinstance(node, compiler.ast.Node): - return - for child in node.getChildren(): - #print "child:", child - if isinstance(child, compiler.ast.CallFunc): - funcname = "" - attrname = "" - n = child.node - #print "n:", n - #print n.__class__ - if isinstance(n, compiler.ast.Getattr): - expr = n.expr - if isinstance(expr, compiler.ast.Name): - funcname = expr.name - attrname = n.attrname - func_called = "" - if funcname: func_called = funcname + "." - func_called += attrname - if func_called: - fc[func_called] = 1 - get_function_calls(child, fc) - + self.modfullname, []).append(modname) + def processModuleAst(ast, name, system): - mv = ModuleVistor(system, name) + mv = system.ModuleVistor(system, name) walk(ast, mv) while mv.morenodes: @@ -405,14 +584,22 @@ def fromText(src, modname='', system=None): if system is None: - system = System() - processModuleAst(parse(src), modname, system) - return system.rootobjects[0] + _system = System() + else: + _system = system + processModuleAst(parse(src), modname, _system) + if system is None: + _system.finalStateComputations() + return _system.rootobjects[0] def preprocessDirectory(system, dirpath): - package = system.pushPackage(os.path.basename(dirpath), None) + assert system.state in ['blank', 'preparse'] + if os.path.basename(dirpath): + package = system.pushPackage(os.path.basename(dirpath), None) + else: + package = None for fname in os.listdir(dirpath): fullname = os.path.join(dirpath, fname) - if os.path.isdir(fullname) and os.path.exists(os.path.join(fullname, '__init__.py')): + if os.path.isdir(fullname) and os.path.exists(os.path.join(fullname, '__init__.py')) and fname != 'test': preprocessDirectory(system, fullname) elif fname.endswith('.py'): @@ -422,29 +609,71 @@ mod.processed = False system.popModule() - system.popPackage() - -def processDirectory(system, dirpath): - preprocessDirectory(system, dirpath) + if package: + system.popPackage() + system.state = 'preparse' + +def findImportStars(system): + assert system.state in ['preparse'] modlist = list(system.objectsOfType(Module)) for mod in modlist: system.push(mod.parent) isf = ImportStarFinder(system, mod.fullName()) - walk(parseFile(mod.filepath), isf) + try: + ast = parseFile(mod.filepath) + except (SyntaxError, ValueError): + system.warning("cannot parse", mod.filepath) + walk(ast, isf) system.pop(mod.parent) - - # snarl; a toposort is meant to go here. - newlist = modlist + system.state = 'importstarred' + +def extractDocstrings(system): + assert system.state in ['preparse', 'importstarred'] + # and so much more... + modlist = list(system.objectsOfType(Module)) + newlist = toposort([m.fullName() for m in modlist], system.importstargraph) for mod in newlist: + mod = system.allobjects[mod] system.push(mod.parent) - processModuleAst(parseFile(mod.filepath), mod.name, system) + try: + ast = parseFile(mod.filepath) + except (SyntaxError, ValueError): + system.warning("cannot parse", mod.filepath) + processModuleAst(ast, mod.name, system) mod.processed = True system.pop(mod.parent) - -def main(argv): + system.state = 'parsed' + +def finalStateComputations(system): + assert system.state in ['parsed'] + system.finalStateComputations() + system.state = 'finalized' + +def processDirectory(system, dirpath): + preprocessDirectory(system, dirpath) + findImportStars(system) + extractDocstrings(system) + finalStateComputations(system) + +def toposort(input, edges): + # this doesn't detect cycles in any clever way. + output = [] + input = dict.fromkeys(input) + def p(i): + for j in edges.get(i, []): + if j in input: + del input[j] + p(j) + output.append(i) + while input: + p(input.popitem()[0]) + return output + + +def main(systemcls, argv): if '-r' in argv: argv.remove('-r') assert len(argv) == 1 - system = System() + system = systemcls() processDirectory(system, argv[0]) pickle.dump(system, open('da.out', 'wb'), pickle.HIGHEST_PROTOCOL) @@ -454,5 +683,5 @@ print k, len(v) else: - system = System() + system = systemcls() for fname in argv: modname = os.path.splitext(os.path.basename(fname))[0] # XXX! @@ -463,3 +692,3 @@ if __name__ == '__main__': - main(sys.argv[1:]) + main(System, sys.argv[1:]) Index: /trunk/cheesecake/ast_pp.py =================================================================== --- /trunk/cheesecake/ast_pp.py (revision 150) +++ /trunk/cheesecake/ast_pp.py (revision 150) @@ -0,0 +1,335 @@ +# this is stolen from exarkun's sandbox +import sys + +from StringIO import StringIO +from pprint import pprint +from compiler import parse, walk + +class SourceWriter(object): + _i = 0 + + def __init__(self): + self.s = StringIO() + + def w(self, s): + self.s.write(s) + + def nl(self): + self.s.write('\n') + self.s.write(' ' * 4 * self._i) + + def indent(self): + self._i += 1 + self.nl() + + def dedent(self): + self._i -= 1 + self.nl() + + def visitModule(self, node): + if node.doc is not None: + self.wl(repr(node.doc)) + walk(node.node, self) + + def visitStmt(self, node): + for n in node.getChildren(): + walk(n, self) + + def visitFunction(self, node): + fmt = 'def %s(%s):' + if node.defaults: + nargs = len(node.argnames) + ndefs = len(node.defaults) + noDefaults = node.argnames[:nargs-ndefs] + s = ', '.join(node.argnames[:noDefaults]) + if ndefs < nargs: + argdefs = zip(node.argnames[noDefaults:], node.defaults) + s = s + ', ' + ', '.join(['='.join(x) for x in argdefs]) + else: + s = ', '.join(node.argnames) + self.w(fmt % (node.name, s)) + self.indent() + try: + walk(node.code, self) + finally: + self.dedent() + + def visitAssign(self, node): + walk(node.nodes[0], self) + self.w(' = ') + walk(node.expr, self) + self.nl() + + def visitAssName(self, node): + self.w(node.name) + + def visitCallFunc(self, node): + walk(node.node, self) + self.w('(') + for a in node.args[:-1]: + walk(a, self) + self.w(', ') + for a in node.args[-1:]: + walk(a, self) + self.w(')') + + def visitListComp(self, node): + self.w('[') + walk(node.expr, self) + for q in node.quals: + walk(q, self) + self.w(']') + + def visitListCompFor(self, node): + self.w(' for ') + walk(node.assign, self) + self.w(' in ') + walk(node.list, self) + for expr in node.ifs: + self.w(' if ') + walk(expr, self) + + def visitName(self, node): + self.w(node.name) + + def visitDiscard(self, node): + walk(node.expr, self) + self.nl() + + def visitPrintnl(self, node): + self.w('print ') + if node.dest: + self.w('>>') + walk(node.dest, self) + self.w(', ') + for e in node.nodes: + walk(e, self) + self.nl() + + def visitGetattr(self, node): + walk(node.expr, self) + self.w('.') + self.w(node.attrname) + + def visitImport(self, node): + self.w('import ') + for (mod, as) in node.names: + self.w(mod) + if as is not None: + self.w(' as ') + self.w(as) + self.w(', ') + self.nl() + + def visitFrom(self, node): + self.w('from ') + self.w(node.modname) + self.w(' import ') + for (mod, as) in node.names: + self.w(mod) + if as is not None: + self.w(' as ') + self.w(as) + self.w(', ') + self.nl() + + def visitConst(self, node): + self.w(repr(node.value)) + + def visitReturn(self, node): + self.w('return ') + walk(node.value, self) + self.nl() + + def visitClass(self, node): + self.w('class ') + self.w(node.name) + if node.bases: + self.w('(') + for b in node.bases: + walk(b, self) + self.w(', ') + self.w('):') + self.indent() + try: + if node.doc is not None: + self.w(repr(node.doc)) + walk(node.code, self) + finally: + self.dedent() + + def visitAssAttr(self, node): + walk(node.expr, self) + self.w('.') + self.w(node.attrname) + + def visitMul(self, node): + walk(node.left, self) + self.w(' * ') + walk(node.right, self) + + def visitSub(self, node): + walk(node.left, self) + self.w(' - ') + walk(node.right, self) + + def visitAdd(self, node): + walk(node.left, self) + self.w(' + ') + walk(node.right, self) + + def visitMod(self, node): + walk(node.left, self) + self.w(' % ') + walk(node.right, self) + + def visitAugAssign(self, node): + walk(node.node, self) + self.w(' ') + self.w(node.op) + self.w(' ') + walk(node.expr, self) + self.nl() + + def visitIf(self, node): + keyword = 'if' + for (cond, body) in node.tests: + self.w(keyword) + self.w(' ') + walk(cond, self) + self.w(':') + self.indent() + try: + walk(body, self) + finally: + self.dedent() + keyword = 'elif' + if node.else_: + self.w('else:') + self.indent() + try: + walk(node.else_, self) + finally: + self.dedent() + + def visitCompare(self, node): + walk(node.expr, self) + for (op, arg) in node.ops: + self.w(' ') + self.w(op) + self.w(' ') + walk(arg, self) + + def visitFor(self, node): + self.w('for ') + walk(node.assign, self) + self.w(' in ') + walk(node.list, self) + self.w(':') + self.indent() + try: + walk(node.body, self) + finally: + self.dedent() + if node.else_: + self.w('else:') + self.indent() + try: + walk(node.else_, self) + finally: + self.dedent() + + def visitSlice(self, node): + walk(node.expr, self) + self.w('[') + if node.lower: + walk(node.lower, self) + self.w(':') + if node.upper: + walk(node.upper, self) + self.w(']') + + def visitTuple(self, node): + self.w('(') + if len(node.nodes) == 0: + pass + elif len(node.nodes) == 1: + walk(node.nodes[0], self) + self.w(',') + else: + for expr in node.nodes[:-1]: + walk(expr, self) + self.w(', ') + walk(node.nodes[-1], self) + self.w(')') + + def visitTryFinally(self, node): + self.w('try:') + self.indent() + try: + walk(node.body, self) + finally: + self.dedent() + self.w('finally:') + self.indent() + try: + walk(node.final, self) + finally: + self.dedent() + + def visitSubscript(self, node): + walk(node.expr, self) + self.w('[') + walk(node.subs[0], self) + self.w(']') + + def visitUnarySub(self, node): + self.w('-') + walk(node.expr, self) + + def visitAssTuple(self, node): + self.w('(') + for expr in node.nodes: + walk(expr, self) + self.w(', ') + self.w(')') + + def visitRaise(self, node): + self.w('raise ') + walk(node.expr1, self) + if node.expr2: + self.w(', ') + walk(node.expr2, self) + if node.expr3: + self.w(', ') + walk(node.expr3, self) + self.nl() + + def visitDict(self, node): + self.w('{') + for (k, v) in node.items: + walk(k, self) + self.w(':') + walk(v, self) + self.w(',') + self.w('}') + + def __str__(self): + return self.s.getvalue() + +def pp(ast): + sw = SourceWriter() + walk(ast, sw) + return sw.s.getvalue() + +def magic(s): + ast = parse(s) + sw = SourceWriter() + walk(ast, sw) + return ast, sw + +if __name__ == '__main__': + f = file(__file__, 'r').read() + ast, sw = magic(f) + print sw + print ast Index: /trunk/cheesecake/cheesecake_index.py =================================================================== --- /trunk/cheesecake/cheesecake_index.py (revision 11) +++ /trunk/cheesecake/cheesecake_index.py (revision 150) @@ -1,23 +1,22 @@ #!/usr/bin/env python +"""Cheesecake: How tasty is your code? + +The idea of the Cheesecake project is to rank Python packages based on various +empirical "kwalitee" factors, such as: + + * whether the package can be downloaded from PyPI given its name + * whether the package can be unpacked + * whether the package can be installed into an alternate directory + * existence of certain files such as README, INSTALL, LICENSE, setup.py etc. + * percentage of modules/functions/classes/methods with docstrings + * ... and many others """ -Cheesecake: How tasty is your code? - -The idea of the Cheesecake project is to rank Python packages -based on various empiric "kwalitee" factors, such as: - - * whether the package can be downloaded - * whether the package can be unpacked - * whether the package can be installed into an alternate directory - * existence of certain files such as README, INSTALL, LICENSE, setup.py etc. - * existence of certain directories such as doc, test, demo, examples - * percentage of modules/functions/classes/methods with docstrings - * percentage of functions/methods that are unit tested - * average pylint score for all non-test and non-demo modules - * whether the package can be unpacked - * whether the package can be installed into an alternate directory -""" - -import os, sys, re, shutil -import tarfile, zipfile + +import os +import re +import shutil +import sys +import tempfile + from optparse import OptionParser from urllib import urlretrieve @@ -25,85 +24,1157 @@ from math import ceil -from _util import run_cmd, pad_with_dots, pad_left_spaces, pad_msg, pad_line -from _util import StdoutRedirector import logger -from config import get_pkg_config + +from util import pad_with_dots, pad_left_spaces, pad_right_spaces, pad_msg, pad_line +from util import run_cmd, command_successful +from util import unzip_package, untar_package, unegg_package +from util import mkdirs +from util import StdoutRedirector +from util import time_function from codeparser import CodeParser +from cheesecake import __version__ as VERSION + +__docformat__ = 'reStructuredText en' + + +################################################################################ +## Helpers. +################################################################################ + +if 'sorted' not in dir(__builtins__): + def sorted(L): + new_list = L[:] + new_list.sort() + return new_list + +if 'set' not in dir(__builtins__): + from sets import Set as set + +def isiterable(obj): + """Check whether object is iterable. + + >>> isiterable([1,2,3]) + True + >>> isiterable("string") + True + >>> isiterable(object) + False + """ + return hasattr(obj, '__iter__') or isinstance(obj, basestring) + +def has_extension(filename, ext): + """Check if filename has given extension. + + >>> has_extension("foobar.py", ".py") + True + >>> has_extension("foo.bar.py", ".py") + True + >>> has_extension("foobar.pyc", ".py") + False + + This function is case insensitive. + >>> has_extension("FOOBAR.PY", ".py") + True + """ + return os.path.splitext(filename.lower())[1] == ext.lower() + +def discover_file_type(filename): + """Discover type of a file according to its name and its parent directory. + + Currently supported file types: + * pyc + * pyo + * module: .py files of an application + * demo: .py files for documentation/demonstration purposes + * test: .py files used for testing + * special: .py file for special purposes + + :Note: This function only check file's name, and doesn't touch the + filesystem. If you have to, check if file exists by yourself. + + >>> discover_file_type('module.py') + 'module' + >>> discover_file_type('./setup.py') + 'special' + >>> discover_file_type('some/directory/junk.pyc') + 'pyc' + >>> discover_file_type('examples/readme.txt') + >>> discover_file_type('examples/runthis.py') + 'demo' + >>> discover_file_type('optimized.pyo') + 'pyo' + + >>> test_files = ['ut/test_this_and_that.py', + ... 'another_test.py', + ... 'TEST_MY_MODULE.PY'] + >>> for filename in test_files: + ... assert discover_file_type(filename) == 'test', filename + + >>> discover_file_type('this_is_not_a_test_really.py') + 'module' + """ + dirs = filename.split(os.path.sep) + dirs, filename = dirs[:-1], dirs[-1] + + if filename in ["setup.py", "ez_setup.py", "__pkginfo__.py"]: + return 'special' + + if has_extension(filename, ".pyc"): + return 'pyc' + if has_extension(filename, ".pyo"): + return 'pyo' + if has_extension(filename, ".py"): + for dir in dirs: + if dir in ['test', 'tests']: + return 'test' + elif dir in ['doc', 'docs', 'demo', 'example', 'examples']: + return 'demo' + + # Most test frameworks look for files starting with "test_". + # py.test also looks at files with trailing "_test". + if filename.lower().startswith('test_') or \ + os.path.splitext(filename)[0].lower().endswith('_test'): + return 'test' + + return 'module' + +def get_files_of_type(file_list, file_type): + """Return files from `file_list` that match given `file_type`. + + >>> file_list = ['test/test_foo.py', 'setup.py', 'README', 'test/test_bar.py'] + >>> get_files_of_type(file_list, 'test') + ['test/test_foo.py', 'test/test_bar.py'] + """ + return filter(lambda x: discover_file_type(x) == file_type, file_list) + +def get_package_name_from_path(path): + """Get package name as file portion of path. + + >>> get_package_name_from_path('/some/random/path/package.tar.gz') + 'package.tar.gz' + >>> get_package_name_from_path('/path/underscored_name.zip') + 'underscored_name.zip' + >>> get_package_name_from_path('/path/unknown.extension.txt') + 'unknown.extension.txt' + """ + dir, filename = os.path.split(path) + return filename + +def get_package_name_from_url(url): + """Use ``urlparse`` to obtain package name from URL. + + >>> get_package_name_from_url('http://www.example.com/file.tar.bz2') + 'file.tar.bz2' + >>> get_package_name_from_url('https://www.example.com/some/dir/file.txt') + 'file.txt' + """ + (scheme,location,path,param,query,fragment_id) = urlparse(url) + return get_package_name_from_path(path) + +def get_package_name_and_type(package, known_extensions): + """Return package name and type. + + Package type must exists in known_extensions list. Otherwise None is + returned. + + >>> extensions = ['tar.gz', 'zip'] + >>> get_package_name_and_type('underscored_name.zip', extensions) + ('underscored_name', 'zip') + >>> get_package_name_and_type('unknown.extension.txt', extensions) + """ + for package_type in known_extensions: + if package.endswith('.'+package_type): + # Package name is name of package without file extension (ex. twill-7.3). + return package[:package.rfind('.'+package_type)], package_type + +def get_method_arguments(method): + """Return tuple of arguments for given method, excluding self. + + >>> class Class: + ... def method(s, arg1, arg2, other_arg): + ... pass + >>> get_method_arguments(Class.method) + ('arg1', 'arg2', 'other_arg') + """ + return method.func_code.co_varnames[1:method.func_code.co_argcount] + +def get_attributes(obj, names): + """Return attributes dictionary with keys from `names`. + + Object is queried for each attribute name, if it doesn't have this + attribute, default value None will be returned. + + >>> class Class: + ... pass + >>> obj = Class() + >>> obj.attr = True + >>> obj.value = 13 + >>> obj.string = "Hello" + + >>> d = get_attributes(obj, ['attr', 'string', 'other']) + >>> d == {'attr': True, 'string': "Hello", 'other': None} + True + """ + attrs = {} + + for name in names: + attrs[name] = getattr(obj, name, None) + + return attrs + +def camel2underscore(name): + """Convert name from CamelCase to underscore_name. + + >>> camel2underscore('CamelCase') + 'camel_case' + >>> camel2underscore('already_underscore_name') + 'already_underscore_name' + >>> camel2underscore('BigHTMLClass') + 'big_html_class' + >>> camel2underscore('') + '' + """ + if name and name[0].upper: + name = name[0].lower() + name[1:] + + def capitalize(match): + string = match.group(1).lower().capitalize() + return string[:-1] + string[-1].upper() + + def underscore(match): + return '_' + match.group(1).lower() + + name = re.sub(r'([A-Z]+)', capitalize, name) + return re.sub(r'([A-Z])', underscore, name) + +def index_class_to_name(clsname): + """Covert index class name to index name. + + >>> index_class_to_name("IndexDownload") + 'download' + >>> index_class_to_name("IndexUnitTests") + 'unit_tests' + >>> index_class_to_name("IndexPyPIDownload") + 'py_pi_download' + """ + return camel2underscore(clsname.replace('Index', '', 1)) + +def is_empty(path): + """Returns True if file or directory pointed by `path` is empty. + """ + if os.path.isfile(path) and os.path.getsize(path) == 0: + return True + if os.path.isdir(path) and os.listdir(path) == []: + return True + + return False + +def strip_dir_part(path, root): + """Strip `root` part from `path`. + + >>> strip_dir_part('/home/ruby/file', '/home') + 'ruby/file' + >>> strip_dir_part('/home/ruby/file', '/home/') + 'ruby/file' + >>> strip_dir_part('/home/ruby/', '/home') + 'ruby/' + >>> strip_dir_part('/home/ruby/', '/home/') + 'ruby/' + """ + path = path.replace(root, '', 1) + + if path.startswith(os.path.sep): + path = path[1:] + + return path + +def get_files_dirs_list(root): + """Return list of all files and directories below `root`. + + Root directory is excluded from files/directories paths. + """ + files = [] + directories = [] + + for dirpath, dirnames, filenames in os.walk(root): + dirpath = strip_dir_part(dirpath, root) + files.extend(map(lambda x: os.path.join(dirpath, x), filenames)) + directories.extend(map(lambda x: os.path.join(dirpath, x), dirnames)) + + return files, directories + +def length(L): + """Overall length of all strings in list. + + >>> length(['a', 'bc', 'd', '', 'efg']) + 7 + """ + return sum(map(lambda x: len(x), L)) + +def generate_arguments(arguments, max_length): + """Pass list of strings in chunks of size not greater than max_length. + + >>> for x in generate_arguments(['abc', 'def'], 4): + ... print x + ['abc'] + ['def'] + + >>> for x in generate_arguments(['a', 'bc', 'd', 'e', 'f'], 2): + ... print x + ['a'] + ['bc'] + ['d', 'e'] + ['f'] + + If a single argument is larger than max_length, ValueError is raised. + >>> L = [] + >>> for x in generate_arguments(['abc', 'de', 'fghijk', 'l'], 4): + ... L.append(x) + Traceback (most recent call last): + ... + ValueError: Argument 'fghijk' larger than 4. + >>> L + [['abc'], ['de']] + """ + L = [] + i = 0 + + # We have to look ahead, so C-style loop here. + while arguments: + if L == [] and len(arguments[i]) > max_length: + raise ValueError("Argument '%s' larger than %d." % (arguments[i], max_length)) + + L.append(arguments[i]) + + # End of arguments: yield then terminate. + if i == len(arguments) - 1: + yield L + break + + # Adding next argument would exceed max_length, so yield now. + if length(L) + len(arguments[i+1]) > max_length: + yield L + L = [] + + i += 1 + +################################################################################ +## Main index class. +################################################################################ + +class NameSetter(type): + def __init__(cls, name, bases, dict): + if 'name' not in dict: + setattr(cls, 'name', name) + + if 'compute_with' in dict: + orig_compute_with = cls.compute_with + + def _timed_compute_with(self, cheesecake): + (ret, self.time_taken) = time_function(lambda: orig_compute_with(self, cheesecake)) + self.cheesecake.log.debug("Index %s computed in %.2f seconds." % (self.name, self.time_taken)) + return ret + + setattr(cls, 'compute_with', _timed_compute_with) + + def __repr__(cls): + return '' % cls.name + +def make_indices_dict(indices): + indices_dict = {} + for index in indices: + indices_dict[index.name] = index + return indices_dict class Index(object): - """ - Encapsulates index attributes such as name, value, details - """ - - def __init__(self, type, name="", value=0, details=""): - self.type = "index_" + type - self.name = self.type - if name: self.name += "_" + name - self.value = value - self.details = details - + """Class describing one index. + + Use it as a container index or subclass to create custom indices. + + During class initialization, special attribute `name` is magically + set based on class name. See `NameSetter` definitions for details. + """ + __metaclass__ = NameSetter + + subindices = None + + name = "unnamed" + value = -1 + details = "" + info = "" + + def __init__(self, *indices): + # When indices are given explicitly they override the default. + if indices: + self.subindices = [] + self._indices_dict = {} + for index in indices: + self.add_subindex(index) + else: + if self.subindices: + new_subindices = [] + for index in self.subindices: + # index must be a class subclassing from Index. + assert isinstance(index, type) + assert issubclass(index, Index) + new_subindices.append(index()) + self.subindices = new_subindices + else: + self.subindices = [] + # Create dictionary for fast reference. + self._indices_dict = make_indices_dict(self.subindices) + + self._compute_arguments = get_method_arguments(self.compute) + + def _iter_indices(self): + """Iterate over each subindex and yield their values. + """ + for index in self.subindices: + # Pass Cheesecake instance to other indices. + yield index.compute_with(self.cheesecake) + # Print index info after computing. + if not self.cheesecake.quiet: + index.print_info() + + def compute_with(self, cheesecake): + """Take given Cheesecake instance and compute index value. + """ + self.cheesecake = cheesecake + return self.compute(**get_attributes(cheesecake, self._compute_arguments)) + + def compute(self): + """Compute index value and return it. + + By default this method computes sum of all subindices. Override this + method when subclassing for different behaviour. + + Parameters to this function are dynamically prepared with use of + `get_attributes` function. + + :Warning: Don't use \*args and \*\*kwds arguments for this method. + """ + self.value = sum(self._iter_indices()) + return self.value + + def decide(self, cheesecake, when): + """Decide if this index should be computed. + + If index has children, it will automatically remove all for which + decide() return false. + """ + if self.subindices: + # Iterate over copy, as we may remove some elements. + for index in self.subindices[:]: + if not getattr(index, 'decide_' + when)(cheesecake): + self.remove_subindex(index.name) + return self.subindices + return True + + def decide_before_download(self, cheesecake): + return self.decide(cheesecake, 'before_download') + + def decide_after_download(self, cheesecake): + return self.decide(cheesecake, 'after_download') + + def add_info(self, info_line): + """Add information about index computation process, which will + be visible with --verbose flag. + """ + self.info += "[%s] %s\n" % (index_class_to_name(self.name), info_line) + + def _get_max_value(self): + if self.subindices: + return sum(map(lambda index: index.max_value, + self.subindices)) + return 0 + + max_value = property(_get_max_value) + + def _get_requirements(self): + if self.subindices: + return list(self._compute_arguments) + \ + reduce(lambda x,y: x + y.requirements, self.subindices, []) + return list(self._compute_arguments) + + requirements = property(_get_requirements) + + def add_subindex(self, index): + """Add subindex. + + :Parameters: + `index` : Index instance + Index instance for inclusion. + """ + if not isinstance(index, Index): + raise ValueError("subindex have to be instance of Index") + + self.subindices.append(index) + self._indices_dict[index.name] = index + + def remove_subindex(self, index_name): + """Remove subindex (refered by name). + + :Parameters: + `index` : Index name + Index name to be removed. + """ + index = self._indices_dict[index_name] + self.subindices.remove(index) + del self._indices_dict[index_name] + + def _print_info_one(self): + if self.cheesecake.verbose: + sys.stdout.write(self.get_info()) + print "%s (%s)" % (pad_msg(index_class_to_name(self.name), self.value), self.details) + + def _print_info_many(self): + max_value = self.max_value + if max_value == 0: + return + + percentage = int(ceil(float(self.value) / float(max_value) * 100)) + print pad_line("-") + + print pad_msg("%s INDEX (ABSOLUTE)" % self.name, self.value) + msg = pad_msg("%s INDEX (RELATIVE)" % self.name, percentage) + msg += " (%d out of a maximum of %d points is %d%%)" %\ + (self.value, max_value, percentage) + + print msg + print + def print_info(self): - """ - Print index name padded with dots, followed by value and details - """ - msg = pad_with_dots(self.name) - msg += pad_left_spaces(self.value) - msg += " (" + self.details + ")" - print msg - -class CompositeIndex(object): - """ - Collection of indexes of same type (e.g. files, dirs) - """ - - def __init__(self, type): - """ - Indexes is a dict mapping names to Index objects - """ - self.type = type - self.indexes = {} - - def set_index(self, name, value=0, details=""): - """ - Create new index or update existing index with specified attributes - """ - if self.indexes.has_key(name): - index = self.indexes[name] - index.value = value - index.details = details + """Print index name padded with dots, followed by value and details. + """ + if self.subindices: + self._print_info_many() else: - self.indexes[name] = Index(self.type, name, value, details) - - def print_info(self): - """ - Print index info for all indexes sorted alphanumerically by name - """ - names = self.indexes.keys() - names.sort() - for name in names: - index = self.indexes[name] - index.print_info() - - def get_value(self): - """ - Return sum of individual index values - """ + self._print_info_one() + + def __getitem__(self, name): + return self._indices_dict[name] + + def get_info(self): + if self.subindices: + return ''.join(map(lambda index: index.get_info(), self.subindices)) + return self.info + +################################################################################ +## Index that computes scores based on files and directories. +################################################################################ + +class OneOf(object): + def __init__(self, *possibilities): + self.possibilities = possibilities + def __str__(self): + return '/'.join(map(lambda x: str(x), self.possibilities)) + +def WithOptionalExt(name, extensions): + """Handy way of writing Cheese rules for files with extensions. + + Instead of writing: + >>> one_of = OneOf('readme', 'readme.html', 'readme.txt') + + Write this: + >>> opt_ext = WithOptionalExt('readme', ['html', 'txt']) + + It means the same! (representation have a meaning) + >>> str(one_of) == str(opt_ext) + True + """ + possibilities = [name] + possibilities.extend(map(lambda x: name + '.' + x, extensions)) + + return OneOf(*possibilities) + +def Doc(name): + return WithOptionalExt(name, ['html', 'txt']) + +class FilesIndex(Index): + _used_rules = [] + + def _compute_from_rules(self, files_list, package_dir, files_rules): + self._used_rules = [] + files_count = 0 value = 0 - for key in self.indexes.keys(): - index = self.indexes[key] - value += index.value - return value - - value = property(get_value) + + for filename in files_list: + if not is_empty(os.path.join(package_dir, filename)): + score = self.get_score(os.path.basename(filename), files_rules) + if score != 0: + value += score + files_count += 1 + + return files_count, value + + def get_score(self, name, specs): + for entry, value in specs.iteritems(): + if self.match_filename(name, entry): + self.cheesecake.log.debug("%d points entry found: %s (%s)" % \ + (value, name, entry)) + return value + + return 0 + + def get_not_used(self, files_rules): + """Get only these of files_rules that didn't match during computation. + + >>> rules = { + ... Doc('readme'): 30, + ... OneOf(Doc('license'), Doc('copying')): 30, + ... 'demo': 10, + ... } + >>> index = FilesIndex() + >>> index._used_rules.append('demo') + >>> map(lambda x: str(x), index.get_not_used(rules.keys())) + ['license/license.html/license.txt/copying/copying.html/copying.txt', 'readme/readme.html/readme.txt'] + """ + return filter(lambda rule: rule not in self._used_rules, + files_rules) + + def match_filename(self, name, rule): + """Check if `name` matches given `rule`. + """ + def equal(x, y): + x_root, x_ext = os.path.splitext(x) + y_root, y_ext = os.path.splitext(y.lower()) + if x_root in [y_root.lower(), y_root.upper(), y_root.capitalize()] \ + and x_ext in [y_ext.lower(), y_ext.upper()]: + return True + return False + + if rule in self._used_rules: + return False + + if isinstance(rule, basestring): + if equal(name, rule): + self._used_rules.append(rule) + return True + elif isinstance(rule, OneOf): + for poss in rule.possibilities: + if self.match_filename(name, poss): + self._used_rules.append(rule) + return True + + return False + +################################################################################ +## Installability index. +################################################################################ + +class IndexUrlDownload(Index): + """Give points for successful downloading of a package. + """ + max_value = 25 + + def compute(self, downloaded_from_url, package, url): + if downloaded_from_url: + self.details = "downloaded package %s from URL %s" % (package, url) + self.value = self.max_value + else: + self.value = 0 + + return self.value + + def decide_before_download(self, cheesecake): + return cheesecake.url + +class IndexUnpack(Index): + """Give points for successful unpacking of a package archive. + """ + max_value = 25 + + def compute(self, unpacked): + if unpacked: + self.details = "package unpacked successfully" + self.value = self.max_value + else: + self.details = "package couldn't be unpacked" + self.value = 0 + + return self.value + +class IndexUnpackDir(Index): + """Check if package unpack directory resembles package archive name. + """ + max_value = 15 + + def compute(self, unpack_dir, original_package_name): + self.details = "unpack directory is " + unpack_dir + + if original_package_name: + self.details += " instead of the expected " + original_package_name + self.value = 0 + else: + self.details += " as expected" + self.value = self.max_value + + return self.value + + def decide_after_download(self, cheesecake): + return cheesecake.package_type != 'egg' + +class IndexSetupPy(FilesIndex): + """Reward packages that have setup.py file. + """ + name = "setup.py" + max_value = 25 + + files_rules = { + 'setup.py': 25, + } + + def compute(self, files_list, package_dir): + setup_py_found, self.value = self._compute_from_rules(files_list, package_dir, self.files_rules) + + if setup_py_found: + self.details = "setup.py found" + else: + self.details = "setup.py not found" + + return self.value + + def decide_after_download(self, cheesecake): + return cheesecake.package_type != 'egg' + +class IndexInstall(Index): + """Check if package can be installed via "python setup.py" command. + """ + max_value = 50 + + def compute(self, installed, sandbox_install_dir): + if installed: + self.details = "package installed in %s" % sandbox_install_dir + self.value = self.max_value + else: + self.details = "could not install package in %s" % sandbox_install_dir + self.value = 0 + + return self.value + + def decide_before_download(self, cheesecake): + return not cheesecake.static_only + +class IndexPyPIDownload(Index): + """Check if package was successfully downloaded from PyPI + and how far from it actual package was. + + Distance is number of links user have to follow to download + a given software package. + """ + max_value = 50 + distance_penalty = -5 + + def compute(self, package, found_on_cheeseshop, found_locally, distance_from_pypi, download_url): + if download_url: + self.value = self.max_value + + self.details = "downloaded package " + package + + if not found_on_cheeseshop: + self.value += (distance_from_pypi - 1) * self.distance_penalty + + if distance_from_pypi: + self.details += " following %d link" % distance_from_pypi + if distance_from_pypi > 1: + self.details += "s" + self.details += " from PyPI" + else: + self.details += " from " + download_url + else: + self.details += " directly from the Cheese Shop" + else: + if found_locally: + self.details = "found on local filesystem" + self.value = 0 + + return self.value + + def decide_before_download(self, cheesecake): + return cheesecake.name + +class IndexGeneratedFiles(Index): + """Lower score for automatically generated files that should + not be present in a package. + """ + generated_files_penalty = -20 + max_value = 0 + + def compute(self, files_list): + self.value = 0 + + pyc_files = len(get_files_of_type(files_list, 'pyc')) + pyo_files = len(get_files_of_type(files_list, 'pyo')) + + if pyc_files > 0 or pyo_files > 0: + self.value += self.generated_files_penalty + + self.details = "%d .pyc and %d .pyo files found" % \ + (pyc_files, pyo_files) + + return self.value + + def decide_after_download(self, cheesecake): + return cheesecake.package_type != 'egg' + +class IndexInstallability(Index): + name = "INSTALLABILITY" + + subindices = [ + IndexPyPIDownload, + IndexUrlDownload, + IndexUnpack, + IndexUnpackDir, + IndexSetupPy, + IndexInstall, + IndexGeneratedFiles, + ] + +################################################################################ +## Documentation index. +################################################################################ + +class IndexRequiredFiles(FilesIndex): + """Check for existence of important files, like README or INSTALL. + """ + cheese_files = { + Doc('readme'): 30, + OneOf(Doc('license'), Doc('copying')): 30, + + OneOf(Doc('announce'), Doc('changelog'), Doc('changes')): 20, + Doc('install'): 20, + + Doc('authors'): 10, + Doc('faq'): 10, + Doc('news'): 10, + Doc('thanks'): 10, + Doc('todo'): 10, + } + + cheese_dirs = { + OneOf('doc', 'docs'): 30, + OneOf('test', 'tests'): 30, + + OneOf('demo', 'example', 'examples'): 10, + } + + max_value = sum(cheese_files.values() + cheese_dirs.values()) + + def compute(self, files_list, dirs_list, package_dir): + # Inform user of files and directories the package is missing. + def make_info(dictionary, what): + missing = self.get_not_used(dictionary.keys()) + importance = {30: ' critical', 20: ' important'} + + positive_msg = "Package has%s %s: %s." + negative_msg = "Package doesn't have%s %s: %s." + + for key in dictionary.keys(): + msg = positive_msg + if key in missing: + msg = negative_msg + self.add_info(msg % (importance.get(dictionary[key], ''), what, str(key))) + + # Compute required files. + files_count, files_value = self._compute_from_rules(files_list, package_dir, self.cheese_files) + make_info(self.cheese_files, 'file') + + # Compute required directories. + dirs_count, dirs_value = self._compute_from_rules(dirs_list, package_dir, self.cheese_dirs) + make_info(self.cheese_dirs, 'directory') + + self.value = files_value + dirs_value + + self.details = "%d files and %d required directories found" % \ + (files_count, dirs_count) + + return self.value + +class IndexDocstrings(Index): + """Compute how many objects have relevant docstrings. + """ + max_value = 100 + + def compute(self, object_cnt, docstring_cnt): + percent = 0 + if object_cnt > 0: + percent = float(docstring_cnt)/float(object_cnt) + + # Scale the result. + self.value = int(ceil(percent * self.max_value)) + + self.details = "found %d/%d=%.2f%% objects with docstrings" %\ + (docstring_cnt, object_cnt, percent*100) + + return self.value + +class IndexFormattedDocstrings(Index): + """Compute how many of existing docstrings include any formatting, + like epytext or reST. + """ + max_value = 30 + + def compute(self, object_cnt, docformat_cnt): + percent = 0 + if object_cnt > 0: + percent = float(docformat_cnt)/float(object_cnt) + + # Scale the result. + # We give 10p for 25% of formatted docstrings, 20p for 50% and 30p for 75%. + self.value = 0 + if percent > 0.75: + self.add_info("%.2f%% formatted docstrings found, which is > 75%% and is worth 30p." % (percent*100)) + self.value = 30 + elif percent > 0.50: + self.add_info("%.2f%% formatted docstrings found, which is > 50%% and is worth 20p." % (percent*100)) + self.value = 20 + elif percent > 0.25: + self.add_info("%.2f%% formatted docstrings found, which is > 25%% and is worth 10p." % (percent*100)) + self.value = 10 + else: + self.add_info("%.2f%% formatted docstrings found, which is < 25%%, no points given." % (percent*100)) + + self.details = "found %d/%d=%.2f%% objects with formatted docstrings" %\ + (docformat_cnt, object_cnt, percent*100) + + return self.value + +class IndexDocumentation(Index): + name = "DOCUMENTATION" + + subindices = [ + IndexRequiredFiles, + IndexDocstrings, + IndexFormattedDocstrings, + ] + +################################################################################ +## Code "kwalitee" index. +################################################################################ + +class IndexUnitTests(Index): + """Compute unittest index as percentage of methods/functions + that are exercised in unit tests. + """ + max_value = 50 + + def compute(self, files_list, functions, classes, package_dir): + unittest_cnt = 0 + functions_tested = set() + + # Gather all function names called from test files. + for testfile in get_files_of_type(files_list, 'test'): + fullpath = os.path.join(package_dir, testfile) + code = CodeParser(fullpath, self.cheesecake.log.debug) + + functions_tested = functions_tested.union(code.functions_called) + + for name in functions + classes: + if name in functions_tested: + unittest_cnt += 1 + self.cheesecake.log.debug("%s is unit tested" % name) + + functions_classes_cnt = len(functions) + len(classes) + percent = 0 + if functions_classes_cnt > 0: + percent = float(unittest_cnt)/float(functions_classes_cnt) + + # Scale the result. + self.value = int(ceil(percent * self.max_value)) + + self.details = "found %d/%d=%.2f%% unit tested classes/methods/functions." %\ + (unittest_cnt, functions_classes_cnt, percent*100) + + return self.value + +class IndexUnitTested(Index): + """Check if the package have unit tests which can be easily found by + any of known test frameworks. + """ + max_value = 30 + + def compute(self, doctests_count, unittests_count, files_list, classes, methods): + unit_tested = False + + if doctests_count > 0: + self.add_info("Package includes doctest tests.") + unit_tested = True + + if unittests_count > 0: + self.add_info("Package have tests that inherit from unittest.TestCase.") + unit_tested = True + + if get_files_of_type(files_list, 'test'): + self.add_info("Package have filenames which probably contain tests (in format test_* or *_test)") + unit_tested = True + + for method in methods: + if self._is_test_method(method): + self.add_info("Some classes have setUp/tearDown methods which are commonly used in unit tests.") + unit_tested = True + break + + if unit_tested: + self.value = self.max_value + self.details = "has unit tests" + else: + self.value = 0 + self.details = "doesn't have unit tests" + + return self.value + + def _is_test_method(self, method): + nose_methods = ['setup', 'setup_package', 'setup_module', 'setUp', + 'setUpPackage', 'setUpModule', + 'teardown', 'teardown_package', 'teardown_module', + 'tearDown', 'tearDownModule', 'tearDownPackage'] + + for test_method in nose_methods: + if method.endswith(test_method): + return True + return False + +class IndexPyLint(Index): + """Compute pylint index of the whole package. + """ + name = "pylint" + max_value = 50 + + disabled_messages = [ + 'W0403', # relative import + 'W0406', # importing of self + ] + pylint_args = ' '.join(map(lambda x: '--disable-msg=%s' % x, disabled_messages)) + + def compute(self, files_list, package_dir): + # Maximum length of arguments (not very precise). + max_arguments_length = 65536 + + # Exclude __init__.py files from score as they cause pylint + # to fail with ImportError "Unable to find module for %s in %s". + files_to_lint = filter(lambda name: not name.endswith('__init__.py'), + get_files_of_type(files_list, 'module')) + + # Switching cwd so that pylint works correctly regarding + # running it on individual modules. + original_cwd = os.getcwd() + + # Note: package_dir may be a file if the archive contains a single file. + # If this is the case, change dir to the parent dir of that file. + if os.path.isfile(package_dir): + package_dir = os.path.dirname(package_dir) + + os.chdir(package_dir) + + pylint_score = 0 + count = 0 + error_count = 0 + + for filenames in generate_arguments(files_to_lint, max_arguments_length - len(self.pylint_args)): + filenames = ' '.join(filenames) + self.cheesecake.log.debug("Running pylint on files: %s." % filenames) + + rc, output = run_cmd("pylint %s --persistent=n %s" % (filenames, self.pylint_args)) + if rc: + self.cheesecake.log.debug("encountered an error (%d):\n***\n%s\n***\n" % (rc, output)) + error_count += 1 + else: + # Extract score from pylint output. + score_line = output.split("\n")[-3] + s = re.search(r" (-?\d+\.\d+)/10", score_line) + if s: + pylint_score += float(s.group(1)) + count += 1 + + # Switching back to the original cwd. + os.chdir(original_cwd) + + if count: + pylint_score = float(pylint_score)/float(count) + self.details = "pylint score was %.2f out of 10" % pylint_score + elif error_count: + self.details = "encountered an error during pylint execution" + else: + self.details = "no files to check found" + + # Assume scores below zero as zero for means of index value computation. + if pylint_score < 0: + pylint_score = 0 + self.value = int(ceil(pylint_score/10.0 * self.max_value)) + + self.add_info("Score is %.2f/10, which is %d%% of maximum %d points = %d." % + (pylint_score, int(pylint_score*10), self.max_value, self.value)) + + return self.value + + def decide_before_download(self, cheesecake): + # Try to run the pylint script + if not command_successful("pylint --version"): + cheesecake.log.debug("pylint not properly installed, omitting pylint index.") + return False + + return not cheesecake.lite + +class IndexCodeKwalitee(Index): + name = "CODE KWALITEE" + + subindices = [ + IndexPyLint, + #IndexUnitTests, + IndexUnitTested, + ] + +################################################################################ +## Main Cheesecake class. +################################################################################ class CheesecakeError(Exception): - """ - Custom exception class for Cheesecake-specific errors + """Custom exception class for Cheesecake-specific errors. """ pass + +class CheesecakeIndex(Index): + name = "Cheesecake" + subindices = [ + IndexInstallability, + IndexDocumentation, + IndexCodeKwalitee, + ] + + +class Step(object): + """Single step during computation of package score. + """ + def __init__(self, provides): + self.provides = provides + + def decide(self, cheesecake): + """Decide if step should be run. + + It checks if there's at least one index from current profile that need + variables provided by this step. Override this method for other behaviour. + """ + for provide in self.provides: + if provide in cheesecake.index.requirements: + return True + return False + +class StepByVariable(Step): + """Step which is always run if given Cheesecake instance variable is true. + """ + def __init__(self, variable_name, provides): + self.variable_name = variable_name + Step.__init__(self, provides) + + def decide(self, cheesecake): + if getattr(cheesecake, self.variable_name, None): + return True + + # Fallback to the default. + return Step.decide(self, cheesecake) + class Cheesecake(object): - """ - Computes 'goodness' of Python packages + """Computes 'goodness' of Python packages. Generates "cheesecake index" that takes into account things like: @@ -119,150 +1190,124 @@ """ - def __init__(self, name="", url="", path="", sandbox=None, - verbose=False, quiet=False): - """ - Initialize critical variables, download and unpack package, walk package tree - + steps = {} + + package_types = { + "tar.gz": untar_package, + "tgz": untar_package, + "zip": unzip_package, + "egg": unegg_package, + } + + def __init__(self, name="", url="", path="", sandbox=None, + logfile=None, verbose=False, quiet=False, static_only=False, + lite=False, keep_log=False): + """Initialize critical variables, download and unpack package, + walk package tree. """ self.name = name self.url = url self.package_path = path - if not self.name and not self.url and not self.package_path: - self.raise_exception("No package name, URL or path specified ... exiting") - self.sandbox = sandbox or "/tmp/cheesecake_sandbox" + + if self.name: + self.package = self.name + elif self.url: + self.package = get_package_name_from_url(self.url) + elif self.package_path: + self.package = get_package_name_from_path(self.package_path) + else: + self.raise_exception("No package name, URL or path specified... exiting") + + # Setup a sandbox. + self.sandbox = sandbox or tempfile.mkdtemp(prefix='cheesecake') if not os.path.isdir(self.sandbox): os.mkdir(self.sandbox) + self.verbose = verbose self.quiet = quiet - - self.package_types = ["tar.gz", "tgz", "zip"] + self.static_only = static_only + self.lite = lite + self.keep_log = keep_log + self.sandbox_pkg_file = "" self.sandbox_pkg_dir = "" self.sandbox_install_dir = "" - self.determine_pkg_name() - self.configure_logging() - self.set_defaults() - self.get_config() - self.init_indexes() - self.retrieve_pkg() - self.unpack_pkg() - self.walk_pkg() + # Configure logging as soon as possible. + self.configure_logging(logfile) + + # Setup Cheesecake index. + self.index = CheesecakeIndex() + + self.index.decide_before_download(self) + self.log.debug("Profile requirements: %s." % ', '.join(sorted(self.index.requirements))) + + # Get the package. + self.run_step('get_pkg_from_pypi') + self.run_step('download_pkg') + self.run_step('copy_pkg') + + # Get package name and type. + name_and_type = get_package_name_and_type(self.package, self.package_types.keys()) + + if not name_and_type: + msg = "Could not determine package type for package '%s'" % self.package + msg += "\nCurrently recognized types: " + ", ".join(self.package_types.keys()) + self.raise_exception(msg) + + self.package_name, self.package_type = name_and_type + self.log.debug("Package name: " + self.package_name) + self.log.debug("Package type: " + self.package_type) + + # Make last indices decisions. + self.index.decide_after_download(self) + + # Unpack package and list its files. + self.run_step('unpack_pkg') + self.run_step('walk_pkg') + + # Install package. + self.run_step('install_pkg') def raise_exception(self, msg): - """ - Cleanup, print error message and raise CheesecakeError - - Don't use logging, since it can be called before logging has been setup - """ - self.cleanup() - os.unlink(os.path.join(self.sandbox, self.logfile)) - - msg += "\n" + pad_msg("CHEESECAKE INDEX", 0) - raise CheesecakeError(msg) - - def cleanup(self): - """ - Delete temporary directories and files that were - created in the sandbox + """Cleanup, print error message and raise CheesecakeError. + + Don't use logging, since it can be called before logging has been setup. + """ + self.cleanup(remove_log_file=False) + + msg += "\nDetailed info available in log file %s" % self.logfile + + raise CheesecakeError("Error: " + msg) + + def cleanup(self, remove_log_file=True): + """Delete temporary directories and files that were created + in the sandbox. At the end delete the sandbox itself. """ if os.path.isfile(self.sandbox_pkg_file): self.log("Removing file %s" % self.sandbox_pkg_file) os.unlink(self.sandbox_pkg_file) - if os.path.isdir(self.sandbox_pkg_dir): - self.log("Removing directory %s" % self.sandbox_pkg_dir) - shutil.rmtree(self.sandbox_pkg_dir) - if os.path.isdir(self.sandbox_install_dir): - self.log("Removing directory %s" % self.sandbox_install_dir) - shutil.rmtree(self.sandbox_install_dir) - - def set_defaults(self): - """ - Set default values for variables that can also be defined - in the config file - """ - self.INDEX_PYPI_DOWNLOAD = 50 - self.INDEX_PYPI_DISTANCE = 5 - self.INDEX_URL_DOWNLOAD = 25 - self.INDEX_UNPACK = 25 - self.INDEX_UNPACK_DIR = 15 - self.INDEX_INSTALL = 50 - self.INDEX_FILE_CRITICAL = 15 - self.INDEX_FILE = 10 - self.INDEX_REQUIRED_FILES = 100 - self.INDEX_FILE_PYC = -20 - self.INDEX_DIR_CRITICAL = 25 - self.INDEX_DIR = 20 - self.INDEX_DIR_EMPTY = 5 - self.MAX_INDEX_DOCSTRINGS = 100 # max. percentage of modules/classes/methods/functions with docstrings - self.MAX_INDEX_UNITTESTS = 100 # max. percentage of methods/functions that are unit tested - self.MAX_INDEX_PYLINT = 100 # max. pylint score - self.cheese_files = ["readme", "install", "changelog", - "news", "faq", - "todo", "thanks", - "license", "announce", - "setup.py", - ] - self.critical_cheese_files = ["readme", "license", "setup.py"] - self.cheese_dirs = ["doc", "test", "example", "demo"] - self.critical_cheese_dirs = ["doc", "test"] - - def get_config(self): - """ - Retrieve values from configuration file - """ - self.config = get_pkg_config(self.short_pkg_name) - for config_var in ["INDEX_PYPI_DOWNLOAD", "INDEX_PYPI_DISTANCE", - "INDEX_URL_DOWNLOAD", "INDEX_UNPACK", "INDEX_UNPACK_DIR", - "INDEX_INSTALL", "INDEX_FILE_CRITICAL", "INDEX_FILE", - "INDEX_REQUIRED_FILES", "INDEX_FILE_PYC", - "INDEX_DIR_CRITICAL", "INDEX_DIR", "INDEX_DIR_EMPTY", - "MAX_INDEX_DOCSTRINGS", "MAX_INDEX_PYLINT", - "cheese_files", "critical_cheese_files", - "cheese_dirs", "critical_cheese_dirs", - ]: - value = self.config.get(config_var) - if value: setattr(self, config_var, value) - - def determine_pkg_name(self): - if self.name: - self.package = self.name - self.short_pkg_name = self.name - elif self.package_path: - self.package = self.get_package_from_path(self.package_path) + + def delete_dir(dirname): + "Delete directory recursively and generate log message." + if os.path.isdir(dirname): + self.log("Removing directory %s" % dirname) + shutil.rmtree(dirname) + + delete_dir(self.sandbox) + + if remove_log_file and not self.keep_log: + os.unlink(os.path.join(self.sandbox, self.logfile)) + + def configure_logging(self, logfile=None): + """Default settings for logging. + + If verbose, log goes to console, else it goes to logfile. + log.debug and log.info goes to logfile. + log.warn and log.error go to both logfile and stdout. + """ + if logfile: + self.logfile = logfile else: - self.package = self.get_package_from_url() - - def get_package_from_url(self): - """ - Use ``urlparse`` to obtain package path from URL - """ - (scheme,location,path,param,query,fragment_id) = urlparse(self.url) - return self.get_package_from_path(path) - - - def get_package_from_path(self, path): - """ - Get package name as file portion of path - """ - dir, file = os.path.split(path) - self.short_pkg_name = file - for package_type in self.package_types: - s = re.search("(.+)\.%s" % package_type, file) - if s: - self.short_pkg_name = s.group(1) - break - return file - - def configure_logging(self): - """ - Default settings for logging - - if verbose, log goes to console, else it goes to logfile - log.debug goes to logfile - log.info goes to console - log.warn and log.error go to both logfile and stdout - """ - self.logfile = os.path.join(self.sandbox, self.short_pkg_name + ".log") + self.logfile = os.path.join(tempfile.gettempdir(), self.package + ".log") logger.setconsumer('logfile', open(str(self.logfile), 'w', buffering=1)) @@ -270,183 +1315,156 @@ logger.setconsumer('null', None) - if self.verbose: - self.log = logger.MultipleProducer('cheesecake console') - else: - self.log = logger.MultipleProducer('cheesecake logfile') - if self.quiet: - self.log.info = logger.MultipleProducer('cheesecake logfile') - else: - self.log.info = logger.MultipleProducer('cheesecake console') + self.log = logger.MultipleProducer('cheesecake logfile') + self.log.info = logger.MultipleProducer('cheesecake logfile') self.log.debug = logger.MultipleProducer('cheesecake logfile') self.log.warn = logger.MultipleProducer('cheesecake console') self.log.error = logger.MultipleProducer('cheesecake console') - self.log.debug("package = ", self.short_pkg_name) - - def init_indexes(self): - """ - Initialize variables used in index computation - - * cheesecake_index: overall index for the package - * index: dict holding Index or CompositeIndex objects of various types - """ - self.cheesecake_index = 0 - self.cheesecake_index_installability = 0 - self.cheesecake_index_documentation = 0 - self.cheesecake_index_codekwalitee = 0 - self.max_cheesecake_index = self.INDEX_PYPI_DOWNLOAD + \ - self.INDEX_UNPACK + \ - self.INDEX_UNPACK_DIR + \ - self.INDEX_INSTALL + \ - self.MAX_INDEX_DOCSTRINGS + \ - self.MAX_INDEX_PYLINT -# self.MAX_INDEX_UNITTESTS - self.max_cheesecake_index_installability = self.INDEX_PYPI_DOWNLOAD + \ - self.INDEX_UNPACK + \ - self.INDEX_UNPACK_DIR + \ - self.INDEX_INSTALL - self.max_cheesecake_index_documentation = self.INDEX_REQUIRED_FILES + \ - self.MAX_INDEX_DOCSTRINGS - self.max_cheesecake_index_codekwalitee = self.MAX_INDEX_PYLINT -# self.MAX_INDEX_UNITTESTS - - self.index = {} - for index_type in ["file", "dir"]: - self.index[index_type] = CompositeIndex(index_type) - for index_type in ["pypi_download", "url_download", - "unpack_dir", "unpack", "install", - "docstrings", "unittests", "pylint"]: - self.index[index_type] = Index(index_type) - - for cheese_file in self.cheese_files: - self.index["file"].set_index(name=cheese_file, details="file not found") - if cheese_file in self.critical_cheese_files: - self.max_cheesecake_index += self.INDEX_FILE_CRITICAL - self.max_cheesecake_index_documentation += self.INDEX_FILE_CRITICAL - else: - self.max_cheesecake_index += self.INDEX_FILE - self.max_cheesecake_index_documentation += self.INDEX_FILE - self.log.debug("cheese_files: " + ",".join(self.cheese_files)) - self.log.debug("critical_cheese_files: " + ",".join(self.critical_cheese_files)) - - for cheese_dir in self.cheese_dirs: - self.index["dir"].set_index(name=cheese_dir, details="directory not found") - if cheese_dir in self.critical_cheese_dirs: - self.max_cheesecake_index += self.INDEX_DIR_CRITICAL - self.max_cheesecake_index_documentation += self.INDEX_DIR_CRITICAL - else: - self.max_cheesecake_index += self.INDEX_DIR - self.max_cheesecake_index_documentation += self.INDEX_DIR - self.log.debug("cheese_dirs: " + ",".join(self.cheese_dirs)) - self.log.debug("critical_cheese_dirs: " + ",".join(self.critical_cheese_dirs)) - - self.pkg_files = {} - self.pkg_dirs = {} - self.file_types = ["py", "pyc", "test", - ] - for type in self.file_types: - self.pkg_files[type] = [] - - self.object_cnt = 0 # Number of modules/functions/classes/methods in .py files found - self.docstring_cnt = 0 - self.functions = [] # List of methods/functions found in .py files - - def retrieve_pkg(self): - if self.name: - self.get_pkg_from_pypi() - elif self.url: - self.download_pkg() - else: - self.copy_pkg() - - def get_package_from_url(self): - """ - Use ``urlparse`` to obtain package path from URL - """ - (scheme,location,path,param,query,fragment_id) = urlparse(self.url) - return self.get_package_from_path(path) - - - def get_package_from_path(self, path): - """ - Get package name as file portion of path - """ - dir, file = os.path.split(path) - self.short_pkg_name = file - for package_type in self.package_types: - s = re.search("(.+)\.%s" % package_type, file) - if s: - self.short_pkg_name = s.group(1) - break - return file - + def run_step(self, step_name): + """Run step if its decide() method returns True. + """ + step = self.steps[step_name] + if step.decide(self): + step_method = getattr(self, step_name) + step_method() + + steps['get_pkg_from_pypi'] = StepByVariable('name', + ['download_url', + 'distance_from_pypi', + 'found_on_cheeseshop', + 'found_locally', + 'sandbox_pkg_file']) def get_pkg_from_pypi(self): - """ - Download package using setuptools utilities - """ + """Download package using setuptools utilities. + + New attributes: + download_url : str + URL that package was downloaded from. + distance_from_pypi : int + How many hops setuptools had to make to download package. + found_on_cheeseshop : bool + Whenever package has been found on CheeseShop. + found_locally : bool + Whenever package has been already installed. + """ + self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name) + try: - self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name) from setuptools.package_index import PackageIndex from pkg_resources import Requirement from distutils import log - # Temporarily set the log verbosity to INFO so we can capture setuptools info messages - old_threshold = log.set_threshold(log.INFO) - pkgindex = PackageIndex() - old_stdout = sys.stdout - sys.stdout = StdoutRedirector() - output = pkgindex.fetch(Requirement.parse(self.name), - self.sandbox, - force_scan=True, - source=True) - captured_stdout = sys.stdout.read_buffer() - sys.stdout = old_stdout - log.set_threshold(old_threshold) - if output is None: - self.raise_exception("Error: Could not find distribution for " + self.name) - download_url = "" - distance_from_pypi = 0 - #print captured_stdout - for line in captured_stdout.split('\n'): - s = re.search(r"Reading http(.*)", line) - if s: - inspected_url = s.group(1) - if not re.search(r"www.python.org\/pypi", inspected_url): - distance_from_pypi += 1 - continue - s = re.search(r"Downloading (.*)", line) - if s: - download_url = s.group(1) - break - self.sandbox_pkg_file = output - self.package = self.get_package_from_path(output) - self.log.info("Downloaded package %s from %s" % (self.package, download_url)) - index_type = "pypi_download" - found_on_cheeseshop = False - if re.search(r"cheeseshop.python.org", download_url): - value = self.INDEX_PYPI_DOWNLOAD - found_on_cheeseshop = True - else: - value = self.INDEX_PYPI_DOWNLOAD - distance_from_pypi * self.INDEX_PYPI_DISTANCE - self.index[index_type].value = value - details = "downloaded package " + self.package - if found_on_cheeseshop: - details += " directly from the Cheese Shop" - elif distance_from_pypi: - details += " following %d link" % distance_from_pypi - if distance_from_pypi > 1: - details += "s" - details += " from PyPI" - else: - details += "from " + download_url - self.index[index_type].details = details + from distutils.errors import DistutilsError except ImportError, e: - msg = "Error: setuptools is not installed and is required for downloading a package by name\n" - msg += "You can donwload and process a package by its full URL via the -u or --url option\n" + msg = "setuptools is not installed and is required for downloading a package by name\n" + msg += "You can download and process a package by its full URL via the -u or --url option\n" msg += "Example: python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz" self.raise_exception(msg) - + + def drop_setuptools_info(stdout, error=None): + """Drop all setuptools output as INFO. + """ + self.log.info("*** Begin setuptools output") + map(self.log.info, stdout.splitlines()) + if error: + self.log.info(str(error)) + self.log.info("*** End setuptools output") + + def fetch_package(mode): + """Fetch package from PyPI. + + Mode can be one of: + * 'pypi_source': get source package from PyPI + * 'pypi_any': get source/egg package from PyPI + * 'any': get package from PyPI or local filesystem + + Returns tuple (status, output), where `status` is True + if fetch was successful and False if it failed. `output` + is PackageIndex.fetch() return value. + """ + if 'pypi' in mode: + pkgindex = PackageIndex(search_path=[]) + else: + pkgindex = PackageIndex() + + if mode == 'pypi_source': + source = True + else: + source = False + + try: + output = pkgindex.fetch(Requirement.parse(self.name), + self.sandbox, + force_scan=True, + source=source) + return True, output + except DistutilsError, e: + return False, e + + # Temporarily set the log verbosity to INFO so we can capture setuptools + # info messages. + old_threshold = log.set_threshold(log.INFO) + old_stdout = sys.stdout + sys.stdout = StdoutRedirector() + + # Try to get source package from PyPI first, then egg from PyPI, and if + # that fails search in locally installed packages. + for mode, info in [('pypi_source', "source package on PyPI"), + ('pypi_any', "egg on PyPI"), + ('any', "locally installed package")]: + msg = "Looking for %s... " % info + status, output = fetch_package(mode) + if status and output: + self.log.info(msg + "found!") + break + self.log.info(msg + "failed.") + + # Bring back old stdout. + captured_stdout = sys.stdout.read_buffer() + sys.stdout = old_stdout + log.set_threshold(old_threshold) + + # If all runs failed, we must raise an error. + if not status: + drop_setuptools_info(captured_stdout, output) + self.raise_exception("setuptools returned an error: %s\n" % str(output).splitlines()[0]) + + # If fetch returned nothing, package wasn't found. + if output is None: + drop_setuptools_info(captured_stdout) + self.raise_exception("Could not find distribution for " + self.name) + + # Defaults. + self.download_url = "" + self.distance_from_pypi = 0 + self.found_on_cheeseshop = False + self.found_locally = False + + for line in captured_stdout.splitlines(): + s = re.search(r"Reading http(.*)", line) + if s: + inspected_url = s.group(1) + if not re.search(r"www.python.org\/pypi", inspected_url): + self.distance_from_pypi += 1 + continue + s = re.search(r"Downloading (.*)", line) + if s: + self.download_url = s.group(1) + break + + self.sandbox_pkg_file = output + self.package = get_package_name_from_path(output) + self.log.info("Downloaded package %s from %s" % (self.package, self.download_url)) + + if os.path.isdir(self.sandbox_pkg_file): + self.found_locally = True + + if re.search(r"cheeseshop.python.org", self.download_url): + self.found_on_cheeseshop = True + + steps['download_pkg'] = StepByVariable('url', + ['sandbox_pkg_file', + 'downloaded_from_url']) def download_pkg(self): - """ - Use ``urllib.urlretrieve`` to download package to file in sandbox dir + """Use ``urllib.urlretrieve`` to download package to file in sandbox dir. """ #self.log("Downloading package %s from URL %s" % (self.package, self.url)) @@ -458,5 +1476,6 @@ self.raise_exception(str(e)) #self.log("Downloaded package %s to %s" % (self.package, downloaded_filename)) - if re.search("Content-Type: details/html", str(headers)): + + if headers.gettype() in ["text/html"]: f = open(downloaded_filename) if re.search("404 Not Found", "".join(f.readlines())): @@ -464,11 +1483,11 @@ self.raise_exception("Got '404 Not Found' error while trying to download package ... exiting") f.close() - index_type = "url_download" - self.index[index_type].value = self.INDEX_URL_DOWNLOAD - self.index[index_type].details = "downloaded package %s from URL %s" % (self.package, self.url) - + + self.downloaded_from_url = True + + steps['copy_pkg'] = StepByVariable('package_path', + ['sandbox_pkg_file']) def copy_pkg(self): - """ - Copy package file to sandbox directory + """Copy package file to sandbox directory. """ self.sandbox_pkg_file = os.path.join(self.sandbox, self.package) @@ -478,497 +1497,219 @@ shutil.copyfile(self.package_path, self.sandbox_pkg_file) + steps['unpack_pkg'] = Step(['original_package_name', + 'sandbox_pkg_dir', + 'unpacked', + 'unpack_dir']) def unpack_pkg(self): - """ - Unpack the package in the sandbox directory - - Currently supported archive types: - - * .tar.gz (handled with ``tarfile`` module) - * .zip (handled with ``zipfile`` module) - """ - self.package_type = "" - for type in self.package_types: - s = re.search(r"(.+)\.%s" % type, self.package) - if s: - # package_name is name of package without file extension (ex. twill-7.3) - self.package_name = s.group(1) - self.package_type = type - break - if not self.package_type: - msg = "Could not determine package type for package '%s'" % self.package - msg += "\nCurrently recognized types: " + " ".join(self.package_types) - self.raise_exception(msg) - self.log.debug("Package name: " + self.package_name) - self.log.debug("Package type: " + self.package_type) - + """Unpack the package in the sandbox directory. + + Check `package_types` attribute for list of currently supported + archive types. + + New attributes: + original_package_name : str + Package name guessed from the package name. Will be set only + if package name is different than unpacked directory name. + """ self.sandbox_pkg_dir = os.path.join(self.sandbox, self.package_name) if os.path.isdir(self.sandbox_pkg_dir): shutil.rmtree(self.sandbox_pkg_dir) - if self.package_type in ["tar.gz", "tgz"]: - self.untar_pkg() - elif self.package_type == "zip": - self.unzip_pkg() - - index_type = "unpack_dir" - details = "unpack directory is " + self.unpack_dir + # Call appropriate function to unpack the package. + unpack = self.package_types[self.package_type] + self.unpack_dir = unpack(self.sandbox_pkg_file, self.sandbox) + + if self.unpack_dir is None: + self.raise_exception("Could not unpack package %s ... exiting" % \ + self.sandbox_pkg_file) + + self.unpacked = True + if self.unpack_dir != self.package_name: - details += " instead of the expected " + self.package_name + self.original_package_name = self.package_name self.package_name = self.unpack_dir + + steps['walk_pkg'] = Step(['dirs_list', + 'docstring_cnt', + 'docformat_cnt', + 'doctests_count', + 'unittests_count', + 'files_list', + 'functions', + 'classes', + 'methods', + 'object_cnt', + 'package_dir']) + def walk_pkg(self): + """Get package files and directories. + + New attributes: + dirs_list : list + List of directories package contains. + docstring_cnt : int + Number of docstrings found in all package objects. + docformat_cnt : int + Number of formatted docstrings found in all package objects. + doctests_count : int + Number of docstrings that include doctests. + unittests_count : int + Number of classes which inherit from unittest.TestCase. + files_list : list + List of files package contains. + functions : list + List of all functions defined in package sources. + classes : list + List of all classes defined in package sources. + methods : list + List of all methods defined in package sources. + object_cnt : int + Number of documentable objects found in all package modules. + package_dir : str + Path to project directory. + """ + self.package_dir = os.path.join(self.sandbox, self.package_name) + + self.files_list, self.dirs_list = get_files_dirs_list(self.package_dir) + + self.object_cnt = 0 + self.docstring_cnt = 0 + self.docformat_cnt = 0 + self.doctests_count = 0 + self.functions = [] + self.classes = [] + self.methods = [] + self.unittests_count = 0 + + # Parse all application files and count objects + # (modules/classes/functions) and their associated docstrings. + for py_file in get_files_of_type(self.files_list, 'module'): + pyfile = os.path.join(self.package_dir, py_file) + code = CodeParser(pyfile, self.log.debug) + + self.object_cnt += code.object_count() + self.docstring_cnt += code.docstring_count() + self.docformat_cnt += code.formatted_docstrings_count + self.functions += code.functions + self.classes += code.classes + self.methods += code.methods + self.doctests_count += code.doctests_count + self.unittests_count += code.unittests_count + + # Log a bit of debugging info. + self.log.debug("Found %d files: %s." % (len(self.files_list), + ', '.join(self.files_list))) + self.log.debug("Found %d directories: %s." % (len(self.dirs_list), + ', '.join(self.dirs_list))) + + steps['install_pkg'] = Step(['installed']) + def install_pkg(self): + """Verify that package can be installed in alternate directory. + + New attributes: + installed : bool + Describes whenever package has been succefully installed. + """ + self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name) + + if self.package_type == 'egg': + # Create dummy Python directories. + mkdirs('%s/lib/python2.3/site-packages/' % self.sandbox_install_dir) + mkdirs('%s/lib/python2.4/site-packages/' % self.sandbox_install_dir) + + environment = {'PYTHONPATH': + '%(sandbox)s/lib/python2.3/site-packages/:'\ + '%(sandbox)s/lib/python2.4/site-packages/' % \ + {'sandbox': self.sandbox_install_dir}, + # Pass PATH to child process. + 'PATH': os.getenv('PATH')} + rc, output = run_cmd("easy_install --no-deps --prefix %s %s" % \ + (self.sandbox_install_dir, + self.sandbox_pkg_file), + environment) else: - details += " as expected" - self.index[index_type].value = self.INDEX_UNPACK_DIR - self.index[index_type].details = details - - if not self.quiet: - self.log.info("Detailed info available in log file %s" % self.logfile) - - def untar_pkg(self): - """ - Untar the package in the sandbox directory - - Uses tarfile module - """ - try: - t = tarfile.open(self.sandbox_pkg_file) - except tarfile.ReadError, e: - self.raise_exception("Could not read tar file %s ... exiting" % self.sandbox_pkg_file) - - for member in t.getmembers(): - t.extract(member, self.sandbox) - - tarinfo = t.members[0] - self.unpack_dir = tarinfo.name.split(os.sep)[0] - - index_type = "unpack" - self.index[index_type].value = self.INDEX_UNPACK - self.index[index_type].details = "package untar-ed successfully" - - def unzip_pkg(self): - """ - Unzip the package in the sandbox directory - - Uses zipfile module - """ - try: - z = zipfile.ZipFile(self.sandbox_pkg_file) - except zipfile.error: - self.raise_exception("Error unzipping file %s ... exiting" % self.sandbox_pkg_file) - - # Get directory structure from zip and create it in sandbox - for name in z.namelist(): - (dir, file) = os.path.split(name) - unpack_dir = dir - target_dir = os.path.join(self.sandbox, dir) - if not os.path.exists(target_dir): - os.makedirs(target_dir) - - # Extract files to directory structure - for i, name in enumerate(z.namelist()): - if not name.endswith('/'): - outfile = open(os.path.join(self.sandbox, name), 'wb') - outfile.write(z.read(name)) - outfile.flush() - outfile.close() - - self.unpack_dir = unpack_dir.split(os.sep)[0] - - index_type = "unpack" - self.index[index_type].value = self.INDEX_UNPACK - self.index[index_type].details = "package unzipped successfully" - - def walk_pkg(self): - """ - Traverse the file system tree rooted at sandbox/package_name - - * Compute indexes for special files and directories - * Identify Python files, test files, etc. - """ - cwd = os.getcwd() - os.chdir(self.sandbox) - for rootdir, dirs, files in os.walk(self.package_name): - head, tail = os.path.split(rootdir) - dirs_in_rootdir = rootdir.split(os.path.sep) - for cheese_dir in self.cheese_dirs: - if re.search("^%s" % cheese_dir, tail): - if files or dirs: - if cheese_dir in self.critical_cheese_dirs: - value = self.INDEX_DIR_CRITICAL - details = "critical directory found" - self.log.debug("critical_cheese_dir found: " + cheese_dir) - else: - value = self.INDEX_DIR - details = "directory found" - self.log.debug("cheese_dir found: " + cheese_dir) - else: - value = self.INDEX_DIR_EMPTY - details = "empty directory found" - self.log.debug("empty cheese_dir found: " + cheese_dir) - self.index["dir"].set_index(cheese_dir, value, details) - for file in files: - fullpath = os.path.join(rootdir, file) - for cheese_file in self.cheese_files: - if re.search(r"^%s(\.txt)*" % cheese_file, file, re.IGNORECASE): - if cheese_file in self.critical_cheese_files: - value = self.INDEX_FILE_CRITICAL - details = "critical file found" - self.log.debug("critical_cheese_file found: " + cheese_file) - else: - value = self.INDEX_FILE - details = "file found" - self.log.debug("cheese_file found: " + cheese_file) - self.index["file"].set_index(cheese_file, value, details) - - if self.is_py_file(file, dirs_in_rootdir): - self.pkg_files["py"].append(fullpath) - self.log.debug("py file found: " + fullpath) - pyfile = os.path.join(self.sandbox, fullpath) - # Parse the file and count objects (modules/classes/functions) - # and their associated docstrings - code = CodeParser(pyfile, self.log.debug) - self.object_cnt += code.object_count() - self.docstring_cnt += code.docstring_count() - self.functions += code.functions - - if os.path.splitext(file)[1] == ".pyc": - self.pkg_files["pyc"].append(fullpath) - self.log.debug("pyc file found: " + fullpath) - - if self.is_test_file(file, dirs_in_rootdir): - self.pkg_files["test"].append(fullpath) - self.log.debug("test file found: " + fullpath) - - len_pyc_list = len(self.pkg_files["pyc"]) - if len_pyc_list: - self.index["file"].set_index("pyc", value=self.INDEX_FILE_PYC, - details="%d .pyc files found" % len_pyc_list) - self.log.debug("Found %d py files" % len(self.pkg_files["py"])) - self.log.debug("Found %d pyc files" % len(self.pkg_files["pyc"])) - self.log.debug("Found %d test files" % len(self.pkg_files["test"])) - - os.chdir(cwd) - - def is_py_file(self, file, dirs): - """ - Return True if file ends with .py and it is not a special file and it is not - in special directory - """ - if os.path.splitext(file)[1] != ".py": - return False - if file in ["setup.py", "ez_setup.py", "__init__.py", "__pkginfo__.py"]: - return False - for dir in dirs: - if dir.startswith("test") or \ - dir.startswith("docs") or \ - dir.startswith("demo") or \ - dir.startswith("example"): - return False - return True - - def is_test_file(self, file, dirs): - """ - Return True is file is in directory rooted at "test" or "tests" - """ - if not file.endswith(".py"): - return False - if file in ["__init__.py"]: - return False - for dir in dirs: - if dir.startswith("test"): - return True - return False - - def index_file(self): - """ - Return CompositeIndex object of type "file" - """ - return self.index["file"] - - def index_dir(self): - """ - Return CompositeIndex object of type "dir" - """ - return self.index["dir"] - - def index_pypi_download(self): - """ - Verify that package can be downloaded from PyPI - - Return Index object of type "pypi_download" - """ - index_type = "pypi_download" - if self.url: - # Package was downloaded directly from URL - self.index[index_type].value = 0 - self.index[index_type].details = "package was downloaded directly from URL" - - if self.package_path: - # Package was processed from file system path - self.index[index_type].value = 0 - self.index[index_type].details = "package was processed from file system path" - - # Otherwise, index["pypi_download"] was already set in get_pkg_from_pypi() - return self.index["pypi_download"] - - def index_url_download(self): - """ - Verify that package can be downloaded from an URL - - Return Index object of type "download" - """ - # index["download"] is already set in download_pkg() - return self.index["url_download"] - - def index_unpack(self): - """ - Verify that package can be unpacked - - Return Index object of type "unpack" - """ - # index["unpack"] is already set in unpack_pkg() - return self.index["unpack"] - - - def index_unpack_dir(self): - """ - Verify that unpack directory has same name as package - - Return Index object of type "unpack_dir" - """ - # index["unpack_dir"] is already set in unpack_pkg() - return self.index["unpack_dir"] - - def index_install(self): - """ - Verify that package can be installed in alternate directory - - Return Index object of type "install" - """ - index_type = "install" - self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name) - cwd = os.getcwd() - os.chdir(os.path.join(self.sandbox, self.package_name)) - rc, output = run_cmd("python setup.py install --root=" + self.sandbox_install_dir) - if not rc: - # Install succeeded - self.index[index_type].value = self.INDEX_INSTALL - self.index[index_type].details = details="package installed in %s" % self.sandbox_install_dir + package_dir = os.path.join(self.sandbox, self.package_name) + if not os.path.isdir(package_dir): + package_dir = self.sandbox + cwd = os.getcwd() + os.chdir(package_dir) + rc, output = run_cmd("python setup.py install --root=%s" % \ + self.sandbox_install_dir) + os.chdir(cwd) + + if rc: + self.log('*** Installation failed. Captured output:') + # Stringify output as it may be an exception. + for output_line in str(output).splitlines(): + self.log(output_line) + self.log('*** End of captured output.') else: - # Install failed - self.index[index_type].details = "could not install package in %s" % self.sandbox_install_dir - os.chdir(cwd) - return self.index[index_type] - - def index_docstrings(self): - """ - Compute docstring index as percentage of modules/classes/methods/functions - that have docstrings associated with them - - Return Index object of type "docstrings" - """ - index_type = "docstrings" - if self.object_cnt: - percent = float(self.docstring_cnt)/float(self.object_cnt) + self.log('Installation into %s successful.' % \ + self.sandbox_install_dir) + self.installed = True + + def compute_cheesecake_index(self): + """Compute overall Cheesecake index for the package by adding up + specific indexes. + """ + # Recursively compute all indices. + max_cheesecake_index = self.index.max_value + + # Pass Cheesecake instance to the main Index object. + cheesecake_index = self.index.compute_with(self) + percentage = (cheesecake_index * 100) / max_cheesecake_index + + self.log.info("A given package can currently reach a MAXIMUM number of %d points" % max_cheesecake_index) + self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package)) + + # Print summary. + if self.quiet: + print "Cheesecake index: %d (%d / %d)" % (percentage, + cheesecake_index, + max_cheesecake_index) else: - percent = 0 - index_value = int(ceil(percent*100)) - details = "found %d/%d=%.2f%% modules/classes/methods/functions with docstrings" %\ - (self.docstring_cnt, self.object_cnt, percent*100) - self.index[index_type].value = index_value - self.index[index_type].details = details - return self.index[index_type] - - def index_unittests(self): - """ - Compute unittest index as percentage of methods/functions - that are exercised in unit tests - - Return Index object of type "unittests" - """ - unittest_cnt = 0 - index_type = "unittests" - self.functions_tested = {} - for testfile in self.pkg_files["test"]: - fullpath = os.path.join(self.sandbox, testfile) - code = CodeParser(fullpath, self.log.debug) - func_called = code.functions_called() - self.log.debug("Functions called in unit test:") - self.log.debug(func_called) - for func in func_called: - self.functions_tested[func] = 1 - self.log.debug("FUNCTIONS TO BE CHECKED WHETHER THEY ARE UNIT TESTED:") - self.log.debug(self.functions) - self.log.debug("FUNCTIONS THAT ARE UNIT TESTED:") - self.log.debug(self.functions_tested.keys()) - for funcname in self.functions: - if self.is_unit_tested(funcname): - unittest_cnt += 1 - self.log.debug("%s is unit tested" % funcname) - cnt = len(self.functions) - if cnt: - percent = float(unittest_cnt)/float(cnt) - else: - percent = 0 - index_value = int(ceil(percent*100)) - details = "found %d/%d=%.2f%% unit tested methods/functions" % (unittest_cnt, cnt, percent*100) - self.index[index_type].value = index_value - self.index[index_type].details = details - return self.index[index_type] - - def is_unit_tested(self, funcname): - elem = funcname.split(".") - n1 = elem[-1] - n2 = "" - if len(elem) > 1: - n2 = elem[-2] + "." + elem[-1] - for key in self.functions_tested.keys(): - if key.startswith(n1) or (n2 and key.startswith(n2)): - return True - return False - - def index_pylint(self): - """ - Compute pylint index as average of positive pylint scores obtained for - the Python files identified in the package - - Return Index object of type "pylint" - """ - index_type = "pylint" - # Try to run the pylint script - rc, output = run_cmd("pylint --version") - if rc: - # We encountered an error - self.index[index_type].details = "pylint not properly installed" - return self.index[index_type] - index_pylint = 0 - cnt = 0 - for pyfile in self.pkg_files["py"]: - (path, filename) = os.path.split(pyfile) - (module, ext) = os.path.splitext(filename) - if module == "setup" or module == "ez_setup" or module.startswith("__"): - continue - fullpath = os.path.join(self.sandbox, pyfile) - self.log.debug("Running pylint on file " + fullpath) - rc, output = run_cmd("pylint " + fullpath) - if rc: - # We encountered an error - continue - score_line = output.split("\n")[-3] - s = re.search(r" (\d+\.\d+)/10", score_line) - # We only take positive scores into account - if s: - score = s.group(1) - self.log.debug("pylint score for module %s: %s" % (module, score)) - if score == "0.00": - self.log.debug("Ignoring scores of 0.00") - continue - index_pylint += float(score) - cnt += 1 - if cnt: - avg_value = float(index_pylint)/float(cnt) - else: - avg_value = 0 - index_value = int(ceil(avg_value*10)) - self.index[index_type].value = index_value - self.index[index_type].details = "average score is %.2f out of 10" % avg_value - return self.index[index_type] - - def compute_cheesecake_index(self): - """ - Compute overall Cheesecake index for the package by adding up - specific indexes - """ - self.log.info("A given package can currently reach a MAXIMUM number of %d points" % self.max_cheesecake_index) - self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package)) - - index_types = [] - #if self.name: - # index_types.append("pypi_download") - index_types.append("pypi_download") - if self.url: - index_types.append("url_download") - index_types += ["unpack", "unpack_dir", "install"] - self.cheesecake_index_installability = self.process_partial_index("INSTALLABILITY",\ - index_types, self.max_cheesecake_index_installability) - - index_types = ["file", "dir", "docstrings"] - self.cheesecake_index_documentation = self.process_partial_index("DOCUMENTATION",\ - index_types, self.max_cheesecake_index_documentation) - - index_types = [ - #"unittests", - "pylint", - ] - self.cheesecake_index_codekwalitee = self.process_partial_index("CODE KWALITEE",\ - index_types, self.max_cheesecake_index_codekwalitee) - - print - self.print_separator_line("=") - print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", self.cheesecake_index) - percentage = (self.cheesecake_index * 100) / self.max_cheesecake_index - msg = pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage) - msg += " (%d out of a maximum of %d points is %d%%)" %\ - (self.cheesecake_index, self.max_cheesecake_index, percentage) - print msg - self.cleanup() - - return self.cheesecake_index - - def process_partial_index(self, partial_index_name, index_types, max_value): - print - self.log.info("Starting computation of %s index (max. points = %d)" % \ - (partial_index_name, max_value)) - partial_index_value = 0 - for index_type in index_types: - partial_index_value += self.process_index(index_type) - - self.print_separator_line() - print pad_msg("%s INDEX (ABSOLUTE)" % partial_index_name, partial_index_value) - percentage = (partial_index_value * 100) / max_value - msg = pad_msg("%s INDEX (RELATIVE)" % partial_index_name, percentage) - msg += " (%d out of a maximum of %d points is %d%%)" %\ - (partial_index_value, max_value, percentage) - print msg - return partial_index_value - - def process_index(self, index_type): - """ - Compute and print index of specified type - """ - index = self.index[index_type] - index_method = "index_" + index_type - getattr(self, index_method)() - if not self.quiet: - index.print_info() - self.cheesecake_index += index.value - return index.value - - def print_separator_line(self, char="-"): - """ - Print line of text, unless quiet flag was given - """ - if self.quiet: - return - print pad_line(char) - + print + print pad_line("=") + print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", cheesecake_index) + print "%s (%d out of a maximum of %d points is %d%%)" % \ + (pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage), + cheesecake_index, + max_cheesecake_index, + percentage) + + return cheesecake_index + +################################################################################ +## Command line. +################################################################################ def process_cmdline_args(): - """ - Parse command-line options + """Parse command-line options. """ parser = OptionParser() + parser.add_option("--keep-log", action="store_true", dest="keep_log", + default=False, help="don't remove log file even if run was successful") + parser.add_option("--lite", action="store_true", dest="lite", + default=False, help="don't run time-consuming tests (default=False)") + parser.add_option("-l", "--logfile", dest="logfile", + default=None, + help="file to log all cheesecake messages") parser.add_option("-n", "--name", dest="name", default="", help="package name (will be retrieved via setuptools utilities, if present)") + parser.add_option("-p", "--path", dest="path", + default="", help="path of tar.gz/zip package on local file system") + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", + default=False, help="only print Cheesecake index value (default=False)") + parser.add_option("-s", "--sandbox", dest="sandbox", + default=None, + help="directory where package will be unpacked "\ + "(default is to use random directory inside %s)" % tempfile.gettempdir()) + parser.add_option("-t", "--static", action="store_true", dest="static", + default=False, help="don't run any code from the package being tested (default=False)") parser.add_option("-u", "--url", dest="url", default="", help="package URL") - parser.add_option("-p", "--path", dest="path", - default="", help="package path on local file system") - parser.add_option("-s", "--sandbox", dest="sandbox", - default="/tmp/cheesecake_sandbox", - help="directory where package will be unpacked (default=/tmp/cheesecake_sandbox)") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="verbose output (default=False)") - parser.add_option("-q", "--quiet", action="store_true", dest="quiet", - default=False, help="only print Cheesecake index value (default=False)") + parser.add_option("-V", "--version", action="store_true", dest="version", + default=False, help="Output cheesecake version and exit") (options, args) = parser.parse_args() @@ -976,14 +1717,22 @@ def main(): - """ - Display Cheesecake index for package specified via command-line options + """Display Cheesecake index for package specified via command-line options. """ options = process_cmdline_args() + keep_log = options.keep_log + lite = options.lite + logfile = options.logfile name = options.name + path = options.path + quiet = options.quiet + sandbox = options.sandbox + static_only = options.static url = options.url - path = options.path - sandbox = options.sandbox verbose = options.verbose - quiet = options.quiet + version = options.version + + if version: + print "cheesecake version %s" % VERSION + sys.exit(0) if not name and not url and not path: @@ -992,6 +1741,10 @@ try: - c = Cheesecake(name=name, url=url, path=path, sandbox=sandbox, verbose=verbose, quiet=quiet) + c = Cheesecake(name=name, url=url, path=path, sandbox=sandbox, + logfile=logfile, verbose=verbose, + quiet=quiet, static_only=static_only, lite=lite, + keep_log=keep_log) c.compute_cheesecake_index() + c.cleanup() except CheesecakeError, e: print str(e) Index: /trunk/cheesecake/codeparser.py =================================================================== --- /trunk/cheesecake/codeparser.py (revision 11) +++ /trunk/cheesecake/codeparser.py (revision 150) @@ -1,8 +1,100 @@ +import doctest import os +import re + +import logger from model import System, Module, Class, Function, parseFile, processModuleAst + +# Python 2.3/2.4 compatibilty hacks. +if getattr(doctest, 'DocTestParser', False): + # Python 2.4 have DocTestParser class. + get_doctests = doctest.DocTestParser().get_examples +else: + # Python 2.3 have _extract_examples function. + get_doctests = doctest._extract_examples + + +def compile_regex(pattern, user_map=None): + """Compile a regex pattern using default or user mapping. + """ + + # Handy regular expressions. + mapping = {'ALPHA': r'[-.,?!\w]', 'WORD': r'[-.,?!\s\w]', + 'START': r'(^|\s)', 'END': r'([.,?!\s]|$)'} + + if user_map: + mapping = mapping.copy() + mapping.update(user_map) + + def sub(text, mapping): + for From, To in mapping.iteritems(): + text = text.replace(From, To) + return text + + pattern = sub(pattern, mapping) + + return re.compile(pattern, re.LOCALE | re.VERBOSE) + +def inline_markup(start, end=None, mapping=None): + if end is None: + end = start + return compile_regex(r'''(START %(start)s ALPHA %(end)s END) | + (START %(start)s ALPHA WORD* ALPHA %(end)s END)'''\ + % {'start': start, 'end': end}, mapping) + +def line_markup(start, end=None): + return inline_markup(start, end, mapping={'ALPHA': r'[-.,?!\s\w]', + 'START': r'(\n|^)[\ \t]*', + 'END': r''}) + +supported_formats = { + # reST refrence: http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html + 'reST': [ + inline_markup(r'\*'), # emphasis + inline_markup(r'\*\*'), # strong + inline_markup(r'``'), # inline + inline_markup(r'\(', r'_\)', # hyperlink + {'ALPHA': r'\w', 'WORD': r'[-.\w]'}), + inline_markup(r'\(`', r'`_\)'), # long hyperlink + line_markup(r':'), # field + line_markup(r'[*+-]', r''), # unordered list + line_markup(r'((\d+) | ([a-zA-Z]+) [.\)])', r''), # ordered list + line_markup(r'\( ((\d+) | ([a-zA-Z]+)) \)', r''), # ordered list + ], + + # epytext reference: http://epydoc.sourceforge.net/epytext.html + 'epytext': [ + re.compile(r'[BCEGILMSUX]\{.*\}'), # inline elements + line_markup(r'@[a-z]+([\ \t][a-zA-Z]+)?:', r''), # fields + line_markup(r'-', r''), # unordered list + line_markup(r'\d+(\.\d+)*', r''), # ordered list + ], + + # javadoc reference: http://java.sun.com/j2se/1.4.2/docs/tooldocs/solaris/javadoc.html + 'javadoc': [ + re.compile(r'<[a-zA-z]+[^>]*>'), # HTML elements + line_markup(r'@[a-z][a-zA-Z]*\s', r''), # normal tags + re.compile(r'{@ ((docRoot) | (inheritDoc) | (link) | (linkplain) |'\ + ' (value)) [^}]* }', re.VERBOSE), # special tags + ], +} + + +def use_format(text, format): + """Return True if text includes given documentation format + and False otherwise. + + See supported_formats for list of known formats. + """ + for pattern in supported_formats[format]: + if re.search(pattern, text): + return True + + return False + + class CodeParser(object): - """ - Information about the structure of a Python module + """Information about the structure of a Python module. * Collects modules, classes, methods, functions and associated docstrings @@ -10,8 +102,15 @@ """ def __init__(self, pyfile, log=None): + """Initialize Code Parser object. + + :Parameters: + `pyfile` : str + Path to a Python module to parse. + `log` : logger.Producer instance + Logger to use during code parsing. + """ if log: self.log = log.codeparser else: - import logger self.log = logger.default.codeparser self.modules = [] @@ -20,6 +119,14 @@ self.method_func = [] self.functions = [] - self.docstrings = {} - + self.docstrings = [] # objects that have docstrings + self.docstrings_by_format = {} + self.formatted_docstrings_count = 0 + self.doctests_count = 0 + self.unittests_count = 0 + + # Initialize lists of format docstrings. + for format in supported_formats: + self.docstrings_by_format[format] = [] + (path, filename) = os.path.split(pyfile) (module, ext) = os.path.splitext(filename) @@ -29,5 +136,6 @@ try: processModuleAst(parseFile(pyfile), module, self.system) - except: + except Exception, e: + self.log("Code parsing error occured:\n***\n%s\n***" % str(e)) return @@ -35,11 +143,25 @@ fullname = obj.fullName() if isinstance(obj, Module): - self.modules.append(obj.fullName()) + self.modules.append(fullname) if isinstance(obj, Class): - self.classes.append(obj.fullName()) + if 'unittest.TestCase' in obj.bases or 'TestCase' in obj.bases: + self.unittests_count += 1 + self.classes.append(fullname) if isinstance(obj, Function): self.method_func.append(fullname) - if obj.docstring: - self.docstrings[fullname] = 1 + if isinstance(obj.docstring, str) and obj.docstring.strip(): + self.docstrings.append(fullname) + # Check docstring for known documenation formats. + formatted = False + for format in supported_formats: + if use_format(obj.docstring, format): + self.docstrings_by_format[format].append(fullname) + formatted = True + if formatted: + self.formatted_docstrings_count += 1 + + # Check if docstring include any doctests. + if get_doctests(obj.docstring): + self.doctests_count += 1 for method_or_func in self.method_func: @@ -57,9 +179,11 @@ self.log("methods: " + ",".join(self.methods)) self.log("functions: " + ",".join(self.functions)) + self.log("docstrings: %s" % self.docstrings_by_format) + self.log("number of doctests: %d" % self.doctests_count) def object_count(self): - """ - Return number of objects found in this module - + """Return number of objects found in this module. + + Objects include: * module * classes @@ -74,13 +198,18 @@ def docstring_count(self): - """ - Return number of docstrings found in this module - """ - return len(self.docstrings.keys()) - - def functions_called(self): - """ - Return list of functions called by functions/methods - defined in this module + """Return number of docstrings found in this module. + """ + return len(self.docstrings) + + def docstring_count_by_type(self, type): + """Return number of docstrings of given type found in this module. + """ + return len(self.docstrings_by_format[type]) + + def _functions_called(self): + """Return list of functions called by functions/methods + defined in this module. """ return self.system.func_called.keys() + + functions_called = property(_functions_called) Index: /unk/cheesecake/_util.py =================================================================== --- /trunk/cheesecake/_util.py (revision 11) +++ (revision ) @@ -1,99 +1,0 @@ -#!/usr/bin/env python -""" -Cheesecake: How tasty is your code? - -The idea of the Cheesecake project is to rank Python packages -based on various empiric "kwalitee" factors, such as: - - * whether the package can be downloaded - * whether the package can be unpacked - * whether the package can be installed into an alternate directory - * existence of certain files such as README, INSTALL, LICENSE, setup.py etc. - * existence of certain directories such as doc, test, demo, examples - * percentage of modules/functions/classes/methods with docstrings - * percentage of functions/methods that are unit tested - * average pylint score for all non-test and non-demo modules - * whether the package can be unpacked - * whether the package can be installed into an alternate directory -""" - -import os, sys -from subprocess import call, Popen, PIPE, STDOUT - -PAD_TEXT = 40 -PAD_VALUE = 4 - -def run_cmd(cmd): - """ - Run command and return its return code and its output - """ - arglist = cmd.split() - p = Popen(arglist, stdout=PIPE, stderr=STDOUT) - output = p.communicate()[0] - return p.returncode, output - -class StdoutRedirector(object): - """ - Redirect stdout to a temp file - """ - - def __init__(self, filename=None): - if filename: - self.fh = open(filename, 'w') - else: - self.fh = os.tmpfile() - - def write(self, buf): - self.fh.write(buf) - - def flush(self): - self.fh.flush() - - def read_buffer(self): - """ - Return contents of the temp file - """ - self.fh.seek(0) - return self.fh.read() - -def pad_with_dots(msg, length=PAD_TEXT): - """ - Pad text with dots up to given length - """ - length = len(msg) - msg = msg + " " - for i in range(length, PAD_TEXT): - msg += "." - return msg - -def pad_left_spaces(value, length=PAD_VALUE): - """ - Pad value with spaces at left up to given length - """ - msg = "" - diff = length - len(str(value)) - for i in range(diff): - msg += " " - msg += str(value) - return msg - -def pad_msg(msg, value): - """ - Pad message with dots and pad value with spaces - """ - length = len(msg) - msg = msg + " " - for i in range(length, PAD_TEXT): - msg += "." - diff = PAD_VALUE - len(str(value)) - for i in range(diff): - msg += " " - msg += str(value) - return msg - -def pad_line(char="="): - """ - Return line consisting of 'char' characters - """ - msg = char * (PAD_TEXT + PAD_VALUE + 1) - return msg Index: /unk/cheesecake/config.py =================================================================== --- /trunk/cheesecake/config.py (revision 11) +++ (revision ) @@ -1,84 +1,0 @@ -""" -Cheesecake configuration file -""" - -import os, sys - -# Set defaults -_config = dict( - INDEX_PYPI_DOWNLOAD = 50, - INDEX_PYPI_DISTANCE = 5, - INDEX_URL_DOWNLOAD = 25, - INDEX_UNPACK = 25, - INDEX_UNPACK_DIR = 15, - INDEX_INSTALL = 50, - INDEX_FILE_CRITICAL = 15, - INDEX_FILE = 10, - INDEX_REQUIRED_FILES = 100, - INDEX_FILE_PYC = -20, - INDEX_DIR_CRITICAL = 25, - INDEX_DIR = 20, - INDEX_DIR_EMPTY = 5, - MAX_INDEX_DOCSTRINGS = 100, # max. percentage of modules/classes/methods/functions with docstrings - MAX_INDEX_UNITTESTS = 100, # max. percentage of methods/functions that are unit tested - MAX_INDEX_PYLINT = 100, # max. pylint score - cheese_files = ["readme", "install", "changelog", - "news", "faq", "todo", "thanks", - "license", "announce", "setup.py", - ], - critical_cheese_files = ["readme", "license", "setup.py"], - cheese_dirs = ["doc", "test", "example", "demo"], - critical_cheese_dirs = ["doc", "test"], - exclude_files = [], - exclude_dirs = [], -) - -def get_pkg_config(package): - # try getting the user's home directory - homedir = "~" - homedir = os.path.expanduser(homedir) - if homedir is "~": - # can't get it...fall back to defaults - print "Couldn't expand ~ (home directory)" - pass - else: - # check for .cheesecake dir and create it if it's not there - cheesecake_dir = os.path.join(homedir, ".cheesecake") - if not os.path.isdir(cheesecake_dir): - os.mkdir(cheesecake_dir) - # check for config. file and create it if it's not there - cfile = os.path.join(cheesecake_dir, "%s_config.py" % package) - if not os.path.isfile(cfile): - try: - c = open(cfile, 'w') - c.write("my_config = {\n") - keys = _config.keys() - keys.sort() - for key in keys: - c.write("\t'%s': %s,\n" % (key, _config[key])) - c.write("}\n") - c.close() - except OSError, e: - pass - - else: - # if we find the file, update _config with contents of the file - sys.path.insert(0, cheesecake_dir) - try: - from my_config import my_config - _config.update(my_config) - except ImportError, e: - pass - return _config - -def get(key, default=None): - """ - Get the configuration value for the given key. - """ - return _config.get(key, default) - -def set(key, value): - """ - Set the configuration value for the given key. - """ - _config[key] = value Index: /trunk/tests/unit/test_index_unittests.py =================================================================== --- /trunk/tests/unit/test_index_unittests.py (revision 89) +++ /trunk/tests/unit/test_index_unittests.py (revision 89) @@ -0,0 +1,68 @@ +import os +import shutil +import tempfile + +from math import ceil + +import _path_cheesecake + +from cheesecake.cheesecake_index import IndexUnitTests +from cheesecake import logger + + +def dump_str_to_file(string, filename): + fd = file(filename, 'w') + fd.write(string) + fd.close() + + +test_contents = """ +import some_module +import different_module + +class TestSomeFunction: + def test_some_function(self): + value = some_module.some_function(4, 2) + assert value is True + + def test_a_method(self): + self.object = some_module.SomeClass() + + def test_different_module(self): + self.object = different_module.NotTestedClass() + different_module.some_module.some_function() +""" + +class TestUnitTestsIndex(object): + def setUp(self): + self.project_dir = tempfile.mkdtemp() + + def tearDown(self): + if os.path.exists(self.project_dir): + shutil.rmtree(self.project_dir) + + def test_unit_tests_index(self): + test_dir = os.path.join(self.project_dir, 'test') + os.mkdir(test_dir) + + test_filename = os.path.join(test_dir, 'test_some_function.py') + dump_str_to_file(test_contents, test_filename) + + logger.setconsumer('console', logger.STDOUT) + console_log = logger.MultipleProducer('cheesecake console') + + class CheesecakeMockup(object): + files_list = [test_filename] + functions = ["some_module.some_function", + "some_module.other_function"] + classes = ["some_module.SomeClass", + "some_module.NotTestedClass"] + package_dir = self.project_dir + log = console_log + + index = IndexUnitTests() + + index.compute_with(CheesecakeMockup()) + + print "Index: %d/%d -- %s" % (index.value, index.max_value, index.details) + assert index.value == int(ceil(index.max_value * 2.0/4.0)) Index: /trunk/tests/unit/test_index_class.py =================================================================== --- /trunk/tests/unit/test_index_class.py (revision 98) +++ /trunk/tests/unit/test_index_class.py (revision 98) @@ -0,0 +1,86 @@ +""" +Prepare enviornment. + >>> import _path_cheesecake + >>> from cheesecake.cheesecake_index import Index + +***** + +Default maximum value for an index should be 0. + >>> index = Index() + >>> index.max_value + 0 + +To learn a class name, ask for its representation. + >>> Index + + >>> class NamedIndex(Index): + ... pass + >>> NamedIndex + + +***** + +Create two indices. + >>> big_index = Index() + >>> index = Index() + >>> index.name = 'small_index' + +Add one index to another. + >>> big_index.add_subindex(index) + >>> index in big_index.subindices + True + +Try to add non-Index object as a subindex. + >>> big_index.add_subindex(42) + Traceback (most recent call last): + ... + ValueError: subindex have to be instance of Index + +Now remove subindex. + >>> big_index.remove_subindex('small_index') + >>> index in big_index.subindices + False + +***** + +Test passing subindices to index constructor. + >>> def create_index(name): + ... idx = Index() + ... idx.name = name + ... return idx + + >>> index_one = create_index('one') + >>> index_two = create_index('two') + >>> index_three = create_index('three') + >>> index = Index(index_one, index_two, index_three) + + >>> def get_names(indices): + ... return map(lambda idx: idx.name, indices) + + >>> get_names(index.subindices) + ['one', 'two', 'three'] + >>> index.remove_subindex('one') + >>> get_names(index.subindices) + ['two', 'three'] + +***** + +Test requirements. + >>> class NewIndex(Index): + ... def compute(self, one, two, three): + ... pass + >>> new = NewIndex() + >>> new.requirements + ['one', 'two', 'three'] + +Now create other index and add it to the NewIndex. + >>> class OtherIndex(Index): + ... def compute(self, four): + ... pass + >>> other = OtherIndex() + >>> other.requirements + ['four'] + >>> new.add_subindex(other) + >>> new.requirements + ['one', 'two', 'three', 'four'] +""" Index: /trunk/tests/unit/_path_cheesecake.py =================================================================== --- /trunk/tests/unit/_path_cheesecake.py (revision 55) +++ /trunk/tests/unit/_path_cheesecake.py (revision 55) @@ -0,0 +1,4 @@ +import sys, os +testdir = os.path.dirname(__file__) +sys.path.insert(0, os.path.join(testdir, '../../')) + Index: /trunk/tests/unit/test_index_install.py =================================================================== --- /trunk/tests/unit/test_index_install.py (revision 93) +++ /trunk/tests/unit/test_index_install.py (revision 93) @@ -0,0 +1,35 @@ +import os + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH +from cheesecake.cheesecake_index import Cheesecake + + +class TestIndexInstall(object): + def setUp(self): + self.cheesecake = None + + def tearDown(self): + if not self.cheesecake: + return + self.cheesecake.cleanup() + + def test_index_install_correct_package(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "nose-0.8.3.tar.gz")) + + index = self.cheesecake.index["INSTALLABILITY"]["IndexInstall"] + index.compute_with(self.cheesecake) + + assert index.name == "IndexInstall" + assert index.value == index.max_value + assert index.details == "package installed in " + self.cheesecake.sandbox_install_dir + + def test_index_install_incorrect_package(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package1.tar.gz")) + + index = self.cheesecake.index["INSTALLABILITY"]["IndexInstall"] + index.compute_with(self.cheesecake) + + assert index.name == "IndexInstall" + assert index.value == 0 + assert index.details == "could not install package in " + self.cheesecake.sandbox_install_dir Index: /trunk/tests/unit/_mockup_cheesecake.py =================================================================== --- /trunk/tests/unit/_mockup_cheesecake.py (revision 97) +++ /trunk/tests/unit/_mockup_cheesecake.py (revision 97) @@ -0,0 +1,61 @@ + +import os +import shutil +import tempfile + +import _path_cheesecake +from cheesecake.cheesecake_index import Cheesecake +from cheesecake.cheesecake_index import CheesecakeIndex + +from _helper_cheesecake import create_empty_files_in_directory + + +class MockupCheesecakeTest(object): + project_name = 'test_project' + + class CheesecakeMockup(Cheesecake): + def run_step(self, step_name): + if step_name == 'install_pkg': + return + Cheesecake.run_step(self, step_name) + + def __init__(self, sandbox, package_name, logfile): + self.name = package_name + self.package_name = package_name + self.package = package_name + self.sandbox = sandbox + + self.verbose = False + self.quiet = True + + self.unpack_dir = sandbox + + self.configure_logging(logfile) + self.index = CheesecakeIndex() + + def setUp(self): + self.temp_top_dir = tempfile.mkdtemp() + self.temp_project_dir = os.path.join(self.temp_top_dir, self.project_name) + os.mkdir(self.temp_project_dir) + + self._mock_logfile = tempfile.mktemp() + + self.cheesecake = self.CheesecakeMockup(self.temp_top_dir, + self.project_name, + self._mock_logfile) + + def tearDown(self): + shutil.rmtree(self.temp_top_dir) + os.unlink(self._mock_logfile) + + def create_files(self, files): + for f in files: + fd = file(os.path.join(self.temp_top_dir, f), "w") + fd.write('not_empty') + fd.close() + + def create_empty_files(self, files): + create_empty_files_in_directory(files, self.temp_top_dir) + + def prefix_with_package_name(self, files): + return map(lambda x: os.path.join(self.cheesecake.package_name, x), files) Index: /trunk/tests/unit/test_index_unpack_dir.py =================================================================== --- /trunk/tests/unit/test_index_unpack_dir.py (revision 93) +++ /trunk/tests/unit/test_index_unpack_dir.py (revision 93) @@ -0,0 +1,38 @@ +import os + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH +from cheesecake.cheesecake_index import Cheesecake, IndexUnpackDir + + +class IndexUnpackDirTest(object): + def setUp(self): + self.cheesecake = None + + def tearDown(self): + if not self.cheesecake: + return + self.cheesecake.cleanup() + +class TestIndexUnpackDirCorrectPackage(IndexUnpackDirTest): + def test_index_unpack_dir_correct_package(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package1.tar.gz")) + index = self.cheesecake.index["INSTALLABILITY"]["IndexUnpackDir"] + + index.compute_with(self.cheesecake) + + assert index.name == "IndexUnpackDir" + assert index.value == IndexUnpackDir.max_value + assert index.details == "unpack directory is " + self.cheesecake.package_name + " as expected" + +class TestIndexUnpackDirCorrectPackage(IndexUnpackDirTest): + def test_index_unpack_dir_incorrect_package(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package_renamed.tar.gz")) + index = self.cheesecake.index["INSTALLABILITY"]["IndexUnpackDir"] + + index.compute_with(self.cheesecake) + + assert index.name == "IndexUnpackDir" + print index.value, index.max_value + assert index.value == 0 + assert index.details == "unpack directory is package1 instead of the expected package_renamed" Index: /trunk/tests/unit/test_index_pylint.py =================================================================== --- /trunk/tests/unit/test_index_pylint.py (revision 109) +++ /trunk/tests/unit/test_index_pylint.py (revision 109) @@ -0,0 +1,49 @@ +import os +import shutil +import tempfile + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH, Glutton, create_empty_file +from cheesecake.cheesecake_index import IndexPyLint +from cheesecake import logger + + +class TestIndexPyLint(object): + def test_import_self(self): + files_list = ['import_self.py'] + package_dir = DATA_PATH + + index = IndexPyLint() + index.cheesecake = Glutton() + + index.compute(files_list, package_dir) + + # Check that package got maximum score, what means importing self + # is not decreasing the score. + print index.value + assert index.value == index.max_value + + def test_long_file_list(self): + "Test if pylint index works for long files lists." + _package_dir = tempfile.mkdtemp() + _files_list = map(lambda x: 'long_module_with_a_number_%d.py' % x, xrange(4000)) + + for filename in _files_list: + create_empty_file(os.path.join(_package_dir, filename)) + + logger.setconsumer('console', logger.STDOUT) + console_log = logger.MultipleProducer('cheesecake console') + + class CheesecakeMockup(object): + package_dir = _package_dir + files_list = _files_list + log = console_log + + index = IndexPyLint() + cheesecake = CheesecakeMockup() + + index.compute_with(cheesecake) + assert index.details != "encountered an error during pylint execution" + + # Clean up. + shutil.rmtree(_package_dir) Index: /trunk/tests/unit/test_index_docstrings.py =================================================================== --- /trunk/tests/unit/test_index_docstrings.py (revision 93) +++ /trunk/tests/unit/test_index_docstrings.py (revision 93) @@ -0,0 +1,34 @@ +import os +from math import ceil + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH +from cheesecake.cheesecake_index import Cheesecake, CodeParser + + +class TestIndexDocstrings(object): + def setUp(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package2.tar.gz")) + + modules = 5 + classes = 2 + functions = 4 + methods = 3 + + self.documentable_objects = modules + classes + functions + methods + self.docstring_count = 7 + + self.index_float = float(self.docstring_count) / self.documentable_objects + self.index_int = int(ceil(self.index_float*100)) + + def tearDown(self): + self.cheesecake.cleanup() + + def test_index_docstrings(self): + index = self.cheesecake.index["DOCUMENTATION"]["IndexDocstrings"] + index.compute_with(self.cheesecake) + + assert index.name == "IndexDocstrings" + assert index.value == self.index_int + assert index.details == "found %d/%d=%.2f%% objects with docstrings" %\ + (self.docstring_count, self.documentable_objects, self.index_float*100) Index: /trunk/tests/unit/test_index_unpack.py =================================================================== --- /trunk/tests/unit/test_index_unpack.py (revision 146) +++ /trunk/tests/unit/test_index_unpack.py (revision 146) @@ -0,0 +1,65 @@ +import os +import tempfile + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH +from cheesecake.cheesecake_index import Cheesecake, CheesecakeError, pad_msg + +default_temp_directory = os.path.join(tempfile.gettempdir(), 'cheesecake_sandbox') + +class TestIndexUnpack(object): + def setUp(self): + self.cheesecake = None + self.logfile = None + + def tearDown(self): + if self.cheesecake: + self.cheesecake.cleanup() + + if self.logfile: + os.unlink(self.logfile) + + def _run_valid(self, package_file): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, package_file)) + + index = self.cheesecake.index["INSTALLABILITY"]["IndexUnpack"] + index.compute_with(self.cheesecake) + + assert index.name == "IndexUnpack" + assert index.value == index.max_value + assert index.details == "package unpacked successfully" + + def test_index_unpack_valid_tar_gz(self): + self._run_valid("package1.tar.gz") + + def test_index_unpack_valid_tgz(self): + self._run_valid("package1.tgz") + + def test_index_unpack_valid_zip(self): + self._run_valid("package1.zip") + + def _run_invalid(self, package_file): + self.logfile = tempfile.mktemp() + + try: + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, package_file), + sandbox=default_temp_directory, + logfile=self.logfile) + assert 0 # This statement should not be reached + except CheesecakeError, e: + msg = "Error: Could not unpack package %s ... exiting" % \ + os.path.join(default_temp_directory, package_file) + msg += "\nDetailed info available in log file %s" % self.logfile + assert str(e) == msg + + # If run failed log file should not be deleted. + assert os.path.isfile(self.logfile) + + def test_index_unpack_invalid_tar_gz(self): + self._run_invalid("invalid_package.tar.gz") + + def test_index_unpack_invalid_tgz(self): + self._run_invalid("invalid_package.tgz") + + def test_index_unpack_invalid_zip(self): + self._run_invalid("invalid_package.zip") Index: /trunk/tests/unit/test_count_files.py =================================================================== --- /trunk/tests/unit/test_count_files.py (revision 55) +++ /trunk/tests/unit/test_count_files.py (revision 55) @@ -0,0 +1,41 @@ + +import _path_cheesecake +from cheesecake.cheesecake_index import get_files_of_type + +from _mockup_cheesecake import MockupCheesecakeTest +from _helper_cheesecake import set + + +class TestNoTestFiles(MockupCheesecakeTest): + def test_discover_no_test_files(self): + py_files = ['main.py', 'module.py'] + self.create_files(self.prefix_with_package_name(py_files)) + self.cheesecake.walk_pkg() + self.cheesecake.compute_cheesecake_index() + + def get_list(type): + return get_files_of_type(self.cheesecake.files_list, type) + + assert set(get_list('module')) == set(py_files) + assert get_list('pyc') == [] + assert get_list('pyo') == [] + assert get_list('test') == [] + + +class TestPycPyoFiles(MockupCheesecakeTest): + def test_some_pyc_and_pyo_files(self): + py_files = ['main.py'] + pyc_files = ['main.pyc', 'missing.pyc'] + pyo_files = ['main.pyo', 'optimised.pyo'] + + self.create_files(self.prefix_with_package_name(py_files + pyc_files + pyo_files)) + self.cheesecake.walk_pkg() + self.cheesecake.compute_cheesecake_index() + + def get_list(type): + return get_files_of_type(self.cheesecake.files_list, type) + + assert set(get_list('module')) == set(py_files) + assert set(get_list('pyc')) == set(pyc_files) + assert set(get_list('pyo')) == set(pyo_files) + assert get_list('test') == [] Index: /trunk/tests/unit/test_init_cleanup.py =================================================================== --- /trunk/tests/unit/test_init_cleanup.py (revision 66) +++ /trunk/tests/unit/test_init_cleanup.py (revision 66) @@ -0,0 +1,42 @@ +import os +import shutil +import tempfile + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH +from cheesecake.cheesecake_index import Cheesecake + + +class TestInitCleanup(object): + def tearDown(self): + if hasattr(self, 'cheesecake'): + if os.path.isdir(self.cheesecake.sandbox): + shutil.rmtree(self.cheesecake.sandbox) + if hasattr(self, 'logfile'): + if os.path.isfile(self.logfile): + os.unlink(self.logfile) + + def test_init(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package1.tar.gz")) + assert os.path.isdir(self.cheesecake.sandbox_pkg_dir) + assert os.path.isfile(self.cheesecake.sandbox_pkg_file) + self.logfile = self.cheesecake.logfile + assert os.path.isfile(self.logfile) + + def test_init_custom_logfile(self): + self.logfile = tempfile.mktemp() + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package1.tar.gz"), + logfile=self.logfile) + assert os.path.isdir(self.cheesecake.sandbox_pkg_dir) + assert os.path.isfile(self.cheesecake.sandbox_pkg_file) + assert os.path.isfile(self.logfile) + + def test_cleanup_after_install(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "package1.tar.gz")) + self.cheesecake.cleanup() + assert not os.path.exists(self.cheesecake.sandbox_pkg_dir) + assert not os.path.exists(self.cheesecake.sandbox_pkg_file) + assert not os.path.exists(self.cheesecake.sandbox_install_dir) + assert not os.path.exists(self.cheesecake.sandbox) + self.logfile = self.cheesecake.logfile + assert not os.path.isfile(self.logfile) Index: /trunk/tests/unit/test_index_installability.py =================================================================== --- /trunk/tests/unit/test_index_installability.py (revision 80) +++ /trunk/tests/unit/test_index_installability.py (revision 80) @@ -0,0 +1,45 @@ +import os + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH + +from cheesecake.cheesecake_index import Cheesecake +from cheesecake.cheesecake_index import IndexPyPIDownload +from cheesecake.cheesecake_index import IndexUnpack +from cheesecake.cheesecake_index import IndexUnpackDir +from cheesecake.cheesecake_index import IndexSetupPy +from cheesecake.cheesecake_index import IndexInstall +from cheesecake.cheesecake_index import IndexUrlDownload +from cheesecake.cheesecake_index import IndexGeneratedFiles + + +class TestIndexInstallability(object): + def setUp(self): + self.cheesecake = None + + def tearDown(self): + if not self.cheesecake: + return + self.cheesecake.cleanup() + + def test_index_installability_local_path(self): + self.cheesecake = Cheesecake(path=os.path.join(DATA_PATH, "nose-0.8.3.tar.gz")) + + index = self.cheesecake.index["INSTALLABILITY"] + parts = [IndexUnpack, IndexUnpackDir, IndexSetupPy, IndexInstall, IndexGeneratedFiles] + + assert index.max_value == sum(map(lambda x: x.max_value, parts)) + + index.compute_with(self.cheesecake) + assert index.value == sum(map(lambda x: x.max_value, parts)) + + def test_index_installability_url_download(self): + self.cheesecake = Cheesecake(url="http://www.agilistas.org/cheesecake/nose-0.8.3.tar.gz") + + index = self.cheesecake.index["INSTALLABILITY"] + parts = [IndexUrlDownload, IndexUnpack, IndexUnpackDir, IndexSetupPy, IndexInstall, IndexGeneratedFiles] + + assert index.max_value == sum(map(lambda x: x.max_value, parts)) + + index.compute_with(self.cheesecake) + assert index.value == sum(map(lambda x: x.max_value, parts)) Index: /trunk/tests/unit/test_index_unit_tested.py =================================================================== --- /trunk/tests/unit/test_index_unit_tested.py (revision 136) +++ /trunk/tests/unit/test_index_unit_tested.py (revision 136) @@ -0,0 +1,139 @@ +import os +import shutil +import tempfile + +from math import ceil + +import _path_cheesecake + +from cheesecake.cheesecake_index import Cheesecake +from cheesecake.cheesecake_index import IndexUnitTested +from cheesecake import logger + +from _helper_cheesecake import create_empty_files_in_directory + + +def dump_str_to_file(string, filename): + fd = file(filename, 'w') + fd.write(string) + fd.close() + + +main_contents = """ +class SomeClass(object): + '''This is a docstring with doctests. + + >>> print "Hello" + Hello + ''' + def method(self): + 'No doctest here.' + pass + +def function_without_docstring(x): + return x**x +""" + +test_contents = """ +class TestSomeModule: + def setUp(self): + pass + def test_this(self): + pass + def test_that(self): + pass +""" + +unittest_test_contents = """ +class TestThisAndThat(unittest.TestCase): + def test_this(self): + pass +""" + +class TestIndexUnitTested(object): + def setUp(self): + self.sandbox_dir = tempfile.mkdtemp() + + def tearDown(self): + if os.path.exists(self.sandbox_dir): + shutil.rmtree(self.sandbox_dir) + + def test_doctest(self): + "Test unit_tested index with package that uses doctest." + def setup(project_dir): + main_filename = os.path.join(project_dir, 'main.py') + dump_str_to_file(main_contents, main_filename) + + def asserts(cheesecake): + assert cheesecake.functions == ['main.function_without_docstring'] + assert cheesecake.classes == ['main.SomeClass'] + + self._run_it(setup, asserts) + + def _run_it(self, setup=None, asserts=None): + package_name = 'index_test' + + project_dir = os.path.join(self.sandbox_dir, package_name) + os.mkdir(project_dir) + + if setup: + setup(project_dir) + + logger.setconsumer('console', logger.STDOUT) + console_log = logger.MultipleProducer('cheesecake console') + + # Aliasing package_name because of Python optimizations. + # ("package_name = package_name" will simply not work) + pkg_name = package_name + + class CheesecakeMockup(Cheesecake): + def __init__(self): + pass + sandbox = self.sandbox_dir + package_name = pkg_name + log = console_log + + cheesecake = CheesecakeMockup() + cheesecake.walk_pkg() + + if asserts: + asserts(cheesecake) + + index = IndexUnitTested() + index.compute_with(cheesecake) + + # Unit tests presence should be discovered and package should get maximum score. + print "Index: %d/%d -- %s" % (index.value, index.max_value, index.details) + assert index.value == index.max_value + + def test_special_filenames_1(self): + "Test unit_tested index with package that uses test_* filenames." + def setup(project_dir): + files = ['some_module.py', 'README', 'test_some_module.py'] + create_empty_files_in_directory(files, project_dir) + + self._run_it(setup) + + def test_special_filenames_2(self): + "Test unit_tested index with package that uses *_test filenames." + def setup(project_dir): + files = ['some_module.py', 'README', 'some_module_test.py'] + create_empty_files_in_directory(files, project_dir) + + self._run_it(setup) + + def test_special_methods(self): + "Test unit_tested index with package that uses setUp/tearDown methods." + def setup(project_dir): + test_filename = os.path.join(project_dir, 'do_checks.py') + dump_str_to_file(test_contents, test_filename) + + self._run_it(setup) + + def test_unittest_classes(self): + "Test unit_tested index with package that uses unittest library." + def setup(project_dir): + test_filename = os.path.join(project_dir, 'do_checks.py') + dump_str_to_file(unittest_test_contents, test_filename) + + self._run_it(setup) Index: /trunk/tests/unit/test_cheese_files.py =================================================================== --- /trunk/tests/unit/test_cheese_files.py (revision 74) +++ /trunk/tests/unit/test_cheese_files.py (revision 74) @@ -0,0 +1,61 @@ + +from _mockup_cheesecake import MockupCheesecakeTest + + +def readlines_from_file(filename): + fd = open(filename) + lines = fd.readlines() + fd.close() + return lines + + +def cheesefile_in_log(filename, loglines): + return "entry found: %s" % filename in loglines + + +class CheeseFilesTest(MockupCheesecakeTest): + def _do_it(self, files, create_files, inside_log): + create_files(self.prefix_with_package_name(files)) + self.cheesecake.walk_pkg() + self.cheesecake.compute_cheesecake_index() + + loglines = ''.join(readlines_from_file(self._mock_logfile)) + + for filename in files: + print "Checking if %s was counted..." % filename + assert cheesefile_in_log(filename, loglines) is inside_log + +class TestBogusCheeseFiles(CheeseFilesTest): + def test_bogus_filenames(self): + bogus_filenames = ['ReAdMe', + 'install.txt.txt', + 'setupXpy', + 'newsXtxt', + 'todoGARBAGE', + 'setup.py.txt'] + self._do_it(bogus_filenames, self.create_files, False) + +class TestGoodCheeseFiles(CheeseFilesTest): + def test_good_filenames(self): + good_filenames = ['Readme', 'INSTALL.txt', 'setup.py', 'news.TXT'] + self._do_it(good_filenames, self.create_files, True) + +class TestEmptyCheeseFiles(CheeseFilesTest): + def test_empty_filenames(self): + empty_filenames = ['Readme', 'INSTALL', 'setup.py'] + self._do_it(empty_filenames, self.create_empty_files, False) + + +class TestDoubleFiles(MockupCheesecakeTest): + def test_double_files(self): + filenames = ['Readme', 'README.txt'] + + self.create_files(self.prefix_with_package_name(filenames)) + self.cheesecake.walk_pkg() + self.cheesecake.compute_cheesecake_index() + + loglines = ''.join(readlines_from_file(self._mock_logfile)) + + # Make sure that README was counted only once. + assert cheesefile_in_log('Readme', loglines) + assert not cheesefile_in_log('README.txt', loglines) Index: /trunk/tests/unit/_helper_cheesecake.py =================================================================== --- /trunk/tests/unit/_helper_cheesecake.py (revision 97) +++ /trunk/tests/unit/_helper_cheesecake.py (revision 97) @@ -0,0 +1,27 @@ + +import os + +if 'set' not in dir(__builtins__): + from sets import Set as set + +DATA_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../data/')) + +class Glutton(object): + "Eat everything." + def __getattr__(self, name): + return Glutton() + + def __setattr__(self, name, value): + pass + + def __call__(self, *args, **kwds): + pass + + +def create_empty_file(file_path): + fd = file(file_path, "w") + fd.close() + +def create_empty_files_in_directory(files, directory): + for filename in files: + create_empty_file(os.path.join(directory, filename)) Index: /trunk/tests/unit/test_index_url_download.py =================================================================== --- /trunk/tests/unit/test_index_url_download.py (revision 146) +++ /trunk/tests/unit/test_index_url_download.py (revision 146) @@ -0,0 +1,65 @@ +import os +import tempfile + +import _path_cheesecake +from _helper_cheesecake import DATA_PATH +from cheesecake.cheesecake_index import Cheesecake, CheesecakeError, pad_msg, IndexUrlDownload + +default_temp_directory = os.path.join(tempfile.gettempdir(), 'cheesecake_sandbox') + + +class TestIndexInstallability(object): + def setUp(self): + self.cheesecake = None + + def _run_it(self, test_fun): + logfile = tempfile.mktemp() + + try: + test_fun(logfile) + finally: + if self.cheesecake: + self.cheesecake.cleanup() + + if os.path.exists(logfile): + os.unlink(logfile) + + def test_index_url_download_valid_url(self): + urls = [ + "http://www.agilistas.org/cheesecake/nose-0.8.3.tar.gz", + "file://%s" % os.path.join(DATA_PATH, "nose-0.8.3.tar.gz") + ] + + for url in urls: + def test_fun(logfile): + try: + self.cheesecake = Cheesecake(url=url, logfile=logfile) + + index = self.cheesecake.index["INSTALLABILITY"]["IndexUrlDownload"] + index.compute_with(self.cheesecake) + + assert index.name == "IndexUrlDownload" + assert index.value == IndexUrlDownload.max_value + assert index.details == "downloaded package " + \ + self.cheesecake.package + " from URL " + \ + self.cheesecake.url + except CheesecakeError, e: + # it's OK if we get "connection refused" sometimes + msg = "[Errno socket error] (111, 'Connection refused')\n" + msg += "Detailed info available in log file %s" % logfile + assert str(e) == msg + + self._run_it(test_fun) + + def test_index_url_download_invalid_url(self): + def test_fun(logfile): + try: + self.cheesecake = Cheesecake(url="http://www.agilistas.org/cheesecake/not_there.tar.gz", + sandbox=default_temp_directory, logfile=logfile) + assert 0 # This statement should not be reached + except CheesecakeError, e: + msg = "Error: Got '404 Not Found' error while trying to download package ... exiting" + msg += "\nDetailed info available in log file %s" % logfile + assert str(e) == msg + + self._run_it(test_fun) Index: /trunk/tests/unit/test_code_parser.py =================================================================== --- /trunk/tests/unit/test_code_parser.py (revision 66) +++ /trunk/tests/unit/test_code_parser.py (revision 66) @@ -0,0 +1,166 @@ +import os + +import _path_cheesecake +from _helper_cheesecake import set, DATA_PATH + +from cheesecake.codeparser import CodeParser, use_format + + +class TestCodeParser(object): + def setUp(self): + self.code1 = CodeParser(os.path.join(DATA_PATH, "module1.py")) + + def test_modules(self): + assert self.code1.modules == ["module1"] + + def test_classes(self): + assert self.code1.classes == [ + "module1.Class1", + "module1.Class2", + "module1.Class3", + ] + + def test_methods(self): + assert self.code1.methods == [ + "module1.Class1.__init__", + "module1.Class1.__another_method__", + "module1.Class1.method1", + "module1.Class1.method2", + "module1.Class1.method3", + "module1.Class1.method4", + "module1.Class1.method5", + ] + + def test_functions(self): + assert self.code1.functions == [ + "module1.func1", + "module1.func2", + "module1.func3", + "module1.func4", + "module1.__func5__", + "module1.func6", + "module1.func7", + "module1.func8", + "module1.outer_function", + "module1.outer_function.inner_function", + ] + + def test_count(self): + assert self.code1.object_count() == 21 + assert self.code1.docstring_count() == 17 + assert self.code1.docstring_count_by_type('reST') == 2 + assert self.code1.docstring_count_by_type('epytext') == 3 + assert self.code1.docstring_count_by_type('javadoc') == 2 + + def test_docstrings(self): + objects_with_docstrings = [ + "module1", + "module1.Class1", + "module1.Class2", + "module1.Class3", + "module1.Class1.__init__", + "module1.Class1.__another_method__", + "module1.Class1.method1", + "module1.Class1.method2", + "module1.Class1.method3", + "module1.Class1.method5", + "module1.func1", + "module1.func2", + "module1.func3", + "module1.__func5__", + "module1.func7", + "module1.func8", + "module1.outer_function.inner_function", + ] + objects_with_rest_docstrings = [ + "module1.Class1.method5", + "module1.func7", + ] + objects_with_epytext_docstrings = [ + "module1", + "module1.Class3", + "module1.func8", + ] + objects_with_javadoc_docstrings = [ + "module1.Class1", + "module1.func8", # intentional overlap with epytext + ] + + print self.code1.docstrings + + assert set(objects_with_docstrings) == set(self.code1.docstrings) + assert set(objects_with_rest_docstrings) == set(self.code1.docstrings_by_format['reST']) + assert set(objects_with_epytext_docstrings) == set(self.code1.docstrings_by_format['epytext']) + assert set(objects_with_javadoc_docstrings) == set(self.code1.docstrings_by_format['javadoc']) + + +class TestDocumentationFormats(object): + def _do_it(self, format, valid, invalid): + for test in valid: + print "Trying '%s'" % test + assert use_format(test, format) is True + + for test in invalid: + print "Trying '%s'" % test + assert use_format(test, format) is False + + def test_reST(self): + valid_test_strings = [ + "String with *emphasis*.", + "*Multi-word emphasis.*", + "How about testing **strong string**?", + "Some *noisy!* punctuation", + "**characters?**, in the way.", + "Don't forget ``inline literals``.", + "This is reST (hyperlink_).", + "This is (`quite long hyperlink`_).", + "* Bullet\n* List\n", + "+ Another\n+ Bullet\n+ List\n", + "1. Ordered\n2. List\n", + " a) Another\n b) ordered\n c) list\n", + " (a) one\n (b) more", + ":Field: list\n:indeed: it is\n", + ] + invalid_test_strings = [ + "Plain string.", + "Do some math: 2 * 2a* 2 = 8a", + "Not*really*strong.", + "Interpreted `text` is widely used as quotes, so exclude it.", + "Not a :field:.", + ] + + self._do_it('reST', valid_test_strings, invalid_test_strings) + + def test_epytext(self): + valid_test_strings = [ + "- Bullet\n- List\n", + "1. Ordered\n2. List\n", + "1.1 Few points\n1.2 To remember\n", + "Some I{italics} here.", + "And a small bit of C{code}.", + "@param self: You know what it means.", + "@return: Return a long\ndescription.", + ] + invalid_test_strings = [ + "Aha - This is not an unordered list.", + "email@example.com", + "Short Python dictionary: {0: 'zero', 1: 'one'}.", + "@ not a field: at all", + ] + + self._do_it('epytext', valid_test_strings, invalid_test_strings) + + def test_javadoc(self): + valid_test_strings = [ + 'Inline are ugly!', + "Call {@link #test_javadoc(object) test_javadoc} method.", + '@see Why#java(sucks)', + ] + invalid_test_strings = [ + "Normal text.", + "mail.address@example.com", + "Mathematical: a < b < c while x > y.", + "@it: is not javadoc, but epytext!", + ] + + self._do_it('javadoc', valid_test_strings, invalid_test_strings) Index: /trunk/tests/functional/test_options.py =================================================================== --- /trunk/tests/functional/test_options.py (revision 129) +++ /trunk/tests/functional/test_options.py (revision 129) @@ -0,0 +1,101 @@ + +import os +import tempfile + +from _helper_cheesecake import FunctionalTest, read_file_contents, NOSE_PATH, PACKAGE_PATH + +from cheesecake.util import pad_msg +from cheesecake.cheesecake_index import IndexUnpack +from cheesecake.cheesecake_index import IndexUnpackDir +from cheesecake.cheesecake_index import IndexInstall +from cheesecake.cheesecake_index import IndexUrlDownload +from cheesecake.cheesecake_index import IndexPyPIDownload + + +class TestOptions(FunctionalTest): + def test_no_args(self): + self._run_cheesecake('') + + assert self.return_code != 0 + + # Make sure that there's a reference to --help. + stdout = read_file_contents(self.stdout_name) + assert '--help' in stdout + + def test_help(self): + self._run_cheesecake('--help') + + self._assert_success() + + # Make sure usage information has been shown. + stdout = read_file_contents(self.stdout_name) + assert 'usage:' in stdout + + def test_name(self): + self._run_cheesecake('--name nose') + + self._assert_success() + + # Make sure that appropriate indices have been counted. + stdout = read_file_contents(self.stdout_name) + assert pad_msg('unpack', IndexUnpack.max_value) in stdout + assert pad_msg('install', IndexInstall.max_value) in stdout + # PyPI score can be lowered by penalties, so we don't include score check here. + assert 'py_pi_download' in stdout + + def test_url(self): + self._run_cheesecake('--url http://www.agilistas.org/cheesecake/nose-0.8.3.tar.gz') + + self._assert_success() + + # Make sure that appropriate indices have been counted. + stdout = read_file_contents(self.stdout_name) + assert pad_msg('unpack', IndexUnpack.max_value) in stdout + assert pad_msg('unpack_dir', IndexUnpackDir.max_value) in stdout + assert pad_msg('install', IndexInstall.max_value) in stdout + assert pad_msg('url_download', IndexUrlDownload.max_value) in stdout + + def test_path(self): + self._run_cheesecake('--path %s' % NOSE_PATH) + + self._assert_success() + + # Make sure that appropriate indices have been counted. + stdout = read_file_contents(self.stdout_name) + assert pad_msg('unpack', IndexUnpack.max_value) in stdout + assert pad_msg('unpack_dir', IndexUnpackDir.max_value) in stdout + assert pad_msg('install', IndexInstall.max_value) in stdout + + def test_verbose(self): + self._run_cheesecake('--path %s' % PACKAGE_PATH) + normal = read_file_contents(self.stdout_name) + self._cleanup() + + self._run_cheesecake('--path %s --verbose' % PACKAGE_PATH) + verbose = read_file_contents(self.stdout_name) + + # Make sure that --verbose generates more information than default operation. + assert len(verbose) > len(normal) + + def test_quiet(self): + self._run_cheesecake('--path %s' % PACKAGE_PATH) + normal = read_file_contents(self.stdout_name) + self._cleanup() + + self._run_cheesecake('--path %s --quiet' % PACKAGE_PATH) + quiet = read_file_contents(self.stdout_name) + + # Make sure that --quiet generates less information than default operation. + assert len(quiet) < len(normal) + + def test_keep_log(self): + logfile = tempfile.mktemp(prefix='log') + self._run_cheesecake('--path %s --logfile %s --keep-log' % (NOSE_PATH, logfile)) + + self._assert_success() + + # Make sure that log file was left. + assert os.path.exists(logfile) + + # Delete the logfile now. + os.unlink(logfile) Index: /trunk/tests/functional/test_score.py =================================================================== --- /trunk/tests/functional/test_score.py (revision 67) +++ /trunk/tests/functional/test_score.py (revision 67) @@ -0,0 +1,60 @@ + +import os +import re + +from math import ceil + +from _helper_cheesecake import FunctionalTest, read_file_contents, DATA_PATH + +from cheesecake.util import pad_msg + + +class TestScore(FunctionalTest): + def test_required_files(self): + self._run_cheesecake('-p %s' % os.path.join(DATA_PATH, 'required.tar.gz')) + + self._assert_success() + + stdout = read_file_contents(self.stdout_name) + # Files in package: INSTALL, Install.html, README and TODO. + assert '(2 files and 0 required directories found)' in stdout + # One not documented module with a single function with a docstring. + assert '(found 1/2=50.00% objects with docstrings)' in stdout + assert pad_msg('docstrings', 50) in stdout + assert '(found 0/2=0.00% objects with formatted docstrings)' in stdout + + def test_sum(self): + installability_regex = r'INSTALLABILITY INDEX \(RELATIVE\) \.\.\.\.\.\.\.\.\s+(\d+)\s+\((\d+) out of a maximum of (\d+) points is (\d+)%\)' + documentation_regex = r'DOCUMENTATION INDEX \(RELATIVE\) \.\.\.\.\.\.\.\.\.\s+(\d+)\s+\((\d+) out of a maximum of (\d+) points is (\d+)%\)' + code_kwalitee_regex = r'CODE KWALITEE INDEX \(RELATIVE\) \.\.\.\.\.\.\.\.\.\s+(\d+)\s+\((\d+) out of a maximum of (\d+) points is (\d+)%\)' + + self._run_cheesecake('-p %s' % os.path.join(DATA_PATH, 'required.tar.gz')) + + self._assert_success() + + # Check that scores are added up and scaled properly. + stdout = read_file_contents(self.stdout_name) + + installability_match = re.search(installability_regex, stdout) + documentation_match = re.search(documentation_regex, stdout) + code_kwalitee_match = re.search(code_kwalitee_regex, stdout) + + assert installability_match + assert documentation_match + assert code_kwalitee_match + + overall_score = 0 + overall_maximum = 0 + for index in [installability_match, documentation_match, code_kwalitee_match]: + percent_one, current, maximum, percent_two = index.groups() + assert percent_one == percent_two + overall_score += int(current) + overall_maximum += int(maximum) + print "Score: %d/%d" % (int(current), int(maximum)) + + overall_percent = ceil(overall_score / float(overall_maximum)) + + print "Computed overall score: %d" % overall_score + assert 'OVERALL CHEESECAKE INDEX (ABSOLUTE) .... %3d' % overall_score in stdout + assert 'OVERALL CHEESECAKE INDEX (RELATIVE) .... %3d (%d out of a maximum of %d points is %d%%)' % \ + (overall_percent, overall_score, overall_maximum, overall_percent) Index: /trunk/tests/functional/_helper_cheesecake.py =================================================================== --- /trunk/tests/functional/_helper_cheesecake.py (revision 137) +++ /trunk/tests/functional/_helper_cheesecake.py (revision 137) @@ -0,0 +1,56 @@ +import os +import sys +import tempfile + +current_dir = os.path.dirname(__file__) +sys.path.insert(0, os.path.join(current_dir, '../../')) + +try: + import subprocess +except ImportError, ex: + from cheesecake import subprocess + + +CHEESECAKE_PATH = os.path.abspath(os.path.join(current_dir, + '../../cheesecake_index')) +DATA_PATH = os.path.abspath(os.path.join(current_dir, '../data/')) +NOSE_PATH = os.path.join(DATA_PATH, 'nose-0.8.3.tar.gz') +PACKAGE_PATH = os.path.join(DATA_PATH, 'package2.tar.gz') +INVALID_PACKAGE_PATH = os.path.join(DATA_PATH, 'invalid_package.tar.gz') + + +class FunctionalTest(object): + def _run_cheesecake(self, arguments): + self.stdout_fd, self.stdout_name = tempfile.mkstemp(prefix='functional') + self.stderr_fd, self.stderr_name = tempfile.mkstemp(prefix='functional') + self.process = subprocess.Popen('%s %s' % (CHEESECAKE_PATH, arguments), + stdout=self.stdout_fd, + stderr=self.stderr_fd, + shell=True) + self.return_code = self.process.wait() + + def _assert_success(self): + # Check that Cheesecake exited sucessfully. + print "Return code: %d" % self.return_code + assert self.return_code == 0 + + # Check that Cheesecake didn't wrote anything into stderr. + stderr_contents = read_file_contents(self.stderr_name) + print "Stderr contents:\n***\n%s\n***\n" % stderr_contents + assert stderr_contents == '' + + def _cleanup(self): + os.unlink(self.stdout_name) + os.unlink(self.stderr_name) + + def tearDown(self): + self._cleanup() + + +def read_file_contents(filename): + fd = file(filename) + + contents = fd.read() + fd.close() + + return contents Index: /trunk/tests/functional/test_one_file.py =================================================================== --- /trunk/tests/functional/test_one_file.py (revision 77) +++ /trunk/tests/functional/test_one_file.py (revision 77) @@ -0,0 +1,12 @@ +import os + +from _helper_cheesecake import FunctionalTest, read_file_contents, DATA_PATH + + +class TestOneFile(FunctionalTest): + def test_one_file(self): + "Make sure that archives with one file are handled properly." + self._run_cheesecake('-p %s' % (os.path.join(DATA_PATH, 'module1.tar.gz'))) + + self._assert_success() + Index: /trunk/tests/functional/test_logfile.py =================================================================== --- /trunk/tests/functional/test_logfile.py (revision 64) +++ /trunk/tests/functional/test_logfile.py (revision 64) @@ -0,0 +1,34 @@ + +import os +import tempfile + +from _helper_cheesecake import FunctionalTest, read_file_contents, PACKAGE_PATH, INVALID_PACKAGE_PATH + + +class TestLogfile(FunctionalTest): + def setUp(self): + self.logfile = tempfile.mktemp(prefix='log') + + def tearDown(self): + super(self.__class__, self).tearDown() + if os.path.exists(self.logfile): + os.unlink(self.logfile) + + def test_valid_package(self): + self._run_cheesecake('--path %s --logfile %s' % \ + (PACKAGE_PATH, self.logfile)) + + self._assert_success() + + # After successful installation there's no need to keep a logfile. + assert not os.path.exists(self.logfile) + + def test_broken_package(self): + self._run_cheesecake('--path %s --logfile %s' % \ + (INVALID_PACKAGE_PATH, self.logfile)) + + # Package is broken, but error was handled. + self._assert_success() + + # Unpacking failed - logfile should have been left. + assert os.path.exists(self.logfile) Index: /trunk/tests/functional/test_cleaning_up.py =================================================================== --- /trunk/tests/functional/test_cleaning_up.py (revision 131) +++ /trunk/tests/functional/test_cleaning_up.py (revision 131) @@ -0,0 +1,67 @@ +from glob import glob +import os +import tempfile + +from _helper_cheesecake import FunctionalTest, read_file_contents, NOSE_PATH, INVALID_PACKAGE_PATH + + +def filter_our_files(files): + uid = os.getuid() + return filter(lambda filename: os.lstat(filename).st_uid == uid, files) + +def get_tmp_files_starting_with(prefix): + return glob(os.path.join(tempfile.gettempdir(), prefix + "*")) + +def get_tmp_files(): + return get_tmp_files_starting_with(tempfile.gettempprefix()) + +def get_cheesecake_files(): + return get_tmp_files_starting_with("cheesecake") + + +class TestCleaningUp(FunctionalTest): + def setUp(self): + self.temp_files = filter_our_files(get_tmp_files()) + self.cheesecake_files = filter_our_files(get_cheesecake_files()) + self.sandbox = tempfile.mkdtemp() + self.logfile = tempfile.mktemp(prefix='log') + + def test_valid_no_tmp(self): + "Check that no files are left in temp by Cheesecake." + self._run_cheesecake('-p %s -s %s -l %s' % (NOSE_PATH, self.sandbox, self.logfile)) + + self._assert_success() + + # Check that Cheesecake didn't leave sandbox. + assert not os.path.exists(self.sandbox) + + # Check that log file has been removed. + assert not os.path.exists(self.logfile) + + # Check that Cheesecake didn't leave any cheesecake* files. + assert filter_our_files(get_cheesecake_files()) == self.cheesecake_files + + # Check that Cheesecake didn't leave any new tmp* files. + assert filter_our_files(get_tmp_files()) == self.temp_files + + def test_invalid_no_tmp(self): + "Check that no files are left in temp by Cheesecake during scoring an invalid package." + self._run_cheesecake('-p %s -s %s -l %s' % (INVALID_PACKAGE_PATH, self.sandbox, self.logfile)) + + # Package cannot be unpacked, but error was handled and scores, so no error here. + self._assert_success() + + # Check that Cheesecake didn't leave sandbox. + assert not os.path.exists(self.sandbox) + + # Check that Cheesecake didn't leave any cheesecake* files. + assert filter_our_files(get_cheesecake_files()) == self.cheesecake_files + + # Check that Cheesecake didn't leave any new tmp* files. + assert filter_our_files(get_tmp_files()) == self.temp_files + + # Delete the log file, so that it doesn't pollute /tmp + self._cleanup_logfile() + + def _cleanup_logfile(self): + os.unlink(self.logfile) Index: /trunk/tests/functional/test_static.py =================================================================== --- /trunk/tests/functional/test_static.py (revision 79) +++ /trunk/tests/functional/test_static.py (revision 79) @@ -0,0 +1,22 @@ + +import os +import tempfile + +from _helper_cheesecake import FunctionalTest, DATA_PATH + + +special_file_name = os.path.join(tempfile.gettempdir(), 'cheesecake_special') + + +class TestStatic(FunctionalTest): + def test_static(self): + self._run_cheesecake('-p %s --static' % os.path.join(DATA_PATH, 'static.tar.gz')) + + self._assert_success() + assert not os.path.exists(special_file_name) + + def tearDown(self): + self._cleanup() + + if os.path.exists(special_file_name): + os.unlink(special_file_name) Index: /trunk/tests/data/import_self.py =================================================================== --- /trunk/tests/data/import_self.py (revision 150) +++ /trunk/tests/data/import_self.py (revision 150) @@ -0,0 +1,9 @@ +"""This module will get W0406 warning for importing self, +which should be ignored in Cheesecake score. +""" + +__revision__ = 'satisfy pylint checker' + +import import_self + +print import_self.__revision__ # use imported module Index: /trunk/tests/data/module1.py =================================================================== --- /trunk/tests/data/module1.py (revision 8) +++ /trunk/tests/data/module1.py (revision 150) @@ -1,4 +1,6 @@ """ Docstring for module1 + +@summary: Code used inside test_code_parser.py unit test. """ @@ -6,9 +8,11 @@ """ Docstring for Class1 + + @see how.Tests#are(performed) """ def __init__(self): """ - Methods starting with __ are not kipped + Methods starting with __ are not skipped """ pass @@ -36,4 +40,11 @@ pass + def method5(self): + """Method with few definitions. + + :Word: And its definition. + """ + pass + class Class2: @@ -42,4 +53,5 @@ """ pass + def func1(): @@ -66,2 +78,36 @@ """ return + +def func6(): + """ + """ + pass + +def func7(): + "Time to get *a bit* of reST." + pass + +def func8(argument): + """This is test function for the epytext parser. + + @param argument: And you really can't say if this is + epytext or javadoc! We count both. + """ + pass + + +class Class3(object): + """ + New-style class with epytext link: U{http://pycheesecake.org}. + """ + pass + + +def outer_function(*args): + x = 42 + + def inner_function(): + """Short docstring.""" + pass + + return x Index: /unk/tests/test_init_cleanup.py =================================================================== --- /trunk/tests/test_init_cleanup.py (revision 2) +++ (revision ) @@ -1,34 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestInitCleanup: - - def test_init(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - self.logfile = os.path.join(self.cheesecake.sandbox, self.cheesecake.logfile) - assert os.path.isdir(self.cheesecake.sandbox_pkg_dir) - assert os.path.isfile(self.cheesecake.sandbox_pkg_file) - assert os.path.isfile(self.logfile) - - def test_cleanup(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - self.logfile = os.path.join(self.cheesecake.sandbox, self.cheesecake.logfile) - self.cheesecake.cleanup() - assert not os.path.exists(self.cheesecake.sandbox_pkg_dir) - assert not os.path.exists(self.cheesecake.sandbox_pkg_file) - # Log file should not have been deleted - assert os.path.isfile(self.logfile) - - def test_cleanup_after_install(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - self.cheesecake.index_install() - self.logfile = os.path.join(self.cheesecake.sandbox, self.cheesecake.logfile) - self.cheesecake.cleanup() - assert not os.path.exists(self.cheesecake.sandbox_pkg_dir) - assert not os.path.exists(self.cheesecake.sandbox_pkg_file) - assert not os.path.exists(self.cheesecake.sandbox_install_dir) - # Log file should not have been deleted - assert os.path.isfile(self.logfile) - Index: /unk/tests/test_index_installability.py =================================================================== --- /trunk/tests/test_index_installability.py (revision 8) +++ (revision ) @@ -1,39 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestIndexInstallability: - - def setUp(self): - self.cheesecake = None - - def tearDown(self): - if not self.cheesecake: - return - self.cheesecake.cleanup() - os.unlink(self.cheesecake.logfile) - - def test_index_installability_local_path(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "nose-0.8.3.tar.gz")) - assert self.cheesecake.max_cheesecake_index_installability == \ - self.cheesecake.INDEX_PYPI_DOWNLOAD + self.cheesecake.INDEX_UNPACK + \ - self.cheesecake.INDEX_UNPACK_DIR + self.cheesecake.INDEX_INSTALL - - index_types = ["pypi_download", "unpack", "unpack_dir", "install"] - cheesecake_index_installability = self.cheesecake.process_partial_index("INSTALLABILITY",\ - index_types, self.cheesecake.max_cheesecake_index_installability) - assert cheesecake_index_installability == self.cheesecake.INDEX_UNPACK + \ - self.cheesecake.INDEX_UNPACK_DIR + self.cheesecake.INDEX_INSTALL - - def test_index_installability_url_download(self): - self.cheesecake = Cheesecake(url="http://www.agilistas.org/cheesecake/nose-0.8.3.tar.gz") - assert self.cheesecake.max_cheesecake_index_installability == \ - self.cheesecake.INDEX_PYPI_DOWNLOAD + self.cheesecake.INDEX_UNPACK + \ - self.cheesecake.INDEX_UNPACK_DIR + self.cheesecake.INDEX_INSTALL - index_types = ["pypi_download", "url_download", "unpack", "unpack_dir", "install"] - cheesecake_index_installability = self.cheesecake.process_partial_index("INSTALLABILITY",\ - index_types, self.cheesecake.max_cheesecake_index_installability) - assert cheesecake_index_installability == \ - self.cheesecake.INDEX_URL_DOWNLOAD + self.cheesecake.INDEX_UNPACK + \ - self.cheesecake.INDEX_UNPACK_DIR + self.cheesecake.INDEX_INSTALL Index: /unk/tests/_path_cheesecake.py =================================================================== --- /trunk/tests/_path_cheesecake.py (revision 2) +++ (revision ) @@ -1,4 +1,0 @@ -import sys, os -testdir = os.path.dirname(__file__) -sys.path.insert(0, os.path.join(testdir, '../')) - Index: /unk/tests/test_index_install.py =================================================================== --- /trunk/tests/test_index_install.py (revision 8) +++ (revision ) @@ -1,29 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestIndexInstall: - - def setUp(self): - self.cheesecake = None - - def tearDown(self): - if not self.cheesecake: - return - self.cheesecake.cleanup() - os.unlink(self.cheesecake.logfile) - - def test_index_install_correct_package(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "nose-0.8.3.tar.gz")) - index = self.cheesecake.index_install() - assert index.name == "index_install" - assert index.value == self.cheesecake.INDEX_INSTALL - assert index.details == "package installed in " + self.cheesecake.sandbox_install_dir - - def test_index_install_incorrect_package(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - index = self.cheesecake.index_install() - assert index.name == "index_install" - assert index.value == 0 - assert index.details == "could not install package in " + self.cheesecake.sandbox_install_dir Index: /unk/tests/test_index_url_download.py =================================================================== --- /trunk/tests/test_index_url_download.py (revision 8) +++ (revision ) @@ -1,39 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake, CheesecakeError, pad_msg - -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestIndexInstallability: - - def setUp(self): - self.cheesecake = None - - def tearDown(self): - if not self.cheesecake: - return - self.cheesecake.cleanup() - os.unlink(self.cheesecake.logfile) - - def test_index_url_download_valid_url(self): - try: - self.cheesecake = Cheesecake(url="http://www.agilistas.org/cheesecake/nose-0.8.3.tar.gz") - index = self.cheesecake.index_url_download() - assert index.name == "index_url_download" - assert index.value == self.cheesecake.INDEX_URL_DOWNLOAD - assert index.details == "downloaded package " + self.cheesecake.package + " from URL " + self.cheesecake.url - except CheesecakeError, e: - # it's OK if we get "connection refused" sometimes - msg = "[Errno socket error] (111, 'Connection refused')" - msg += pad_msg("CHEESECAKE INDEX", 0) - if str(e) == msg: - pass - - def test_index_url_download_invalid_url(self): - try: - self.cheesecake = Cheesecake(url="http://www.agilistas.org/cheesecake/not_there.tar.gz") - assert 0 # This statement should not be reached - except CheesecakeError, e: - msg = "Could not read tar file /tmp/cheesecake_sandbox/not_there.tar.gz ... exiting\n" - msg += pad_msg("CHEESECAKE INDEX", 0) - assert str(e) == msg Index: /unk/tests/test_config.py =================================================================== --- /trunk/tests/test_config.py (revision 11) +++ (revision ) @@ -1,27 +1,0 @@ -import os, sys, shutil -import _path_cheesecake - -class TestCheesecakeConfig: - - def setUp(self): - # try getting the user's home directory - homedir = "~" - homedir = os.path.expanduser(homedir) - self.cheesecake_dir = os.path.join(homedir, ".cheesecake") - - def test_no_custom_config(self): - # remove custom .cheesecake dir if it exists - if os.path.isdir(self.cheesecake_dir): - shutil.rmtree(self.cheesecake_dir) - from cheesecake import config - pkg_name = "my" - pkg_config_file = os.path.join(self.cheesecake_dir, "my_config.py") - config = config.get_pkg_config(pkg_name) - # the .cheesecke dir and my_config.py file should have been created - assert os.path.isdir(self.cheesecake_dir) - assert os.path.isfile(pkg_config_file) - # my_config should contain default values - sys.path.insert(0, self.cheesecake_dir) - from my_config import my_config - for key, value in my_config.items(): - assert config.get(key) == value Index: /unk/tests/test_code_parser.py =================================================================== --- /trunk/tests/test_code_parser.py (revision 11) +++ (revision ) @@ -1,47 +1,0 @@ -import _path_cheesecake -from cheesecake.codeparser import CodeParser -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestCodeParser: - def setUp(self): - self.code1 = CodeParser(os.path.join(datadir, "module1.py")) - - def test_modules(self): - assert self.code1.modules == ["module1"] - - def test_classes(self): - assert self.code1.classes == ["module1.Class1", "module1.Class2"] - - def test_methods(self): - assert self.code1.methods== ["module1.Class1.__init__", - "module1.Class1.__another_method__", - "module1.Class1.method1", - "module1.Class1.method2", - "module1.Class1.method3", - "module1.Class1.method4"] - - def test_functions(self): - assert self.code1.functions == [ - "module1.func1", - "module1.func2", - "module1.func3", - "module1.func4", - "module1.__func5__"] - - def test_count(self): - assert self.code1.object_count() == 14 - assert self.code1.docstring_count() == 12 - - def test_docstrings(self): - print self.code1.docstrings - assert self.code1.docstrings.get("module1") == 1 - for object in ["module1.Class1", "module1.Class2"]: - assert self.code1.docstrings.get(object) == 1 - for object in ["module1.Class1.__init__", "module1.Class1.__another_method__",\ - "module1.Class1.method1", "module1.Class1.method2", "module1.Class1.method3"]: - assert self.code1.docstrings.get(object) == 1 - assert not self.code1.docstrings.get("module1.Class1.method4") - for object in ["module1.func1", "module1.func2", "module1.func3", "module1.__func5__"]: - assert self.code1.docstrings.get(object) == 1 - assert not self.code1.docstrings.get("module1.func4") Index: /unk/tests/test_index_docstrings.py =================================================================== --- /trunk/tests/test_index_docstrings.py (revision 2) +++ (revision ) @@ -1,22 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake, CodeParser -import os -from math import ceil -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestIndexDocstrings: - def setUp(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - self.code = CodeParser(os.path.join(self.cheesecake.sandbox, "package1/module1.py")) - self.index_float = float(self.code.docstring_count()) / float(self.code.object_count()) - self.index_int = int(ceil(self.index_float*100)) - - def tearDown(self): - self.cheesecake.cleanup() - - def test_index_docstrings(self): - index = self.cheesecake.index_docstrings() - assert index.name == "index_docstrings" - assert index.value == self.index_int - assert index.details == "found %d/%d=%.2f%% modules/classes/methods/functions with docstrings" %\ - (self.code.docstring_count(), self.code.object_count(), self.index_float*100) Index: /unk/tests/test_index_unpack_dir.py =================================================================== --- /trunk/tests/test_index_unpack_dir.py (revision 8) +++ (revision ) @@ -1,29 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestIndexUnpackDir: - - def setUp(self): - self.cheesecake = None - - def tearDown(self): - if not self.cheesecake: - return - self.cheesecake.cleanup() - os.unlink(self.cheesecake.logfile) - - def test_index_unpack_dir_correct_package(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - index = self.cheesecake.index_unpack_dir() - assert index.name == "index_unpack_dir" - assert index.value == self.cheesecake.INDEX_UNPACK_DIR - assert index.details == "unpack directory is " + self.cheesecake.package_name + " as expected" - - def test_index_unpack_dir_incorrect_package(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package_renamed.tar.gz")) - index = self.cheesecake.index_unpack_dir() - assert index.name == "index_unpack_dir" - assert index.value == 0 - assert index.details == "unpack directory is package1 instead of the expected package_renamed" Index: /unk/tests/test_index_unpack.py =================================================================== --- /trunk/tests/test_index_unpack.py (revision 8) +++ (revision ) @@ -1,63 +1,0 @@ -import _path_cheesecake -from cheesecake.cheesecake_index import Cheesecake, CheesecakeError, pad_msg -import os -datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) - -class TestIndexUnpack: - - def setUp(self): - self.cheesecake = None - - def tearDown(self): - if not self.cheesecake: - return - self.cheesecake.cleanup() - os.unlink(self.cheesecake.logfile) - - def test_index_unpack_valid_tar_gz(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tar.gz")) - index = self.cheesecake.index_unpack() - assert index.name == "index_unpack" - assert index.value == self.cheesecake.INDEX_UNPACK - assert index.details == "package untar-ed successfully" - - def test_index_unpack_valid_tgz(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.tgz")) - index = self.cheesecake.index_unpack() - assert index.name == "index_unpack" - assert index.value == self.cheesecake.INDEX_UNPACK - assert index.details == "package untar-ed successfully" - - def test_index_unpack_valid_zip(self): - self.cheesecake = Cheesecake(path=os.path.join(datadir, "package1.zip")) - index = self.cheesecake.index_unpack() - assert index.name == "index_unpack" - assert index.value == self.cheesecake.INDEX_UNPACK - assert index.details == "package unzipped successfully" - - def test_index_unpack_invalid_tar_gz(self): - try: - self.cheesecake = Cheesecake(path=os.path.join(datadir, "invalid_package.tar.gz")) - assert 0 # This statement should not be reached - except CheesecakeError, e: - msg = "Could not read tar file /tmp/cheesecake_sandbox/invalid_package.tar.gz ... exiting\n" - msg += pad_msg("CHEESECAKE INDEX", 0) - assert str(e) == msg - - def test_index_unpack_invalid_tgz(self): - try: - self.cheesecake = Cheesecake(path=os.path.join(datadir, "invalid_package.tgz")) - assert 0 # This statement should not be reached - except CheesecakeError, e: - msg = "Could not read tar file /tmp/cheesecake_sandbox/invalid_package.tgz ... exiting\n" - msg += pad_msg("CHEESECAKE INDEX", 0) - assert str(e) == msg - - def test_index_unpack_invalid_zip(self): - try: - self.cheesecake = Cheesecake(path=os.path.join(datadir, "invalid_package.zip")) - assert 0 # This statement should not be reached - except CheesecakeError, e: - msg = "Error unzipping file /tmp/cheesecake_sandbox/invalid_package.zip ... exiting\n" - msg += pad_msg("CHEESECAKE INDEX", 0) - assert str(e) == msg Index: /trunk/AUTHORS =================================================================== --- /trunk/AUTHORS (revision 150) +++ /trunk/AUTHORS (revision 150) @@ -0,0 +1,2 @@ +Grig Gheorghiu +Michal Kwiatkowski Index: /trunk/INSTALL =================================================================== --- /trunk/INSTALL (revision 150) +++ /trunk/INSTALL (revision 150) @@ -0,0 +1,39 @@ +Cheesecake can be installed in two ways: either via downloading a +Cheesecake package and installing it yourself or using easy_install. + +============ +Requirements +============ + +To make Cheesecake score maximally reliable make sure you have +both `pylint `_ and +`setuptools `_ +installed. + +=================== +Manual installation +=================== + +If you have downloaded a Cheesecake package, follow this steps: + +1) Unpack it:: + + tar zxf cheesecake-0.6.tar.gz + +2) Change to the cheesecake directory:: + + cd cheesecake-0.6/ + +3) Run setup.py that will install Cheesecake automatically. This step + need to be run as root:: + + python setup.py install + +================== +Using easy_install +================== + +If you have easy_install on your system, installing Cheesecake is as +simple as running: + + easy_install cheesecake Index: /trunk/setup.py =================================================================== --- /trunk/setup.py (revision 5) +++ /trunk/setup.py (revision 150) @@ -1,20 +1,27 @@ #! /usr/bin/env python import sys -import os.path +import os from setuptools import setup from pkg_resources import require +from cheesecake import __version__ as VERSION + +# Instruct nose to use doctests. +os.environ['NOSE_WITH_DOCTEST'] = 'True' +os.environ['NOSE_DOCTEST_TESTS'] = 'True' +os.environ['NOSE_INCLUDE'] = 'unit' +os.environ['NOSE_INCLUDE_EXE'] = 'True' setup( name = 'Cheesecake', - version = '0.1', + version = VERSION, # metadata for upload to PyPI - author = "Grig Gheorghiu", - author_email = "grig@gheorghiu.net", + author = "Grig Gheorghiu and Michal Kwiatkowski", + author_email = "grig@gheorghiu.net and ruby@joker.linuxstuff.pl", description = 'Computes "goodness" index for Python packages based on various empirical "kwalitee" factors', license = "PSF", keywords = "cheesecake quality index kwalitee cheeseshop pypi", - url = "http://tracos.org/cheesecake", + url = "http://pycheesecake.org/", packages = ['cheesecake', @@ -22,4 +29,9 @@ scripts = ['cheesecake_index', ], + entry_points = { + 'console_scripts': [ + 'cheesecake_index = cheesecake.cheesecake_index:main', + ] + }, test_suite = 'nose.collector', - ) +) Index: /trunk/CHANGES =================================================================== --- /trunk/CHANGES (revision 150) +++ /trunk/CHANGES (revision 150) @@ -0,0 +1,12 @@ +Version 0.6 + + * Recognition of three docstring formatting syntaxes: ReST, epytext and + javadoc. + * Ability to score egg packages. + * Implementation of --static and --lite command line switches for running + different subsets of indices. + * Speeded up pylint execution. + * Cheesecake recognizes usage of unit test frameworks. + * Added lots of missing unit and functional tests. + * Major refactoring of Cheesecake class and indices classes. + * Implemented --keep-log option. Index: /trunk/THANKS =================================================================== --- /trunk/THANKS (revision 150) +++ /trunk/THANKS (revision 150) @@ -0,0 +1,1 @@ +Thanks to Will Guaraldi for his great support, patches, bug reports and lots of useful comments. Index: /trunk/support/score_pypi.py =================================================================== --- /trunk/support/score_pypi.py (revision 72) +++ /trunk/support/score_pypi.py (revision 72) @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Compute Cheesecake scores for all packages on PyPI. +# + +import datetime +import os +import re +import sys +import time +import urllib2 + +current_dir = os.path.dirname(__file__) +sys.path.insert(0, os.path.join(current_dir, '../')) + +try: + import subprocess +except ImportError, ex: + from cheesecake import subprocess + + +CHEESECAKE_PATH = os.path.abspath(os.path.join(current_dir, + '../cheesecake_index')) + +LOG_PATH = '/tmp/cheesecake_pypi_results' + + +def read_file_contents(filename): + fd = file(filename) + + contents = fd.read() + fd.close() + + return contents + +def replace_chars(string): + replacements = {'%20': '_', + '%27': "\\'", + '%28': '\\(', + '%29': '\\)', + '%2A': '\\*', + '%3A': ':', + '%3F': '\\?', + '%C3%B1': 'ñ', + } + + for From, To in replacements.iteritems(): + string = string.replace(From, To) + + return string + +def get_package_names(): + """Get list of all packages on PyPI. + + For each package return (name, version) tuple. + """ + package_regex = r'' + + pypi = urllib2.urlopen("http://python.org/pypi?%3Aaction=index") + html_lines = pypi.readlines() + pypi.close() + + for line in html_lines: + m = re.search(package_regex, line) + if m: + # To make setuptools download a package, convert all spaces to undescores. + yield (replace_chars(m.group(1)), replace_chars(m.group(2))) + +def score_one_package(package_name, log_template): + """Score one package leaving information in logs along the way. + + :Logs: + * .stdout -> Cheesecake stdout + * .stderr -> Cheesecake stderr + * .log -> Cheesecake log for given package + """ + log_file = log_template % 'log' + + stdout_fd = file(log_template % 'stdout', 'w') + stderr_fd = file(log_template % 'stderr', 'w') + + process = subprocess.Popen('%s -l %s -n %s' % \ + (CHEESECAKE_PATH, log_file, package_name), + stdout=stdout_fd, + stderr=stderr_fd, + shell=True) + + result = process.wait() + + stdout_fd.close() + stderr_fd.close() + + if result == 0: + score_regex = r'OVERALL CHEESECAKE INDEX \(RELATIVE\) \.\.\.\.\s+([\d]+)' + stdout = read_file_contents(log_template % 'stdout') + m = re.search(score_regex, stdout) + if m: + return int(m.group(1)) + + return -1 + +def time2datetime(t): + t = time.localtime(t) + return datetime.datetime(t.tm_year, t.tm_mon, t.tm_mday, + t.tm_hour, t.tm_min, t.tm_sec) + +def time_delta(start, end): + return str(time2datetime(end) - time2datetime(start)) + +def score_all_packages(): + packages_failed = [] + packages_scores = [] + + if not os.path.exists(LOG_PATH): + os.mkdir(LOG_PATH) + + for name, version in get_package_names(): + name_and_version = '%s-%s' % (name, version) + log_template = os.path.join(LOG_PATH, name_and_version + '.%s') + start = time.time() + result = score_one_package('%s==%s' % (name, version), log_template) + end = time.time() + if result == -1: + packages_failed.append(name_and_version) + else: + packages_scores.append((name_and_version, result, time_delta(start, end))) + + print "=== Packages that Cheesecake failed to score ===" + for failed in packages_failed: + print failed + + print + print "=== All packages scores ===" + # Sorty by score. + packages_scores.sort(lambda x,y: cmp(x[1], y[1])) + + for name, score, timing in packages_scores: + print "%s SCORE:%s (in %s time)" % (name, score, timing) + + print + print "=== Summary ===" + print "Checked %d packages in overall." % (len(packages_scores) + len(packages_failed)) + print "Failed for %d." % len(packages_failed) + print "%d packages got more than 50%% Cheesecake score." % len(filter(lambda x: x[1] > 50, packages_scores)) + + +if __name__ == '__main__': + score_all_packages() + Index: /trunk/support/master.cfg =================================================================== --- /trunk/support/master.cfg (revision 138) +++ /trunk/support/master.cfg (revision 138) @@ -0,0 +1,201 @@ +# -*- python -*- +# ex: set syntax=python: + +# This is a sample buildmaster config file. It must be installed as +# 'master.cfg' in your buildmaster's base directory (although the filename +# can be changed with the --basedir option to 'mktap buildbot master'). + +# It has one job: define a dictionary named BuildmasterConfig. This +# dictionary has a variety of keys to control different aspects of the +# buildmaster. They are documented in docs/config.xhtml . + +import os.path +from buildbot.changes.freshcvs import FreshCVSSource +from buildbot.scheduler import Scheduler, Periodic, Nightly +from buildbot.process import step, factory +from buildbot.status import html +s = factory.s + +import secret + +# This is the dictionary that the buildmaster pays attention to. We also use +# a shorter alias to save typing. +c = BuildmasterConfig = {} + +# the 'bots' list defines the set of allowable buildslaves. Each element is a +# tuple of bot-name and bot-password. These correspond to values given to the +# buildslave's mktap invocation. +c['bots'] = [("soc_x86_rh9", secret.password)] + + +# the 'sources' list tells the buildmaster how it should find out about +# source code changes. Any class which implements IChangeSource can be added +# to this list: there are several in buildbot/changes/*.py to choose from. + +c['sources'] = [] + +# For example, if you had CVSToys installed on your repository, and your +# CVSROOT/freshcfg file had an entry like this: +#pb = ConfigurationSet([ +# (None, None, None, PBService(userpass=('foo', 'bar'), port=4519)), +# ]) + +# then you could use the following buildmaster Change Source to subscribe to +# the FreshCVS daemon and be notified on every commit: +# +#fc_source = FreshCVSSource("cvs.example.com", 4519, "foo", "bar") +#c['sources'].append(fc_source) + +# or, use a PBChangeSource, and then have your repository's commit script run +# 'buildbot sendchange', or contrib/svn_buildbot.py, or +# contrib/arch_buildbot.py : +# +#from buildbot.changes.pb import PBChangeSource +#c['sources'].append(PBChangeSource()) + + +## configure the Schedulers + +c['schedulers'] = [ + Nightly("every_6_hours", ["x86_rh9_trunk"], hour=[9,15,21,3], minute=0), + Nightly("every_6_hours+10min", ["x86_rh9_mk"], hour=[9,15,21,3], minute=10), +] + + +# the 'builders' list defines the Builders. Each one is configured with a +# dictionary, using the following keys: +# name (required): the name used to describe this bilder +# slavename (required): which slave to use, must appear in c['bots'] +# builddir (required): which subdirectory to run the builder in +# factory (required): a BuildFactory to define how the build is run +# periodicBuildTime (optional): if set, force a build every N seconds + +# buildbot/process/factory.py provides several BuildFactory classes you can +# start with, which implement build processes for common targets (GNU +# autoconf projects, CPAN perl modules, etc). The factory.BuildFactory is the +# base class, and is configured with a series of BuildSteps. When the build +# is run, the appropriate buildslave is told to execute each Step in turn. + +# the first BuildStep is typically responsible for obtaining a copy of the +# sources. There are source-obtaining Steps in buildbot/process/step.py for +# CVS, SVN, and others. + +make_source = lambda branch: s(step.SVN, mode='update', + baseURL='http://svn.pycheesecake.org/', + defaultBranch=branch) + +source_mk = make_source('branches/mk/') +source_trunk = make_source('trunk') + +class StepBuildEggInfo(step.ShellCommand): + name = "build egg info" + description = ["building egg info"] + descriptionDone = [name] + +build_egg_info = s(StepBuildEggInfo, command="python setup.py egg_info") + +class StepUnitTest(step.ShellCommand): + name = "unit tests" + description = ["running unit tests"] + descriptionDone = [name] + +unit_tests = s(StepUnitTest, command="nosetests --exe --with-doctest --doctest-tests --with-coverage --verbose --include unit") + +class StepFunctionalTest(step.ShellCommand): + name = "functional tests" + description = ["running functional tests"] + descriptionDone = [name] + +functional_tests = s(StepFunctionalTest, command="nosetests --verbose --include functional") + +class EpyDocBuild(step.ShellCommand): + name = "documentation" + description = ["building documentation"] + descriptionDone = [name] + +epydoc_build = lambda doc_dest: s(EpyDocBuild, + command="/bin/sh support/generate_docs.sh %s" % doc_dest) + +class CoverageBuild(step.ShellCommand): + name = "coverage" + description = ["building coverage statistics"] + descriptionDone = [name] + +coverage_build = lambda doc_dest: s(CoverageBuild, + command="/bin/sh support/generate_coverage.sh %s" % doc_dest) + +make_factory = lambda source, doc_dest: factory.BuildFactory([ + source, + build_egg_info, + unit_tests, + functional_tests, + epydoc_build(doc_dest), + coverage_build(doc_dest)]) + +f_mk = make_factory(source_mk, "/var/www/agilistas/cheesecake/mk") +f_trunk = make_factory(source_trunk, "/var/www/agilistas/cheesecake/trunk") + +c['builders'] = [ + {'name': 'x86_rh9_trunk', + 'slavename': 'soc_x86_rh9', + 'builddir': 'test-trunk', + 'factory': f_trunk + }, + {'name':'x86_rh9_mk', + 'slavename':'soc_x86_rh9', + 'builddir':'test-mk', + 'factory':f_mk + }, +] + +# 'slavePortnum' defines the TCP port to listen on. This must match the value +# configured into the buildslaves (with their --master option) + +c['slavePortnum'] = secret.port + +# 'status' is a list of Status Targets. The results of each build will be +# pushed to these targets. buildbot/status/*.py has a variety to choose from, +# including web pages, email senders, and IRC bots. + +c['status'] = [] +c['status'].append(html.Waterfall(http_port=8888)) + +# from buildbot.status import mail +# c['status'].append(mail.MailNotifier(fromaddr="buildbot@localhost", +# extraRecipients=["builds@example.com"], +# sendToInterestedUsers=False)) +# from buildbot.status import words +# c['status'].append(words.IRC(host="irc.example.com", nick="bb", +# channels=["#example"])) + + +# if you set 'debugPassword', then you can connect to the buildmaster with +# the diagnostic tool in contrib/debugclient.py . From this tool, you can +# manually force builds and inject changes, which may be useful for testing +# your buildmaster without actually commiting changes to your repository (or +# before you have a functioning 'sources' set up). The debug tool uses the +# same port number as the slaves do: 'slavePortnum'. + +#c['debugPassword'] = "debugpassword" + +# if you set 'manhole', you can telnet into the buildmaster and get an +# interactive python shell, which may be useful for debugging buildbot +# internals. It is probably only useful for buildbot developers. +#from buildbot.master import Manhole +#c['manhole'] = Manhole(9999, "admin", "password") + +# the 'projectName' string will be used to describe the project that this +# buildbot is working on. For example, it is used as the title of the +# waterfall HTML page. The 'projectURL' string will be used to provide a link +# from buildbot HTML pages to your project's home page. + +c['projectName'] = "Cheesecake" +c['projectURL'] = "http://pycheesecake.org/" + +# the 'buildbotURL' string should point to the location where the buildbot's +# internal web server (usually the html.Waterfall page) is visible. This +# typically uses the port number set in the Waterfall 'status' entry, but +# with an externally-visible host name which the buildbot cannot figure out +# without some help. + +c['buildbotURL'] = "http://agilistas.org:8888" Index: /trunk/support/generate_docs.sh =================================================================== --- /trunk/support/generate_docs.sh (revision 121) +++ /trunk/support/generate_docs.sh (revision 121) @@ -0,0 +1,16 @@ +#!/bin/sh + +PATH=$PATH:/usr/bin:/usr/local/bin + +# Generate documentation for Cheesecake. +epydoc \ + --html \ + --verbose \ + --docformat restructuredtext \ + --name Cheesecake \ + --url http://pycheesecake.org \ + -o $1/docs/ \ + cheesecake/ + +# Give read permissions so that web server can read generated files. +chmod 755 $1/docs/ -R Index: /trunk/support/cover2html.py =================================================================== --- /trunk/support/cover2html.py (revision 47) +++ /trunk/support/cover2html.py (revision 47) @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# +# Convert coverage output to HTML table. +# +# Copyright (c) 2006 Michal Kwiatkowski +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of the author nor the names of his contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import re +import sys + + +def sort_by_cover(lines): + def get_percent(obj): + return int(obj[3][:-1]) + + def compare(x, y): + return cmp(get_percent(x), get_percent(y)) + + lines.sort(compare) + + +def make_row(line, header=False, emphasis=False): + result = [] + + tag = 'td' + if header: + tag = 'th' + + result.append('\n') + + for field in line: + if emphasis: + result.append('<%(tag)s>%(field)s\n' % \ + {'tag': tag, 'field': field}) + else: + result.append('<%(tag)s>%(field)s\n' % \ + {'tag': tag, 'field': field}) + + result.append('\n') + + return ''.join(result) + + +def cover2html(text): + text_lines = map(lambda x: re.split(r'\s+', x, 4), text.splitlines()) + text_lines = filter(lambda x: x[0].strip('-'), text_lines) + + title_line = text_lines.pop(0) + summary_line = text_lines.pop() + + sort_by_cover(text_lines) + + result = [] + result.append('\n') + result.append(make_row(title_line, header=True)) + + for line in text_lines: + result.append(make_row(line)) + + result.append(make_row(summary_line, emphasis=True)) + result.append('
\n') + + return ''.join(result) + + +if __name__ == '__main__': + print cover2html(sys.stdin.read()) Index: /trunk/support/generate_coverage.sh =================================================================== --- /trunk/support/generate_coverage.sh (revision 107) +++ /trunk/support/generate_coverage.sh (revision 107) @@ -0,0 +1,19 @@ +#!/bin/sh + +tempfile=/tmp/coverage.tmp + +# Generate coverage statistics for Cheesecake. +python /usr/lib/python2.4/site-packages/coverage.py -r \ + cheesecake/util.py \ + cheesecake/cheesecake_index.py \ + cheesecake/codeparser.py \ + cheesecake/logger.py > $tempfile + +# Show generated coverage statistics on standard output. +cat $tempfile + +# Save formatted output into HTML file. +cat $tempfile| python support/cover2html.py > $1/coverage.html + +# Give read permissions so that web server can read generated files. +chmod 755 $1/coverage.html Index: /trunk/README =================================================================== --- /trunk/README (revision 5) +++ /trunk/README (revision 150) @@ -12,14 +12,10 @@ * whether the package can be downloaded from PyPI given its name - * whether the package can be downloaded from a full URL * whether the package can be unpacked - * whether the unpack directory is the same as the package name * whether the package can be installed into an alternate directory * existence of certain files such as README, INSTALL, LICENSE, setup.py etc. - * existence of certain directories such as doc, test, demo, examples * percentage of modules/functions/classes/methods with docstrings - * percentage of functions/methods that are unit tested (not currently - implemented) - * average pylint score for all non-test and non-demo modules + * pylint score + * ... and many others Currently, the Cheesecake index is computed for invidual packages obtained @@ -74,8 +70,8 @@ If the package can be successfully downloaded and unpacked, a log file is -created in the sandbox directory and named .log (e.g. the log file -for twill-0.7.4.tar.gz is /tmp/cheesecake_sandbox/twill-0.7.4.tar.gz.log). -The log file is not automatically deleted after the Cheesecake index is -computed, since its purpose is to be inspected for debug information. +created in the system /tmp directory and named .log (e.g. the log file +for twill-0.7.4.tar.gz is /tmp/twill-0.7.4.tar.gz.log). +The log file is automatically deleted after the Cheesecake index is +computed, except for situations when errors have occured. Command-line examples: @@ -98,16 +94,84 @@ For more options, run cheesecake.py with -h or --help. +Requirements +------------ + +* `pylint `_ is required for + part of the code kwalitee index computation +* `setuptools `_ is + required for the installability index computation + Obtaining the source code ------------------------- -The Cheesecake project has not yet been released as a tarball or -a Python egg. You can obtain the source code from SourceForge via CVS:: - - cvs -z3 -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/cheesecake co -P cheesecake - -Mailing list ------------- - -Developer mailing list: http://lists.sourceforge.net/lists/listinfo/cheesecake-devel +You can get the source code via svn:: + + svn co http://svn.pycheesecake.org/trunk cheesecake + +*Note*: make sure you indicate the target directory when you do the svn checkout, +otherwise the cheesecake package files will be checked out directly in your +current directory. + +You may want to modify your subversion client configuration to automatically +expand tags, like $Id$, $Author$ etc. To do so add following two lines to your +``/.subversion/config``:: + + enable-auto-props = yes + +in [miscellany] section, and:: + + *.py = svn:eol-style=native;svn:keywords=Author Date Id Revision + +in [auto-props] section. + +Documentation +------------- + +The most recent code documentation should be always available +at http://agilistas.org/cheesecake/mk/docs/. You can also generate +this documentation directly from the Cheesecake sources. Run this command +from the main source directory:: + + sh support/generate_docs.sh . + +:Note: Generating documentation requires `epydoc `_ + tool installed. + +Unit tests +---------- + +We use `nose `_ for automatic +testing of our project, so if you want to test Cheesecake on your machine, please +install that first. Running the standard set of Cheesecake unit test is as easy as:: + + python setup.py test + +This command is equivalent to:: + + nosetests --verbose --with-doctest --doctest-tests --include unit --exe + +We also have a set of functional tests, which can be run by issuing this command:: + + nosetests --verbose --include functional + +Functional tests can take a bit longer to complete, as they test cheesecake_index +script as a whole (as opposed to testing modules and classes separately). + +If you happen to find any of our tests failing, please don't hesitate to contact +us, either via +`cheesecake-devel mailing list `_ +or via `Cheesecake Trac `_. + +Buildbot +-------- + +A buildbot is happily running svn updates and unit tests. Check it out +`here `_. + +Mailing lists +------------- + +* Developer mailing list: http://lists2.idyll.org/listinfo/cheesecake-dev +* User mailing list: http://lists2.idyll.org/listinfo/cheesecake-users License @@ -120,203 +184,80 @@ http://www.opensource.org/licenses/PythonSoftFoundation.php. -Author contact info -------------------- +Authors contact info +-------------------- Grig Gheorghiu -Email: - -Web site: http://agiletesting.blogspot.com +:Email: +:Web site: http://agiletesting.blogspot.com + +Michal Kwiatkowski + +:Email: +:Web site: http://joker.linuxstuff.pl + +Note: clipart for the cheesecake slice logo used with permission from +Kazumi Hatasa, Director, the Japanese School at Middlebury College, +Purdue University. Algorithm for computing the Cheesecake index -------------------------------------------- -The cheesecake.py module uses the following constants:: - - INDEX_PYPI_DOWNLOAD = 50 - INDEX_PYPI_DISTANCE = 5 - INDEX_URL_DOWNLOAD = 25 - INDEX_UNPACK = 25 - INDEX_UNPACK_DIR = 15 - INDEX_INSTALL = 50 - INDEX_FILE_CRITICAL = 15 - INDEX_FILE = 10 - INDEX_FILE_PYC = 20 - INDEX_DIR_CRITICAL = 25 - INDEX_DIR = 20 - INDEX_DIR_EMPTY = 5 - - MAX_INDEX_DOCSTRINGS = 100 # max. percentage of modules/classes/methods/functions with docstrings - MAX_INDEX_PYLINT = 100 # max. pylint score - -**Step 0** - -Initialize the Cheesecake index to 0. Also initialize to 0 -the partial Cheesecake indexes for installability, documentation -and code kwalitee. - -Compute the maximum overall Cheesecake index that can be reached by -any given package, which is the sum:: - - INDEX_PYPI_DOWNLOAD + - INDEX_UNPACK + INDEX_UNPACK_DIR + - INDEX_INSTALL + - MAX_INDEX_DOCSTRINGS + MAX_INDEX_PYLINT + - (INDEX_FILE * number_of_expected_files) + - (INDEX_FILE_CRITICAL * number_of_expected_critical_files) + - (INDEX_DIR * number_of_expected_dirs) + - (INDEX_DIR_CRITICAL * number_of_expected_critical_dirs) - -Compute the maximum Cheesecake index for installability, which is the sum:: - - INDEX_PYPI_DOWNLOAD + - INDEX_UNPACK + INDEX_UNPACK_DIR + - INDEX_INSTALL - -Compute the maximum Cheesecake index for documentation, which is the sum:: - - (INDEX_FILE * number_of_expected_files) + - (INDEX_FILE_CRITICAL * number_of_expected_critical_files) + - (INDEX_DIR * number_of_expected_dirs) + - (INDEX_DIR_CRITICAL * number_of_expected_critical_dirs) + - MAX_INDEX_DOCSTRINGS - -Compute the maximum Cheesecake index for code kwalitee, which is currently:: - - MAX_INDEX_PYLINT - -**Step 1a** - -If short name of the package was specified with ``-n`` or ``--name``, -try to download the package from the PyPI index page by following the links to -the package home page and the package download URL (this is accomplished -using setuptools utilities). - -If not successful, exit with a Cheesecake index of 0. If successful and -package was found at the Cheese Shop, add ``INDEX_PYPI_DOWNLOAD`` to -the overall Cheesecake index and to the installability Cheesecake index. - -If successful but package was not found at the Cheese Shop, add -``INDEX_PYPI_DOWNLOAD - (INDEX_PYPI_DISTANCE * number_of_links_to_package)`` -to the overall Cheesecake index and to the installability Cheesecake index. - -**Step 1b** - -If full URL of the package was specified with ``-u`` or ``--url``, -try to download the package from the specified URL. - -If not successful, exit with a Cheesecake index of 0. If successful, -add ``INDEX_URL_DOWNLOAD`` to the overall Cheesecake index and to -the installability Cheesecake index. - -**Step 1c** - -If path to package on local file system was specified with ``-p`` or -``--path``, copy the package to the sandbox directory. - -**Step 2** - -Unpack the package (currently supported archive types are zip and -tar.gz/tgz; in the near future we will support Python Eggs.) - -If not successful, exit with a Cheesecake index of 0. If successful, add -``INDEX_UNPACK`` to the overall Cheesecake index and to the installability -Cheesecake index. - -**Step 3** - -Check that the unpack directory has the same name as the package name -(i.e. when unpacking twill-0.7.4.tar.gz, we expect the unpack directory -to be twill-0.7.4.) - -If the unpack directory name is the same as the package name, add -``INDEX_UNPACK_DIR`` -to the overall Cheesecake index and to the installability Cheesecake index. - -**Step 4** - -Install the package to a temporary directory in a non-default location. -If successful, add ``INDEX_INSTALL`` to the overall Cheesecake index and to the -installability Cheesecake index. - -**Step 5** - -Check for existence of specific files. -For each file found, add ``INDEX_FILE`` to the overall -Cheesecake index and to the documentation Cheesecake index. -If the file is deemed critical, add ``INDEX_FILE_CRITICAL`` instead. - -The following special files ("cheese_files") are currently checked:: - - cheese_files = ["install", "changelog", - "news", "faq", - "todo", "thanks", "announce", - "ez_setup.py", - ] - -The following files are currently deemed critical:: - - critical_cheese_files = ["readme", "license", "setup.py"] - -To check if a file FILE is among the cheese files, the following regular -expression is used:: - - re.search(r"^%s(\.txt)*" % cheese_file, file, re.IGNORECASE) - -**Step 6** - -Check for existence of specific directories. -For each directory found, add ``INDEX_DIR`` to the overall Cheesecake -index and to the documentation Cheesecake index. -If the directory is deemed critical, add ``INDEX_DIR_CRITICAL`` instead. -If the directory is found empty, add ``INDEX_DIR_EMPTY`` instead. - -The following directories ("cheese_dirs") are currently checked:: - - cheese_dirs = ["example", "demo"] - -The following directories are currently deemed critical:: - - critical_cheese_dirs = ["doc", "test"] - -To check if a directory DIR is among the cheese directories, -the following regular expression is used:: - - re.search(r"^%s" % cheese_dir, DIR, re.ignorecase) - -**Step 7** - -Check for existence of .pyc files. If found, decrease the score -by subtracting ``INDEX_FILE_PYC`` from the overall Cheesecake index -and from the documentation Cheesecake index. - -**Step 8** - -Compute the percentage of modules/classes/methods/functions that have -docstrings associated with them. Only Python modules that are not in test, -doc, demo and example directories are checked. -Round up the percentage and add it to the overall Cheesecake index and to the -documentation Cheesecake index. - -**Step 9** - -If pylint is present on the system, run pylint against all Python files -that are not in the test, docs or demo directories. -Average the non-negative pylint scores, multiply the average by 10 and -add it to the overall Cheesecake index and to the code kwalitee -Cheesecake index. - -**Step 10** - -For each of the partial Cheesecake index types (installability, -documentation and code kwalitee), display the absolute Cheesecake -index for that type as the sum of all indexes of that type computed in -the previous steps. -Also display the relative Cheesecake index for that type as the percentage -of ``(absolute_index / maximum_index)``. - -Display the absolute Cheesecake index for the package as the sum of all -indexes computed in the previous steps. Also display the relative Cheesecake -index for the package as the percentage of ``(absolute_index / maximum_index)``. +The overall Cheesecake score is the sum of values of 3 main indexes +(installability, documentation and code kwalitee). The values of these +indexes rely on values of their subindexes and so on. The whole index tree +and corresponding values for each leaf are presented below: + +* Installability + + * package is listed on and can be downloaded from PyPI: 50 + * package can be downloaded from given URL: 25 + * package can be unpacked without problems: 25 + * unpacked package directory is the same as package name: 15 + * package has setup.py: 25 + * package can be installed to given directory via "setup.py install": 50 + * package contain generated files, like .pyc: -20 + +* Documentation + + * package contain files listed below + + * README: 30 + * LICENCE/COPYING: 30 [#oneof]_ + * ANNOUNCE/CHANGELOG: 20 [#oneof]_ + * INSTALL: 20 + * AUTHORS: 10 + * FAQ: 10 + * NEWS: 10 + * THANKS: 10 + * TODO: 10 + + * package contain directories listed below + + * doc/docs: 30 [#oneof]_ + * test/tests: 30 [#oneof]_ + * demo/example/examples: 10 [#oneof]_ + + * code is documented by docstrings: 100 [#docstrings]_ + * docstrings have proper formatting (like epytext or reST): 30 [#formatted]_ + +* Code Kwalitee + + * package has high pylint score: 50 + * package has unit tests: 30 + +The final score depends on how well the package scores for all indexes +listed above. The score is presented in absolute range (number of points) +and relative (percent of points obtained compared to maximum possible points). + +.. [#oneof] It is enough for a package to contain only one of listed files. +.. [#docstrings] Number of points is proportional to percent of documentable objects + (module, class or function) that have docstrings. For example, if + you have 50 documentable objects and 32 of them have docstrings + your code will get 64 points (because 64% of objects are documented). +.. [#formatted] Number of points depends on number of docstrings that are found + to contain one of known markup. Currently ReST, epytext and javadoc are + recognized. We give 10 points for 25% of formatted docstrings, 20 points + for 50% and 30 points for 75%. Sample output @@ -325,51 +266,41 @@ :: - $ python cheesecake.py -n Durus - [cheesecake:console] Trying to download package durus from PyPI using setuptools utilities - [cheesecake:console] Downloaded package Durus-3.1.tar.gz from http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz - [cheesecake:console] Detailed info available in log file /tmp/cheesecake_sandbox/durus.log - [cheesecake:console] A given package can currently reach a MAXIMUM number of 555 points - [cheesecake:console] Starting computation of Cheesecake index for package 'Durus-3.1.tar.gz' - - [cheesecake:console] Starting computation of INSTALLABILITY index (max. points = 140) - index_pypi_download ..................... 45 (downloaded package Durus-3.1.tar.gz following 1 link from PyPI) - index_unpack ............................ 25 (package untar-ed successfully) - index_unpack_dir ........................ 15 (unpack directory is Durus-3.1 as expected) - index_install ........................... 50 (package installed in /tmp/cheesecake_sandbox/tmp_install_Durus-3.1) - --------------------------------------------- - INSTALLABILITY INDEX (ABSOLUTE) ......... 135 - INSTALLABILITY INDEX (RELATIVE) ......... 96 (135 out of a maximum of 140 points is 96%) - - [cheesecake:console] Starting computation of DOCUMENTATION index (max. points = 415) - index_file_announce ..................... 0 (file not found) - index_file_changelog .................... 0 (file not found) - index_file_ez_setup.py .................. 0 (file not found) - index_file_faq .......................... 10 (file found) - index_file_install ...................... 10 (file found) - index_file_license ...................... 15 (critical file found) - index_file_news ......................... 0 (file not found) - index_file_readme ....................... 15 (critical file found) - index_file_setup.py ..................... 15 (critical file found) - index_file_thanks ....................... 0 (file not found) - index_file_todo ......................... 0 (file not found) - index_dir_demo .......................... 0 (directory not found) - index_dir_doc ........................... 25 (critical directory found) - index_dir_example ....................... 0 (directory not found) - index_dir_test .......................... 25 (critical directory found) - index_docstrings ........................ 42 (found 104/249=41.77% modules/classes/methods/functions with docstrings) - --------------------------------------------- - DOCUMENTATION INDEX (ABSOLUTE) .......... 157 - DOCUMENTATION INDEX (RELATIVE) .......... 37 (157 out of a maximum of 415 points is 37%) - - [cheesecake:console] Starting computation of CODE KWALITEE index (max. points = 100) - index_pylint ............................ 64 (average score is 6.30 out of 10) - --------------------------------------------- - CODE KWALITEE INDEX (ABSOLUTE) .......... 64 - CODE KWALITEE INDEX (RELATIVE) .......... 64 (64 out of a maximum of 100 points is 64%) - - ============================================= - OVERALL CHEESECAKE INDEX (ABSOLUTE) ..... 356 - OVERALL CHEESECAKE INDEX (RELATIVE) ..... 64 (356 out of a maximum of 555 points is 64%) - + $ cheesecake_index -n Durus + py_pi_download ......................... 50 (downloaded package Durus-3.4.1.tar.gz following 1 link from http://www.mems-exchange.org/software/durus/Durus-3.4.1.tar.gz) + unpack ................................. 25 (package unpacked successfully) + unpack_dir ............................. 15 (unpack directory is Durus-3.4.1 as expected) + setup.py ............................... 25 (setup.py found) + install ................................ 50 (package installed in /tmp/cheesecakeUrZH1A/tmp_install_Durus-3.4.1) + generated_files ........................ 0 (0 .pyc and 0 .pyo files found) + --------------------------------------------- + INSTALLABILITY INDEX (ABSOLUTE) ........ 165 + INSTALLABILITY INDEX (RELATIVE) ........ 100 (165 out of a maximum of 165 points is 100%) + + required_files ......................... 170 (5 files and 2 required directories found) + docstrings ............................. 33 (found 121/369=32.79% objects with docstrings) + formatted_docstrings ................... 0 (found 6/369=1.63% objects with formatted docstrings) + --------------------------------------------- + DOCUMENTATION INDEX (ABSOLUTE) ......... 203 + DOCUMENTATION INDEX (RELATIVE) ......... 58 (203 out of a maximum of 350 points is 58%) + + pylint ................................. 33 (pylint score was 6.59 out of 10) + unit_tested ............................ 30 (have unit tests) + --------------------------------------------- + CODE KWALITEE INDEX (ABSOLUTE) ......... 63 + CODE KWALITEE INDEX (RELATIVE) ......... 79 (63 out of a maximum of 80 points is 79%) + + + ============================================= + OVERALL CHEESECAKE INDEX (ABSOLUTE) .... 431 + OVERALL CHEESECAKE INDEX (RELATIVE) .... 72 (431 out of a maximum of 595 points is 72%) + +Case study: Cleaning up PyBlosxom +--------------------------------- + +Many thanks to Will Guaraldi for writing +`this article `_ about his +experiences in using Cheesecake to clean up and improve the structure of his +PyBlosxom package. + Future plans ------------ @@ -377,10 +308,7 @@ index measurement, followed by other metrics inspired from the `kwalitee indicators `_. -Please edit the `IndexMeasurementIdeas `_ +Please edit the `IndexMeasurementIdeas `_ Wiki page to add things that you would like to see covered by the Cheesecake metrics. -.. footer:: Generated with rst2html.py from the - `docutils `_ - distribution. Last modified 2005-12-20 by - `Grig Gheorghiu `_. +.. footer:: Last modified 2006-08-21 by `Michal Kwiatkowski `_. Index: /unk/README.html =================================================================== --- /trunk/README.html (revision 5) +++ (revision ) @@ -1,380 +1,0 @@ - - - - - - -Cheesecake: How tasty is your code? - - - -
-

Cheesecake: How tasty is your code?

-
-
-

Summary

-

The idea of the Cheesecake project is to rank Python packages based on various -empirical "kwalitee" factors, such as:

-
-
    -
  • whether the package can be downloaded from PyPI given its name
  • -
  • whether the package can be downloaded from a full URL
  • -
  • whether the package can be unpacked
  • -
  • whether the unpack directory is the same as the package name
  • -
  • whether the package can be installed into an alternate directory
  • -
  • existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
  • -
  • existence of certain directories such as doc, test, demo, examples
  • -
  • percentage of modules/functions/classes/methods with docstrings
  • -
  • percentage of functions/methods that are unit tested (not currently -implemented)
  • -
  • average pylint score for all non-test and non-demo modules
  • -
-
-

Currently, the Cheesecake index is computed for invidual packages obtained -through a variety of methods (detailed below). One of the goals of the -Cheesecake project is to automatically compute the Cheesecake index for -all packages uploaded to the PyPI Cheese Shop (possibly at upload time) and -to maintain a collection of Web pages with statistics related to the -various indexes of the packages.

-

Cheesecake currently computes 3 types of indexes:

-
-
    -
  • installability index
  • -
  • documentation index
  • -
  • code kwalitee index
  • -
-
-

The algorithms for computing each index type are detailed below.

-
-
-

Why Cheesecake?

-

The concept of "kwalitee" originated in the Perl community. Here's a relevant -quote:

-
-It looks like quality, it sounds like quality, but it's not quite quality.
-

Kwalitee is an empiric measure of how good a specific body of code is. It -defines quality indicators and measures the code along them. It is currently -used by the CPANTS Testing Service -to evaluate the 'goodness' of CPAN packages.

-

Since the Python package repository (aka PyPI) -is hosted at the Cheese Shop, -it stands to reason that the quality indicator of a PyPI package should be -called the Cheesecake index!

-
-
-

Usage examples

-

To compute the Cheesecake index for a given project, run the cheesecake.py -module from the command line and indicate either:

-
- -
-

In all cases, the cheesecake module will attempt to download the package -if necessary, then to unpack it in a sandbox directory (/tmp/cheesecake_sandbox -by default). If either of these operations fails, the Cheesecake index for -the package will be 0. If the package can be successfully unpacked, the -cheesecake module will compute the values for a variety of indexes detailed -in the algorithm given at the end of this file.

-

If the package can be successfully downloaded and unpacked, a log file is -created in the sandbox directory and named <package>.log (e.g. the log file -for twill-0.7.4.tar.gz is /tmp/cheesecake_sandbox/twill-0.7.4.tar.gz.log). -The log file is not automatically deleted after the Cheesecake index is -computed, since its purpose is to be inspected for debug information.

-

Command-line examples:

-
-
    -
  1. Compute the Cheesecake index for the Durus package by using setuptools -utilities to download the package from PyPI:

    -
    -python cheesecake.py --name=Durus
    -
    -
  2. -
  3. Compute the Cheesecake index for the Durus package by indicating its URL:

    -
    -python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz
    -
    -
  4. -
  5. Compute the Cheesecake index for the twill package by indicating its path -on the local file system:

    -
    -python cheesecake.py --path=/tmp/twill-latest.tar.gz
    -
    -
  6. -
  7. To increase the verbosity of the output, use the -v or --verbose option. -For more options, run cheesecake.py with -h or --help.

    -
  8. -
-
-
-
-

Obtaining the source code

-

The Cheesecake project has not yet been released as a tarball or -a Python egg. You can obtain the source code from SourceForge via CVS:

-
-cvs -z3 -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/cheesecake co -P cheesecake
-
-
- -
-

License

-

Cheesecake is licensed under the Python Software Foundation license, -the same license that governs Python itself. The text of the license is -available in the LICENSE file in the source code distribution and -can also be downloaded from -http://www.opensource.org/licenses/PythonSoftFoundation.php.

-
-
-

Author contact info

-

Grig Gheorghiu

-

Email: <grig at gheorghiu dot net>

-

Web site: http://agiletesting.blogspot.com

-
-
-

Algorithm for computing the Cheesecake index

-

The cheesecake.py module uses the following constants:

-
-INDEX_PYPI_DOWNLOAD = 50
-INDEX_PYPI_DISTANCE = 5
-INDEX_URL_DOWNLOAD  = 25
-INDEX_UNPACK        = 25
-INDEX_UNPACK_DIR    = 15
-INDEX_INSTALL       = 50
-INDEX_FILE_CRITICAL = 15
-INDEX_FILE          = 10
-INDEX_FILE_PYC      = 20
-INDEX_DIR_CRITICAL  = 25
-INDEX_DIR           = 20
-INDEX_DIR_EMPTY     = 5
-
-MAX_INDEX_DOCSTRINGS = 100 # max. percentage of modules/classes/methods/functions with docstrings
-MAX_INDEX_PYLINT     = 100 # max. pylint score
-
-

Step 0

-

Initialize the Cheesecake index to 0. Also initialize to 0 -the partial Cheesecake indexes for installability, documentation -and code kwalitee.

-

Compute the maximum overall Cheesecake index that can be reached by -any given package, which is the sum:

-
-INDEX_PYPI_DOWNLOAD + 
-INDEX_UNPACK + INDEX_UNPACK_DIR + 
-INDEX_INSTALL +
-MAX_INDEX_DOCSTRINGS + MAX_INDEX_PYLINT + 
-(INDEX_FILE * number_of_expected_files) +
-(INDEX_FILE_CRITICAL * number_of_expected_critical_files) +
-(INDEX_DIR * number_of_expected_dirs) +
-(INDEX_DIR_CRITICAL * number_of_expected_critical_dirs)
-
-

Compute the maximum Cheesecake index for installability, which is the sum:

-
-INDEX_PYPI_DOWNLOAD + 
-INDEX_UNPACK + INDEX_UNPACK_DIR + 
-INDEX_INSTALL
-
-

Compute the maximum Cheesecake index for documentation, which is the sum:

-
-(INDEX_FILE * number_of_expected_files) +
-(INDEX_FILE_CRITICAL * number_of_expected_critical_files) +
-(INDEX_DIR * number_of_expected_dirs) +
-(INDEX_DIR_CRITICAL * number_of_expected_critical_dirs) +
-MAX_INDEX_DOCSTRINGS
-
-

Compute the maximum Cheesecake index for code kwalitee, which is currently:

-
-MAX_INDEX_PYLINT
-
-

Step 1a

-

If short name of the package was specified with -n or --name, -try to download the package from the PyPI index page by following the links to -the package home page and the package download URL (this is accomplished -using setuptools utilities).

-

If not successful, exit with a Cheesecake index of 0. If successful and -package was found at the Cheese Shop, add INDEX_PYPI_DOWNLOAD to -the overall Cheesecake index and to the installability Cheesecake index.

-

If successful but package was not found at the Cheese Shop, add -INDEX_PYPI_DOWNLOAD - (INDEX_PYPI_DISTANCE * number_of_links_to_package) -to the overall Cheesecake index and to the installability Cheesecake index.

-

Step 1b

-

If full URL of the package was specified with -u or --url, -try to download the package from the specified URL.

-

If not successful, exit with a Cheesecake index of 0. If successful, -add INDEX_URL_DOWNLOAD to the overall Cheesecake index and to -the installability Cheesecake index.

-

Step 1c

-

If path to package on local file system was specified with -p or ---path, copy the package to the sandbox directory.

-

Step 2

-

Unpack the package (currently supported archive types are zip and -tar.gz/tgz; in the near future we will support Python Eggs.)

-

If not successful, exit with a Cheesecake index of 0. If successful, add -INDEX_UNPACK to the overall Cheesecake index and to the installability -Cheesecake index.

-

Step 3

-

Check that the unpack directory has the same name as the package name -(i.e. when unpacking twill-0.7.4.tar.gz, we expect the unpack directory -to be twill-0.7.4.)

-

If the unpack directory name is the same as the package name, add -INDEX_UNPACK_DIR -to the overall Cheesecake index and to the installability Cheesecake index.

-

Step 4

-

Install the package to a temporary directory in a non-default location. -If successful, add INDEX_INSTALL to the overall Cheesecake index and to the -installability Cheesecake index.

-

Step 5

-

Check for existence of specific files. -For each file found, add INDEX_FILE to the overall -Cheesecake index and to the documentation Cheesecake index. -If the file is deemed critical, add INDEX_FILE_CRITICAL instead.

-

The following special files ("cheese_files") are currently checked:

-
-cheese_files = ["install", "changelog",
-                "news", "faq",
-                "todo", "thanks", "announce",
-                "ez_setup.py",
-               ]
-
-

The following files are currently deemed critical:

-
-critical_cheese_files = ["readme", "license", "setup.py"]
-
-

To check if a file FILE is among the cheese files, the following regular -expression is used:

-
-re.search(r"^%s(\.txt)*" % cheese_file, file, re.IGNORECASE)
-
-

Step 6

-

Check for existence of specific directories. -For each directory found, add INDEX_DIR to the overall Cheesecake -index and to the documentation Cheesecake index. -If the directory is deemed critical, add INDEX_DIR_CRITICAL instead. -If the directory is found empty, add INDEX_DIR_EMPTY instead.

-

The following directories ("cheese_dirs") are currently checked:

-
-cheese_dirs = ["example", "demo"]
-
-

The following directories are currently deemed critical:

-
-critical_cheese_dirs = ["doc", "test"]
-
-

To check if a directory DIR is among the cheese directories, -the following regular expression is used:

-
-re.search(r"^%s" % cheese_dir, DIR, re.ignorecase)
-
-

Step 7

-

Check for existence of .pyc files. If found, decrease the score -by subtracting INDEX_FILE_PYC from the overall Cheesecake index -and from the documentation Cheesecake index.

-

Step 8

-

Compute the percentage of modules/classes/methods/functions that have -docstrings associated with them. Only Python modules that are not in test, -doc, demo and example directories are checked. -Round up the percentage and add it to the overall Cheesecake index and to the -documentation Cheesecake index.

-

Step 9

-

If pylint is present on the system, run pylint against all Python files -that are not in the test, docs or demo directories. -Average the non-negative pylint scores, multiply the average by 10 and -add it to the overall Cheesecake index and to the code kwalitee -Cheesecake index.

-

Step 10

-

For each of the partial Cheesecake index types (installability, -documentation and code kwalitee), display the absolute Cheesecake -index for that type as the sum of all indexes of that type computed in -the previous steps. -Also display the relative Cheesecake index for that type as the percentage -of (absolute_index / maximum_index).

-

Display the absolute Cheesecake index for the package as the sum of all -indexes computed in the previous steps. Also display the relative Cheesecake -index for the package as the percentage of (absolute_index / maximum_index).

-
-
-

Sample output

-
-$ python cheesecake.py -n Durus
-[cheesecake:console] Trying to download package durus from PyPI using setuptools utilities
-[cheesecake:console] Downloaded package Durus-3.1.tar.gz from http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz
-[cheesecake:console] Detailed info available in log file /tmp/cheesecake_sandbox/durus.log
-[cheesecake:console] A given package can currently reach a MAXIMUM number of 555 points
-[cheesecake:console] Starting computation of Cheesecake index for package 'Durus-3.1.tar.gz'
-
-[cheesecake:console] Starting computation of INSTALLABILITY index (max. points = 140)
-index_pypi_download .....................  45 (downloaded package Durus-3.1.tar.gz following 1 link from PyPI)
-index_unpack ............................  25 (package untar-ed successfully)
-index_unpack_dir ........................  15 (unpack directory is Durus-3.1 as expected)
-index_install ...........................  50 (package installed in /tmp/cheesecake_sandbox/tmp_install_Durus-3.1)
----------------------------------------------
-INSTALLABILITY INDEX (ABSOLUTE) ......... 135
-INSTALLABILITY INDEX (RELATIVE) .........  96 (135 out of a maximum of 140 points is 96%)
-
-[cheesecake:console] Starting computation of DOCUMENTATION index (max. points = 415)
-index_file_announce .....................   0 (file not found)
-index_file_changelog ....................   0 (file not found)
-index_file_ez_setup.py ..................   0 (file not found)
-index_file_faq ..........................  10 (file found)
-index_file_install ......................  10 (file found)
-index_file_license ......................  15 (critical file found)
-index_file_news .........................   0 (file not found)
-index_file_readme .......................  15 (critical file found)
-index_file_setup.py .....................  15 (critical file found)
-index_file_thanks .......................   0 (file not found)
-index_file_todo .........................   0 (file not found)
-index_dir_demo ..........................   0 (directory not found)
-index_dir_doc ...........................  25 (critical directory found)
-index_dir_example .......................   0 (directory not found)
-index_dir_test ..........................  25 (critical directory found)
-index_docstrings ........................  42 (found 104/249=41.77% modules/classes/methods/functions with docstrings)
----------------------------------------------
-DOCUMENTATION INDEX (ABSOLUTE) .......... 157
-DOCUMENTATION INDEX (RELATIVE) ..........  37 (157 out of a maximum of 415 points is 37%)
-
-[cheesecake:console] Starting computation of CODE KWALITEE index (max. points = 100)
-index_pylint ............................  64 (average score is 6.30 out of 10)
----------------------------------------------
-CODE KWALITEE INDEX (ABSOLUTE) ..........  64
-CODE KWALITEE INDEX (RELATIVE) ..........  64 (64 out of a maximum of 100 points is 64%)
-
-=============================================
-OVERALL CHEESECAKE INDEX (ABSOLUTE) ..... 356
-OVERALL CHEESECAKE INDEX (RELATIVE) .....  64 (356 out of a maximum of 555 points is 64%)
-
-
-
-

Future plans

-

Cheesecake is under very active development. The immediate goal is to add the unit test -index measurement, followed by other metrics inspired from the -kwalitee indicators. -Please edit the IndexMeasurementIdeas -Wiki page to add things that you would like to see covered -by the Cheesecake metrics.

-
-
- - -