Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/contributing/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1440,7 +1440,7 @@ setup-python, ``build`` on conda's base ``python`` under the login shell, from
:file:`conda/pypcapkit`, and ``upload`` on setup-python's. A project
pin runs each on the cases the shell it replaced handled and requires the same
status, output and calls.
:file:`util/_actions.py` is standard library only, and keeps to 3.7's syntax,
:file:`util/_actions.py` is standard library only, and keeps to 3.8's syntax,
since the scripts on ``/usr/bin/python3`` and on every leg's interpreter import it.

The scripts a person runs as well, or alone, sit beside the CI-only ones in
Expand Down
73 changes: 17 additions & 56 deletions tests/project/test_actions_helper_2047.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
"""#2047: :file:`util/_actions.py`, the GitHub Actions plumbing the ``util/`` scripts share.

* :class:`TestStandardLibraryOnly` holds the helper to the standard library and
to 3.7's syntax, since :file:`util/pip_install.py` imports it on every
interpreter the matrices hold, before anything is installed.
to the syntax of ``requires-python``'s floor, 3.8, since
:file:`util/pip_install.py` imports it on every interpreter the matrices hold,
before anything is installed.
* :class:`TestCommands`, :class:`TestFiles` and :class:`TestRun` pin each
function's exact output, GitHub's escaping included.
* :class:`TestScriptsImportIt` loads each script that imports the helper in the
Expand All @@ -26,16 +27,16 @@
import subprocess # nosec: B404
import sys
import tempfile
import tokenize
import unittest
from typing import TYPE_CHECKING
from unittest import mock

from tests._workflows import WORKFLOWS, load
from tests.project.test_requires_python_floor_2051 import FLOOR
from util import _actions

if TYPE_CHECKING:
from typing import Any, Iterator, Optional
from typing import Any, Iterator

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


def self_documenting(source: 'str') -> 'list[str]':
"""Each f-string replacement field in ``source`` that ends in 3.8's ``=``, as ``line:col``.

``ast.parse(..., feature_version=(3, 7))`` accepts ``f'{x=}'``. Since 3.12 an
f-string tokenizes into its fields (PEP 701), and there the form is an ``=``
token followed at once by the ``}``, ``!`` or ``:`` that ends the expression;
``==``, ``:=`` and a keyword argument's ``=`` are other tokens or are followed
by an operand.

"""
found = []
depth = 0
previous = None # type: Optional[tokenize.TokenInfo]
for token in tokenize.generate_tokens(io.StringIO(source).readline):
if token.type == tokenize.FSTRING_START: # pylint: disable=no-member
depth += 1
elif token.type == tokenize.FSTRING_END: # pylint: disable=no-member
depth -= 1
elif depth and token.type == tokenize.OP and token.string in ('}', '!', ':') \
and previous is not None and previous.type == tokenize.OP and previous.string == '=':
found.append('%d:%d' % previous.start)
if token.type not in (tokenize.NL, tokenize.NEWLINE, tokenize.COMMENT):
previous = token
return found


class TestStandardLibraryOnly(unittest.TestCase):
"""The helper imports nothing but 3.7's standard library, and is written in 3.7's syntax."""
"""The helper imports nothing but 3.8's standard library, and is written in 3.8's syntax."""

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

def test_it_parses_as_python_3_7(self) -> None:
ast.parse(self.source, filename=str(HELPER), feature_version=(3, 7))
def test_it_parses_as_python_3_8(self) -> None:
ast.parse(self.source, filename=str(HELPER), feature_version=FLOOR)

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

def test_the_import_check_sees_a_newer_module(self) -> None:
self.assertEqual(sorted(imported('import tomllib\nfrom os import path\n') - STDLIB_37), ['tomllib'])
self.assertEqual(sorted(imported('def f():\n from . import x\n') - STDLIB_37), ['.'])

@unittest.skipUnless(hasattr(tokenize, 'FSTRING_START'), 'f-strings tokenize into fields from 3.12 (PEP 701)')
def test_it_has_no_self_documenting_f_string(self) -> None:
self.assertEqual(self_documenting(self.source), [])

@unittest.skipUnless(hasattr(tokenize, 'FSTRING_START'), 'f-strings tokenize into fields from 3.12 (PEP 701)')
def test_the_f_string_check_tells_the_forms_apart(self) -> None:
for source in ('f"{x=}"', 'f"{x = }"', 'f"{x=!r}"', 'f"{x=:>4}"', 'f"a {f(y)=} b"', 'f"{f\'{x=}\'}"'):
with self.subTest(source=source):
self.assertEqual(len(self_documenting(source + '\n')), 1, source)
for source in ('f"{a==b}"', 'f"x={x!r}"', 'f"{x:=^5}"', 'f"{(y:=1)}"', 'f"{g(a=1)}"', 'f"{x!r:=>4}"',
'x = {"a": 1}', 'def f(a=1): return {a: a}', "f'{d[\"k=\"]}'"):
with self.subTest(source=source):
self.assertEqual(self_documenting(source + '\n'), [], source)
self.assertEqual(sorted(imported('import tomllib\nfrom os import path\n') - STDLIB_38), ['tomllib'])
self.assertEqual(sorted(imported('def f():\n from . import x\n') - STDLIB_38), ['.'])


class TestCommands(unittest.TestCase):
Expand Down
7 changes: 4 additions & 3 deletions tests/project/test_release_conda_scripts_1967.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
run writes, and the errors for a leg on another platform.
* :class:`TestTheStepsRunTheScripts` pins each step to its script and its
``env:``, both jobs' legs to :data:`release_conda.PLATFORMS`, and the script
to Python 3.7's syntax.
to Python 3.8's syntax.

"""

Expand All @@ -33,6 +33,7 @@
from typing import TYPE_CHECKING

from tests._workflows import job, steps
from tests.project.test_requires_python_floor_2051 import FLOOR
from util import release_conda

if TYPE_CHECKING:
Expand Down Expand Up @@ -561,8 +562,8 @@ def test_every_leg_is_mapped_and_every_mapping_is_a_leg(self) -> None:
self.assertLessEqual(set(labels), set(release_conda.PLATFORMS))
self.assertEqual(set(release_conda.PLATFORMS), legs)

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


if __name__ == '__main__':
Expand Down
7 changes: 4 additions & 3 deletions tests/project/test_release_reset_build_1967.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
shell ends.
* :class:`TestTheStepRunsTheScript` pins the step to its script and its
``env:``, after a whole checkout of the repository, its ``git`` calls to ones
that never pull, merge, rebase, reset or force, and the script to Python 3.7's
that never pull, merge, rebase, reset or force, and the script to Python 3.8's
syntax.

The shell ran as a step with no ``shell:`` runs on the runner, ``bash -e``, and
Expand All @@ -37,6 +37,7 @@
from typing import TYPE_CHECKING

from tests._workflows import steps
from tests.project.test_requires_python_floor_2051 import FLOOR

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

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


if __name__ == '__main__':
Expand Down
7 changes: 4 additions & 3 deletions tests/project/test_release_scripts_1967.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
scripts against the same stand-ins and requires the same status, standard
output and error, ``$GITHUB_OUTPUT`` and calls.
* :class:`TestTheStepsRunTheScripts` pins each step to its script, after a whole
checkout of the repository, and the scripts to Python 3.7's syntax.
checkout of the repository, and the scripts to Python 3.8's syntax.

Their runs against a real repository, and the steps' ``env:``, are
:mod:`tests.project.test_release_tags_the_run_sha_1634`'s.
Expand All @@ -32,6 +32,7 @@
from typing import TYPE_CHECKING

from tests._workflows import load, steps
from tests.project.test_requires_python_floor_2051 import FLOOR

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

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


if __name__ == '__main__':
Expand Down
7 changes: 4 additions & 3 deletions tests/project/test_release_status_scripts_1967.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
runs that ``run:`` on its own case table, which reaches each of its branches.
* :class:`TestTheStepsRunTheScripts` pins ``release_status``'s step to its
script, after a checkout of :file:`util/` alone, and the script to the
standard library and Python 3.7's syntax.
standard library and Python 3.8's syntax.

The shell ran as a step with no ``shell:`` runs on the runner, ``bash -e``, and
``bash`` prefixes each of its own errors with the script's path and line, which
Expand Down Expand Up @@ -43,6 +43,7 @@

from tests._workflows import load, steps
from tests.project.test_release_gates import job_blocks
from tests.project.test_requires_python_floor_2051 import FLOOR

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

def test_the_scripts_need_only_util_and_python_3_7(self) -> None:
def test_the_scripts_need_only_util_and_python_3_8(self) -> None:
"""The sparse checkout holds :file:`util/`, and the image's ``python3`` nothing installed."""
allowed = {'__future__', 'argparse', 'os', 'subprocess', 'sys', 'typing', '_actions', 'util'}
for name in ('release_status.py',):
with self.subTest(script=name):
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=(3, 7))
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=FLOOR)
imported = {alias.name.split('.')[0] for node in ast.walk(tree) if isinstance(node, ast.Import)
for alias in node.names}
imported |= {str(node.module).split('.')[0] for node in ast.walk(tree)
Expand Down
7 changes: 4 additions & 3 deletions tests/project/test_release_version_check_scripts_1967.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* :class:`TestAPlantedDivergenceIsCaught` changes each script in a copy, one
plant at a time, and requires the cases to tell it from the shell.
* :class:`TestTheStepsRunTheScripts` pins each step to its script, and the
scripts to the standard library and Python 3.7's syntax.
scripts to the standard library and Python 3.8's syntax.

The steps run with no ``shell:``, so on the runner's ``bash -e``, and ``bash``
prefixes each of its own errors with the script's path and line, which
Expand All @@ -39,6 +39,7 @@
from typing import TYPE_CHECKING

from tests._workflows import load, steps
from tests.project.test_requires_python_floor_2051 import FLOOR

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

def test_the_scripts_need_only_util_and_python_3_7(self) -> None:
def test_the_scripts_need_only_util_and_python_3_8(self) -> None:
allowed = {'__future__', 'argparse', 'os', 're', 'subprocess', 'sys', 'typing', '_actions', 'util'}
for name in ('release_tiers.py', 'release_published.py'):
with self.subTest(script=name):
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=(3, 7))
tree = ast.parse((UTIL / name).read_text(encoding='utf-8'), feature_version=FLOOR)
imported = {alias.name.split('.')[0] for node in ast.walk(tree) if isinstance(node, ast.Import)
for alias in node.names}
imported |= {str(node.module).split('.')[0] for node in ast.walk(tree)
Expand Down
2 changes: 1 addition & 1 deletion util/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
The scripts run on the image's ``/usr/bin/python3``, or on a leg's own
interpreter, which may be an end-of-life KTLO one, GraalPy or a free-threaded
build, and :file:`util/pip_install.py` is the step that installs anything at
all. So this module is standard library only and keeps to 3.7's syntax; a
all. So this module is standard library only and keeps to 3.8's syntax; a
project test, :file:`tests/project/test_actions_helper_2047.py`, holds it to
both.

Expand Down
2 changes: 1 addition & 1 deletion util/release_conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
step: every label is ``main`` or ``dev``. On ``windows-latest`` the paths are
Windows' own rather than Git Bash's ``/tmp/...``.

The script is standard library only, and keeps to Python 3.7's syntax.
The script is standard library only, and keeps to Python 3.8's syntax.

"""

Expand Down
2 changes: 1 addition & 1 deletion util/release_published.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
``[``'s error and reads ``false``; a variable it reads that is unset, which
Actions never leaves it, is ``<name>: unbound variable`` and status 1.

It keeps to the standard library and Python 3.7's syntax as the other
It keeps to the standard library and Python 3.8's syntax as the other
:file:`util/` scripts do.

"""
Expand Down
2 changes: 1 addition & 1 deletion util/release_reset_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
``set -u`` did.

``tag`` has no setup-python, so this runs on the image's ``python3``: standard
library only, and Python 3.7's syntax. Its ``git switch`` replaces the checkout,
library only, and Python 3.8's syntax. Its ``git switch`` replaces the checkout,
:file:`util/` included, so everything it imports is imported first.

"""
Expand Down
2 changes: 1 addition & 1 deletion util/release_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
``set -u`` would have, rather than with a traceback.

The job checks out only :file:`util/` and has no setup-python, so this runs on
the image's ``python3``: standard library only, and Python 3.7's syntax.
the image's ``python3``: standard library only, and Python 3.8's syntax.

"""

Expand Down
2 changes: 1 addition & 1 deletion util/release_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

The ``conda`` legs run it before setup-python, so ``python3`` is the image's,
and ``github`` has no setup-python at all. So the script is standard library
only, and keeps to Python 3.7's syntax.
only, and keeps to Python 3.8's syntax.

"""

Expand Down
2 changes: 1 addition & 1 deletion util/release_tiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
-d`` left it.

It runs on setup-python's ``python3``, the interpreter it runs the tag's
reader on, but keeps to the standard library and Python 3.7's syntax as the
reader on, but keeps to the standard library and Python 3.8's syntax as the
other :file:`util/` scripts do.

"""
Expand Down
2 changes: 1 addition & 1 deletion util/release_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
that is not then a regular file gets an ``::error`` and exits 1. A failing
``gh`` ends the script with its status, as ``set -e`` ended the step.

The script is standard library only, and keeps to Python 3.7's syntax.
The script is standard library only, and keeps to Python 3.8's syntax.

"""

Expand Down
Loading