root/branches/mk/cheesecake/cheesecake_index.py

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

Added missing unit tests for command_successful, run_cmd and Index.repr.

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