Skip to content

Commit 876fa18

Browse files
authored
test(util): raise the util scripts' syntax floor to 3.8 (#1103) (#2197)
1 parent dc5c880 commit 876fa18

15 files changed

Lines changed: 46 additions & 80 deletions

‎docs/source/contributing/workflows.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1438,7 +1438,7 @@ setup-python, ``build`` on conda's base ``python`` under the login shell, from
14381438
:file:`conda/pypcapkit`, and ``upload`` on setup-python's. A project
14391439
pin runs each on the cases the shell it replaced handled and requires the same
14401440
status, output and calls.
1441-
:file:`util/_actions.py` is standard library only, and keeps to 3.7's syntax,
1441+
:file:`util/_actions.py` is standard library only, and keeps to 3.8's syntax,
14421442
since the scripts on ``/usr/bin/python3`` and on every leg's interpreter import it.
14431443

14441444
The scripts a person runs as well, or alone, sit beside the CI-only ones in

‎tests/project/test_actions_helper_2047.py‎

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"""#2047: :file:`util/_actions.py`, the GitHub Actions plumbing the ``util/`` scripts share.
33
44
* :class:`TestStandardLibraryOnly` holds the helper to the standard library and
5-
to 3.7's syntax, since :file:`util/pip_install.py` imports it on every
6-
interpreter the matrices hold, before anything is installed.
5+
to the syntax of ``requires-python``'s floor, 3.8, since
6+
:file:`util/pip_install.py` imports it on every interpreter the matrices hold,
7+
before anything is installed.
78
* :class:`TestCommands`, :class:`TestFiles` and :class:`TestRun` pin each
89
function's exact output, GitHub's escaping included.
910
* :class:`TestScriptsImportIt` loads each script that imports the helper in the
@@ -26,16 +27,16 @@
2627
import subprocess # nosec: B404
2728
import sys
2829
import tempfile
29-
import tokenize
3030
import unittest
3131
from typing import TYPE_CHECKING
3232
from unittest import mock
3333

3434
from tests._workflows import WORKFLOWS, load
35+
from tests.project.test_requires_python_floor_2051 import FLOOR
3536
from util import _actions
3637

3738
if TYPE_CHECKING:
38-
from typing import Any, Iterator, Optional
39+
from typing import Any, Iterator
3940

4041
#: Test tier (#1958): reads and runs the tree's own scripts, a question about the tree.
4142
__tier__ = 'LATEST'
@@ -48,10 +49,10 @@
4849
'python_versions', 'regenerate_samples', 'release_conda', 'release_published', 'release_reset_build',
4950
'release_status', 'release_tags', 'release_tiers', 'release_upload', 'superseded',
5051
'update_python_versions', 'vendor_code_changed', 'vendor_test_gate', 'verdict_covers_checkout')
51-
#: The top-level modules the helper may import, each in Python 3.7's standard
52+
#: The top-level modules the helper may import, each in Python 3.8's standard
5253
#: library: the running interpreter's :data:`sys.stdlib_module_names` would
53-
#: pass ``tomllib``, which 3.7 lacks.
54-
STDLIB_37 = frozenset({'__future__', 'contextlib', 'os', 'subprocess', 'sys', 'typing', 'uuid'})
54+
#: pass ``tomllib``, which 3.8 lacks.
55+
STDLIB_38 = frozenset({'__future__', 'contextlib', 'os', 'subprocess', 'sys', 'typing', 'uuid'})
5556
#: A trivial run of each script, ``(arguments, environment)``, that gets past its imports.
5657
RUNS = {
5758
'apt_packages': (['--help'], {}),
@@ -106,65 +107,25 @@ def imported(source: 'str') -> 'set[str]':
106107
return names
107108

108109

109-
def self_documenting(source: 'str') -> 'list[str]':
110-
"""Each f-string replacement field in ``source`` that ends in 3.8's ``=``, as ``line:col``.
111-
112-
``ast.parse(..., feature_version=(3, 7))`` accepts ``f'{x=}'``. Since 3.12 an
113-
f-string tokenizes into its fields (PEP 701), and there the form is an ``=``
114-
token followed at once by the ``}``, ``!`` or ``:`` that ends the expression;
115-
``==``, ``:=`` and a keyword argument's ``=`` are other tokens or are followed
116-
by an operand.
117-
118-
"""
119-
found = []
120-
depth = 0
121-
previous = None # type: Optional[tokenize.TokenInfo]
122-
for token in tokenize.generate_tokens(io.StringIO(source).readline):
123-
if token.type == tokenize.FSTRING_START: # pylint: disable=no-member
124-
depth += 1
125-
elif token.type == tokenize.FSTRING_END: # pylint: disable=no-member
126-
depth -= 1
127-
elif depth and token.type == tokenize.OP and token.string in ('}', '!', ':') \
128-
and previous is not None and previous.type == tokenize.OP and previous.string == '=':
129-
found.append('%d:%d' % previous.start)
130-
if token.type not in (tokenize.NL, tokenize.NEWLINE, tokenize.COMMENT):
131-
previous = token
132-
return found
133-
134-
135110
class TestStandardLibraryOnly(unittest.TestCase):
136-
"""The helper imports nothing but 3.7's standard library, and is written in 3.7's syntax."""
111+
"""The helper imports nothing but 3.8's standard library, and is written in 3.8's syntax."""
137112

138113
def setUp(self) -> None:
139114
self.source = HELPER.read_text(encoding='utf-8')
140115

141-
def test_it_parses_as_python_3_7(self) -> None:
142-
ast.parse(self.source, filename=str(HELPER), feature_version=(3, 7))
116+
def test_it_parses_as_python_3_8(self) -> None:
117+
ast.parse(self.source, filename=str(HELPER), feature_version=FLOOR)
143118

144-
def test_it_imports_only_what_3_7_has(self) -> None:
145-
extra = sorted(imported(self.source) - STDLIB_37)
119+
def test_it_imports_only_what_3_8_has(self) -> None:
120+
extra = sorted(imported(self.source) - STDLIB_38)
146121
self.assertEqual(extra, [], f'util/_actions.py imports {extra}: check that each is in the standard '
147-
'library of Python 3.7 and every interpreter the matrices hold, then add '
148-
'it to STDLIB_37 in this test')
122+
'library of Python 3.8 and every interpreter the matrices hold, then add '
123+
'it to STDLIB_38 in this test')
149124
self.assertIn('uuid', imported(self.source), 'the import inside a function is seen')
150125

151126
def test_the_import_check_sees_a_newer_module(self) -> None:
152-
self.assertEqual(sorted(imported('import tomllib\nfrom os import path\n') - STDLIB_37), ['tomllib'])
153-
self.assertEqual(sorted(imported('def f():\n from . import x\n') - STDLIB_37), ['.'])
154-
155-
@unittest.skipUnless(hasattr(tokenize, 'FSTRING_START'), 'f-strings tokenize into fields from 3.12 (PEP 701)')
156-
def test_it_has_no_self_documenting_f_string(self) -> None:
157-
self.assertEqual(self_documenting(self.source), [])
158-
159-
@unittest.skipUnless(hasattr(tokenize, 'FSTRING_START'), 'f-strings tokenize into fields from 3.12 (PEP 701)')
160-
def test_the_f_string_check_tells_the_forms_apart(self) -> None:
161-
for source in ('f"{x=}"', 'f"{x = }"', 'f"{x=!r}"', 'f"{x=:>4}"', 'f"a {f(y)=} b"', 'f"{f\'{x=}\'}"'):
162-
with self.subTest(source=source):
163-
self.assertEqual(len(self_documenting(source + '\n')), 1, source)
164-
for source in ('f"{a==b}"', 'f"x={x!r}"', 'f"{x:=^5}"', 'f"{(y:=1)}"', 'f"{g(a=1)}"', 'f"{x!r:=>4}"',
165-
'x = {"a": 1}', 'def f(a=1): return {a: a}', "f'{d[\"k=\"]}'"):
166-
with self.subTest(source=source):
167-
self.assertEqual(self_documenting(source + '\n'), [], source)
127+
self.assertEqual(sorted(imported('import tomllib\nfrom os import path\n') - STDLIB_38), ['tomllib'])
128+
self.assertEqual(sorted(imported('def f():\n from . import x\n') - STDLIB_38), ['.'])
168129

169130

170131
class TestCommands(unittest.TestCase):

‎tests/project/test_release_conda_scripts_1967.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
run writes, and the errors for a leg on another platform.
1414
* :class:`TestTheStepsRunTheScripts` pins each step to its script and its
1515
``env:``, both jobs' legs to :data:`release_conda.PLATFORMS`, and the script
16-
to Python 3.7's syntax.
16+
to Python 3.8's syntax.
1717
1818
"""
1919

@@ -33,6 +33,7 @@
3333
from typing import TYPE_CHECKING
3434

3535
from tests._workflows import job, steps
36+
from tests.project.test_requires_python_floor_2051 import FLOOR
3637
from util import release_conda
3738

3839
if TYPE_CHECKING:
@@ -561,8 +562,8 @@ def test_every_leg_is_mapped_and_every_mapping_is_a_leg(self) -> None:
561562
self.assertLessEqual(set(labels), set(release_conda.PLATFORMS))
562563
self.assertEqual(set(release_conda.PLATFORMS), legs)
563564

564-
def test_the_scripts_parse_as_python_3_7(self) -> None:
565-
ast.parse((UTIL / 'release_conda.py').read_text(encoding='utf-8'), feature_version=(3, 7))
565+
def test_the_scripts_parse_as_python_3_8(self) -> None:
566+
ast.parse((UTIL / 'release_conda.py').read_text(encoding='utf-8'), feature_version=FLOOR)
566567

567568

568569
if __name__ == '__main__':

‎tests/project/test_release_reset_build_1967.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
shell ends.
1212
* :class:`TestTheStepRunsTheScript` pins the step to its script and its
1313
``env:``, after a whole checkout of the repository, its ``git`` calls to ones
14-
that never pull, merge, rebase, reset or force, and the script to Python 3.7's
14+
that never pull, merge, rebase, reset or force, and the script to Python 3.8's
1515
syntax.
1616
1717
The shell ran as a step with no ``shell:`` runs on the runner, ``bash -e``, and
@@ -37,6 +37,7 @@
3737
from typing import TYPE_CHECKING
3838

3939
from tests._workflows import steps
40+
from tests.project.test_requires_python_floor_2051 import FLOOR
4041

4142
if TYPE_CHECKING:
4243
from typing import Callable, Optional
@@ -462,9 +463,9 @@ def test_its_git_never_pulls_merges_rebases_resets_or_forces(self) -> None:
462463
self.assertEqual(commands, {'rev-parse', 'show', 'fetch', 'merge-base', 'ls-remote', 'switch', 'commit',
463464
'push'})
464465

465-
def test_it_parses_as_python_3_7(self) -> None:
466+
def test_it_parses_as_python_3_8(self) -> None:
466467
"""``tag`` has no setup-python, so it runs on the image's ``python3``."""
467-
ast.parse(SCRIPT.read_text(encoding='utf-8'), feature_version=(3, 7))
468+
ast.parse(SCRIPT.read_text(encoding='utf-8'), feature_version=FLOOR)
468469

469470

470471
if __name__ == '__main__':

‎tests/project/test_release_scripts_1967.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
scripts against the same stand-ins and requires the same status, standard
1111
output and error, ``$GITHUB_OUTPUT`` and calls.
1212
* :class:`TestTheStepsRunTheScripts` pins each step to its script, after a whole
13-
checkout of the repository, and the scripts to Python 3.7's syntax.
13+
checkout of the repository, and the scripts to Python 3.8's syntax.
1414
1515
Their runs against a real repository, and the steps' ``env:``, are
1616
:mod:`tests.project.test_release_tags_the_run_sha_1634`'s.
@@ -32,6 +32,7 @@
3232
from typing import TYPE_CHECKING
3333

3434
from tests._workflows import load, steps
35+
from tests.project.test_requires_python_floor_2051 import FLOOR
3536

3637
if TYPE_CHECKING:
3738
from typing import Callable
@@ -517,11 +518,11 @@ def test_each_job_checks_the_whole_tree_out_first(self) -> None:
517518
self.assertFalse([step for step in found if (step.get('name'), job) in {(n, j) for j, n in STEPS}
518519
and 'working-directory' in step])
519520

520-
def test_the_scripts_parse_as_python_3_7(self) -> None:
521+
def test_the_scripts_parse_as_python_3_8(self) -> None:
521522
"""The steps run them on the image's ``python3``, or on a KTLO leg's."""
522523
for name in ('release_tags.py', 'release_upload.py'):
523524
with self.subTest(script=name):
524-
ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=(3, 7))
525+
ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=FLOOR)
525526

526527

527528
if __name__ == '__main__':

‎tests/project/test_release_status_scripts_1967.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
runs that ``run:`` on its own case table, which reaches each of its branches.
1616
* :class:`TestTheStepsRunTheScripts` pins ``release_status``'s step to its
1717
script, after a checkout of :file:`util/` alone, and the script to the
18-
standard library and Python 3.7's syntax.
18+
standard library and Python 3.8's syntax.
1919
2020
The shell ran as a step with no ``shell:`` runs on the runner, ``bash -e``, and
2121
``bash`` prefixes each of its own errors with the script's path and line, which
@@ -43,6 +43,7 @@
4343

4444
from tests._workflows import load, steps
4545
from tests.project.test_release_gates import job_blocks
46+
from tests.project.test_requires_python_floor_2051 import FLOOR
4647

4748
if TYPE_CHECKING:
4849
from typing import Callable, Optional
@@ -615,12 +616,12 @@ def test_no_step_holds_the_former_shell(self) -> None:
615616
with self.subTest(job=job, step=step.get('name'), line=line):
616617
self.assertNotIn(line, step.get('run', ''))
617618

618-
def test_the_scripts_need_only_util_and_python_3_7(self) -> None:
619+
def test_the_scripts_need_only_util_and_python_3_8(self) -> None:
619620
"""The sparse checkout holds :file:`util/`, and the image's ``python3`` nothing installed."""
620621
allowed = {'__future__', 'argparse', 'os', 'subprocess', 'sys', 'typing', '_actions', 'util'}
621622
for name in ('release_status.py',):
622623
with self.subTest(script=name):
623-
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=(3, 7))
624+
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=FLOOR)
624625
imported = {alias.name.split('.')[0] for node in ast.walk(tree) if isinstance(node, ast.Import)
625626
for alias in node.names}
626627
imported |= {str(node.module).split('.')[0] for node in ast.walk(tree)

‎tests/project/test_release_version_check_scripts_1967.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* :class:`TestAPlantedDivergenceIsCaught` changes each script in a copy, one
1414
plant at a time, and requires the cases to tell it from the shell.
1515
* :class:`TestTheStepsRunTheScripts` pins each step to its script, and the
16-
scripts to the standard library and Python 3.7's syntax.
16+
scripts to the standard library and Python 3.8's syntax.
1717
1818
The steps run with no ``shell:``, so on the runner's ``bash -e``, and ``bash``
1919
prefixes each of its own errors with the script's path and line, which
@@ -39,6 +39,7 @@
3939
from typing import TYPE_CHECKING
4040

4141
from tests._workflows import load, steps
42+
from tests.project.test_requires_python_floor_2051 import FLOOR
4243

4344
if TYPE_CHECKING:
4445
from typing import Optional, Union
@@ -818,11 +819,11 @@ def test_no_step_holds_the_former_shell(self) -> None:
818819
with self.subTest(job=job, step=step.get('name'), line=line):
819820
self.assertNotIn(line, step.get('run', ''))
820821

821-
def test_the_scripts_need_only_util_and_python_3_7(self) -> None:
822+
def test_the_scripts_need_only_util_and_python_3_8(self) -> None:
822823
allowed = {'__future__', 'argparse', 'os', 're', 'subprocess', 'sys', 'typing', '_actions', 'util'}
823824
for name in ('release_tiers.py', 'release_published.py'):
824825
with self.subTest(script=name):
825-
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=(3, 7))
826+
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=FLOOR)
826827
imported = {alias.name.split('.')[0] for node in ast.walk(tree) if isinstance(node, ast.Import)
827828
for alias in node.names}
828829
imported |= {str(node.module).split('.')[0] for node in ast.walk(tree)

‎util/_actions.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
The scripts run on the image's ``/usr/bin/python3``, or on a leg's own
1313
interpreter, which may be an end-of-life KTLO one, GraalPy or a free-threaded
1414
build, and :file:`util/pip_install.py` is the step that installs anything at
15-
all. So this module is standard library only and keeps to 3.7's syntax; a
15+
all. So this module is standard library only and keeps to 3.8's syntax; a
1616
project test, :file:`tests/project/test_actions_helper_2047.py`, holds it to
1717
both.
1818

‎util/release_conda.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
step: every label is ``main`` or ``dev``. On ``windows-latest`` the paths are
4747
Windows' own rather than Git Bash's ``/tmp/...``.
4848
49-
The script is standard library only, and keeps to Python 3.7's syntax.
49+
The script is standard library only, and keeps to Python 3.8's syntax.
5050
5151
"""
5252

‎util/release_published.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
``[``'s error and reads ``false``; a variable it reads that is unset, which
3535
Actions never leaves it, is ``<name>: unbound variable`` and status 1.
3636
37-
It keeps to the standard library and Python 3.7's syntax as the other
37+
It keeps to the standard library and Python 3.8's syntax as the other
3838
:file:`util/` scripts do.
3939
4040
"""

0 commit comments

Comments
 (0)