root/branches/mk/cheesecake/cheesecake_index.py

Revision 101, 55.0 kB (checked in by mk, 7 years ago)

Fixed a bug in IndexRequiredFiles? compute method (thanks to Will Guaraldi for report and solution).

  • Property svn:executable set to
Line 
1 #!/usr/bin/env python
2 """
3 Cheesecake: How tasty is your code?
4
5 The idea of the Cheesecake project is to rank Python packages
6 based on various empiric "kwalitee" factors, such as:
7
8         * whether the package can be downloaded
9         * whether the package can be unpacked
10         * whether the package can be installed into an alternate directory
11         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
12         * existence of certain directories such as doc, test, demo, examples
13         * percentage of modules/functions/classes/methods with docstrings
14         * percentage of functions/methods that are unit tested
15         * average pylint score for all non-test and non-demo modules
16         * whether the package can be unpacked
17         * whether the package can be installed into an alternate directory
18 """
19
20 import os
21 import re
22 import shutil
23 import sys
24 import tempfile
25
26 from optparse import OptionParser
27 from urllib import urlretrieve
28 from urlparse import urlparse
29 from math import ceil
30
31 import logger
32
33 from util import pad_with_dots, pad_left_spaces, pad_right_spaces, pad_msg, pad_line
34 from util import run_cmd, command_successful
35 from util import unzip_package, untar_package, unegg_package
36 from util import mkdirs
37 from util import StdoutRedirector
38 from util import time_function
39 from config import get_pkg_config
40 from codeparser import CodeParser
41
42 __docformat__ = 'reStructuredText en'
43
44
45 ################################################################################
46 ## Helpers.
47 ################################################################################
48
49 if 'sorted' not in dir(__builtins__):
50     def sorted(L):
51         new_list = L[:]
52         new_list.sort()
53         return new_list
54
55 if 'set' not in dir(__builtins__):
56     from sets import Set as set
57
58 def isiterable(obj):
59     """Check whether object is iterable.
60
61     >>> isiterable([1,2,3])
62     True
63     >>> isiterable("string")
64     True
65     >>> isiterable(object)
66     False
67     """
68     return hasattr(obj, '__iter__') or isinstance(obj, basestring)
69
70 def has_extension(filename, ext):
71     """Check if filename has given extension.
72
73     >>> has_extension("foobar.py", ".py")
74     True
75     >>> has_extension("foo.bar.py", ".py")
76     True
77     >>> has_extension("foobar.pyc", ".py")
78     False
79
80     This function is case insensitive.
81         >>> has_extension("FOOBAR.PY", ".py")
82         True
83     """
84     return os.path.splitext(filename.lower())[1] == ext.lower()
85
86 def discover_file_type(filename):
87     """Discover type of a file according to its name and its parent directory.
88
89     Currently supported file types:
90         * pyc
91         * pyo
92         * module: .py files of an application
93         * demo: .py files for documentation/demonstration purposes
94         * test: .py files used for testing
95         * special: .py file for special purposes
96
97     :Note: This function only check file's name, and doesn't touch the
98            filesystem. If you have to, check if file exists by yourself.
99
100     >>> discover_file_type('module.py')
101     'module'
102     >>> discover_file_type('./setup.py')
103     'special'
104     >>> discover_file_type('some/directory/junk.pyc')
105     'pyc'
106     >>> discover_file_type('examples/readme.txt')
107     >>> discover_file_type('examples/runthis.py')
108     'demo'
109     >>> discover_file_type('optimized.pyo')
110     'pyo'
111
112     >>> test_files = ['ut/test_this_and_that.py',
113     ...               'another_test.py',
114     ...               'TEST_MY_MODULE.PY']
115     >>> for filename in test_files:
116     ...     assert discover_file_type(filename) == 'test', filename
117
118     >>> discover_file_type('this_is_not_a_test_really.py')
119     'module'
120     """
121     dirs = filename.split(os.path.sep)
122     dirs, filename = dirs[:-1], dirs[-1]
123
124     if filename in ["setup.py", "ez_setup.py", "__pkginfo__.py"]:
125         return 'special'
126
127     if has_extension(filename, ".pyc"):
128         return 'pyc'
129     if has_extension(filename, ".pyo"):
130         return 'pyo'
131     if has_extension(filename, ".py"):
132         for dir in dirs:
133             if dir in ['test', 'tests']:
134                 return 'test'
135             elif dir in ['doc', 'docs', 'demo', 'example', 'examples']:
136                 return 'demo'
137
138         # Most test frameworks look for files starting with "test_".
139         # py.test also looks at files with trailing "_test".
140         if filename.lower().startswith('test_') or \
141                os.path.splitext(filename)[0].lower().endswith('_test'):
142             return 'test'
143
144         return 'module'
145
146 def get_files_of_type(file_list, file_type):
147     """Return files from `file_list` that match given `file_type`.
148
149     >>> file_list = ['test/test_foo.py', 'setup.py', 'README', 'test/test_bar.py']
150     >>> get_files_of_type(file_list, 'test')
151     ['test/test_foo.py', 'test/test_bar.py']
152     """
153     return filter(lambda x: discover_file_type(x) == file_type, file_list)
154
155 def get_package_name_from_path(path):
156     """Get package name as file portion of path.
157
158     >>> get_package_name_from_path('/some/random/path/package.tar.gz')
159     'package.tar.gz'
160     >>> get_package_name_from_path('/path/underscored_name.zip')
161     'underscored_name.zip'
162     >>> get_package_name_from_path('/path/unknown.extension.txt')
163     'unknown.extension.txt'
164     """
165     dir, filename = os.path.split(path)
166     return filename
167
168 def get_package_name_from_url(url):
169     """Use ``urlparse`` to obtain package name from URL.
170
171     >>> get_package_name_from_url('http://www.example.com/file.tar.bz2')
172     'file.tar.bz2'
173     >>> get_package_name_from_url('https://www.example.com/some/dir/file.txt')
174     'file.txt'
175     """
176     (scheme,location,path,param,query,fragment_id) = urlparse(url)
177     return get_package_name_from_path(path)
178
179 def get_package_name_and_type(package, known_extensions):
180     """Return package name and type.
181
182     Package type must exists in known_extensions list. Otherwise None is
183     returned.
184
185     >>> extensions = ['tar.gz', 'zip']
186     >>> get_package_name_and_type('underscored_name.zip', extensions)
187     ('underscored_name', 'zip')
188     >>> get_package_name_and_type('unknown.extension.txt', extensions)
189     """
190     for package_type in known_extensions:
191         if package.endswith('.'+package_type):
192             # Package name is name of package without file extension (ex. twill-7.3).
193             return package[:package.rfind('.'+package_type)], package_type
194
195 def get_method_arguments(method):
196     """Return tuple of arguments for given method, excluding self.
197
198     >>> class Class:
199     ...     def method(s, arg1, arg2, other_arg):
200     ...         pass
201     >>> get_method_arguments(Class.method)
202     ('arg1', 'arg2', 'other_arg')
203     """
204     return method.func_code.co_varnames[1:method.func_code.co_argcount]
205
206 def get_attributes(obj, names):
207     """Return attributes dictionary with keys from `names`.
208
209     Object is queried for each attribute name, if it doesn't have this
210     attribute, default value None will be returned.
211
212     >>> class Class:
213     ...     pass
214     >>> obj = Class()
215     >>> obj.attr = True
216     >>> obj.value = 13
217     >>> obj.string = "Hello"
218
219     >>> d = get_attributes(obj, ['attr', 'string', 'other'])
220     >>> d == {'attr': True, 'string': "Hello", 'other': None}
221     True
222     """
223     attrs = {}
224
225     for name in names:
226         attrs[name] = getattr(obj, name, None)
227
228     return attrs
229
230 def camel2underscore(name):
231     """Convert name from CamelCase to underscore_name.
232
233     >>> camel2underscore('CamelCase')
234     'camel_case'
235     >>> camel2underscore('already_underscore_name')
236     'already_underscore_name'
237     >>> camel2underscore('BigHTMLClass')
238     'big_html_class'
239     >>> camel2underscore('')
240     ''
241     """
242     if name and name[0].upper:
243         name = name[0].lower() + name[1:]
244
245     def capitalize(match):
246         string = match.group(1).lower().capitalize()
247         return string[:-1] + string[-1].upper()
248
249     def underscore(match):
250         return '_' + match.group(1).lower()
251
252     name = re.sub(r'([A-Z]+)', capitalize, name)
253     return re.sub(r'([A-Z])', underscore, name)
254
255 def index_class_to_name(clsname):
256     """Covert index class name to index name.
257
258     >>> index_class_to_name("IndexDownload")
259     'download'
260     >>> index_class_to_name("IndexUnitTests")
261     'unit_tests'
262     >>> index_class_to_name("IndexPyPIDownload")
263     'py_pi_download'
264     """
265     return camel2underscore(clsname.replace('Index', '', 1))
266
267 def is_empty(path):
268     """Returns True if file or directory pointed by `path` is empty.
269     """
270     if os.path.isfile(path) and os.path.getsize(path) == 0:
271         return True
272     if os.path.isdir(path) and os.listdir(path) == []:
273         return True
274
275     return False
276
277 def strip_dir_part(path, root):
278     """Strip `root` part from `path`.
279
280     >>> strip_dir_part('/home/ruby/file', '/home')
281     'ruby/file'
282     >>> strip_dir_part('/home/ruby/file', '/home/')
283     'ruby/file'
284     >>> strip_dir_part('/home/ruby/', '/home')
285     'ruby/'
286     >>> strip_dir_part('/home/ruby/', '/home/')
287     'ruby/'
288     """
289     path = path.replace(root, '', 1)
290
291     if path.startswith(os.path.sep):
292         path = path[1:]
293
294     return path
295
296 def get_files_dirs_list(root):
297     """Return list of all files and directories below `root`.
298
299     Root directory is excluded from files/directories paths.
300     """
301     files = []
302     directories = []
303
304     for dirpath, dirnames, filenames in os.walk(root):
305         dirpath = strip_dir_part(dirpath, root)
306         files.extend(map(lambda x: os.path.join(dirpath, x), filenames))
307         directories.extend(map(lambda x: os.path.join(dirpath, x), dirnames))
308
309     return files, directories
310
311 ################################################################################
312 ## Main index class.
313 ################################################################################
314
315 class NameSetter(type):
316     def __init__(cls, name, bases, dict):
317         if 'name' not in dict:
318             setattr(cls, 'name', name)
319
320         if 'compute_with' in dict:
321             orig_compute_with = cls.compute_with
322
323             def _timed_compute_with(self, cheesecake):
324                 (ret, self.time_taken) = time_function(lambda: orig_compute_with(self, cheesecake))
325                 self.cheesecake.log.debug("Index %s computed in %.2f seconds." % (self.name, self.time_taken))
326                 return ret
327
328             setattr(cls, 'compute_with', _timed_compute_with)
329
330     def __repr__(cls):
331         return '<Index class: %s>' % cls.name
332
333 def make_indices_dict(indices):
334     indices_dict = {}
335     for index in indices:
336         indices_dict[index.name] = index
337     return indices_dict
338
339 class Index(object):
340     """Class describing one index.
341
342     Use it as a container index or subclass to create custom indices.
343
344     During class initialization, special attribute `name` is magically
345     set based on class name. See `NameSetter` definitions for details.
346     """
347     __metaclass__ = NameSetter
348
349     subindices = None
350
351     name = "unnamed"
352     value = -1
353     details = ""
354     info = ""
355
356     def __init__(self, *indices):
357         # When indices are given explicitly they override the default.
358         if indices:
359             self.subindices = []
360             self._indices_dict = {}
361             for index in indices:
362                 self.add_subindex(index)
363         else:
364             if self.subindices:
365                 new_subindices = []
366                 for index in self.subindices:
367                     # index must be a class subclassing from Index.
368                     assert isinstance(index, type)
369                     assert issubclass(index, Index)
370                     new_subindices.append(index())
371                 self.subindices = new_subindices
372             else:
373                 self.subindices = []
374             # Create dictionary for fast reference.
375             self._indices_dict = make_indices_dict(self.subindices)
376
377         self._compute_arguments = get_method_arguments(self.compute)
378
379     def _iter_indices(self):
380         """Iterate over each subindex and yield their values.
381         """
382         for index in self.subindices:
383             # Pass Cheesecake instance to other indices.
384             yield index.compute_with(self.cheesecake)
385             # Print index info after computing.
386             if not self.cheesecake.quiet:
387                 index.print_info()
388
389     def compute_with(self, cheesecake):
390         """Take given Cheesecake instance and compute index value.
391         """
392         self.cheesecake = cheesecake
393         return self.compute(**get_attributes(cheesecake, self._compute_arguments))
394
395     def compute(self):
396         """Compute index value and return it.
397
398         By default this method computes sum of all subindices. Override this
399         method when subclassing for different behaviour.
400
401         Parameters to this function are dynamically prepared with use of
402         `get_attributes` function.
403
404         :Warning: Don't use *args and **kwds arguments for this method.
405         """
406         self.value = sum(self._iter_indices())
407         return self.value
408
409     def decide(self, cheesecake, when):
410         """Decide if this index should be computed.
411
412         If index has children, it will automatically remove all for which
413         decide() return false.
414         """
415         if self.subindices:
416             # Iterate over copy, as we may remove some elements.
417             for index in self.subindices[:]:
418                 if not getattr(index, 'decide_' + when)(cheesecake):
419                     self.remove_subindex(index.name)
420             return self.subindices
421         return True
422
423     def decide_before_download(self, cheesecake):
424         return self.decide(cheesecake, 'before_download')
425
426     def decide_after_download(self, cheesecake):
427         return self.decide(cheesecake, 'after_download')
428
429     def _get_max_value(self):
430         if self.subindices:
431             return sum(map(lambda index: index.max_value,
432                            self.subindices))
433         return 0
434
435     max_value = property(_get_max_value)
436
437     def _get_requirements(self):
438         if self.subindices:
439             return list(self._compute_arguments) + \
440                    reduce(lambda x,y: x + y.requirements, self.subindices, [])
441         return list(self._compute_arguments)
442
443     requirements = property(_get_requirements)
444
445     def add_subindex(self, index):
446         """Add subindex.
447
448         :Parameters:
449           `index` : Index instance
450               Index instance for inclusion.
451         """
452         if not isinstance(index, Index):
453             raise ValueError("subindex have to be instance of Index")
454
455         self.subindices.append(index)
456         self._indices_dict[index.name] = index
457
458     def remove_subindex(self, index_name):
459         """Remove subindex (refered by name).
460
461         :Parameters:
462           `index` : Index name
463               Index name to be removed.
464         """
465         index = self._indices_dict[index_name]
466         self.subindices.remove(index)
467         del self._indices_dict[index_name]
468
469     def _print_info_one(self):
470         print "%s  (%s)" % (pad_msg(index_class_to_name(self.name), self.value), self.details)
471
472     def _print_info_many(self):
473         max_value = self.max_value
474         if max_value == 0:
475             return
476
477         percentage = int(ceil(float(self.value) / float(max_value) * 100))
478         print pad_line("-")
479
480         print pad_msg("%s INDEX (ABSOLUTE)" % self.name, self.value)
481         msg = pad_msg("%s INDEX (RELATIVE)" % self.name, percentage)
482         msg += "  (%d out of a maximum of %d points is %d%%)" %\
483                (self.value, max_value, percentage)
484
485         print msg
486         print
487
488     def print_info(self):
489         """Print index name padded with dots, followed by value and details.
490         """
491         if self.subindices:
492             self._print_info_many()
493         else:
494             self._print_info_one()
495
496     def __getitem__(self, name):
497         return self._indices_dict[name]
498
499     def get_info(self):
500         if self.subindices:
501             return ''.join(map(lambda index: index.get_info(), self.subindices))
502         return self.info
503
504 ################################################################################
505 ## Index that computes scores based on files and directories.
506 ################################################################################
507
508 class OneOf(object):
509     def __init__(self, *possibilities):
510         self.possibilities = possibilities
511     def __str__(self):
512         return '/'.join(map(lambda x: str(x), self.possibilities))
513
514 def WithOptionalExt(name, extensions):
515     """Handy way of writing Cheese rules for files with extensions.
516
517     Instead of writing:
518         >>> one_of = OneOf('readme', 'readme.html', 'readme.txt')
519
520     Write this:
521         >>> opt_ext = WithOptionalExt('readme', ['html', 'txt'])
522
523     It means the same! (representation have a meaning)
524         >>> str(one_of) == str(opt_ext)
525         True
526     """
527     possibilities = [name]
528     possibilities.extend(map(lambda x: name + '.' + x, extensions))
529
530     return OneOf(*possibilities)
531
532 def Doc(name):
533     return WithOptionalExt(name, ['html', 'txt'])
534
535 class FilesIndex(Index):
536     _used_rules = []
537
538     def _compute_from_rules(self, files_list, package_dir, files_rules):
539         self._used_rules = []
540         files_count = 0
541         value = 0
542
543         for filename in files_list:
544             if not is_empty(os.path.join(package_dir, filename)):
545                 score = self.get_score(os.path.basename(filename), files_rules)
546                 if score != 0:
547                     value += score
548                     files_count += 1
549
550         return files_count, value
551
552     def get_score(self, name, specs):
553         for entry, value in specs.iteritems():
554             if self.match_filename(name, entry):
555                 self.cheesecake.log.debug("%d points entry found: %s (%s)" % \
556                                           (value, name, entry))
557                 return value
558
559         return 0
560
561     def get_not_used(self, files_rules):
562         """Get only these of files_rules that didn't match during computation.
563
564         >>> rules = {
565         ...     Doc('readme'): 30,
566         ...     OneOf(Doc('license'), Doc('copying')): 30,
567         ...     'demo': 10,
568         ... }
569         >>> index = FilesIndex()
570         >>> index._used_rules.append('demo')
571         >>> map(lambda x: str(x), index.get_not_used(rules.keys()))
572         ['license/license.html/license.txt/copying/copying.html/copying.txt', 'readme/readme.html/readme.txt']
573         """
574         return filter(lambda rule: rule not in self._used_rules,
575                       files_rules)
576
577     def match_filename(self, name, rule):
578         """Check if `name` matches given `rule`.
579         """
580         def equal(x, y):
581             x_root, x_ext = os.path.splitext(x)
582             y_root, y_ext = os.path.splitext(y.lower())
583             if x_root in [y_root.lower(), y_root.upper(), y_root.capitalize()] \
584                    and x_ext in [y_ext.lower(), y_ext.upper()]:
585                 return True
586             return False
587
588         if rule in self._used_rules:
589             return False
590
591         if isinstance(rule, basestring):
592             if equal(name, rule):
593                 self._used_rules.append(rule)
594                 return True
595         elif isinstance(rule, OneOf):
596             for poss in rule.possibilities:
597                 if self.match_filename(name, poss):
598                     self._used_rules.append(rule)
599                     return True
600
601         return False
602
603 ################################################################################
604 ## Installability index.
605 ################################################################################
606
607 class IndexUrlDownload(Index):
608     max_value = 25
609
610     def compute(self, downloaded_from_url, package, url):
611         if downloaded_from_url:
612             self.details = "downloaded package %s from URL %s"  % (package, url)
613             self.value = self.max_value
614         else:
615             self.value = 0
616
617         return self.value
618
619     def decide_before_download(self, cheesecake):
620         return cheesecake.url
621
622 class IndexUnpack(Index):
623     max_value = 25
624
625     def compute(self, unpacked):
626         if unpacked:
627             self.details = "package unpacked successfully"
628             self.value = self.max_value
629         else:
630             self.value = 0
631
632         return self.value
633
634 class IndexUnpackDir(Index):
635     max_value = 15
636
637     def compute(self, unpack_dir, original_package_name):
638         self.details = "unpack directory is " + unpack_dir
639
640         if original_package_name:
641             self.details += " instead of the expected " + original_package_name
642             self.value = 0
643         else:
644             self.details += " as expected"
645             self.value = self.max_value
646
647         return self.value
648
649     def decide_after_download(self, cheesecake):
650         return cheesecake.package_type != 'egg'
651
652 class IndexSetupPy(FilesIndex):
653     name = "setup.py"
654     max_value = 25
655
656     files_rules = {
657         'setup.py': 25,
658     }
659
660     def compute(self, files_list, package_dir):
661         setup_py_found, self.value = self._compute_from_rules(files_list, package_dir, self.files_rules)
662
663         if setup_py_found:
664             self.details = "setup.py found"
665         else:
666             self.details = "setup.py not found"
667
668         return self.value
669
670     def decide_after_download(self, cheesecake):
671         return cheesecake.package_type != 'egg'
672
673 class IndexInstall(Index):
674     max_value = 50
675
676     def compute(self, installed, sandbox_install_dir):
677         if installed:
678             self.details = "package installed in %s" % sandbox_install_dir
679             self.value = self.max_value
680         else:
681             self.details = "could not install package in %s" % sandbox_install_dir
682             self.value = 0
683
684         return self.value
685
686     def decide_before_download(self, cheesecake):
687         return not cheesecake.static_only
688
689 class IndexPyPIDownload(Index):
690     max_value = 50
691     distance_penalty = -5
692
693     def compute(self, package, found_on_cheeseshop, found_locally, distance_from_pypi, download_url):
694         if download_url:
695             self.value = self.max_value
696
697             self.details = "downloaded package " + package
698
699             if not found_on_cheeseshop:
700                 self.value += (distance_from_pypi - 1) * self.distance_penalty
701
702                 if distance_from_pypi:
703                     self.details += " following %d link" % distance_from_pypi
704                     if distance_from_pypi > 1:
705                         self.details += "s"
706                         self.details += " from PyPI"
707                     else:
708                         self.details += " from " + download_url
709             else:
710                 self.details += " directly from the Cheese Shop"
711         else:
712             if found_locally:
713                 self.details = "found on local filesystem"
714             self.value = 0
715
716         return self.value
717
718     def decide_before_download(self, cheesecake):
719         return cheesecake.name
720
721 class IndexGeneratedFiles(Index):
722     generated_files_penalty = -20
723     max_value = 0
724
725     def compute(self, files_list):
726         self.value = 0
727
728         pyc_files = len(get_files_of_type(files_list, 'pyc'))
729         pyo_files = len(get_files_of_type(files_list, 'pyo'))
730
731         if pyc_files > 0 or pyo_files > 0:
732             self.value += self.generated_files_penalty
733
734         self.details = "%d .pyc and %d .pyo files found" % \
735                                   (pyc_files, pyo_files)
736
737         return self.value
738
739     def decide_after_download(self, cheesecake):
740         return cheesecake.package_type != 'egg'
741
742 class IndexInstallability(Index):
743     name = "INSTALLABILITY"
744
745     subindices = [
746         IndexPyPIDownload,
747         IndexUrlDownload,
748         IndexUnpack,
749         IndexUnpackDir,
750         IndexSetupPy,
751         IndexInstall,
752         IndexGeneratedFiles,
753     ]
754
755 ################################################################################
756 ## Documentation index.
757 ################################################################################
758
759 class IndexRequiredFiles(FilesIndex):
760     cheese_files = {
761         Doc('readme'): 30,
762         OneOf(Doc('license'), Doc('copying')): 30,
763
764         OneOf(Doc('announce'), Doc('changelog')): 20,
765         Doc('install'): 20,
766
767         Doc('authors'): 10,
768         Doc('faq'): 10,
769         Doc('news'): 10,
770         Doc('thanks'): 10,
771         Doc('todo'): 10,
772     }
773
774     cheese_dirs = {
775         OneOf('doc', 'docs'): 30,
776         OneOf('test', 'tests'): 30,
777
778         'demo': 10,
779         OneOf('example', 'examples'): 10,
780     }
781
782     max_value = sum(cheese_files.values() + cheese_dirs.values())
783
784     def compute(self, files_list, dirs_list, package_dir):
785         # Inform user of files and directories the package is missing.
786         def make_info(dictionary, what):
787             missing = self.get_not_used(dictionary.keys())
788             importance = {30: ' critical', 20: ' important', 10: ''}
789             info = []
790
791             positive_msg = "[%s] Package has%s %s: %s.\n"
792             negative_msg = "[%s] Package doesn't have%s %s: %s.\n"
793
794             for key in dictionary.keys():
795                 msg = positive_msg
796                 if key in missing:
797                     msg = negative_msg
798                 info.append(msg % (index_class_to_name(self.name), importance[dictionary[key]], what, str(key)))
799
800             return ''.join(info)
801
802         # Compute required files.
803         files_count, files_value = self._compute_from_rules(files_list, package_dir, self.cheese_files)
804         self.info = make_info(self.cheese_files, 'file')
805
806         # Compute required directories.
807         dirs_count, dirs_value = self._compute_from_rules(dirs_list, package_dir, self.cheese_dirs)
808         self.info += make_info(self.cheese_dirs, 'directory')
809
810         self.value = files_value + dirs_value
811
812         self.details = "%d files and %d required directories found" % \
813                        (files_count, dirs_count)
814
815         return self.value
816
817 class IndexDocstrings(Index):
818     max_value = 100
819
820     def compute(self, object_cnt, docstring_cnt):
821         percent = 0
822         if object_cnt > 0:
823             percent = float(docstring_cnt)/float(object_cnt)
824
825         # Scale the result.
826         self.value = int(ceil(percent * self.max_value))
827
828         self.details = "found %d/%d=%.2f%% objects with docstrings" %\
829                  (docstring_cnt, object_cnt, percent*100)
830
831         return self.value
832
833 class IndexFormattedDocstrings(Index):
834     max_value = 50
835
836     def compute(self, object_cnt, docformat_cnt):
837         percent = 0
838         if object_cnt > 0:
839             percent = float(docformat_cnt)/float(object_cnt)
840
841         # Scale the result.
842         # We give 20p for 25% of formatted docstrings, 35p for 50% and 50p for 75%.
843         self.value = 0
844         if percent > 0.75:
845             self.value = 50
846         elif percent > 0.50:
847             self.value = 35
848         elif percent > 0.25:
849             self.value = 20
850
851         self.details = "found %d/%d=%.2f%% objects with formatted docstrings" %\
852                  (docformat_cnt, object_cnt, percent*100)
853
854         return self.value
855
856 class IndexDocumentation(Index):
857     name = "DOCUMENTATION"
858
859     subindices = [
860         IndexRequiredFiles,
861         IndexDocstrings,
862         IndexFormattedDocstrings,
863     ]
864
865 ################################################################################
866 ## Code "kwalitee" index.
867 ################################################################################
868
869 class IndexUnitTests(Index):
870     """Compute unittest index as percentage of methods/functions
871     that are exercised in unit tests.
872     """
873     max_value = 50
874
875     def compute(self, files_list, functions, classes, package_dir):
876         unittest_cnt = 0
877         functions_tested = set()
878
879         # Gather all function names called from test files.
880         for testfile in get_files_of_type(files_list, 'test'):
881             fullpath = os.path.join(package_dir, testfile)
882             code = CodeParser(fullpath, self.cheesecake.log.debug)
883
884             functions_tested = functions_tested.union(code.functions_called)
885
886         for name in functions + classes:
887             if name in functions_tested:
888                 unittest_cnt += 1
889                 self.cheesecake.log.debug("%s is unit tested" % name)
890
891         functions_classes_cnt = len(functions) + len(classes)
892         percent = 0
893         if functions_classes_cnt > 0:
894             percent = float(unittest_cnt)/float(functions_classes_cnt)
895
896         # Scale the result.
897         self.value = int(ceil(percent * self.max_value))
898
899         self.details = "found %d/%d=%.2f%% unit tested classes/methods/functions." %\
900                  (unittest_cnt, functions_classes_cnt, percent*100)
901
902         return self.value
903
904 class IndexUseTestFramework(Index):
905     """Check if a package uses any of known test frameworks.
906
907     Currently only checking for doctest.
908     """
909     max_value = 30
910
911     def compute(self, doctests_count, files_list, classes, methods):
912         frameworks_found = False
913
914         if doctests_count > 0:
915             frameworks_found = True
916
917         if get_files_of_type(files_list, 'test'):
918             frameworks_found = True
919
920         for method in methods:
921             if self._is_test_method(method):
922                 frameworks_found = True
923                 break
924
925         if frameworks_found:
926             self.value = self.max_value
927             self.details = "use one or more of known test frameworks"
928         else:
929             self.value = 0
930             self.details = "don't use any of known test frameworks"
931
932         return self.value
933
934     def _is_test_method(self, method):
935         nose_methods = ['setup', 'setup_package', 'setup_module', 'setUp',
936                         'setUpPackage', 'setUpModule',
937                         'teardown', 'teardown_package', 'teardown_module',
938                         'tearDown', 'tearDownModule', 'tearDownPackage']
939
940         for test_method in nose_methods:
941             if method.endswith(test_method):
942                 return True
943         return False
944
945 class IndexPyLint(Index):
946     """Compute pylint index as average of positive pylint scores obtained for
947     the Python files identified in the package.
948     """
949     name = "pylint"
950     max_value = 50
951
952     disabled_messages = [
953         'W0403', # relative import
954         'W0406', # importing of self
955     ]
956     pylint_args = ' '.join(map(lambda x: '--disable-msg=%s' % x, disabled_messages))
957
958     def compute(self, files_list, package_dir):
959         self.value = 0
960
961         # Try to run the pylint script
962         if not command_successful("pylint --version"):
963             self.details = "pylint not properly installed"
964             return self.value
965
966         pylint_value = 0
967         cnt = 0
968
969         # wbg: switching cwd so that pylint works correctly regarding
970         # running it on individual modules
971         original_cwd = os.getcwd()
972         # change dir to top of the package tree
973         # package_dir may be a file if the archive contains a single file...
974         # if this is the case, change dir to the parent dir of that file
975         if os.path.isfile(package_dir):
976             package_dir = os.path.dirname(package_dir)
977         os.chdir(package_dir)
978
979         for pyfile in get_files_of_type(files_list, 'module'):
980             # wbg: because we change the working dir, we can use relative
981             # paths which pylint is happier about.
982             # fullpath = os.path.join(package_dir, pyfile)
983             fullpath = pyfile
984             path, filename = os.path.split(fullpath)
985             module, ext = os.path.splitext(filename)
986
987             self.cheesecake.log.debug("Running pylint on file " + fullpath)
988             rc, output = run_cmd("pylint %s %s" % (self.pylint_args, fullpath))
989             if rc:
990                 self.cheesecake.log.debug("encountered an error (%d)." % rc)
991                 continue
992
993             score_line = output.split("\n")[-3]
994             s = re.search(r" (\d+\.\d+)/10", score_line)
995
996             # We only take positive scores into account
997             if s:
998                 score = s.group(1)
999                 if score == "0.00":
1000                     self.cheesecake.log.debug("ignoring 0.00 score.")
1001                     continue
1002                 else:
1003                     self.cheesecake.log.debug("pylint score for module %s: %s" % (module, score))
1004                 pylint_value += float(score)
1005                 cnt += 1
1006
1007         # wbg: switching back to the original cwd in case that's important
1008         os.chdir(original_cwd)
1009
1010         avg_score = 0
1011         if cnt:
1012             avg_score = float(pylint_value)/float(cnt)
1013
1014         self.value = int(ceil(avg_score/10.0 * self.max_value))
1015         self.details = "average pylint score is %.2f out of 10" % avg_score
1016
1017         return self.value
1018
1019     def decide_before_download(self, cheesecake):
1020         return not cheesecake.lite
1021
1022 class IndexCodeKwalitee(Index):
1023     name = "CODE KWALITEE"
1024
1025     subindices = [
1026         IndexPyLint,
1027         IndexUnitTests,
1028         IndexUseTestFramework,
1029     ]
1030
1031 ################################################################################
1032 ## Main Cheesecake class.
1033 ################################################################################
1034
1035 class CheesecakeError(Exception):
1036     """Custom exception class for Cheesecake-specific errors.
1037     """
1038     pass
1039
1040
1041 class CheesecakeIndex(Index):
1042     name = "Cheesecake"
1043     subindices = [
1044         IndexInstallability,
1045         IndexDocumentation,
1046         IndexCodeKwalitee,
1047     ]
1048
1049
1050 class Step(object):
1051     """Single step during computation of package score.
1052     """
1053     def __init__(self, provides):
1054         self.provides = provides
1055
1056     def decide(self, cheesecake):
1057         """Decide if step should be run.
1058
1059         It checks if there's at least one index from current profile that need
1060         variables provided by this step. Override this method for other behaviour.
1061         """
1062         for provide in self.provides:
1063             if provide in cheesecake.index.requirements:
1064                 return True
1065         return False
1066
1067 class StepByVariable(Step):
1068     """Step which is always run if given Cheesecake instance variable is true.
1069     """
1070     def __init__(self, variable_name, provides):
1071         self.variable_name = variable_name
1072         Step.__init__(self, provides)
1073
1074     def decide(self, cheesecake):
1075         if getattr(cheesecake, self.variable_name, None):
1076             return True
1077
1078         # Fallback to the default.
1079         return Step.decide(self, cheesecake)
1080
1081 class Cheesecake(object):
1082     """Computes 'goodness' of Python packages.
1083
1084     Generates "cheesecake index" that takes into account things like:
1085
1086         * whether the package can be downloaded
1087         * whether the package can be unpacked
1088         * whether the package can be installed into an alternate directory
1089         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
1090         * existence of certain directories such as doc, test, demo, examples
1091         * percentage of modules/functions/classes/methods with docstrings
1092         * percentage of functions/methods that are unit tested
1093         * average pylint score for all non-test and non-demo modules
1094     """
1095
1096     steps = {}
1097
1098     package_types = {
1099         "tar.gz": untar_package,
1100         "tgz": untar_package,
1101         "zip": unzip_package,
1102         "egg": unegg_package,
1103     }
1104
1105     def __init__(self, name="", url="", path="", sandbox=None, config=None,
1106                  logfile=None, verbose=False, quiet=False, static_only=False,
1107                  lite=False):
1108         """Initialize critical variables, download and unpack package,
1109         walk package tree.
1110         """
1111         self.name = name
1112         self.url = url
1113         self.package_path = path
1114
1115         if self.name:
1116             self.package = self.name
1117         elif self.url:
1118             self.package = get_package_name_from_url(self.url)
1119         elif self.package_path:
1120             self.package = get_package_name_from_path(self.package_path)
1121         else:
1122             self.raise_exception("No package name, URL or path specified... exiting")
1123
1124         # Setup a sandbox.
1125         self.sandbox = sandbox or tempfile.mkdtemp(prefix='cheesecake')
1126         if not os.path.isdir(self.sandbox):
1127             os.mkdir(self.sandbox)
1128
1129         self.config = config
1130         self.verbose = verbose
1131         self.quiet = quiet
1132         self.static_only = static_only
1133         self.lite = lite
1134
1135         self.sandbox_pkg_file = ""
1136         self.sandbox_pkg_dir = ""
1137         self.sandbox_install_dir = ""
1138
1139         # Configure logging as soon as possible.
1140         self.configure_logging(logfile)
1141
1142         # Setup Cheesecake index.
1143         self.index = CheesecakeIndex()
1144
1145         self.index.decide_before_download(self)
1146         self.log.debug("Profile requirements: %s." % ', '.join(sorted(self.index.requirements)))
1147
1148         # Get the package.
1149         self.run_step('get_pkg_from_pypi')
1150         self.run_step('download_pkg')
1151         self.run_step('copy_pkg')
1152
1153         # Get package name and type.
1154         name_and_type = get_package_name_and_type(self.package, self.package_types.keys())
1155
1156         if not name_and_type:
1157             msg = "Could not determine package type for package '%s'" % self.package
1158             msg += "\nCurrently recognized types: " + ", ".join(self.package_types.keys())
1159             self.raise_exception(msg)
1160
1161         self.package_name, self.package_type = name_and_type
1162         self.log.debug("Package name: " + self.package_name)
1163         self.log.debug("Package type: " + self.package_type)
1164
1165         # Make last indices decisions.
1166         self.index.decide_after_download(self)
1167
1168         # Unpack package and list its files.
1169         self.run_step('unpack_pkg')
1170         self.run_step('walk_pkg')
1171
1172         # Install package.
1173         self.run_step('install_pkg')
1174
1175     def raise_exception(self, msg):
1176         """Cleanup, print error message and raise CheesecakeError.
1177
1178         Don't use logging, since it can be called before logging has been setup.
1179         """
1180         self.cleanup(remove_log_file=False)
1181
1182         msg += "\nDetailed info available in log file %s" % self.logfile
1183
1184         raise CheesecakeError(msg)
1185
1186     def cleanup(self, remove_log_file=True):
1187         """Delete temporary directories and files that were created
1188         in the sandbox. At the end delete the sandbox itself.
1189         """
1190         if os.path.isfile(self.sandbox_pkg_file):
1191             self.log("Removing file %s" % self.sandbox_pkg_file)
1192             os.unlink(self.sandbox_pkg_file)
1193
1194         def delete_dir(dirname):
1195             "Delete directory recursively and generate log message."
1196             if os.path.isdir(dirname):
1197                 self.log("Removing directory %s" % dirname)
1198                 shutil.rmtree(dirname)
1199
1200         delete_dir(self.sandbox)
1201
1202         if remove_log_file:
1203             os.unlink(os.path.join(self.sandbox, self.logfile))
1204
1205     def set_defaults(self):
1206         """Set default values for variables that can also be defined
1207         in the config file.
1208         """
1209         pass
1210
1211     def get_config(self, config_dir=None):
1212         """Retrieve values from configuration file.
1213         """
1214         pass
1215
1216     def configure_logging(self, logfile=None):
1217         """Default settings for logging.
1218
1219         If verbose, log goes to console, else it goes to logfile.
1220         log.debug and log.info goes to logfile.
1221         log.warn and log.error go to both logfile and stdout.
1222         """
1223         if logfile:
1224             self.logfile = logfile
1225         else:
1226             self.logfile = os.path.join(tempfile.gettempdir(), self.package + ".log")
1227
1228         logger.setconsumer('logfile', open(str(self.logfile), 'w', buffering=1))
1229         logger.setconsumer('console', logger.STDOUT)
1230         logger.setconsumer('null', None)
1231
1232         self.log = logger.MultipleProducer('cheesecake logfile')
1233         self.log.info = logger.MultipleProducer('cheesecake logfile')
1234         self.log.debug = logger.MultipleProducer('cheesecake logfile')
1235         self.log.warn = logger.MultipleProducer('cheesecake console')
1236         self.log.error = logger.MultipleProducer('cheesecake console')
1237
1238     def run_step(self, step_name):
1239         """Run step if its decide() method returns True.
1240         """
1241         step = self.steps[step_name]
1242         if step.decide(self):
1243             step_method = getattr(self, step_name)
1244             step_method()
1245
1246     steps['get_pkg_from_pypi'] = StepByVariable('name',
1247                                                 ['download_url',
1248                                                  'distance_from_pypi',
1249                                                  'found_on_cheeseshop',
1250                                                  'found_locally',
1251                                                  'sandbox_pkg_file'])
1252     def get_pkg_from_pypi(self):
1253         """Download package using setuptools utilities.
1254
1255         :Ivariables:
1256           download_url : str
1257               URL that package was downloaded from.
1258           distance_from_pypi : int
1259               How many hops setuptools had to make to download package.
1260           found_on_cheeseshop : bool
1261               Whenever package has been found on CheeseShop.
1262           found_locally : bool
1263               Whenever package has been already installed.
1264         """
1265         self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name)
1266
1267         try:
1268             from setuptools.package_index import PackageIndex
1269             from pkg_resources import Requirement
1270             from distutils import log
1271             from distutils.errors import DistutilsError
1272
1273         except ImportError, e:
1274             msg = "Error: setuptools is not installed and is required for downloading a package by name\n"
1275             msg += "You can download and process a package by its full URL via the -u or --url option\n"
1276             msg += "Example: python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz"
1277             self.raise_exception(msg)
1278
1279         def drop_setuptools_info(stdout, error=None):
1280             """Drop all setuptools output as INFO.
1281             """
1282             self.log.info("*** Begin setuptools output")
1283             map(self.log.info, stdout.splitlines())
1284             if error:
1285                 self.log.info(str(error))
1286             self.log.info("*** End setuptools output")
1287
1288         try:
1289             # Temporarily set the log verbosity to INFO so we can capture setuptools info messages
1290             old_threshold = log.set_threshold(log.INFO)
1291             pkgindex = PackageIndex()
1292             old_stdout = sys.stdout
1293             sys.stdout = StdoutRedirector()
1294             output = pkgindex.fetch(Requirement.parse(self.name),
1295                                     self.sandbox,
1296                                     force_scan=True,
1297                                     source=False)
1298             captured_stdout = sys.stdout.read_buffer()
1299             sys.stdout = old_stdout
1300             log.set_threshold(old_threshold)
1301
1302         except DistutilsError, e:
1303             # Bring back old stdout.
1304             captured_stdout = sys.stdout.read_buffer()
1305             sys.stdout = old_stdout
1306             log.set_threshold(old_threshold)
1307
1308             drop_setuptools_info(captured_stdout, error)
1309             self.raise_exception("Error: setuptools returned an error: %s\n" % e)
1310
1311         if output is None:
1312             drop_setuptools_info(captured_stdout)
1313             self.raise_exception("Error: Could not find distribution for " + self.name)
1314
1315         # Defaults.
1316         self.download_url = ""
1317         self.distance_from_pypi = 0
1318         self.found_on_cheeseshop = False
1319         self.found_locally = False
1320
1321         for line in captured_stdout.splitlines():
1322             s = re.search(r"Reading http(.*)", line)
1323             if s:
1324                 inspected_url = s.group(1)
1325                 if not re.search(r"www.python.org\/pypi", inspected_url):
1326                     self.distance_from_pypi += 1
1327                 continue
1328             s = re.search(r"Downloading (.*)", line)
1329             if s:
1330                 self.download_url = s.group(1)
1331                 break
1332
1333         self.sandbox_pkg_file = output
1334         self.package = get_package_name_from_path(output)
1335         self.log.info("Downloaded package %s from %s" % (self.package, self.download_url))
1336
1337         if os.path.isdir(self.sandbox_pkg_file):
1338             self.found_locally = True
1339
1340         if re.search(r"cheeseshop.python.org", self.download_url):
1341             self.found_on_cheeseshop = True
1342
1343     steps['download_pkg'] = StepByVariable('url',
1344                                            ['sandbox_pkg_file',
1345                                             'downloaded_from_url'])
1346     def download_pkg(self):
1347         """Use ``urllib.urlretrieve`` to download package to file in sandbox dir.
1348         """
1349         #self.log("Downloading package %s from URL %s" % (self.package, self.url))
1350         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1351         try:
1352             downloaded_filename, headers = urlretrieve(self.url, self.sandbox_pkg_file)
1353         except IOError, e:
1354             self.log.error("Error downloading package %s from URL %s"  % (self.package, self.url))
1355             self.raise_exception(str(e))
1356         #self.log("Downloaded package %s to %s" % (self.package, downloaded_filename))
1357
1358         if headers.gettype() in ["text/html"]:
1359             f = open(downloaded_filename)
1360             if re.search("404 Not Found", "".join(f.readlines())):
1361                 f.close()
1362                 self.raise_exception("Got '404 Not Found' error while trying to download package ... exiting")
1363             f.close()
1364
1365         self.downloaded_from_url = True
1366
1367     steps['copy_pkg'] = StepByVariable('package_path',
1368                                        ['sandbox_pkg_file'])
1369     def copy_pkg(self):
1370         """Copy package file to sandbox directory.
1371         """
1372         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1373         if not os.path.isfile(self.package_path):
1374             self.raise_exception("%s is not a valid file ... exiting" % self.package_path)
1375         self.log("Copying file %s to %s" % (self.package_path, self.sandbox_pkg_file))
1376         shutil.copyfile(self.package_path, self.sandbox_pkg_file)
1377
1378     steps['unpack_pkg'] = Step(['original_package_name',
1379                                 'sandbox_pkg_dir',
1380                                 'unpacked',
1381                                 'unpack_dir'])
1382     def unpack_pkg(self):
1383         """Unpack the package in the sandbox directory.
1384
1385         Check `package_types` attribute for list of currently supported
1386         archive types.
1387
1388         :Ivariables:
1389           original_package_name : str
1390         """
1391         self.sandbox_pkg_dir = os.path.join(self.sandbox, self.package_name)
1392         if os.path.isdir(self.sandbox_pkg_dir):
1393             shutil.rmtree(self.sandbox_pkg_dir)
1394
1395         # Call appropriate function to unpack the package.
1396         unpack = self.package_types[self.package_type]
1397         self.unpack_dir = unpack(self.sandbox_pkg_file, self.sandbox)
1398
1399         if self.unpack_dir is None:
1400             self.raise_exception("Could not unpack package %s ... exiting" % \
1401                                  self.sandbox_pkg_file)
1402
1403         self.unpacked = True
1404
1405         if self.unpack_dir != self.package_name:
1406             self.original_package_name = self.package_name
1407             self.package_name = self.unpack_dir
1408
1409     steps['walk_pkg'] = Step(['dirs_list',
1410                               'docstring_cnt',
1411                               'docformat_cnt',
1412                               'files_list',
1413                               'functions',
1414                               'classes',
1415                               'methods',
1416                               'object_cnt',
1417                               'package_dir'])
1418     def walk_pkg(self):
1419         """Get package files and directories.
1420
1421         :Ivariables:
1422           dirs_list : list
1423               List of directories package contains.
1424           docstring_cnt : int
1425               Number of docstrings found in all package objects.
1426           docformat_cnt : int
1427               Number of formatted docstrings found in all package objects.
1428           doctests_count : int
1429               Number of docstrings that include doctests.
1430           files_list : list
1431               List of files package contains.
1432           functions : list
1433               List of all functions defined in package sources.
1434           classes : list
1435               List of all classes defined in package sources.
1436           methods : list
1437               List of all methods defined in package sources.
1438           object_cnt : int
1439               Number of documentable objects found in all package modules.
1440           package_dir : str
1441               Path to project directory.
1442         """
1443         self.package_dir = os.path.join(self.sandbox, self.package_name)
1444
1445         self.files_list, self.dirs_list = get_files_dirs_list(self.package_dir)
1446
1447         self.object_cnt = 0
1448         self.docstring_cnt = 0
1449         self.docformat_cnt = 0
1450         self.doctests_count = 0
1451         self.functions = []
1452         self.classes = []
1453         self.methods = []
1454
1455         # Parse all application files and count objects
1456         # (modules/classes/functions) and their associated docstrings.
1457         for py_file in get_files_of_type(self.files_list, 'module'):
1458             pyfile = os.path.join(self.package_dir, py_file)
1459             code = CodeParser(pyfile, self.log.debug)
1460
1461             self.object_cnt += code.object_count()
1462             self.docstring_cnt += code.docstring_count()
1463             self.docformat_cnt += code.formatted_docstrings_count
1464             self.functions += code.functions
1465             self.classes += code.classes
1466             self.methods += code.methods
1467             self.doctests_count += code.doctests_count
1468
1469         # Log a bit of debugging info.
1470         self.log.debug("Found %d files: %s." % (len(self.files_list),
1471                                                 ', '.join(self.files_list)))
1472         self.log.debug("Found %d directories: %s." % (len(self.dirs_list),
1473                                                       ', '.join(self.dirs_list)))
1474
1475     steps['install_pkg'] = Step(['installed'])
1476     def install_pkg(self):
1477         """Verify that package can be installed in alternate directory.
1478
1479         :Ivariables:
1480           installed : bool
1481               Describes whenever package has been succefully installed.
1482         """
1483         self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name)
1484
1485         if self.package_type == 'egg':
1486             # Create dummy Python directories.
1487             mkdirs('%s/lib/python2.3/site-packages/' % self.sandbox_install_dir)
1488             mkdirs('%s/lib/python2.4/site-packages/' % self.sandbox_install_dir)
1489
1490             environment = {'PYTHONPATH':
1491                            '%(sandbox)s/lib/python2.3/site-packages/:'\
1492                            '%(sandbox)s/lib/python2.4/site-packages/' % \
1493                            {'sandbox': self.sandbox_install_dir}}
1494             rc, output = run_cmd("easy_install --no-deps --prefix %s %s" % \
1495                                  (self.sandbox_install_dir,
1496                                   self.sandbox_pkg_file),
1497                                  environment)
1498         else:
1499             package_dir = os.path.join(self.sandbox, self.package_name)
1500             if not os.path.isdir(package_dir):
1501                 package_dir = self.sandbox
1502             cwd = os.getcwd()
1503             os.chdir(package_dir)
1504             rc, output = run_cmd("python setup.py install --root=%s" % \
1505                                  self.sandbox_install_dir)
1506             os.chdir(cwd)
1507
1508         if rc:
1509             self.log('*** Installation failed. Captured output:')
1510             # Stringify output as it may be an exception.
1511             for output_line in str(output).splitlines():
1512                 self.log(output_line)
1513             self.log('*** End of captured output.')
1514         else:
1515             self.log('Installation into %s successful.' % \
1516                      self.sandbox_install_dir)
1517             self.installed = True
1518
1519     def compute_cheesecake_index(self):
1520         """Compute overall Cheesecake index for the package by adding up
1521         specific indexes.
1522         """
1523         # Recursively compute all indices.
1524         max_cheesecake_index = self.index.max_value
1525
1526         # Pass Cheesecake instance to the main Index object.
1527         cheesecake_index = self.index.compute_with(self)
1528         percentage = (cheesecake_index * 100) / max_cheesecake_index
1529
1530         self.log.info("A given package can currently reach a MAXIMUM number of %d points" % max_cheesecake_index)
1531         self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package))
1532
1533         # Print summary.
1534         if self.quiet:
1535             print "Cheesecake index: %d (%d / %d)" % (percentage,
1536                                                       cheesecake_index,
1537                                                       max_cheesecake_index)
1538         else:
1539             print
1540             print pad_line("=")
1541             print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", cheesecake_index)
1542             print "%s  (%d out of a maximum of %d points is %d%%)" % \
1543                   (pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage),
1544                    cheesecake_index,
1545                    max_cheesecake_index,
1546                    percentage)
1547
1548             if self.verbose:
1549                 print
1550                 print self.index.get_info(),
1551
1552         return cheesecake_index
1553
1554 ################################################################################
1555 ## Command line.
1556 ################################################################################
1557
1558 def process_cmdline_args():
1559     """Parse command-line options.
1560     """
1561     parser = OptionParser()
1562     parser.add_option("-c", "--config", dest="config",
1563                       default=None,
1564                       help="directory with custom configuration (default=~/.cheesecake)")
1565     parser.add_option("--lite", action="store_true", dest="lite",
1566                       default=False, help="don't run time-consuming tests (default=False)")
1567     parser.add_option("-l", "--logfile", dest="logfile",
1568                       default=None,
1569                       help="file to log all cheesecake messages")
1570     parser.add_option("-n", "--name", dest="name",
1571                       default="", help="package name (will be retrieved via setuptools utilities, if present)")
1572     parser.add_option("-p", "--path", dest="path",
1573                       default="", help="path of tar.gz/zip package on local file system")
1574     parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
1575                       default=False, help="only print Cheesecake index value (default=False)")
1576     parser.add_option("-s", "--sandbox", dest="sandbox",
1577                       default=None,
1578                       help="directory where package will be unpacked "\
1579                            "(default is to use random directory inside %s)" % tempfile.gettempdir())
1580     parser.add_option("-t", "--static", action="store_true", dest="static",
1581                       default=False, help="don't run any code from the package being tested (default=False)")
1582     parser.add_option("-u", "--url", dest="url",
1583                       default="", help="package URL")
1584     parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
1585                       default=False, help="verbose output (default=False)")
1586
1587     (options, args) = parser.parse_args()
1588     return options
1589
1590 def main():
1591     """Display Cheesecake index for package specified via command-line options.
1592     """
1593     options = process_cmdline_args()
1594     name = options.name
1595     url = options.url
1596     path = options.path
1597     sandbox = options.sandbox
1598     config = options.config
1599     logfile = options.logfile
1600     verbose = options.verbose
1601     quiet = options.quiet
1602     static_only = options.static
1603     lite = options.lite
1604
1605     if not name and not url and not path:
1606         print "Error: No package name, URL or path specified (see --help)"
1607         sys.exit(1)
1608
1609     try:
1610         c = Cheesecake(name=name, url=url, path=path, sandbox=sandbox,
1611                        config=config, logfile=logfile, verbose=verbose,
1612                        quiet=quiet, static_only=static_only, lite=lite)
1613         c.compute_cheesecake_index()
1614         c.cleanup()
1615     except CheesecakeError, e:
1616         print str(e)
1617
1618 if __name__ == "__main__":
1619     main()
Note: See TracBrowser for help on using the browser.