Changeset 40

Show
Ignore:
Timestamp:
06/07/06 17:27:35 (3 years ago)
Author:
mk
Message:

Refactored code for index computation (closes tickets #17 #18 #19 #20 #21 #22).

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • branches/mk/cheesecake/_util.py

    r11 r40  
    3232    output = p.communicate()[0] 
    3333    return p.returncode, output 
     34 
     35def command_successful(cmd): 
     36    """Returns True if command exited normally, False otherwise. 
     37    """ 
     38    rc, output = run_cmd(cmd) 
     39    if rc: 
     40        return False 
     41    return True 
    3442 
    3543class StdoutRedirector(object): 
  • branches/mk/cheesecake/cheesecake_index.py

    r38 r40  
    2626from math import ceil 
    2727 
    28 from _util import run_cmd, pad_with_dots, pad_left_spaces, pad_msg, pad_line 
     28from _util import run_cmd, pad_with_dots, pad_left_spaces, pad_msg, pad_line, command_successful 
    2929from _util import StdoutRedirector 
    3030import logger 
     
    3232from codeparser import CodeParser 
    3333 
    34 default_temp_directory = os.path.join(tempfile.gettempdir(), 'cheesecake_sandbox') 
    35  
     34__docformat__ = 'reStructuredText en' 
     35 
     36 
     37default_temp_directory = os.path.join(tempfile.gettempdir(), 
     38                                      'cheesecake_sandbox') 
     39 
     40################################################################################ 
     41## Helpers. 
     42################################################################################ 
     43 
     44def isiterable(obj): 
     45    return hasattr(obj, '__iter__') 
    3646 
    3747def has_extension(filename, ext): 
     
    4757    return os.path.splitext(filename)[1] == ext 
    4858 
     59def discover_file_type(filename): 
     60    """Discover type of a file according to its name and its parent directory. 
     61 
     62    Currently supported file types: 
     63        * pyc 
     64        * pyo 
     65        * module: .py files of an application 
     66        * demo: .py files for documentation/demonstration purposes 
     67        * test: .py files used for testing 
     68        * special: .py file for special purposes 
     69 
     70    :Note: This function only check file's name, and doesn't touch the 
     71           filesystem. If you have to, check if file exists by yourself. 
     72 
     73    >>> discover_file_type('module.py') 
     74    'module' 
     75    >>> discover_file_type('./setup.py') 
     76    'special' 
     77    >>> discover_file_type('some/directory/junk.pyc') 
     78    'pyc' 
     79    """ 
     80    dirs = filename.split(os.path.sep) 
     81    dirs, filename = dirs[:-1], dirs[-1] 
     82 
     83    if filename in ["setup.py", "ez_setup.py", "__pkginfo__.py"]: 
     84        return 'special' 
     85 
     86    if has_extension(filename, ".pyc"): 
     87        return 'pyc' 
     88    if has_extension(filename, ".pyo"): 
     89        return 'pyo' 
     90    if has_extension(filename, ".py"): 
     91        for dir in dirs: 
     92            if dir in ['test', 'tests']: 
     93                return 'test' 
     94            elif dir in ['docs', 'demo', 'example']: 
     95                return 'demo' 
     96        return 'module' 
     97 
     98def get_files_of_type(file_list, file_type): 
     99    """Return files from `file_list` that match given `file_type`. 
     100 
     101    >>> file_list = ['test/test_foo.py', 'setup.py', 'README', 'test/test_bar.py'] 
     102    >>> get_files_of_type(file_list, 'test') 
     103    ['test/test_foo.py', 'test/test_bar.py'] 
     104    """ 
     105    return filter(lambda x: discover_file_type(x) == file_type, file_list) 
     106 
     107def get_method_arguments(method): 
     108    """Return tuple of arguments for given method, excluding self. 
     109 
     110    >>> class Class: 
     111    ...     def method(s, arg1, arg2, other_arg): 
     112    ...         pass 
     113    >>> get_method_arguments(Class.method) 
     114    ('arg1', 'arg2', 'other_arg') 
     115    """ 
     116    return method.func_code.co_varnames[1:method.func_code.co_argcount] 
     117 
     118def get_attributes(obj, names): 
     119    """Return attributes dictionary with keys from `names`. 
     120 
     121    Object is queried for each attribute name, if it doesn't have this 
     122    attribute, default value None will be returned. 
     123 
     124    >>> class Class: 
     125    ...     pass 
     126    >>> obj = Class() 
     127    >>> obj.attr = True 
     128    >>> obj.value = 13 
     129    >>> obj.string = "Hello" 
     130 
     131    >>> d = get_attributes(obj, ['attr', 'string', 'other']) 
     132    >>> d == {'attr': True, 'string': "Hello", 'other': None} 
     133    True 
     134    """ 
     135    attrs = {} 
     136 
     137    for name in names: 
     138        attrs[name] = getattr(obj, name, None) 
     139 
     140    return attrs 
     141 
     142def camel2underscore(name): 
     143    """Convert name from CamelCase to underscore_name. 
     144 
     145    >>> camel2underscore('CamelCase') 
     146    'camel_case' 
     147    >>> camel2underscore('already_underscore_name') 
     148    'already_underscore_name' 
     149    >>> camel2underscore('BigHTMLClass') 
     150    'big_html_class' 
     151    >>> camel2underscore('') 
     152    '' 
     153    """ 
     154    if name and name[0].upper: 
     155        name = name[0].lower() + name[1:] 
     156 
     157    def capitalize(match): 
     158        string = match.group(1).lower().capitalize() 
     159        return string[:-1] + string[-1].upper() 
     160 
     161    def underscore(match): 
     162        return '_' + match.group(1).lower() 
     163 
     164    name = re.sub(r'([A-Z]+)', capitalize, name) 
     165    return re.sub(r'([A-Z])', underscore, name) 
     166 
     167def index_class_to_name(clsname): 
     168    """Covert index class name to index name. 
     169 
     170    >>> index_class_to_name("IndexDownload") 
     171    'download' 
     172    >>> index_class_to_name("IndexUnitTests") 
     173    'unit_tests' 
     174    >>> index_class_to_name("IndexPyPIDownload") 
     175    'py_pi_download' 
     176    """ 
     177    return camel2underscore(clsname.replace('Index', '', 1)) 
     178 
     179def is_empty(path): 
     180    """Returns True if file or directory pointed by `path` is empty. 
     181    """ 
     182    if os.path.isfile(path) and os.path.getsize(path) == 0: 
     183        return True 
     184    if os.path.isdir(path) and os.listdir(path) == []: 
     185        return True 
     186 
     187    return False 
     188 
     189def strip_dir_part(path, root): 
     190    """Strip `root` part from `path`. 
     191 
     192    >>> strip_dir_part('/home/ruby/file', '/home') 
     193    'ruby/file' 
     194    >>> strip_dir_part('/home/ruby/file', '/home/') 
     195    'ruby/file' 
     196    >>> strip_dir_part('/home/ruby/', '/home') 
     197    'ruby/' 
     198    >>> strip_dir_part('/home/ruby/', '/home/') 
     199    'ruby/' 
     200    """ 
     201    path = path.replace(root, '', 1) 
     202 
     203    if path.startswith(os.path.sep): 
     204        path = path[1:] 
     205 
     206    return path 
     207 
     208def get_files_dirs_list(root): 
     209    """Return list of all files and directories below `root`. 
     210 
     211    Root directory is excluded from files/directories paths. 
     212    """ 
     213    files = [] 
     214    directories = [] 
     215 
     216    for dirpath, dirnames, filenames in os.walk(root): 
     217        dirpath = strip_dir_part(dirpath, root) 
     218        files.extend(map(lambda x: os.path.join(dirpath, x), filenames)) 
     219        directories.extend(map(lambda x: os.path.join(dirpath, x), dirnames)) 
     220 
     221    return files, directories 
     222 
     223################################################################################ 
     224## Main index class. 
     225################################################################################ 
     226 
     227class NameSetter(type): 
     228    def __init__(cls, name, bases, dict): 
     229        if 'name' not in dict: 
     230            setattr(cls, 'name', index_class_to_name(name)) 
     231 
     232def make_indices_dict(indices): 
     233    indices_dict = {} 
     234    for index in indices: 
     235        indices_dict[index.name] = index 
     236    return indices_dict 
    49237 
    50238class Index(object): 
    51     """ 
    52     Encapsulates index attributes such as name, value, details 
    53     """ 
    54  
    55     def __init__(self, type, name="", value=0, details=""): 
    56         self.type = "index_" + type 
    57         self.name = self.type 
    58         if name: self.name += "_" + name 
    59         self.value = value 
    60         self.details = details 
    61          
     239    """Class describing one index. 
     240 
     241    Use it as a container index or subclass to create custom indices. 
     242 
     243    During class initialization, special attribute `name` is magically 
     244    set based on class name. See `index_class_to_name` and `NameSetter` 
     245    definitions for details. 
     246    """ 
     247    __metaclass__ = NameSetter 
     248 
     249    subindices = None 
     250 
     251    name = "unnamed" 
     252    value = -1 
     253    details = "" 
     254 
     255    def __init__(self, indices=[]): 
     256        if not self.subindices: 
     257            self.subindices = [] 
     258 
     259        # Create dictionary for fast reference. 
     260        self._indices_dict = make_indices_dict(self.subindices) 
     261 
     262        for index in indices: 
     263            self.add_subindex(index) 
     264 
     265        self._compute_arguments = get_method_arguments(self.compute) 
     266 
     267    def _iter_indices(self): 
     268        """Iterate over each subindex and yield their values. 
     269        """ 
     270        for index in self.subindices: 
     271            # Pass Cheesecake instance to other indices. 
     272            yield index.compute_with(self.cheesecake) 
     273            # Print index info after computing. 
     274            index.print_info() 
     275 
     276    def compute_with(self, cheesecake): 
     277        """Take given Cheesecake instance and compute index value. 
     278        """ 
     279        self.cheesecake = cheesecake 
     280        return self.compute(**get_attributes(cheesecake, self._compute_arguments)) 
     281 
     282    def compute(self): 
     283        """Compute index value and return it. 
     284 
     285        By default this method computes sum of all subindices. Override this 
     286        method when subclassing for different behaviour. 
     287 
     288        Parameters to this function are dynamically prepared with use of 
     289        `get_attributes` function. 
     290 
     291        :Warning: Don't use *args and **kwds arguments for this method. 
     292        """ 
     293        self.value = sum(self._iter_indices()) 
     294        return self.value 
     295 
     296    def _get_max_value(self): 
     297        if self.subindices: 
     298            return sum(map(lambda index: index.max_value, 
     299                           self.subindices)) 
     300        return 0 
     301 
     302    max_value = property(_get_max_value) 
     303 
     304    def add_subindex(self, index): 
     305        """Add subindex. 
     306 
     307        :Parameters: 
     308          `index` : Index instance 
     309              Index instance for inclusion. 
     310        """ 
     311        if not isinstance(index, Index): 
     312            raise ValueError("subindex have to be instance of Index") 
     313 
     314        self.subindices.append(index) 
     315        self._indices_dict[index.name] = index 
     316 
    62317    def print_info(self): 
    63         """ 
    64         Print index name padded with dots, followed by value and details 
    65         """ 
    66         msg = pad_with_dots(self.name) 
    67         msg += pad_left_spaces(self.value) 
    68         msg += " (" + self.details + ")" 
     318        """Print index name padded with dots, followed by value and details. 
     319        """ 
     320        print "%s%s (%s)" % (pad_with_dots(self.name), 
     321                             pad_left_spaces(self.value), 
     322                             self.details) 
     323 
     324    def __getitem__(self, name): 
     325        return self._indices_dict[name] 
     326 
     327class MegaIndex(Index): 
     328    """Index with special information schema, suitable for composite indices. 
     329    """ 
     330    def print_info(self): 
     331        max_value = self.max_value 
     332        if max_value == 0: 
     333            return 
     334 
     335        percentage = int(ceil(float(self.value) / float(max_value) * 100)) 
     336        print pad_line("-") 
     337 
     338        print pad_msg("%s INDEX (ABSOLUTE)" % self.name, self.value) 
     339        msg = pad_msg("%s INDEX (RELATIVE)" % self.name, percentage) 
     340        msg += " (%d out of a maximum of %d points is %d%%)" %\ 
     341             (self.value, max_value, percentage) 
     342 
    69343        print msg 
    70  
    71 class CompositeIndex(object): 
    72     """ 
    73     Collection of indexes of same type (e.g. files, dirs) 
    74     """ 
    75  
    76     def __init__(self, type): 
    77         """ 
    78         Indexes is a dict mapping names to Index objects 
    79         """ 
    80         self.type = type 
    81         self.indexes = {} 
    82  
    83     def set_index(self, name, value=0, details=""): 
    84         """ 
    85         Create new index or update existing index with specified attributes 
    86         """ 
    87         if self.indexes.has_key(name): 
    88             index = self.indexes[name] 
    89             index.value = value 
    90             index.details = details 
     344        print 
     345 
     346################################################################################ 
     347## Installability index. 
     348################################################################################ 
     349 
     350class IndexUrlDownload(Index): 
     351    max_value = 25 
     352 
     353    def compute(self, downloaded_from_url, package, url): 
     354        if downloaded_from_url: 
     355            self.details = "downloaded package %s from URL %s"  % (package, url) 
     356            self.value = self.max_value 
    91357        else: 
    92             self.indexes[name] = Index(self.type, name, value, details) 
    93          
    94     def print_info(self): 
    95         """ 
    96         Print index info for all indexes sorted alphanumerically by name 
    97         """ 
    98         names = self.indexes.keys() 
    99         names.sort() 
    100         for name in names: 
    101             index = self.indexes[name] 
    102             index.print_info() 
    103  
    104     def get_value(self): 
    105         """ 
    106         Return sum of individual index values 
    107         """ 
    108         value = 0 
    109         for key in self.indexes.keys(): 
    110             index = self.indexes[key] 
    111             value += index.value 
    112         return value 
    113  
    114     value = property(get_value) 
     358            self.value = 0 
     359 
     360        return self.value 
     361 
     362class IndexUnpack(Index): 
     363    max_value = 25 
     364 
     365    def compute(self, unpacked): 
     366        if unpacked: 
     367            self.details = "package unpacked successfully" 
     368            self.value = self.max_value 
     369        else: 
     370            self.value = 0 
     371 
     372        return self.value 
     373 
     374class IndexUnpackDir(Index): 
     375    max_value = 15 
     376 
     377    def compute(self, unpack_dir, original_package_name): 
     378        self.details = "unpack directory is " + unpack_dir 
     379 
     380        if original_package_name: 
     381            self.details += " instead of the expected " + original_package_name 
     382            self.value = 0 
     383        else: 
     384            self.details += " as expected" 
     385            self.value = self.max_value 
     386 
     387        return self.value 
     388 
     389class IndexInstall(Index): 
     390    max_value = 50 
     391 
     392    def compute(self, installed, sandbox_install_dir): 
     393        if installed: 
     394            self.details = "package installed in %s" % sandbox_install_dir 
     395            self.value = self.max_value 
     396        else: 
     397            self.details = "could not install package in %s" % sandbox_install_dir 
     398            self.value = 0 
     399 
     400        return self.value 
     401 
     402class IndexPyPIDownload(Index): 
     403    max_value = 50 
     404    distance_penalty = -5 
     405 
     406    def compute(self, package, found_on_cheeseshop, distance_from_pypi, download_url): 
     407        if download_url: 
     408            self.value = self.max_value 
     409 
     410            self.details = "downloaded package " + package 
     411 
     412            if not found_on_cheeseshop: 
     413                self.value += distance_from_pypi * self.distance_penalty 
     414 
     415                if distance_from_pypi: 
     416                    self.details += " following %d link" % distance_from_pypi 
     417                    if distance_from_pypi > 1: 
     418                        self.details += "s" 
     419                        self.details += " from PyPI" 
     420                    else: 
     421                        self.details += " from " + download_url 
     422            else: 
     423                self.details += " directly from the Cheese Shop" 
     424        else: 
     425            self.value = 0 
     426 
     427        return self.value 
     428 
     429class IndexGeneratedFiles(Index): 
     430    generated_files_penalty = -20 
     431 
     432    def compute(self, files_list): 
     433        self.value = 0 
     434 
     435        pyc_files = len(get_files_of_type(files_list, 'pyc')) 
     436        pyo_files = len(get_files_of_type(files_list, 'pyo')) 
     437 
     438        if pyc_files > 0 or pyo_files > 0: 
     439            self.value += self.generated_files_penalty 
     440 
     441        self.details = "%d .pyc and %d .pyo files found" % \ 
     442                                  (pyc_files, pyo_files) 
     443 
     444        return self.value 
     445 
     446class IndexInstallability(MegaIndex): 
     447    name = "INSTALLABILITY" 
     448 
     449    subindices = [ 
     450        IndexUnpack(), 
     451        IndexUnpackDir(), 
     452        IndexInstall(), 
     453        IndexGeneratedFiles(), 
     454    ] 
     455 
     456################################################################################ 
     457## Documentation index. 
     458################################################################################ 
     459 
     460def match_filename(name, rule): 
     461    """Check if `name` matches given `rule`. 
     462    """ 
     463    def equal(x, y): 
     464        x_root, x_ext = os.path.splitext(x) 
     465        y_root, y_ext = os.path.splitext(y.lower()) 
     466        if x_root in [y_root.lower(), y_root.upper(), y_root.capitalize()] \ 
     467               and x_ext in [y_ext.lower(), y_ext.upper()]: 
     468            return True 
     469        return False 
     470 
     471    if isinstance(rule, basestring): 
     472        if equal(name, rule): 
     473            return True 
     474    elif isinstance(rule, OneOf) and not rule.used: 
     475        for poss in rule.possibilities: 
     476            if match_filename(name, poss): 
     477                rule.used = True 
     478                return True 
     479 
     480    return False 
     481 
     482class OneOf(object): 
     483    def __init__(self, *possibilities): 
     484        self.possibilities = possibilities 
     485        self.used = False 
     486    def __str__(self): 
     487        return 'one of %s' % (self.possibilities,) 
     488 
     489def WithOptionalExt(name, extensions): 
     490    """Handy way of writing Cheese rules for files with extensions. 
     491 
     492    Instead of writing: 
     493        >>> one_of = OneOf('readme', 'readme.html', 'readme.txt') 
     494 
     495    Write this: 
     496        >>> opt_ext = WithOptionalExt('readme', ['html', 'txt']) 
     497 
     498    It means the same! (representation have a meaning) 
     499        >>> str(one_of) == str(opt_ext) 
     500        True 
     501    """ 
     502    possibilities = [name] 
     503    possibilities.extend(map(lambda x: name + '.' + x, extensions)) 
     504 
     505    return OneOf(*possibilities) 
     506 
     507def Doc(name): 
     508    return WithOptionalExt(name, ['html', 'txt']) 
     509 
     510class IndexRequiredFiles(Index): 
     511    cheese_files = { 
     512        'setup.py': 15, 
     513        Doc('readme'): 15, 
     514        OneOf(Doc('license'), Doc('copying')): 15, 
     515 
     516        Doc('authors'): 10, 
     517        Doc('announce'): 10, 
     518        Doc('changelog'): 10, 
     519        Doc('faq'): 10, 
     520        Doc('install'): 10, 
     521        Doc('news'): 10, 
     522        Doc('thanks'): 10, 
     523        Doc('todo'): 10, 
     524    } 
     525 
     526    cheese_dirs = { 
     527        'demo': 20, 
     528        'doc': 25, 
     529        'example': 20, 
     530        OneOf('test', 'tests'): 25, 
     531    } 
     532 
     533    max_value = sum(cheese_files.values() + cheese_dirs.values()) 
     534 
     535    def compute(self, files_list, dirs_list, package_dir): 
     536        self.value = 0 
     537        self.reset_rules(self.cheese_files.keys() + self.cheese_dirs.keys()) 
     538 
     539        files_count = 0 
     540        for filename in files_list: 
     541            if not is_empty(os.path.join(package_dir, filename)): 
     542                score = self.get_score(os.path.basename(filename), self.cheese_files) 
     543                if score != 0: 
     544                    self.value += score 
     545                    files_count += 1 
     546 
     547        directories_count = 0 
     548        for directory in dirs_list: 
     549            if not is_empty(os.path.join(package_dir, directory)): 
     550                score = self.get_score(os.path.basename(directory), self.cheese_dirs) 
     551                if score != 0: 
     552                    self.value += score 
     553                    directories_count += 1 
     554 
     555        self.details = "%d files and %d required directories found" % \ 
     556                       (files_count, directories_count) 
     557 
     558        return self.value 
     559 
     560    def get_score(self, name, specs): 
     561        for entry, value in specs.iteritems(): 
     562            if match_filename(name, entry): 
     563                self.cheesecake.log.debug("%d points entry found: %s (%s)" % \ 
     564                                          (value, name, entry)) 
     565                return value 
     566 
     567        return 0 
     568 
     569    def reset_rules(self, rules): 
     570        if isiterable(rules): 
     571            for rule in rules: 
     572                self.reset_rules(rule) 
     573        elif isinstance(rules, OneOf): 
     574            rules.used = False 
     575            self.reset_rules(rules.possibilities) 
     576 
     577class IndexDocstrings(Index): 
     578    max_value = 100 
     579 
     580    def compute(self, object_cnt, docstring_cnt): 
     581        percent = 0 
     582        if object_cnt > 0: 
     583            percent = float(docstring_cnt)/float(object_cnt) 
     584 
     585        # Scale the result. 
     586        self.value = int(ceil(percent * self.max_value)) 
     587 
     588        self.details = "found %d/%d=%.2f%% objects with docstrings" %\ 
     589                 (docstring_cnt, object_cnt, percent*100) 
     590 
     591        return self.value 
     592 
     593class IndexFormattedDocstrings(Index): 
     594    max_value = 50 
     595 
     596    def compute(self, object_cnt, docformat_cnt): 
     597        percent = 0 
     598        if object_cnt > 0: 
     599            percent = float(docformat_cnt)/float(object_cnt) 
     600 
     601        # Scale the result. 
     602        self.value = int(ceil(percent * self.max_value)) 
     603 
     604        self.details = "found %d/%d=%.2f%% objects with formatted docstrings" %\ 
     605                 (docformat_cnt, object_cnt, percent*100) 
     606 
     607        return self.value 
     608 
     609class IndexDocumentation(MegaIndex): 
     610    name = "DOCUMENTATION" 
     611 
     612    subindices = [ 
     613        IndexRequiredFiles(), 
     614        IndexDocstrings(), 
     615        IndexFormattedDocstrings(), 
     616    ] 
     617 
     618################################################################################ 
     619## Code "kwalitee" index. 
     620################################################################################ 
     621 
     622class IndexUnitTests(Index): 
     623    """Compute unittest index as percentage of methods/functions 
     624    that are exercised in unit tests. 
     625    """ 
     626    max_value = 50 
     627 
     628    def compute(self, files_list, functions, package_dir): 
     629        unittest_cnt = 0 
     630        self.functions_tested = {} 
     631 
     632        for testfile in get_files_of_type(files_list, 'test'): 
     633            fullpath = os.path.join(package_dir, testfile) 
     634            code = CodeParser(fullpath, self.cheesecake.log.debug) 
     635 
     636            func_called = code.functions_called() 
     637 
     638            for func in func_called: 
     639                self.functions_tested[func] = 1 
     640 
     641        for funcname in functions: 
     642            if self.is_unit_tested(funcname): 
     643                unittest_cnt += 1 
     644                self.log.cheesecake.debug("%s is unit tested" % funcname) 
     645 
     646        percent = 0 
     647        if len(functions) > 0: 
     648            percent = float(unittest_cnt)/float(len(functions)) 
     649 
     650        # Scale the result. 
     651        self.value = int(ceil(percent * self.max_value)) 
     652 
     653        self.details = "found %d/%d=%.2f%% unit tested methods/functions." %\ 
     654                 (unittest_cnt, len(functions), percent*100) 
     655 
     656        return self.value 
     657 
     658    def is_unit_tested(self, funcname): 
     659        elem = funcname.split(".") 
     660        n1 = elem[-1] 
     661        n2 = "" 
     662        if len(elem) > 1: 
     663            n2 = elem[-2] + "." + elem[-1] 
     664        for key in self.functions_tested.keys(): 
     665            if key.startswith(n1) or (n2 and key.startswith(n2)): 
     666                return True 
     667        return False 
     668 
     669class IndexPyLint(Index): 
     670    """Compute pylint index as average of positive pylint scores obtained for 
     671    the Python files identified in the package. 
     672    """ 
     673    name = "pylint" 
     674    max_value = 50 
     675 
     676    def compute(self, files_list, package_dir): 
     677        self.value = 0 
     678 
     679        # Try to run the pylint script 
     680        if not command_successful("pylint --version"): 
     681            self.details = "pylint not properly installed" 
     682            return self.value 
     683 
     684        pylint_value = 0 
     685        cnt = 0 
     686        for pyfile in get_files_of_type(files_list, 'module'): 
     687            fullpath = os.path.join(package_dir, pyfile) 
     688            path, filename = os.path.split(fullpath) 
     689            module, ext = os.path.splitext(filename) 
     690 
     691            self.cheesecake.log.debug("Running pylint on file " + fullpath) 
     692            rc, output = run_cmd("pylint " + fullpath) 
     693            if rc: 
     694                self.cheesecake.log.debug("encountered an error (%d)." % rc) 
     695                continue 
     696 
     697            score_line = output.split("\n")[-3] 
     698            s = re.search(r" (\d+\.\d+)/10", score_line) 
     699            # We only take positive scores into account 
     700            if s: 
     701                score = s.group(1) 
     702                if score == "0.00": 
     703                    self.cheesecake.log.debug("ignoring 0.00 score.") 
     704                    continue 
     705                else: 
     706                    self.cheesecake.log.debug("pylint score for module %s: %s" % (module, score)) 
     707                pylint_value += float(score) 
     708                cnt += 1 
     709 
     710        avg_score = 0 
     711        if cnt: 
     712            avg_score = float(pylint_value)/float(cnt) 
     713 
     714        self.value = int(ceil(avg_score/10.0 * self.max_value)) 
     715        self.details = "average pylint score is %.2f out of 10" % avg_score 
     716 
     717        return self.value 
     718 
     719class IndexCodeKwalitee(MegaIndex): 
     720    name = "CODE KWALITEE" 
     721 
     722    subindices = [ 
     723        IndexPyLint(), 
     724        # IndexUnitTests(), TODO 
     725    ] 
     726 
     727################################################################################ 
     728## Main Cheesecake class. 
     729################################################################################ 
    115730 
    116731class CheesecakeError(Exception): 
     
    119734    """ 
    120735    pass 
     736 
     737 
     738class CheesecakeIndex(Index): 
     739    name = "Cheesecake" 
     740    subindices = [ 
     741        IndexInstallability(), 
     742        IndexDocumentation(), 
     743        IndexCodeKwalitee(), 
     744    ] 
     745 
    121746 
    122747class Cheesecake(object): 
     
    135760        * average pylint score for all non-test and non-demo modules 
    136761    """ 
     762    index = CheesecakeIndex() 
    137763 
    138764    def __init__(self, name="", url="", path="", sandbox=None, config=None, 
    139765                logfile=None, verbose=False, quiet=False): 
    140         """ 
    141         Initialize critical variables, download and unpack package, walk package tree 
    142  
     766        """Initialize critical variables, download and unpack package, 
     767        walk package tree. 
    143768        """ 
    144769        self.name = name 
     
    162787        self.sandbox_install_dir = "" 
    163788 
     789        # Include indices revelant to current situation. 
     790        if self.name: 
     791            self.index["INSTALLABILITY"].add_subindex(IndexPyPIDownload()) 
     792        if self.url: 
     793            self.index["INSTALLABILITY"].add_subindex(IndexUrlDownload()) 
     794 
    164795        self.determine_pkg_name() 
    165796        self.configure_logging(logfile) 
    166         self.set_defaults() 
    167         self.get_config() 
    168         self.init_indexes() 
     797        #self.set_defaults() 
     798        #self.get_config() 
    169799        self.retrieve_pkg() 
    170800        self.unpack_pkg() 
    171801        self.walk_pkg() 
     802        self.install_pkg() 
    172803 
    173804    def raise_exception(self, msg): 
    174         """ 
    175         Cleanup, print error message and raise CheesecakeError 
    176  
    177         Don't use logging, since it can be called before logging has been setup 
     805        """Cleanup, print error message and raise CheesecakeError. 
     806 
     807        Don't use logging, since it can be called before logging has been setup. 
    178808        """ 
    179809        self.cleanup() 
     
    184814  
    185815    def cleanup(self): 
    186         """ 
    187         Delete temporary directories and files that were 
    188         created in the sandbox. At the end delete the sandbox itself. 
     816        """Delete temporary directories and files that were created 
     817        in the sandbox. At the end delete the sandbox itself. 
    189818        """ 
    190819        if os.path.isfile(self.sandbox_pkg_file): 
     
    198827                shutil.rmtree(dirname) 
    199828 
    200         for dir in [self.sandbox_pkg_dir, self.sandbox_install_dir, 
    201                     self.sandbox]: 
    202             delete_dir(dir) 
     829        delete_dir(self.sandbox) 
    203830 
    204831    def set_defaults(self): 
    205         """ 
    206         Set default values for variables that can also be defined 
    207         in the config file 
    208         """ 
    209         self.INDEX_PYPI_DOWNLOAD = 50 
    210         self.INDEX_PYPI_DISTANCE = 5 
    211         self.INDEX_URL_DOWNLOAD  = 25 
    212         self.INDEX_UNPACK        = 25 
    213         self.INDEX_UNPACK_DIR    = 15 
    214         self.INDEX_INSTALL       = 50 
    215         self.INDEX_FILE_CRITICAL = 15 
    216         self.INDEX_FILE          = 10 
    217         self.INDEX_REQUIRED_FILES = 100 
    218         self.INDEX_FILE_PYC      = -20 
    219         self.INDEX_FILE_PYO      = -20 
    220         self.INDEX_DIR_CRITICAL  = 25 
    221         self.INDEX_DIR           = 20 
    222         self.INDEX_DIR_EMPTY     = 5 
    223         self.MAX_INDEX_DOCSTRINGS = 100 # max. percentage of modules/classes/methods/functions with docstrings 
    224         self.MAX_INDEX_DOCFORMAT  = 100 # max. percentage of modules/classes/methods/functions with formatted docstrings 
    225         self.MAX_INDEX_UNITTESTS  = 100 # max. percentage of methods/functions that are unit tested 
    226         self.MAX_INDEX_PYLINT     = 100 # max. pylint score 
    227         self.cheese_files = ["readme", "install", "changelog", 
    228                             "news", "faq", 
    229                             "todo", "thanks", 
    230                             "license", "announce", 
    231                             "setup.py", 
    232                             ] 
    233         self.critical_cheese_files = ["readme", "license", "setup.py"] 
    234         self.cheese_dirs = ["doc", "test", "example", "demo"] 
    235         self.critical_cheese_dirs = ["doc", "test"] 
     832        """Set default values for variables that can also be defined 
     833        in the config file. 
     834        """ 
     835        pass 
    236836 
    237837    def get_config(self, config_dir=None): 
    238         """ 
    239         Retrieve values from configuration file 
    240         """ 
    241         self.config = get_pkg_config(self.short_pkg_name, config_dir) 
    242         for config_var in ["INDEX_PYPI_DOWNLOAD", "INDEX_PYPI_DISTANCE", 
    243             "INDEX_URL_DOWNLOAD", "INDEX_UNPACK", "INDEX_UNPACK_DIR", 
    244             "INDEX_INSTALL", "INDEX_FILE_CRITICAL", "INDEX_FILE", 
    245             "INDEX_REQUIRED_FILES", "INDEX_FILE_PYC", 
    246             "INDEX_FILE_PYO", 
    247             "INDEX_DIR_CRITICAL", "INDEX_DIR", "INDEX_DIR_EMPTY", 
    248             "MAX_INDEX_DOCSTRINGS", "MAX_INDEX_PYLINT", 
    249             "cheese_files", "critical_cheese_files",  
    250             "cheese_dirs", "critical_cheese_dirs", 
    251             ]: 
    252             value = self.config.get(config_var) 
    253             if value: setattr(self, config_var, value) 
     838        """Retrieve values from configuration file. 
     839        """ 
     840        pass 
    254841 
    255842    def determine_pkg_name(self): 
     
    263850 
    264851    def get_package_from_url(self): 
    265         """ 
    266         Use ``urlparse`` to obtain package path from URL 
     852        """Use ``urlparse`` to obtain package path from URL. 
    267853        """ 
    268854        (scheme,location,path,param,query,fragment_id) = urlparse(self.url) 
    269855        return self.get_package_from_path(path) 
    270856 
    271  
    272857    def get_package_from_path(self, path): 
    273         """ 
    274         Get package name as file portion of path 
     858        """Get package name as file portion of path. 
    275859        """ 
    276860        dir, file = os.path.split(path) 
     
    284868 
    285869    def configure_logging(self, logfile=None): 
    286         """ 
    287         Default settings for logging 
    288  
    289         if verbose, log goes to console, else it goes to logfile 
     870        """Default settings for logging. 
     871 
     872        If verbose, log goes to console, else it goes to logfile 
    290873        log.debug goes to logfile 
    291874        log.info goes to console 
     
    315898        self.log.debug("package = ", self.short_pkg_name) 
    316899 
    317     def init_indexes(self): 
    318         """ 
    319         Initialize variables used in index computation 
    320  
    321         * cheesecake_index: overall index for the package 
    322         * index: dict holding Index or CompositeIndex objects of various types 
    323         """ 
    324         self.cheesecake_index = 0 
    325         self.cheesecake_index_installability = 0 
    326         self.cheesecake_index_documentation = 0 
    327         self.cheesecake_index_codekwalitee = 0 
    328         self.max_cheesecake_index = self.INDEX_PYPI_DOWNLOAD + \ 
    329                                     self.INDEX_UNPACK + \ 
    330                                     self.INDEX_UNPACK_DIR + \ 
    331                                     self.INDEX_INSTALL + \ 
    332                                     self.MAX_INDEX_DOCSTRINGS + \ 
    333                                     self.MAX_INDEX_PYLINT  
    334 #                                    self.MAX_INDEX_UNITTESTS 
    335         self.max_cheesecake_index_installability = self.INDEX_PYPI_DOWNLOAD + \ 
    336                                             self.INDEX_UNPACK + \ 
    337                                             self.INDEX_UNPACK_DIR + \ 
    338                                             self.INDEX_INSTALL 
    339         self.max_cheesecake_index_documentation = self.INDEX_REQUIRED_FILES + \ 
    340                                         self.MAX_INDEX_DOCSTRINGS + \ 
    341                                         self.MAX_INDEX_DOCFORMAT 
    342         self.max_cheesecake_index_codekwalitee = self.MAX_INDEX_PYLINT  
    343 #                                        self.MAX_INDEX_UNITTESTS 
    344                                          
    345         self.index = {} 
    346         for index_type in ["file", "dir"]: 
    347             self.index[index_type] = CompositeIndex(index_type) 
    348         for index_type in ["pypi_download", "url_download",  
    349                            "unpack_dir", "unpack", "install", 
    350                            "docstrings", "doc_format", "unittests", "pylint"]: 
    351             self.index[index_type] = Index(index_type) 
    352  
    353         for cheese_file in self.cheese_files: 
    354             self.index["file"].set_index(name=cheese_file, details="file not found") 
    355             if cheese_file in self.critical_cheese_files: 
    356                 self.max_cheesecake_index += self.INDEX_FILE_CRITICAL 
    357                 self.max_cheesecake_index_documentation += self.INDEX_FILE_CRITICAL 
    358             else: 
    359                 self.max_cheesecake_index += self.INDEX_FILE 
    360                 self.max_cheesecake_index_documentation += self.INDEX_FILE 
    361         self.log.debug("cheese_files: " + ",".join(self.cheese_files)) 
    362         self.log.debug("critical_cheese_files: " + ",".join(self.critical_cheese_files)) 
    363  
    364         for cheese_dir in self.cheese_dirs: 
    365             self.index["dir"].set_index(name=cheese_dir, details="directory not found") 
    366             if cheese_dir in self.critical_cheese_dirs: 
    367                 self.max_cheesecake_index += self.INDEX_DIR_CRITICAL 
    368                 self.max_cheesecake_index_documentation += self.INDEX_DIR_CRITICAL 
    369             else: 
    370                 self.max_cheesecake_index += self.INDEX_DIR 
    371                 self.max_cheesecake_index_documentation += self.INDEX_DIR 
    372         self.log.debug("cheese_dirs: " + ",".join(self.cheese_dirs)) 
    373         self.log.debug("critical_cheese_dirs: " + ",".join(self.critical_cheese_dirs)) 
    374  
    375         self.pkg_files = {} 
    376         self.pkg_dirs = {} 
    377         self.file_types = ["py", "pyc", "pyo", "test"] 
    378         for type in self.file_types: 
    379             self.pkg_files[type] = [] 
    380  
    381         self.object_cnt = 0  # Number of modules/functions/classes/methods in .py files found 
    382         self.docstring_cnt = 0 
    383         self.docformat_cnt = 0 
    384         self.functions = [] # List of methods/functions found in .py files 
    385  
    386900    def retrieve_pkg(self): 
    387901        if self.name: 
     
    393907 
    394908    def get_package_from_url(self): 
    395         """ 
    396         Use ``urlparse`` to obtain package path from URL 
     909        """Use ``urlparse`` to obtain package path from URL. 
    397910        """ 
    398911        (scheme,location,path,param,query,fragment_id) = urlparse(self.url) 
    399         return self.get_package_from_path(path) 
    400          
     912        return self.get_package_from_path(path)         
    401913 
    402914    def get_package_from_path(self, path): 
    403         """ 
    404         Get package name as file portion of path 
     915        """Get package name as file portion of path. 
    405916        """ 
    4069