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
5 changes: 3 additions & 2 deletions docs/source/contributing/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1320,8 +1320,9 @@ to a script. Each script's docstring says what it does; each has tests under
- No.
* - :file:`util/_actions.py`
- No step: it is imported by the scripts that share its annotations, step
outputs, job summary lines and logged runs (`#2047
<https://github.com/JarryShaw/PyPCAPKit/issues/2047>`__).
outputs, job summary lines and logged runs (:issue:`2047`), and its
environment read as bytes where Windows has no ``os.environb``
(:issue:`2120`).
- No: imported only.
* - :file:`util/codeql_report.py`
- ``Write the report`` in ``codeql-analysis.yml``'s ``discussion`` (see `CI
Expand Down
73 changes: 71 additions & 2 deletions tests/project/test_conda_windows_leg_2060.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
:file:`util/conda_vendor.py` reads a ``.pyd``'s PE machine, and ``--smoke``
takes a Windows package's ``Lib/site-packages``, or from Python 3.15 its
``lib/python/site-packages`` (conda-forge's CFEP-27), whose path names no Python.
* :class:`TestTheEnvironmentWithoutEnvironb`: the scripts read their variables
where :data:`os.environb` is missing, as on Windows (#2120), where
``release_conda.py build`` died reading ``PCAPKIT_CONDA_OS``.

Nothing here runs on Windows; what a real Conda Update run shows is the rest.

Expand All @@ -28,6 +31,7 @@
from __future__ import annotations

import bz2
import contextlib
import copy
import io
import json
Expand All @@ -45,10 +49,10 @@
from tests import _workflows
from tests.project.test_conda_native_platforms_1822 import DISTRIBUTION, SHIM, elf, load, pe
from tests.project.test_release_gates import PUBLISHING_MARKERS, STATUS, evaluate_condition
from util import release_conda
from util import _actions, release_conda, release_tiers

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

#: Test tier (#1958): reads the workflow and recipe files, and drives the script offline.
__tier__ = 'LATEST'
Expand Down Expand Up @@ -370,5 +374,70 @@ def test_a_windows_package_for_another_python_fails(self) -> None:
self.assertIn('is built for python2.7, and this is ', log)


@contextlib.contextmanager
def without_environb(**environ: 'str') -> 'Iterator[None]':
"""``os`` as Windows has it, no :data:`os.environb`, with ``environ`` set and nothing else (#2120)."""
environb = os.environb
with mock.patch.dict(os.environ, environ, clear=True), \
mock.patch.object(os, 'supports_bytes_environ', False):
del os.environb
try:
yield
finally:
os.environb = environb


class TestTheEnvironmentWithoutEnvironb(unittest.TestCase):
"""#2120: each ``var()``, and the helper they share, on an ``os`` with no ``environb``."""

VALUE = 'caf\xe9 windows-latest'

def test_the_emulation_has_no_environb(self) -> None:
with without_environb():
self.assertFalse(hasattr(os, 'environb'))
self.assertTrue(hasattr(os, 'environb'))

def test_the_helper_reads_os_environ(self) -> None:
with without_environb(PCAPKIT_CONDA_OS=self.VALUE):
self.assertEqual(_actions.getenvb('PCAPKIT_CONDA_OS'), os.fsencode(self.VALUE))
with self.assertRaises(KeyError):
_actions.getenvb('PCAPKIT_UNSET')

def test_the_helper_reads_os_environb_where_it_exists(self) -> None:
with mock.patch.dict(os.environb, {b'PCAPKIT_CONDA_OS': b'\xff'}):
self.assertEqual(_actions.getenvb('PCAPKIT_CONDA_OS'), b'\xff')

def test_each_var_reads_the_value_and_refuses_an_unset_one(self) -> None:
for module in (release_conda.release_published, release_tiers):
with self.subTest(module=module.__name__), without_environb(PCAPKIT_CONDA_OS=self.VALUE):
self.assertEqual(module.var('PCAPKIT_CONDA_OS'), os.fsencode(self.VALUE))
with self.assertRaises(module.Unbound) as caught:
module.var('PCAPKIT_UNSET')
self.assertEqual(str(caught.exception), 'PCAPKIT_UNSET')

def test_build_gets_past_its_variables(self) -> None:
"""``build`` reads both variables and runs ``conda build``, where it raised AttributeError."""
with tempfile.TemporaryDirectory() as temp:
output = os.path.join(temp, 'output')
out_dir = os.path.join(temp, 'compilation-x')
with without_environb(PCAPKIT_CONDA_OS=WINDOWS, PCAPKIT_PYTHON_VERSION='3.15', GITHUB_OUTPUT=output), \
mock.patch.object(release_conda.tempfile, 'mkdtemp', return_value=out_dir), \
mock.patch.object(release_conda, 'call', return_value=7) as call:
status = release_conda.main(['build'])
with open(output, encoding='utf-8') as file:
written = file.read()
self.assertEqual(status, 7)
call.assert_called_once_with('conda', 'build', '.', '--package-format', '.tar.bz2', '--no-anaconda-upload',
'--output-folder', out_dir, '--python', '3.15')
self.assertEqual(written, f'out_dir={out_dir}\n')

def test_build_still_names_an_unset_variable(self) -> None:
err = io.StringIO()
with without_environb(), mock.patch.object(release_conda, 'call') as call, mock.patch('sys.stderr', err):
status = release_conda.main(['build'])
self.assertEqual((status, err.getvalue()), (1, 'PCAPKIT_CONDA_OS: unbound variable\n'))
call.assert_not_called()


if __name__ == '__main__':
unittest.main()
30 changes: 23 additions & 7 deletions util/_actions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
"""The GitHub Actions plumbing the :file:`util/` scripts share (#2047).

Workflow commands, ``$GITHUB_OUTPUT`` and ``$GITHUB_STEP_SUMMARY`` writes, and
the run that copies a command's output to the log and to a file, written once
here instead of in each script. A script that the steps run as ``python
util/<script>.py`` has :file:`util/` first on :data:`sys.path`, so it imports
this module as ``_actions``; imported as ``util.<script>``, or loaded by path,
from the repository root, it imports ``util._actions``.
Workflow commands, ``$GITHUB_OUTPUT`` and ``$GITHUB_STEP_SUMMARY`` writes, the
run that copies a command's output to the log and to a file, and a variable
read as bytes on Windows too (#2120), written once here instead of in each
script. A script that the steps run as ``python util/<script>.py`` has
:file:`util/` first on :data:`sys.path`, so it imports this module as
``_actions``; imported as ``util.<script>``, or loaded by path, from the
repository root, it imports ``util._actions``.

The scripts run on the image's ``/usr/bin/python3``, or on a leg's own
interpreter, which may be 3.7, GraalPy or a free-threaded build, and
Expand Down Expand Up @@ -44,7 +45,7 @@
from typing import IO, Iterator, Mapping, Optional, Sequence, Tuple, Union

__all__ = ['LEVELS', 'SUMMARY_COPY', 'escape_data', 'escape_property', 'annotation', 'annotate', 'group',
'set_output', 'summary', 'open_summary', 'emit', 'say', 'exit_status', 'run']
'set_output', 'summary', 'open_summary', 'emit', 'say', 'exit_status', 'run', 'getenvb']

#: The annotation commands, by level.
LEVELS = ('error', 'warning', 'notice')
Expand Down Expand Up @@ -253,3 +254,18 @@ def run(command: 'Sequence[str]', log_path: 'Union[str, PathLike[str]]') -> 'Tup
output.extend(line)
status = process.returncode
return status, bytes(output)


def getenvb(name: 'str') -> 'bytes':
"""``$name``, as bytes; :exc:`KeyError` where it is unset.

That is :data:`os.environb`'s value where :data:`os.supports_bytes_environ`
holds, and :data:`os.environ`'s, encoded as :func:`os.fsencode` encodes it,
where it does not, as on Windows, which has no :data:`os.environb`.

"""
if os.supports_bytes_environ:
return os.environb[name.encode()]
# #2120: Windows keeps its environment as text and has no os.environb, so
# reading it there raised AttributeError on the win-64 conda leg.
return os.fsencode(os.environ[name])
4 changes: 3 additions & 1 deletion util/release_published.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ def __init__(self, status: 'int') -> 'None':
def var(name: 'str') -> 'bytes':
"""``$name``, as bytes; :exc:`Unbound` where it is unset."""
try:
return os.environb[name.encode()]
# #2120: through the helper, which reads os.environ where os.environb is
# missing, as on Windows.
return _actions.getenvb(name)
except KeyError:
raise Unbound(name) from None

Expand Down
4 changes: 3 additions & 1 deletion util/release_tiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ def __init__(self, status: 'int') -> 'None':
def var(name: 'str') -> 'bytes':
"""``$name``, as bytes; :exc:`Unbound` where it is unset."""
try:
return os.environb[name.encode()]
# #2120: through the helper, which reads os.environ where os.environb is
# missing, as on Windows.
return _actions.getenvb(name)
except KeyError:
raise Unbound(name) from None

Expand Down
Loading