root/branches/mk/cheesecake/cheesecake_index.py

Revision 24, 40.9 kB (checked in by mk, 7 years ago)

Decrease score for .pyo files (closes ticket #4).

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