root/trunk/cheesecake/cheesecake_index.py

Revision 159, 62.4 kB (checked in by grig, 6 years ago)

Added pep8.py from Johann Rocholl and corresponding IndexPEP8 as part of the code kwalitee index.

Corrected minor misspellings.

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