root/branches/mk/cheesecake/cheesecake_index.py

Revision 100, 54.8 kB (checked in by mk, 7 years ago)

Use verbose to display notes for package being scored (like which files it is missing).

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