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 2026-08-21 16:50:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-asttokens (Old)
 and      /work/SRC/openSUSE:Factory/.python-asttokens.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-asttokens"

Fri Aug 21 16:50:16 2026 rev:16 rq:1372222 version:3.0.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-asttokens/python-asttokens.changes        
2025-11-18 15:30:08.490881748 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-asttokens.new.1258/python-asttokens.changes  
    2026-08-21 16:50:56.495883916 +0200
@@ -1,0 +2,10 @@
+Thu Aug 20 09:06:20 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.0.2:
+  * Modernize type annotations
+  * Fix pypy CI by skipping redundant mypy runs on it
+  * Fix IndexError on source mixing lone CR with LF line endings
+  * Release modernization, and publish to PyPI from CI via
+    trusted publishing
+
+-------------------------------------------------------------------

Old:
----
  asttokens-3.0.1.tar.gz

New:
----
  asttokens-3.0.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-asttokens.spec ++++++
--- /var/tmp/diff_new_pack.J3e8DB/_old  2026-08-21 16:50:57.184908662 +0200
+++ /var/tmp/diff_new_pack.J3e8DB/_new  2026-08-21 16:50:57.187908769 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-asttokens
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 # Copyright (c) 2019-2021 Malcolm J Lewis <[email protected]>
 #
 # All modifications and additions to the file contributed by third parties
@@ -19,7 +19,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-asttokens
-Version:        3.0.1
+Version:        3.0.2
 Release:        0
 Summary:        Annotate AST trees with source code positions
 License:        Apache-2.0

++++++ asttokens-3.0.1.tar.gz -> asttokens-3.0.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/.github/workflows/build-and-test.yml 
new/asttokens-3.0.2/.github/workflows/build-and-test.yml
--- old/asttokens-3.0.1/.github/workflows/build-and-test.yml    2025-11-15 
17:04:32.000000000 +0100
+++ new/asttokens-3.0.2/.github/workflows/build-and-test.yml    2026-07-12 
05:31:23.000000000 +0200
@@ -64,8 +64,10 @@
           pip install .[test] 'astroid${{ matrix.astroid-version }}'
 
       - name: Mypy testing
+        # Skip testing mypy on pypy since mypy no longer installs there, and
+        # type-checking on pypy is anyway redundant with CPython jobs.
+        if: ${{ !startsWith(matrix.python-version, 'pypy') }}
         run: |
-          # Not an exact mypy version, as we need 0.942 for pypy-3.8 support, 
but it's not available on 3.5
           pip install "mypy>=1.10"
           python -m mypy asttokens tests/*.py
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/.github/workflows/publish.yml 
new/asttokens-3.0.2/.github/workflows/publish.yml
--- old/asttokens-3.0.1/.github/workflows/publish.yml   1970-01-01 
01:00:00.000000000 +0100
+++ new/asttokens-3.0.2/.github/workflows/publish.yml   2026-07-12 
05:31:23.000000000 +0200
@@ -0,0 +1,22 @@
+name: Publish to PyPI
+
+on:
+  release:
+    types: [published]
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    environment: pypi
+    permissions:
+      id-token: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0  # setuptools_scm derives the version from git tags
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.x"
+      - run: pip install build
+      - run: python -m build
+      - uses: pypa/gh-action-pypi-publish@release/v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/Makefile new/asttokens-3.0.2/Makefile
--- old/asttokens-3.0.1/Makefile        2024-11-30 05:46:25.000000000 +0100
+++ new/asttokens-3.0.2/Makefile        1970-01-01 01:00:00.000000000 +0100
@@ -1,21 +0,0 @@
-dist:
-       @echo Build python distribution
-       @echo "(If no 'build' module, install with 'python -m pip install build 
setuptools_scm')"
-       python -m build
-
-publish:
-       @echo "Publish to PyPI at https://pypi.python.org/pypi/asttokens/";
-       @VER=`python -c 'import setuptools_scm; 
print(setuptools_scm.get_version())'`; \
-       echo "Version in setup.py is $$VER"; \
-       echo "Git tag is `git describe --tags`"; \
-       echo "Run this manually: twine upload dist/asttokens-$$VER*"
-
-docs:
-       @echo Build documentation in docs/_build/html
-       source env/bin/activate ; PYTHONPATH=$(abspath .) $(MAKE) -C docs html
-
-clean:
-       python setup.py clean
-       source env/bin/activate ; PYTHONPATH=$(abspath .) $(MAKE) -C docs clean
-
-.PHONY: dist publish docs clean
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/PKG-INFO new/asttokens-3.0.2/PKG-INFO
--- old/asttokens-3.0.1/PKG-INFO        2025-11-15 17:13:26.206192000 +0100
+++ new/asttokens-3.0.2/PKG-INFO        2026-07-12 05:31:31.914345300 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: asttokens
-Version: 3.0.1
+Version: 3.0.2
 Summary: Annotate AST trees with source code positions
 Home-page: https://github.com/gristlabs/asttokens
 Author: Dmitry Sagalovskiy, Grist Labs
@@ -119,3 +119,23 @@
 4. Run tests across all supported interpreters with the ``tox`` command. You 
will need to have the interpreters installed separately. We recommend ``pyenv`` 
for that. Use ``tox -p auto`` to run the tests in parallel.
 5. By default certain tests which take a very long time to run are skipped, 
but they are run in CI.
    These are marked using the ``pytest`` marker ``slow`` and can be run on 
their own with ``pytest -m slow`` or as part of the full suite with ``pytest -m 
''``.
+
+Documentation
+-------------
+
+Documentation at https://asttokens.readthedocs.io/ is built by Read the Docs 
automatically on
+every push, from the sources in ``docs/`` (configured in 
``.readthedocs.yaml``). To preview
+changes locally::
+
+    pip install -r docs/requirements.txt
+    sphinx-build -M html docs docs/_build
+
+Release
+-------
+
+The version is derived from the git tag by ``setuptools_scm``; there is no 
version to update in
+the source. To release, draft a new release at 
https://github.com/gristlabs/asttokens/releases
+with a new tag named ``vX.Y.Z``, and use "Generate release notes" to fill in 
the description.
+Publishing the release triggers the ``publish.yml`` workflow, which builds the 
package and
+uploads it to PyPI via `trusted publishing
+<https://docs.pypi.org/trusted-publishers/>`_.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/README.rst 
new/asttokens-3.0.2/README.rst
--- old/asttokens-3.0.1/README.rst      2025-11-03 06:34:32.000000000 +0100
+++ new/asttokens-3.0.2/README.rst      2026-07-12 05:31:23.000000000 +0200
@@ -80,3 +80,23 @@
 4. Run tests across all supported interpreters with the ``tox`` command. You 
will need to have the interpreters installed separately. We recommend ``pyenv`` 
for that. Use ``tox -p auto`` to run the tests in parallel.
 5. By default certain tests which take a very long time to run are skipped, 
but they are run in CI.
    These are marked using the ``pytest`` marker ``slow`` and can be run on 
their own with ``pytest -m slow`` or as part of the full suite with ``pytest -m 
''``.
+
+Documentation
+-------------
+
+Documentation at https://asttokens.readthedocs.io/ is built by Read the Docs 
automatically on
+every push, from the sources in ``docs/`` (configured in 
``.readthedocs.yaml``). To preview
+changes locally::
+
+    pip install -r docs/requirements.txt
+    sphinx-build -M html docs docs/_build
+
+Release
+-------
+
+The version is derived from the git tag by ``setuptools_scm``; there is no 
version to update in
+the source. To release, draft a new release at 
https://github.com/gristlabs/asttokens/releases
+with a new tag named ``vX.Y.Z``, and use "Generate release notes" to fill in 
the description.
+Publishing the release triggers the ``publish.yml`` workflow, which builds the 
package and
+uploads it to PyPI via `trusted publishing
+<https://docs.pypi.org/trusted-publishers/>`_.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens/asttokens.py 
new/asttokens-3.0.2/asttokens/asttokens.py
--- old/asttokens-3.0.1/asttokens/asttokens.py  2025-11-15 16:46:22.000000000 
+0100
+++ new/asttokens-3.0.2/asttokens/asttokens.py  2026-07-12 05:31:23.000000000 
+0200
@@ -18,17 +18,14 @@
 import sys
 import token
 from ast import Module
-from typing import Iterable, Iterator, List, Optional, Tuple, Any, cast, 
TYPE_CHECKING
+from typing import Iterable, Iterator, List, Optional, Tuple, Any, cast
 
 from .line_numbers import LineNumbers
 from .util import (
-  Token, match_token, is_non_coding_token, patched_generate_tokens, last_stmt,
+  AstNode, Token, TokenInfo, match_token, is_non_coding_token, 
patched_generate_tokens, last_stmt,
   annotate_fstring_nodes, generate_tokens, is_module, is_stmt
 )
 
-if TYPE_CHECKING:  # pragma: no cover
-  from .util import AstNode, TokenInfo
-
 
 class ASTTextBase(metaclass=abc.ABCMeta):
   def __init__(self, source_text: str, filename: str) -> None:
@@ -43,8 +40,9 @@
     self._line_numbers = LineNumbers(source_text)
 
   @abc.abstractmethod
-  def get_text_positions(self, node, padded):
-    # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+  def get_text_positions(
+    self, node: AstNode, padded: 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,
@@ -56,8 +54,7 @@
     """
     raise NotImplementedError  # pragma: no cover
 
-  def get_text_range(self, node, padded=True):
-    # type: (AstNode, bool) -> Tuple[int, int]
+  def get_text_range(self, node: AstNode, padded: bool = True) -> 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.
@@ -70,8 +67,7 @@
       self._line_numbers.line_to_offset(*end),
     )
 
-  def get_text(self, node, padded=True):
-    # type: (AstNode, bool) -> str
+  def get_text(self, node: AstNode, padded: bool = True) -> str:
     """
     Returns the text corresponding to the given node.
     Returns '' for nodes (like `Load`) that don't correspond to any particular 
text.
@@ -102,9 +98,15 @@
   tree created separately.
   """
 
-  def __init__(self, source_text, parse=False, tree=None, 
filename='<unknown>', tokens=None):
-    # type: (Any, bool, Optional[Module], str, Optional[Iterable[TokenInfo]]) 
-> None
-    super(ASTTokens, self).__init__(source_text, filename)
+  def __init__(
+    self,
+    source_text: Any,
+    parse: bool = False,
+    tree: Optional[Module] = None,
+    filename: str = '<unknown>',
+    tokens: Optional[Iterable[TokenInfo]] = None
+  ) -> None:
+    super().__init__(source_text, filename)
 
     self._tree = ast.parse(source_text, filename) if parse else tree
 
@@ -119,8 +121,7 @@
     if self._tree:
       self.mark_tokens(self._tree)
 
-  def mark_tokens(self, root_node):
-    # type: (Module) -> None
+  def mark_tokens(self, root_node: Module) -> None:
     """
     Given the root of the AST or Astroid tree produced from source_text, 
visits all nodes marking
     them with token and position information by adding ``.first_token`` and
@@ -131,8 +132,7 @@
     from .mark_tokens import MarkTokens  # to avoid import loops
     MarkTokens(self).visit_tree(root_node)
 
-  def _translate_tokens(self, original_tokens):
-    # type: (Iterable[TokenInfo]) -> Iterator[Token]
+  def _translate_tokens(self, original_tokens: Iterable[TokenInfo]) -> 
Iterator[Token]:
     """
     Translates the given standard library tokens into our own representation.
     """
@@ -143,39 +143,33 @@
                   self._line_numbers.line_to_offset(end[0], end[1]))
 
   @property
-  def text(self):
-    # type: () -> str
+  def text(self) -> str:
     """The source code passed into the constructor."""
     return self._text
 
   @property
-  def tokens(self):
-    # type: () -> List[Token]
+  def tokens(self) -> List[Token]:
     """The list of tokens corresponding to the source code from the 
constructor."""
     return self._tokens
 
   @property
-  def tree(self):
-    # type: () -> Optional[Module]
+  def tree(self) -> Optional[Module]:
     """The root of the AST tree passed into the constructor or parsed from the 
source code."""
     return self._tree
 
   @property
-  def filename(self):
-    # type: () -> str
+  def filename(self) -> str:
     """The filename that was parsed"""
     return self._filename
 
-  def get_token_from_offset(self, offset):
-    # type: (int) -> Token
+  def get_token_from_offset(self, offset: int) -> Token:
     """
     Returns the token containing the given character offset (0-based position 
in source text),
     or the preceeding token if the position is between tokens.
     """
     return self._tokens[bisect.bisect(self._token_offsets, offset) - 1]
 
-  def get_token(self, lineno, col_offset):
-    # type: (int, int) -> Token
+  def get_token(self, lineno: int, col_offset: int) -> Token:
     """
     Returns the token containing the given (lineno, col_offset) position, or 
the preceeding token
     if the position is between tokens.
@@ -185,15 +179,13 @@
     # but isn't explicit.
     return 
self.get_token_from_offset(self._line_numbers.line_to_offset(lineno, 
col_offset))
 
-  def get_token_from_utf8(self, lineno, col_offset):
-    # type: (int, int) -> Token
+  def get_token_from_utf8(self, lineno: int, col_offset: int) -> Token:
     """
     Same as get_token(), but interprets col_offset as a UTF8 offset, which is 
what `ast` uses.
     """
     return self.get_token(lineno, self._line_numbers.from_utf8_col(lineno, 
col_offset))
 
-  def next_token(self, tok, include_extra=False):
-    # type: (Token, bool) -> Token
+  def next_token(self, tok: Token, include_extra: bool = False) -> Token:
     """
     Returns the next token after the given one. If include_extra is True, 
includes non-coding
     tokens from the tokenize module, such as NL and COMMENT.
@@ -204,8 +196,7 @@
         i += 1
     return self._tokens[i]
 
-  def prev_token(self, tok, include_extra=False):
-    # type: (Token, bool) -> Token
+  def prev_token(self, tok: Token, include_extra: bool = False) -> Token:
     """
     Returns the previous token before the given one. If include_extra is True, 
includes non-coding
     tokens from the tokenize module, such as NL and COMMENT.
@@ -216,8 +207,9 @@
         i -= 1
     return self._tokens[i]
 
-  def find_token(self, start_token, tok_type, tok_str=None, reverse=False):
-    # type: (Token, int, Optional[str], bool) -> Token
+  def find_token(
+    self, start_token: Token, tok_type: int, tok_str: Optional[str] = None, 
reverse: bool = False
+  ) -> Token:
     """
     Looks for the first token, starting at start_token, that matches tok_type 
and, if given, the
     token string. Searches backwards if reverse is True. Returns ENDMARKER 
token if not found (you
@@ -229,12 +221,12 @@
       t = advance(t, include_extra=True)
     return t
 
-  def token_range(self,
-                  first_token,  # type: Token
-                  last_token,  # type: Token
-                  include_extra=False,  # type: bool
-                  ):
-    # type: (...) -> Iterator[Token]
+  def token_range(
+    self,
+    first_token: Token,
+    last_token: Token,
+    include_extra: bool = False,
+  ) -> Iterator[Token]:
     """
     Yields all tokens in order from first_token through and including 
last_token. If
     include_extra is True, includes non-coding tokens such as tokenize.NL and 
.COMMENT.
@@ -243,16 +235,14 @@
       if include_extra or not is_non_coding_token(self._tokens[i].type):
         yield self._tokens[i]
 
-  def get_tokens(self, node, include_extra=False):
-    # type: (AstNode, bool) -> Iterator[Token]
+  def get_tokens(self, node: AstNode, include_extra: bool = False) -> 
Iterator[Token]:
     """
     Yields all tokens making up the given node. If include_extra is True, 
includes non-coding
     tokens such as tokenize.NL and .COMMENT.
     """
     return self.token_range(node.first_token, node.last_token, 
include_extra=include_extra)
 
-  def get_text_positions(self, node, padded):
-    # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+  def get_text_positions(self, node: AstNode, padded: 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,
@@ -287,27 +277,24 @@
   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
-    super(ASTText, self).__init__(source_text, filename)
+  def __init__(self, source_text: Any, tree: Optional[Module] = None, 
filename: str = '<unknown>') -> None:
+    super().__init__(source_text, filename)
 
     self._tree = tree
     if self._tree is not None:
       annotate_fstring_nodes(self._tree)
 
-    self._asttokens = None  # type: Optional[ASTTokens]
+    self._asttokens: Optional[ASTTokens] = None
 
   @property
-  def tree(self):
-    # type: () -> Module
+  def tree(self) -> 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
+  def asttokens(self) -> ASTTokens:
     if self._asttokens is None:
       self._asttokens = ASTTokens(
           self._text,
@@ -316,8 +303,9 @@
       )
     return self._asttokens
 
-  def _get_text_positions_tokenless(self, node, padded):
-    # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+  def _get_text_positions_tokenless(
+    self, node: AstNode, padded: bool
+  ) -> Tuple[Tuple[int, int], Tuple[int, int]]:
     """
     Version of ``get_text_positions()`` that doesn't use tokens.
     """
@@ -382,8 +370,7 @@
 
     return start, end
 
-  def get_text_positions(self, node, padded):
-    # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
+  def get_text_positions(self, node: AstNode, padded: 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,
@@ -405,7 +392,7 @@
 
 # Node types that _get_text_positions_tokenless doesn't support.
 # These initial values are missing lineno.
-_unsupported_tokenless_types = ("arguments", "Arguments", "withitem")  # type: 
Tuple[str, ...]
+_unsupported_tokenless_types: Tuple[str, ...] = ("arguments", "Arguments", 
"withitem")
 if sys.version_info[:2] == (3, 8):
   # _get_text_positions_tokenless works incorrectly for these types due to 
bugs in Python 3.8.
   _unsupported_tokenless_types += ("arg", "Starred")
@@ -413,8 +400,7 @@
   _unsupported_tokenless_types += ("Slice", "ExtSlice", "Index", "keyword")
 
 
-def supports_tokenless(node=None):
-  # type: (Any) -> bool
+def supports_tokenless(node: Any = None) -> 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``.
@@ -440,10 +426,8 @@
       and not (
         # astroid nodes
         not isinstance(node, ast.AST) and node is not None and (
-          (
             type(node).__name__ == "AssignName"
             and type(node.parent).__name__ in ("Arguments", "ExceptHandler")
-          )
         )
       )
       and 'pypy' not in sys.version.lower()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens/line_numbers.py 
new/asttokens-3.0.2/asttokens/line_numbers.py
--- old/asttokens-3.0.1/asttokens/line_numbers.py       2025-11-03 
07:07:34.000000000 +0100
+++ new/asttokens-3.0.2/asttokens/line_numbers.py       2026-07-12 
05:31:23.000000000 +0200
@@ -16,7 +16,10 @@
 import re
 from typing import Dict, List, Tuple
 
-_line_start_re = re.compile(r'^', re.M)
+# Matches the end-of-line sequences that Python treats as line boundaries in 
source code, i.e.
+# "\r\n", "\r", or "\n". Using this (rather than a plain `re.M` `^`) means we 
recognise a lone
+# "\r" as a line separator, matching how the tokenizer and ast module number 
lines. See issue #105.
+_line_end_re = re.compile(r'\r\n|\r|\n')
 
 class LineNumbers:
   """
@@ -26,16 +29,15 @@
   This class expects unicode for input and stores positions in unicode. But it 
supports
   translating to and from utf8 offsets, which are used by ast parsing.
   """
-  def __init__(self, text):
-    # type: (str) -> None
-    # A list of character offsets of each line's first character.
-    self._line_offsets = [m.start(0) for m in _line_start_re.finditer(text)]
+  def __init__(self, text: str) -> None:
+    # A list of character offsets of each line's first character. The first 
line always starts at
+    # offset 0, and each subsequent line starts right after an end-of-line 
sequence.
+    self._line_offsets = [0] + [m.end(0) for m in _line_end_re.finditer(text)]
     self._text = text
     self._text_len = len(text)
-    self._utf8_offset_cache = {} # type: Dict[int, List[int]] # maps line num 
to list of char offset for each byte in line
+    self._utf8_offset_cache: Dict[int, List[int]] = {} # maps line num to list 
of char offset for each byte in line
 
-  def from_utf8_col(self, line, utf8_column):
-    # type: (int, int) -> int
+  def from_utf8_col(self, line: int, utf8_column: int) -> int:
     """
     Given a 1-based line number and 0-based utf8 column, returns a 0-based 
unicode column.
     """
@@ -50,8 +52,7 @@
 
     return offsets[max(0, min(len(offsets)-1, utf8_column))]
 
-  def line_to_offset(self, line, column):
-    # type: (int, int) -> int
+  def line_to_offset(self, line: int, column: int) -> int:
     """
     Converts 1-based line number and 0-based column to 0-based character 
offset into text.
     """
@@ -63,8 +64,7 @@
     else:
       return min(self._line_offsets[line] + max(0, column), self._text_len)
 
-  def offset_to_line(self, offset):
-    # type: (int) -> Tuple[int, int]
+  def offset_to_line(self, offset: int) -> Tuple[int, int]:
     """
     Converts 0-based character offset to pair (line, col) of 1-based line and 
0-based column
     numbers.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens/mark_tokens.py 
new/asttokens-3.0.2/asttokens/mark_tokens.py
--- old/asttokens-3.0.1/asttokens/mark_tokens.py        2025-11-15 
17:04:32.000000000 +0100
+++ new/asttokens-3.0.2/asttokens/mark_tokens.py        2026-07-12 
05:31:23.000000000 +0200
@@ -22,9 +22,7 @@
 from . import util
 from .asttokens import ASTTokens
 from .astroid_compat import astroid_node_classes as nc, BaseContainer as 
AstroidBaseContainer
-
-if TYPE_CHECKING:
-  from .util import AstNode
+from .util import AstNode
 
 
 # Mapping of matching braces. To find a token here, look up token[:2].
@@ -46,19 +44,18 @@
   Helper that visits all nodes in the AST tree and assigns .first_token and 
.last_token attributes
   to each of them. This is the heart of the token-marking logic.
   """
-  def __init__(self, code):
-    # type: (ASTTokens) -> None
+  def __init__(self, code: ASTTokens) -> None:
     self._code = code
     self._methods = util.NodeMethods()
-    self._iter_children = None # type: Optional[Callable]
+    self._iter_children: Optional[Callable] = None
 
-  def visit_tree(self, node):
-    # type: (Module) -> None
+  def visit_tree(self, node: Module) -> None:
     self._iter_children = util.iter_children_func(node)
     util.visit_tree(node, self._visit_before_children, 
self._visit_after_children)
 
-  def _visit_before_children(self, node, parent_token):
-    # type: (AstNode, Optional[util.Token]) -> Tuple[Optional[util.Token], 
Optional[util.Token]]
+  def _visit_before_children(
+    self, node: AstNode, parent_token: Optional[util.Token]
+  ) -> Tuple[Optional[util.Token], Optional[util.Token]]:
     col = getattr(node, 'col_offset', None)
     token = self._code.get_token_from_utf8(node.lineno, col) if col is not 
None else None
 
@@ -70,8 +67,9 @@
     # parent_token argument. The second value becomes the token argument of 
_visit_after_children.
     return (token or parent_token, token)
 
-  def _visit_after_children(self, node, parent_token, token):
-    # type: (AstNode, Optional[util.Token], Optional[util.Token]) -> None
+  def _visit_after_children(
+    self, node: AstNode, parent_token: Optional[util.Token], token: 
Optional[util.Token]
+  ) -> None:
     # This processes the node generically first, after all children have been 
processed.
 
     # Get the first and last tokens that belong to children. Note how this 
doesn't assume that we
@@ -112,8 +110,7 @@
     node.first_token = nfirst
     node.last_token = nlast
 
-  def _find_last_in_stmt(self, start_token):
-    # type: (util.Token) -> util.Token
+  def _find_last_in_stmt(self, start_token: util.Token) -> util.Token:
     t = start_token
     while (not util.match_token(t, token.NEWLINE) and
            not util.match_token(t, token.OP, ';') and
@@ -121,15 +118,16 @@
       t = self._code.next_token(t, include_extra=True)
     return self._code.prev_token(t)
 
-  def _expand_to_matching_pairs(self, first_token, last_token, node):
-    # type: (util.Token, util.Token, AstNode) -> Tuple[util.Token, util.Token]
+  def _expand_to_matching_pairs(
+    self, first_token: util.Token, last_token: util.Token, node: AstNode
+  ) -> Tuple[util.Token, util.Token]:
     """
     Scan tokens in [first_token, last_token] range that are between node's 
children, and for any
     unmatched brackets, adjust first/last tokens to include the closing pair.
     """
     # We look for opening parens/braces among non-child tokens (i.e. tokens 
between our actual
     # child nodes). If we find any closing ones, we match them to the opens.
-    to_match_right = [] # type: List[Tuple[int, str]]
+    to_match_right: List[Tuple[int, str]] = []
     to_match_left = []
     for tok in self._code.token_range(first_token, last_token):
       tok_info = tok[:2]
@@ -162,39 +160,43 @@
   # Node visitors. Each takes a preliminary first and last tokens, and returns 
the adjusted pair
   # that will actually be assigned.
 
-  def visit_default(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_default(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # pylint: disable=no-self-use
     # By default, we don't need to adjust the token we computed earlier.
     return (first_token, last_token)
 
-  def handle_comp(self, open_brace, node, first_token, last_token):
-    # type: (str, AstNode, util.Token, util.Token) -> Tuple[util.Token, 
util.Token]
+  def handle_comp(
+    self, open_brace: str, node: AstNode, first_token: util.Token, last_token: 
util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # For list/set/dict comprehensions, we only get the token of the first 
child, so adjust it to
     # include the opening brace (the closing brace will be matched 
automatically).
     before = self._code.prev_token(first_token)
     util.expect_token(before, token.OP, open_brace)
     return (before, last_token)
 
-  def visit_comprehension(self,
-                          node,  # type: AstNode
-                          first_token,  # type: util.Token
-                          last_token,  # type: util.Token
-                          ):
-    # type: (...) -> Tuple[util.Token, util.Token]
+  def visit_comprehension(
+    self,
+    node: AstNode,
+    first_token: util.Token,
+    last_token: util.Token,
+  ) -> Tuple[util.Token, util.Token]:
     # The 'comprehension' node starts with 'for' but we only get first child; 
we search backwards
     # to find the 'for' keyword.
     first = self._code.find_token(first_token, token.NAME, 'for', reverse=True)
     return (first, last_token)
 
-  def visit_if(self, node, first_token, last_token):
-    # type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, 
util.Token]
+  def visit_if(
+    self, node: util.Token, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     while first_token.string not in ('if', 'elif'):
       first_token = self._code.prev_token(first_token)
     return first_token, last_token
 
-  def handle_attr(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def handle_attr(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # Attribute node has ".attr" (2 tokens) after the last child.
     dot = self._code.find_token(last_token, token.OP, '.')
     name = self._code.next_token(dot)
@@ -205,8 +207,9 @@
   visit_assignattr = handle_attr
   visit_delattr = handle_attr
 
-  def handle_def(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def handle_def(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # With astroid, nodes that start with a doc-string can have an empty body, 
in which case we
     # need to adjust the last token to include the doc string.
     if not node.body and (getattr(node, 'doc_node', None) or getattr(node, 
'doc', None)): # type: ignore[union-attr]
@@ -222,8 +225,9 @@
   visit_classdef = handle_def
   visit_functiondef = handle_def
 
-  def handle_following_brackets(self, node, last_token, opening_bracket):
-    # type: (AstNode, util.Token, str) -> util.Token
+  def handle_following_brackets(
+    self, node: AstNode, last_token: util.Token, opening_bracket: str
+  ) -> util.Token:
     # This is for calls and subscripts, which have a pair of brackets
     # at the end which may contain no nodes, e.g. foo() or bar[:].
     # We look for the opening bracket and then let the matching pair be found 
automatically
@@ -235,8 +239,9 @@
       last_token = call_start
     return last_token
 
-  def visit_call(self, node, first_token, last_token):
-    # type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, 
util.Token]
+  def visit_call(
+    self, node: util.Token, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     last_token = self.handle_following_brackets(node, last_token, '(')
 
     # Handling a python bug with decorators with empty parens, e.g.
@@ -246,22 +251,24 @@
       first_token = self._code.next_token(first_token)
     return (first_token, last_token)
 
-  def visit_matchclass(self, node, first_token, last_token):
-    # type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, 
util.Token]
+  def visit_matchclass(
+    self, node: util.Token, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     last_token = self.handle_following_brackets(node, last_token, '(')
     return (first_token, last_token)
 
-  def visit_subscript(self,
-                      node,  # type: AstNode
-                      first_token,  # type: util.Token
-                      last_token,  # type: util.Token
-                      ):
-    # type: (...) -> Tuple[util.Token, util.Token]
+  def visit_subscript(
+    self,
+    node: AstNode,
+    first_token: util.Token,
+    last_token: util.Token,
+  ) -> Tuple[util.Token, util.Token]:
     last_token = self.handle_following_brackets(node, last_token, '[')
     return (first_token, last_token)
 
-  def visit_slice(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_slice(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # consume `:` tokens to the left and right. In Python 3.9, Slice nodes are
     # given a col_offset, (and end_col_offset), so this will always start 
inside
     # the slice, even if it is the empty slice. However, in 3.8 and below, this
@@ -280,8 +287,9 @@
       last_token = next_
     return (first_token, last_token)
 
-  def handle_bare_tuple(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def handle_bare_tuple(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # A bare tuple doesn't include parens; if there is a trailing comma, make 
it part of the tuple.
     maybe_comma = self._code.next_token(last_token)
     if util.match_token(maybe_comma, token.OP, ','):
@@ -289,8 +297,9 @@
     return (first_token, last_token)
 
   # In Python3.8 parsed tuples include parentheses when present.
-  def handle_tuple_nonempty(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def handle_tuple_nonempty(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     assert isinstance(node, ast.Tuple) or isinstance(node, 
AstroidBaseContainer)
     # It's a bare tuple if the first token belongs to the first child. The 
first child may
     # include extraneous parentheses (which don't create new nodes), so 
account for those too.
@@ -302,16 +311,18 @@
       return self.handle_bare_tuple(node, first_token, last_token)
     return (first_token, last_token)
 
-  def visit_tuple(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_tuple(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     assert isinstance(node, ast.Tuple) or isinstance(node, 
AstroidBaseContainer)
     if not node.elts:
       # An empty tuple is just "()", and we need no further info.
       return (first_token, last_token)
     return self.handle_tuple_nonempty(node, first_token, last_token)
 
-  def _gobble_parens(self, first_token, last_token, include_all=False):
-    # type: (util.Token, util.Token, bool) -> Tuple[util.Token, util.Token]
+  def _gobble_parens(
+    self, first_token: util.Token, last_token: util.Token, include_all: bool = 
False
+  ) -> Tuple[util.Token, util.Token]:
     # Expands a range of tokens to include one or all pairs of surrounding 
parentheses, and
     # returns (first, last) tokens that include these parens.
     while first_token.index > 0:
@@ -324,16 +335,17 @@
       break
     return (first_token, last_token)
 
-  def visit_str(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_str(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     return self.handle_str(first_token, last_token)
 
-  def visit_joinedstr(self,
-                      node,  # type: AstNode
-                      first_token,  # type: util.Token
-                      last_token,  # type: util.Token
-                      ):
-    # type: (...) -> Tuple[util.Token, util.Token]
+  def visit_joinedstr(
+    self,
+    node: AstNode,
+    first_token: util.Token,
+    last_token: util.Token,
+  ) -> Tuple[util.Token, util.Token]:
     if sys.version_info < (3, 12):
       # Older versions don't tokenize the contents of f-strings
       return self.handle_str(first_token, last_token)
@@ -363,12 +375,14 @@
         break
     return (first_token, last_token)
 
-  def visit_bytes(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_bytes(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     return self.handle_str(first_token, last_token)
 
-  def handle_str(self, first_token, last_token):
-    # type: (util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def handle_str(
+    self, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # Multiple adjacent STRING tokens form a single string.
     last = self._code.next_token(last_token)
     while util.match_token(last, token.STRING):
@@ -376,13 +390,13 @@
       last = self._code.next_token(last_token)
     return (first_token, last_token)
 
-  def handle_num(self,
-                 node,  # type: AstNode
-                 value,  # type: Union[complex, int, numbers.Number]
-                 first_token,  # type: util.Token
-                 last_token,  # type: util.Token
-                 ):
-    # type: (...) -> Tuple[util.Token, util.Token]
+  def handle_num(
+    self,
+    node: AstNode,
+    value: Union[complex, int, numbers.Number],
+    first_token: util.Token,
+    last_token: util.Token,
+  ) -> Tuple[util.Token, util.Token]:
     # A constant like '-1' gets turned into two tokens; this will skip the '-'.
     while util.match_token(last_token, token.OP):
       last_token = self._code.next_token(last_token)
@@ -398,14 +412,16 @@
         first_token = self._code.prev_token(first_token)
     return (first_token, last_token)
 
-  def visit_num(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_num(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     n = node.n  # type: ignore[union-attr] # ast.Num has been removed in 
python 3.14
     assert isinstance(n, (complex, int, numbers.Number))
     return self.handle_num(node, n, first_token, last_token)
 
-  def visit_const(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_const(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     assert isinstance(node, ast.Constant) or isinstance(node, nc.Const)
     if isinstance(node.value, numbers.Number):
       return self.handle_num(node, node.value, first_token, last_token)
@@ -415,8 +431,9 @@
 
   visit_constant = visit_const
 
-  def visit_keyword(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_keyword(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # Until python 3.9 (https://bugs.python.org/issue40141),
     # ast.keyword nodes didn't have line info. Astroid has lineno None.
     assert isinstance(node, ast.keyword) or isinstance(node, nc.Keyword)
@@ -427,8 +444,9 @@
       first_token = name
     return (first_token, last_token)
 
-  def visit_starred(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_starred(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # Astroid has 'Starred' nodes (for "foo(*bar)" type args), but they need 
to be adjusted.
     if not util.match_token(first_token, token.OP, '*'):
       star = self._code.prev_token(first_token)
@@ -436,8 +454,9 @@
         first_token = star
     return (first_token, last_token)
 
-  def visit_assignname(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def visit_assignname(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     # Astroid may turn 'except' clause into AssignName, but we need to adjust 
it.
     if util.match_token(first_token, token.NAME, 'except'):
       colon = self._code.find_token(last_token, token.OP, ':')
@@ -448,8 +467,9 @@
   # but Python < 3.7 doesn't put the col_offset there
   # AsyncFunctionDef is slightly different because it might have
   # decorators before that, which visit_functiondef handles
-  def handle_async(self, node, first_token, last_token):
-    # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
+  def handle_async(
+    self, node: AstNode, first_token: util.Token, last_token: util.Token
+  ) -> Tuple[util.Token, util.Token]:
     if not first_token.string == 'async':
       first_token = self._code.prev_token(first_token)
     return (first_token, last_token)
@@ -457,12 +477,12 @@
   visit_asyncfor = handle_async
   visit_asyncwith = handle_async
 
-  def visit_asyncfunctiondef(self,
-                             node,  # type: AstNode
-                             first_token,  # type: util.Token
-                             last_token,  # type: util.Token
-                             ):
-    # type: (...) -> Tuple[util.Token, util.Token]
+  def visit_asyncfunctiondef(
+    self,
+    node: AstNode,
+    first_token: util.Token,
+    last_token: util.Token,
+  ) -> Tuple[util.Token, util.Token]:
     if util.match_token(first_token, token.NAME, 'def'):
       # Include the 'async' token
       first_token = self._code.prev_token(first_token)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens/util.py 
new/asttokens-3.0.2/asttokens/util.py
--- old/asttokens-3.0.1/asttokens/util.py       2025-11-15 17:04:32.000000000 
+0100
+++ new/asttokens-3.0.2/asttokens/util.py       2026-07-12 05:31:23.000000000 
+0200
@@ -15,6 +15,7 @@
 import ast
 import collections
 import io
+import re
 import sys
 import token
 import tokenize
@@ -38,27 +39,18 @@
 
 if TYPE_CHECKING:  # pragma: no cover
   from .astroid_compat import NodeNG
+else:
+  NodeNG = Any
 
-  # Type class used to expand out the definition of AST to include fields 
added by this library
-  # It's not actually used for anything other than type checking though!
-  class EnhancedAST(AST):
-    # Additional attributes set by mark_tokens
-    first_token = None  # type: Token
-    last_token = None  # type: Token
-    lineno = 0  # type: int
-    end_lineno = 0 # type : int
-    end_col_offset = 0 # type : int
 
-  AstNode = Union[EnhancedAST, NodeNG]
+TokenInfo = tokenize.TokenInfo
+_lone_cr_re = re.compile(r'\r(?!\n)')
 
-  TokenInfo = tokenize.TokenInfo
 
-
-def token_repr(tok_type, string):
-  # type: (int, Optional[str]) -> str
+def token_repr(tok_type: int, string: Optional[str]) -> str:
   """Returns a human-friendly representation of a token with the given type 
and string."""
   # repr() prefixes unicode with 'u' on Python2 but not Python3; strip it out 
for consistency.
-  return '%s:%s' % (token.tok_name[tok_type], repr(string).lstrip('u'))
+  return f"{token.tok_name[tok_type]}:{repr(string).lstrip('u')}"
 
 
 class Token(collections.namedtuple('Token', 'type string start end line index 
startpos endpos')):
@@ -75,50 +67,64 @@
   - [6] .startpos Starting character offset into the input text.
   - [7] .endpos   Ending character offset into the input text.
   """
-  def __str__(self):
-    # type: () -> str
+  def __str__(self) -> str:
     return token_repr(self.type, self.string)
 
 
-def match_token(token, tok_type, tok_str=None):
-  # type: (Token, int, Optional[str]) -> bool
+# Type class used to expand out the definition of AST to include fields added 
by this library
+# It's not actually used for anything other than type checking though!
+class EnhancedAST(AST):
+  # Additional attributes set by mark_tokens
+  first_token: Token = None  # type: ignore
+  last_token: Token = None  # type: ignore
+  lineno: int = 0
+  end_lineno: int = 0
+  end_col_offset: int = 0
+
+
+AstNode = Union[EnhancedAST, NodeNG]
+
+
+def match_token(token: Token, tok_type: int, tok_str: Optional[str] = None) -> 
bool:
   """Returns true if token is of the given type and, if a string is given, has 
that string."""
   return token.type == tok_type and (tok_str is None or token.string == 
tok_str)
 
 
-def expect_token(token, tok_type, tok_str=None):
-  # type: (Token, int, Optional[str]) -> None
+def expect_token(token: Token, tok_type: int, tok_str: Optional[str] = None) 
-> None:
   """
   Verifies that the given token is of the expected type. If tok_str is given, 
the token string
   is verified too. If the token doesn't match, raises an informative 
ValueError.
   """
   if not match_token(token, tok_type, tok_str):
-    raise ValueError("Expected token %s, got %s on line %s col %s" % (
-      token_repr(tok_type, tok_str), str(token),
-      token.start[0], token.start[1] + 1))
+    raise ValueError(
+      f"Expected token {token_repr(tok_type, tok_str)}, "
+      f"got {str(token)} on line {token.start[0]} col {token.start[1] + 1}"
+    )
 
 
-def is_non_coding_token(token_type):
-  # type: (int) -> bool
+def is_non_coding_token(token_type: int) -> bool:
   """
   These are considered non-coding tokens, as they don't affect the syntax tree.
   """
   return token_type in (token.NL, token.COMMENT, token.ENCODING)
 
 
-def generate_tokens(text):
-  # type: (str) -> Iterator[TokenInfo]
+def generate_tokens(text: str) -> Iterator[TokenInfo]:
   """
   Generates standard library tokens for the given code.
   """
+  # Before Python 3.12, tokenize treats an entire line containing a lone 
carriage return as a
+  # non-coding NL token, even though ast.parse treats the carriage return as a 
line boundary.
+  # Replacing lone carriage returns is length-preserving, so token offsets 
still map to the
+  # original source. Keep CRLF intact because changing its length would shift 
later offsets.
+  text = _lone_cr_re.sub('\n', text)
   # tokenize.generate_tokens is technically an undocumented API for Python3, 
but allows us to use the same API as for
   # Python2. See https://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: (AST) -> Callable
+def iter_children_func(node: AST) -> Callable:
   """
   Returns a function which yields all direct children of a AST node,
   skipping children that are singleton nodes.
@@ -127,8 +133,7 @@
   return iter_children_astroid if hasattr(node, 'get_children') else 
iter_children_ast
 
 
-def iter_children_astroid(node, include_joined_str=False):
-  # type: (NodeNG, bool) -> Union[Iterator, List]
+def iter_children_astroid(node: NodeNG, include_joined_str: bool = False) -> 
Union[Iterator, list]:
   if not include_joined_str and is_joined_str(node):
     return []
 
@@ -139,8 +144,7 @@
               issubclass(c, (ast.expr_context, ast.boolop, ast.operator, 
ast.unaryop, ast.cmpop))}
 
 
-def iter_children_ast(node, include_joined_str=False):
-  # type: (AST, bool) -> Iterator[Union[AST, expr]]
+def iter_children_ast(node: AST, include_joined_str: bool = False) -> 
Iterator[Union[AST, expr]]:
   if not include_joined_str and is_joined_str(node):
     return
 
@@ -169,31 +173,26 @@
 
 # These feel hacky compared to isinstance() but allow us to work with both ast 
and astroid nodes
 # in the same way, and without even importing astroid.
-def is_expr(node):
-  # type: (AstNode) -> bool
+def is_expr(node: AstNode) -> bool:
   """Returns whether node is an expression node."""
   return node.__class__.__name__ in expr_class_names
 
-def is_stmt(node):
-  # type: (AstNode) -> bool
+def is_stmt(node: AstNode) -> bool:
   """Returns whether node is a statement node."""
   return node.__class__.__name__ in stmt_class_names
 
-def is_module(node):
-  # type: (AstNode) -> bool
+def is_module(node: AstNode) -> bool:
   """Returns whether node is a module node."""
   return node.__class__.__name__ == 'Module'
 
-def is_joined_str(node):
-  # type: (AstNode) -> bool
+def is_joined_str(node: AstNode) -> bool:
   """Returns whether node is a JoinedStr node, used to represent f-strings."""
   # At the moment, nodes below JoinedStr have wrong line/col info, and trying 
to process them only
   # leads to errors.
   return node.__class__.__name__ == 'JoinedStr'
 
 
-def is_expr_stmt(node):
-  # type: (AstNode) -> bool
+def is_expr_stmt(node: AstNode) -> bool:
   """Returns whether node is an `Expr` node, which is a statement that is an 
expression."""
   return node.__class__.__name__ == 'Expr'
 
@@ -207,26 +206,22 @@
   # astroid is not available
   pass
 
-def is_constant(node):
-  # type: (AstNode) -> bool
+def is_constant(node: AstNode) -> bool:
   """Returns whether node is a Constant node."""
   return isinstance(node, CONSTANT_CLASSES)
 
 
-def is_ellipsis(node):
-  # type: (AstNode) -> bool
+def is_ellipsis(node: AstNode) -> bool:
   """Returns whether node is an Ellipsis node."""
   return is_constant(node) and node.value is Ellipsis  # type: ignore
 
 
-def is_starred(node):
-  # type: (AstNode) -> bool
+def is_starred(node: AstNode) -> bool:
   """Returns whether node is a starred expression node."""
   return node.__class__.__name__ == 'Starred'
 
 
-def is_slice(node):
-  # type: (AstNode) -> bool
+def is_slice(node: AstNode) -> bool:
   """Returns whether node represents a slice, e.g. `1:2` in `x[1:2]`"""
   # Before 3.9, a tuple containing a slice is an ExtSlice,
   # but this was removed in https://bugs.python.org/issue34822
@@ -239,8 +234,7 @@
   )
 
 
-def is_empty_astroid_slice(node):
-  # type: (AstNode) -> bool
+def is_empty_astroid_slice(node: AstNode) -> bool:
   return (
       node.__class__.__name__ == "Slice"
       and not isinstance(node, ast.AST)
@@ -251,8 +245,11 @@
 # Sentinel value used by visit_tree().
 _PREVISIT = object()
 
-def visit_tree(node, previsit, postvisit):
-  # type: (Module, Callable[[AstNode, Optional[Token]], Tuple[Optional[Token], 
Optional[Token]]], Optional[Callable[[AstNode, Optional[Token], 
Optional[Token]], None]])   -> None
+def visit_tree(
+  node: Module,
+  previsit: Callable[[AstNode, Optional[Token]], Tuple[Optional[Token], 
Optional[Token]]],
+  postvisit: Optional[Callable[[AstNode, Optional[Token], Optional[Token]], 
None]]
+) -> None:
   """
   Scans the tree under the node depth-first using an explicit stack. It avoids 
implicit recursion
   via the function call stack to avoid hitting 'maximum recursion depth 
exceeded' error.
@@ -275,7 +272,7 @@
   iter_children = iter_children_func(node)
   done = set()
   ret = None
-  stack = [(node, None, _PREVISIT)] # type: List[Tuple[AstNode, 
Optional[Token], Union[Optional[Token], object]]]
+  stack: List[Tuple[AstNode, Optional[Token], object]] = [(node, None, 
_PREVISIT)]
   while stack:
     current, par_value, value = stack.pop()
     if value is _PREVISIT:
@@ -294,8 +291,7 @@
   return ret
 
 
-def walk(node, include_joined_str=False):
-  # type: (AST, bool) -> Iterator[Union[Module, AstNode]]
+def walk(node: AST, include_joined_str: bool = False) -> 
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).
@@ -323,8 +319,7 @@
       stack.insert(ins, c)
 
 
-def replace(text, replacements):
-  # type: (str, List[Tuple[int, int, str]]) -> str
+def replace(text: str, replacements: List[Tuple[int, int, str]]) -> str:
   """
   Replaces multiple slices of text with new values. This is a convenience 
method for making code
   modifications of ranges e.g. as identified by 
``ASTTokens.get_text_range(node)``. Replacements is
@@ -347,12 +342,10 @@
   """
   Helper to get `visit_{node_type}` methods given a node's class and cache the 
results.
   """
-  def __init__(self):
-    # type: () -> None
-    self._cache = {} # type: Dict[Union[ABCMeta, type], Callable[[AstNode, 
Token, Token], Tuple[Token, Token]]]
+  def __init__(self) -> None:
+    self._cache: Dict[Union[ABCMeta, type], Callable[[AstNode, Token, Token], 
Tuple[Token, Token]]] = {}
 
-  def get(self, obj, cls):
-    # type: (Any, Union[ABCMeta, type]) -> Callable
+  def get(self, obj: Any, cls: Union[ABCMeta, type]) -> Callable:
     """
     Using the lowercase name of the class as node_type, returns 
`obj.visit_{node_type}`,
     or `obj.visit_default` if the type-specific method is not found.
@@ -365,8 +358,7 @@
     return method
 
 
-def patched_generate_tokens(original_tokens):
-    # type: (Iterable[TokenInfo]) -> Iterator[TokenInfo]
+def patched_generate_tokens(original_tokens: 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.
@@ -374,7 +366,7 @@
     because it assumes that error tokens are not actually errors.
     Combines groups of consecutive NAME, NUMBER, and/or ERRORTOKEN tokens into 
a single NAME token.
     """
-    group = []  # type: List[tokenize.TokenInfo]
+    group: List[tokenize.TokenInfo] = []
     for tok in original_tokens:
       if (
           tok.type in (tokenize.NAME, tokenize.ERRORTOKEN, tokenize.NUMBER)
@@ -390,8 +382,7 @@
     for combined_token in combine_tokens(group):
       yield combined_token
 
-def combine_tokens(group):
-    # type: (List[tokenize.TokenInfo]) -> List[tokenize.TokenInfo]
+def combine_tokens(group: List[tokenize.TokenInfo]) -> 
List[tokenize.TokenInfo]:
     if not any(tok.type == tokenize.ERRORTOKEN for tok in group) or 
len({tok.line for tok in group}) != 1:
       return group
     return [
@@ -405,8 +396,7 @@
     ]
 
 
-def last_stmt(node):
-  # type: (AstNode) -> AstNode  
+def last_stmt(node: AstNode) -> AstNode:
   """
   If the given AST node contains multiple statements, return the last one.
   Otherwise, just return the node.
@@ -429,8 +419,7 @@
 
 
 @lru_cache(maxsize=None)
-def fstring_positions_work():
-  # type: () -> bool
+def fstring_positions_work() -> 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.
@@ -460,8 +449,7 @@
   )
   return positions_are_unique and correct_source_segments
 
-def annotate_fstring_nodes(tree):
-  # type: (ast.AST) -> None
+def annotate_fstring_nodes(tree: ast.AST) -> None:
   """
   Add a special attribute `_broken_positions` to nodes inside f-strings
   if the lineno/col_offset cannot be trusted.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens/version.py 
new/asttokens-3.0.2/asttokens/version.py
--- old/asttokens-3.0.1/asttokens/version.py    2025-11-15 17:13:25.000000000 
+0100
+++ new/asttokens-3.0.2/asttokens/version.py    2026-07-12 05:31:31.000000000 
+0200
@@ -1 +1 @@
-__version__ = "3.0.1"
+__version__ = "3.0.2"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens.egg-info/PKG-INFO 
new/asttokens-3.0.2/asttokens.egg-info/PKG-INFO
--- old/asttokens-3.0.1/asttokens.egg-info/PKG-INFO     2025-11-15 
17:13:26.000000000 +0100
+++ new/asttokens-3.0.2/asttokens.egg-info/PKG-INFO     2026-07-12 
05:31:31.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: asttokens
-Version: 3.0.1
+Version: 3.0.2
 Summary: Annotate AST trees with source code positions
 Home-page: https://github.com/gristlabs/asttokens
 Author: Dmitry Sagalovskiy, Grist Labs
@@ -119,3 +119,23 @@
 4. Run tests across all supported interpreters with the ``tox`` command. You 
will need to have the interpreters installed separately. We recommend ``pyenv`` 
for that. Use ``tox -p auto`` to run the tests in parallel.
 5. By default certain tests which take a very long time to run are skipped, 
but they are run in CI.
    These are marked using the ``pytest`` marker ``slow`` and can be run on 
their own with ``pytest -m slow`` or as part of the full suite with ``pytest -m 
''``.
+
+Documentation
+-------------
+
+Documentation at https://asttokens.readthedocs.io/ is built by Read the Docs 
automatically on
+every push, from the sources in ``docs/`` (configured in 
``.readthedocs.yaml``). To preview
+changes locally::
+
+    pip install -r docs/requirements.txt
+    sphinx-build -M html docs docs/_build
+
+Release
+-------
+
+The version is derived from the git tag by ``setuptools_scm``; there is no 
version to update in
+the source. To release, draft a new release at 
https://github.com/gristlabs/asttokens/releases
+with a new tag named ``vX.Y.Z``, and use "Generate release notes" to fill in 
the description.
+Publishing the release triggers the ``publish.yml`` workflow, which builds the 
package and
+uploads it to PyPI via `trusted publishing
+<https://docs.pypi.org/trusted-publishers/>`_.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens.egg-info/SOURCES.txt 
new/asttokens-3.0.2/asttokens.egg-info/SOURCES.txt
--- old/asttokens-3.0.1/asttokens.egg-info/SOURCES.txt  2025-11-15 
17:13:26.000000000 +0100
+++ new/asttokens-3.0.2/asttokens.egg-info/SOURCES.txt  2026-07-12 
05:31:31.000000000 +0200
@@ -5,13 +5,12 @@
 .readthedocs.yaml
 LICENSE
 MANIFEST.in
-Makefile
 README.rst
 pyproject.toml
 setup.cfg
-setup.py
 tox.ini
 .github/workflows/build-and-test.yml
+.github/workflows/publish.yml
 asttokens/__init__.py
 asttokens/astroid_compat.py
 asttokens/asttokens.py
@@ -24,8 +23,9 @@
 asttokens.egg-info/SOURCES.txt
 asttokens.egg-info/dependency_links.txt
 asttokens.egg-info/requires.txt
+asttokens.egg-info/scm_file_list.json
+asttokens.egg-info/scm_version.json
 asttokens.egg-info/top_level.txt
-docs/Makefile
 docs/api-index.rst
 docs/conf.py
 docs/index.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/asttokens-3.0.1/asttokens.egg-info/scm_file_list.json 
new/asttokens-3.0.2/asttokens.egg-info/scm_file_list.json
--- old/asttokens-3.0.1/asttokens.egg-info/scm_file_list.json   1970-01-01 
01:00:00.000000000 +0100
+++ new/asttokens-3.0.2/asttokens.egg-info/scm_file_list.json   2026-07-12 
05:31:31.000000000 +0200
@@ -0,0 +1,70 @@
+{
+  "files": [
+    ".coveragerc",
+    "setup.cfg",
+    "tox.ini",
+    "LICENSE",
+    "pyproject.toml",
+    "README.rst",
+    "MANIFEST.in",
+    ".editorconfig",
+    ".pylintrc",
+    ".gitignore",
+    ".readthedocs.yaml",
+    "docs/api-index.rst",
+    "docs/index.rst",
+    "docs/requirements.txt",
+    "docs/requirements.in",
+    "docs/user-guide.rst",
+    "docs/conf.py",
+    "tests/tools.py",
+    "tests/test_line_numbers.py",
+    "tests/test_astroid.py",
+    "tests/__init__.py",
+    "tests/test_tokenless.py",
+    "tests/context.py",
+    "tests/test_mark_tokens.py",
+    "tests/test_asttokens.py",
+    "tests/test_util.py",
+    "tests/testdata/README.md",
+    "tests/testdata/python3/astroid/suppliermodule_test.py",
+    "tests/testdata/python3/astroid/__init__.py",
+    "tests/testdata/python3/astroid/descriptor_crash.py",
+    "tests/testdata/python3/astroid/joined_strings.py",
+    "tests/testdata/python3/astroid/format.py",
+    "tests/testdata/python3/astroid/email.py",
+    "tests/testdata/python3/astroid/clientmodule_test.py",
+    "tests/testdata/python3/astroid/all.py",
+    "tests/testdata/python3/astroid/nonregr.py",
+    "tests/testdata/python3/astroid/recursion.py",
+    "tests/testdata/python3/astroid/notall.py",
+    "tests/testdata/python3/astroid/absimport.py",
+    "tests/testdata/python3/astroid/noendingnewline.py",
+    "tests/testdata/python3/astroid/module2.py",
+    "tests/testdata/python3/astroid/module.py",
+    "tests/testdata/python2/astroid/suppliermodule_test.py",
+    "tests/testdata/python2/astroid/__init__.py",
+    "tests/testdata/python2/astroid/descriptor_crash.py",
+    "tests/testdata/python2/astroid/joined_strings.py",
+    "tests/testdata/python2/astroid/format.py",
+    "tests/testdata/python2/astroid/email.py",
+    "tests/testdata/python2/astroid/clientmodule_test.py",
+    "tests/testdata/python2/astroid/all.py",
+    "tests/testdata/python2/astroid/nonregr.py",
+    "tests/testdata/python2/astroid/recursion.py",
+    "tests/testdata/python2/astroid/notall.py",
+    "tests/testdata/python2/astroid/absimport.py",
+    "tests/testdata/python2/astroid/noendingnewline.py",
+    "tests/testdata/python2/astroid/module2.py",
+    "tests/testdata/python2/astroid/module.py",
+    "asttokens/asttokens.py",
+    "asttokens/py.typed",
+    "asttokens/__init__.py",
+    "asttokens/astroid_compat.py",
+    "asttokens/line_numbers.py",
+    "asttokens/util.py",
+    "asttokens/mark_tokens.py",
+    ".github/workflows/build-and-test.yml",
+    ".github/workflows/publish.yml"
+  ]
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/asttokens.egg-info/scm_version.json 
new/asttokens-3.0.2/asttokens.egg-info/scm_version.json
--- old/asttokens-3.0.1/asttokens.egg-info/scm_version.json     1970-01-01 
01:00:00.000000000 +0100
+++ new/asttokens-3.0.2/asttokens.egg-info/scm_version.json     2026-07-12 
05:31:31.000000000 +0200
@@ -0,0 +1,8 @@
+{
+  "tag": "3.0.2",
+  "distance": 0,
+  "node": "gf97a6e1403d9f0e93940a11b507524550a5c3896",
+  "dirty": false,
+  "branch": "HEAD",
+  "node_date": "2026-07-12"
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/docs/Makefile 
new/asttokens-3.0.2/docs/Makefile
--- old/asttokens-3.0.1/docs/Makefile   2016-12-10 19:00:48.000000000 +0100
+++ new/asttokens-3.0.2/docs/Makefile   1970-01-01 01:00:00.000000000 +0100
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-SPHINXPROJ    = asttokens
-SOURCEDIR     = .
-BUILDDIR      = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
-       @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-       @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/setup.py new/asttokens-3.0.2/setup.py
--- old/asttokens-3.0.1/setup.py        2020-02-22 20:10:30.000000000 +0100
+++ new/asttokens-3.0.2/setup.py        1970-01-01 01:00:00.000000000 +0100
@@ -1,11 +0,0 @@
-"""A setuptools based setup module.
-
-See:
-https://packaging.python.org/en/latest/distributing.html
-https://github.com/pypa/sampleproject
-"""
-
-from setuptools import setup
-
-if __name__ == "__main__":
-    setup()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/tests/test_line_numbers.py 
new/asttokens-3.0.2/tests/test_line_numbers.py
--- old/asttokens-3.0.1/tests/test_line_numbers.py      2024-11-30 
02:47:20.000000000 +0100
+++ new/asttokens-3.0.2/tests/test_line_numbers.py      2026-07-12 
05:31:23.000000000 +0200
@@ -36,6 +36,15 @@
     self.assertEqual(ln.offset_to_line(100), (8, 0))
     self.assertEqual(ln.offset_to_line(-100), (1, 0))
 
+  def test_carriage_returns(self):
+    # A lone "\r" (old-Mac line ending) and "\r\n" (Windows) must both count 
as line boundaries,
+    # matching how Python's tokenizer and ast module number source lines. See 
issue #105.
+    ln = asttokens.LineNumbers("a\rb\r\nc\n\rd\r\re\r\r\nf")
+    line_offsets = [0, 2, 5, 7, 8, 10, 11, 13, 15]
+    for line, offset in enumerate(line_offsets, 1):
+      self.assertEqual(ln.line_to_offset(line, 0), offset)
+      self.assertEqual(ln.offset_to_line(offset), (line, 0))
+
   def test_unicode(self):
     ln = asttokens.LineNumbers("фыва\nячсм")
     self.assertEqual(ln.line_to_offset(1, 0), 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/tests/test_mark_tokens.py 
new/asttokens-3.0.2/tests/test_mark_tokens.py
--- old/asttokens-3.0.1/tests/test_mark_tokens.py       2025-11-15 
17:04:32.000000000 +0100
+++ new/asttokens-3.0.2/tests/test_mark_tokens.py       2026-07-12 
05:31:23.000000000 +0200
@@ -303,6 +303,21 @@
           "%s:%s" % ("AssignName" if self.is_astroid_test else "Name", 
source.split("=")[0]),
         })
 
+  def test_mixed_line_endings(self):
+      # Test of https://github.com/gristlabs/asttokens/issues/105
+      source = "a=1\rb.c\r\nd=2\n\re.f\r\rg=3\rh.i"
+      atok = self.create_asttokens(source)
+      self.assertEqual(atok.tokens[-1].type, token.ENDMARKER)
+      self.assertEqual(
+        [atok.get_text(stmt) for stmt in atok.tree.body],
+        ["a=1", "b.c", "d=2", "e.f", "g=3", "h.i"],
+      )
+      for node in util.walk(atok.tree):
+        first, last = getattr(node, 'first_token', None), getattr(node, 
'last_token', None)
+        if first is not None:
+          self.assertNotEqual(last.type, token.ENDMARKER, node)
+          self.assertLessEqual(last.index, len(atok.tokens) - 1)
+
   def test_bytes_smoke(self):
       const = 'Const' if self.is_astroid_test else (
           'Constant'
@@ -931,4 +946,3 @@
     m = self.create_mark_checker(source)
     assert 'DictComp'  in str(m.view_nodes_at(0, 0))
 
-    
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/tests/test_util.py 
new/asttokens-3.0.2/tests/test_util.py
--- old/asttokens-3.0.1/tests/test_util.py      2025-11-03 06:34:32.000000000 
+0100
+++ new/asttokens-3.0.2/tests/test_util.py      2026-07-12 05:31:23.000000000 
+0200
@@ -114,6 +114,26 @@
     asttokens.util.expect_token(tok, token.OP)
 
 
+def test_generate_tokens_normalizes_lone_carriage_returns():
+  tokens = list(asttokens.util.generate_tokens("\ry.y\n"))
+  coding_tokens = [tok for tok in tokens if tok.type in (token.NAME, token.OP)]
+
+  assert [(tok.type, tok.string, tok.start, tok.end) for tok in coding_tokens] 
== [
+    (token.NAME, "y", (2, 0), (2, 1)),
+    (token.OP, ".", (2, 1), (2, 2)),
+    (token.NAME, "y", (2, 2), (2, 3)),
+  ]
+
+  crlf_tokens = list(asttokens.util.generate_tokens("\r\ny.y\n"))
+  assert crlf_tokens[0].string == "\r\n"
+
+  triple_tokens = list(asttokens.util.generate_tokens('value = """a\rb"""\n'))
+  string_token = next(tok for tok in triple_tokens if tok.type == token.STRING)
+  assert string_token.string == '"""a\nb"""'
+  assert string_token.start == (1, 8)
+  assert string_token.end == (2, 4)
+
+
 def test_combine_tokens():
     from tokenize import TokenInfo, generate_tokens, ERRORTOKEN, OP, NUMBER, 
NAME
     from asttokens.util import combine_tokens, patched_generate_tokens
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/asttokens-3.0.1/tox.ini new/asttokens-3.0.2/tox.ini
--- old/asttokens-3.0.1/tox.ini 2025-11-03 06:34:32.000000000 +0100
+++ new/asttokens-3.0.2/tox.ini 2026-07-12 05:31:23.000000000 +0200
@@ -4,7 +4,7 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = py{38,39,310,311,312,313,py3}
+envlist = py{39,310,311,312,313,314,py3}
 
 [testenv]
 commands = pytest {posargs}

Reply via email to