Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pyflakes for openSUSE:Factory checked in at 2023-08-14 22:35:01 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyflakes (Old) and /work/SRC/openSUSE:Factory/.python-pyflakes.new.11712 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyflakes" Mon Aug 14 22:35:01 2023 rev:37 rq:1102806 version:3.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyflakes/python-pyflakes.changes 2023-07-12 17:28:31.278988747 +0200 +++ /work/SRC/openSUSE:Factory/.python-pyflakes.new.11712/python-pyflakes.changes 2023-08-14 22:35:13.756213766 +0200 @@ -1,0 +2,15 @@ +Tue Aug 8 06:20:25 UTC 2023 - Steve Kowalik <steven.kowa...@suse.com> + +- Update to 3.1.0: + * Drop support for EOL python 3.6 / 3.7 + * Remove ``ContinueInFinally`` check (only relevant in python < 3.8) + * Fix forward annotations inside a nested scope + * Produce an error when a definition shadows an unused variable + * Fix accessed global annotation being redefined in a local scope + * Allow redefinition of functions across ``match`` arms + * Fix potential ``None`` for ``lineno`` during tokenization errors + * Add support for PEP 695 and python 3.12 +- Switch to pyproject macros. +- Drop patch py3114.patch, included now. + +------------------------------------------------------------------- Old: ---- py3114.patch pyflakes-3.0.1.tar.gz New: ---- pyflakes-3.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyflakes.spec ++++++ --- /var/tmp/diff_new_pack.iiVdmP/_old 2023-08-14 22:35:14.316217327 +0200 +++ /var/tmp/diff_new_pack.iiVdmP/_new 2023-08-14 22:35:14.320217352 +0200 @@ -18,17 +18,16 @@ %{?sle15_python_module_pythons} Name: python-pyflakes -Version: 3.0.1 +Version: 3.1.0 Release: 0 Summary: Passive checker of Python programs License: MIT -Group: Development/Languages/Python URL: https://github.com/PyCQA/pyflakes Source: https://files.pythonhosted.org/packages/source/p/pyflakes/pyflakes-%{version}.tar.gz -#PATCH-FIX-UPSTREAM https://github.com/PyCQA/pyflakes/commit/836631f2f73d45baa4021453d89fc9fd6f52be58 fix error reporter and testsuite in 3.11.4+ -Patch: py3114.patch BuildRequires: %{python_module base >= 3.8} +BuildRequires: %{python_module pip} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros # the pkg_resources module is required at runtime @@ -47,10 +46,10 @@ %autosetup -p1 -n pyflakes-%{version} %build -%python_build +%pyproject_wheel %install -%python_install +%pyproject_install %python_expand %fdupes %{buildroot}%{$python_sitelib}/pyflakes/ %python_clone -a %{buildroot}%{_bindir}/pyflakes @@ -68,6 +67,6 @@ %doc NEWS.rst README.rst AUTHORS %python_alternative %{_bindir}/pyflakes %{python_sitelib}/pyflakes/ -%{python_sitelib}/pyflakes-%{version}-py*.egg-info +%{python_sitelib}/pyflakes-%{version}.dist-info %changelog ++++++ pyflakes-3.0.1.tar.gz -> pyflakes-3.1.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/NEWS.rst new/pyflakes-3.1.0/NEWS.rst --- old/pyflakes-3.0.1/NEWS.rst 2022-11-24 17:52:19.000000000 +0100 +++ new/pyflakes-3.1.0/NEWS.rst 2023-07-29 18:57:43.000000000 +0200 @@ -1,3 +1,14 @@ +3.1.0 (2023-07-29) + +- Drop support for EOL python 3.6 / 3.7 +- Remove ``ContinueInFinally`` check (only relevant in python < 3.8) +- Fix forward annotations inside a nested scope +- Produce an error when a definition shadows an unused variable +- Fix accessed global annotation being redefined in a local scope +- Allow redefinition of functions across ``match`` arms +- Fix potential ``None`` for ``lineno`` during tokenization errors +- Add support for PEP 695 and python 3.12 + 3.0.1 (2022-11-24) - Fix crash on augmented assign to ``print`` builtin diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/PKG-INFO new/pyflakes-3.1.0/PKG-INFO --- old/pyflakes-3.0.1/PKG-INFO 2022-11-24 17:53:21.689930700 +0100 +++ new/pyflakes-3.1.0/PKG-INFO 2023-07-29 19:00:24.935112200 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pyflakes -Version: 3.0.1 +Version: 3.1.0 Summary: passive checker of Python programs Home-page: https://github.com/PyCQA/pyflakes Author: A lot of people @@ -12,17 +12,12 @@ Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development Classifier: Topic :: Utilities -Requires-Python: >=3.6 +Requires-Python: >=3.8 License-File: LICENSE ======== @@ -89,8 +84,8 @@ Issues are tracked on `GitHub <https://github.com/PyCQA/pyflakes/issues>`_. -Patches may be submitted via a `GitHub pull request`_ or via the mailing list -if you prefer. If you are comfortable doing so, please `rebase your changes`_ +Patches may be submitted via a `GitHub pull request`_. +If you are comfortable doing so, please `rebase your changes`_ so they may be applied to main with a fast-forward merge, and each commit is a coherent unit of work with a well-written log message. If you are not comfortable with this rebase workflow, the project maintainers will be happy to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/README.rst new/pyflakes-3.1.0/README.rst --- old/pyflakes-3.0.1/README.rst 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/README.rst 2023-01-31 19:28:24.000000000 +0100 @@ -62,8 +62,8 @@ Issues are tracked on `GitHub <https://github.com/PyCQA/pyflakes/issues>`_. -Patches may be submitted via a `GitHub pull request`_ or via the mailing list -if you prefer. If you are comfortable doing so, please `rebase your changes`_ +Patches may be submitted via a `GitHub pull request`_. +If you are comfortable doing so, please `rebase your changes`_ so they may be applied to main with a fast-forward merge, and each commit is a coherent unit of work with a well-written log message. If you are not comfortable with this rebase workflow, the project maintainers will be happy to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/__init__.py new/pyflakes-3.1.0/pyflakes/__init__.py --- old/pyflakes-3.0.1/pyflakes/__init__.py 2022-11-24 17:52:30.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/__init__.py 2023-07-29 18:58:22.000000000 +0200 @@ -1 +1 @@ -__version__ = '3.0.1' +__version__ = '3.1.0' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/checker.py new/pyflakes-3.1.0/pyflakes/checker.py --- old/pyflakes-3.0.1/pyflakes/checker.py 2022-11-24 17:51:58.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/checker.py 2023-07-29 18:51:35.000000000 +0200 @@ -7,6 +7,7 @@ import __future__ import builtins import ast +import collections import contextlib import doctest import functools @@ -18,7 +19,6 @@ from pyflakes import messages -PY38_PLUS = sys.version_info >= (3, 8) PYPY = hasattr(sys, 'pypy_version_info') builtin_vars = dir(builtins) @@ -29,21 +29,20 @@ def getAlternatives(n): if isinstance(n, ast.If): return [n.body] - if isinstance(n, ast.Try): + elif isinstance(n, ast.Try): return [n.body + n.orelse] + [[hdl] for hdl in n.handlers] + elif sys.version_info >= (3, 10) and isinstance(n, ast.Match): + return [mc.body for mc in n.cases] FOR_TYPES = (ast.For, ast.AsyncFor) -if PY38_PLUS: - def _is_singleton(node): # type: (ast.AST) -> bool - return ( - isinstance(node, ast.Constant) and - isinstance(node.value, (bool, type(Ellipsis), type(None))) - ) -else: - def _is_singleton(node): # type: (ast.AST) -> bool - return isinstance(node, (ast.NameConstant, ast.Ellipsis)) + +def _is_singleton(node): # type: (ast.AST) -> bool + return ( + isinstance(node, ast.Constant) and + isinstance(node.value, (bool, type(Ellipsis), type(None))) + ) def _is_tuple_constant(node): # type: (ast.AST) -> bool @@ -53,16 +52,8 @@ ) -if PY38_PLUS: - def _is_constant(node): - return isinstance(node, ast.Constant) or _is_tuple_constant(node) -else: - def _is_constant(node): - return ( - isinstance(node, (ast.Str, ast.Num, ast.Bytes)) or - _is_singleton(node) or - _is_tuple_constant(node) - ) +def _is_constant(node): + return isinstance(node, ast.Constant) or _is_tuple_constant(node) def _is_const_non_singleton(node): # type: (ast.AST) -> bool @@ -209,27 +200,12 @@ def convert_to_value(item): - if isinstance(item, ast.Str): - return item.s - elif hasattr(ast, 'Bytes') and isinstance(item, ast.Bytes): - return item.s + if isinstance(item, ast.Constant): + return item.value elif isinstance(item, ast.Tuple): return tuple(convert_to_value(i) for i in item.elts) - elif isinstance(item, ast.Num): - return item.n elif isinstance(item, ast.Name): - result = VariableKey(item=item) - constants_lookup = { - 'True': True, - 'False': False, - 'None': None, - } - return constants_lookup.get( - result.name, - result, - ) - elif isinstance(item, ast.NameConstant): - return item.value + return VariableKey(item=item) else: return UnhandledKeyType() @@ -274,6 +250,11 @@ """ A binding that defines a function or a class. """ + def redefines(self, other): + return ( + super().redefines(other) or + (isinstance(other, Assignment) and self.name == other.name) + ) class Builtin(Definition): @@ -521,8 +502,8 @@ def _add_to_names(container): for node in container.elts: - if isinstance(node, ast.Str): - self.names.append(node.s) + if isinstance(node, ast.Constant) and isinstance(node.value, str): + self.names.append(node.value) if isinstance(source.value, (ast.List, ast.Tuple)): _add_to_names(source.value) @@ -596,6 +577,10 @@ yield name, binding +class TypeScope(Scope): + pass + + class GeneratorScope(Scope): pass @@ -730,17 +715,7 @@ class Checker: - """ - I check the cleanliness and sanity of Python code. - - @ivar _deferredFunctions: Tracking list used by L{deferFunction}. Elements - of the list are two-tuples. The first element is the callable passed - to L{deferFunction}. The second element is a copy of the scope stack - at the time L{deferFunction} was called. - - @ivar _deferredAssignments: Similar to C{_deferredFunctions}, but for - callables which are deferred assignment checks. - """ + """I check the cleanliness and sanity of Python code.""" _ast_node_scope = { ast.Module: ModuleScope, @@ -757,7 +732,6 @@ nodeDepth = 0 offset = None _in_annotation = AnnotationState.NONE - _in_deferred = False builtIns = set(builtin_vars).union(_MAGIC_GLOBALS) _customBuiltIns = os.environ.get('PYFLAKES_BUILTINS') @@ -768,34 +742,28 @@ def __init__(self, tree, filename='(none)', builtins=None, withDoctest='PYFLAKES_DOCTEST' in os.environ, file_tokens=()): self._nodeHandlers = {} - self._deferredFunctions = [] - self._deferredAssignments = [] + self._deferred = collections.deque() self.deadScopes = [] self.messages = [] self.filename = filename if builtins: self.builtIns = self.builtIns.union(builtins) self.withDoctest = withDoctest + self.exceptHandlers = [()] + self.root = tree + + self.scopeStack = [] try: - self.scopeStack = [Checker._ast_node_scope[type(tree)]()] + scope_tp = Checker._ast_node_scope[type(tree)] except KeyError: raise RuntimeError('No scope implemented for the node %r' % tree) - self.exceptHandlers = [()] - self.root = tree - for builtin in self.builtIns: - self.addBinding(None, Builtin(builtin)) - self.handleChildren(tree) - self._in_deferred = True - self.runDeferred(self._deferredFunctions) - # Set _deferredFunctions to None so that deferFunction will fail - # noisily if called after we've run through the deferred functions. - self._deferredFunctions = None - self.runDeferred(self._deferredAssignments) - # Set _deferredAssignments to None so that deferAssignment will fail - # noisily if called after we've run through the deferred assignments. - self._deferredAssignments = None - del self.scopeStack[1:] - self.popScope() + + with self.in_scope(scope_tp): + for builtin in self.builtIns: + self.addBinding(None, Builtin(builtin)) + self.handleChildren(tree) + self._run_deferred() + self.checkDeadScopes() if file_tokens: @@ -813,24 +781,18 @@ `callable` is called, the scope at the time this is called will be restored, however it will contain any new bindings added to it. """ - self._deferredFunctions.append((callable, self.scopeStack[:], self.offset)) + self._deferred.append((callable, self.scopeStack[:], self.offset)) - def deferAssignment(self, callable): - """ - Schedule an assignment handler to be called just after deferred - function handlers. - """ - self._deferredAssignments.append((callable, self.scopeStack[:], self.offset)) + def _run_deferred(self): + orig = (self.scopeStack, self.offset) - def runDeferred(self, deferred): - """ - Run the callables in C{deferred} using their associated scope stack. - """ - for handler, scope, offset in deferred: - self.scopeStack = scope - self.offset = offset + while self._deferred: + handler, scope, offset = self._deferred.popleft() + self.scopeStack, self.offset = scope, offset handler() + self.scopeStack, self.offset = orig + def _in_doctest(self): return (len(self.scopeStack) >= 2 and isinstance(self.scopeStack[1], DoctestScope)) @@ -866,8 +828,13 @@ def scope(self): return self.scopeStack[-1] - def popScope(self): - self.deadScopes.append(self.scopeStack.pop()) + @contextlib.contextmanager + def in_scope(self, cls): + self.scopeStack.append(cls()) + try: + yield + finally: + self.deadScopes.append(self.scopeStack.pop()) def checkDeadScopes(self): """ @@ -879,6 +846,12 @@ if isinstance(scope, ClassScope): continue + if isinstance(scope, FunctionScope): + for name, binding in scope.unused_assignments(): + self.report(messages.UnusedVariable, binding.source, name) + for name, binding in scope.unused_annotations(): + self.report(messages.UnusedAnnotation, binding.source, name) + all_binding = scope.get('__all__') if all_binding and not isinstance(all_binding, ExportBinding): all_binding = None @@ -929,9 +902,6 @@ messg = messages.RedefinedWhileUnused self.report(messg, node, value.name, value.source) - def pushScope(self, scopeClass=FunctionScope): - self.scopeStack.append(scopeClass()) - def report(self, messageClass, *args, **kwargs): self.messages.append(messageClass(self.filename, *args, **kwargs)) @@ -1073,7 +1043,12 @@ if not name: return - in_generators = None + # only the following can access class scoped variables (since classes + # aren't really a scope) + # - direct accesses (not within a nested scope) + # - generators + # - type annotations (for generics, etc.) + can_access_class_vars = None importStarred = None # try enclosing function scopes and global scope @@ -1081,7 +1056,7 @@ if isinstance(scope, ClassScope): if name == '__class__': return - elif in_generators is False: + elif can_access_class_vars is False: # only generators used in a class scope can access the # names of the class. this is skipped during the first # iteration @@ -1089,7 +1064,7 @@ binding = scope.get(name, None) if isinstance(binding, Annotation) and not self._in_postponed_annotation: - scope[name].used = True + scope[name].used = (self.scope, node) continue if name == 'print' and isinstance(binding, Builtin): @@ -1116,8 +1091,10 @@ importStarred = importStarred or scope.importStarred - if in_generators is not False: - in_generators = isinstance(scope, GeneratorScope) + if can_access_class_vars is not False: + can_access_class_vars = isinstance( + scope, (TypeScope, GeneratorScope), + ) if importStarred: from_list = [] @@ -1181,7 +1158,7 @@ ) ): binding = ExportBinding(name, node._pyflakes_parent, self.scope) - elif PY38_PLUS and isinstance(parent_stmt, ast.NamedExpr): + elif isinstance(parent_stmt, ast.NamedExpr): binding = NamedExprAssignment(name, node) else: binding = Assignment(name, node) @@ -1248,22 +1225,21 @@ Determine if the given node is a docstring, as long as it is at the correct place in the node tree. """ - return isinstance(node, ast.Str) or (isinstance(node, ast.Expr) and - isinstance(node.value, ast.Str)) + return ( + isinstance(node, ast.Expr) and + isinstance(node.value, ast.Constant) and + isinstance(node.value.value, str) + ) def getDocstring(self, node): - if isinstance(node, ast.Expr): - node = node.value - if not isinstance(node, ast.Str): - return (None, None) - - if PYPY or PY38_PLUS: - doctest_lineno = node.lineno - 1 + if ( + isinstance(node, ast.Expr) and + isinstance(node.value, ast.Constant) and + isinstance(node.value.value, str) + ): + return node.value.value, node.lineno - 1 else: - # Computed incorrectly if the docstring has backslash - doctest_lineno = node.lineno - node.s.count('\n') - 1 - - return (node.s, doctest_lineno) + return None, None def handleNode(self, node, parent): if node is None: @@ -1271,8 +1247,12 @@ if self.offset and getattr(node, 'lineno', None) is not None: node.lineno += self.offset[0] node.col_offset += self.offset[1] - if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or - self.isDocstring(node)): + if ( + self.futuresAllowed and + self.nodeDepth == 0 and + not isinstance(node, ast.ImportFrom) and + not self.isDocstring(node) + ): self.futuresAllowed = False self.nodeDepth += 1 node._pyflakes_depth = self.nodeDepth @@ -1300,22 +1280,21 @@ saved_stack = self.scopeStack self.scopeStack = [self.scopeStack[0]] node_offset = self.offset or (0, 0) - self.pushScope(DoctestScope) - if '_' not in self.scopeStack[0]: - self.addBinding(None, Builtin('_')) - for example in examples: - try: - tree = ast.parse(example.source, "<doctest>") - except SyntaxError as e: - position = (node_lineno + example.lineno + e.lineno, - example.indent + 4 + (e.offset or 0)) - self.report(messages.DoctestSyntaxError, node, position) - else: - self.offset = (node_offset[0] + node_lineno + example.lineno, - node_offset[1] + example.indent + 4) - self.handleChildren(tree) - self.offset = node_offset - self.popScope() + with self.in_scope(DoctestScope): + if '_' not in self.scopeStack[0]: + self.addBinding(None, Builtin('_')) + for example in examples: + try: + tree = ast.parse(example.source, "<doctest>") + except SyntaxError as e: + position = (node_lineno + example.lineno + e.lineno, + example.indent + 4 + (e.offset or 0)) + self.report(messages.DoctestSyntaxError, node, position) + else: + self.offset = (node_offset[0] + node_lineno + example.lineno, + node_offset[1] + example.indent + 4) + self.handleChildren(tree) + self.offset = node_offset self.scopeStack = saved_stack @in_string_annotation @@ -1342,21 +1321,27 @@ self.handleNode(parsed_annotation, node) + def handle_annotation_always_deferred(self, annotation, parent): + fn = in_annotation(Checker.handleNode) + self.deferFunction(lambda: fn(self, annotation, parent)) + @in_annotation def handleAnnotation(self, annotation, node): - if isinstance(annotation, ast.Str): + if ( + isinstance(annotation, ast.Constant) and + isinstance(annotation.value, str) + ): # Defer handling forward annotation. self.deferFunction(functools.partial( self.handleStringAnnotation, - annotation.s, + annotation.value, node, annotation.lineno, annotation.col_offset, messages.ForwardAnnotationSyntaxError, )) elif self.annotationsFutureEnabled: - fn = in_annotation(Checker.handleNode) - self.deferFunction(lambda: fn(self, annotation, node)) + self.handle_annotation_always_deferred(annotation, node) else: self.handleNode(annotation, node) @@ -1413,7 +1398,7 @@ def _handle_string_dot_format(self, node): try: - placeholders = tuple(parse_format_string(node.func.value.s)) + placeholders = tuple(parse_format_string(node.func.value.value)) except ValueError as e: self.report(messages.StringDotFormatInvalidFormat, node, e) return @@ -1529,7 +1514,8 @@ def CALL(self, node): if ( isinstance(node.func, ast.Attribute) and - isinstance(node.func.value, ast.Str) and + isinstance(node.func.value, ast.Constant) and + isinstance(node.func.value.value, str) and node.func.attr == 'format' ): self._handle_string_dot_format(node) @@ -1610,7 +1596,7 @@ def _handle_percent_format(self, node): try: - placeholders = parse_percent_format(node.left.s) + placeholders = parse_percent_format(node.left.value) except ValueError: self.report( messages.PercentFormatInvalidFormat, @@ -1689,13 +1675,16 @@ if ( isinstance(node.right, ast.Dict) and - all(isinstance(k, ast.Str) for k in node.right.keys) + all( + isinstance(k, ast.Constant) and isinstance(k.value, str) + for k in node.right.keys + ) ): if positional and positional_count > 1: self.report(messages.PercentFormatExpectedSequence, node) return - substitution_keys = {k.s for k in node.right.keys} + substitution_keys = {k.value for k in node.right.keys} extra_keys = substitution_keys - named missing_keys = named - substitution_keys if not positional and extra_keys: @@ -1714,32 +1703,23 @@ def BINOP(self, node): if ( isinstance(node.op, ast.Mod) and - isinstance(node.left, ast.Str) + isinstance(node.left, ast.Constant) and + isinstance(node.left.value, str) ): self._handle_percent_format(node) self.handleChildren(node) - def STR(self, node): - if self._in_annotation: + def CONSTANT(self, node): + if isinstance(node.value, str) and self._in_annotation: fn = functools.partial( self.handleStringAnnotation, - node.s, + node.value, node, node.lineno, node.col_offset, messages.ForwardAnnotationSyntaxError, ) - if self._in_deferred: - fn() - else: - self.deferFunction(fn) - - if PY38_PLUS: - def CONSTANT(self, node): - if isinstance(node.value, str): - return self.STR(node) - else: - NUM = BYTES = ELLIPSIS = CONSTANT = ignore + self.deferFunction(fn) # "slice" type nodes SLICE = EXTSLICE = INDEX = handleChildren @@ -1867,9 +1847,8 @@ NONLOCAL = GLOBAL def GENERATOREXP(self, node): - self.pushScope(GeneratorScope) - self.handleChildren(node) - self.popScope() + with self.in_scope(GeneratorScope): + self.handleChildren(node) LISTCOMP = DICTCOMP = SETCOMP = GENERATOREXP @@ -1905,11 +1884,6 @@ return if isinstance(n, (ast.FunctionDef, ast.ClassDef)): break - # Handle Try/TryFinally difference in Python < and >= 3.3 - if hasattr(n, 'finalbody') and isinstance(node, ast.Continue): - if n_child in n.finalbody and not PY38_PLUS: - self.report(messages.ContinueInFinally, node) - return if isinstance(node, ast.Continue): self.report(messages.ContinueOutsideLoop, node) else: # ast.Break @@ -1942,7 +1916,10 @@ def FUNCTIONDEF(self, node): for deco in node.decorator_list: self.handleNode(deco, node) - self.LAMBDA(node) + + with self._type_param_scope(node): + self.LAMBDA(node) + self.addBinding(node, FunctionDefinition(node.name, node)) # doctest does not process doctest within a doctest, # or in nested functions. @@ -1957,10 +1934,9 @@ args = [] annotations = [] - if PY38_PLUS: - for arg in node.args.posonlyargs: - args.append(arg.arg) - annotations.append(arg.annotation) + for arg in node.args.posonlyargs: + args.append(arg.arg) + annotations.append(arg.annotation) for arg in node.args.args + node.args.kwonlyargs: args.append(arg.arg) annotations.append(arg.annotation) @@ -1991,29 +1967,11 @@ self.handleNode(default, node) def runFunction(): - - self.pushScope() - - self.handleChildren(node, omit=['decorator_list', 'returns']) - - def check_unused_assignments(): - """ - Check to see if any assignments have not been used. - """ - for name, binding in self.scope.unused_assignments(): - self.report(messages.UnusedVariable, binding.source, name) - - def check_unused_annotations(): - """ - Check to see if any annotations have not been used. - """ - for name, binding in self.scope.unused_annotations(): - self.report(messages.UnusedAnnotation, binding.source, name) - - self.deferAssignment(check_unused_assignments) - self.deferAssignment(check_unused_annotations) - - self.popScope() + with self.in_scope(FunctionScope): + self.handleChildren( + node, + omit=('decorator_list', 'returns', 'type_params'), + ) self.deferFunction(runFunction) @@ -2031,20 +1989,22 @@ """ for deco in node.decorator_list: self.handleNode(deco, node) - for baseNode in node.bases: - self.handleNode(baseNode, node) - for keywordNode in node.keywords: - self.handleNode(keywordNode, node) - self.pushScope(ClassScope) - # doctest does not process doctest within a doctest - # classes within classes are processed. - if (self.withDoctest and - not self._in_doctest() and - not isinstance(self.scope, FunctionScope)): - self.deferFunction(lambda: self.handleDoctests(node)) - for stmt in node.body: - self.handleNode(stmt, node) - self.popScope() + + with self._type_param_scope(node): + for baseNode in node.bases: + self.handleNode(baseNode, node) + for keywordNode in node.keywords: + self.handleNode(keywordNode, node) + with self.in_scope(ClassScope): + # doctest does not process doctest within a doctest + # classes within classes are processed. + if (self.withDoctest and + not self._in_doctest() and + not isinstance(self.scope, FunctionScope)): + self.deferFunction(lambda: self.handleDoctests(node)) + for stmt in node.body: + self.handleNode(stmt, node) + self.addBinding(node, ClassDefinition(node.name, node)) def AUGASSIGN(self, node): @@ -2218,3 +2178,21 @@ self.handleChildren(node) MATCHAS = MATCHMAPPING = MATCHSTAR = _match_target + + @contextlib.contextmanager + def _type_param_scope(self, node): + with contextlib.ExitStack() as ctx: + if sys.version_info >= (3, 12): + ctx.enter_context(self.in_scope(TypeScope)) + for param in node.type_params: + self.handleNode(param, node) + yield + + def TYPEVAR(self, node): + self.handleNodeStore(node) + self.handle_annotation_always_deferred(node.bound, node) + + def TYPEALIAS(self, node): + self.handleNode(node.name, node) + with self._type_param_scope(node): + self.handle_annotation_always_deferred(node.value, node) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/messages.py new/pyflakes-3.1.0/pyflakes/messages.py --- old/pyflakes-3.0.1/pyflakes/messages.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/messages.py 2023-01-31 19:28:24.000000000 +0100 @@ -198,13 +198,6 @@ message = '\'break\' outside loop' -class ContinueInFinally(Message): - """ - Indicates a continue statement in a finally block in a while or for loop. - """ - message = '\'continue\' not supported inside \'finally\' clause' - - class DefaultExceptNotLast(Message): """ Indicates an except: block as not the last exception handler. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/reporter.py new/pyflakes-3.1.0/pyflakes/reporter.py --- old/pyflakes-3.0.1/pyflakes/reporter.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/reporter.py 2023-06-13 03:11:44.000000000 +0200 @@ -56,12 +56,11 @@ else: line = text.splitlines()[-1] + # lineno might be None if the error was during tokenization # lineno might be 0 if the error came from stdin - lineno = max(lineno, 1) + lineno = max(lineno or 0, 1) if offset is not None: - if sys.version_info < (3, 8) and text is not None: - offset = offset - (len(text) - len(line)) + 1 # some versions of python emit an offset of -1 for certain encoding errors offset = max(offset, 1) self._stderr.write('%s:%d:%d: %s\n' % diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/test/test_api.py new/pyflakes-3.1.0/pyflakes/test/test_api.py --- old/pyflakes-3.0.1/pyflakes/test/test_api.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/test/test_api.py 2023-06-13 03:50:05.000000000 +0200 @@ -233,9 +233,7 @@ """ err = io.StringIO() reporter = Reporter(None, err) - reporter.syntaxError('foo.py', 'a problem', 3, - 8 if sys.version_info >= (3, 8) else 7, - 'bad line of source') + reporter.syntaxError('foo.py', 'a problem', 3, 8, 'bad line of source') self.assertEqual( ("foo.py:3:8: a problem\n" "bad line of source\n" @@ -281,11 +279,10 @@ reporter = Reporter(None, err) reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7, '\n'.join(lines)) - column = 25 if sys.version_info >= (3, 8) else 7 self.assertEqual( - ("foo.py:3:%d: a problem\n" % column + + ("foo.py:3:25: a problem\n" + lines[-1] + "\n" + - " " * (column - 1) + "^\n"), + " " * 24 + "^\n"), err.getvalue()) def test_unexpectedError(self): @@ -417,10 +414,8 @@ if PYPY or sys.version_info >= (3, 10): column = 12 - elif sys.version_info >= (3, 8): - column = 8 else: - column = 11 + column = 8 self.assertHasErrors( sourcePath, ["""\ @@ -479,6 +474,11 @@ pass """ with self.makeTempFile(source) as sourcePath: + if sys.version_info >= (3, 12): + msg = 'parameter without a default follows parameter with a default' # noqa: E501 + else: + msg = 'non-default argument follows default argument' + if PYPY and sys.version_info >= (3, 9): column = 18 elif PYPY: @@ -487,18 +487,16 @@ column = 18 elif sys.version_info >= (3, 9): column = 21 - elif sys.version_info >= (3, 8): - column = 9 else: - column = 8 + column = 9 last_line = ' ' * (column - 1) + '^\n' - columnstr = '%d:' % column self.assertHasErrors( sourcePath, - ["""\ -{}:1:{} non-default argument follows default argument + [f"""\ +{sourcePath}:1:{column}: {msg} def foo(bar=baz, bax): -{}""".format(sourcePath, columnstr, last_line)]) +{last_line}"""] + ) def test_nonKeywordAfterKeywordSyntaxError(self): """ @@ -512,7 +510,7 @@ with self.makeTempFile(source) as sourcePath: if sys.version_info >= (3, 9): column = 17 - elif not PYPY and sys.version_info >= (3, 8): + elif not PYPY: column = 14 else: column = 13 @@ -539,7 +537,7 @@ column = 7 elif PYPY: column = 6 - elif sys.version_info >= (3, 9): + elif (3, 9) <= sys.version_info < (3, 12): column = 13 else: column = 7 @@ -628,8 +626,12 @@ x = "%s" """ % SNOWMAN).encode('utf-16') with self.makeTempFile(source) as sourcePath: - self.assertHasErrors( - sourcePath, [f"{sourcePath}: problem decoding source\n"]) + if sys.version_info < (3, 11, 4): + expected = f"{sourcePath}: problem decoding source\n" + else: + expected = f"{sourcePath}:1: source code string cannot contain null bytes\n" # noqa: E501 + + self.assertHasErrors(sourcePath, [expected]) def test_checkRecursive(self): """ @@ -679,18 +681,10 @@ "max(1 for i in range(10), key=lambda x: x+1)", " ^", ] - elif sys.version_info >= (3, 8): + else: expected_error = [ "<stdin>:1:5: Generator expression must be parenthesized", ] - elif sys.version_info >= (3, 7): - expected_error = [ - "<stdin>:1:4: Generator expression must be parenthesized", - ] - elif sys.version_info >= (3, 6): - expected_error = [ - "<stdin>:1:4: Generator expression must be parenthesized if not sole argument", # noqa: E501 - ] self.assertEqual(errlines, expected_error) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/test/test_doctests.py new/pyflakes-3.1.0/pyflakes/test/test_doctests.py --- old/pyflakes-3.0.1/pyflakes/test/test_doctests.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/test/test_doctests.py 2023-01-31 19:28:24.000000000 +0100 @@ -1,4 +1,3 @@ -import sys import textwrap from pyflakes import messages as m @@ -323,7 +322,7 @@ m.DoctestSyntaxError).messages exc = exceptions[0] self.assertEqual(exc.lineno, 4) - if not PYPY and sys.version_info >= (3, 8): + if not PYPY: self.assertEqual(exc.col, 18) else: self.assertEqual(exc.col, 26) @@ -339,10 +338,7 @@ self.assertEqual(exc.col, 16) exc = exceptions[2] self.assertEqual(exc.lineno, 6) - if PYPY or sys.version_info >= (3, 8): - self.assertEqual(exc.col, 13) - else: - self.assertEqual(exc.col, 18) + self.assertEqual(exc.col, 13) def test_indentationErrorInDoctest(self): exc = self.flakes(''' @@ -353,10 +349,7 @@ """ ''', m.DoctestSyntaxError).messages[0] self.assertEqual(exc.lineno, 5) - if PYPY or sys.version_info >= (3, 8): - self.assertEqual(exc.col, 13) - else: - self.assertEqual(exc.col, 16) + self.assertEqual(exc.col, 13) def test_offsetWithMultiLineArgs(self): (exc1, exc2) = self.flakes( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/test/test_match.py new/pyflakes-3.1.0/pyflakes/test/test_match.py --- old/pyflakes-3.0.1/pyflakes/test/test_match.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/test/test_match.py 2023-06-13 02:24:07.000000000 +0200 @@ -81,3 +81,14 @@ case {'foo': k1, **rest}: print(f'{k1=} {rest=}') ''') + + def test_defined_in_different_branches(self): + self.flakes(''' + def f(x): + match x: + case 1: + def y(): pass + case _: + def y(): print(1) + return y + ''') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/test/test_other.py new/pyflakes-3.1.0/pyflakes/test/test_other.py --- old/pyflakes-3.0.1/pyflakes/test/test_other.py 2022-11-24 17:51:58.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/test/test_other.py 2023-01-31 19:28:24.000000000 +0100 @@ -118,6 +118,12 @@ def a(): pass ''', m.RedefinedWhileUnused) + def test_redefined_function_shadows_variable(self): + self.flakes(''' + x = 1 + def x(): pass + ''', m.RedefinedWhileUnused) + def test_redefinedUnderscoreFunction(self): """ Test that shadowing a function definition named with underscore doesn't @@ -445,36 +451,6 @@ continue ''') - @skipIf(version_info > (3, 8), "Python <= 3.8 only") - def test_continueInFinally(self): - # 'continue' inside 'finally' is a special syntax error - # that is removed in 3.8 - self.flakes(''' - while True: - try: - pass - finally: - continue - ''', m.ContinueInFinally) - - self.flakes(''' - while True: - try: - pass - finally: - if 1: - if 2: - continue - ''', m.ContinueInFinally) - - # Even when not in a loop, this is the error Python gives - self.flakes(''' - try: - pass - finally: - continue - ''', m.ContinueInFinally) - def test_breakOutsideLoop(self): self.flakes(''' break @@ -1716,7 +1692,6 @@ print(f'\x7b4*baz\N{RIGHT CURLY BRACKET}') ''') - @skipIf(version_info < (3, 8), 'new in Python 3.8') def test_assign_expr(self): """Test PEP 572 assignment expressions are treated as usage / write.""" self.flakes(''' @@ -1725,7 +1700,6 @@ print(x) ''') - @skipIf(version_info < (3, 8), 'new in Python 3.8') def test_assign_expr_generator_scope(self): """Test assignment expressions in generator expressions.""" self.flakes(''' @@ -1733,7 +1707,6 @@ print(y) ''') - @skipIf(version_info < (3, 8), 'new in Python 3.8') def test_assign_expr_nested(self): """Test assignment expressions in nested expressions.""" self.flakes(''' @@ -1972,19 +1945,6 @@ return output ''', m.BreakOutsideLoop) - @skipIf(version_info > (3, 8), "Python <= 3.8 only") - def test_continueInAsyncForFinally(self): - self.flakes(''' - async def read_data(db): - output = [] - async for row in db.cursor(): - try: - output.append(row) - finally: - continue - return output - ''', m.ContinueInFinally) - def test_asyncWith(self): self.flakes(''' async def commit(session, data): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes/test/test_type_annotations.py new/pyflakes-3.1.0/pyflakes/test/test_type_annotations.py --- old/pyflakes-3.0.1/pyflakes/test/test_type_annotations.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes/test/test_type_annotations.py 2023-07-29 18:51:35.000000000 +0200 @@ -367,6 +367,13 @@ x = 3 ''', m.UnusedVariable) + def test_unused_annotation_in_outer_scope_reassigned_in_local_scope(self): + self.flakes(''' + x: int + x.__dict__ + def f(): x = 1 + ''', m.UndefinedName, m.UnusedVariable) + def test_unassigned_annotation_is_undefined(self): self.flakes(''' name: str @@ -379,7 +386,6 @@ async def func(c: c) -> None: pass ''') - @skipIf(version_info < (3, 7), 'new in Python 3.7') def test_postponed_annotations(self): self.flakes(''' from __future__ import annotations @@ -434,7 +440,6 @@ return Y """, m.UndefinedName) - @skipIf(version_info < (3, 8), 'new in Python 3.8') def test_positional_only_argument_annotations(self): self.flakes(""" from x import C @@ -584,7 +589,6 @@ return None """) - @skipIf(version_info < (3, 7), 'new in Python 3.7') def test_partial_string_annotations_with_future_annotations(self): self.flakes(""" from __future__ import annotations @@ -597,6 +601,20 @@ return None """) + def test_forward_annotations_for_classes_in_scope(self): + # see #749 + self.flakes(""" + from typing import Optional + + def f(): + class C: + a: "D" + b: Optional["D"] + c: "Optional[D]" + + class D: pass + """) + def test_idomiatic_typing_guards(self): # typing.TYPE_CHECKING: python3.5.3+ self.flakes(""" @@ -695,3 +713,70 @@ def g(x: Shape[*Ts]) -> Shape[*Ts]: ... """) + + @skipIf(version_info < (3, 12), 'new in Python 3.12') + def test_type_statements(self): + self.flakes(""" + type ListOrSet[T] = list[T] | set[T] + + def f(x: ListOrSet[str]) -> None: ... + + type RecursiveType = int | list[RecursiveType] + + type ForwardRef = int | C + + type ForwardRefInBounds[T: C] = T + + class C: pass + """) + + @skipIf(version_info < (3, 12), 'new in Python 3.12') + def test_type_parameters_functions(self): + self.flakes(""" + def f[T](t: T) -> T: return t + + async def g[T](t: T) -> T: return t + + def with_forward_ref[T: C](t: T) -> T: return t + + def can_access_inside[T](t: T) -> T: + print(T) + return t + + class C: pass + """) + + @skipIf(version_info < (3, 12), 'new in Python 3.12') + def test_type_parameters_do_not_escape_function_scopes(self): + self.flakes(""" + from x import g + + @g(T) # not accessible in decorators + def f[T](t: T) -> T: return t + + T # not accessible afterwards + """, m.UndefinedName, m.UndefinedName) + + @skipIf(version_info < (3, 12), 'new in Python 3.12') + def test_type_parameters_classes(self): + self.flakes(""" + class C[T](list[T]): pass + + class UsesForward[T: Forward](list[T]): pass + + class Forward: pass + + class WithinBody[T](list[T]): + t = T + """) + + @skipIf(version_info < (3, 12), 'new in Python 3.12') + def test_type_parameters_do_not_escape_class_scopes(self): + self.flakes(""" + from x import g + + @g(T) # not accessible in decorators + class C[T](list[T]): pass + + T # not accessible afterwards + """, m.UndefinedName, m.UndefinedName) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/pyflakes.egg-info/PKG-INFO new/pyflakes-3.1.0/pyflakes.egg-info/PKG-INFO --- old/pyflakes-3.0.1/pyflakes.egg-info/PKG-INFO 2022-11-24 17:53:21.000000000 +0100 +++ new/pyflakes-3.1.0/pyflakes.egg-info/PKG-INFO 2023-07-29 19:00:24.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pyflakes -Version: 3.0.1 +Version: 3.1.0 Summary: passive checker of Python programs Home-page: https://github.com/PyCQA/pyflakes Author: A lot of people @@ -12,17 +12,12 @@ Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development Classifier: Topic :: Utilities -Requires-Python: >=3.6 +Requires-Python: >=3.8 License-File: LICENSE ======== @@ -89,8 +84,8 @@ Issues are tracked on `GitHub <https://github.com/PyCQA/pyflakes/issues>`_. -Patches may be submitted via a `GitHub pull request`_ or via the mailing list -if you prefer. If you are comfortable doing so, please `rebase your changes`_ +Patches may be submitted via a `GitHub pull request`_. +If you are comfortable doing so, please `rebase your changes`_ so they may be applied to main with a fast-forward merge, and each commit is a coherent unit of work with a well-written log message. If you are not comfortable with this rebase workflow, the project maintainers will be happy to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyflakes-3.0.1/setup.py new/pyflakes-3.1.0/setup.py --- old/pyflakes-3.0.1/setup.py 2022-11-24 17:02:51.000000000 +0100 +++ new/pyflakes-3.1.0/setup.py 2023-01-31 19:28:24.000000000 +0100 @@ -42,7 +42,7 @@ author_email="code-qual...@python.org", url="https://github.com/PyCQA/pyflakes", packages=["pyflakes", "pyflakes.scripts", "pyflakes.test"], - python_requires='>=3.6', + python_requires='>=3.8', classifiers=[ "Development Status :: 6 - Mature", "Environment :: Console", @@ -50,11 +50,6 @@ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy",