Whole file
PyCQA/pyflakes
The author described this change as “Fix TypeError when processing relative imports (#61)”. It counts as a record because the checks below fail on the code as it stood at 885a8e539 and pass on d8591997d, with nothing else changed between the two runs.
ProjectPyCQA/pyflakes
Fix saved2016-05-12
Sharing licenceMIT · LICENSE
Change size+964 −953
What the code was meant to do, written into the code itself as a save note
Fix TypeError when processing relative imports (#61)
The change
| 209 | 209 | def __init__(self, name, source, module, real_name=None): | |
| 210 | 210 | self.module = module | |
| 211 | 211 | self.real_name = real_name or name | |
| 212 | - | full_name = module + '.' + self.real_name | |
| 213 | - | super(ImportationFrom, self).__init__(name, source, full_name) | |
| 214 | - | ||
| 215 | - | def __str__(self): | |
| 216 | - | """Return import full name with alias.""" | |
| 217 | - | if self.real_name != self.name: | |
| 218 | - | return self.fullName + ' as ' + self.name | |
| 219 | - | else: | |
| 220 | - | return self.fullName | |
| 221 | - | ||
| 222 | - | @property | |
| 223 | - | def source_statement(self): | |
| 224 | - | if self.real_name != self.name: | |
| 225 | - | return 'from %s import %s as %s' % (self.module, | |
| 226 | - | self.real_name, | |
| 227 | - | self.name) | |
| 228 | - | else: | |
| 229 | - | return 'from %s import %s' % (self.module, self.name) | |
| 230 | - | ||
| 231 | - | ||
| 232 | - | class StarImportation(Importation): | |
| 233 | - | """A binding created by an 'from x import *' statement.""" | |
| 234 | - | ||
| 235 | - | def __init__(self, name, source): | |
| 236 | - | super(StarImportation, self).__init__('*', source) | |
| 237 | - | # Each star importation needs a unique name, and | |
| 238 | - | # may not be the module name otherwise it will be deemed imported | |
| 239 | - | self.name = name + '.*' | |
| 240 | - | self.fullName = name | |
| 241 | - | ||
| 242 | - | @property | |
| 243 | - | def source_statement(self): | |
| 244 | - | return 'from ' + self.fullName + ' import *' | |
| 245 | - | ||
| 246 | - | def __str__(self): | |
| 247 | - | return self.name | |
| 248 | - | ||
| 249 | - | ||
| 250 | - | class FutureImportation(ImportationFrom): | |
| 251 | - | """ | |
| 252 | - | A binding created by a from `__future__` import statement. | |
| 253 | - | ||
| 254 | - | `__future__` imports are implicitly used. | |
| 255 | - | """ | |
| 256 | - | ||
| 257 | - | def __init__(self, name, source, scope): | |
| 258 | - | super(FutureImportation, self).__init__(name, source, '__future__') | |
| 259 | - | self.used = (scope, source) | |
| 260 | - | ||
| 261 | - | ||
| 262 | - | class Argument(Binding): | |
| 263 | - | """ | |
| 264 | - | Represents binding a name as an argument. | |
| 265 | - | """ | |
| 266 | - | ||
| 267 | - | ||
| 268 | - | class Assignment(Binding): | |
| 269 | - | """ | |
| 270 | - | Represents binding a name with an explicit assignment. | |
| 271 | - | ||
| 272 | - | The checker will raise warnings for any Assignment that isn't used. Also, | |
| 273 | - | the checker does not consider assignments in tuple/list unpacking to be | |
| 274 | - | Assignments, rather it treats them as simple Bindings. | |
| 275 | - | """ | |
| 276 | - | ||
| 277 | - | ||
| 278 | - | class FunctionDefinition(Definition): | |
| 279 | - | pass | |
| 280 | - | ||
| 281 | - | ||
| 282 | - | class ClassDefinition(Definition): | |
| 283 | - | pass | |
| 284 | - | ||
| 285 | - | ||
| 286 | - | class ExportBinding(Binding): | |
| 287 | - | """ | |
| 288 | - | A binding created by an C{__all__} assignment. If the names in the list | |
| 289 | - | can be determined statically, they will be treated as names for export and | |
| 290 | - | additional checking applied to them. | |
| 291 | - | ||
| 292 | - | The only C{__all__} assignment that can be recognized is one which takes | |
| 293 | - | the value of a literal list containing literal strings. For example:: | |
| 294 | - | ||
| 295 | - | __all__ = ["foo", "bar"] | |
| 296 | - | ||
| 297 | - | Names which are imported and not otherwise used but appear in the value of | |
| 298 | - | C{__all__} will not have an unused import warning reported for them. | |
| 299 | - | """ | |
| 300 | - | ||
| 301 | - | def __init__(self, name, source, scope): | |
| 302 | - | if '__all__' in scope and isinstance(source, ast.AugAssign): | |
| 303 | - | self.names = list(scope['__all__'].names) | |
| 304 | - | else: | |
| 305 | - | self.names = [] | |
| 306 | - | if isinstance(source.value, (ast.List, ast.Tuple)): | |
| 307 | - | for node in source.value.elts: | |
| 308 | - | if isinstance(node, ast.Str): | |
| 309 | - | self.names.append(node.s) | |
| 310 | - | super(ExportBinding, self).__init__(name, source) | |
| 311 | - | ||
| 312 | - | ||
| 313 | - | class Scope(dict): | |
| 314 | - | importStarred = False # set to True when import * is found | |
| 315 | - | ||
| 316 | - | def __repr__(self): | |
| 317 | - | scope_cls = self.__class__.__name__ | |
| 318 | - | return '<%s at 0x%x %s>' % (scope_cls, id(self), dict.__repr__(self)) | |
| 319 | - | ||
| 320 | - | ||
| 321 | - | class ClassScope(Scope): | |
| 322 | - | pass | |
| 323 | - | ||
| 324 | - | ||
| 325 | - | class FunctionScope(Scope): | |
| 326 | - | """ | |
| 327 | - | I represent a name scope for a function. | |
| 328 | - | ||
| 329 | - | @ivar globals: Names declared 'global' in this function. | |
| 330 | - | """ | |
| 331 | - | usesLocals = False | |
| 332 | - | alwaysUsed = set(['__tracebackhide__', | |
| 333 | - | '__traceback_info__', '__traceback_supplement__']) | |
| 334 | - | ||
| 335 | - | def __init__(self): | |
| 336 | - | super(FunctionScope, self).__init__() | |
| 337 | - | # Simplify: manage the special locals as globals | |
| 338 | - | self.globals = self.alwaysUsed.copy() | |
| 339 | - | self.returnValue = None # First non-empty return | |
| 340 | - | self.isGenerator = False # Detect a generator | |
| 341 | - | ||
| 342 | - | def unusedAssignments(self): | |
| 343 | - | """ | |
| 344 | - | Return a generator for the assignments which have not been used. | |
| 345 | - | """ | |
| 346 | - | for name, binding in self.items(): | |
| 347 | - | if (not binding.used and name not in self.globals | |
| 348 | - | and not self.usesLocals | |
| 349 | - | and isinstance(binding, Assignment)): | |
| 350 | - | yield name, binding | |
| 351 | - | ||
| 352 | - | ||
| 353 | - | class GeneratorScope(Scope): | |
| 354 | - | pass | |
| 355 | - | ||
| 356 | - | ||
| 357 | - | class ModuleScope(Scope): | |
| 358 | - | """Scope for a module.""" | |
| 359 | - | _futures_allowed = True | |
| 360 | - | ||
| 361 | - | ||
| 362 | - | class DoctestScope(ModuleScope): | |
| 363 | - | """Scope for a doctest.""" | |
| 364 | - | ||
| 365 | - | ||
| 366 | - | # Globally defined names which are not attributes of the builtins module, or | |
| 367 | - | # are only present on some platforms. | |
| 368 | - | _MAGIC_GLOBALS = ['__file__', '__builtins__', 'WindowsError'] | |
| 369 | - | ||
| 370 | - | ||
| 371 | - | def getNodeName(node): | |
| 372 | - | # Returns node.id, or node.name, or None | |
| 373 | - | if hasattr(node, 'id'): # One of the many nodes with an id | |
| 374 | - | return node.id | |
| 375 | - | if hasattr(node, 'name'): # an ExceptHandler node | |
| 376 | - | return node.name | |
| 377 | - | ||
| 378 | - | ||
| 379 | - | class Checker(object): | |
| 380 | - | """ | |
| 381 | - | I check the cleanliness and sanity of Python code. | |
| 382 | - | ||
| 383 | - | @ivar _deferredFunctions: Tracking list used by L{deferFunction}. Elements | |
| 384 | - | of the list are two-tuples. The first element is the callable passed | |
| 385 | - | to L{deferFunction}. The second element is a copy of the scope stack | |
| 386 | - | at the time L{deferFunction} was called. | |
| 387 | - | ||
| 388 | - | @ivar _deferredAssignments: Similar to C{_deferredFunctions}, but for | |
| 389 | - | callables which are deferred assignment checks. | |
| 390 | - | """ | |
| 391 | - | ||
| 392 | - | nodeDepth = 0 | |
| 393 | - | offset = None | |
| 394 | - | traceTree = False | |
| 395 | - | ||
| 396 | - | builtIns = set(builtin_vars).union(_MAGIC_GLOBALS) | |
| 397 | - | _customBuiltIns = os.environ.get('PYFLAKES_BUILTINS') | |
| 398 | - | if _customBuiltIns: | |
| 399 | - | builtIns.update(_customBuiltIns.split(',')) | |
| 400 | - | del _customBuiltIns | |
| 401 | - | ||
| 402 | - | def __init__(self, tree, filename='(none)', builtins=None, | |
| 403 | - | withDoctest='PYFLAKES_DOCTEST' in os.environ): | |
| 404 | - | self._nodeHandlers = {} | |
| 405 | - | self._deferredFunctions = [] | |
| 406 | - | self._deferredAssignments = [] | |
| 407 | - | self.deadScopes = [] | |
| 408 | - | self.messages = [] | |
| 409 | - | self.filename = filename | |
| 410 | - | if builtins: | |
| 411 | - | self.builtIns = self.builtIns.union(builtins) | |
| 412 | - | self.withDoctest = withDoctest | |
| 413 | - | self.scopeStack = [ModuleScope()] | |
| 414 | - | self.exceptHandlers = [()] | |
| 415 | - | self.root = tree | |
| 416 | - | self.handleChildren(tree) | |
| 417 | - | self.runDeferred(self._deferredFunctions) | |
| 418 | - | # Set _deferredFunctions to None so that deferFunction will fail | |
| 419 | - | # noisily if called after we've run through the deferred functions. | |
| 420 | - | self._deferredFunctions = None | |
| 421 | - | self.runDeferred(self._deferredAssignments) | |
| 422 | - | # Set _deferredAssignments to None so that deferAssignment will fail | |
| 423 | - | # noisily if called after we've run through the deferred assignments. | |
| 424 | - | self._deferredAssignments = None | |
| 425 | - | del self.scopeStack[1:] | |
| 426 | - | self.popScope() | |
| 427 | - | self.checkDeadScopes() | |
| 428 | - | ||
| 429 | - | def deferFunction(self, callable): | |
| 430 | - | """ | |
| 431 | - | Schedule a function handler to be called just before completion. | |
| 432 | - | ||
| 433 | - | This is used for handling function bodies, which must be deferred | |
| 434 | - | because code later in the file might modify the global scope. When | |
| 435 | - | `callable` is called, the scope at the time this is called will be | |
| 436 | - | restored, however it will contain any new bindings added to it. | |
| 437 | - | """ | |
| 438 | - | self._deferredFunctions.append((callable, self.scopeStack[:], self.offset)) | |
| 439 | - | ||
| 440 | - | def deferAssignment(self, callable): | |
| 441 | - | """ | |
| 442 | - | Schedule an assignment handler to be called just after deferred | |
| 443 | - | function handlers. | |
| 444 | - | """ | |
| 445 | - | self._deferredAssignments.append((callable, self.scopeStack[:], self.offset)) | |
| 446 | - | ||
| 447 | - | def runDeferred(self, deferred): | |
| 448 | - | """ | |
| 449 | - | Run the callables in C{deferred} using their associated scope stack. | |
| 450 | - | """ | |
| 451 | - | for handler, scope, offset in deferred: | |
| 452 | - | self.scopeStack = scope | |
| 453 | - | self.offset = offset | |
| 454 | - | handler() | |
| 455 | - | ||
| 456 | - | def _in_doctest(self): | |
| 457 | - | return (len(self.scopeStack) >= 2 and | |
| 458 | - | isinstance(self.scopeStack[1], DoctestScope)) | |
| 459 | - | ||
| 460 | - | @property | |
| 461 | - | def futuresAllowed(self): | |
| 462 | - | if not all(isinstance(scope, ModuleScope) | |
| 463 | - | for scope in self.scopeStack): | |
| 464 | - | return False | |
| 465 | - | ||
| 466 | - | return self.scope._futures_allowed | |
| 467 | - | ||
| 468 | - | @futuresAllowed.setter | |
| 469 | - | def futuresAllowed(self, value): | |
| 470 | - | assert value is False | |
| 471 | - | if isinstance(self.scope, ModuleScope): | |
| 472 | - | self.scope._futures_allowed = False | |
| 473 | - | ||
| 474 | - | @property | |
| 475 | - | def scope(self): | |
| 476 | - | return self.scopeStack[-1] | |
| 477 | - | ||
| 478 | - | def popScope(self): | |
| 479 | - | self.deadScopes.append(self.scopeStack.pop()) | |
| 480 | - | ||
| 481 | - | def checkDeadScopes(self): | |
| 482 | - | """ | |
| 483 | - | Look at scopes which have been fully examined and report names in them | |
| 484 | - | which were imported but unused. | |
| 485 | - | """ | |
| 486 | - | for scope in self.deadScopes: | |
| 487 | - | # imports in classes are public members | |
| 488 | - | if isinstance(scope, ClassScope): | |
| 489 | - | continue | |
| 490 | - | ||
| 491 | - | all_binding = scope.get('__all__') | |
| 492 | - | if all_binding and not isinstance(all_binding, ExportBinding): | |
| 493 | - | all_binding = None | |
| 494 | - | ||
| 495 | - | if all_binding: | |
| 496 | - | all_names = set(all_binding.names) | |
| 497 | - | undefined = all_names.difference(scope) | |
| 498 | - | else: | |
| 499 | - | all_names = undefined = [] | |
| 500 | - | ||
| 501 | - | if undefined: | |
| 502 | - | if not scope.importStarred and \ | |
| 503 | - | os.path.basename(self.filename) != '__init__.py': | |
| 504 | - | # Look for possible mistakes in the export list | |
| 505 | - | for name in undefined: | |
| 506 | - | self.report(messages.UndefinedExport, | |
| 507 | - | scope['__all__'].source, name) | |
| 508 | - | ||
| 509 | - | # mark all import '*' as used by the undefined in __all__ | |
| 510 | - | if scope.importStarred: | |
| 511 | - | for binding in scope.values(): | |
| 512 | - | if isinstance(binding, StarImportation): | |
| 513 | - | binding.used = all_binding | |
| 514 | - | ||
| 515 | - | # Look for imported names that aren't used. | |
| 516 | - | for value in scope.values(): | |
| 517 | - | if isinstance(value, Importation): | |
| 518 | - | used = value.used or value.name in all_names | |
| 519 | - | if not used: | |
| 520 | - | messg = messages.UnusedImport | |
| 521 | - | self.report(messg, value.source, str(value)) | |
| 522 | - | for node in value.redefined: | |
| 523 | - | if isinstance(self.getParent(node), ast.For): | |
| 524 | - | messg = messages.ImportShadowedByLoopVar | |
| 525 | - | elif used: | |
| 526 | - | continue | |
| 527 | - | else: | |
| 528 | - | messg = messages.RedefinedWhileUnused | |
| 529 | - | self.report(messg, node, value.name, value.source) | |
| 530 | - | ||
| 531 | - | def pushScope(self, scopeClass=FunctionScope): | |
| 532 | - | self.scopeStack.append(scopeClass()) | |
| 533 | - | ||
| 534 | - | def report(self, messageClass, *args, **kwargs): | |
| 535 | - | self.messages.append(messageClass(self.filename, *args, **kwargs)) | |
| 536 | - | ||
| 537 | - | def getParent(self, node): | |
| 538 | - | # Lookup the first parent which is not Tuple, List or Starred | |
| 539 | - | while True: | |
| 540 | - | node = node.parent | |
| 541 | - | if not hasattr(node, 'elts') and not hasattr(node, 'ctx'): | |
| 542 | - | return node | |
| 543 | - | ||
| 544 | - | def getCommonAncestor(self, lnode, rnode, stop): | |
| 545 | - | if stop in (lnode, rnode) or not (hasattr(lnode, 'parent') and | |
| 546 | - | hasattr(rnode, 'parent')): | |
| 547 | - | return None | |
| 548 | - | if lnode is rnode: | |
| 549 | - | return lnode | |
| 550 | - | ||
| 551 | - | if (lnode.depth > rnode.depth): | |
| 552 | - | return self.getCommonAncestor(lnode.parent, rnode, stop) | |
| 553 | - | if (lnode.depth < rnode.depth): | |
| 554 | - | return self.getCommonAncestor(lnode, rnode.parent, stop) | |
| 555 | - | return self.getCommonAncestor(lnode.parent, rnode.parent, stop) | |
| 556 | - | ||
| 557 | - | def descendantOf(self, node, ancestors, stop): | |
| 558 | - | for a in ancestors: | |
| 559 | - | if self.getCommonAncestor(node, a, stop): | |
| 560 | - | return True | |
| 561 | - | return False | |
| 562 | - | ||
| 563 | - | def differentForks(self, lnode, rnode): | |
| 564 | - | """True, if lnode and rnode are located on different forks of IF/TRY""" | |
| 565 | - | ancestor = self.getCommonAncestor(lnode, rnode, self.root) | |
| 566 | - | parts = getAlternatives(ancestor) | |
| 567 | - | if parts: | |
| 568 | - | for items in parts: | |
| 569 | - | if self.descendantOf(lnode, items, ancestor) ^ \ | |
| 570 | - | self.descendantOf(rnode, items, ancestor): | |
| 571 | - | return True | |
| 572 | - | return False | |
| 573 | - | ||
| 574 | - | def addBinding(self, node, value): | |
| 575 | - | """ | |
| 576 | - | Called when a binding is altered. | |
| 577 | - | ||
| 578 | - | - `node` is the statement responsible for the change | |
| 579 | - | - `value` is the new value, a Binding instance | |
| 580 | - | """ | |
| 581 | - | # assert value.source in (node, node.parent): | |
| 582 | - | for scope in self.scopeStack[::-1]: | |
| 583 | - | if value.name in scope: | |
| 584 | - | break | |
| 585 | - | existing = scope.get(value.name) | |
| 586 | - | ||
| 587 | - | if existing and not self.differentForks(node, existing.source): | |
| 588 | - | ||
| 589 | - | parent_stmt = self.getParent(value.source) | |
| 590 | - | if isinstance(existing, Importation) and isinstance(parent_stmt, ast.For): | |
| 591 | - | self.report(messages.ImportShadowedByLoopVar, | |
| 592 | - | node, value.name, existing.source) | |
| 593 | - | ||
| 594 | - | elif scope is self.scope: | |
| 595 | - | if (isinstance(parent_stmt, ast.comprehension) and | |
| 596 | - | not isinstance(self.getParent(existing.source), | |
| 597 | - | (ast.For, ast.comprehension))): | |
| 598 | - | self.report(messages.RedefinedInListComp, | |
| 599 | - | node, value.name, existing.source) | |
| 600 | - | elif not existing.used and value.redefines(existing): | |
| 601 | - | self.report(messages.RedefinedWhileUnused, | |
| 602 | - | node, value.name, existing.source) | |
| 603 | - | ||
| 604 | - | elif isinstance(existing, Importation) and value.redefines(existing): | |
| 605 | - | existing.redefined.append(node) | |
| 606 | - | ||
| 607 | - | if value.name in self.scope: | |
| 608 | - | # then assume the rebound name is used as a global or within a loop | |
| 1523 further changed lines not shown | |||
The check that tells the two apart
fail→pass·pyflakes/test/test_imports.py::Test::test_importStar_relative
fail→pass·pyflakes/test/test_imports.py::Test::test_unusedImport_relative
fail→pass·pyflakes/test/test_imports.py::Test::test_usedImport_relative
fail→pass·pyflakes/test/test_imports.py::TestImportationObject::test_importfrom_relative
fail→pass·pyflakes/test/test_imports.py::TestImportationObject::test_importfrom_relative_parent
Check file pyflakes/test/test_imports.py, taken without changes from the fix and copied onto the older code, so the exact same check runs against both versions.
Origin and history
The code before it885a8e5395f3eac58a995a49dbe347b8f6648f64
Broken version dated2016-05-06
Modulepyflakes.checker
Units changedChecker, ImportationFrom, StarImportation
Fingerprint294770267807fb7b
Checked2026-08-18 by goldset/0.1
Every field above is generated by our program. None of it is written by hand.
Other bugs found in PyCQA/pyflakes
- 2023-06-13Reporter
- 2019-01-23ExportBinding
- 2015-11-12Fix global removing all UndefinedName