root/branches/mk/cheesecake/cheesecake_index.py

Revision 103, 54.4 kB (checked in by mk, 7 years ago)

Omit computing of pylint index (instead of giving 0 score for it) if pylint is not installed.

  • 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 of the whole package.
947     """
948     name = "pylint"
949     max_value = 50
950
951     disabled_messages = [
952         'W0403', # relative import
953         'W0406', # importing of self
954     ]
955     pylint_args = ' '.join(map(lambda x: '--disable-msg=%s' % x, disabled_messages))
956
957     def compute(self, files_list, package_dir):
958         # Exclude __init__.py files from score as they cause pylint
959         #     to fail with ImportError "Unable to find module for %s in %s".
960         files_to_lint = filter(lambda name: not name.endswith('__init__.py'),
961                                get_files_of_type(files_list, 'module'))
962
963         # Switching cwd so that pylint works correctly regarding
964         #     running it on individual modules.
965         original_cwd = os.getcwd()
966
967         # Note: package_dir may be a file if the archive contains a single file.
968         # If this is the case, change dir to the parent dir of that file.
969         if os.path.isfile(package_dir):
970             package_dir = os.path.dirname(package_dir)
971
972         os.chdir(package_dir)
973
974         self.cheesecake.log.debug("Running pylint on files: %s." % ' '.join(files_to_lint))
975
976         rc, output = run_cmd("pylint %s %s" % (' '.join(files_to_lint), self.pylint_args))
977         if rc:
978             self.cheesecake.log.debug("encountered an error (%d):\n***\n%s\n***\n" % (rc, output))
979
980         # Switching back to the original cwd.
981         os.chdir(original_cwd)
982
983         # Extract score from pylint output.
984         score_line = output.split("\n")[-3]
985         s = re.search(r" (\d+\.\d+)/10", score_line)
986
987         # Scores below 0 are treated as 0.
988         pylint_score = 0
989         if s:
990             pylint_score = float(s.group(1))
991
992         self.value = int(ceil(pylint_score/10.0 * self.max_value))
993         self.details = "pylint score was %.2f out of 10" % pylint_score
994
995         return self.value
996
997     def decide_before_download(self, cheesecake):
998         # Try to run the pylint script
999         if not command_successful("pylint --version"):
1000             cheesecake.log.debug("pylint not properly installed, omitting pylint index.")
1001             return False
1002
1003         return not cheesecake.lite
1004
1005 class IndexCodeKwalitee(Index):
1006     name = "CODE KWALITEE"
1007
1008     subindices = [
1009         IndexPyLint,
1010         IndexUnitTests,
1011         IndexUseTestFramework,
1012     ]
1013
1014 ################################################################################
1015 ## Main Cheesecake class.
1016 ################################################################################
1017
1018 class CheesecakeError(Exception):
1019     """Custom exception class for Cheesecake-specific errors.
1020     """
1021     pass
1022
1023
1024 class CheesecakeIndex(Index):
1025     name = "Cheesecake"
1026     subindices = [
1027         IndexInstallability,
1028         IndexDocumentation,
1029         IndexCodeKwalitee,
1030     ]
1031
1032
1033 class Step(object):
1034     """Single step during computation of package score.
1035     """
1036     def __init__(self, provides):
1037         self.provides = provides
1038
1039     def decide(self, cheesecake):
1040         """Decide if step should be run.
1041
1042         It checks if there's at least one index from current profile that need
1043         variables provided by this step. Override this method for other behaviour.
1044         """
1045         for provide in self.provides:
1046             if provide in cheesecake.index.requirements:
1047                 return True
1048         return False
1049
1050 class StepByVariable(Step):
1051     """Step which is always run if given Cheesecake instance variable is true.
1052     """
1053     def __init__(self, variable_name, provides):
1054         self.variable_name = variable_name
1055         Step.__init__(self, provides)
1056
1057     def decide(self, cheesecake):
1058         if getattr(cheesecake, self.variable_name, None):
1059             return True
1060
1061         # Fallback to the default.
1062         return Step.decide(self, cheesecake)
1063
1064 class Cheesecake(object):
1065     """Computes 'goodness' of Python packages.
1066
1067     Generates "cheesecake index" that takes into account things like:
1068
1069         * whether the package can be downloaded
1070         * whether the package can be unpacked
1071         * whether the package can be installed into an alternate directory
1072         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
1073         * existence of certain directories such as doc, test, demo, examples
1074         * percentage of modules/functions/classes/methods with docstrings
1075         * percentage of functions/methods that are unit tested
1076         * average pylint score for all non-test and non-demo modules
1077     """
1078
1079     steps = {}
1080
1081     package_types = {
1082         "tar.gz": untar_package,
1083         "tgz": untar_package,
1084         "zip": unzip_package,
1085         "egg": unegg_package,
1086     }
1087
1088     def __init__(self, name="", url="", path="", sandbox=None, config=None,
1089                  logfile=None, verbose=False, quiet=False, static_only=False,
1090                  lite=False):
1091         """Initialize critical variables, download and unpack package,
1092         walk package tree.
1093         """
1094         self.name = name
1095         self.url = url
1096         self.package_path = path
1097
1098         if self.name:
1099             self.package = self.name
1100         elif self.url:
1101             self.package = get_package_name_from_url(self.url)
1102         elif self.package_path:
1103             self.package = get_package_name_from_path(self.package_path)
1104         else:
1105             self.raise_exception("No package name, URL or path specified... exiting")
1106
1107         # Setup a sandbox.
1108         self.sandbox = sandbox or tempfile.mkdtemp(prefix='cheesecake')
1109         if not os.path.isdir(self.sandbox):
1110             os.mkdir(self.sandbox)
1111
1112         self.config = config
1113         self.verbose = verbose
1114         self.quiet = quiet
1115         self.static_only = static_only
1116         self.lite = lite
1117
1118         self.sandbox_pkg_file = ""
1119         self.sandbox_pkg_dir = ""
1120         self.sandbox_install_dir = ""
1121
1122         # Configure logging as soon as possible.
1123         self.configure_logging(logfile)
1124
1125         # Setup Cheesecake index.
1126         self.index = CheesecakeIndex()
1127
1128         self.index.decide_before_download(self)
1129         self.log.debug("Profile requirements: %s." % ', '.join(sorted(self.index.requirements)))
1130
1131         # Get the package.
1132         self.run_step('get_pkg_from_pypi')
1133         self.run_step('download_pkg')
1134         self.run_step('copy_pkg')
1135
1136         # Get package name and type.
1137         name_and_type = get_package_name_and_type(self.package, self.package_types.keys())
1138
1139         if not name_and_type:
1140             msg = "Could not determine package type for package '%s'" % self.package
1141             msg += "\nCurrently recognized types: " + ", ".join(self.package_types.keys())
1142             self.raise_exception(msg)
1143
1144         self.package_name, self.package_type = name_and_type
1145         self.log.debug("Package name: " + self.package_name)
1146         self.log.debug("Package type: " + self.package_type)
1147
1148         # Make last indices decisions.
1149         self.index.decide_after_download(self)
1150
1151         # Unpack package and list its files.
1152         self.run_step('unpack_pkg')
1153         self.run_step('walk_pkg')
1154
1155         # Install package.
1156         self.run_step('install_pkg')
1157
1158     def raise_exception(self, msg):
1159         """Cleanup, print error message and raise CheesecakeError.
1160
1161         Don't use logging, since it can be called before logging has been setup.
1162         """
1163         self.cleanup(remove_log_file=False)
1164
1165         msg += "\nDetailed info available in log file %s" % self.logfile
1166
1167         raise CheesecakeError(msg)
1168
1169     def cleanup(self, remove_log_file=True):
1170         """Delete temporary directories and files that were created
1171         in the sandbox. At the end delete the sandbox itself.
1172         """
1173         if os.path.isfile(self.sandbox_pkg_file):
1174             self.log("Removing file %s" % self.sandbox_pkg_file)
1175             os.unlink(self.sandbox_pkg_file)
1176
1177         def delete_dir(dirname):
1178             "Delete directory recursively and generate log message."
1179             if os.path.isdir(dirname):
1180                 self.log("Removing directory %s" % dirname)
1181                 shutil.rmtree(dirname)
1182
1183         delete_dir(self.sandbox)
1184
1185         if remove_log_file:
1186             os.unlink(os.path.join(self.sandbox, self.logfile))
1187
1188     def set_defaults(self):
1189         """Set default values for variables that can also be defined
1190         in the config file.
1191         """
1192         pass
1193
1194     def get_config(self, config_dir=None):
1195         """Retrieve values from configuration file.
1196         """
1197         pass
1198
1199     def configure_logging(self, logfile=None):
1200         """Default settings for logging.
1201
1202         If verbose, log goes to console, else it goes to logfile.
1203         log.debug and log.info goes to logfile.
1204         log.warn and log.error go to both logfile and stdout.
1205         """
1206         if logfile:
1207             self.logfile = logfile
1208         else:
1209             self.logfile = os.path.join(tempfile.gettempdir(), self.package + ".log")
1210
1211         logger.setconsumer('logfile', open(str(self.logfile), 'w', buffering=1))
1212         logger.setconsumer('console', logger.STDOUT)
1213         logger.setconsumer('null', None)
1214
1215         self.log = logger.MultipleProducer('cheesecake logfile')
1216         self.log.info = logger.MultipleProducer('cheesecake logfile')
1217         self.log.debug = logger.MultipleProducer('cheesecake logfile')
1218         self.log.warn = logger.MultipleProducer('cheesecake console')
1219         self.log.error = logger.MultipleProducer('cheesecake console')
1220
1221     def run_step(self, step_name):
1222         """Run step if its decide() method returns True.
1223         """
1224         step = self.steps[step_name]
1225         if step.decide(self):
1226             step_method = getattr(self, step_name)
1227             step_method()
1228
1229     steps['get_pkg_from_pypi'] = StepByVariable('name',
1230                                                 ['download_url',
1231                                                  'distance_from_pypi',
1232                                                  'found_on_cheeseshop',
1233                                                  'found_locally',
1234                                                  'sandbox_pkg_file'])
1235     def get_pkg_from_pypi(self):
1236         """Download package using setuptools utilities.
1237
1238         :Ivariables:
1239           download_url : str
1240               URL that package was downloaded from.
1241           distance_from_pypi : int
1242               How many hops setuptools had to make to download package.
1243           found_on_cheeseshop : bool
1244               Whenever package has been found on CheeseShop.
1245           found_locally : bool
1246               Whenever package has been already installed.
1247         """
1248         self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name)
1249
1250         try:
1251             from setuptools.package_index import PackageIndex
1252             from pkg_resources import Requirement
1253             from distutils import log
1254             from distutils.errors import DistutilsError
1255
1256         except ImportError, e:
1257             msg = "Error: setuptools is not installed and is required for downloading a package by name\n"
1258             msg += "You can download and process a package by its full URL via the -u or --url option\n"
1259             msg += "Example: python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz"
1260             self.raise_exception(msg)
1261
1262         def drop_setuptools_info(stdout, error=None):
1263             """Drop all setuptools output as INFO.
1264             """
1265             self.log.info("*** Begin setuptools output")
1266             map(self.log.info, stdout.splitlines())
1267             if error:
1268                 self.log.info(str(error))
1269             self.log.info("*** End setuptools output")
1270
1271         try:
1272             # Temporarily set the log verbosity to INFO so we can capture setuptools info messages
1273             old_threshold = log.set_threshold(log.INFO)
1274             pkgindex = PackageIndex()
1275             old_stdout = sys.stdout
1276             sys.stdout = StdoutRedirector()
1277             output = pkgindex.fetch(Requirement.parse(self.name),
1278                                     self.sandbox,
1279                                     force_scan=True,
1280                                     source=False)
1281             captured_stdout = sys.stdout.read_buffer()
1282             sys.stdout = old_stdout
1283             log.set_threshold(old_threshold)
1284
1285         except DistutilsError, e:
1286             # Bring back old stdout.
1287             captured_stdout = sys.stdout.read_buffer()
1288             sys.stdout = old_stdout
1289             log.set_threshold(old_threshold)
1290
1291             drop_setuptools_info(captured_stdout, error)
1292             self.raise_exception("Error: setuptools returned an error: %s\n" % e)
1293
1294         if output is None:
1295             drop_setuptools_info(captured_stdout)
1296             self.raise_exception("Error: Could not find distribution for " + self.name)
1297
1298         # Defaults.
1299         self.download_url = ""
1300         self.distance_from_pypi = 0
1301         self.found_on_cheeseshop = False
1302         self.found_locally = False
1303
1304         for line in captured_stdout.splitlines():
1305             s = re.search(r"Reading http(.*)", line)
1306             if s:
1307                 inspected_url = s.group(1)
1308                 if not re.search(r"www.python.org\/pypi", inspected_url):
1309                     self.distance_from_pypi += 1
1310                 continue
1311             s = re.search(r"Downloading (.*)", line)
1312             if s:
1313                 self.download_url = s.group(1)
1314                 break
1315
1316         self.sandbox_pkg_file = output
1317         self.package = get_package_name_from_path(output)
1318         self.log.info("Downloaded package %s from %s" % (self.package, self.download_url))
1319
1320         if os.path.isdir(self.sandbox_pkg_file):
1321             self.found_locally = True
1322
1323         if re.search(r"cheeseshop.python.org", self.download_url):
1324             self.found_on_cheeseshop = True
1325
1326     steps['download_pkg'] = StepByVariable('url',
1327                                            ['sandbox_pkg_file',
1328                                             'downloaded_from_url'])
1329     def download_pkg(self):
1330         """Use ``urllib.urlretrieve`` to download package to file in sandbox dir.
1331         """
1332         #self.log("Downloading package %s from URL %s" % (self.package, self.url))
1333         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1334         try:
1335             downloaded_filename, headers = urlretrieve(self.url, self.sandbox_pkg_file)
1336         except IOError, e:
1337             self.log.error("Error downloading package %s from URL %s"  % (self.package, self.url))
1338             self.raise_exception(str(e))
1339         #self.log("Downloaded package %s to %s" % (self.package, downloaded_filename))
1340
1341         if headers.gettype() in ["text/html"]:
1342             f = open(downloaded_filename)
1343             if re.search("404 Not Found", "".join(f.readlines())):
1344                 f.close()
1345                 self.raise_exception("Got '404 Not Found' error while trying to download package ... exiting")
1346             f.close()
1347
1348         self.downloaded_from_url = True
1349
1350     steps['copy_pkg'] = StepByVariable('package_path',
1351                                        ['sandbox_pkg_file'])
1352     def copy_pkg(self):
1353         """Copy package file to sandbox directory.
1354         """
1355         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1356         if not os.path.isfile(self.package_path):
1357             self.raise_exception("%s is not a valid file ... exiting" % self.package_path)
1358         self.log("Copying file %s to %s" % (self.package_path, self.sandbox_pkg_file))
1359         shutil.copyfile(self.package_path, self.sandbox_pkg_file)
1360
1361     steps['unpack_pkg'] = Step(['original_package_name',
1362                                 'sandbox_pkg_dir',
1363                                 'unpacked',
1364                                 'unpack_dir'])
1365     def unpack_pkg(self):
1366         """Unpack the package in the sandbox directory.
1367
1368         Check `package_types` attribute for list of currently supported
1369         archive types.
1370
1371         :Ivariables:
1372           original_package_name : str
1373         """
1374         self.sandbox_pkg_dir = os.path.join(self.sandbox, self.package_name)
1375         if os.path.isdir(self.sandbox_pkg_dir):
1376             shutil.rmtree(self.sandbox_pkg_dir)
1377
1378         # Call appropriate function to unpack the package.
1379         unpack = self.package_types[self.package_type]
1380         self.unpack_dir = unpack(self.sandbox_pkg_file, self.sandbox)
1381
1382         if self.unpack_dir is None:
1383             self.raise_exception("Could not unpack package %s ... exiting" % \
1384                                  self.sandbox_pkg_file)
1385
1386         self.unpacked = True
1387
1388         if self.unpack_dir != self.package_name:
1389             self.original_package_name = self.package_name
1390             self.package_name = self.unpack_dir
1391
1392     steps['walk_pkg'] = Step(['dirs_list',
1393                               'docstring_cnt',
1394                               'docformat_cnt',
1395                               'files_list',
1396                               'functions',
1397                               'classes',
1398                               'methods',
1399                               'object_cnt',
1400                               'package_dir'])
1401     def walk_pkg(self):
1402         """Get package files and directories.
1403
1404         :Ivariables:
1405           dirs_list : list
1406               List of directories package contains.
1407           docstring_cnt : int
1408               Number of docstrings found in all package objects.
1409           docformat_cnt : int
1410               Number of formatted docstrings found in all package objects.
1411           doctests_count : int
1412               Number of docstrings that include doctests.
1413           files_list : list
1414               List of files package contains.
1415           functions : list
1416               List of all functions defined in package sources.
1417           classes : list
1418               List of all classes defined in package sources.
1419           methods : list
1420               List of all methods defined in package sources.
1421           object_cnt : int
1422               Number of documentable objects found in all package modules.
1423           package_dir : str
1424               Path to project directory.
1425         """
1426         self.package_dir = os.path.join(self.sandbox, self.package_name)
1427
1428         self.files_list, self.dirs_list = get_files_dirs_list(self.package_dir)
1429
1430         self.object_cnt = 0
1431         self.docstring_cnt = 0
1432         self.docformat_cnt = 0
1433         self.doctests_count = 0
1434         self.functions = []
1435         self.classes = []
1436         self.methods = []
1437
1438         # Parse all application files and count objects
1439         # (modules/classes/functions) and their associated docstrings.
1440         for py_file in get_files_of_type(self.files_list, 'module'):
1441             pyfile = os.path.join(self.package_dir, py_file)
1442             code = CodeParser(pyfile, self.log.debug)
1443
1444             self.object_cnt += code.object_count()
1445             self.docstring_cnt += code.docstring_count()
1446             self.docformat_cnt += code.formatted_docstrings_count
1447             self.functions += code.functions
1448             self.classes += code.classes
1449             self.methods += code.methods
1450             self.doctests_count += code.doctests_count
1451
1452         # Log a bit of debugging info.
1453         self.log.debug("Found %d files: %s." % (len(self.files_list),
1454                                                 ', '.join(self.files_list)))
1455         self.log.debug("Found %d directories: %s." % (len(self.dirs_list),
1456                                                       ', '.join(self.dirs_list)))
1457
1458     steps['install_pkg'] = Step(['installed'])
1459     def install_pkg(self):
1460         """Verify that package can be installed in alternate directory.
1461
1462         :Ivariables:
1463           installed : bool
1464               Describes whenever package has been succefully installed.
1465         """
1466         self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name)
1467
1468         if self.package_type == 'egg':
1469             # Create dummy Python directories.
1470             mkdirs('%s/lib/python2.3/site-packages/' % self.sandbox_install_dir)
1471             mkdirs('%s/lib/python2.4/site-packages/' % self.sandbox_install_dir)
1472
1473             environment = {'PYTHONPATH':
1474                            '%(sandbox)s/lib/python2.3/site-packages/:'\
1475                            '%(sandbox)s/lib/python2.4/site-packages/' % \
1476                            {'sandbox': self.sandbox_install_dir}}
1477             rc, output = run_cmd("easy_install --no-deps --prefix %s %s" % \
1478                                  (self.sandbox_install_dir,
1479                                   self.sandbox_pkg_file),
1480                                  environment)
1481         else:
1482             package_dir = os.path.join(self.sandbox, self.package_name)
1483             if not os.path.isdir(package_dir):
1484                 package_dir = self.sandbox
1485             cwd = os.getcwd()
1486             os.chdir(package_dir)
1487             rc, output = run_cmd("python setup.py install --root=%s" % \
1488                                  self.sandbox_install_dir)
1489             os.chdir(cwd)
1490
1491         if rc:
1492             self.log('*** Installation failed. Captured output:')
1493             # Stringify output as it may be an exception.
1494             for output_line in str(output).splitlines():
1495                 self.log(output_line)
1496             self.log('*** End of captured output.')
1497         else:
1498             self.log('Installation into %s successful.' % \
1499                      self.sandbox_install_dir)
1500             self.installed = True
1501
1502     def compute_cheesecake_index(self):
1503         """Compute overall Cheesecake index for the package by adding up
1504         specific indexes.
1505         """
1506         # Recursively compute all indices.
1507         max_cheesecake_index = self.index.max_value
1508
1509         # Pass Cheesecake instance to the main Index object.
1510         cheesecake_index = self.index.compute_with(self)
1511         percentage = (cheesecake_index * 100) / max_cheesecake_index
1512
1513         self.log.info("A given package can currently reach a MAXIMUM number of %d points" % max_cheesecake_index)
1514         self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package))
1515
1516         # Print summary.
1517         if self.quiet:
1518             print "Cheesecake index: %d (%d / %d)" % (percentage,
1519                                                       cheesecake_index,
1520                                                       max_cheesecake_index)
1521         else:
1522             print
1523             print pad_line("=")
1524             print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", cheesecake_index)
1525             print "%s  (%d out of a maximum of %d points is %d%%)" % \
1526                   (pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage),
1527                    cheesecake_index,
1528                    max_cheesecake_index,
1529                    percentage)
1530
1531             if self.verbose:
1532                 print
1533                 print self.index.get_info(),
1534
1535         return cheesecake_index
1536
1537 ################################################################################
1538 ## Command line.
1539 ################################################################################
1540
1541 def process_cmdline_args():
1542     """Parse command-line options.
1543     """
1544     parser = OptionParser()
1545     parser.add_option("-c", "--config", dest="config",
1546                       default=None,
1547                       help="directory with custom configuration (default=~/.cheesecake)")
1548     parser.add_option("--lite", action="store_true", dest="lite",
1549                       default=False, help="don't run time-consuming tests (default=False)")
1550     parser.add_option("-l", "--logfile", dest="logfile",
1551                       default=None,
1552                       help="file to log all cheesecake messages")
1553     parser.add_option("-n", "--name", dest="name",
1554                       default="", help="package name (will be retrieved via setuptools utilities, if present)")
1555     parser.add_option("-p", "--path", dest="path",
1556                       default="", help="path of tar.gz/zip package on local file system")
1557     parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
1558                       default=False, help="only print Cheesecake index value (default=False)")
1559     parser.add_option("-s", "--sandbox", dest="sandbox",
1560                       default=None,
1561                       help="directory where package will be unpacked "\
1562                            "(default is to use random directory inside %s)" % tempfile.gettempdir())
1563     parser.add_option("-t", "--static", action="store_true", dest="static",
1564                       default=False, help="don't run any code from the package being tested (default=False)")
1565     parser.add_option("-u", "--url", dest="url",
1566                       default="", help="package URL")
1567     parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
1568                       default=False, help="verbose output (default=False)")
1569
1570     (options, args) = parser.parse_args()
1571     return options
1572
1573 def main():
1574     """Display Cheesecake index for package specified via command-line options.
1575     """
1576     options = process_cmdline_args()
1577     name = options.name
1578     url = options.url
1579     path = options.path
1580     sandbox = options.sandbox
1581     config = options.config
1582     logfile = options.logfile
1583     verbose = options.verbose
1584     quiet = options.quiet
1585     static_only = options.static
1586     lite = options.lite
1587
1588     if not name and not url and not path:
1589         print "Error: No package name, URL or path specified (see --help)"
1590         sys.exit(1)
1591
1592     try:
1593         c = Cheesecake(name=name, url=url, path=path, sandbox=sandbox,
1594                        config=config, logfile=logfile, verbose=verbose,
1595                        quiet=quiet, static_only=static_only, lite=lite)
1596         c.compute_cheesecake_index()
1597         c.cleanup()
1598     except CheesecakeError, e:
1599         print str(e)
1600
1601 if __name__ == "__main__":
1602     main()
Note: See TracBrowser for help on using the browser.