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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "ubuntu-24.04-arm", "macos-latest", "windows-latest"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9", "pypy3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.9", "pypy3.10"]
exclude:
- os: "macos-latest"
python-version: "pypy3.10"
Expand Down Expand Up @@ -118,7 +118,7 @@ jobs:
run: nox -vs integration -p ${{ matrix.python-version }} -- -m "not require_secrets"
- name: Run integration tests (with secrets)
# Limit CI workload by running integration tests with secrets only on edge Python versions.
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' && contains(fromJSON('["3.8", "pypy3.10", "3.13"]'), matrix.python-version) }}
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' && contains(fromJSON('["3.8", "pypy3.10", "3.14"]'), matrix.python-version) }}
run: nox -vs integration -p ${{ matrix.python-version }} -- -m "require_secrets" --cleanup
test-docker:
timeout-minutes: 90
Expand Down
9 changes: 8 additions & 1 deletion b2/_internal/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,14 @@ def _encode_description(self, value: str):
return textwrap.dedent(value)
else:
encoding = self._get_encoding()
return rst2ansi(value.encode(encoding), output_encoding=encoding)
try:
return rst2ansi(value.encode(encoding), output_encoding=encoding)
except SystemError:
# FALLBACK(PARSER): rst2ansi can raise SystemError on Python 3.14+ due to
# buffer overflow bug in get_terminal_size ioctl call.
# See: https://github.com/Backblaze/B2_Command_Line_Tool/issues/1119
# TODO-REMOVE-BY: When rst2ansi is updated or replaced
return textwrap.dedent(value)

def _make_short_description(self, usage: str, raw_description: str) -> str:
if usage:
Expand Down
1 change: 1 addition & 0 deletions changelog.d/+python-3.14-support.infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added Python 3.14 support to CI/CD pipeline and test matrix.
1 change: 1 addition & 0 deletions changelog.d/1119.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed SystemError buffer overflow crash on Python 3.14+ caused by rst2ansi's terminal size detection bug. The CLI now gracefully handles this error and continues to function normally.
1 change: 1 addition & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'3.11',
'3.12',
'3.13',
'3.14',
]
if NOX_PYTHONS is None
else NOX_PYTHONS.split(',')
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"argcomplete>=3.5.2,<4",
Expand Down
36 changes: 36 additions & 0 deletions test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#
######################################################################
import platform
import sys

import pexpect
import pytest

_MISSING = object()
Expand All @@ -21,6 +23,40 @@ def skip_on_windows(*args, reason='Not supported on Windows', **kwargs):
)(*args, **kwargs)


def patched_spawn(*args, **kwargs):
"""
Wrapper around pexpect.spawn with improved error messages.

pexpect's errors are confusing to interpret when things go wrong,
because it doesn't output the actual stdout by default. This wrapper
addresses that inconvenience.
"""
instance = pexpect.spawn(*args, **kwargs)

def _patch_expect(func):
def _wrapper(pattern_list, **kwargs):
try:
return func(pattern_list, **kwargs)
except pexpect.exceptions.TIMEOUT as exc:
raise pexpect.exceptions.TIMEOUT(
f'Timeout reached waiting for `{pattern_list}`'
) from exc
except pexpect.exceptions.EOF as exc:
raise pexpect.exceptions.EOF(f'Received EOF waiting for `{pattern_list}`') from exc
except Exception as exc:
raise RuntimeError(f'Unexpected error waiting for `{pattern_list}`') from exc

return _wrapper

instance.expect = _patch_expect(instance.expect)
instance.expect_exact = _patch_expect(instance.expect_exact)

# capture child shell's output for debugging
instance.logfile = sys.stdout.buffer

return instance


def b2_uri_args_v3(bucket_name, path=_MISSING):
if path is _MISSING:
return [bucket_name]
Expand Down
10 changes: 9 additions & 1 deletion test/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,15 @@ def print_json_indented(value):


def remove_warnings(text):
return linesep.join(line for line in text.split(linesep) if 'DeprecationWarning' not in line)
"""Filter out Python warnings from command output."""
return linesep.join(
line
for line in text.split(linesep)
if 'DeprecationWarning' not in line
and 'resource_tracker' not in line # Python 3.14+ multiprocessing warnings
and 'UserWarning' not in line # Python 3.14+ shows more warning details
and 'warnings.warn(' not in line # Python 3.14+ shows source line in warnings
)


class StringReader:
Expand Down
37 changes: 1 addition & 36 deletions test/integration/test_autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import pexpect
import pytest

from test.helpers import skip_on_windows
from test.helpers import patched_spawn, skip_on_windows

TIMEOUT = 120 # CI can be slow at times when parallelization is extreme

Expand All @@ -27,41 +27,6 @@
"""


def patched_spawn(*args, **kwargs):
"""
Patch pexpect.spawn to improve error messages
"""

instance = pexpect.spawn(*args, **kwargs)

def _patch_expect(func):
def _wrapper(pattern_list, **kwargs):
try:
return func(pattern_list, **kwargs)
except pexpect.exceptions.TIMEOUT as exc:
raise pexpect.exceptions.TIMEOUT(
f'Timeout reached waiting for `{pattern_list}` to be autocompleted'
) from exc
except pexpect.exceptions.EOF as exc:
raise pexpect.exceptions.EOF(
f'Received EOF waiting for `{pattern_list}` to be autocompleted'
) from exc
except Exception as exc:
raise RuntimeError(
f'Unexpected error waiting for `{pattern_list}` to be autocompleted'
) from exc

return _wrapper

instance.expect = _patch_expect(instance.expect)
instance.expect_exact = _patch_expect(instance.expect_exact)

# capture child shell's output for debugging
instance.logfile = sys.stdout.buffer

return instance


@pytest.fixture(scope='session')
def bashrc(homedir):
bashrc_path = homedir / '.bashrc'
Expand Down
57 changes: 57 additions & 0 deletions test/integration/test_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
import os
import platform
import re
import subprocess

import pexpect
import pytest

from test.helpers import patched_spawn


def test_help(cli_version):
p = subprocess.run(
Expand All @@ -26,3 +32,54 @@ def test_help(cli_version):
expected_name += '.exe'
assert re.match(r'^_?b2(v\d+)?(\.exe)?$', expected_name) # test sanity check
assert f'{expected_name} <command> --help' in p.stdout


@pytest.mark.skipif(
platform.system() == 'Windows',
reason='PTY tests require Unix-like system',
)
def test_help_with_tty(cli_version):
"""
Test that B2 CLI --help works correctly with a real PTY.

Verifies fix for rst2ansi buffer overflow on Python 3.14+.
See: https://github.com/Backblaze/B2_Command_Line_Tool/issues/1119

NOTE: Works in CI with pytest-xdist, but may not trigger the bug locally.
"""
# Set up environment - remove LINES/COLUMNS to ensure ioctl is called
env = os.environ.copy()
env.pop('LINES', None)
env.pop('COLUMNS', None)

# Spawn b2 --help with pexpect to create a real PTY
# This is where the bug would trigger on Python 3.14 without our fix
child = patched_spawn(
cli_version,
['--help'],
env=env,
timeout=10,
)

# Wait for process to complete
child.expect(pexpect.EOF)

# Get the output
output = child.before.decode('utf-8', errors='replace')

# Check exit status
child.close()
exit_code = child.exitstatus

# Verify the command succeeded and produced help output
assert exit_code == 0, (
f'b2 --help failed with exit code {exit_code}.\n'
f'This may indicate the buffer overflow bug is not properly handled.\n'
f'Output: {output}\n'
f'See: https://github.com/Backblaze/B2_Command_Line_Tool/issues/1119'
)

# Verify help output contains expected content
assert (
'b2 <command>' in output or cli_version in output
), f'Help output does not contain expected content.\nOutput: {output}'
Loading