From a1099bbaa2d955a885d55376b2be274de17280c0 Mon Sep 17 00:00:00 2001 From: EnumDev Date: Fri, 25 Sep 2026 19:55:19 +0300 Subject: [PATCH] Initial commit --- .gitignore | 3 + check-version.sh | 9 ++ info.yml | 28 +++++ recipe.sh | 34 ++++++ ...he-multiprocessing-forkserver-method.patch | 105 ++++++++++++++++++ source-files/fix-test.patch | 13 +++ source-files/python-3.13-compatibility.patch | 37 ++++++ 7 files changed, 229 insertions(+) create mode 100644 .gitignore create mode 100644 check-version.sh create mode 100644 info.yml create mode 100644 recipe.sh create mode 100644 source-files/avoid-the-multiprocessing-forkserver-method.patch create mode 100644 source-files/fix-test.patch create mode 100644 source-files/python-3.13-compatibility.patch diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d96df7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Ignore BPM archives and signatures +*.bpm +*.bpm.sig diff --git a/check-version.sh b/check-version.sh new file mode 100644 index 0000000..8eba508 --- /dev/null +++ b/check-version.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Github repository +REPO="pexpect/pexpect" + +# Get tag name using Github Rest API +TAG_NAME=$(curl -s -L -H "X-GitHub-Api-Version: 2022-11-28" -H "Authorization: token ${GITHUB_API_TOKEN}" https://api.github.com/repos/"$REPO"/releases/latest | jq -r '.tag_name') + +echo "$TAG_NAME" diff --git a/info.yml b/info.yml new file mode 100644 index 0000000..31e5752 --- /dev/null +++ b/info.yml @@ -0,0 +1,28 @@ +name: python-pexpect +description: For controlling and automating applications +version: "4.9" +revision: 1 +url: https://github.com/pexpect/pexpect +license: ISC +maintainers: + - enumdev@enumerated.dev +architecture: any +output_architecture: any +type: source +depends: + - python3=3.14.* + - python-ptyprocess +make_depends: + - python-build + - python-installer + - python-setuptools + - python-wheel +check_depends: + - man-db + - python-pytest + - zsh +downloads: + - url: https://github.com/pexpect/pexpect/archive/refs/tags/${BPM_PKG_VERSION}.tar.gz + extract_to: ${BPM_SOURCE} + extract_strip_components: 1 + checksum: 13756b834dba0adf7603be77d943cc427011983ecd7c58b59b9bf122a1063e40 diff --git a/recipe.sh b/recipe.sh new file mode 100644 index 0000000..23d8e01 --- /dev/null +++ b/recipe.sh @@ -0,0 +1,34 @@ +# This is the recipe.sh script. It is executed by BPM in a temporary directory when compiling a source package +# BPM Expects the source code to be extracted into the automatically created 'source' directory which can be accessed using $BPM_SOURCE +# BPM Expects the output files to be present in the automatically created 'output' directory which can be accessed using $BPM_OUTPUT + +# The prepare function is executed in the root of the temp directory +# This function is used for putting downloaded files to the correct location or applying patches +prepare() { + cd "$BPM_SOURCE" + patch -Np1 -i "$BPM_WORKDIR"/python-3.13-compatibility.patch + patch -Np1 -i "$BPM_WORKDIR"/fix-test.patch + patch -Np1 -i "$BPM_WORKDIR"/avoid-the-multiprocessing-forkserver-method.patch + +} + +# The build function is executed in the source directory +# This function is used to compile the source code +build() { + python3 -m build --wheel --no-isolation . +} + +# The check function is executed in the source directory +# This function is used to run tests to verify the package has been compiled correctly +check() { + pytest +} + +# The package function is executed in the source directory +# This function is used to move the compiled files into the output directory +package() { + python3 -m installer --destdir="$BPM_OUTPUT" dist/*.whl + + # Install package license + install -Dm644 "$BPM_SOURCE"/LICENSE "$BPM_OUTPUT"/usr/share/licenses/python-pexpect/LICENSE +} diff --git a/source-files/avoid-the-multiprocessing-forkserver-method.patch b/source-files/avoid-the-multiprocessing-forkserver-method.patch new file mode 100644 index 0000000..9916481 --- /dev/null +++ b/source-files/avoid-the-multiprocessing-forkserver-method.patch @@ -0,0 +1,105 @@ + +From 456bc10d94b57e254568e7ea9a8b3cffb856ebff Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= +Date: Fri, 22 Nov 2024 16:41:55 +0100 +Subject: [PATCH] Tests: Avoid the multiprocessing forkserver method + +Fixes https://github.com/pexpect/pexpect/issues/807 +--- + tests/test_expect.py | 12 ++++++++++-- + tests/test_socket.py | 24 ++++++++++++++++-------- + 2 files changed, 26 insertions(+), 10 deletions(-) + +diff --git a/tests/test_expect.py b/tests/test_expect.py +index c16e0551..fb1e30e2 100755 +--- a/tests/test_expect.py ++++ b/tests/test_expect.py +@@ -33,6 +33,14 @@ + + PY3 = bool(sys.version_info.major >= 3) + ++# Python 3.14 changed the non-macOS POSIX default to forkserver ++# but the code in this module does not work with it ++# See https://github.com/python/cpython/issues/125714 ++if multiprocessing.get_start_method() == 'forkserver': ++ mp_context = multiprocessing.get_context(method='fork') ++else: ++ mp_context = multiprocessing.get_context() ++ + # Many of these test cases blindly assume that sequential directory + # listings of the /bin directory will yield the same results. + # This may not be true, but seems adequate for testing now. +@@ -682,7 +690,7 @@ def test_stdin_closed(self): + ''' + Ensure pexpect continues to operate even when stdin is closed + ''' +- class Closed_stdin_proc(multiprocessing.Process): ++ class Closed_stdin_proc(mp_context.Process): + def run(self): + sys.__stdin__.close() + cat = pexpect.spawn('cat') +@@ -698,7 +706,7 @@ def test_stdin_stdout_closed(self): + ''' + Ensure pexpect continues to operate even when stdin and stdout is closed + ''' +- class Closed_stdin_stdout_proc(multiprocessing.Process): ++ class Closed_stdin_stdout_proc(mp_context.Process): + def run(self): + sys.__stdin__.close() + sys.__stdout__.close() +diff --git a/tests/test_socket.py b/tests/test_socket.py +index b801b00a..6521d368 100644 +--- a/tests/test_socket.py ++++ b/tests/test_socket.py +@@ -29,6 +29,14 @@ + import time + import errno + ++# Python 3.14 changed the non-macOS POSIX default to forkserver ++# but the code in this module does not work with it ++# See https://github.com/python/cpython/issues/125714 ++if multiprocessing.get_start_method() == 'forkserver': ++ mp_context = multiprocessing.get_context(method='fork') ++else: ++ mp_context = multiprocessing.get_context() ++ + + class SocketServerError(Exception): + pass +@@ -83,8 +91,8 @@ def setUp(self): + self.prompt3 = b'Press X to exit:' + self.enter = b'\r\n' + self.exit = b'X\r\n' +- self.server_up = multiprocessing.Event() +- self.server_process = multiprocessing.Process(target=self.socket_server, args=(self.server_up,)) ++ self.server_up = mp_context.Event() ++ self.server_process = mp_context.Process(target=self.socket_server, args=(self.server_up,)) + self.server_process.daemon = True + self.server_process.start() + counter = 0 +@@ -189,9 +197,9 @@ def test_timeout(self): + session.expect(b'Bogus response') + + def test_interrupt(self): +- timed_out = multiprocessing.Event() +- all_read = multiprocessing.Event() +- test_proc = multiprocessing.Process(target=self.socket_fn, args=(timed_out, all_read)) ++ timed_out = mp_context.Event() ++ all_read = mp_context.Event() ++ test_proc = mp_context.Process(target=self.socket_fn, args=(timed_out, all_read)) + test_proc.daemon = True + test_proc.start() + while not all_read.is_set(): +@@ -203,9 +211,9 @@ def test_interrupt(self): + self.assertEqual(test_proc.exitcode, errno.ETIMEDOUT) + + def test_multiple_interrupts(self): +- timed_out = multiprocessing.Event() +- all_read = multiprocessing.Event() +- test_proc = multiprocessing.Process(target=self.socket_fn, args=(timed_out, all_read)) ++ timed_out = mp_context.Event() ++ all_read = mp_context.Event() ++ test_proc = mp_context.Process(target=self.socket_fn, args=(timed_out, all_read)) + test_proc.daemon = True + test_proc.start() + while not all_read.is_set(): diff --git a/source-files/fix-test.patch b/source-files/fix-test.patch new file mode 100644 index 0000000..2293da2 --- /dev/null +++ b/source-files/fix-test.patch @@ -0,0 +1,13 @@ +diff --git a/tests/test_replwrap.py b/tests/test_replwrap.py +index 5ac782a..993e576 100644 +--- a/tests/test_replwrap.py ++++ b/tests/test_replwrap.py +@@ -92,6 +92,8 @@ class REPLWrapTestCase(unittest.TestCase): + "PS1='{0}' PS2='{1}' " + "PROMPT_COMMAND=''") + ++ repl.run_command("bind 'set enable-bracketed-paste off'") ++ + print(repl) + res = repl.run_command("echo $HOME") + print(res) diff --git a/source-files/python-3.13-compatibility.patch b/source-files/python-3.13-compatibility.patch new file mode 100644 index 0000000..838cfdd --- /dev/null +++ b/source-files/python-3.13-compatibility.patch @@ -0,0 +1,37 @@ +From 95d09c54d2502d8d48f2da591089ceb6d09df056 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= +Date: Wed, 29 May 2024 12:21:52 +0200 +Subject: [PATCH] Force NO_COLOR=1 to fix test failures with Python 3.13+ REPL + +Python 3.13+ has colors now. Always setting this variable should be safe. +--- + pexpect/replwrap.py | 2 +- + tests/test_replwrap.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/pexpect/replwrap.py b/pexpect/replwrap.py +index 08dbd5e8..c8714a23 100644 +--- a/pexpect/replwrap.py ++++ b/pexpect/replwrap.py +@@ -35,7 +35,7 @@ def __init__(self, cmd_or_spawn, orig_prompt, prompt_change, + continuation_prompt=PEXPECT_CONTINUATION_PROMPT, + extra_init_cmd=None): + if isinstance(cmd_or_spawn, basestring): +- self.child = pexpect.spawn(cmd_or_spawn, echo=False, encoding='utf-8') ++ self.child = pexpect.spawn(cmd_or_spawn, echo=False, encoding='utf-8', env={'NO_COLOR': '1'}) + else: + self.child = cmd_or_spawn + if self.child.echo: +diff --git a/tests/test_replwrap.py b/tests/test_replwrap.py +index ddafa5d6..5ac782a4 100644 +--- a/tests/test_replwrap.py ++++ b/tests/test_replwrap.py +@@ -124,7 +124,7 @@ def test_no_change_prompt(self): + if platform.python_implementation() == 'PyPy': + raise unittest.SkipTest(skip_pypy) + +- child = pexpect.spawn(sys.executable, echo=False, timeout=5, encoding='utf-8') ++ child = pexpect.spawn(sys.executable, echo=False, timeout=5, encoding='utf-8', env={'NO_COLOR': '1'}) + # prompt_change=None should mean no prompt change + py = replwrap.REPLWrapper(child, u">>> ", prompt_change=None, + continuation_prompt=u"... ")