root/branches/mk/cheesecake/cheesecake_index.py

Revision 97, 52.9 kB (checked in by mk, 7 years ago)

Search for classes that define special methods (like setUp/tearDown) and files that match test_* or *_test and assume they contain unit tests.

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