root/trunk/cheesecake/cheesecake_index.py

Revision 192, 66.0 kB (checked in by mk, 5 years ago)

Two more debugging messages.

  • Property svn:executable set to
  • Property svn:keywords set to Revision
Line 
1 #!/usr/bin/env python
2 """Cheesecake: How tasty is your code?
3
4 The idea of the Cheesecake project is to rank Python packages based on various
5 empirical "kwalitee" factors, such as:
6
7  * whether the package can be downloaded from PyPI given its name
8  * whether the package can be unpacked
9  * whether the package can be installed into an alternate directory
10  * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
11  * percentage of modules/functions/classes/methods with docstrings
12  * ... and many others
13 """
14
15 import os
16 import re
17 import shutil
18 import sys
19 import tempfile
20
21 from optparse import OptionParser
22 from urllib import urlretrieve
23 from urlparse import urlparse
24 from math import ceil
25
26 import logger
27
28 from util import pad_with_dots, pad_left_spaces, pad_right_spaces, pad_msg, pad_line
29 from util import run_cmd, command_successful
30 from util import unzip_package, untar_package, unegg_package
31 from util import mkdirs
32 from util import StdoutRedirector
33 from util import time_function
34 from util import rmtree
35 from codeparser import CodeParser
36 from __init__ import __version__ as VERSION
37 import pep8
38
39 __docformat__ = 'reStructuredText en'
40 __revision__ = '$Revision$'[11:-1].strip()
41
42
43 ################################################################################
44 ## Helpers.
45 ################################################################################
46
47 if 'sorted' not in dir(__builtins__):
48     def sorted(L):
49         new_list = L[:]
50         new_list.sort()
51         return new_list
52
53 if 'set' not in dir(__builtins__):
54     from sets import Set as set
55
56 def isiterable(obj):
57     """Check whether object is iterable.
58
59     >>> isiterable([1,2,3])
60     True
61     >>> isiterable("string")
62     True
63     >>> isiterable(object)
64     False
65     """
66     return hasattr(obj, '__iter__') or isinstance(obj, basestring)
67
68 def has_extension(filename, ext):
69     """Check if filename has given extension.
70
71     >>> has_extension("foobar.py", ".py")
72     True
73     >>> has_extension("foo.bar.py", ".py")
74     True
75     >>> has_extension("foobar.pyc", ".py")
76     False
77
78     This function is case insensitive.
79         >>> has_extension("FOOBAR.PY", ".py")
80         True
81     """
82     return os.path.splitext(filename.lower())[1] == ext.lower()
83
84 def discover_file_type(filename):
85     """Discover type of a file according to its name and its parent directory.
86
87     Currently supported file types:
88         * pyc
89         * pyo
90         * module: .py files of an application
91         * demo: .py files for documentation/demonstration purposes
92         * test: .py files used for testing
93         * special: .py file for special purposes
94
95     :Note: This function only checks file's name, and doesn't touch the
96            filesystem. If you have to, check if file exists by yourself.
97
98     >>> discover_file_type('module.py')
99     'module'
100     >>> discover_file_type('./setup.py')
101     'special'
102     >>> discover_file_type('some/directory/junk.pyc')
103     'pyc'
104     >>> discover_file_type('examples/readme.txt')
105     >>> discover_file_type('examples/runthis.py')
106     'demo'
107     >>> discover_file_type('optimized.pyo')
108     'pyo'
109
110     >>> test_files = ['ut/test_this_and_that.py',
111     ...               'another_test.py',
112     ...               'TEST_MY_MODULE.PY']
113     >>> for filename in test_files:
114     ...     assert discover_file_type(filename) == 'test', filename
115
116     >>> discover_file_type('this_is_not_a_test_really.py')
117     'module'
118     """
119     dirs = filename.split(os.path.sep)
120     dirs, filename = dirs[:-1], dirs[-1]
121
122     if filename in ["setup.py", "ez_setup.py", "__pkginfo__.py"]:
123         return 'special'
124
125     if has_extension(filename, ".pyc"):
126         return 'pyc'
127     if has_extension(filename, ".pyo"):
128         return 'pyo'
129     if has_extension(filename, ".py"):
130         for dir in dirs:
131             if dir in ['test', 'tests']:
132                 return 'test'
133             elif dir in ['doc', 'docs', 'demo', 'example', 'examples']:
134                 return 'demo'
135
136         # Most test frameworks look for files starting with "test_".
137         # py.test also looks at files with trailing "_test".
138         if filename.lower().startswith('test_') or \
139                os.path.splitext(filename)[0].lower().endswith('_test'):
140             return 'test'
141
142         return 'module'
143
144 def get_files_of_type(file_list, file_type):
145     """Return files from `file_list` that match given `file_type`.
146
147     >>> file_list = ['test/test_foo.py', 'setup.py', 'README', 'test/test_bar.py']
148     >>> get_files_of_type(file_list, 'test')
149     ['test/test_foo.py', 'test/test_bar.py']
150     """
151     return filter(lambda x: discover_file_type(x) == file_type, file_list)
152
153 def get_package_name_from_path(path):
154     """Get package name as file portion of path.
155
156     >>> get_package_name_from_path('/some/random/path/package.tar.gz')
157     'package.tar.gz'
158     >>> get_package_name_from_path('/path/underscored_name.zip')
159     'underscored_name.zip'
160     >>> get_package_name_from_path('/path/unknown.extension.txt')
161     'unknown.extension.txt'
162     """
163     dir, filename = os.path.split(path)
164     return filename
165
166 def get_package_name_from_url(url):
167     """Use ``urlparse`` to obtain package name from URL.
168
169     >>> get_package_name_from_url('http://www.example.com/file.tar.bz2')
170     'file.tar.bz2'
171     >>> get_package_name_from_url('https://www.example.com/some/dir/file.txt')
172     'file.txt'
173     """
174     (scheme,location,path,param,query,fragment_id) = urlparse(url)
175     return get_package_name_from_path(path)
176
177 def get_package_name_and_type(package, known_extensions):
178     """Return package name and type.
179
180     Package type must exists in known_extensions list. Otherwise None is
181     returned.
182
183     >>> extensions = ['tar.gz', 'zip']
184     >>> get_package_name_and_type('underscored_name.zip', extensions)
185     ('underscored_name', 'zip')
186     >>> get_package_name_and_type('unknown.extension.txt', extensions)
187     """
188     for package_type in known_extensions:
189         if package.endswith('.'+package_type):
190             # Package name is name of package without file extension (ex. twill-7.3).
191             return package[:package.rfind('.'+package_type)], package_type
192
193 def get_method_arguments(method):
194     """Return tuple of arguments for given method, excluding self.
195
196     >>> class Class:
197     ...     def method(s, arg1, arg2, other_arg):
198     ...         pass
199     >>> get_method_arguments(Class.method)
200     ('arg1', 'arg2', 'other_arg')
201     """
202     return method.func_code.co_varnames[1:method.func_code.co_argcount]
203
204 def get_attributes(obj, names):
205     """Return attributes dictionary with keys from `names`.
206
207     Object is queried for each attribute name, if it doesn't have this
208     attribute, default value None will be returned.
209
210     >>> class Class:
211     ...     pass
212     >>> obj = Class()
213     >>> obj.attr = True
214     >>> obj.value = 13
215     >>> obj.string = "Hello"
216
217     >>> d = get_attributes(obj, ['attr', 'string', 'other'])
218     >>> d == {'attr': True, 'string': "Hello", 'other': None}
219     True
220     """
221     attrs = {}
222
223     for name in names:
224         attrs[name] = getattr(obj, name, None)
225
226     return attrs
227
228 def camel2underscore(name):
229     """Convert name from CamelCase to underscore_name.
230
231     >>> camel2underscore('CamelCase')
232     'camel_case'
233     >>> camel2underscore('already_underscore_name')
234     'already_underscore_name'
235     >>> camel2underscore('BigHTMLClass')
236     'big_html_class'
237     >>> camel2underscore('')
238     ''
239     """
240     if name and name[0].upper:
241         name = name[0].lower() + name[1:]
242
243     def capitalize(match):
244         string = match.group(1).lower().capitalize()
245         return string[:-1] + string[-1].upper()
246
247     def underscore(match):
248         return '_' + match.group(1).lower()
249
250     name = re.sub(r'([A-Z]+)', capitalize, name)
251     return re.sub(r'([A-Z])', underscore, name)
252
253 def index_class_to_name(clsname):
254     """Covert index class name to index name.
255
256     >>> index_class_to_name("IndexDownload")
257     'download'
258     >>> index_class_to_name("IndexUnitTests")
259     'unit_tests'
260     >>> index_class_to_name("IndexPyPIDownload")
261     'py_pi_download'
262     """
263     return camel2underscore(clsname.replace('Index', '', 1))
264
265 def is_empty(path):
266     """Returns True if file or directory pointed by `path` is empty.
267     """
268     if os.path.isfile(path) and os.path.getsize(path) == 0:
269         return True
270     if os.path.isdir(path) and os.listdir(path) == []:
271         return True
272
273     return False
274
275 def strip_dir_part(path, root):
276     """Strip `root` part from `path`.
277
278     >>> strip_dir_part('/home/ruby/file', '/home')
279     'ruby/file'
280     >>> strip_dir_part('/home/ruby/file', '/home/')
281     'ruby/file'
282     >>> strip_dir_part('/home/ruby/', '/home')
283     'ruby/'
284     >>> strip_dir_part('/home/ruby/', '/home/')
285     'ruby/'
286     """
287     path = path.replace(root, '', 1)
288
289     if path.startswith(os.path.sep):
290         path = path[1:]
291
292     return path
293
294 def get_files_dirs_list(root):
295     """Return list of all files and directories below `root`.
296
297     Root directory is excluded from files/directories paths.
298     """
299     files = []
300     directories = []
301
302     for dirpath, dirnames, filenames in os.walk(root):
303         dirpath = strip_dir_part(dirpath, root)
304         files.extend(map(lambda x: os.path.join(dirpath, x), filenames))
305         directories.extend(map(lambda x: os.path.join(dirpath, x), dirnames))
306
307     return files, directories
308
309 def length(L):
310     """Overall length of all strings in list.
311
312     >>> length(['a', 'bc', 'd', '', 'efg'])
313     7
314     """
315     return sum(map(lambda x: len(x), L))
316
317 def generate_arguments(arguments, max_length):
318     """Pass list of strings in chunks of size not greater than max_length.
319
320     >>> for x in generate_arguments(['abc', 'def'], 4):
321     ...     print x
322     ['abc']
323     ['def']
324
325     >>> for x in generate_arguments(['a', 'bc', 'd', 'e', 'f'], 2):
326     ...     print x
327     ['a']
328     ['bc']
329     ['d', 'e']
330     ['f']
331
332     If a single argument is larger than max_length, ValueError is raised.
333         >>> L = []
334         >>> for x in generate_arguments(['abc', 'de', 'fghijk', 'l'], 4):
335         ...     L.append(x)
336         Traceback (most recent call last):
337           ...
338         ValueError: Argument 'fghijk' larger than 4.
339         >>> L
340         [['abc'], ['de']]
341     """
342     L = []
343     i = 0
344
345     # We have to look ahead, so C-style loop here.
346     while arguments:
347         if L == [] and len(arguments[i]) > max_length:
348             raise ValueError("Argument '%s' larger than %d." % (arguments[i], max_length))
349
350         L.append(arguments[i])
351
352         # End of arguments: yield then terminate.
353         if i == len(arguments) - 1:
354             yield L
355             break
356
357         # Adding next argument would exceed max_length, so yield now.
358         if length(L) + len(arguments[i+1]) > max_length:
359             yield L
360             L = []
361
362         i += 1
363
364 ################################################################################
365 ## Main index class.
366 ################################################################################
367
368 class NameSetter(type):
369     def __init__(cls, name, bases, dict):
370         if 'name' not in dict:
371             setattr(cls, 'name', name)
372
373         if 'compute_with' in dict:
374             orig_compute_with = cls.compute_with
375
376             def _timed_compute_with(self, cheesecake):
377                 (ret, self.time_taken) = time_function(lambda: orig_compute_with(self, cheesecake))
378                 self.cheesecake.log.debug("Index %s computed in %.2f seconds." % (self.name, self.time_taken))
379                 return ret
380
381             setattr(cls, 'compute_with', _timed_compute_with)
382
383     def __repr__(cls):
384         return '<Index class: %s>' % cls.name
385
386 def make_indices_dict(indices):
387     indices_dict = {}
388     for index in indices:
389         indices_dict[index.name] = index
390     return indices_dict
391
392 class Index(object):
393     """Class describing one index.
394
395     Use it as a container index or subclass to create custom indices.
396
397     During class initialization, special attribute `name` is magically
398     set based on class name. See `NameSetter` definitions for details.
399     """
400     __metaclass__ = NameSetter
401
402     subindices = None
403
404     name = "unnamed"
405     value = -1
406     details = ""
407     info = ""
408
409     def __init__(self, *indices):
410         # When indices are given explicitly they override the default.
411         if indices:
412             self.subindices = []
413             self._indices_dict = {}
414             for index in indices:
415                 self.add_subindex(index)
416         else:
417             if self.subindices:
418                 new_subindices = []
419                 for index in self.subindices:
420                     # index must be a class subclassing from Index.
421                     assert isinstance(index, type)
422                     assert issubclass(index, Index)
423                     new_subindices.append(index())
424                 self.subindices = new_subindices
425             else:
426                 self.subindices = []
427             # Create dictionary for fast reference.
428             self._indices_dict = make_indices_dict(self.subindices)
429
430         self._compute_arguments = get_method_arguments(self.compute)
431
432     def _iter_indices(self):
433         """Iterate over each subindex and yield their values.
434         """
435         for index in self.subindices[:]:
436             try:
437                 # Pass Cheesecake instance to a subindex.
438                 yield index.compute_with(self.cheesecake)
439                 # Print index info after computing.
440                 if not self.cheesecake.quiet:
441                     index.print_info()
442             except:
443                 # When exception is thrown, silence it
444                 #   and remove this subindex from the list.
445                 self.subindices.remove(index)
446
447     def compute_with(self, cheesecake):
448         """Take given Cheesecake instance and compute index value.
449         """
450         self.cheesecake = cheesecake
451         return self.compute(**get_attributes(cheesecake, self._compute_arguments))
452
453     def compute(self):
454         """Compute index value and return it.
455
456         By default this method computes sum of all subindices. Override this
457         method when subclassing for different behaviour.
458
459         Parameters to this function are dynamically prepared with use of
460         `get_attributes` function.
461
462         :Warning: Don't use \*args and \*\*kwds arguments for this method.
463         """
464         self.value = sum(self._iter_indices())
465         return self.value
466
467     def decide(self, cheesecake, when):
468         """Decide if this index should be computed.
469
470         If index has children, it will automatically remove all for which
471         decide() return false.
472         """
473         if self.subindices:
474             # Iterate over copy, as we may remove some elements.
475             for index in self.subindices[:]:
476                 if not getattr(index, 'decide_' + when)(cheesecake):
477                     self.remove_subindex(index.name)
478             return self.subindices
479         return True
480
481     def decide_before_download(self, cheesecake):
482         return self.decide(cheesecake, 'before_download')
483
484     def decide_after_download(self, cheesecake):
485         return self.decide(cheesecake, 'after_download')
486
487     def add_info(self, info_line):
488         """Add information about index computation process, which will
489         be visible with --verbose flag.
490         """
491         self.info += "[%s] %s\n" % (index_class_to_name(self.name), info_line)
492
493     def _get_max_value(self):
494         if self.subindices:
495             return sum(map(lambda index: index.max_value,
496                            self.subindices))
497         return 0
498
499     max_value = property(_get_max_value)
500
501     def _get_requirements(self):
502         if self.subindices:
503             return list(self._compute_arguments) + \
504                    reduce(lambda x,y: x + y.requirements, self.subindices, [])
505         return list(self._compute_arguments)
506
507     requirements = property(_get_requirements)
508
509     def add_subindex(self, index):
510         """Add subindex.
511
512         :Parameters:
513           `index` : Index instance
514               Index instance for inclusion.
515         """
516         if not isinstance(index, Index):
517             raise ValueError("subindex has to be instance of Index")
518
519         self.subindices.append(index)
520         self._indices_dict[index.name] = index
521
522     def remove_subindex(self, index_name):
523         """Remove subindex (refered by name).
524
525         :Parameters:
526           `index` : Index name
527               Index name to be removed.
528         """
529         index = self._indices_dict[index_name]
530         self.subindices.remove(index)
531         del self._indices_dict[index_name]
532
533     def _print_info_one(self):
534         if self.cheesecake.verbose:
535             sys.stdout.write(self.get_info())
536         print "%s  (%s)" % (pad_msg(index_class_to_name(self.name), self.value), self.details)
537
538     def _print_info_many(self):
539         max_value = self.max_value
540         if max_value == 0:
541             return
542
543         percentage = int(ceil(float(self.value) / float(max_value) * 100))
544         print pad_line("-")
545
546         print pad_msg("%s INDEX (ABSOLUTE)" % self.name, self.value)
547         msg = pad_msg("%s INDEX (RELATIVE)" % self.name, percentage)
548         msg += "  (%d out of a maximum of %d points is %d%%)" %\
549                (self.value, max_value, percentage)
550
551         print msg
552         print
553
554     def print_info(self):
555         """Print index name padded with dots, followed by value and details.
556         """
557         if self.subindices:
558             self._print_info_many()
559         else:
560             self._print_info_one()
561
562     def __getitem__(self, name):
563         return self._indices_dict[name]
564
565     def get_info(self):
566         if self.subindices:
567             return ''.join(map(lambda index: index.get_info(), self.subindices))
568         return self.info
569
570 ################################################################################
571 ## Index that computes scores based on files and directories.
572 ################################################################################
573
574 class OneOf(object):
575     def __init__(self, *possibilities):
576         self.possibilities = possibilities
577     def __str__(self):
578         return '/'.join(map(lambda x: str(x), self.possibilities))
579
580 def WithOptionalExt(name, extensions):
581     """Handy way of writing Cheese rules for files with extensions.
582
583     Instead of writing:
584         >>> one_of = OneOf('readme', 'readme.html', 'readme.txt')
585
586     Write this:
587         >>> opt_ext = WithOptionalExt('readme', ['html', 'txt'])
588
589     It means the same! (representation has a meaning)
590         >>> str(one_of) == str(opt_ext)
591         True
592     """
593     possibilities = [name]
594     possibilities.extend(map(lambda x: name + '.' + x, extensions))
595
596     return OneOf(*possibilities)
597
598 def Doc(name):
599     return WithOptionalExt(name, ['html', 'txt'])
600
601 class FilesIndex(Index):
602     _used_rules = []
603
604     def _compute_from_rules(self, files_list, package_dir, files_rules):
605         self._used_rules = []
606         files_count = 0
607         value = 0
608
609         for filename in files_list:
610             if not is_empty(os.path.join(package_dir, filename)):
611                 score = self.get_score(os.path.basename(filename), files_rules)
612                 if score != 0:
613                     value += score
614                     files_count += 1
615
616         return files_count, value
617
618     def get_score(self, name, specs):
619         for entry, value in specs.iteritems():
620             if self.match_filename(name, entry):
621                 self.cheesecake.log.debug("%d points entry found: %s (%s)" % \
622                                           (value, name, entry))
623                 return value
624
625         return 0
626
627     def get_not_used(self, files_rules):
628         """Get only these of files_rules that didn't match during computation.
629
630         >>> rules = {
631         ...     Doc('readme'): 30,
632         ...     OneOf(Doc('license'), Doc('copying')): 30,
633         ...     'demo': 10,
634         ... }
635         >>> index = FilesIndex()
636         >>> index._used_rules.append('demo')
637         >>> map(lambda x: str(x), index.get_not_used(rules.keys()))
638         ['license/license.html/license.txt/copying/copying.html/copying.txt', 'readme/readme.html/readme.txt']
639         """
640         return filter(lambda rule: rule not in self._used_rules,
641                       files_rules)
642
643     def match_filename(self, name, rule):
644         """Check if `name` matches given `rule`.
645         """
646         def equal(x, y):
647             x_root, x_ext = os.path.splitext(x)
648             y_root, y_ext = os.path.splitext(y.lower())
649             if x_root in [y_root.lower(), y_root.upper(), y_root.capitalize()] \
650                    and x_ext in [y_ext.lower(), y_ext.upper()]:
651                 return True
652             return False
653
654         if rule in self._used_rules:
655             return False
656
657         if isinstance(rule, basestring):
658             if equal(name, rule):
659                 self._used_rules.append(rule)
660                 return True
661         elif isinstance(rule, OneOf):
662             for poss in rule.possibilities:
663                 if self.match_filename(name, poss):
664                     self._used_rules.append(rule)
665                     return True
666
667         return False
668
669 ################################################################################
670 ## Installability index.
671 ################################################################################
672
673 class IndexUrlDownload(Index):
674     """Give points for successful downloading of a package.
675     """
676     max_value = 25
677
678     def compute(self, downloaded_from_url, package, url):
679         if downloaded_from_url:
680             self.details = "downloaded package %s from URL %s"  % (package, url)
681             self.value = self.max_value
682         else:
683             self.value = 0
684
685         return self.value
686
687     def decide_before_download(self, cheesecake):
688         return cheesecake.url
689
690 class IndexUnpack(Index):
691     """Give points for successful unpacking of a package archive.
692     """
693     max_value = 25
694
695     def compute(self, unpacked):
696         if unpacked:
697             self.details = "package unpacked successfully"
698             self.value = self.max_value
699         else:
700             self.details = "package couldn't be unpacked"
701             self.value = 0
702
703         return self.value
704
705 class IndexUnpackDir(Index):
706     """Check if package unpack directory resembles package archive name.
707     """
708     max_value = 15
709
710     def compute(self, unpack_dir, original_package_name):
711         self.details = "unpack directory is " + unpack_dir
712
713         if original_package_name:
714             self.details += " instead of the expected " + original_package_name
715             self.value = 0
716         else:
717             self.details += " as expected"
718             self.value = self.max_value
719
720         return self.value
721
722     def decide_after_download(self, cheesecake):
723         return cheesecake.package_type != 'egg'
724
725 class IndexSetupPy(FilesIndex):
726     """Reward packages that have setup.py file.
727     """
728     name = "setup.py"
729     max_value = 25
730
731     files_rules = {
732         'setup.py': 25,
733     }
734
735     def compute(self, files_list, package_dir):
736         setup_py_found, self.value = self._compute_from_rules(files_list, package_dir, self.files_rules)
737
738         if setup_py_found:
739             self.details = "setup.py found"
740         else:
741             self.details = "setup.py not found"
742
743         return self.value
744
745     def decide_after_download(self, cheesecake):
746         return cheesecake.package_type != 'egg'
747
748 class IndexInstall(Index):
749     """Check if package can be installed via "python setup.py" command.
750     """
751     max_value = 50
752
753     def compute(self, installed, sandbox_install_dir):
754         if installed:
755             self.details = "package installed in %s" % sandbox_install_dir
756             self.value = self.max_value
757         else:
758             self.details = "could not install package in %s" % sandbox_install_dir
759             self.value = 0
760
761         return self.value
762
763     def decide_before_download(self, cheesecake):
764         return not cheesecake.static_only
765
766 class IndexPyPIDownload(Index):
767     """Check if package was successfully downloaded from PyPI
768     and how far from it actual package was.
769
770     Distance is number of links user has to follow to download
771     a given software package.
772     """
773     max_value = 50
774     distance_penalty = -5
775
776     def compute(self, package, found_on_cheeseshop, found_locally, distance_from_pypi, download_url):
777         if download_url:
778             self.value = self.max_value
779
780             self.details = "downloaded package " + package
781
782             if not found_on_cheeseshop:
783                 if distance_from_pypi > 0:
784                     self.value += (distance_from_pypi - 1) * self.distance_penalty
785                     self.details += " following %d link" % distance_from_pypi
786
787                     if distance_from_pypi > 1:
788                         self.details += "s"
789                         self.details += " from PyPI"
790                     else:
791                         self.details += " from " + download_url
792             else:
793                 self.details += " directly from the Cheese Shop"
794         else:
795             if found_locally:
796                 self.details = "found on local filesystem"
797             self.value = 0
798
799         return self.value
800
801     def decide_before_download(self, cheesecake):
802         return cheesecake.name
803
804 class IndexGeneratedFiles(Index):
805     """Lower score for automatically generated files that should
806     not be present in a package.
807     """
808     generated_files_penalty = -20
809     max_value = 0
810
811     def compute(self, files_list):
812         self.value = 0
813
814         pyc_files = len(get_files_of_type(files_list, 'pyc'))
815         pyo_files = len(get_files_of_type(files_list, 'pyo'))
816
817         if pyc_files > 0 or pyo_files > 0:
818             self.value += self.generated_files_penalty
819
820         self.details = "%d .pyc and %d .pyo files found" % \
821                                   (pyc_files, pyo_files)
822
823         return self.value
824
825     def decide_after_download(self, cheesecake):
826         return cheesecake.package_type != 'egg'
827
828 class IndexInstallability(Index):
829     name = "INSTALLABILITY"
830
831     subindices = [
832         IndexPyPIDownload,
833         IndexUrlDownload,
834         IndexUnpack,
835         IndexUnpackDir,
836         IndexSetupPy,
837         IndexInstall,
838         IndexGeneratedFiles,
839     ]
840
841 ################################################################################
842 ## Documentation index.
843 ################################################################################
844
845 class IndexRequiredFiles(FilesIndex):
846     """Check for existence of important files, like README or INSTALL.
847     """
848     cheese_files = {
849         Doc('readme'): 30,
850         OneOf(Doc('license'), Doc('copying')): 30,
851
852         OneOf(Doc('announce'), Doc('changelog'), Doc('changes')): 20,
853         Doc('install'): 20,
854
855         Doc('authors'): 10,
856         Doc('faq'): 10,
857         Doc('news'): 10,
858         Doc('thanks'): 10,
859         Doc('todo'): 10,
860     }
861
862     cheese_dirs = {
863         OneOf('doc', 'docs'): 30,
864         OneOf('test', 'tests'): 30,
865
866         OneOf('demo', 'example', 'examples'): 10,
867     }
868
869     max_value = sum(cheese_files.values() + cheese_dirs.values())
870
871     def compute(self, files_list, dirs_list, package_dir):
872         # Inform user of files and directories the package is missing.
873         def make_info(dictionary, what):
874             missing = self.get_not_used(dictionary.keys())
875             importance = {30: ' critical', 20: ' important'}
876
877             positive_msg = "Package has%s %s: %s."
878             negative_msg = "Package doesn't have%s %s: %s."
879
880             for key in dictionary.keys():
881                 msg = positive_msg
882                 if key in missing:
883                     msg = negative_msg
884                 self.add_info(msg % (importance.get(dictionary[key], ''), what, str(key)))
885
886         # Compute required files.
887         files_count, files_value = self._compute_from_rules(files_list, package_dir, self.cheese_files)
888         make_info(self.cheese_files, 'file')
889
890         # Compute required directories.
891         dirs_count, dirs_value = self._compute_from_rules(dirs_list, package_dir, self.cheese_dirs)
892         make_info(self.cheese_dirs, 'directory')
893
894         self.value = files_value + dirs_value
895
896         self.details = "%d files and %d required directories found" % \
897                        (files_count, dirs_count)
898
899         return self.value
900
901 class IndexDocstrings(Index):
902     """Compute how many objects have relevant docstrings.
903     """
904     max_value = 100
905
906     def compute(self, object_cnt, docstring_cnt):
907         percent = 0
908         if object_cnt > 0:
909             percent = float(docstring_cnt)/float(object_cnt)
910
911         # Scale the result.
912         self.value = int(ceil(percent * self.max_value))
913
914         self.details = "found %d/%d=%.2f%% objects with docstrings" %\
915                  (docstring_cnt, object_cnt, percent*100)
916
917         return self.value
918
919 class IndexFormattedDocstrings(Index):
920     """Compute how many of existing docstrings include any formatting,
921     like epytext or reST.
922     """
923     max_value = 30
924
925     def compute(self, object_cnt, docformat_cnt):
926         percent = 0
927         if object_cnt > 0:
928             percent = float(docformat_cnt)/float(object_cnt)
929
930         # Scale the result.
931         # We give 10p for 25% of formatted docstrings, 20p for 50% and 30p for 75%.
932         self.value = 0
933         if percent > 0.75:
934             self.add_info("%.2f%% formatted docstrings found, which is > 75%% and is worth 30p." % (percent*100))
935             self.value = 30
936         elif percent > 0.50:
937             self.add_info("%.2f%% formatted docstrings found, which is > 50%% and is worth 20p." % (percent*100))
938             self.value = 20
939         elif percent > 0.25:
940             self.add_info("%.2f%% formatted docstrings found, which is > 25%% and is worth 10p." % (percent*100))
941             self.value = 10
942         else:
943             self.add_info("%.2f%% formatted docstrings found, which is < 25%%, no points given." % (percent*100))
944
945         self.details = "found %d/%d=%.2f%% objects with formatted docstrings" %\
946                  (docformat_cnt, object_cnt, percent*100)
947
948         return self.value
949
950 class IndexDocumentation(Index):
951     name = "DOCUMENTATION"
952
953     subindices = [
954         IndexRequiredFiles,
955         IndexDocstrings,
956         IndexFormattedDocstrings,
957     ]
958
959 ################################################################################
960 ## Code "kwalitee" index.
961 ################################################################################
962
963 class IndexUnitTests(Index):
964     """Compute unittest index as percentage of methods/functions
965     that are exercised in unit tests.
966     """
967     max_value = 50
968
969     def compute(self, files_list, functions, classes, package_dir):
970         unittest_cnt = 0
971         functions_tested = set()
972
973         # Gather all function names called from test files.
974         for testfile in get_files_of_type(files_list, 'test'):
975             fullpath = os.path.join(package_dir, testfile)
976             code = CodeParser(fullpath, self.cheesecake.log.debug)
977
978             functions_tested = functions_tested.union(code.functions_called)
979
980         for name in functions + classes:
981             if name in functions_tested:
982                 unittest_cnt += 1
983                 self.cheesecake.log.debug("%s is unit tested" % name)
984
985         functions_classes_cnt = len(functions) + len(classes)
986         percent = 0
987         if functions_classes_cnt > 0:
988             percent = float(unittest_cnt)/float(functions_classes_cnt)
989
990         # Scale the result.
991         self.value = int(ceil(percent * self.max_value))
992
993         self.details = "found %d/%d=%.2f%% unit tested classes/methods/functions." %\
994                  (unittest_cnt, functions_classes_cnt, percent*100)
995
996         return self.value
997
998 class IndexUnitTested(Index):
999     """Check if the package has unit tests which can be easily found by
1000     any of known test frameworks.
1001     """
1002     max_value = 30
1003
1004     def compute(self, doctests_count, unittests_count, files_list, classes, methods):
1005         unit_tested = False
1006
1007         if doctests_count > 0:
1008             self.add_info("Package includes doctest tests.")
1009             unit_tested = True
1010
1011         if unittests_count > 0:
1012             self.add_info("Package has tests that inherit from unittest.TestCase.")
1013             unit_tested = True
1014
1015         if get_files_of_type(files_list, 'test'):
1016             self.add_info("Package has filenames which probably contain tests (in format test_* or *_test)")
1017             unit_tested = True
1018
1019         for method in methods:
1020             if self._is_test_method(method):
1021                 self.add_info("Some classes have setUp/tearDown methods which are commonly used in unit tests.")
1022                 unit_tested = True
1023                 break
1024
1025         if unit_tested:
1026             self.value = self.max_value
1027             self.details = "has unit tests"
1028         else:
1029             self.value = 0
1030             self.details = "doesn't have unit tests"
1031
1032         return self.value
1033
1034     def _is_test_method(self, method):
1035         nose_methods = ['setup', 'setup_package', 'setup_module', 'setUp',
1036                         'setUpPackage', 'setUpModule',
1037                         'teardown', 'teardown_package', 'teardown_module',
1038                         'tearDown', 'tearDownModule', 'tearDownPackage']
1039
1040         for test_method in nose_methods:
1041             if method.endswith(test_method):
1042                 return True
1043         return False
1044
1045 class IndexPyLint(Index):
1046     """Compute pylint index of the whole package.
1047     """
1048     name = "pylint"
1049     max_value = 50
1050
1051     disabled_messages = [
1052         'W0403', # relative import
1053         'W0406', # importing of self
1054     ]
1055     pylint_args = ' '.join(map(lambda x: '--disable-msg=%s' % x, disabled_messages))
1056
1057     def compute(self, files_list, package_dir, pylint_max_execution_time):
1058         # See if pylint script location is set via environment variable
1059         pylint_location = os.environ.get("PYLINT", "pylint")
1060
1061         # Maximum length of arguments (not very precise).
1062         max_arguments_length = 65536
1063
1064         # Exclude __init__.py files from score as they cause pylint
1065         #     to fail with ImportError "Unable to find module for %s in %s".
1066         files_to_lint = filter(lambda name: not name.endswith('__init__.py'),
1067                                get_files_of_type(files_list, 'module'))
1068
1069         # Switching cwd so that pylint works correctly regarding
1070         #     running it on individual modules.
1071         original_cwd = os.getcwd()
1072
1073         # Note: package_dir may be a file if the archive contains a single file.
1074         # If this is the case, change dir to the parent dir of that file.
1075         if os.path.isfile(package_dir):
1076             package_dir = os.path.dirname(package_dir)
1077
1078         os.chdir(package_dir)
1079
1080         pylint_score = 0
1081         count = 0
1082         error_count = 0
1083
1084         for filenames in generate_arguments(files_to_lint, max_arguments_length - len(self.pylint_args)):
1085             filenames =  ' '.join(filenames)
1086             self.cheesecake.log.debug("Running pylint on files: %s." % filenames)
1087
1088             # Run pylint, but don't allow it to work longer than one minute.
1089             rc, output = run_cmd("%s %s --persistent=n %s" % (pylint_location, filenames, self.pylint_args),
1090                                  max_timeout=pylint_max_execution_time)
1091             if rc:
1092                 if output == 'Time exceeded':
1093                     # Raise and exception what will cause PyLint to be removed from list of indices
1094                     #   and thus won't affect the score.
1095                     self.cheesecake.log.debug("pylint exceeded maximum execution time of %d seconds and was terminated." % \
1096                                               pylint_max_execution_time)
1097                     raise OSError
1098                 self.cheesecake.log.debug("encountered an error (%d):\n***\n%s\n***\n" % (rc, output))
1099                 error_count += 1
1100             else:
1101                 # Extract score from pylint output.
1102                 score_line = output.split("\n")[-3]
1103                 s = re.search(r" (-?\d+\.\d+)/10", score_line)
1104                 if s:
1105                     pylint_score += float(s.group(1))
1106                     count += 1
1107
1108         # Switching back to the original cwd.
1109         os.chdir(original_cwd)
1110
1111         if count:
1112             pylint_score = float(pylint_score)/float(count)
1113             self.details = "pylint score was %.2f out of 10" % pylint_score
1114         elif error_count:
1115             self.details = "encountered an error during pylint execution"
1116         else:
1117             self.details = "no files to check found"
1118
1119         # Assume scores below zero as zero for means of index value computation.
1120         if pylint_score < 0:
1121             pylint_score = 0
1122         self.value = int(ceil(pylint_score/10.0 * self.max_value))
1123
1124         self.add_info("Score is %.2f/10, which is %d%% of maximum %d points = %d." %
1125                       (pylint_score, int(pylint_score*10), self.max_value, self.value))
1126
1127         return self.value
1128
1129     def decide_before_download(self, cheesecake):
1130         # Try to run the pylint script
1131         if not command_successful("pylint --version"):
1132             cheesecake.log.debug("pylint not properly installed, omitting pylint index.")
1133             return False
1134
1135         return not cheesecake.lite
1136
1137 class IndexPEP8(Index):
1138     """Compute PEP8 index for the modules in the package.
1139     """
1140     name = "pep8"
1141
1142     #
1143     # Max value is a number of possible pep8 errors times 2,
1144     #   plus number of possible pep8 warnings.
1145     #
1146     # Currently pep8 module support 15 errors:
1147     #   E101,
1148     #   E111, E112, E113,
1149     #   E201, E202, E203,
1150     #   E211,
1151     #   E221, E222,
1152     #   E301, E302, E303,
1153     #   E401,
1154     #   E501
1155     # and 4 warnings:
1156     #   W191,
1157     #   W291,
1158     #   W601,
1159     #   W602
1160     #
1161     max_value = 34
1162
1163     error_score = -2
1164     warning_score = -1
1165
1166     def compute(self, files_list, package_dir):
1167         files_to_score = get_files_of_type(files_list, 'module')
1168         if len(files_to_score) == 0:
1169             self.value = 0
1170             self.details = "no modules found"
1171             return self.value
1172
1173         full_paths = [os.path.join(package_dir, file) for file in files_to_score]
1174         arglist = ["-qq"] + full_paths
1175         pep8.process_options(arglist)
1176         for file in files_to_score:
1177             fullpath = os.path.join(package_dir, file)
1178             pep8.input_file(fullpath)
1179         error_stats = pep8.get_error_statistics()
1180         warning_stats = pep8.get_warning_statistics()
1181
1182         errors = len(error_stats)
1183         warnings = len(warning_stats)
1184
1185         total_error_score = self.error_score * errors
1186         total_warning_score = self.warning_score * warnings
1187
1188         score = total_error_score + total_warning_score
1189         self.value = self.max_value + score
1190
1191         self.add_info("Errors:")
1192         self.add_info("Count   Details")
1193         for stat in error_stats:
1194             self.add_info(stat)
1195         self.add_info("pep8.py found %d error types; we're scoring %d per error type" % (errors, self.error_score))
1196         self.add_info("Error score: %d" % total_error_score)
1197         self.add_info("Warnings:")
1198         self.add_info("Count   Details")
1199         for stat in warning_stats:
1200             self.add_info(stat)
1201         self.add_info("pep8.py found %d warning types; we're scoring %d per warning type" % (warnings, self.warning_score))
1202         self.add_info("Warning score: %d" % total_warning_score)
1203         self.add_info("Total pep8 score: %d - %d = %d" % (self.max_value, abs(score), self.value))
1204
1205         self.details = "pep8.py check: %d error types, %d warning types" % (errors, warnings)
1206         return self.value
1207
1208     def decide_before_download(self, cheesecake):
1209         return cheesecake.with_pep8
1210
1211 class IndexCodeKwalitee(Index):
1212     name = "CODE KWALITEE"
1213
1214     subindices = [
1215         #IndexUnitTests,
1216         IndexUnitTested,
1217         IndexPyLint,
1218         IndexPEP8,
1219     ]
1220
1221 ################################################################################
1222 ## Main Cheesecake class.
1223 ################################################################################
1224
1225 class CheesecakeError(Exception):
1226     """Custom exception class for Cheesecake-specific errors.
1227     """
1228     pass
1229
1230
1231 class CheesecakeIndex(Index):
1232     name = "Cheesecake"
1233     subindices = [
1234         IndexInstallability,
1235         IndexDocumentation,
1236         IndexCodeKwalitee,
1237     ]
1238
1239
1240 class Step(object):
1241     """Single step during computation of package score.
1242     """
1243     def __init__(self, provides):
1244         self.provides = provides
1245
1246     def decide(self, cheesecake):
1247         """Decide if step should be run.
1248
1249         It checks if there's at least one index from current profile that need
1250         variables provided by this step. Override this method for other behaviour.
1251         """
1252         for provide in self.provides:
1253             if provide in cheesecake.index.requirements:
1254                 return True
1255         return False
1256
1257 class StepByVariable(Step):
1258     """Step which is always run if given Cheesecake instance variable is true.
1259     """
1260     def __init__(self, variable_name, provides):
1261         self.variable_name = variable_name
1262         Step.__init__(self, provides)
1263
1264     def decide(self, cheesecake):
1265         if getattr(cheesecake, self.variable_name, None):
1266             return True
1267
1268         # Fallback to the default.
1269         return Step.decide(self, cheesecake)
1270
1271 class Cheesecake(object):
1272     """Computes 'goodness' of Python packages.
1273
1274     Generates "cheesecake index" that takes into account things like:
1275
1276         * whether the package can be downloaded
1277         * whether the package can be unpacked
1278         * whether the package can be installed into an alternate directory
1279         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
1280         * existence of certain directories such as doc, test, demo, examples
1281         * percentage of modules/functions/classes/methods with docstrings
1282         * percentage of functions/methods that are unit tested
1283         * average pylint score for all non-test and non-demo modules
1284     """
1285
1286     steps = {}
1287
1288     package_types = {
1289         "tar.gz": untar_package,
1290         "tgz": untar_package,
1291         "zip": unzip_package,
1292         "egg": unegg_package,
1293     }
1294
1295     def __init__(self,
1296                  keep_log                  = False,
1297                  lite                      = False,
1298                  logfile                   = None,
1299                  name                      = "",
1300                  path                      = "",
1301                  pylint_max_execution_time = None,
1302                  quiet                     = False,
1303                  sandbox                   = None,
1304                  static_only               = False,
1305                  url                       = "",
1306                  verbose                   = False,
1307                  with_pep8                 = False):
1308         """Initialize critical variables, download and unpack package,
1309         walk package tree.
1310         """
1311         self.name = name
1312         self.url = url
1313         self.package_path = path
1314
1315         if self.name:
1316             self.package = self.name
1317         elif self.url:
1318             self.package = get_package_name_from_url(self.url)
1319         elif self.package_path:
1320             self.package = get_package_name_from_path(self.package_path)
1321         else:
1322             self.raise_exception("No package name, URL or path specified... exiting")
1323
1324         # Setup a sandbox.
1325         self.sandbox = sandbox or tempfile.mkdtemp(prefix='cheesecake')
1326         if not os.path.isdir(self.sandbox):
1327             os.mkdir(self.sandbox)
1328
1329         self.verbose = verbose
1330         self.quiet = quiet
1331         self.static_only = static_only
1332         self.lite = lite
1333         self.keep_log = keep_log
1334         self.with_pep8 = with_pep8
1335         self.pylint_max_execution_time = pylint_max_execution_time
1336
1337         self.sandbox_pkg_file = ""
1338         self.sandbox_pkg_dir = ""
1339         self.sandbox_install_dir = ""
1340
1341         # Configure logging as soon as possible.
1342         self.configure_logging(logfile)
1343
1344         # Log missing data.
1345         self.log.debug("Using sandbox directory %s." % self.sandbox)
1346
1347         # Setup Cheesecake index.
1348         self.index = CheesecakeIndex()
1349
1350         self.index.decide_before_download(self)
1351         self.log.debug("Profile requirements: %s." % ', '.join(sorted(self.index.requirements)))
1352
1353         # Get the package.
1354         self.run_step('get_pkg_from_pypi')
1355         self.run_step('download_pkg')
1356         self.run_step('copy_pkg')
1357
1358         # Get package name and type.
1359         name_and_type = get_package_name_and_type(self.package, self.package_types.keys())
1360
1361         if not name_and_type:
1362             msg = "Could not determine package type for package '%s'" % self.package
1363             msg += "\nCurrently recognized types: " + ", ".join(self.package_types.keys())
1364             self.raise_exception(msg)
1365
1366         self.package_name, self.package_type = name_and_type
1367         self.log.debug("Package name: " + self.package_name)
1368         self.log.debug("Package type: " + self.package_type)
1369
1370         # Make last indices decisions.
1371         self.index.decide_after_download(self)
1372
1373         # Unpack package and list its files.
1374         self.run_step('unpack_pkg')
1375         self.run_step('walk_pkg')
1376
1377         # Install package.
1378         self.run_step('install_pkg')
1379
1380     def raise_exception(self, msg):
1381         """Cleanup, print error message and raise CheesecakeError.
1382
1383         Don't use logging, since it can be called before logging has been setup.
1384         """
1385         self.cleanup(remove_log_file=False)
1386
1387         msg += "\nDetailed info available in log file %s" % self.logfile
1388
1389         raise CheesecakeError("Error: " + msg)
1390
1391     def cleanup(self, remove_log_file=True):
1392         """Delete temporary directories and files that were created
1393         in the sandbox. At the end delete the sandbox itself.
1394         """
1395         if os.path.isfile(self.sandbox_pkg_file):
1396             self.log("Removing file %s" % self.sandbox_pkg_file)
1397             os.unlink(self.sandbox_pkg_file)
1398
1399         def delete_dir(dirname):
1400             "Delete directory recursively and generate log message."
1401             if os.path.isdir(dirname):
1402                 self.log("Removing directory %s" % dirname)
1403                 rmtree(dirname)
1404
1405         delete_dir(self.sandbox)
1406
1407         if remove_log_file and not self.keep_log:
1408             # Close the log file descriptor before removing
1409             # (Linux doesn't care, but it matters on Windows).
1410             if self.logfile_descriptor:
1411                 self.logfile_descriptor.close()
1412
1413             if os.path.exists(self.logfile):
1414                 os.unlink(self.logfile)
1415
1416     def configure_logging(self, logfile=None):
1417         """Default settings for logging.
1418
1419         If verbose, log goes to console, else it goes to logfile.
1420         log.debug and log.info goes to logfile.
1421         log.warn and log.error go to both logfile and stdout.
1422         """
1423         if logfile:
1424             self.logfile = logfile
1425         else:
1426             self.logfile = os.path.join(tempfile.gettempdir(), self.package + ".log")
1427
1428         self.logfile_descriptor = open(str(self.logfile), 'w', buffering=1)
1429         logger.setconsumer('logfile', self.logfile_descriptor)
1430         logger.setconsumer('console', logger.STDOUT)
1431         logger.setconsumer('null', None)
1432
1433         self.log = logger.MultipleProducer('cheesecake logfile')
1434         self.log.info = logger.MultipleProducer('cheesecake logfile')
1435         self.log.debug = logger.MultipleProducer('cheesecake logfile')
1436         self.log.warn = logger.MultipleProducer('cheesecake console')
1437         self.log.error = logger.MultipleProducer('cheesecake console')
1438
1439     def run_step(self, step_name):
1440         """Run step if its decide() method returns True.
1441         """
1442         step = self.steps[step_name]
1443         if step.decide(self):
1444             step_method = getattr(self, step_name)
1445             step_method()
1446
1447     steps['get_pkg_from_pypi'] = StepByVariable('name',
1448                                                 ['download_url',
1449                                                  'distance_from_pypi',
1450                                                  'found_on_cheeseshop',
1451                                                  'found_locally',
1452                                                  'sandbox_pkg_file'])
1453     def get_pkg_from_pypi(self):
1454         """Download package using setuptools utilities.
1455
1456         New attributes:
1457           download_url : str
1458               URL that package was downloaded from.
1459           distance_from_pypi : int
1460               How many hops setuptools had to make to download package.
1461           found_on_cheeseshop : bool
1462               Whenever package has been found on CheeseShop.
1463           found_locally : bool
1464               Whenever package has been already installed.
1465         """
1466         self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name)
1467
1468         try:
1469             from setuptools.package_index import PackageIndex
1470             from pkg_resources import Requirement
1471             from distutils import log
1472             from distutils.errors import DistutilsError
1473         except ImportError, e:
1474             msg = "setuptools is not installed and is required for downloading a package by name\n"
1475             msg += "You can download and process a package by its full URL via the -u or --url option\n"
1476             msg += "Example: python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz"
1477             self.raise_exception(msg)
1478
1479         def drop_setuptools_info(stdout, error=None):
1480             """Drop all setuptools output as INFO.
1481             """
1482             self.log.info("*** Begin setuptools output")
1483             map(self.log.info, stdout.splitlines())
1484             if error:
1485                 self.log.info(str(error))
1486             self.log.info("*** End setuptools output")
1487
1488         def fetch_package(mode):
1489             """Fetch package from PyPI.
1490
1491             Mode can be one of:
1492               * 'pypi_source': get source package from PyPI
1493               * 'pypi_any': get source/egg package from PyPI
1494               * 'any': get package from PyPI or local filesystem
1495
1496             Returns tuple (status, output), where `status` is True
1497             if fetch was successful and False if it failed. `output`
1498             is PackageIndex.fetch() return value.
1499             """
1500             if 'pypi' in mode:
1501                 pkgindex = PackageIndex(search_path=[])
1502             else:
1503                 pkgindex = PackageIndex()
1504
1505             if mode == 'pypi_source':
1506                 source = True
1507             else:
1508                 source = False
1509
1510             try:
1511                 output = pkgindex.fetch(Requirement.parse(self.name),
1512                                         self.sandbox,
1513                                         force_scan=True,
1514                                         source=source)
1515                 return True, output
1516             except DistutilsError, e:
1517                 return False, e
1518
1519         # Temporarily set the log verbosity to INFO so we can capture setuptools
1520         # info messages.
1521         old_threshold = log.set_threshold(log.INFO)
1522         old_stdout = sys.stdout
1523         sys.stdout = StdoutRedirector()
1524
1525         # Try to get source package from PyPI first, then egg from PyPI, and if
1526         # that fails search in locally installed packages.
1527         for mode, info in [('pypi_source', "source package on PyPI"),
1528                            ('pypi_any', "egg on PyPI"),
1529                            ('any', "locally installed package")]:
1530             msg = "Looking for %s... " % info
1531             status, output = fetch_package(mode)
1532             if status and output:
1533                 self.log.info(msg + "found!")
1534                 break
1535             self.log.info(msg + "failed.")
1536
1537         # Bring back old stdout.
1538         captured_stdout = sys.stdout.read_buffer()
1539         sys.stdout = old_stdout
1540         log.set_threshold(old_threshold)
1541
1542         # If all runs failed, we must raise an error.
1543         if not status:
1544             drop_setuptools_info(captured_stdout, output)
1545             self.raise_exception("setuptools returned an error: %s\n" % str(output).splitlines()[0])
1546
1547         # If fetch returned nothing, package wasn't found.
1548         if output is None:
1549             drop_setuptools_info(captured_stdout)
1550             self.raise_exception("Could not find distribution for " + self.name)
1551
1552         # Defaults.
1553         self.download_url = ""
1554         self.distance_from_pypi = 0
1555         self.found_on_cheeseshop = False
1556         self.found_locally = False
1557
1558         for line in captured_stdout.splitlines():
1559             s = re.search(r"Reading http(.*)", line)
1560             if s:
1561                 inspected_url = s.group(1)
1562                 if "python.org/pypi" not in inspected_url:
1563                     self.distance_from_pypi += 1
1564                 continue
1565             s = re.search(r"Downloading (.*)", line)
1566             if s:
1567                 self.download_url = s.group(1)
1568                 break
1569
1570         self.sandbox_pkg_file = output
1571         self.package = get_package_name_from_path(output)
1572         self.log.info("Downloaded package %s from %s" % (self.package, self.download_url))
1573
1574         if os.path.isdir(self.sandbox_pkg_file):
1575             self.found_locally = True
1576
1577         if "cheeseshop.python.org" in self.download_url:
1578             self.found_on_cheeseshop = True
1579
1580     steps['download_pkg'] = StepByVariable('url',
1581                                            ['sandbox_pkg_file',
1582                                             'downloaded_from_url'])
1583     def download_pkg(self):
1584         """Use ``urllib.urlretrieve`` to download package to file in sandbox dir.
1585         """
1586         #self.log("Downloading package %s from URL %s" % (self.package, self.url))
1587         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1588         try:
1589             downloaded_filename, headers = urlretrieve(self.url, self.sandbox_pkg_file)
1590         except IOError, e:
1591             self.log.error("Error downloading package %s from URL %s"  % (self.package, self.url))
1592             self.raise_exception(str(e))
1593         #self.log("Downloaded package %s to %s" % (self.package, downloaded_filename))
1594
1595         if headers.gettype() in ["text/html"]:
1596             f = open(downloaded_filename)
1597             if re.search("404 Not Found", "".join(f.readlines())):
1598                 f.close()
1599                 self.raise_exception("Got '404 Not Found' error while trying to download package ... exiting")
1600             f.close()
1601
1602         self.downloaded_from_url = True
1603
1604     steps['copy_pkg'] = StepByVariable('package_path',
1605                                        ['sandbox_pkg_file'])
1606     def copy_pkg(self):
1607         """Copy package file to sandbox directory.
1608         """
1609         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1610         if not os.path.isfile(self.package_path):
1611             self.raise_exception("%s is not a valid file ... exiting" % self.package_path)
1612         self.log("Copying file %s to %s" % (self.package_path, self.sandbox_pkg_file))
1613         shutil.copyfile(self.package_path, self.sandbox_pkg_file)
1614
1615     steps['unpack_pkg'] = Step(['original_package_name',
1616                                 'sandbox_pkg_dir',
1617                                 'unpacked',
1618                                 'unpack_dir'])
1619     def unpack_pkg(self):
1620         """Unpack the package in the sandbox directory.
1621
1622         Check `package_types` attribute for list of currently supported
1623         archive types.
1624
1625         New attributes:
1626           original_package_name : str
1627               Package name guessed from the package name. Will be set only
1628               if package name is different than unpacked directory name.
1629         """
1630         self.sandbox_pkg_dir = os.path.join(self.sandbox, self.package_name)
1631         if os.path.isdir(self.sandbox_pkg_dir):
1632             self.log("Directory %s exist - removing..." % self.sandbox_pkg_dir)
1633             rmtree(self.sandbox_pkg_dir)
1634
1635         # Call appropriate function to unpack the package.
1636         unpack = self.package_types[self.package_type]
1637         self.unpack_dir = unpack(self.sandbox_pkg_file, self.sandbox)
1638
1639         if self.unpack_dir is None:
1640             self.raise_exception("Could not unpack package %s ... exiting" % \
1641                                  self.sandbox_pkg_file)
1642
1643         self.unpacked = True
1644
1645         if self.unpack_dir != self.package_name:
1646             self.original_package_name = self.package_name
1647             self.package_name = self.unpack_dir
1648
1649     steps['walk_pkg'] = Step(['dirs_list',
1650                               'docstring_cnt',
1651                               'docformat_cnt',
1652                               'doctests_count',
1653                               'unittests_count',
1654                               'files_list',
1655                               'functions',
1656                               'classes',
1657                               'methods',
1658                               'object_cnt',
1659                               'package_dir'])
1660     def walk_pkg(self):
1661         """Get package files and directories.
1662
1663         New attributes:
1664           dirs_list : list
1665               List of directories package contains.
1666           docstring_cnt : int
1667               Number of docstrings found in all package objects.
1668           docformat_cnt : int
1669               Number of formatted docstrings found in all package objects.
1670           doctests_count : int
1671               Number of docstrings that include doctests.
1672           unittests_count : int
1673               Number of classes which inherit from unittest.TestCase.
1674           files_list : list
1675               List of files package contains.
1676           functions : list
1677               List of all functions defined in package sources.
1678           classes : list
1679               List of all classes defined in package sources.
1680           methods : list
1681               List of all methods defined in package sources.
1682           object_cnt : int
1683               Number of documentable objects found in all package modules.
1684           package_dir : str
1685               Path to project directory.
1686         """
1687         self.package_dir = os.path.join(self.sandbox, self.package_name)
1688
1689         self.files_list, self.dirs_list = get_files_dirs_list(self.package_dir)
1690
1691         self.object_cnt = 0
1692         self.docstring_cnt = 0
1693         self.docformat_cnt = 0
1694         self.doctests_count = 0
1695         self.functions = []
1696         self.classes = []
1697         self.methods = []
1698         self.unittests_count = 0
1699
1700         # Parse all application files and count objects
1701         # (modules/classes/functions) and their associated docstrings.
1702         for py_file in get_files_of_type(self.files_list, 'module'):
1703             pyfile = os.path.join(self.package_dir, py_file)
1704             code = CodeParser(pyfile, self.log.debug)
1705
1706             self.object_cnt += code.object_count()
1707             self.docstring_cnt += code.docstring_count()
1708             self.docformat_cnt += code.formatted_docstrings_count
1709             self.functions += code.functions
1710             self.classes += code.classes
1711             self.methods += code.methods
1712             self.doctests_count += code.doctests_count
1713             self.unittests_count += code.unittests_count
1714
1715         # Log a bit of debugging info.
1716         self.log.debug("Found %d files: %s." % (len(self.files_list),
1717                                                 ', '.join(self.files_list)))
1718         self.log.debug("Found %d directories: %s." % (len(self.dirs_list),
1719                                                       ', '.join(self.dirs_list)))
1720
1721     steps['install_pkg'] = Step(['installed'])
1722     def install_pkg(self):
1723         """Verify that package can be installed in alternate directory.
1724
1725         New attributes:
1726           installed : bool
1727               Describes whenever package has been succefully installed.
1728         """
1729         self.log.info("Trying to install package %s" % self.name)
1730
1731         self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name)
1732
1733         if self.package_type == 'egg':
1734             # Create dummy Python directories.
1735             mkdirs('%s/lib/python2.3/site-packages/' % self.sandbox_install_dir)
1736             mkdirs('%s/lib/python2.4/site-packages/' % self.sandbox_install_dir)
1737
1738             environment = {'PYTHONPATH':
1739                            '%(sandbox)s/lib/python2.3/site-packages/:'\
1740                            '%(sandbox)s/lib/python2.4/site-packages/' % \
1741                            {'sandbox': self.sandbox_install_dir},
1742                            # Pass PATH to child process.
1743                            'PATH': os.getenv('PATH')}
1744             rc, output = run_cmd("easy_install --no-deps --prefix %s %s" % \
1745                                  (self.sandbox_install_dir,
1746                                   self.sandbox_pkg_file),
1747                                  environment)
1748         else:
1749             package_dir = os.path.join(self.sandbox, self.package_name)
1750             if not os.path.isdir(package_dir):
1751                 package_dir = self.sandbox
1752             cwd = os.getcwd()
1753             os.chdir(package_dir)
1754             rc, output = run_cmd("python setup.py install --root=%s" % \
1755                                  self.sandbox_install_dir)
1756             os.chdir(cwd)
1757
1758         if rc:
1759             self.log('*** Installation failed. Captured output:')
1760             # Stringify output as it may be an exception.
1761             for output_line in str(output).splitlines():
1762                 self.log(output_line)
1763             self.log('*** End of captured output.')
1764         else:
1765             self.log('Installation into %s successful.' % \
1766                      self.sandbox_install_dir)
1767             self.installed = True
1768
1769     def compute_cheesecake_index(self):
1770         """Compute overall Cheesecake index for the package by adding up
1771         specific indexes.
1772         """
1773         # Pass Cheesecake instance to the main Index object.
1774         cheesecake_index = self.index.compute_with(self)
1775
1776         # Get max value *after* computing indices, because computing
1777         #   process can remove some indices from the list.
1778         max_cheesecake_index = self.index.max_value
1779
1780         percentage = (cheesecake_index * 100) / max_cheesecake_index
1781
1782         self.log.info("A given package can currently reach a MAXIMUM number of %d points" % max_cheesecake_index)
1783         self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package))
1784
1785         # Print summary.
1786         if self.quiet:
1787             print "Cheesecake index: %d (%d / %d)" % (percentage,
1788                                                       cheesecake_index,
1789                                                       max_cheesecake_index)
1790         else:
1791             print
1792             print pad_line("=")
1793             print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", cheesecake_index)
1794             print "%s  (%d out of a maximum of %d points is %d%%)" % \
1795                   (pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage),
1796                    cheesecake_index,
1797                    max_cheesecake_index,
1798                    percentage)
1799
1800         return cheesecake_index
1801
1802 ################################################################################
1803 ## Command line.
1804 ################################################################################
1805
1806 def process_cmdline_args():
1807     """Parse command-line options.
1808     """
1809     parser = OptionParser()
1810
1811     # Options for package retrieval.
1812     parser.add_option("-n", "--name", dest="name",
1813                       default="", help="package name (will be retrieved via setuptools utilities, if present)")
1814     parser.add_option("-p", "--path", dest="path",
1815                       default="", help="path of tar.gz/zip package on local file system")
1816     parser.add_option("-u", "--url", dest="url",
1817                       default="", help="package URL")
1818
1819     # Output formatting options.
1820     parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
1821                       default=False, help="only print Cheesecake index value (default=False)")
1822     parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
1823                       default=False, help="verbose output (default=False)")
1824
1825     # Index choice options.
1826     parser.add_option("--lite", action="store_true", dest="lite",
1827                       default=False, help="don't run time-consuming tests (default=False)")
1828     parser.add_option("-t", "--static", action="store_true", dest="static",
1829                       default=False, help="don't run any code from the package being tested (default=False)")
1830     parser.add_option("--with-pep8", action="store_true", dest="with_pep8",
1831                       default=False, help="check pep8 conformance")
1832
1833     # Other options.
1834     parser.add_option("-l", "--logfile", dest="logfile",
1835                       default=None,
1836                       help="file to log all cheesecake messages")
1837     parser.add_option("-s", "--sandbox", dest="sandbox",
1838                       default=None,
1839                       help="directory where package will be unpacked "\
1840                            "(default is to use random directory inside %s)" % tempfile.gettempdir())
1841     parser.add_option("--keep-log", action="store_true", dest="keep_log",
1842                       default=False, help="don't remove log file even if run was successful")
1843     parser.add_option("--pylint-max-execution-time", action="store", dest="pylint_max_execution_time",
1844                       default=120, help="maximum time (in seconds) you allow pylint process to run (default=120)")
1845
1846     parser.add_option("-V", "--version", action="store_true", dest="version",
1847                       default=False, help="Output cheesecake version and exit")
1848
1849     (options, args) = parser.parse_args()
1850     return options
1851
1852 def main():
1853     """Display Cheesecake index for package specified via command-line options.
1854     """
1855     options = process_cmdline_args()
1856     keep_log = options.keep_log
1857     lite = options.lite
1858     logfile = options.logfile
1859     name = options.name
1860     path = options.path
1861     quiet = options.quiet
1862     sandbox = options.sandbox
1863     static_only = options.static
1864     url = options.url
1865     verbose = options.verbose
1866     version = options.version
1867     with_pep8 = options.with_pep8
1868     pylint_max_execution_time = int(options.pylint_max_execution_time)
1869
1870     if version:
1871         print "Cheesecake version %s (rev. %s)" % (VERSION, __revision__)
1872         sys.exit(0)
1873
1874     if not name and not url and not path:
1875         print "Error: No package name, URL or path specified (see --help)"
1876         sys.exit(1)
1877
1878     try:
1879         c = Cheesecake(keep_log                  = keep_log,
1880                        lite                      = lite,
1881                        logfile                   = logfile,
1882                        name                      = name,
1883                        path                      = path,
1884                        pylint_max_execution_time = pylint_max_execution_time,
1885                        quiet                     = quiet,
1886                        sandbox                   = sandbox,
1887                        static_only               = static_only,
1888                        url                       = url,
1889                        verbose                   = verbose,
1890                        with_pep8                 = with_pep8)
1891         c.compute_cheesecake_index()
1892         c.cleanup()
1893     except CheesecakeError, e:
1894         print str(e)
1895
1896 if __name__ == "__main__":
1897     main()
Note: See TracBrowser for help on using the browser.