root/branches/mk/cheesecake/cheesecake_index.py

Revision 110, 56.5 kB (checked in by mk, 7 years ago)

Search for classes that inherit from unittest.TestCase? (closes ticket #43).

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