root/trunk/cheesecake/cheesecake_index.py

Revision 168, 62.5 kB (checked in by mk, 6 years ago)

Resolve Revision for cheesecake_index.py.

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