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 <[email protected]>
+
+- 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="[email protected]",
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",