root/branches/mk/cheesecake/cheesecake_index.py

Revision 21, 40.0 kB (checked in by mk, 7 years ago)

Use tempfile.gettempdir() instead of fixed /tmp/.

  • Property svn:executable set to
Line 
1 #!/usr/bin/env python
2 """
3 Cheesecake: How tasty is your code?
4
5 The idea of the Cheesecake project is to rank Python packages
6 based on various empiric "kwalitee" factors, such as:
7
8         * whether the package can be downloaded
9         * whether the package can be unpacked
10         * whether the package can be installed into an alternate directory
11         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
12         * existence of certain directories such as doc, test, demo, examples
13         * percentage of modules/functions/classes/methods with docstrings
14         * percentage of functions/methods that are unit tested
15         * average pylint score for all non-test and non-demo modules
16         * whether the package can be unpacked
17         * whether the package can be installed into an alternate directory
18 """
19
20 import os, sys, re, shutil
21 import tarfile, zipfile
22 import tempfile
23 from optparse import OptionParser
24 from urllib import urlretrieve
25 from urlparse import urlparse
26 from math import ceil
27
28 from _util import run_cmd, pad_with_dots, pad_left_spaces, pad_msg, pad_line
29 from _util import StdoutRedirector
30 import logger
31 from config import get_pkg_config
32 from codeparser import CodeParser
33
34 default_temp_directory = os.path.join(tempfile.gettempdir(), 'cheesecake_sandbox')
35
36
37 class Index(object):
38     """
39     Encapsulates index attributes such as name, value, details
40     """
41
42     def __init__(self, type, name="", value=0, details=""):
43         self.type = "index_" + type
44         self.name = self.type
45         if name: self.name += "_" + name
46         self.value = value
47         self.details = details
48        
49     def print_info(self):
50         """
51         Print index name padded with dots, followed by value and details
52         """
53         msg = pad_with_dots(self.name)
54         msg += pad_left_spaces(self.value)
55         msg += " (" + self.details + ")"
56         print msg
57
58 class CompositeIndex(object):
59     """
60     Collection of indexes of same type (e.g. files, dirs)
61     """
62
63     def __init__(self, type):
64         """
65         Indexes is a dict mapping names to Index objects
66         """
67         self.type = type
68         self.indexes = {}
69
70     def set_index(self, name, value=0, details=""):
71         """
72         Create new index or update existing index with specified attributes
73         """
74         if self.indexes.has_key(name):
75             index = self.indexes[name]
76             index.value = value
77             index.details = details
78         else:
79             self.indexes[name] = Index(self.type, name, value, details)
80        
81     def print_info(self):
82         """
83         Print index info for all indexes sorted alphanumerically by name
84         """
85         names = self.indexes.keys()
86         names.sort()
87         for name in names:
88             index = self.indexes[name]
89             index.print_info()
90
91     def get_value(self):
92         """
93         Return sum of individual index values
94         """
95         value = 0
96         for key in self.indexes.keys():
97             index = self.indexes[key]
98             value += index.value
99         return value
100
101     value = property(get_value)
102
103 class CheesecakeError(Exception):
104     """
105     Custom exception class for Cheesecake-specific errors
106     """
107     pass
108
109 class Cheesecake(object):
110     """
111     Computes 'goodness' of Python packages
112
113     Generates "cheesecake index" that takes into account things like:
114
115         * whether the package can be downloaded
116         * whether the package can be unpacked
117         * whether the package can be installed into an alternate directory
118         * existence of certain files such as README, INSTALL, LICENSE, setup.py etc.
119         * existence of certain directories such as doc, test, demo, examples
120         * percentage of modules/functions/classes/methods with docstrings
121         * percentage of functions/methods that are unit tested
122         * average pylint score for all non-test and non-demo modules
123     """
124
125     def __init__(self, name="", url="", path="", sandbox=None, config=None,
126                 logfile=None, verbose=False, quiet=False):
127         """
128         Initialize critical variables, download and unpack package, walk package tree
129
130         """
131         self.name = name
132         self.url = url
133         self.package_path = path
134         if not self.name and not self.url and not self.package_path:
135             self.raise_exception("No package name, URL or path specified ... exiting")
136         self.sandbox = sandbox or default_temp_directory
137         if not os.path.isdir(self.sandbox):
138             os.mkdir(self.sandbox)
139         self.config = config
140         self.verbose = verbose
141         self.quiet = quiet
142
143         self.package_types = ["tar.gz", "tgz", "zip"]
144         self.sandbox_pkg_file = ""
145         self.sandbox_pkg_dir = ""
146         self.sandbox_install_dir = ""
147
148         self.determine_pkg_name()
149         self.configure_logging(logfile)
150         self.set_defaults()
151         self.get_config()
152         self.init_indexes()
153         self.retrieve_pkg()
154         self.unpack_pkg()
155         self.walk_pkg()
156
157     def raise_exception(self, msg):
158         """
159         Cleanup, print error message and raise CheesecakeError
160
161         Don't use logging, since it can be called before logging has been setup
162         """
163         self.cleanup()
164         os.unlink(os.path.join(self.sandbox, self.logfile))
165
166         msg += "\n" + pad_msg("CHEESECAKE INDEX", 0)
167         raise CheesecakeError(msg)
168  
169     def cleanup(self):
170         """
171         Delete temporary directories and files that were
172         created in the sandbox. At the end delete the sandbox itself.
173         """
174         if os.path.isfile(self.sandbox_pkg_file):
175             self.log("Removing file %s" % self.sandbox_pkg_file)
176             os.unlink(self.sandbox_pkg_file)
177
178         def delete_dir(dirname):
179             "Delete directory recursively and generate log message."
180             if os.path.isdir(dirname):
181                 self.log("Removing directory %s" % dirname)
182                 shutil.rmtree(dirname)
183
184         for dir in [self.sandbox_pkg_dir, self.sandbox_install_dir,
185                     self.sandbox]:
186             delete_dir(dir)
187
188     def set_defaults(self):
189         """
190         Set default values for variables that can also be defined
191         in the config file
192         """
193         self.INDEX_PYPI_DOWNLOAD = 50
194         self.INDEX_PYPI_DISTANCE = 5
195         self.INDEX_URL_DOWNLOAD  = 25
196         self.INDEX_UNPACK        = 25
197         self.INDEX_UNPACK_DIR    = 15
198         self.INDEX_INSTALL       = 50
199         self.INDEX_FILE_CRITICAL = 15
200         self.INDEX_FILE          = 10
201         self.INDEX_REQUIRED_FILES = 100
202         self.INDEX_FILE_PYC      = -20
203         self.INDEX_DIR_CRITICAL  = 25
204         self.INDEX_DIR           = 20
205         self.INDEX_DIR_EMPTY     = 5
206         self.MAX_INDEX_DOCSTRINGS = 100 # max. percentage of modules/classes/methods/functions with docstrings
207         self.MAX_INDEX_UNITTESTS  = 100 # max. percentage of methods/functions that are unit tested
208         self.MAX_INDEX_PYLINT     = 100 # max. pylint score
209         self.cheese_files = ["readme", "install", "changelog",
210                             "news", "faq",
211                             "todo", "thanks",
212                             "license", "announce",
213                             "setup.py",
214                             ]
215         self.critical_cheese_files = ["readme", "license", "setup.py"]
216         self.cheese_dirs = ["doc", "test", "example", "demo"]
217         self.critical_cheese_dirs = ["doc", "test"]
218
219     def get_config(self, config_dir=None):
220         """
221         Retrieve values from configuration file
222         """
223         self.config = get_pkg_config(self.short_pkg_name, config_dir)
224         for config_var in ["INDEX_PYPI_DOWNLOAD", "INDEX_PYPI_DISTANCE",
225             "INDEX_URL_DOWNLOAD", "INDEX_UNPACK", "INDEX_UNPACK_DIR",
226             "INDEX_INSTALL", "INDEX_FILE_CRITICAL", "INDEX_FILE",
227             "INDEX_REQUIRED_FILES", "INDEX_FILE_PYC",
228             "INDEX_DIR_CRITICAL", "INDEX_DIR", "INDEX_DIR_EMPTY",
229             "MAX_INDEX_DOCSTRINGS", "MAX_INDEX_PYLINT",
230             "cheese_files", "critical_cheese_files",
231             "cheese_dirs", "critical_cheese_dirs",
232             ]:
233             value = self.config.get(config_var)
234             if value: setattr(self, config_var, value)
235
236     def determine_pkg_name(self):
237         if self.name:
238             self.package = self.name
239             self.short_pkg_name = self.name
240         elif self.package_path:
241             self.package = self.get_package_from_path(self.package_path)
242         else:
243             self.package = self.get_package_from_url()
244
245     def get_package_from_url(self):
246         """
247         Use ``urlparse`` to obtain package path from URL
248         """
249         (scheme,location,path,param,query,fragment_id) = urlparse(self.url)
250         return self.get_package_from_path(path)
251
252
253     def get_package_from_path(self, path):
254         """
255         Get package name as file portion of path
256         """
257         dir, file = os.path.split(path)
258         self.short_pkg_name = file
259         for package_type in self.package_types:
260             s = re.search("(.+)\.%s" % package_type, file)
261             if s:
262                 self.short_pkg_name = s.group(1)
263                 break
264         return file
265
266     def configure_logging(self, logfile=None):
267         """
268         Default settings for logging
269
270         if verbose, log goes to console, else it goes to logfile
271         log.debug goes to logfile
272         log.info goes to console
273         log.warn and log.error go to both logfile and stdout
274         """
275         if logfile:
276             self.logfile = logfile
277         else:
278             self.logfile = os.path.join(tempfile.gettempdir(), self.short_pkg_name + ".log")
279
280         logger.setconsumer('logfile', open(str(self.logfile), 'w', buffering=1))
281         logger.setconsumer('console', logger.STDOUT)
282         logger.setconsumer('null', None)
283
284         if self.verbose:
285             self.log = logger.MultipleProducer('cheesecake console')
286         else:
287             self.log = logger.MultipleProducer('cheesecake logfile')
288         if self.quiet:
289             self.log.info = logger.MultipleProducer('cheesecake logfile')
290         else:
291             self.log.info = logger.MultipleProducer('cheesecake console')
292         self.log.debug = logger.MultipleProducer('cheesecake logfile')
293         self.log.warn = logger.MultipleProducer('cheesecake console')
294         self.log.error = logger.MultipleProducer('cheesecake console')
295
296         self.log.debug("package = ", self.short_pkg_name)
297
298     def init_indexes(self):
299         """
300         Initialize variables used in index computation
301
302         * cheesecake_index: overall index for the package
303         * index: dict holding Index or CompositeIndex objects of various types
304         """
305         self.cheesecake_index = 0
306         self.cheesecake_index_installability = 0
307         self.cheesecake_index_documentation = 0
308         self.cheesecake_index_codekwalitee = 0
309         self.max_cheesecake_index = self.INDEX_PYPI_DOWNLOAD + \
310                                     self.INDEX_UNPACK + \
311                                     self.INDEX_UNPACK_DIR + \
312                                     self.INDEX_INSTALL + \
313                                     self.MAX_INDEX_DOCSTRINGS + \
314                                     self.MAX_INDEX_PYLINT
315 #                                    self.MAX_INDEX_UNITTESTS
316         self.max_cheesecake_index_installability = self.INDEX_PYPI_DOWNLOAD + \
317                                             self.INDEX_UNPACK + \
318                                             self.INDEX_UNPACK_DIR + \
319                                             self.INDEX_INSTALL
320         self.max_cheesecake_index_documentation = self.INDEX_REQUIRED_FILES + \
321                                         self.MAX_INDEX_DOCSTRINGS
322         self.max_cheesecake_index_codekwalitee = self.MAX_INDEX_PYLINT
323 #                                        self.MAX_INDEX_UNITTESTS
324                                        
325         self.index = {}
326         for index_type in ["file", "dir"]:
327             self.index[index_type] = CompositeIndex(index_type)
328         for index_type in ["pypi_download", "url_download",
329                            "unpack_dir", "unpack", "install",
330                            "docstrings", "unittests", "pylint"]:
331             self.index[index_type] = Index(index_type)
332
333         for cheese_file in self.cheese_files:
334             self.index["file"].set_index(name=cheese_file, details="file not found")
335             if cheese_file in self.critical_cheese_files:
336                 self.max_cheesecake_index += self.INDEX_FILE_CRITICAL
337                 self.max_cheesecake_index_documentation += self.INDEX_FILE_CRITICAL
338             else:
339                 self.max_cheesecake_index += self.INDEX_FILE
340                 self.max_cheesecake_index_documentation += self.INDEX_FILE
341         self.log.debug("cheese_files: " + ",".join(self.cheese_files))
342         self.log.debug("critical_cheese_files: " + ",".join(self.critical_cheese_files))
343
344         for cheese_dir in self.cheese_dirs:
345             self.index["dir"].set_index(name=cheese_dir, details="directory not found")
346             if cheese_dir in self.critical_cheese_dirs:
347                 self.max_cheesecake_index += self.INDEX_DIR_CRITICAL
348                 self.max_cheesecake_index_documentation += self.INDEX_DIR_CRITICAL
349             else:
350                 self.max_cheesecake_index += self.INDEX_DIR
351                 self.max_cheesecake_index_documentation += self.INDEX_DIR
352         self.log.debug("cheese_dirs: " + ",".join(self.cheese_dirs))
353         self.log.debug("critical_cheese_dirs: " + ",".join(self.critical_cheese_dirs))
354
355         self.pkg_files = {}
356         self.pkg_dirs = {}
357         self.file_types =   ["py", "pyc", "test",
358                             ]
359         for type in self.file_types:
360             self.pkg_files[type] = []
361
362         self.object_cnt = 0  # Number of modules/functions/classes/methods in .py files found
363         self.docstring_cnt = 0
364         self.functions = [] # List of methods/functions found in .py files
365
366     def retrieve_pkg(self):
367         if self.name:
368             self.get_pkg_from_pypi()
369         elif self.url:
370             self.download_pkg()
371         else:
372             self.copy_pkg()
373
374     def get_package_from_url(self):
375         """
376         Use ``urlparse`` to obtain package path from URL
377         """
378         (scheme,location,path,param,query,fragment_id) = urlparse(self.url)
379         return self.get_package_from_path(path)
380        
381
382     def get_package_from_path(self, path):
383         """
384         Get package name as file portion of path
385         """
386         dir, file = os.path.split(path)
387         self.short_pkg_name = file
388         for package_type in self.package_types:
389             s = re.search("(.+)\.%s" % package_type, file)
390             if s:
391                 self.short_pkg_name = s.group(1)
392                 break
393         return file
394
395     def get_pkg_from_pypi(self):
396         """
397         Download package using setuptools utilities
398         """
399         try:
400             self.log.info("Trying to download package %s from PyPI using setuptools utilities" % self.name)
401             from setuptools.package_index import PackageIndex
402             from pkg_resources import Requirement
403             from distutils import log
404             # Temporarily set the log verbosity to INFO so we can capture setuptools info messages
405             old_threshold = log.set_threshold(log.INFO)
406             pkgindex = PackageIndex()
407             old_stdout = sys.stdout
408             sys.stdout = StdoutRedirector()
409             output = pkgindex.fetch(Requirement.parse(self.name),
410                                     self.sandbox,
411                                     force_scan=True,
412                                     source=True)
413             captured_stdout = sys.stdout.read_buffer()
414             sys.stdout = old_stdout
415             log.set_threshold(old_threshold)
416             if output is None:
417                 self.raise_exception("Error: Could not find distribution for " + self.name)
418             download_url = ""
419             distance_from_pypi = 0
420             #print captured_stdout
421             for line in captured_stdout.split('\n'):
422                 s = re.search(r"Reading http(.*)", line)
423                 if s:
424                     inspected_url = s.group(1)
425                     if not re.search(r"www.python.org\/pypi", inspected_url):
426                         distance_from_pypi += 1
427                     continue
428                 s = re.search(r"Downloading (.*)", line)
429                 if s:
430                     download_url = s.group(1)
431                     break
432             self.sandbox_pkg_file = output
433             self.package = self.get_package_from_path(output)
434             self.log.info("Downloaded package %s from %s" % (self.package, download_url))
435             index_type = "pypi_download"
436             found_on_cheeseshop = False
437             if re.search(r"cheeseshop.python.org", download_url):
438                 value = self.INDEX_PYPI_DOWNLOAD
439                 found_on_cheeseshop = True
440             else:
441                 value = self.INDEX_PYPI_DOWNLOAD - distance_from_pypi * self.INDEX_PYPI_DISTANCE
442             self.index[index_type].value = value
443             details = "downloaded package " + self.package
444             if found_on_cheeseshop:
445                 details += " directly from the Cheese Shop"
446             elif distance_from_pypi:
447                 details += " following %d link" % distance_from_pypi
448                 if distance_from_pypi > 1:
449                     details += "s"
450                 details += " from PyPI"
451             else:
452                 details += "from " + download_url
453             self.index[index_type].details = details
454         except ImportError, e:
455             msg = "Error: setuptools is not installed and is required for downloading a package by name\n"
456             msg += "You can donwload and process a package by its full URL via the -u or --url option\n"
457             msg += "Example: python cheesecake.py --url=http://www.mems-exchange.org/software/durus/Durus-3.1.tar.gz"
458             self.raise_exception(msg)
459        
460     def download_pkg(self):
461         """
462         Use ``urllib.urlretrieve`` to download package to file in sandbox dir
463         """
464         #self.log("Downloading package %s from URL %s" % (self.package, self.url))
465         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
466         try:
467             downloaded_filename, headers = urlretrieve(self.url, self.sandbox_pkg_file)
468         except IOError, e:
469             self.log.error("Error downloading package %s from URL %s"  % (self.package, self.url))
470             self.raise_exception(str(e))
471         #self.log("Downloaded package %s to %s" % (self.package, downloaded_filename))
472         if re.search("Content-Type: details/html", str(headers)):
473             f = open(downloaded_filename)
474             if re.search("404 Not Found", "".join(f.readlines())):
475                 f.close()
476                 self.raise_exception("Got '404 Not Found' error while trying to download package ... exiting")
477             f.close()
478         index_type = "url_download"
479         self.index[index_type].value = self.INDEX_URL_DOWNLOAD
480         self.index[index_type].details = "downloaded package %s from URL %s"  % (self.package, self.url)
481        
482     def copy_pkg(self):
483         """
484         Copy package file to sandbox directory
485         """
486         self.sandbox_pkg_file = os.path.join(self.sandbox, self.package)
487         if not os.path.isfile(self.package_path):
488             self.raise_exception("%s is not a valid file ... exiting" % self.package_path)
489         self.log("Copying file %s to %s" % (self.package_path, self.sandbox_pkg_file))
490         shutil.copyfile(self.package_path, self.sandbox_pkg_file)
491
492     def unpack_pkg(self):
493         """
494         Unpack the package in the sandbox directory
495        
496         Currently supported archive types:
497
498         * .tar.gz (handled with ``tarfile`` module)
499         * .zip (handled with ``zipfile`` module)
500         """
501         self.package_type = ""
502         for type in self.package_types:
503             s = re.search(r"(.+)\.%s" % type, self.package)
504             if s:
505                 # package_name is name of package without file extension (ex. twill-7.3)
506                 self.package_name = s.group(1)
507                 self.package_type = type
508                 break
509         if not self.package_type:
510             msg = "Could not determine package type for package '%s'" % self.package
511             msg += "\nCurrently recognized types: " + " ".join(self.package_types)
512             self.raise_exception(msg)
513         self.log.debug("Package name: " + self.package_name)
514         self.log.debug("Package type: " + self.package_type)
515
516         self.sandbox_pkg_dir = os.path.join(self.sandbox, self.package_name)
517         if os.path.isdir(self.sandbox_pkg_dir):
518             shutil.rmtree(self.sandbox_pkg_dir)
519
520         if self.package_type in ["tar.gz", "tgz"]:
521             self.untar_pkg()
522         elif self.package_type == "zip":
523             self.unzip_pkg()
524
525         index_type = "unpack_dir"
526         details = "unpack directory is " + self.unpack_dir
527         if self.unpack_dir != self.package_name:
528             details += " instead of the expected " + self.package_name
529             self.package_name = self.unpack_dir
530         else:
531             details += " as expected"
532             self.index[index_type].value = self.INDEX_UNPACK_DIR
533         self.index[index_type].details = details
534
535         if not self.quiet:
536             self.log.info("Detailed info available in log file %s" % self.logfile)
537
538     def untar_pkg(self):
539         """
540         Untar the package in the sandbox directory
541
542         Uses tarfile module
543         """
544         try:
545             t = tarfile.open(self.sandbox_pkg_file)
546         except tarfile.ReadError, e:
547             self.raise_exception("Could not read tar file %s ... exiting" % self.sandbox_pkg_file)
548
549         for member in t.getmembers():
550             t.extract(member, self.sandbox)
551
552         tarinfo = t.members[0]
553         self.unpack_dir = tarinfo.name.split(os.sep)[0]
554
555         index_type = "unpack"
556         self.index[index_type].value = self.INDEX_UNPACK
557         self.index[index_type].details = "package untar-ed successfully"
558            
559     def unzip_pkg(self):
560         """
561         Unzip the package in the sandbox directory
562
563         Uses zipfile module
564         """
565         try:
566             z = zipfile.ZipFile(self.sandbox_pkg_file)
567         except zipfile.error:
568             self.raise_exception("Error unzipping file %s ... exiting" % self.sandbox_pkg_file)
569
570         # Get directory structure from zip and create it in sandbox
571         for name in z.namelist():
572             (dir, file) = os.path.split(name)
573             unpack_dir = dir
574             target_dir = os.path.join(self.sandbox, dir)
575             if not os.path.exists(target_dir):
576                 os.makedirs(target_dir)
577
578         # Extract files to directory structure
579         for i, name in enumerate(z.namelist()):
580             if not name.endswith('/'):
581                 outfile = open(os.path.join(self.sandbox, name), 'wb')
582                 outfile.write(z.read(name))
583                 outfile.flush()
584                 outfile.close()
585
586         self.unpack_dir = unpack_dir.split(os.sep)[0]
587
588         index_type = "unpack"
589         self.index[index_type].value = self.INDEX_UNPACK
590         self.index[index_type].details = "package unzipped successfully"
591
592     def walk_pkg(self):
593         """
594         Traverse the file system tree rooted at sandbox/package_name
595
596         * Compute indexes for special files and directories
597         * Identify Python files, test files, etc.
598         """
599         cwd = os.getcwd()
600         os.chdir(self.sandbox)
601         for rootdir, dirs, files in os.walk(self.package_name):
602             head, tail = os.path.split(rootdir)
603             dirs_in_rootdir = rootdir.split(os.path.sep)
604             for cheese_dir in self.cheese_dirs:
605                 if re.search("^%s" % cheese_dir, tail):
606                     if files or dirs:
607                         if cheese_dir in self.critical_cheese_dirs:
608                             value = self.INDEX_DIR_CRITICAL
609                             details = "critical directory found"
610                             self.log.debug("critical_cheese_dir found: " + cheese_dir)
611                         else:
612                             value = self.INDEX_DIR
613                             details = "directory found"
614                             self.log.debug("cheese_dir found: " + cheese_dir)
615                     else:
616                         value = self.INDEX_DIR_EMPTY
617                         details = "empty directory found"
618                         self.log.debug("empty cheese_dir found: " + cheese_dir)
619                     self.index["dir"].set_index(cheese_dir, value, details)
620             for file in files:
621                 fullpath = os.path.join(rootdir, file)
622                 for cheese_file in self.cheese_files:
623                     if re.search(r"^%s(\.txt)*" % cheese_file, file, re.IGNORECASE):
624                         if cheese_file in self.critical_cheese_files:
625                             value = self.INDEX_FILE_CRITICAL
626                             details = "critical file found"
627                             self.log.debug("critical_cheese_file found: " + cheese_file)
628                         else:
629                             value = self.INDEX_FILE
630                             details = "file found"
631                             self.log.debug("cheese_file found: " + cheese_file)
632                         self.index["file"].set_index(cheese_file, value, details)
633
634                 if self.is_py_file(file, dirs_in_rootdir):
635                     self.pkg_files["py"].append(fullpath)
636                     self.log.debug("py file found: " + fullpath)
637                     pyfile = os.path.join(self.sandbox, fullpath)
638                     # Parse the file and count objects (modules/classes/functions)
639                     # and their associated docstrings
640                     code = CodeParser(pyfile, self.log.debug)
641                     self.object_cnt += code.object_count()
642                     self.docstring_cnt += code.docstring_count()
643                     self.functions += code.functions
644
645                 if os.path.splitext(file)[1] == ".pyc":
646                     self.pkg_files["pyc"].append(fullpath)
647                     self.log.debug("pyc file found: " + fullpath)
648
649                 if self.is_test_file(file, dirs_in_rootdir):
650                     self.pkg_files["test"].append(fullpath)
651                     self.log.debug("test file found: " + fullpath)
652
653         len_pyc_list = len(self.pkg_files["pyc"])
654         if len_pyc_list:
655             self.index["file"].set_index("pyc", value=self.INDEX_FILE_PYC,
656                 details="%d .pyc files found" % len_pyc_list)
657         self.log.debug("Found %d py files" % len(self.pkg_files["py"]))
658         self.log.debug("Found %d pyc files" % len(self.pkg_files["pyc"]))
659         self.log.debug("Found %d test files" % len(self.pkg_files["test"]))
660
661         os.chdir(cwd)
662
663     def is_py_file(self, file, dirs):
664         """
665         Return True if file ends with .py and it is not a special file and it is not
666         in special directory
667         """
668         if os.path.splitext(file)[1] != ".py":
669             return False
670         if file in ["setup.py", "ez_setup.py", "__init__.py", "__pkginfo__.py"]:
671             return False
672         for dir in dirs:
673             if dir.startswith("test") or \
674                 dir.startswith("docs") or \
675                 dir.startswith("demo") or \
676                 dir.startswith("example"):
677                 return False
678         return True
679
680     def is_test_file(self, file, dirs):
681         """
682         Return True is file is in directory rooted at "test" or "tests"
683         """
684         if not file.endswith(".py"):
685             return False
686         if file in ["__init__.py"]:
687             return False
688         for dir in dirs:
689             if dir.startswith("test"):
690                 return True
691         return False
692
693     def index_file(self):
694         """
695         Return CompositeIndex object of type "file"
696         """
697         return self.index["file"]
698
699     def index_dir(self):
700         """
701         Return CompositeIndex object of type "dir"
702         """
703         return self.index["dir"]
704
705     def index_pypi_download(self):
706         """
707         Verify that package can be downloaded from PyPI
708
709         Return Index object of type "pypi_download"
710         """
711         index_type = "pypi_download"
712         if self.url:
713             # Package was downloaded directly from URL
714             self.index[index_type].value = 0
715             self.index[index_type].details = "package was downloaded directly from URL"
716
717         if self.package_path:
718             # Package was processed from file system path
719             self.index[index_type].value = 0
720             self.index[index_type].details = "package was processed from file system path"
721            
722         # Otherwise, index["pypi_download"] was already set in get_pkg_from_pypi()
723         return self.index["pypi_download"]
724
725     def index_url_download(self):
726         """
727         Verify that package can be downloaded from an URL
728
729         Return Index object of type "download"
730         """
731         # index["download"] is already set in download_pkg()
732         return self.index["url_download"]
733
734     def index_unpack(self):
735         """
736         Verify that package can be unpacked
737
738         Return Index object of type "unpack"
739         """
740         # index["unpack"] is already set in unpack_pkg()
741         return self.index["unpack"]
742
743
744     def index_unpack_dir(self):
745         """
746         Verify that unpack directory has same name as package
747
748         Return Index object of type "unpack_dir"
749         """
750         # index["unpack_dir"] is already set in unpack_pkg()
751         return self.index["unpack_dir"]
752
753     def index_install(self):
754         """
755         Verify that package can be installed in alternate directory
756
757         Return Index object of type "install"
758         """
759         index_type = "install"
760         self.sandbox_install_dir = os.path.join(self.sandbox, "tmp_install_%s" % self.package_name)
761         cwd = os.getcwd()
762         os.chdir(os.path.join(self.sandbox, self.package_name))
763         rc, output = run_cmd("python setup.py install --root=" + self.sandbox_install_dir)
764         if not rc:
765             # Install succeeded
766             self.index[index_type].value = self.INDEX_INSTALL
767             self.index[index_type].details = details="package installed in %s" % self.sandbox_install_dir
768         else:
769             # Install failed
770             self.index[index_type].details = "could not install package in %s" % self.sandbox_install_dir
771         os.chdir(cwd)
772         return self.index[index_type]
773
774     def index_docstrings(self):
775         """
776         Compute docstring index as percentage of modules/classes/methods/functions
777         that have docstrings associated with them
778
779         Return Index object of type "docstrings"
780         """
781         index_type = "docstrings"
782         if self.object_cnt:
783             percent = float(self.docstring_cnt)/float(self.object_cnt)
784         else:
785             percent = 0
786         index_value = int(ceil(percent*100))
787         details = "found %d/%d=%.2f%% modules/classes/methods/functions with docstrings" %\
788                  (self.docstring_cnt, self.object_cnt, percent*100)
789         self.index[index_type].value = index_value
790         self.index[index_type].details = details
791         return self.index[index_type]
792
793     def index_unittests(self):
794         """
795         Compute unittest index as percentage of methods/functions
796         that are exercised in unit tests
797
798         Return Index object of type "unittests"
799         """
800         unittest_cnt = 0
801         index_type = "unittests"
802         self.functions_tested = {}
803         for testfile in self.pkg_files["test"]:
804             fullpath = os.path.join(self.sandbox, testfile)
805             code = CodeParser(fullpath, self.log.debug)
806             func_called = code.functions_called()
807             self.log.debug("Functions called in unit test:")
808             self.log.debug(func_called)
809             for func in func_called:
810                 self.functions_tested[func] = 1
811         self.log.debug("FUNCTIONS TO BE CHECKED WHETHER THEY ARE UNIT TESTED:")
812         self.log.debug(self.functions)
813         self.log.debug("FUNCTIONS THAT ARE UNIT TESTED:")
814         self.log.debug(self.functions_tested.keys())
815         for funcname in self.functions:
816             if self.is_unit_tested(funcname):
817                 unittest_cnt += 1
818                 self.log.debug("%s is unit tested" % funcname)
819         cnt = len(self.functions)
820         if cnt:
821             percent = float(unittest_cnt)/float(cnt)
822         else:
823             percent = 0
824         index_value = int(ceil(percent*100))
825         details = "found %d/%d=%.2f%% unit tested methods/functions" % (unittest_cnt, cnt, percent*100)
826         self.index[index_type].value = index_value
827         self.index[index_type].details = details
828         return self.index[index_type]
829
830     def is_unit_tested(self, funcname):
831         elem = funcname.split(".")
832         n1 = elem[-1]
833         n2 = ""
834         if len(elem) > 1:
835             n2 = elem[-2] + "." + elem[-1]
836         for key in self.functions_tested.keys():
837             if key.startswith(n1) or (n2 and key.startswith(n2)):
838                 return True
839         return False
840
841     def index_pylint(self):
842         """
843         Compute pylint index as average of positive pylint scores obtained for
844         the Python files identified in the package
845
846         Return Index object of type "pylint"
847         """
848         index_type = "pylint"
849             # Try to run the pylint script
850         rc, output = run_cmd("pylint --version")
851         if rc:
852             # We encountered an error
853             self.index[index_type].details = "pylint not properly installed"
854             return self.index[index_type]
855         index_pylint = 0
856         cnt = 0
857         for pyfile in self.pkg_files["py"]:
858             (path, filename) = os.path.split(pyfile)
859             (module, ext) = os.path.splitext(filename)
860             if module == "setup" or module == "ez_setup" or module.startswith("__"):
861                 continue
862             fullpath = os.path.join(self.sandbox, pyfile)
863             self.log.debug("Running pylint on file " + fullpath)
864             rc, output = run_cmd("pylint " + fullpath)
865             if rc:
866                 # We encountered an error
867                 continue
868             score_line = output.split("\n")[-3]
869             s = re.search(r" (\d+\.\d+)/10", score_line)
870             # We only take positive scores into account
871             if s:
872                 score = s.group(1)
873                 self.log.debug("pylint score for module %s: %s" % (module, score))
874                 if score == "0.00":
875                     self.log.debug("Ignoring scores of 0.00")
876                     continue
877                 index_pylint += float(score)
878                 cnt += 1
879         if cnt:
880             avg_value = float(index_pylint)/float(cnt)
881         else:
882             avg_value = 0
883         index_value = int(ceil(avg_value*10))
884         self.index[index_type].value = index_value
885         self.index[index_type].details = "average score is %.2f out of 10" % avg_value
886         return self.index[index_type]
887
888     def compute_cheesecake_index(self):
889         """
890         Compute overall Cheesecake index for the package by adding up
891         specific indexes
892         """
893         self.log.info("A given package can currently reach a MAXIMUM number of %d points" % self.max_cheesecake_index)
894         self.log.info("Starting computation of Cheesecake index for package '%s'" % (self.package))
895
896         index_types = []
897         #if self.name:
898         #    index_types.append("pypi_download")
899         index_types.append("pypi_download")
900         if self.url:
901             index_types.append("url_download")
902         index_types += ["unpack", "unpack_dir", "install"]
903         self.cheesecake_index_installability = self.process_partial_index("INSTALLABILITY",\
904                                          index_types, self.max_cheesecake_index_installability)
905
906         index_types = ["file", "dir", "docstrings"]
907         self.cheesecake_index_documentation = self.process_partial_index("DOCUMENTATION",\
908                                          index_types, self.max_cheesecake_index_documentation)
909
910         index_types = [
911                         #"unittests",
912                         "pylint",
913                         ]
914         self.cheesecake_index_codekwalitee = self.process_partial_index("CODE KWALITEE",\
915                                          index_types, self.max_cheesecake_index_codekwalitee)
916
917         print
918         self.print_separator_line("=")
919         print pad_msg("OVERALL CHEESECAKE INDEX (ABSOLUTE)", self.cheesecake_index)
920         percentage = (self.cheesecake_index * 100) / self.max_cheesecake_index
921         msg = pad_msg("OVERALL CHEESECAKE INDEX (RELATIVE)", percentage)
922         msg += " (%d out of a maximum of %d points is %d%%)" %\
923              (self.cheesecake_index, self.max_cheesecake_index, percentage)
924         print msg
925         self.cleanup()
926
927         return self.cheesecake_index
928
929     def process_partial_index(self, partial_index_name, index_types, max_value):
930         print
931         self.log.info("Starting computation of %s index (max. points = %d)" % \
932                             (partial_index_name, max_value))
933         partial_index_value = 0
934         for index_type in index_types:
935             partial_index_value += self.process_index(index_type)
936
937         self.print_separator_line()
938         print pad_msg("%s INDEX (ABSOLUTE)" % partial_index_name, partial_index_value)
939         percentage = (partial_index_value * 100) / max_value
940         msg = pad_msg("%s INDEX (RELATIVE)" % partial_index_name, percentage)
941         msg += " (%d out of a maximum of %d points is %d%%)" %\
942              (partial_index_value, max_value, percentage)
943         print msg
944         return partial_index_value
945
946     def process_index(self, index_type):
947         """
948         Compute and print index of specified type
949         """
950         index = self.index[index_type]
951         index_method = "index_" + index_type
952         getattr(self, index_method)()
953         if not self.quiet:
954             index.print_info()
955         self.cheesecake_index += index.value
956         return index.value
957
958     def print_separator_line(self, char="-"):
959         """
960         Print line of text, unless quiet flag was given
961         """
962         if self.quiet:
963             return
964         print pad_line(char)
965
966
967 def process_cmdline_args():
968     """
969     Parse command-line options
970     """
971     parser = OptionParser()
972     parser.add_option("-n", "--name", dest="name",
973                       default="", help="package name (will be retrieved via setuptools utilities, if present)")
974     parser.add_option("-u", "--url", dest="url",
975                       default="", help="package URL")
976     parser.add_option("-p", "--path", dest="path",
977                       default="", help="package path on local file system")
978     parser.add_option("-s", "--sandbox", dest="sandbox",
979                       default=default_temp_directory,
980                       help="directory where package will be unpacked (default=%s)" % default_temp_directory)
981     parser.add_option("-c", "--config", dest="config",
982                       default=None,
983                       help="directory with custom configuration (default=~/.cheesecake)")
984     parser.add_option("-l", "--logfile", dest="logfile",
985                       default=None,
986                       help="file to log all cheesecake messages")
987     parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
988                       default=False, help="verbose output (default=False)")
989     parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
990                       default=False, help="only print Cheesecake index value (default=False)")
991
992     (options, args) = parser.parse_args()
993     return options
994
995 def main():
996     """
997     Display Cheesecake index for package specified via command-line options
998     """
999     options = process_cmdline_args()
1000     name = options.name
1001     url = options.url
1002     path = options.path
1003     sandbox = options.sandbox
1004     config = options.config
1005     logfile = options.logfile
1006     verbose = options.verbose
1007     quiet = options.quiet
1008
1009     if not name and not url and not path:
1010         print "Error: No package name, URL or path specified (see --help)"
1011         sys.exit(1)
1012
1013     try:
1014         c = Cheesecake(name=name, url=url, path=path, sandbox=sandbox,
1015                        config=config, logfile=logfile, verbose=verbose,
1016                        quiet=quiet)
1017         c.compute_cheesecake_index()
1018     except CheesecakeError, e:
1019         print str(e)
1020
1021 if __name__ == "__main__":
1022     main()
Note: See TracBrowser for help on using the browser.