root/trunk/cheesecake/cheesecake_index.py

Revision 184, 65.8 kB (checked in by grig, 6 years ago)

Some modifications related to running cheesecake_index on Windows.
- 'permission denied' error when removing log file; ignore the exception
- use '/' and not os.sep when figuring out unpack directory name from tar file

  • 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             try:
436                 # Pass Cheesecake instance to a subindex.
437                 yield index.compute_with(self.cheesecake)
438                 # Print index info after computing.
439                 if not self.cheesecake.quiet:
440                     index.print_info()
441             except:
442                 # When exception is thrown, silence it
443                 #   and remove this subindex from the list.
444                 self.subindices.remove(index)
445
446     def compute_with(self, cheesecake):
447         """Take given Cheesecake instance and compute index value.
448         """
449         self.cheesecake = cheesecake
450         return self.compute(**get_attributes(cheesecake, self._compute_arguments))
451
452     def compute(self):
453         """Compute index value and return it.
454
455         By default this method computes sum of all subindices. Override this
456         method when subclassing for different behaviour.
457
458         Parameters to this function are dynamically prepared with use of
459         `get_attributes` function.
460
461         :Warning: Don't use \*args and \*\*kwds arguments for this method.
462         """
463         self.value = sum(self._iter_indices())
464         return self.value
465
466     def decide(self, cheesecake, when):
467         """Decide if this index should be computed.
468
469         If index has children, it will automatically remove all for which
470         decide() return false.
471         """
472         if self.subindices:
473             # Iterate over copy, as we may remove some elements.
474             for index in self.subindices[:]:
475                 if not getattr(index, 'decide_' + when)(cheesecake):
476                     self.remove_subindex(index.name)
477             return self.subindices
478         return True
479
480     def decide_before_download(self, cheesecake):
481         return self.decide(cheesecake, 'before_download')
482
483     def decide_after_download(self, cheesecake):
484         return self.decide(cheesecake, 'after_download')
485
486     def add_info(self, info_line):
487         """Add information about index computation process, which will
488         be visible with --verbose flag.
489         """
490         self.info += "[%s] %s\n" % (index_class_to_name(self.name), info_line)
491
492     def _get_max_value(self):
493         if self.subindices:
494             return sum(map(lambda index: index.max_value,
495                            self.subindices))
496         return 0
497
498     max_value = property(_get_max_value)
499
500     def _get_requirements(self):
501         if self.subindices:
502             return list(self._compute_arguments) + \
503                    reduce(lambda x,y: x + y.requirements, self.subindices, [])
504         return list(self._compute_arguments)
505
506     requirements = property(_get_requirements)
507
508     def add_subindex(self, index):
509         """Add subindex.
510
511         :Parameters:
512           `index` : Index instance
513               Index instance for inclusion.
514         """
515         if not isinstance(index, Index):
516             raise ValueError("subindex has to be instance of Index")
517
518         self.subindices.append(index)
519         self._indices_dict[index.name] = index
520
521     def remove_subindex(self, index_name):
522         """Remove subindex (refered by name).
523
524         :Parameters:
525           `index` : Index name
526               Index name to be removed.
527         """
528         index = self._indices_dict[index_name]
529         self.subindices.remove(index)
530         del self._indices_dict[index_name]
531
532     def _print_info_one(self):
533         if self.cheesecake.verbose:
534             sys.stdout.write(self.get_info())
535         print "%s  (%s)" % (pad_msg(index_class_to_name(self.name), self.value), self.details)
536
537     def _print_info_many(self):
538         max_value = self.max_value
539         if max_value == 0:
540             return
541
542         percentage = int(ceil(float(self.value) / float(max_value) * 100))
543         print pad_line("-")
544
545         print pad_msg("%s INDEX (ABSOLUTE)" % self.name, self.value)
546         msg = pad_msg("%s INDEX (RELATIVE)" % self.name, percentage)
547         msg += "  (%d out of a maximum of %d points is %d%%)" %\
548                (self.value, max_value, percentage)
549
550         print msg
551         print
552
553     def print_info(self):
554         """Print index name padded with dots, followed by value and details.
555         """
556         if self.subindices:
557             self._print_info_many()
558         else:
559             self._print_info_one()
560
561     def __getitem__(self, name):
562         return self._indices_dict[name]
563
564     def get_info(self):
565         if self.subindices:
566             return ''.join(map(lambda index: index.get_info(), self.subindices))
567         return self.info
568
569 ################################################################################
570 ## Index that computes scores based on files and directories.
571 ################################################################################
572
573 class OneOf(object):
574     def __init__(self, *possibilities):
575         self.possibilities = possibilities
576     def __str__(self):
577         return '/'.join(map(lambda x: str(x), self.possibilities))
578
579 def WithOptionalExt(name, extensions):
580     """Handy way of writing Cheese rules for files with extensions.
581
582     Instead of writing:
583         >>> one_of = OneOf('readme', 'readme.html', 'readme.txt')
584
585     Write this:
586         >>> opt_ext = WithOptionalExt('readme', ['html', 'txt'])
587
588     It means the same! (representation has a meaning)
589         >>> str(one_of) == str(opt_ext)
590         True
591     """
592     possibilities = [name]
593     possibilities.extend(map(lambda x: name + '.' + x, extensions))
594
595     return OneOf(*possibilities)
596
597 def Doc(name):
598     return WithOptionalExt(name, ['html', 'txt'])
599
600 class FilesIndex(Index):
601     _used_rules = []
602
603     def _compute_from_rules(self, files_list, package_dir, files_rules):
604         self._used_rules = []
605         files_count = 0
606         value = 0
607
608         for filename in files_list:
609             if not is_empty(os.path.join(package_dir, filename)):
610                 score = self.get_score(os.path.basename(filename), files_rules)
611                 if score != 0:
612                     value += score
613                     files_count += 1
614
615         return files_count, value
616
617     def get_score(self, name, specs):
618         for entry, value in specs.iteritems():
619             if self.match_filename(name, entry):
620                 self.cheesecake.log.debug("%d points entry found: %s (%s)" % \
621                                           (value, name, entry))
622                 return value
623
624         return 0
625
626     def get_not_used(self, files_rules):
627         """Get only these of files_rules that didn't match during computation.
628
629         >>> rules = {
630         ...     Doc('readme'): 30,
631         ...     OneOf(Doc('license'), Doc('copying')): 30,
632         ...     'demo': 10,
633         ... }
634         >>> index = FilesIndex()
635         >>> index._used_rules.append('demo')
636         >>> map(lambda x: str(x), index.get_not_used(rules.keys()))
637         ['license/license.html/license.txt/copying/copying.html/copying.txt', 'readme/readme.html/readme.txt']
638         """
639         return filter(lambda rule: rule not in self._used_rules,
640                       files_rules)
641
642     def match_filename(self, name, rule):
643         """Check if `name` matches given `rule`.
644         """
645         def equal(x, y):
646             x_root, x_ext = os.path.splitext(x)
647             y_root, y_ext = os.path.splitext(y.lower())
648             if x_root in [y_root.lower(), y_root.upper(), y_root.capitalize()] \
649                    and x_ext in [y_ext.lower(), y_ext.upper()]:
650                 return True
651             return False
652
653         if rule in self._used_rules:
654             return False
655
656         if isinstance(rule, basestring):
657             if equal(name, rule):
658                 self._used_rules.append(rule)
659                 return True
660         elif isinstance(rule, OneOf):
661             for poss in rule.possibilities:
662                 if self.match_filename(name, poss):
663                     self._used_rules.append(rule)
664                     return True
665
666         return False
667
668 ################################################################################
669 ## Installability index.
670 ################################################################################
671
672 class IndexUrlDownload(Index):
673     """Give points for successful downloading of a package.
674     """
675     max_value = 25
676
677     def compute(self, downloaded_from_url, package, url):
678         if downloaded_from_url:
679             self.details = "downloaded package %s from URL %s"  % (package, url)
680             self.value = self.max_value
681         else:
682             self.value = 0
683
684         return self.value
685
686     def decide_before_download(self, cheesecake):
687         return cheesecake.url
688
689 class IndexUnpack(Index):
690     """Give points for successful unpacking of a package archive.
691     """
692     max_value = 25
693
694     def compute(self, unpacked):
695         if unpacked:
696             self.details = "package unpacked successfully"
697             self.value = self.max_value
698         else:
699             self.details = "package couldn't be unpacked"
700             self.value = 0
701
702         return self.value
703
704 class IndexUnpackDir(Index):
705     """Check if package unpack directory resembles package archive name.
706     """
707     max_value = 15
708
709     def compute(self, unpack_dir, original_package_name):
710         self.details = "unpack directory is " + unpack_dir
711
712         if original_package_name:
713             self.details += " instead of the expected " + original_package_name
714             self.value = 0
715         else:
716             self.details += " as expected"
717             self.value = self.max_value
718
719         return self.value
720
721     def decide_after_download(self, cheesecake):
722         return cheesecake.package_type != 'egg'
723
724 class IndexSetupPy(FilesIndex):
725     """Reward packages that have setup.py file.
726     """
727     name = "setup.py"
728     max_value = 25
729
730     files_rules = {
731         'setup.py': 25,
732     }
733
734     def compute(self, files_list, package_dir):
735         setup_py_found, self.value = self._compute_from_rules(files_list, package_dir, self.files_rules)
736
737         if setup_py_found:
738             self.details = "setup.py found"
739         else:
740             self.details = "setup.py not found"
741
742         return self.value
743
744     def decide_after_download(self, cheesecake):
745         return cheesecake.package_type != 'egg'
746
747 class IndexInstall(Index):
748     """Check if package can be installed via "python setup.py" command.
749     """
750     max_value = 50
751
752     def compute(self, installed, sandbox_install_dir):
753         if installed:
754             self.details = "package installed in %s" % sandbox_install_dir
755             self.value = self.max_value
756         else:
757             self.details = "could not install package in %s" % sandbox_install_dir
758             self.value = 0
759
760         return self.value
761
762     def decide_before_download(self, cheesecake):
763         return not cheesecake.static_only
764
765 class IndexPyPIDownload(Index):
766     """Check if package was successfully downloaded from PyPI
767     and how far from it actual package was.
768
769     Distance is number of links user has to follow to download
770     a given software package.
771     """
772     max_value = 50
773     distance_penalty = -5
774
775     def compute(self, package, found_on_cheeseshop, found_locally, distance_from_pypi, download_url):
776         if download_url:
777             self.value = self.max_value
778
779             self.details = "downloaded package " + package
780
781             if not found_on_cheeseshop:
782                 if distance_from_pypi > 0:
783                     self.value += (distance_from_pypi - 1) * self.distance_penalty
784                     self.details += " following %d link" % distance_from_pypi
785
786                     if distance_from_pypi > 1:
787                         self.details += "s"
788                         self.details += " from PyPI"
789                     else:
790                         self.details += " from " + download_url
791             else:
792                 self.details += " directly from the Cheese Shop"
793         else:
794             if found_locally:
795                 self.details = "found on local filesystem"
796             self.value = 0
797
798         return self.value
799
800     def decide_before_download(self, cheesecake):
801         return cheesecake.name
802
803 class IndexGeneratedFiles(Index):
804     """Lower score for automatically generated files that should
805     not be present in a package.
806     """
807     generated_files_penalty = -20
808     max_value = 0
809
810     def compute(self, files_list):
811         self.value = 0
812
813         pyc_files = len(get_files_of_type(files_list, 'pyc'))
814         pyo_files = len(get_files_of_type(files_list, 'pyo'))
815
816         if pyc_files > 0 or pyo_files > 0:
817             self.value += self.generated_files_penalty
818
819         self.details = "%d .pyc and %d .pyo files found" % \
820                                   (pyc_files, pyo_files)
821
822         return self.value
823
824     def decide_after_download(self, cheesecake):
825         return cheesecake.package_type != 'egg'
826
827 class IndexInstallability(Index):
828     name = "INSTALLABILITY"
829
830     subindices = [
831         IndexPyPIDownload,
832         IndexUrlDownload,
833         IndexUnpack,
834         IndexUnpackDir,
835         IndexSetupPy,
836         IndexInstall,
837         IndexGeneratedFiles,
838     ]
839
840 ################################################################################
841 ## Documentation index.
842 ################################################################################
843
844 class IndexRequiredFiles(FilesIndex):
845     """Check for existence of important files, like README or INSTALL.
846     """
847     cheese_files = {
848         Doc('readme'): 30,
849         OneOf(Doc('license'), Doc('copying')): 30,
850
851         OneOf(Doc('announce'), Doc('changelog'), Doc('changes')): 20,
852         Doc('install'): 20,
853
854         Doc('authors'): 10,
855         Doc('faq'): 10,
856         Doc('news'): 10,
857         Doc('thanks'): 10,
858         Doc('todo'): 10,
859     }
860
861     cheese_dirs = {
862         OneOf('doc', 'docs'): 30,
863         OneOf('test', 'tests'): 30,
864
865         OneOf('demo', 'example', 'examples'): 10,
866     }
867
868     max_value = sum(cheese_files.values() + cheese_dirs.values())
869
870     def compute(self, files_list, dirs_list, package_dir):
871         # Inform user of files and directories the package is missing.
872         def make_info(dictionary, what):
873             missing = self.get_not_used(dictionary.keys())
874             importance = {30: ' critical', 20: ' important'}
875
876             positive_msg = "Package has%s %s: %s."
877             negative_msg = "Package doesn't have%s %s: %s."
878
879             for key in dictionary.keys():
880                 msg = positive_msg
881                 if key in missing:
882                     msg = negative_msg
883                 self.add_info(msg % (importance.get(dictionary[key], ''), what, str(key)))
884
885         # Compute required files.
886         files_count, files_value = self._compute_from_rules(files_list, package_dir, self.cheese_files)
887         make_info(self.cheese_files, 'file')
888
889         # Compute required directories.
890         dirs_count, dirs_value = self._compute_from_rules(dirs_list, package_dir, self.cheese_dirs)
891         make_info(self.cheese_dirs, 'directory')
892
893         self.value = files_value + dirs_value
894
895         self.details = "%d files and %d required directories found" % \
896                        (files_count, dirs_count)
897
898         return self.value
899
900 class IndexDocstrings(Index):
901     """Compute how many objects have relevant docstrings.
902     """
903     max_value = 100
904
905     def compute(self, object_cnt, docstring_cnt):
906         percent = 0
907         if object_cnt > 0:
908             percent = float(docstring_cnt)/float(object_cnt)
909
910         # Scale the result.
911         self.value = int(ceil(percent * self.max_value))
912
913         self.details = "found %d/%d=%.2f%% objects with docstrings" %\
914                  (docstring_cnt, object_cnt, percent*100)
915
916         return self.value
917
918 class IndexFormattedDocstrings(Index):
919     """Compute how many of existing docstrings include any formatting,
920     like epytext or reST.
921     """
922     max_value = 30
923
924     def compute(self, object_cnt, docformat_cnt):
925         percent = 0
926         if object_cnt > 0:
927             percent = float(docformat_cnt)/float(object_cnt)
928
929         # Scale the result.
930         # We give 10p for 25% of formatted docstrings, 20p for 50% and 30p for 75%.
931         self.value = 0
932         if percent > 0.75:
933             self.add_info("%.2f%% formatted docstrings found, which is > 75%% and is worth 30p." % (percent*100))
934             self.value = 30
935         elif percent > 0.50:
936             self.add_info("%.2f%% formatted docstrings found, which is > 50%% and is worth 20p." % (percent*100))
937             self.value = 20
938         elif percent > 0.25:
939             self.add_info("%.2f%% formatted docstrings found, which is > 25%% and is worth 10p." % (percent*100))
940             self.value = 10
941         else:
942             self.add_info("%.2f%% formatted docstrings found, which is < 25%%, no points given." % (percent*100))
943
944         self.details = "found %d/%d=%.2f%% objects with formatted docstrings" %\
945                  (docformat_cnt, object_cnt, percent*100)
946
947         return self.value
948
949 class IndexDocumentation(Index):
950     name = "DOCUMENTATION"
951
952     subindices = [
953         IndexRequiredFiles,
954         IndexDocstrings,
955         IndexFormattedDocstrings,
956     ]
957
958 ################################################################################
959 ## Code "kwalitee" index.
960 ################################################################################
961
962 class IndexUnitTests(Index):
963     """Compute unittest index as percentage of methods/functions
964     that are exercised in unit tests.
965     """
966     max_value = 50
967
968     def compute(self, files_list, functions, classes, package_dir):
969         unittest_cnt = 0
970         functions_tested = set()
971
972         # Gather all function names called from test files.
973         for testfile in get_files_of_type(files_list, 'test'):
974             fullpath = os.path.join(package_dir, testfile)
975             code = CodeParser(fullpath, self.cheesecake.log.debug)
976
977             functions_tested = functions_tested.union(code.functions_called)
978
979         for name in functions + classes:
980             if name in functions_tested:
981                 unittest_cnt += 1
982                 self.cheesecake.log.debug("%s is unit tested" % name)
983
984         functions_classes_cnt = len(functions) + len(classes)
985         percent = 0
986         if functions_classes_cnt > 0:
987             percent = float(unittest_cnt)/float(functions_classes_cnt)
988
989         # Scale the result.
990         self.value = int(ceil(percent * self.max_value))
991
992         self.details = "found %d/%d=%.2f%% unit tested classes/methods/functions." %\
993                  (unittest_cnt, functions_classes_cnt, percent*100)
994
995         return self.value
996
997 class IndexUnitTested(Index):
998     """Check if the package has unit tests which can be easily found by
999     any of known test frameworks.
1000     """
1001     max_value = 30
1002
1003     def compute(self, doctests_count, unittests_count, files_list, classes, methods):
1004         unit_tested = False
1005
1006         if doctests_count > 0:
1007             self.add_info("Package includes doctest tests.")
1008             unit_tested = True
1009
1010         if unittests_count > 0:
1011             self.add_info("Package has tests that inherit from unittest.TestCase.")
1012             unit_tested = True
1013
1014         if get_files_of_type(files_list, 'test'):
1015             self.add_info("Package has filenames which probably contain tests (in format test_* or *_test)")
1016             unit_tested = True
1017
1018         for method in methods:
1019             if self._is_test_method(method):
1020                 self.add_info("Some classes have setUp/tearDown methods which are commonly used in unit tests.")
1021                 unit_tested = True
1022                 break
1023
1024         if unit_tested:
1025             self.value = self.max_value
1026             self.details = "has unit tests"
1027         else:
1028             self.value = 0
1029             self.details = "doesn't have unit tests"
1030
1031         return self.value
1032
1033     def _is_test_method(self, method):
1034         nose_methods = ['setup', 'setup_package', 'setup_module', 'setUp',
1035                         'setUpPackage', 'setUpModule',
1036                         'teardown', 'teardown_package', 'teardown_module',
1037                         'tearDown', 'tearDownModule', 'tearDownPackage']
1038
1039         for test_method in nose_methods:
1040             if method.endswith(test_method):
1041                 return True
1042         return False
1043
1044 class IndexPyLint(Index):
1045     """Compute pylint index of the whole package.
1046     """
1047     name = "pylint"
1048     max_value = 50
1049
1050     disabled_messages = [
1051         'W0403', # relative import
1052         'W0406', # importing of self
1053     ]
1054     pylint_args = ' '.join(map(lambda x: '--disable-msg=%s' % x, disabled_messages))
1055
1056     def compute(self, files_list, package_dir, pylint_max_execution_time):
1057         # See if pylint script location is set via environment variable
1058         pylint_location = os.environ.get("PYLINT", "pylint")
1059
1060         # Maximum length of arguments (not very precise).
1061         max_arguments_length = 65536
1062
1063         # Exclude __init__.py files from score as they cause pylint
1064         #     to fail with ImportError "Unable to find module for %s in %s".
1065         files_to_lint = filter(lambda name: not name.endswith('__init__.py'),
1066                                get_files_of_type(files_list, 'module'))
1067
1068         # Switching cwd so that pylint works correctly regarding
1069         #     running it on individual modules.
1070         original_cwd = os.getcwd()
1071
1072         # Note: package_dir may be a file if the archive contains a single file.
1073         # If this is the case, change dir to the parent dir of that file.
1074         if os.path.isfile(package_dir):
1075             package_dir = os.path.dirname(package_dir)
1076
1077         os.chdir(package_dir)
1078
1079         pylint_score = 0
1080         count = 0
1081         error_count = 0
1082
1083         for filenames in generate_arguments(files_to_lint, max_arguments_length - len(self.pylint_args)):
1084             filenames =  ' '.join(filenames)
1085             self.cheesecake.log.debug("Running pylint on files: %s." % filenames)
1086
1087             # Run pylint, but don't allow it to work longer than one minute.
1088             rc, output = run_cmd("%s %s --persistent=n %s" % (pylint_location, filenames, self.pylint_args),
1089                                  max_timeout=pylint_max_execution_time)
1090             if rc:
1091                 if output == 'Time exceeded':
1092                     # Raise and exception what will cause PyLint to be removed from list of indices
1093                     #   and thus won't affect the score.
1094                     self.cheesecake.log.debug("pylint exceeded maximum execution time of %d seconds and was terminated." % \
1095                                               pylint_max_execution_time)
1096                     raise OSError
1097                 self.cheesecake.log.debug("encountered an error (%d):\n***\n%s\n***\n" % (rc, output))
1098                 error_count += 1
1099             else:
1100                 # Extract score from pylint output.
1101                 score_line = output.split("\n")[-3]
1102                 s = re.search(r" (-?\d+\.\d+)/10", score_line)
1103                 if s:
1104                     pylint_score += float(s.group(1))
1105                     count += 1
1106
1107         # Switching back to the original cwd.
1108         os.chdir(original_cwd)
1109
1110         if count:
1111             pylint_score = float(pylint_score)/float(count)
1112             self.details = "pylint score was %.2f out of 10" % pylint_score
1113         elif error_count:
1114             self.details = "encountered an error during pylint execution"
1115         else:
1116             self.details = "no files to check found"
1117
1118         # Assume scores below zero as zero for means of index value computation.
1119         if pylint_score < 0:
1120             pylint_score = 0
1121         self.value = int(ceil(pylint_score/10.0 * self.max_value))
1122
1123         self.add_info("Score is %.2f/10, which is %d%% of maximum %d points = %d." %
1124                       (pylint_score, int(pylint_score*10), self.max_value, self.value))
1125
1126         return self.value
1127
1128     def decide_before_download(self, cheesecake):
1129         # Try to run the pylint script
1130         if not command_successful("pylint --version"):
1131             cheesecake.log.debug("pylint not properly installed, omitting pylint index.")
1132             return False
1133
1134         return not cheesecake.lite
1135
1136 class IndexPEP8(Index):
1137     """Compute PEP8 index for the modules in the package.
1138     """
1139     name = "pep8"
1140
1141     #
1142     # Max value is a number of possible pep8 errors times 2,
1143     #   plus number of possible pep8 warnings.
1144     #
1145     # Currently pep8 module support 15 errors:
1146     #   E101,
1147     #   E111, E112, E113,
1148     #   E201, E202, E203,
1149     #   E211,
1150     #   E221, E222,
1151     #   E301, E302, E303,
1152     #   E401,
1153     #   E501
1154     # and 4 warnings:
1155     #   W191,
1156     #   W291,
1157     #   W601,
1158     #   W602
1159     #
1160     max_value = 34
1161
1162     error_score = -2
1163     warning_score = -1
1164
1165     def compute(self, files_list, package_dir):
1166         files_to_score = get_files_of_type(files_list, 'module')
1167         if len(files_to_score) == 0:
1168             self.value = 0
1169             self.details = "no modules found"
1170             return self.value
1171
1172         full_paths = [os.path.join(package_dir, file) for file in files_to_score]
1173         arglist = ["-qq"] + full_paths
1174         pep8.process_options(arglist)
1175         for file in files_to_score:
1176             fullpath = os.path.join(package_dir, file)
1177             pep8.input_file(fullpath)
1178         error_stats = pep8.get_error_statistics()
1179         warning_stats = pep8.get_warning_statistics()
1180
1181         errors = len(error_stats)
1182         warnings = len(warning_stats)
1183
1184         total_error_score = self.error_score * errors
1185         total_warning_score = self.warning_score * warnings
1186
1187         score = total_error_score + total_warning_score
1188         self.value = self.max_value + score
1189
1190         self.add_info("Errors:")
1191         self.add_info("Count   Details")
1192         for stat in error_stats:
1193             self.add_info(stat)
1194         self.add_info("pep8.py found %d error types; we're scoring %d per error type" % (errors, self.error_score))
1195         self.add_info("Error score: %d" % total_error_score)
1196         self.add_info("Warnings:")
1197         self.add_info("Count   Details")
1198         for stat in warning_stats:
1199             self.add_info(stat)
1200         self.add_info("pep8.py found %d warning types; we're scoring %d per warning type" % (warnings, self.warning_score))
1201         self.add_info("Warning score: %d" % total_warning_score)
1202         self.add_info("Total pep8 score: %d - %d = %d" % (self.max_value, abs(score), self.value))
1203
1204         self.details = "pep8.py check: %d error types, %d warning types" % (errors, warnings)
1205         return self.value
1206
1207     def decide_before_download(self, cheesecake):
1208         return cheesecake.with_pep8
1209
1210 class IndexCodeKwalitee(Index):
1211     name = "CODE KWALITEE"
1212
1213     subindices = [
1214         #IndexUnitTests,
1215         IndexUnitTested,
1216         IndexPyLint,
1217         IndexPEP8,
1218     ]
1219
1220 ################################################################################
1221 ## Main Cheesecake class.
1222 ################################################################################
1223
1224 class CheesecakeError(Exception):
1225     """Custom exception class for Cheesecake-specific errors.
1226     """
1227     pass
1228
1229
1230 class CheesecakeIndex(Index):
1231     name = "Cheesecake"
1232     subindices = [
1233         IndexInstallability,
1234         IndexDocumentation,
1235         IndexCodeKwalitee,
1236     ]
1237
1238
1239 class Step(object):
1240     """Single step during computation of package score.
1241     """
1242     def __init__(self, provides):
1243         self.provides = provides
1244
1245     def decide(self, cheesecake):
1246         """Decide if step should be run.
1247
1248         It checks if there's at least one index from current profile that need
1249         variables provided by this step. Override this method for other behaviour.
1250         """
1251         for provide in self.provides:
1252             if provide in cheesecake.index.requirements:
1253                 return True
1254         return False
1255
1256 class StepByVariable(Step):
1257     """Step which is always run if given Cheesecake instance variable is true.
1258     """
1259     def __init__(self, variable_name, provides):
1260         self.variable_name = variable_name
1261         Step.__init__(self, provides)
1262
1263     def decide(self, cheesecake):
1264         if getattr(cheesecake, self.variable_name, None):
1265             return True
1266
1267         # Fallback to the default.
1268         return Step.decide(self, cheesecake)
1269
1270 class Cheesecake(object):
1271     """Computes 'goodness' of Python packages.
1272
1273     Generates "cheesecake index" that takes into account things like:
1274
1275         * whether the package can be downloaded
1276         * whether the package can be unpacked
1277         * whether the package can be installed into an alternate directory
1278         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
1279         * existence of certain directories such as doc, test, demo, examples
1280         * percentage of modules/functions/classes/methods with docstrings
1281         * percentage of functions/methods that are unit tested
1282         * average pylint score for all non-test and non-demo modules
1283     """
1284
1285     steps = {}
1286
1287     package_types = {
1288         "tar.gz": untar_package,
1289         "tgz": untar_package,
1290         "zip": unzip_package,
1291         "egg": unegg_package,
1292     }
1293
1294     def __init__(self,
1295                  keep_log                  = False,
1296                  lite                      = False,
1297                  logfile                   = None,
1298                  name                      = "",
1299                  path                      = "",
1300                  pylint_max_execution_time = None,
1301                  quiet                     = False,
1302                  sandbox                   = None,
1303                  static_only               = False,
1304                  url                       = "",
1305                  verbose                   = False,
1306                  with_pep8                 = False):
1307         """Initialize critical variables, download and unpack package,
1308         walk package tree.
1309         """
1310         self.name = name
1311         self.url = url
1312         self.package_path = path
1313
1314         if self.name:
1315             self.package = self.name
1316         elif self.url:
1317             self.package = get_package_name_from_url(self.url)
1318         elif self.package_path:
1319             self.package = get_package_name_from_path(self.package_path)
1320         else:
1321             self.raise_exception("No package name, URL or path specified... exiting")
1322
1323         # Setup a sandbox.
1324         self.sandbox = sandbox or tempfile.mkdtemp(prefix='cheesecake')
1325         if not os.path.isdir(self.sandbox):
1326             os.mkdir(self.sandbox)
1327
1328         self.verbose = verbose
1329         self.quiet = quiet
1330         self.static_only = static_only
1331         self.lite = lite
1332         self.keep_log = keep_log
1333         self.with_pep8 = with_pep8
1334         self.pylint_max_execution_time = pylint_max_execution_time
1335
1336         self.sandbox_pkg_file = ""
1337         self.sandbox_pkg_dir = ""
1338         self.sandbox_install_dir = ""
1339
1340         # Configure logging as soon as possible.
1341         self.configure_logging(logfile)
1342
1343         # Setup Cheesecake index.
1344         self.index = CheesecakeIndex()
1345
1346         self.index.decide_before_download(self)
1347         self.log.debug("Profile requirements: %s." % ', '.join(sorted(self.index.requirements)))
1348
1349         # Get the package.
1350         self.run_step('get_pkg_from_pypi')
1351         self.run_step('download_pkg')
1352         self.run_step('copy_pkg')
1353
1354         # Get package name and type.
1355         name_and_type = get_package_name_and_type(self.package, self.package_types.keys())
1356
1357         if not name_and_type:
1358             msg = "Could not determine package type for package '%s'" % self.package
1359             msg += "\nCurrently recognized types: " + ", ".join(self.package_types.keys())
1360             self.raise_exception(msg)
1361
1362         self.package_name, self.package_type = name_and_type
1363         self.log.debug("Package name: " + self.package_name)
1364         self.log.debug("Package type: " + self.package_type)
1365
1366         # Make last indices decisions.
1367         self.index.decide_after_download(self)
1368
1369         # Unpack package and list its files.
1370         self.run_step('unpack_pkg')
1371         self.run_step('walk_pkg')
1372
1373         # Install package.
1374         self.run_step('install_pkg')
1375
1376     def raise_exception(self, msg):
1377         """Cleanup, print error message and raise CheesecakeError.
1378
1379         Don't use logging, since it can be called before logging has been setup.
1380         """
1381         self.cleanup(remove_log_file=False)
1382
1383         msg += "\nDetailed info available in log file %s" % self.logfile
1384
1385         raise CheesecakeError("Error: " + msg)
1386
1387     def cleanup(self, remove_log_file=True):
1388         """Delete temporary directories and files that were created
1389         in the sandbox. At the end delete the sandbox itself.
1390         """
1391         if os.path.isfile(self.sandbox_pkg_file):
1392             self.log("Removing file %s" % self.sandbox_pkg_file)
1393             os.unlink(self.sandbox_pkg_file)
1394
1395         def delete_dir(dirname):
1396             "Delete directory recursively and generate log message."
1397             if os.path.isdir(dirname):
1398                 self.log("Removing directory %s" % dirname)
1399                 shutil.rmtree(dirname)
1400
1401         delete_dir(self.sandbox)
1402
1403         if remove_log_file and not self.keep_log:
1404             #log_path = os.path.join(self.sandbox, self.logfile)
1405             log_path = self.logfile
1406             if os.path.exists(log_path):
1407                 try:
1408                     os.unlink(log_path)
1409                 except OSError, e:
1410                     # TODO
1411                     # need to figure out how to properly delete log on Windows
1412                     # print e
1413                     pass
1414
1415     def configure_logging(self, logfile=None):
1416         """Default settings for logging.
1417
1418         If verbose, log goes to console, else it goes to logfile.
1419         log.debug and log.info goes to logfile.
1420         log.warn and log.error go to both logfile and stdout.
1421         """
1422         if logfile:
1423             self.logfile = logfile
1424         else:
1425             self.logfile = os.path.join(tempfile.gettempdir(), self.package + ".log")
1426
1427         logger.setconsumer('logfile', open(str(self.logfile), 'w', buffering=1))
1428         logger.setconsumer('console', logger.STDOUT)
1429         logger.setconsumer('null', None)
1430
1431         self.log = logger.MultipleProducer('cheesecake logfile')
1432         self.log.info = logger.MultipleProducer('cheesecake logfile')
1433         self.log.debug = logger.MultipleProducer('cheesecake logfile')
1434         self.log.warn = logger.MultipleProducer('cheesecake console')
1435         self.log.error = logger.MultipleProducer('cheesecake console')
1436
1437     def run_step(self, step_name):
1438         """Run step if its decide() method returns True.
1439         """
1440         step = self.steps[step_name]
1441         if step.decide(self):
1442             step_method = getattr(self, step_name)
1443             step_method()
1444
1445     steps['get_pkg_from_pypi'] = StepByVariable('name',
1446                                                 ['download_url',
1447                                                  'distance_from_pypi',
1448                                                  'found_on_cheeseshop',
1449                                                  'found_locally',
1450                                                  'sandbox_pkg_file'])
1451     def get_pkg_from_pypi(self):
1452         """Download package using setuptools utilities.
1453
1454         New attributes:
1455           download_url : str
1456               URL that package was downloaded from.
1457           distance_from_pypi : int
1458               How many hops setuptools had to make to download package.
1459           found_on_cheeseshop : bool
1460               Whenever package has been found on CheeseShop.
1461           found_locally : bool
1462               Whenever package has been already installed.
1463         """
1464         self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name)
1465
1466         try:
1467             from setuptools.package_index import PackageIndex
1468             from pkg_resources import Requirement
1469             from distutils import log
1470             from distutils.errors import DistutilsError
1471         except ImportError, e:
1472             msg = "setuptools is not installed and is required for downloading a package by name\n"
1473             msg += "You can download and process a package by its full URL via the -u or --url option\n"
1474             msg += "Example: python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz"
1475             self.raise_exception(msg)
1476
1477         def drop_setuptools_info(stdout, error=None):
1478             """Drop all setuptools output as INFO.
1479             """
1480             self.log.info("*** Begin setuptools output")
1481             map(self.log.info, stdout.splitlines())
1482             if error:
1483                 self.log.info(str(error))
1484             self.log.info("*** End setuptools output")
1485
1486         def fetch_package(mode):
1487             """Fetch package from PyPI.
1488
1489             Mode can be one of:
1490               * 'pypi_source': get source package from PyPI
1491               * 'pypi_any': get source/egg package from PyPI
1492               * 'any': get package from PyPI or local filesystem
1493
1494             Returns tuple (status, output), where `status` is True
1495             if fetch was successful and False if it failed. `output`
1496             is PackageIndex.fetch() return value.
1497             """
1498             if 'pypi' in mode:
1499                 pkgindex = PackageIndex(search_path=[])
1500             else:
1501                 pkgindex = PackageIndex()
1502
1503             if mode == 'pypi_source':
1504                 source = True
1505             else:
1506                 source = False
1507
1508             try:
1509                 output = pkgindex.fetch(Requirement.parse(self.name),
1510                                         self.sandbox,
1511                                         force_scan=True,
1512                                         source=source)
1513                 return True, output
1514             except DistutilsError, e:
1515                 return False, e
1516
1517         # Temporarily set the log verbosity to INFO so we can capture setuptools
1518         # info messages.
1519         old_threshold = log.set_threshold(log.INFO)
1520         old_stdout = sys.stdout
1521         sys.stdout = StdoutRedirector()
1522
1523         # Try to get source package from PyPI first, then egg from PyPI, and if
1524         # that fails search in locally installed packages.
1525         for mode, info in [('pypi_source', "source package on PyPI"),
1526                            ('pypi_any', "egg on PyPI"),
1527                            ('any', "locally installed package")]:
1528             msg = "Looking for %s... " % info
1529             status, output = fetch_package(mode)
1530             if status and output:
1531                 self.log.info(msg + "found!")
1532                 break
1533             self.log.info(msg + "failed.")
1534
1535         # Bring back old stdout.
1536         captured_stdout = sys.stdout.read_buffer()
1537         sys.stdout = old_stdout
1538         log.set_threshold(old_threshold)
1539
1540         # If all runs failed, we must raise an error.
1541         if not status:
1542             drop_setuptools_info(captured_stdout, output)
1543             self.raise_exception("setuptools returned an error: %s\n" % str(output).splitlines()[0])
1544
1545         # If fetch returned nothing, package wasn't found.
1546         if output is None:
1547             drop_setuptools_info(captured_stdout)
1548             self.raise_exception("Could not find distribution for " + self.name)
1549
1550         # Defaults.
1551         self.download_url = ""
1552         self.distance_from_pypi = 0
1553         self.found_on_cheeseshop = False
1554         self.found_locally = False
1555
1556         for line in captured_stdout.splitlines():
1557             s = re.search(r"Reading http(.*)", line)
1558             if s:
1559                 inspected_url = s.group(1)
1560                 if "python.org/pypi" not in inspected_url:
1561                     self.distance_from_pypi += 1
1562                 continue
1563             s = re.search(r"Downloading (.*)", line)
1564             if s:
1565                 self.download_url = s.group(1)
1566                 break
1567
1568         self.sandbox_pkg_file = output
1569         self.package = get_package_name_from_path(output)
1570         self.log.info("Downloaded package %s from %s" % (self.package, self.download_url))
1571
1572         if os.path.isdir(self.sandbox_pkg_file):
1573             self.found_locally = True
1574
1575         if "cheeseshop.python.org" in self.download_url:
1576             self.found_on_cheeseshop = True
1577
1578     steps['download_pkg'] = StepByVariable('url',
1579                                            ['sandbox_pkg_file',
1580                                             'downloaded_from_url'])
1581     def download_pkg(self):
1582         """Use ``urllib.urlretrieve`` to download package to file in sandbox dir.
1583         """
1584         #self.log("Downloading package %s from URL %s" % (self.package, self.url))
1585         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1586         try:
1587             downloaded_filename, headers = urlretrieve(self.url, self.sandbox_pkg_file)
1588         except IOError, e:
1589             self.log.error("Error downloading package %s from URL %s"  % (self.package, self.url))
1590             self.raise_exception(str(e))
1591         #self.log("Downloaded package %s to %s" % (self.package, downloaded_filename))
1592
1593         if headers.gettype() in ["text/html"]:
1594             f = open(downloaded_filename)
1595             if re.search("404 Not Found", "".join(f.readlines())):
1596                 f.close()
1597                 self.raise_exception("Got '404 Not Found' error while trying to download package ... exiting")
1598             f.close()
1599
1600         self.downloaded_from_url = True
1601
1602     steps['copy_pkg'] = StepByVariable('package_path',
1603                                        ['sandbox_pkg_file'])
1604     def copy_pkg(self):
1605         """Copy package file to sandbox directory.
1606         """
1607         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
1608         if not os.path.isfile(self.package_path):
1609             self.raise_exception("%s is not a valid file ... exiting" % self.package_path)
1610         self.log("Copying file %s to %s" % (self.package_path, self.sandbox_pkg_file))
1611         shutil.copyfile(self.package_path, self.sandbox_pkg_file)
1612
1613     steps['unpack_pkg'] = Step(['original_package_name',
1614                                 'sandbox_pkg_dir',
1615                                 'unpacked',
1616                                 'unpack_dir'])
1617     def unpack_pkg(self):
1618         """Unpack the package in the sandbox directory.
1619
1620         Check `package_types` attribute for list of currently supported
1621         archive types.
1622
1623         New attributes:
1624           original_package_name : str
1625               Package name guessed from the package name. Will be set only
1626               if package name is different than unpacked directory name.
1627         """
1628         self.sandbox_pkg_dir = os.path.join(self.sandbox, self.package_name)
1629         if os.path.isdir(self.sandbox_pkg_dir):
1630             shutil.rmtree(self.sandbox_pkg_dir)
1631
1632         # Call appropriate function to unpack the package.
1633         unpack = self.package_types[self.package_type]
1634         self.unpack_dir = unpack(self.sandbox_pkg_file, self.sandbox)
1635
1636         if self.unpack_dir is None:
1637             self.raise_exception("Could not unpack package %s ... exiting" % \
1638                                  self.sandbox_pkg_file)
1639
1640         self.unpacked = True
1641
1642         if self.unpack_dir != self.package_name:
1643             self.original_package_name = self.package_name
1644             self.package_name = self.unpack_dir
1645
1646     steps['walk_pkg'] = Step(['dirs_list',
1647                               'docstring_cnt',
1648                               'docformat_cnt',
1649                               'doctests_count',
1650                               'unittests_count',
1651                               'files_list',
1652                               'functions',
1653                               'classes',
1654                               'methods',
1655                               'object_cnt',
1656                               'package_dir'])
1657     def walk_pkg(self):
1658         """Get package files and directories.
1659
1660         New attributes:
1661           dirs_list : list
1662               List of directories package contains.
1663           docstring_cnt : int
1664               Number of docstrings found in all package objects.
1665           docformat_cnt : int
1666               Number of formatted docstrings found in all package objects.
1667           doctests_count : int
1668               Number of docstrings that include doctests.
1669           unittests_count : int
1670               Number of classes which inherit from unittest.TestCase.
1671           files_list : list
1672               List of files package contains.
1673           functions : list
1674               List of all functions defined in package sources.
1675           classes : list
1676               List of all classes defined in package sources.
1677           methods : list
1678               List of all methods defined in package sources.
1679           object_cnt : int
1680               Number of documentable objects found in all package modules.
1681           package_dir : str
1682               Path to project directory.
1683         """
1684         self.package_dir = os.path.join(self.sandbox, self.package_name)
1685
1686         self.files_list, self.dirs_list = get_files_dirs_list(self.package_dir)
1687
1688         self.object_cnt = 0
1689         self.docstring_cnt = 0
1690         self.docformat_cnt = 0
1691         self.doctests_count = 0
1692         self.functions = []
1693         self.classes = []
1694         self.methods = []
1695         self.unittests_count = 0
1696
1697         # Parse all application files and count objects
1698         # (modules/classes/functions) and their associated docstrings.
1699         for py_file in get_files_of_type(self.files_list, 'module'):
1700             pyfile = os.path.join(self.package_dir, py_file)
1701             code = CodeParser(pyfile, self.log.debug)
1702
1703             self.object_cnt += code.object_count()
1704             self.docstring_cnt += code.docstring_count()
1705             self.docformat_cnt += code.formatted_docstrings_count
1706             self.functions += code.functions
1707             self.classes += code.classes
1708             self.methods += code.methods
1709             self.doctests_count += code.doctests_count
1710             self.unittests_count += code.unittests_count
1711
1712         # Log a bit of debugging info.
1713         self.log.debug("Found %d files: %s." % (len(self.files_list),
1714                                                 ', '.join(self.files_list)))
1715         self.log.debug("Found %d directories: %s." % (len(self.dirs_list),
1716                                                       ', '.join(self.dirs_list)))
1717
1718     steps['install_pkg'] = Step(['installed'])
1719     def install_pkg(self):
1720         """Verify that package can be installed in alternate directory.
1721
1722         New attributes:
1723           installed : bool
1724               Describes whenever package has been succefully installed.
1725         """
1726         self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name)
1727
1728         if self.package_type == 'egg':
1729             # Create dummy Python directories.
1730             mkdirs('%s/lib/python2.3/site-packages/' % self.sandbox_install_dir)
1731             mkdirs('%s/lib/python2.4/site-packages/' % self.sandbox_install_dir)
1732
1733             environment = {'PYTHONPATH':
1734                            '%(sandbox)s/lib/python2.3/site-packages/:'\
1735                            '%(sandbox)s/lib/python2.4/site-packages/' % \
1736                            {'sandbox': self.sandbox_install_dir},
1737                            # Pass PATH to child process.
1738                            'PATH': os.getenv('PATH')}
1739             rc, output = run_cmd("easy_install --no-deps --prefix %s %s" % \
1740                                  (self.sandbox_install_dir,
1741                                   self.sandbox_pkg_file),
1742                                  environment)
1743         else:
1744             package_dir = os.path.join(self.sandbox, self.package_name)
1745             if not os.path.isdir(package_dir):
1746                 package_dir = self.sandbox
1747             cwd = os.getcwd()
1748             os.chdir(package_dir)
1749             rc, output = run_cmd("python setup.py install --root=%s" % \
1750                                  self.sandbox_install_dir)
1751             os.chdir(cwd)
1752
1753         if rc:
1754             self.log('*** Installation failed. Captured output:')
1755             # Stringify output as it may be an exception.
1756             for output_line in str(output).splitlines():
1757                 self.log(output_line)
1758             self.log('*** End of captured output.')
1759         else:
1760             self.log('Installation into %s successful.' % \
1761                      self.sandbox_install_dir)
1762             self.installed = True
1763
1764     def compute_cheesecake_index(self):
1765         """Compute overall Cheesecake index for the package by adding up
1766         specific indexes.
1767         """
1768         # Pass Cheesecake instance to the main Index object.
1769         cheesecake_index = self.index.compute_with(self)
1770
1771         # Get max value *after* computing indices, because computing
1772         #   process can remove some indices from the list.
1773         max_cheesecake_index = self.index.max_value
1774
1775         percentage = (cheesecake_index * 100) / max_cheesecake_index
1776
1777         self.log.info("A given package can currently reach a MAXIMUM number of %d points" % max_cheesecake_index)
1778         self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package))
1779
1780         # Print summary.
1781         if self.quiet:
1782             print "Cheesecake index: %d (%d / %d)" % (percentage,
1783                                                       cheesecake_index,
1784                                                       max_cheesecake_index)
1785         else:
1786             print
1787             print pad_line("=")
1788             print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", cheesecake_index)
1789             print "%s  (%d out of a maximum of %d points is %d%%)" % \
1790                   (pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage),
1791                    cheesecake_index,
1792                    max_cheesecake_index,
1793                    percentage)
1794
1795         return cheesecake_index
1796
1797 ################################################################################
1798 ## Command line.
1799 ################################################################################
1800
1801 def process_cmdline_args():
1802     """Parse command-line options.
1803     """
1804     parser = OptionParser()
1805
1806     # Options for package retrieval.
1807     parser.add_option("-n", "--name", dest="name",
1808                       default="", help="package name (will be retrieved via setuptools utilities, if present)")
1809     parser.add_option("-p", "--path", dest="path",
1810                       default="", help="path of tar.gz/zip package on local file system")
1811     parser.add_option("-u", "--url", dest="url",
1812                       default="", help="package URL")
1813
1814     # Output formatting options.
1815     parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
1816                       default=False, help="only print Cheesecake index value (default=False)")
1817     parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
1818                       default=False, help="verbose output (default=False)")
1819
1820     # Index choice options.
1821     parser.add_option("--lite", action="store_true", dest="lite",
1822                       default=False, help="don't run time-consuming tests (default=False)")
1823     parser.add_option("-t", "--static", action="store_true", dest="static",
1824                       default=False, help="don't run any code from the package being tested (default=False)")
1825     parser.add_option("--with-pep8", action="store_true", dest="with_pep8",
1826                       default=False, help="check pep8 conformance")
1827
1828     # Other options.
1829     parser.add_option("-l", "--logfile", dest="logfile",
1830                       default=None,
1831                       help="file to log all cheesecake messages")
1832     parser.add_option("-s", "--sandbox", dest="sandbox",
1833                       default=None,
1834                       help="directory where package will be unpacked "\
1835                            "(default is to use random directory inside %s)" % tempfile.gettempdir())
1836     parser.add_option("--keep-log", action="store_true", dest="keep_log",
1837                       default=False, help="don't remove log file even if run was successful")
1838     parser.add_option("--pylint-max-execution-time", action="store", dest="pylint_max_execution_time",
1839                       default=120, help="maximum time (in seconds) you allow pylint process to run (default=120)")
1840
1841     parser.add_option("-V", "--version", action="store_true", dest="version",
1842                       default=False, help="Output cheesecake version and exit")
1843
1844     (options, args) = parser.parse_args()
1845     return options
1846
1847 def main():
1848     """Display Cheesecake index for package specified via command-line options.
1849     """
1850     options = process_cmdline_args()
1851     keep_log = options.keep_log
1852     lite = options.lite
1853     logfile = options.logfile
1854     name = options.name
1855     path = options.path
1856     quiet = options.quiet
1857     sandbox = options.sandbox
1858     static_only = options.static
1859     url = options.url
1860     verbose = options.verbose
1861     version = options.version
1862     with_pep8 = options.with_pep8
1863     pylint_max_execution_time = int(options.pylint_max_execution_time)
1864
1865     if version:
1866         print "Cheesecake version %s (rev. %s)" % (VERSION, __revision__)
1867         sys.exit(0)
1868
1869     if not name and not url and not path:
1870         print "Error: No package name, URL or path specified (see --help)"
1871         sys.exit(1)
1872
1873     try:
1874         c = Cheesecake(keep_log                  = keep_log,
1875                        lite                      = lite,
1876                        logfile                   = logfile,
1877                        name                      = name,
1878                        path                      = path,
1879                        pylint_max_execution_time = pylint_max_execution_time,
1880                        quiet                     = quiet,
1881                        sandbox                   = sandbox,
1882                        static_only               = static_only,
1883                        url                       = url,
1884                        verbose                   = verbose,
1885                        with_pep8                 = with_pep8)
1886         c.compute_cheesecake_index()
1887         c.cleanup()
1888     except CheesecakeError, e:
1889         print str(e)
1890
1891 if __name__ == "__main__":
1892     main()
Note: See TracBrowser for help on using the browser.