| | 59 | def discover_file_type(filename): |
|---|
| | 60 | """Discover type of a file according to its name and its parent directory. |
|---|
| | 61 | |
|---|
| | 62 | Currently supported file types: |
|---|
| | 63 | * pyc |
|---|
| | 64 | * pyo |
|---|
| | 65 | * module: .py files of an application |
|---|
| | 66 | * demo: .py files for documentation/demonstration purposes |
|---|
| | 67 | * test: .py files used for testing |
|---|
| | 68 | * special: .py file for special purposes |
|---|
| | 69 | |
|---|
| | 70 | :Note: This function only check file's name, and doesn't touch the |
|---|
| | 71 | filesystem. If you have to, check if file exists by yourself. |
|---|
| | 72 | |
|---|
| | 73 | >>> discover_file_type('module.py') |
|---|
| | 74 | 'module' |
|---|
| | 75 | >>> discover_file_type('./setup.py') |
|---|
| | 76 | 'special' |
|---|
| | 77 | >>> discover_file_type('some/directory/junk.pyc') |
|---|
| | 78 | 'pyc' |
|---|
| | 79 | """ |
|---|
| | 80 | dirs = filename.split(os.path.sep) |
|---|
| | 81 | dirs, filename = dirs[:-1], dirs[-1] |
|---|
| | 82 | |
|---|
| | 83 | if filename in ["setup.py", "ez_setup.py", "__pkginfo__.py"]: |
|---|
| | 84 | return 'special' |
|---|
| | 85 | |
|---|
| | 86 | if has_extension(filename, ".pyc"): |
|---|
| | 87 | return 'pyc' |
|---|
| | 88 | if has_extension(filename, ".pyo"): |
|---|
| | 89 | return 'pyo' |
|---|
| | 90 | if has_extension(filename, ".py"): |
|---|
| | 91 | for dir in dirs: |
|---|
| | 92 | if dir in ['test', 'tests']: |
|---|
| | 93 | return 'test' |
|---|
| | 94 | elif dir in ['docs', 'demo', 'example']: |
|---|
| | 95 | return 'demo' |
|---|
| | 96 | return 'module' |
|---|
| | 97 | |
|---|
| | 98 | def get_files_of_type(file_list, file_type): |
|---|
| | 99 | """Return files from `file_list` that match given `file_type`. |
|---|
| | 100 | |
|---|
| | 101 | >>> file_list = ['test/test_foo.py', 'setup.py', 'README', 'test/test_bar.py'] |
|---|
| | 102 | >>> get_files_of_type(file_list, 'test') |
|---|
| | 103 | ['test/test_foo.py', 'test/test_bar.py'] |
|---|
| | 104 | """ |
|---|
| | 105 | return filter(lambda x: discover_file_type(x) == file_type, file_list) |
|---|
| | 106 | |
|---|
| | 107 | def get_method_arguments(method): |
|---|
| | 108 | """Return tuple of arguments for given method, excluding self. |
|---|
| | 109 | |
|---|
| | 110 | >>> class Class: |
|---|
| | 111 | ... def method(s, arg1, arg2, other_arg): |
|---|
| | 112 | ... pass |
|---|
| | 113 | >>> get_method_arguments(Class.method) |
|---|
| | 114 | ('arg1', 'arg2', 'other_arg') |
|---|
| | 115 | """ |
|---|
| | 116 | return method.func_code.co_varnames[1:method.func_code.co_argcount] |
|---|
| | 117 | |
|---|
| | 118 | def get_attributes(obj, names): |
|---|
| | 119 | """Return attributes dictionary with keys from `names`. |
|---|
| | 120 | |
|---|
| | 121 | Object is queried for each attribute name, if it doesn't have this |
|---|
| | 122 | attribute, default value None will be returned. |
|---|
| | 123 | |
|---|
| | 124 | >>> class Class: |
|---|
| | 125 | ... pass |
|---|
| | 126 | >>> obj = Class() |
|---|
| | 127 | >>> obj.attr = True |
|---|
| | 128 | >>> obj.value = 13 |
|---|
| | 129 | >>> obj.string = "Hello" |
|---|
| | 130 | |
|---|
| | 131 | >>> d = get_attributes(obj, ['attr', 'string', 'other']) |
|---|
| | 132 | >>> d == {'attr': True, 'string': "Hello", 'other': None} |
|---|
| | 133 | True |
|---|
| | 134 | """ |
|---|
| | 135 | attrs = {} |
|---|
| | 136 | |
|---|
| | 137 | for name in names: |
|---|
| | 138 | attrs[name] = getattr(obj, name, None) |
|---|
| | 139 | |
|---|
| | 140 | return attrs |
|---|
| | 141 | |
|---|
| | 142 | def camel2underscore(name): |
|---|
| | 143 | """Convert name from CamelCase to underscore_name. |
|---|
| | 144 | |
|---|
| | 145 | >>> camel2underscore('CamelCase') |
|---|
| | 146 | 'camel_case' |
|---|
| | 147 | >>> camel2underscore('already_underscore_name') |
|---|
| | 148 | 'already_underscore_name' |
|---|
| | 149 | >>> camel2underscore('BigHTMLClass') |
|---|
| | 150 | 'big_html_class' |
|---|
| | 151 | >>> camel2underscore('') |
|---|
| | 152 | '' |
|---|
| | 153 | """ |
|---|
| | 154 | if name and name[0].upper: |
|---|
| | 155 | name = name[0].lower() + name[1:] |
|---|
| | 156 | |
|---|
| | 157 | def capitalize(match): |
|---|
| | 158 | string = match.group(1).lower().capitalize() |
|---|
| | 159 | return string[:-1] + string[-1].upper() |
|---|
| | 160 | |
|---|
| | 161 | def underscore(match): |
|---|
| | 162 | return '_' + match.group(1).lower() |
|---|
| | 163 | |
|---|
| | 164 | name = re.sub(r'([A-Z]+)', capitalize, name) |
|---|
| | 165 | return re.sub(r'([A-Z])', underscore, name) |
|---|
| | 166 | |
|---|
| | 167 | def index_class_to_name(clsname): |
|---|
| | 168 | """Covert index class name to index name. |
|---|
| | 169 | |
|---|
| | 170 | >>> index_class_to_name("IndexDownload") |
|---|
| | 171 | 'download' |
|---|
| | 172 | >>> index_class_to_name("IndexUnitTests") |
|---|
| | 173 | 'unit_tests' |
|---|
| | 174 | >>> index_class_to_name("IndexPyPIDownload") |
|---|
| | 175 | 'py_pi_download' |
|---|
| | 176 | """ |
|---|
| | 177 | return camel2underscore(clsname.replace('Index', '', 1)) |
|---|
| | 178 | |
|---|
| | 179 | def is_empty(path): |
|---|
| | 180 | """Returns True if file or directory pointed by `path` is empty. |
|---|
| | 181 | """ |
|---|
| | 182 | if os.path.isfile(path) and os.path.getsize(path) == 0: |
|---|
| | 183 | return True |
|---|
| | 184 | if os.path.isdir(path) and os.listdir(path) == []: |
|---|
| | 185 | return True |
|---|
| | 186 | |
|---|
| | 187 | return False |
|---|
| | 188 | |
|---|
| | 189 | def strip_dir_part(path, root): |
|---|
| | 190 | """Strip `root` part from `path`. |
|---|
| | 191 | |
|---|
| | 192 | >>> strip_dir_part('/home/ruby/file', '/home') |
|---|
| | 193 | 'ruby/file' |
|---|
| | 194 | >>> strip_dir_part('/home/ruby/file', '/home/') |
|---|
| | 195 | 'ruby/file' |
|---|
| | 196 | >>> strip_dir_part('/home/ruby/', '/home') |
|---|
| | 197 | 'ruby/' |
|---|
| | 198 | >>> strip_dir_part('/home/ruby/', '/home/') |
|---|
| | 199 | 'ruby/' |
|---|
| | 200 | """ |
|---|
| | 201 | path = path.replace(root, '', 1) |
|---|
| | 202 | |
|---|
| | 203 | if path.startswith(os.path.sep): |
|---|
| | 204 | path = path[1:] |
|---|
| | 205 | |
|---|
| | 206 | return path |
|---|
| | 207 | |
|---|
| | 208 | def get_files_dirs_list(root): |
|---|
| | 209 | """Return list of all files and directories below `root`. |
|---|
| | 210 | |
|---|
| | 211 | Root directory is excluded from files/directories paths. |
|---|
| | 212 | """ |
|---|
| | 213 | files = [] |
|---|
| | 214 | directories = [] |
|---|
| | 215 | |
|---|
| | 216 | for dirpath, dirnames, filenames in os.walk(root): |
|---|
| | 217 | dirpath = strip_dir_part(dirpath, root) |
|---|
| | 218 | files.extend(map(lambda x: os.path.join(dirpath, x), filenames)) |
|---|
| | 219 | directories.extend(map(lambda x: os.path.join(dirpath, x), dirnames)) |
|---|
| | 220 | |
|---|
| | 221 | return files, directories |
|---|
| | 222 | |
|---|
| | 223 | ################################################################################ |
|---|
| | 224 | ## Main index class. |
|---|
| | 225 | ################################################################################ |
|---|
| | 226 | |
|---|
| | 227 | class NameSetter(type): |
|---|
| | 228 | def __init__(cls, name, bases, dict): |
|---|
| | 229 | if 'name' not in dict: |
|---|
| | 230 | setattr(cls, 'name', index_class_to_name(name)) |
|---|
| | 231 | |
|---|
| | 232 | def make_indices_dict(indices): |
|---|
| | 233 | indices_dict = {} |
|---|
| | 234 | for index in indices: |
|---|
| | 235 | indices_dict[index.name] = index |
|---|
| | 236 | return indices_dict |
|---|
| 51 | | """ |
|---|
| 52 | | Encapsulates index attributes such as name, value, details |
|---|
| 53 | | """ |
|---|
| 54 | | |
|---|
| 55 | | def __init__(self, type, name="", value=0, details=""): |
|---|
| 56 | | self.type = "index_" + type |
|---|
| 57 | | self.name = self.type |
|---|
| 58 | | if name: self.name += "_" + name |
|---|
| 59 | | self.value = value |
|---|
| 60 | | self.details = details |
|---|
| 61 | | |
|---|
| | 239 | """Class describing one index. |
|---|
| | 240 | |
|---|
| | 241 | Use it as a container index or subclass to create custom indices. |
|---|
| | 242 | |
|---|
| | 243 | During class initialization, special attribute `name` is magically |
|---|
| | 244 | set based on class name. See `index_class_to_name` and `NameSetter` |
|---|
| | 245 | definitions for details. |
|---|
| | 246 | """ |
|---|
| | 247 | __metaclass__ = NameSetter |
|---|
| | 248 | |
|---|
| | 249 | subindices = None |
|---|
| | 250 | |
|---|
| | 251 | name = "unnamed" |
|---|
| | 252 | value = -1 |
|---|
| | 253 | details = "" |
|---|
| | 254 | |
|---|
| | 255 | def __init__(self, indices=[]): |
|---|
| | 256 | if not self.subindices: |
|---|
| | 257 | self.subindices = [] |
|---|
| | 258 | |
|---|
| | 259 | # Create dictionary for fast reference. |
|---|
| | 260 | self._indices_dict = make_indices_dict(self.subindices) |
|---|
| | 261 | |
|---|
| | 262 | for index in indices: |
|---|
| | 263 | self.add_subindex(index) |
|---|
| | 264 | |
|---|
| | 265 | self._compute_arguments = get_method_arguments(self.compute) |
|---|
| | 266 | |
|---|
| | 267 | def _iter_indices(self): |
|---|
| | 268 | """Iterate over each subindex and yield their values. |
|---|
| | 269 | """ |
|---|
| | 270 | for index in self.subindices: |
|---|
| | 271 | # Pass Cheesecake instance to other indices. |
|---|
| | 272 | yield index.compute_with(self.cheesecake) |
|---|
| | 273 | # Print index info after computing. |
|---|
| | 274 | index.print_info() |
|---|
| | 275 | |
|---|
| | 276 | def compute_with(self, cheesecake): |
|---|
| | 277 | """Take given Cheesecake instance and compute index value. |
|---|
| | 278 | """ |
|---|
| | 279 | self.cheesecake = cheesecake |
|---|
| | 280 | return self.compute(**get_attributes(cheesecake, self._compute_arguments)) |
|---|
| | 281 | |
|---|
| | 282 | def compute(self): |
|---|
| | 283 | """Compute index value and return it. |
|---|
| | 284 | |
|---|
| | 285 | By default this method computes sum of all subindices. Override this |
|---|
| | 286 | method when subclassing for different behaviour. |
|---|
| | 287 | |
|---|
| | 288 | Parameters to this function are dynamically prepared with use of |
|---|
| | 289 | `get_attributes` function. |
|---|
| | 290 | |
|---|
| | 291 | :Warning: Don't use *args and **kwds arguments for this method. |
|---|
| | 292 | """ |
|---|
| | 293 | self.value = sum(self._iter_indices()) |
|---|
| | 294 | return self.value |
|---|
| | 295 | |
|---|
| | 296 | def _get_max_value(self): |
|---|
| | 297 | if self.subindices: |
|---|
| | 298 | return sum(map(lambda index: index.max_value, |
|---|
| | 299 | self.subindices)) |
|---|
| | 300 | return 0 |
|---|
| | 301 | |
|---|
| | 302 | max_value = property(_get_max_value) |
|---|
| | 303 | |
|---|
| | 304 | def add_subindex(self, index): |
|---|
| | 305 | """Add subindex. |
|---|
| | 306 | |
|---|
| | 307 | :Parameters: |
|---|
| | 308 | `index` : Index instance |
|---|
| | 309 | Index instance for inclusion. |
|---|
| | 310 | """ |
|---|
| | 311 | if not isinstance(index, Index): |
|---|
| | 312 | raise ValueError("subindex have to be instance of Index") |
|---|
| | 313 | |
|---|
| | 314 | self.subindices.append(index) |
|---|
| | 315 | self._indices_dict[index.name] = index |
|---|
| | 316 | |
|---|
| 92 | | self.indexes[name] = Index(self.type, name, value, details) |
|---|
| 93 | | |
|---|
| 94 | | def print_info(self): |
|---|
| 95 | | """ |
|---|
| 96 | | Print index info for all indexes sorted alphanumerically by name |
|---|
| 97 | | """ |
|---|
| 98 | | names = self.indexes.keys() |
|---|
| 99 | | names.sort() |
|---|
| 100 | | for name in names: |
|---|
| 101 | | index = self.indexes[name] |
|---|
| 102 | | index.print_info() |
|---|
| 103 | | |
|---|
| 104 | | def get_value(self): |
|---|
| 105 | | """ |
|---|
| 106 | | Return sum of individual index values |
|---|
| 107 | | """ |
|---|
| 108 | | value = 0 |
|---|
| 109 | | for key in self.indexes.keys(): |
|---|
| 110 | | index = self.indexes[key] |
|---|
| 111 | | value += index.value |
|---|
| 112 | | return value |
|---|
| 113 | | |
|---|
| 114 | | value = property(get_value) |
|---|
| | 358 | self.value = 0 |
|---|
| | 359 | |
|---|
| | 360 | return self.value |
|---|
| | 361 | |
|---|
| | 362 | class IndexUnpack(Index): |
|---|
| | 363 | max_value = 25 |
|---|
| | 364 | |
|---|
| | 365 | def compute(self, unpacked): |
|---|
| | 366 | if unpacked: |
|---|
| | 367 | self.details = "package unpacked successfully" |
|---|
| | 368 | self.value = self.max_value |
|---|
| | 369 | else: |
|---|
| | 370 | self.value = 0 |
|---|
| | 371 | |
|---|
| | 372 | return self.value |
|---|
| | 373 | |
|---|
| | 374 | class IndexUnpackDir(Index): |
|---|
| | 375 | max_value = 15 |
|---|
| | 376 | |
|---|
| | 377 | def compute(self, unpack_dir, original_package_name): |
|---|
| | 378 | self.details = "unpack directory is " + unpack_dir |
|---|
| | 379 | |
|---|
| | 380 | if original_package_name: |
|---|
| | 381 | self.details += " instead of the expected " + original_package_name |
|---|
| | 382 | self.value = 0 |
|---|
| | 383 | else: |
|---|
| | 384 | self.details += " as expected" |
|---|
| | 385 | self.value = self.max_value |
|---|
| | 386 | |
|---|
| | 387 | return self.value |
|---|
| | 388 | |
|---|
| | 389 | class IndexInstall(Index): |
|---|
| | 390 | max_value = 50 |
|---|
| | 391 | |
|---|
| | 392 | def compute(self, installed, sandbox_install_dir): |
|---|
| | 393 | if installed: |
|---|
| | 394 | self.details = "package installed in %s" % sandbox_install_dir |
|---|
| | 395 | self.value = self.max_value |
|---|
| | 396 | else: |
|---|
| | 397 | self.details = "could not install package in %s" % sandbox_install_dir |
|---|
| | 398 | self.value = 0 |
|---|
| | 399 | |
|---|
| | 400 | return self.value |
|---|
| | 401 | |
|---|
| | 402 | class IndexPyPIDownload(Index): |
|---|
| | 403 | max_value = 50 |
|---|
| | 404 | distance_penalty = -5 |
|---|
| | 405 | |
|---|
| | 406 | def compute(self, package, found_on_cheeseshop, distance_from_pypi, download_url): |
|---|
| | 407 | if download_url: |
|---|
| | 408 | self.value = self.max_value |
|---|
| | 409 | |
|---|
| | 410 | self.details = "downloaded package " + package |
|---|
| | 411 | |
|---|
| | 412 | if not found_on_cheeseshop: |
|---|
| | 413 | self.value += distance_from_pypi * self.distance_penalty |
|---|
| | 414 | |
|---|
| | 415 | if distance_from_pypi: |
|---|
| | 416 | self.details += " following %d link" % distance_from_pypi |
|---|
| | 417 | if distance_from_pypi > 1: |
|---|
| | 418 | self.details += "s" |
|---|
| | 419 | self.details += " from PyPI" |
|---|
| | 420 | else: |
|---|
| | 421 | self.details += " from " + download_url |
|---|
| | 422 | else: |
|---|
| | 423 | self.details += " directly from the Cheese Shop" |
|---|
| | 424 | else: |
|---|
| | 425 | self.value = 0 |
|---|
| | 426 | |
|---|
| | 427 | return self.value |
|---|
| | 428 | |
|---|
| | 429 | class IndexGeneratedFiles(Index): |
|---|
| | 430 | generated_files_penalty = -20 |
|---|
| | 431 | |
|---|
| | 432 | def compute(self, files_list): |
|---|
| | 433 | self.value = 0 |
|---|
| | 434 | |
|---|
| | 435 | pyc_files = len(get_files_of_type(files_list, 'pyc')) |
|---|
| | 436 | pyo_files = len(get_files_of_type(files_list, 'pyo')) |
|---|
| | 437 | |
|---|
| | 438 | if pyc_files > 0 or pyo_files > 0: |
|---|
| | 439 | self.value += self.generated_files_penalty |
|---|
| | 440 | |
|---|
| | 441 | self.details = "%d .pyc and %d .pyo files found" % \ |
|---|
| | 442 | (pyc_files, pyo_files) |
|---|
| | 443 | |
|---|
| | 444 | return self.value |
|---|
| | 445 | |
|---|
| | 446 | class IndexInstallability(MegaIndex): |
|---|
| | 447 | name = "INSTALLABILITY" |
|---|
| | 448 | |
|---|
| | 449 | subindices = [ |
|---|
| | 450 | IndexUnpack(), |
|---|
| | 451 | IndexUnpackDir(), |
|---|
| | 452 | IndexInstall(), |
|---|
| | 453 | IndexGeneratedFiles(), |
|---|
| | 454 | ] |
|---|
| | 455 | |
|---|
| | 456 | ################################################################################ |
|---|
| | 457 | ## Documentation index. |
|---|
| | 458 | ################################################################################ |
|---|
| | 459 | |
|---|
| | 460 | def match_filename(name, rule): |
|---|
| | 461 | """Check if `name` matches given `rule`. |
|---|
| | 462 | """ |
|---|
| | 463 | def equal(x, y): |
|---|
| | 464 | x_root, x_ext = os.path.splitext(x) |
|---|
| | 465 | y_root, y_ext = os.path.splitext(y.lower()) |
|---|
| | 466 | if x_root in [y_root.lower(), y_root.upper(), y_root.capitalize()] \ |
|---|
| | 467 | and x_ext in [y_ext.lower(), y_ext.upper()]: |
|---|
| | 468 | return True |
|---|
| | 469 | return False |
|---|
| | 470 | |
|---|
| | 471 | if isinstance(rule, basestring): |
|---|
| | 472 | if equal(name, rule): |
|---|
| | 473 | return True |
|---|
| | 474 | elif isinstance(rule, OneOf) and not rule.used: |
|---|
| | 475 | for poss in rule.possibilities: |
|---|
| | 476 | if match_filename(name, poss): |
|---|
| | 477 | rule.used = True |
|---|
| | 478 | return True |
|---|
| | 479 | |
|---|
| | 480 | return False |
|---|
| | 481 | |
|---|
| | 482 | class OneOf(object): |
|---|
| | 483 | def __init__(self, *possibilities): |
|---|
| | 484 | self.possibilities = possibilities |
|---|
| | 485 | self.used = False |
|---|
| | 486 | def __str__(self): |
|---|
| | 487 | return 'one of %s' % (self.possibilities,) |
|---|
| | 488 | |
|---|
| | 489 | def WithOptionalExt(name, extensions): |
|---|
| | 490 | """Handy way of writing Cheese rules for files with extensions. |
|---|
| | 491 | |
|---|
| | 492 | Instead of writing: |
|---|
| | 493 | >>> one_of = OneOf('readme', 'readme.html', 'readme.txt') |
|---|
| | 494 | |
|---|
| | 495 | Write this: |
|---|
| | 496 | >>> opt_ext = WithOptionalExt('readme', ['html', 'txt']) |
|---|
| | 497 | |
|---|
| | 498 | It means the same! (representation have a meaning) |
|---|
| | 499 | >>> str(one_of) == str(opt_ext) |
|---|
| | 500 | True |
|---|
| | 501 | """ |
|---|
| | 502 | possibilities = [name] |
|---|
| | 503 | possibilities.extend(map(lambda x: name + '.' + x, extensions)) |
|---|
| | 504 | |
|---|
| | 505 | return OneOf(*possibilities) |
|---|
| | 506 | |
|---|
| | 507 | def Doc(name): |
|---|
| | 508 | return WithOptionalExt(name, ['html', 'txt']) |
|---|
| | 509 | |
|---|
| | 510 | class IndexRequiredFiles(Index): |
|---|
| | 511 | cheese_files = { |
|---|
| | 512 | 'setup.py': 15, |
|---|
| | 513 | Doc('readme'): 15, |
|---|
| | 514 | OneOf(Doc('license'), Doc('copying')): 15, |
|---|
| | 515 | |
|---|
| | 516 | Doc('authors'): 10, |
|---|
| | 517 | Doc('announce'): 10, |
|---|
| | 518 | Doc('changelog'): 10, |
|---|
| | 519 | Doc('faq'): 10, |
|---|
| | 520 | Doc('install'): 10, |
|---|
| | 521 | Doc('news'): 10, |
|---|
| | 522 | Doc('thanks'): 10, |
|---|
| | 523 | Doc('todo'): 10, |
|---|
| | 524 | } |
|---|
| | 525 | |
|---|
| | 526 | cheese_dirs = { |
|---|
| | 527 | 'demo': 20, |
|---|
| | 528 | 'doc': 25, |
|---|
| | 529 | 'example': 20, |
|---|
| | 530 | OneOf('test', 'tests'): 25, |
|---|
| | 531 | } |
|---|
| | 532 | |
|---|
| | 533 | max_value = sum(cheese_files.values() + cheese_dirs.values()) |
|---|
| | 534 | |
|---|
| | 535 | def compute(self, files_list, dirs_list, package_dir): |
|---|
| | 536 | self.value = 0 |
|---|
| | 537 | self.reset_rules(self.cheese_files.keys() + self.cheese_dirs.keys()) |
|---|
| | 538 | |
|---|
| | 539 | files_count = 0 |
|---|
| | 540 | for filename in files_list: |
|---|
| | 541 | if not is_empty(os.path.join(package_dir, filename)): |
|---|
| | 542 | score = self.get_score(os.path.basename(filename), self.cheese_files) |
|---|
| | 543 | if score != 0: |
|---|
| | 544 | self.value += score |
|---|
| | 545 | files_count += 1 |
|---|
| | 546 | |
|---|
| | 547 | directories_count = 0 |
|---|
| | 548 | for directory in dirs_list: |
|---|
| | 549 | if not is_empty(os.path.join(package_dir, directory)): |
|---|
| | 550 | score = self.get_score(os.path.basename(directory), self.cheese_dirs) |
|---|
| | 551 | if score != 0: |
|---|
| | 552 | self.value += score |
|---|
| | 553 | directories_count += 1 |
|---|
| | 554 | |
|---|
| | 555 | self.details = "%d files and %d required directories found" % \ |
|---|
| | 556 | (files_count, directories_count) |
|---|
| | 557 | |
|---|
| | 558 | return self.value |
|---|
| | 559 | |
|---|
| | 560 | def get_score(self, name, specs): |
|---|
| | 561 | for entry, value in specs.iteritems(): |
|---|
| | 562 | if match_filename(name, entry): |
|---|
| | 563 | self.cheesecake.log.debug("%d points entry found: %s (%s)" % \ |
|---|
| | 564 | (value, name, entry)) |
|---|
| | 565 | return value |
|---|
| | 566 | |
|---|
| | 567 | return 0 |
|---|
| | 568 | |
|---|
| | 569 | def reset_rules(self, rules): |
|---|
| | 570 | if isiterable(rules): |
|---|
| | 571 | for rule in rules: |
|---|
| | 572 | self.reset_rules(rule) |
|---|
| | 573 | elif isinstance(rules, OneOf): |
|---|
| | 574 | rules.used = False |
|---|
| | 575 | self.reset_rules(rules.possibilities) |
|---|
| | 576 | |
|---|
| | 577 | class IndexDocstrings(Index): |
|---|
| | 578 | max_value = 100 |
|---|
| | 579 | |
|---|
| | 580 | def compute(self, object_cnt, docstring_cnt): |
|---|
| | 581 | percent = 0 |
|---|
| | 582 | if object_cnt > 0: |
|---|
| | 583 | percent = float(docstring_cnt)/float(object_cnt) |
|---|
| | 584 | |
|---|
| | 585 | # Scale the result. |
|---|
| | 586 | self.value = int(ceil(percent * self.max_value)) |
|---|
| | 587 | |
|---|
| | 588 | self.details = "found %d/%d=%.2f%% objects with docstrings" %\ |
|---|
| | 589 | (docstring_cnt, object_cnt, percent*100) |
|---|
| | 590 | |
|---|
| | 591 | return self.value |
|---|
| | 592 | |
|---|
| | 593 | class IndexFormattedDocstrings(Index): |
|---|
| | 594 | max_value = 50 |
|---|
| | 595 | |
|---|
| | 596 | def compute(self, object_cnt, docformat_cnt): |
|---|
| | 597 | percent = 0 |
|---|
| | 598 | if object_cnt > 0: |
|---|
| | 599 | percent = float(docformat_cnt)/float(object_cnt) |
|---|
| | 600 | |
|---|
| | 601 | # Scale the result. |
|---|
| | 602 | self.value = int(ceil(percent * self.max_value)) |
|---|
| | 603 | |
|---|
| | 604 | self.details = "found %d/%d=%.2f%% objects with formatted docstrings" %\ |
|---|
| | 605 | (docformat_cnt, object_cnt, percent*100) |
|---|
| | 606 | |
|---|
| | 607 | return self.value |
|---|
| | 608 | |
|---|
| | 609 | class IndexDocumentation(MegaIndex): |
|---|
| | 610 | name = "DOCUMENTATION" |
|---|
| | 611 | |
|---|
| | 612 | subindices = [ |
|---|
| | 613 | IndexRequiredFiles(), |
|---|
| | 614 | IndexDocstrings(), |
|---|
| | 615 | IndexFormattedDocstrings(), |
|---|
| | 616 | ] |
|---|
| | 617 | |
|---|
| | 618 | ################################################################################ |
|---|
| | 619 | ## Code "kwalitee" index. |
|---|
| | 620 | ################################################################################ |
|---|
| | 621 | |
|---|
| | 622 | class IndexUnitTests(Index): |
|---|
| | 623 | """Compute unittest index as percentage of methods/functions |
|---|
| | 624 | that are exercised in unit tests. |
|---|
| | 625 | """ |
|---|
| | 626 | max_value = 50 |
|---|
| | 627 | |
|---|
| | 628 | def compute(self, files_list, functions, package_dir): |
|---|
| | 629 | unittest_cnt = 0 |
|---|
| | 630 | self.functions_tested = {} |
|---|
| | 631 | |
|---|
| | 632 | for testfile in get_files_of_type(files_list, 'test'): |
|---|
| | 633 | fullpath = os.path.join(package_dir, testfile) |
|---|
| | 634 | code = CodeParser(fullpath, self.cheesecake.log.debug) |
|---|
| | 635 | |
|---|
| | 636 | func_called = code.functions_called() |
|---|
| | 637 | |
|---|
| | 638 | for func in func_called: |
|---|
| | 639 | self.functions_tested[func] = 1 |
|---|
| | 640 | |
|---|
| | 641 | for funcname in functions: |
|---|
| | 642 | if self.is_unit_tested(funcname): |
|---|
| | 643 | unittest_cnt += 1 |
|---|
| | 644 | self.log.cheesecake.debug("%s is unit tested" % funcname) |
|---|
| | 645 | |
|---|
| | 646 | percent = 0 |
|---|
| | 647 | if len(functions) > 0: |
|---|
| | 648 | percent = float(unittest_cnt)/float(len(functions)) |
|---|
| | 649 | |
|---|
| | 650 | # Scale the result. |
|---|
| | 651 | self.value = int(ceil(percent * self.max_value)) |
|---|
| | 652 | |
|---|
| | 653 | self.details = "found %d/%d=%.2f%% unit tested methods/functions." %\ |
|---|
| | 654 | (unittest_cnt, len(functions), percent*100) |
|---|
| | 655 | |
|---|
| | 656 | return self.value |
|---|
| | 657 | |
|---|
| | 658 | def is_unit_tested(self, funcname): |
|---|
| | 659 | elem = funcname.split(".") |
|---|
| | 660 | n1 = elem[-1] |
|---|
| | 661 | n2 = "" |
|---|
| | 662 | if len(elem) > 1: |
|---|
| | 663 | n2 = elem[-2] + "." + elem[-1] |
|---|
| | 664 | for key in self.functions_tested.keys(): |
|---|
| | 665 | if key.startswith(n1) or (n2 and key.startswith(n2)): |
|---|
| | 666 | return True |
|---|
| | 667 | return False |
|---|
| | 668 | |
|---|
| | 669 | class IndexPyLint(Index): |
|---|
| | 670 | """Compute pylint index as average of positive pylint scores obtained for |
|---|
| | 671 | the Python files identified in the package. |
|---|
| | 672 | """ |
|---|
| | 673 | name = "pylint" |
|---|
| | 674 | max_value = 50 |
|---|
| | 675 | |
|---|
| | 676 | def compute(self, files_list, package_dir): |
|---|
| | 677 | self.value = 0 |
|---|
| | 678 | |
|---|
| | 679 | # Try to run the pylint script |
|---|
| | 680 | if not command_successful("pylint --version"): |
|---|
| | 681 | self.details = "pylint not properly installed" |
|---|
| | 682 | return self.value |
|---|
| | 683 | |
|---|
| | 684 | pylint_value = 0 |
|---|
| | 685 | cnt = 0 |
|---|
| | 686 | for pyfile in get_files_of_type(files_list, 'module'): |
|---|
| | 687 | fullpath = os.path.join(package_dir, pyfile) |
|---|
| | 688 | path, filename = os.path.split(fullpath) |
|---|
| | 689 | module, ext = os.path.splitext(filename) |
|---|
| | 690 | |
|---|
| | 691 | self.cheesecake.log.debug("Running pylint on file " + fullpath) |
|---|
| | 692 | rc, output = run_cmd("pylint " + fullpath) |
|---|
| | 693 | if rc: |
|---|
| | 694 | self.cheesecake.log.debug("encountered an error (%d)." % rc) |
|---|
| | 695 | continue |
|---|
| | 696 | |
|---|
| | 697 | score_line = output.split("\n")[-3] |
|---|
| | 698 | s = re.search(r" (\d+\.\d+)/10", score_line) |
|---|
| | 699 | # We only take positive scores into account |
|---|
| | 700 | if s: |
|---|
| | 701 | score = s.group(1) |
|---|
| | 702 | if score == "0.00": |
|---|
| | 703 | self.cheesecake.log.debug("ignoring 0.00 score.") |
|---|
| | 704 | continue |
|---|
| | 705 | else: |
|---|
| | 706 | self.cheesecake.log.debug("pylint score for module %s: %s" % (module, score)) |
|---|
| | 707 | pylint_value += float(score) |
|---|
| | 708 | cnt += 1 |
|---|
| | 709 | |
|---|
| | 710 | avg_score = 0 |
|---|
| | 711 | if cnt: |
|---|
| | 712 | avg_score = float(pylint_value)/float(cnt) |
|---|
| | 713 | |
|---|
| | 714 | self.value = int(ceil(avg_score/10.0 * self.max_value)) |
|---|
| | 715 | self.details = "average pylint score is %.2f out of 10" % avg_score |
|---|
| | 716 | |
|---|
| | 717 | return self.value |
|---|
| | 718 | |
|---|
| | 719 | class IndexCodeKwalitee(MegaIndex): |
|---|
| | 720 | name = "CODE KWALITEE" |
|---|
| | 721 | |
|---|
| | 722 | subindices = [ |
|---|
| | 723 | IndexPyLint(), |
|---|
| | 724 | # IndexUnitTests(), TODO |
|---|
| | 725 | ] |
|---|
| | 726 | |
|---|
| | 727 | ################################################################################ |
|---|
| | 728 | ## Main Cheesecake class. |
|---|
| | 729 | ################################################################################ |
|---|
| 317 | | def init_indexes(self): |
|---|
| 318 | | """ |
|---|
| 319 | | Initialize variables used in index computation |
|---|
| 320 | | |
|---|
| 321 | | * cheesecake_index: overall index for the package |
|---|
| 322 | | * index: dict holding Index or CompositeIndex objects of various types |
|---|
| 323 | | """ |
|---|
| 324 | | self.cheesecake_index = 0 |
|---|
| 325 | | self.cheesecake_index_installability = 0 |
|---|
| 326 | | self.cheesecake_index_documentation = 0 |
|---|
| 327 | | self.cheesecake_index_codekwalitee = 0 |
|---|
| 328 | | self.max_cheesecake_index = self.INDEX_PYPI_DOWNLOAD + \ |
|---|
| 329 | | self.INDEX_UNPACK + \ |
|---|
| 330 | | self.INDEX_UNPACK_DIR + \ |
|---|
| 331 | | self.INDEX_INSTALL + \ |
|---|
| 332 | | self.MAX_INDEX_DOCSTRINGS + \ |
|---|
| 333 | | self.MAX_INDEX_PYLINT |
|---|
| 334 | | # self.MAX_INDEX_UNITTESTS |
|---|
| 335 | | self.max_cheesecake_index_installability = self.INDEX_PYPI_DOWNLOAD + \ |
|---|
| 336 | | self.INDEX_UNPACK + \ |
|---|
| 337 | | self.INDEX_UNPACK_DIR + \ |
|---|
| 338 | | self.INDEX_INSTALL |
|---|
| 339 | | self.max_cheesecake_index_documentation = self.INDEX_REQUIRED_FILES + \ |
|---|
| 340 | | self.MAX_INDEX_DOCSTRINGS + \ |
|---|
| 341 | | self.MAX_INDEX_DOCFORMAT |
|---|
| 342 | | self.max_cheesecake_index_codekwalitee = self.MAX_INDEX_PYLINT |
|---|
| 343 | | # self.MAX_INDEX_UNITTESTS |
|---|
| 344 | | |
|---|
| 345 | | self.index = {} |
|---|
| 346 | | for index_type in ["file", "dir"]: |
|---|
| 347 | | self.index[index_type] = CompositeIndex(index_type) |
|---|
| 348 | | for index_type in ["pypi_download", "url_download", |
|---|
| 349 | | "unpack_dir", "unpack", "install", |
|---|
| 350 | | "docstrings", "doc_format", "unittests", "pylint"]: |
|---|
| 351 | | self.index[index_type] = Index(index_type) |
|---|
| 352 | | |
|---|
| 353 | | for cheese_file in self.cheese_files: |
|---|
| 354 | | self.index["file"].set_index(name=cheese_file, details="file not found") |
|---|
| 355 | | if cheese_file in self.critical_cheese_files: |
|---|
| 356 | | self.max_cheesecake_index += self.INDEX_FILE_CRITICAL |
|---|
| 357 | | self.max_cheesecake_index_documentation += self.INDEX_FILE_CRITICAL |
|---|
| 358 | | else: |
|---|
| 359 | | self.max_cheesecake_index += self.INDEX_FILE |
|---|
| 360 | | self.max_cheesecake_index_documentation += self.INDEX_FILE |
|---|
| 361 | | self.log.debug("cheese_files: " + ",".join(self.cheese_files)) |
|---|
| 362 | | self.log.debug("critical_cheese_files: " + ",".join(self.critical_cheese_files)) |
|---|
| 363 | | |
|---|
| 364 | | for cheese_dir in self.cheese_dirs: |
|---|
| 365 | | self.index["dir"].set_index(name=cheese_dir, details="directory not found") |
|---|
| 366 | | if cheese_dir in self.critical_cheese_dirs: |
|---|
| 367 | | self.max_cheesecake_index += self.INDEX_DIR_CRITICAL |
|---|
| 368 | | self.max_cheesecake_index_documentation += self.INDEX_DIR_CRITICAL |
|---|
| 369 | | else: |
|---|
| 370 | | self.max_cheesecake_index += self.INDEX_DIR |
|---|
| 371 | | self.max_cheesecake_index_documentation += self.INDEX_DIR |
|---|
| 372 | | self.log.debug("cheese_dirs: " + ",".join(self.cheese_dirs)) |
|---|
| 373 | | self.log.debug("critical_cheese_dirs: " + ",".join(self.critical_cheese_dirs)) |
|---|
| 374 | | |
|---|
| 375 | | self.pkg_files = {} |
|---|
| 376 | | self.pkg_dirs = {} |
|---|
| 377 | | self.file_types = ["py", "pyc", "pyo", "test"] |
|---|
| 378 | | for type in self.file_types: |
|---|
| 379 | | self.pkg_files[type] = [] |
|---|
| 380 | | |
|---|
| 381 | | self.object_cnt = 0 # Number of modules/functions/classes/methods in .py files found |
|---|
| 382 | | self.docstring_cnt = 0 |
|---|
| 383 | | self.docformat_cnt = 0 |
|---|
| 384 | | self.functions = [] # List of methods/functions found in .py files |
|---|
| 385 | | |
|---|