Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-asttokens for
openSUSE:Factory checked in at 2022-12-03 10:03:11
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asttokens (Old)
and /work/SRC/openSUSE:Factory/.python-asttokens.new.1835 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-asttokens"
Sat Dec 3 10:03:11 2022 rev:6 rq:1039428 version:2.1.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asttokens/python-asttokens.changes
2022-09-25 15:34:30.571508257 +0200
+++
/work/SRC/openSUSE:Factory/.python-asttokens.new.1835/python-asttokens.changes
2022-12-03 10:03:12.475097398 +0100
@@ -1,0 +2,7 @@
+Thu Dec 1 22:18:13 UTC 2022 - Yogalakshmi Arunachalam <[email protected]>
+
+- Update to version 2.1.0
+ * Merge pull request #93 from gristlabs/unmarked2
+ ASTText class that doesn't require tokens
+
+-------------------------------------------------------------------
@@ -10,0 +18,10 @@
+-------------------------------------------------------------------
+Fri Sep 23 02:15:13 UTC 2022 - Yogalakshmi Arunachalam <[email protected]>
+
+- Update to 2.0.8
+ * Merge pull request #90 from palfrey/fix-explicit-import
+ * Fix mypy explicit re-export issues
+
+- Update to 2.0.7
+ * Merge pull request #87 from gristlabs/astroid-type-checking
+ * Fix astroid type checking import errors
Old:
----
asttokens-2.0.8.tar.gz
New:
----
asttokens-2.1.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-asttokens.spec ++++++
--- /var/tmp/diff_new_pack.JKrNGR/_old 2022-12-03 10:03:13.175101288 +0100
+++ /var/tmp/diff_new_pack.JKrNGR/_new 2022-12-03 10:03:13.183101332 +0100
@@ -21,7 +21,7 @@
%define skip_python2 1
%define skip_python36 1
Name: python-asttokens
-Version: 2.0.8
+Version: 2.1.0
Release: 0
Summary: Annotate AST trees with source code positions
License: Apache-2.0
++++++ asttokens-2.0.8.tar.gz -> asttokens-2.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/PKG-INFO new/asttokens-2.1.0/PKG-INFO
--- old/asttokens-2.0.8/PKG-INFO 2022-08-15 12:49:38.757367100 +0200
+++ new/asttokens-2.1.0/PKG-INFO 2022-10-29 13:23:38.843023000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asttokens
-Version: 2.0.8
+Version: 2.1.0
Summary: Annotate AST trees with source code positions
Home-page: https://github.com/gristlabs/asttokens
Author: Dmitry Sagalovskiy, Grist Labs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/asttokens/__init__.py
new/asttokens-2.1.0/asttokens/__init__.py
--- old/asttokens-2.0.8/asttokens/__init__.py 2022-08-15 12:49:10.000000000
+0200
+++ new/asttokens-2.1.0/asttokens/__init__.py 2022-10-29 12:14:52.000000000
+0200
@@ -19,6 +19,6 @@
"""
from .line_numbers import LineNumbers
-from .asttokens import ASTTokens
+from .asttokens import ASTText, ASTTokens, supports_tokenless
-__all__ = ['ASTTokens', 'LineNumbers']
+__all__ = ['ASTText', 'ASTTokens', 'LineNumbers', 'supports_tokenless']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/asttokens/asttokens.py
new/asttokens-2.1.0/asttokens/asttokens.py
--- old/asttokens-2.0.8/asttokens/asttokens.py 2022-08-08 16:46:49.000000000
+0200
+++ new/asttokens-2.1.0/asttokens/asttokens.py 2022-10-29 12:14:52.000000000
+0200
@@ -12,23 +12,81 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import abc
import ast
import bisect
-import io
+import sys
import token
-import tokenize
-from .util import Token, match_token, is_non_coding_token,
patched_generate_tokens
+from ast import Module
+from typing import Iterable, Iterator, List, Optional, Tuple, Any, cast,
TYPE_CHECKING, Type
+
import six
-from six.moves import xrange # pylint: disable=redefined-builtin
+from six.moves import xrange # pylint: disable=redefined-builtin
+
from .line_numbers import LineNumbers
-from typing import Callable, Iterator, List, Optional, Tuple, Any,
cast,TYPE_CHECKING
-from ast import Module
+from .util import Token, match_token, is_non_coding_token,
patched_generate_tokens, last_stmt, annotate_fstring_nodes, generate_tokens
+
+if TYPE_CHECKING: # pragma: no cover
+ from .util import AstNode, TokenInfo
+
+
+class ASTTextBase(six.with_metaclass(abc.ABCMeta, object)):
+ def __init__(self, source_text, filename):
+ # type: (Any, str) -> None
+ # FIXME: Strictly, the type of source_text is one of the six string types,
but hard to specify with mypy given
+ #
https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
-if TYPE_CHECKING:
- from .util import AstNode
+ self._filename = filename
+
+ # Decode source after parsing to let Python 2 handle coding declarations.
+ # (If the encoding was not utf-8 compatible, then even if it parses
correctly,
+ # we'll fail with a unicode error here.)
+ source_text = six.ensure_text(source_text)
+ self._text = source_text
+ self._line_numbers = LineNumbers(source_text)
-class ASTTokens(object):
+ @abc.abstractmethod
+ def get_text_positions(self, node, padded):
+ # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+ """
+ Returns two ``(lineno, col_offset)`` tuples for the start and end of the
given node.
+ If the positions can't be determined, or the nodes don't correspond to any
particular text,
+ returns ``(1, 0)`` for both.
+
+ ``padded`` corresponds to the ``padded`` argument to
``ast.get_source_segment()``.
+ This means that if ``padded`` is True, the start position will be adjusted
to include
+ leading whitespace if ``node`` is a multiline statement.
+ """
+ raise NotImplementedError
+
+ def get_text_range(self, node, padded=True):
+ # type: (AstNode, bool) -> Tuple[int, int]
+ """
+ Returns the (startpos, endpos) positions in source text corresponding to
the given node.
+ Returns (0, 0) for nodes (like `Load`) that don't correspond to any
particular text.
+
+ See ``get_text_positions()`` for details on the ``padded`` argument.
+ """
+ start, end = self.get_text_positions(node, padded)
+ return (
+ self._line_numbers.line_to_offset(*start),
+ self._line_numbers.line_to_offset(*end),
+ )
+
+ def get_text(self, node, padded=True):
+ # type: (AstNode, bool) -> str
+ """
+ Returns the text corresponding to the given node.
+ Returns '' for nodes (like `Load`) that don't correspond to any particular
text.
+
+ See ``get_text_positions()`` for details on the ``padded`` argument.
+ """
+ start, end = self.get_text_range(node, padded)
+ return self._text[start: end]
+
+
+class ASTTokens(ASTTextBase, object):
"""
ASTTokens maintains the text of Python code in several forms: as a string,
as line numbers, and
as tokens, and is used to mark and access token and position information.
@@ -47,24 +105,20 @@
If only ``source_text`` is given, you may use ``.mark_tokens(tree)`` to mark
the nodes of an AST
tree created separately.
"""
- def __init__(self, source_text, parse=False, tree=None,
filename='<unknown>'):
- # type: (Any, bool, Optional[Module], str) -> None
- # FIXME: Strictly, the type of source_type is one of the six string types,
but hard to specify with mypy given
- #
https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
- self._filename = filename
- self._tree = ast.parse(source_text, filename) if parse else tree
+ def __init__(self, source_text, parse=False, tree=None,
filename='<unknown>', tokens=None):
+ # type: (Any, bool, Optional[Module], str, Iterable[TokenInfo]) -> None
+ # FIXME: Strictly, the type of source_text is one of the six string types,
but hard to specify with mypy given
+ #
https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
- # Decode source after parsing to let Python 2 handle coding declarations.
- # (If the encoding was not utf-8 compatible, then even if it parses
correctly,
- # we'll fail with a unicode error here.)
- source_text = six.ensure_text(source_text)
+ super(ASTTokens, self).__init__(source_text, filename)
- self._text = source_text
- self._line_numbers = LineNumbers(source_text)
+ self._tree = ast.parse(source_text, filename) if parse else tree
# Tokenize the code.
- self._tokens = list(self._generate_tokens(source_text))
+ if tokens is None:
+ tokens = generate_tokens(self._text)
+ self._tokens = list(self._translate_tokens(tokens))
# Extract the start positions of all tokens, so that we can quickly map
positions to tokens.
self._token_offsets = [tok.startpos for tok in self._tokens]
@@ -72,7 +126,6 @@
if self._tree:
self.mark_tokens(self._tree)
-
def mark_tokens(self, root_node):
# type: (Module) -> None
"""
@@ -85,16 +138,11 @@
from .mark_tokens import MarkTokens # to avoid import loops
MarkTokens(self).visit_tree(root_node)
-
- def _generate_tokens(self, text):
- # type: (str) -> Iterator[Token]
+ def _translate_tokens(self, original_tokens):
+ # type: (Iterable[TokenInfo]) -> Iterator[Token]
"""
- Generates tokens for the given code.
+ Translates the given standard library tokens into our own representation.
"""
- # tokenize.generate_tokens is technically an undocumented API for Python3,
but allows us to use the same API as for
- # Python2. See http://stackoverflow.com/a/4952291/328565.
- # FIXME: Remove cast once https://github.com/python/typeshed/issues/7003
gets fixed
- original_tokens = tokenize.generate_tokens(cast(Callable[[], str],
io.StringIO(text).readline))
for index, tok in enumerate(patched_generate_tokens(original_tokens)):
tok_type, tok_str, start, end, line = tok
yield Token(tok_type, tok_str, start, end, line, index,
@@ -210,28 +258,188 @@
"""
return self.token_range(node.first_token, node.last_token,
include_extra=include_extra)
- def get_text_range(self, node):
- # type: (AstNode) -> Tuple[int, int]
+ def get_text_positions(self, node, padded):
+ # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
"""
- After mark_tokens() has been called, returns the (startpos, endpos)
positions in source text
- corresponding to the given node. Returns (0, 0) for nodes (like `Load`)
that don't correspond
- to any particular text.
+ Returns two ``(lineno, col_offset)`` tuples for the start and end of the
given node.
+ If the positions can't be determined, or the nodes don't correspond to any
particular text,
+ returns ``(1, 0)`` for both.
+
+ ``padded`` corresponds to the ``padded`` argument to
``ast.get_source_segment()``.
+ This means that if ``padded`` is True, the start position will be adjusted
to include
+ leading whitespace if ``node`` is a multiline statement.
"""
if not hasattr(node, 'first_token'):
- return (0, 0)
+ return (1, 0), (1, 0)
- start = node.first_token.startpos
- if any(match_token(t, token.NEWLINE) for t in self.get_tokens(node)):
- # Multi-line nodes would be invalid unless we keep the indentation of
the first node.
- start = self._text.rfind('\n', 0, start) + 1
+ start = node.first_token.start
+ end = node.last_token.end
+ if padded and any(match_token(t, token.NEWLINE) for t in
self.get_tokens(node)):
+ # Set col_offset to 0 to include leading indentation for multiline
statements.
+ start = (start[0], 0)
- return (start, node.last_token.endpos)
+ return start, end
- def get_text(self, node):
- # type: (AstNode) -> str
- """
- After mark_tokens() has been called, returns the text corresponding to the
given node. Returns
- '' for nodes (like `Load`) that don't correspond to any particular text.
- """
- start, end = self.get_text_range(node)
- return self._text[start : end]
+
+class ASTText(ASTTextBase, object):
+ """
+ Supports the same ``get_text*`` methods as ``ASTTokens``,
+ but uses the AST to determine the text positions instead of tokens.
+ This is faster than ``ASTTokens`` as it requires less setup work.
+
+ It also (sometimes) supports nodes inside f-strings, which ``ASTTokens``
doesn't.
+
+ Astroid trees are not supported at all and will raise an error.
+
+ Some node types and/or Python versions are not supported.
+ In these cases the ``get_text*`` methods will fall back to using
``ASTTokens``
+ which incurs the usual setup cost the first time.
+ If you want to avoid this, check ``supports_tokenless(node)`` before calling
``get_text*`` methods.
+ """
+ def __init__(self, source_text, tree=None, filename='<unknown>'):
+ # type: (Any, Optional[Module], str) -> None
+ # FIXME: Strictly, the type of source_text is one of the six string types,
but hard to specify with mypy given
+ #
https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
+
+ if not isinstance(tree, (ast.AST, type(None))):
+ raise NotImplementedError('ASTText only supports AST trees')
+
+ super(ASTText, self).__init__(source_text, filename)
+
+ self._tree = tree
+ if self._tree is not None:
+ annotate_fstring_nodes(self._tree)
+
+ self._asttokens = None # type: Optional[ASTTokens]
+
+ @property
+ def tree(self):
+ # type: () -> Module
+ if self._tree is None:
+ self._tree = ast.parse(self._text, self._filename)
+ annotate_fstring_nodes(self._tree)
+ return self._tree
+
+ @property
+ def asttokens(self):
+ # type: () -> ASTTokens
+ if self._asttokens is None:
+ self._asttokens = ASTTokens(
+ self._text,
+ tree=self.tree,
+ filename=self._filename,
+ )
+ return self._asttokens
+
+ def _get_text_positions_tokenless(self, node, padded):
+ # type: (ast.AST, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+ """
+ Version of ``get_text_positions()`` that doesn't use tokens.
+ """
+ if sys.version_info[:2] < (3, 8):
+ raise AssertionError("This method should only be called internally after
checking supports_tokenless()")
+
+ if isinstance(node, ast.Module):
+ # Modules don't have position info, so just return the range of the
whole text.
+ # The token-using method does something different, but its behavior
seems weird and inconsistent.
+ # For example, in a file with only comments, it only returns the first
line.
+ # It's hard to imagine a case when this matters.
+ return (1, 0), self._line_numbers.offset_to_line(len(self._text))
+
+ if not hasattr(node, 'lineno'):
+ return (1, 0), (1, 0)
+
+ assert node # tell mypy that node is not None, which we allowed up to
here for compatibility
+
+ decorators = getattr(node, 'decorator_list', [])
+ if decorators:
+ # Function/Class definition nodes are marked by AST as starting at
def/class,
+ # not the first decorator. This doesn't match the token-using behavior,
+ # or inspect.getsource(), and just seems weird.
+ start_node = decorators[0]
+ else:
+ start_node = node
+
+ if padded and last_stmt(node).lineno != node.lineno:
+ # Include leading indentation for multiline statements.
+ start_col_offset = 0
+ else:
+ start_col_offset = self._line_numbers.from_utf8_col(start_node.lineno,
start_node.col_offset)
+
+ start = (start_node.lineno, start_col_offset)
+
+ # To match the token-using behaviour, we exclude trailing semicolons and
comments.
+ # This means that for blocks containing multiple statements, we have to
use the last one
+ # instead of the actual node for end_lineno and end_col_offset.
+ end_node = last_stmt(node)
+ end_lineno = cast(int, end_node.end_lineno)
+ end_col_offset = cast(int, end_node.end_col_offset)
+ end_col_offset = self._line_numbers.from_utf8_col(end_lineno,
end_col_offset)
+ end = (end_lineno, end_col_offset)
+
+ return start, end
+
+ def get_text_positions(self, node, padded):
+ # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+ """
+ Returns two ``(lineno, col_offset)`` tuples for the start and end of the
given node.
+ If the positions can't be determined, or the nodes don't correspond to any
particular text,
+ returns ``(1, 0)`` for both.
+
+ ``padded`` corresponds to the ``padded`` argument to
``ast.get_source_segment()``.
+ This means that if ``padded`` is True, the start position will be adjusted
to include
+ leading whitespace if ``node`` is a multiline statement.
+ """
+ if getattr(node, "_broken_positions", None):
+ # This node was marked in util.annotate_fstring_nodes as having
untrustworthy lineno/col_offset.
+ return (1, 0), (1, 0)
+
+ if supports_tokenless(node):
+ return self._get_text_positions_tokenless(node, padded)
+
+ return self.asttokens.get_text_positions(node, padded)
+
+
+# Node types that _get_text_positions_tokenless doesn't support. Only relevant
for Python 3.8+.
+_unsupported_tokenless_types = () # type: Tuple[Type[ast.AST], ...]
+if sys.version_info[:2] >= (3, 8):
+ _unsupported_tokenless_types += (
+ # no lineno
+ ast.arguments, ast.withitem,
+ )
+ if sys.version_info[:2] == (3, 8):
+ _unsupported_tokenless_types += (
+ # _get_text_positions_tokenless works incorrectly for these types due to
bugs in Python 3.8.
+ ast.arg, ast.Starred,
+ # no lineno in 3.8
+ ast.Slice, ast.ExtSlice, ast.Index, ast.keyword,
+ )
+
+
+def supports_tokenless(node=None):
+ # type: (Any) -> bool
+ """
+ Returns True if the Python version and the node (if given) are supported by
+ the ``get_text*`` methods of ``ASTText`` without falling back to
``ASTTokens``.
+ See ``ASTText`` for why this matters.
+
+ The following cases are not supported:
+
+ - Python 3.7 and earlier
+ - PyPy
+ - Astroid nodes (``get_text*`` methods of ``ASTText`` will raise an error)
+ - ``ast.arguments`` and ``ast.withitem``
+ - The following nodes in Python 3.8 only:
+ - ``ast.arg``
+ - ``ast.Starred``
+ - ``ast.Slice``
+ - ``ast.ExtSlice``
+ - ``ast.Index``
+ - ``ast.keyword``
+ """
+ return (
+ isinstance(node, (ast.AST, type(None)))
+ and not isinstance(node, _unsupported_tokenless_types)
+ and sys.version_info[:2] >= (3, 8)
+ and 'pypy' not in sys.version.lower()
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/asttokens/util.py
new/asttokens-2.1.0/asttokens/util.py
--- old/asttokens-2.0.8/asttokens/util.py 2022-08-15 12:49:10.000000000
+0200
+++ new/asttokens-2.1.0/asttokens/util.py 2022-10-29 12:14:52.000000000
+0200
@@ -14,16 +14,17 @@
import ast
import collections
+import io
import sys
import token
import tokenize
from abc import ABCMeta
from ast import Module, expr, AST
-from typing import Callable, Dict, Iterator, List, Optional, Tuple, Union,
cast, Any, TYPE_CHECKING
+from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple,
Union, cast, Any, TYPE_CHECKING
from six import iteritems
-if TYPE_CHECKING:
+if TYPE_CHECKING: # pragma: no cover
from astroid.node_classes import NodeNG
# Type class used to expand out the definition of AST to include fields
added by this library
@@ -36,6 +37,11 @@
AstNode = Union[EnhancedAST, NodeNG]
+ if sys.version_info[0] == 2:
+ TokenInfo = Tuple[int, str, Tuple[int, int], Tuple[int, int], str]
+ else:
+ TokenInfo = tokenize.TokenInfo
+
def token_repr(tok_type, string):
# type: (int, Optional[str]) -> str
@@ -105,8 +111,19 @@
return token_type >= token.N_TOKENS
+def generate_tokens(text):
+ # type: (str) -> Iterator[TokenInfo]
+ """
+ Generates standard library tokens for the given code.
+ """
+ # tokenize.generate_tokens is technically an undocumented API for Python3,
but allows us to use the same API as for
+ # Python2. See http://stackoverflow.com/a/4952291/328565.
+ # FIXME: Remove cast once https://github.com/python/typeshed/issues/7003
gets fixed
+ return tokenize.generate_tokens(cast(Callable[[], str],
io.StringIO(text).readline))
+
+
def iter_children_func(node):
- # type: (Module) -> Callable
+ # type: (AST) -> Callable
"""
Returns a function which yields all direct children of a AST node,
skipping children that are singleton nodes.
@@ -249,7 +266,7 @@
def walk(node):
- # type: (Module) -> Iterator[Union[Module, AstNode]]
+ # type: (AST) -> Iterator[Union[Module, AstNode]]
"""
Recursively yield all descendant nodes in the tree starting at ``node``
(including ``node``
itself), using depth-first pre-order traversal (yieling parents before their
children).
@@ -320,11 +337,11 @@
# Python 2 doesn't support non-ASCII identifiers, and making the real
patched_generate_tokens support Python 2
# means working with raw tuples instead of tokenize.TokenInfo namedtuples.
def patched_generate_tokens(original_tokens):
- # type: (Any) -> Any
- return original_tokens
+ # type: (Iterable[TokenInfo]) -> Iterator[TokenInfo]
+ return iter(original_tokens)
else:
def patched_generate_tokens(original_tokens):
- # type: (Iterator[tokenize.TokenInfo]) -> Iterator[tokenize.TokenInfo]
+ # type: (Iterable[TokenInfo]) -> Iterator[TokenInfo]
"""
Fixes tokens yielded by `tokenize.generate_tokens` to handle more
non-ASCII characters in identifiers.
Workaround for https://github.com/python/cpython/issues/68382.
@@ -361,3 +378,83 @@
line=group[0].line,
)
]
+
+
+def last_stmt(node):
+ # type: (ast.AST) -> ast.AST
+ """
+ If the given AST node contains multiple statements, return the last one.
+ Otherwise, just return the node.
+ """
+ child_stmts = [
+ child for child in ast.iter_child_nodes(node)
+ if isinstance(child, (ast.stmt, ast.excepthandler))
+ ]
+ if child_stmts:
+ return last_stmt(child_stmts[-1])
+ return node
+
+
+if sys.version_info[:2] >= (3, 8):
+ from functools import lru_cache
+
+ @lru_cache(maxsize=None)
+ def fstring_positions_work():
+ # type: () -> bool
+ """
+ The positions attached to nodes inside f-string FormattedValues have some
bugs
+ that were fixed in Python 3.9.7 in
https://github.com/python/cpython/pull/27729.
+ This checks for those bugs more concretely without relying on the Python
version.
+ Specifically this checks:
+ - Values with a format spec or conversion
+ - Repeated (i.e. identical-looking) expressions
+ - Multiline f-strings implicitly concatenated.
+ """
+ source = """(
+ f"a {b}{b} c {d!r} e {f:g} h {i:{j}} k {l:{m:n}}"
+ f"a {b}{b} c {d!r} e {f:g} h {i:{j}} k {l:{m:n}}"
+ f"{x + y + z} {x} {y} {z} {z} {z!a} {z:z}"
+ )"""
+ tree = ast.parse(source)
+ name_nodes = [node for node in ast.walk(tree) if isinstance(node,
ast.Name)]
+ name_positions = [(node.lineno, node.col_offset) for node in name_nodes]
+ positions_are_unique = len(set(name_positions)) == len(name_positions)
+ correct_source_segments = all(
+ ast.get_source_segment(source, node) == node.id
+ for node in name_nodes
+ )
+ return positions_are_unique and correct_source_segments
+
+ def annotate_fstring_nodes(tree):
+ # type: (ast.AST) -> None
+ """
+ Add a special attribute `_broken_positions` to nodes inside f-strings
+ if the lineno/col_offset cannot be trusted.
+ """
+ for joinedstr in walk(tree):
+ if not isinstance(joinedstr, ast.JoinedStr):
+ continue
+ for part in joinedstr.values:
+ # The ast positions of the FormattedValues/Constant nodes span the
full f-string, which is weird.
+ setattr(part, '_broken_positions', True) # use setattr for mypy
+
+ if isinstance(part, ast.FormattedValue):
+ if not fstring_positions_work():
+ for child in walk(part.value):
+ setattr(child, '_broken_positions', True)
+
+ if part.format_spec: # this is another JoinedStr
+ # Again, the standard positions span the full f-string.
+ setattr(part.format_spec, '_broken_positions', True)
+ # Recursively handle this inner JoinedStr in the same way.
+ # While this is usually automatic for other nodes,
+ # the children of f-strings are explicitly excluded in
iter_children_ast.
+ annotate_fstring_nodes(part.format_spec)
+else:
+ def fstring_positions_work():
+ # type: () -> bool
+ return False
+
+ def annotate_fstring_nodes(_tree):
+ # type: (ast.AST) -> None
+ pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/asttokens/version.py
new/asttokens-2.1.0/asttokens/version.py
--- old/asttokens-2.0.8/asttokens/version.py 2022-08-15 12:49:38.000000000
+0200
+++ new/asttokens-2.1.0/asttokens/version.py 2022-10-29 13:23:38.000000000
+0200
@@ -1 +1 @@
-__version__ = "2.0.8"
+__version__ = "2.1.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/asttokens.egg-info/PKG-INFO
new/asttokens-2.1.0/asttokens.egg-info/PKG-INFO
--- old/asttokens-2.0.8/asttokens.egg-info/PKG-INFO 2022-08-15
12:49:38.000000000 +0200
+++ new/asttokens-2.1.0/asttokens.egg-info/PKG-INFO 2022-10-29
13:23:38.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: asttokens
-Version: 2.0.8
+Version: 2.1.0
Summary: Annotate AST trees with source code positions
Home-page: https://github.com/gristlabs/asttokens
Author: Dmitry Sagalovskiy, Grist Labs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/asttokens.egg-info/SOURCES.txt
new/asttokens-2.1.0/asttokens.egg-info/SOURCES.txt
--- old/asttokens-2.0.8/asttokens.egg-info/SOURCES.txt 2022-08-15
12:49:38.000000000 +0200
+++ new/asttokens-2.1.0/asttokens.egg-info/SOURCES.txt 2022-10-29
13:23:38.000000000 +0200
@@ -33,6 +33,7 @@
tests/test_asttokens.py
tests/test_line_numbers.py
tests/test_mark_tokens.py
+tests/test_tokenless.py
tests/test_util.py
tests/tools.py
tests/testdata/README.md
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/tests/test_asttokens.py
new/asttokens-2.1.0/tests/test_asttokens.py
--- old/asttokens-2.0.8/tests/test_asttokens.py 2022-08-01 00:18:22.000000000
+0200
+++ new/asttokens-2.1.0/tests/test_asttokens.py 2022-10-29 12:14:52.000000000
+0200
@@ -9,10 +9,9 @@
class TestASTTokens(unittest.TestCase):
- def test_tokenizing(self):
- # Test that we produce meaningful tokens on initialization.
+ def assertTokenizing(self, generate_tokens):
source = "import re # comment\n\nfoo = 'bar'\n"
- atok = asttokens.ASTTokens(source)
+ atok = asttokens.ASTTokens(source, tokens=generate_tokens(source))
self.assertEqual(atok.text, source)
self.assertEqual([str(t) for t in atok.tokens], [
"NAME:'import'",
@@ -33,6 +32,28 @@
self.assertEqual(atok.tokens[5].startpos, 22)
self.assertEqual(atok.tokens[5].endpos, 25)
+ def test_tokenizing(self):
+ # Test that we produce meaningful tokens on initialization.
+ self.assertTokenizing(generate_tokens=lambda x: None)
+
+ def test_given_existing_tokens(self):
+ # type: () -> None
+ # Test that we process a give list of tokens on initialization.
+
+ self.was_called = False
+
+ def generate_tokens(source):
+ def tokens_iter():
+ # force nonlocal into scope
+ for token in asttokens.util.generate_tokens(source):
+ yield token
+ self.was_called = True
+ return tokens_iter()
+
+ self.assertTokenizing(generate_tokens)
+
+ self.assertTrue(self.was_called, "Should have used tokens from given
iterable")
+
def test_token_methods(self):
# Test the methods that deal with tokens: prev/next_token, get_token,
get_token_from_offset.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/tests/test_mark_tokens.py
new/asttokens-2.1.0/tests/test_mark_tokens.py
--- old/asttokens-2.0.8/tests/test_mark_tokens.py 2022-08-15
12:49:10.000000000 +0200
+++ new/asttokens-2.1.0/tests/test_mark_tokens.py 2022-10-29
12:14:52.000000000 +0200
@@ -31,7 +31,7 @@
def create_mark_checker(self, source, verify=True):
atok = self.create_asttokens(source)
- checker = tools.MarkChecker(atok)
+ checker = tools.MarkChecker(atok, self.is_astroid_test)
# The last token should always be an ENDMARKER
# None of the nodes should contain that token
@@ -442,6 +442,16 @@
self.assertEqual(m.view_nodes_at(2, 4), {'Name:x', 'Subscript:x[4]'})
if not six.PY2:
+ def test_bad_tokenless_types(self):
+ # Cases where _get_text_positions_tokenless is incorrect in 3.8.
+ source = textwrap.dedent("""
+ def foo(*, name: str): # keyword-only argument with type annotation
+ pass
+
+ f(*(x)) # ast.Starred with parentheses
+ """)
+ self.create_mark_checker(source)
+
def test_return_annotation(self):
# See
https://bitbucket.org/plas/thonny/issues/9/range-marker-crashes-on-function-return
source = textwrap.dedent("""
@@ -554,11 +564,14 @@
17
); d # comment1; comment2
if 2: a; b; # comment3
+if a:
+ if b: c; d # comment4
"""
m = self.create_mark_checker(source)
self.assertEqual(
[m.atok.get_text(n) for n in m.all_nodes if util.is_stmt(n)],
- ['a', 'b', 'c(\n 17\n)', 'd', 'if 2: a; b', 'a', 'b'])
+ ['a', 'b', 'c(\n 17\n)', 'd', 'if 2: a; b', 'a', 'b',
+ 'if a:\n if b: c; d', 'if b: c; d', 'c', 'd'])
def test_complex_numbers(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/tests/test_tokenless.py
new/asttokens-2.1.0/tests/test_tokenless.py
--- old/asttokens-2.0.8/tests/test_tokenless.py 1970-01-01 01:00:00.000000000
+0100
+++ new/asttokens-2.1.0/tests/test_tokenless.py 2022-10-29 12:14:52.000000000
+0200
@@ -0,0 +1,129 @@
+import ast
+import sys
+import unittest
+
+import astroid
+
+from asttokens import ASTText, supports_tokenless
+from asttokens.util import fstring_positions_work
+
+source = """
+x = 1
+if x > 0:
+ for i in range(10):
+ print(i)
+else:
+ print('negative')
+
+def foo(bar):
+ pass
+
+print(f"{xx + 22} is negative {1.23:.2f} {'a'!r} {yy =} {aa:{bb}}")
+
+import a
+import b as c, d.e as f
+from foo.bar import baz as spam
+"""
+
+fstring_node_dumps = [
+ ast.dump(ast.parse(s).body[0].value) # type: ignore
+ for s in ["xx", "yy", "aa", "bb", "xx + 22", "22", "1.23", "'a'"]
+]
+
+
+def is_fstring_internal_node(node):
+ """
+ Returns True if the given node is an internal node in an f-string.
+ Only applies for nodes parsed from the source above.
+ """
+ return ast.dump(node) in fstring_node_dumps
+
+
+def is_fstring_format_spec(node):
+ """
+ Returns True if the given node is a format specifier in an f-string.
+ Only applies for nodes parsed from the source above.
+ """
+ return (
+ isinstance(node, ast.JoinedStr)
+ and len(node.values) == 1
+ and (
+ (
+ isinstance(node.values[0], ast.Str)
+ and node.values[0].value in ['.2f']
+ ) or (
+ isinstance(node.values[0], ast.FormattedValue)
+ and isinstance(node.values[0].value, ast.Name)
+ and node.values[0].value.id == 'bb'
+ )
+ )
+ )
+
+
[email protected](supports_tokenless(), "Python version does not support
not using tokens")
+class TestTokenless(unittest.TestCase):
+ def test_get_text_tokenless(self):
+ atok = ASTText(source)
+
+ for node in ast.walk(atok.tree):
+ if not isinstance(node, (ast.arguments, ast.arg)):
+ self.check_node(atok, node)
+ self.assertTrue(supports_tokenless(node), node)
+
+ # Check that we didn't need to fall back to using tokens
+ self.assertIsNone(atok._asttokens)
+
+ has_tokens = False
+ for node in ast.walk(atok.tree):
+ self.check_node(atok, node)
+
+ if isinstance(node, ast.arguments):
+ has_tokens = True
+
+ self.assertEqual(atok._asttokens is not None, has_tokens)
+
+ # Now we have started using tokens as fallback
+ self.assertIsNotNone(atok._asttokens)
+ self.assertTrue(has_tokens)
+
+ def check_node(self, atok, node):
+ if not hasattr(node, 'lineno'):
+ self.assertEqual(ast.get_source_segment(source, node), None)
+ atok_text = atok.get_text(node)
+ if not isinstance(node, (ast.arg, ast.arguments)):
+ self.assertEqual(atok_text, source if isinstance(node, ast.Module)
else '', node)
+ return
+
+ for padded in [True, False]:
+ ast_text = ast.get_source_segment(source, node, padded=padded)
+ atok_text = atok.get_text(node, padded=padded)
+ if ast_text:
+ if (
+ ast_text.startswith("f") and isinstance(node, (ast.Str,
ast.FormattedValue))
+ or is_fstring_format_spec(node)
+ or (not fstring_positions_work() and is_fstring_internal_node(node))
+ ):
+ self.assertEqual(atok_text, "", node)
+ else:
+ self.assertEqual(atok_text, ast_text, node)
+ self.assertEqual(
+ atok.get_text_positions(node, padded=False),
+ (
+ (node.lineno, node.col_offset),
+ (node.end_lineno, node.end_col_offset),
+ ),
+ )
+
+ def test_lazy_asttext_astroid_errors(self):
+ builder = astroid.builder.AstroidBuilder()
+ tree = builder.string_build(source)
+ with self.assertRaises(NotImplementedError):
+ ASTText(source, tree)
+
+
+class TestFstringPositionsWork(unittest.TestCase):
+ def test_fstring_positions_work(self):
+ self.assertEqual(
+ fstring_positions_work() and supports_tokenless(),
+ sys.version_info >= (3, 9, 7),
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/tests/tools.py
new/asttokens-2.1.0/tests/tools.py
--- old/asttokens-2.0.8/tests/tools.py 2021-09-30 21:43:20.000000000 +0200
+++ new/asttokens-2.1.0/tests/tools.py 2022-10-29 12:14:52.000000000 +0200
@@ -1,11 +1,12 @@
from __future__ import unicode_literals, print_function
+import ast
import io
import os
import re
import sys
-from asttokens import util
+from asttokens import util, supports_tokenless, ASTText
def get_fixture_path(*path_parts):
@@ -35,9 +36,11 @@
"""
Helper tool to parse and mark an AST tree, with useful methods for verifying
it.
"""
- def __init__(self, atok):
+ def __init__(self, atok, is_astroid_test):
self.atok = atok
self.all_nodes = collect_nodes_preorder(self.atok.tree)
+ if not is_astroid_test:
+ self.atext = ASTText(atok.text, atok.tree, atok.filename)
def get_nodes_at(self, line, col):
"""Returns all nodes that start with the token at the given position."""
@@ -69,8 +72,17 @@
number of nodes that were tested this way.
"""
test_case.longMessage = True
+
+ if supports_tokenless() and not test_case.is_astroid_test:
+ num_supported = sum(supports_tokenless(n) for n in self.all_nodes)
+ num_nodes = len(self.all_nodes)
+ test_case.assertGreater(num_supported / num_nodes, 0.5, (num_supported,
num_nodes))
+
tested_nodes = 0
for node in self.all_nodes:
+ text = self.atok.get_text(node)
+ self.check_get_text_tokenless(node, test_case, text)
+
if not (
util.is_stmt(node) or
util.is_expr(node) or
@@ -81,8 +93,6 @@
if util.is_slice(node) and test_case.is_astroid_test:
continue
- text = self.atok.get_text(node)
-
# await is not allowed outside async functions below 3.7
# parsing again would give a syntax error
if 'await' in text and 'async def' not in text and sys.version_info <
(3, 7):
@@ -111,6 +121,43 @@
return tested_nodes
+ def check_get_text_tokenless(self, node, test_case, text):
+ """
+ Check that `text` (returned from get_text()) usually returns the same text
+ whether from `ASTTokens` or `ASTText`.
+ """
+
+ if test_case.is_astroid_test or not supports_tokenless():
+ return
+
+ text_tokenless = self.atext.get_text(node)
+ if isinstance(node, ast.alias):
+ self._check_alias_tokenless(node, test_case, text_tokenless)
+ elif isinstance(node, ast.Module):
+ test_case.assertEqual(text_tokenless, self.atext._text)
+ elif supports_tokenless(node):
+ has_lineno = hasattr(node, 'lineno')
+ test_case.assertEqual(has_lineno, text_tokenless != '')
+ if has_lineno:
+ test_case.assertEqual(text, text_tokenless, ast.dump(node))
+ else:
+ # _get_text_positions_tokenless can't work with nodes without lineno.
+ # Double-check that such nodes are unusual.
+ test_case.assertFalse(util.is_stmt(node) or util.is_expr(node))
+ with test_case.assertRaises(SyntaxError, msg=(text, ast.dump(node))):
+ test_case.parse_snippet(text, node)
+
+ def _check_alias_tokenless(self, node, test_case, text):
+ if sys.version_info < (3, 10):
+ # Before 3.10, aliases don't have position information
+ test_case.assertEqual(text, '')
+ # For 3.10+, ASTTokens.get_text often returns the wrong value for aliases.
+ # So to verify ASTText.get_text, we instead check the general form.
+ elif node.asname:
+ test_case.assertEqual(text.split(), [node.name, 'as', node.asname])
+ else:
+ test_case.assertEqual(text, node.name)
+
def repr_tree(node):
"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/asttokens-2.0.8/tox.ini new/asttokens-2.1.0/tox.ini
--- old/asttokens-2.0.8/tox.ini 2022-08-08 16:30:50.000000000 +0200
+++ new/asttokens-2.1.0/tox.ini 2022-10-29 12:14:52.000000000 +0200
@@ -7,7 +7,7 @@
envlist = py{27,35,36,37,38,39,310,py,py3}
[testenv]
-commands = pytest
+commands = pytest {posargs}
deps =
.[test]
passenv =