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 @@ -103,7 +103,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "ubuntu-24.04-arm", "macos-latest", "windows-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.9", "pypy3.10"]
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt that there's much value in testing against two pypy versions here, so I removed one assuming the tail one should be enough.

python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"]
exclude:
- os: "macos-latest"
python-version: "pypy3.10"
Expand Down Expand Up @@ -140,7 +140,7 @@ jobs:
run: uv 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.9", "pypy3.10", "3.14"]'), matrix.python-version) }}
if: ${{ env.B2_TEST_APPLICATION_KEY != '' && env.B2_TEST_APPLICATION_KEY_ID != '' && contains(fromJSON('["3.10", "pypy3.10", "3.14"]'), matrix.python-version) }}
run: uv run nox -vs integration -p ${{ matrix.python-version }} -- -m "require_secrets" --cleanup
test-docker:
timeout-minutes: 90
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ With `nox`, you can run different sessions (default are `lint` and `test`):

* `format` -> Format the code.
* `lint` -> Run linters.
* `test` (`test-3.9`, `test-3.10`, `test-3.11`) -> Run test suite.
* `test` (`test-3.10`, `test-3.11`) -> Run test suite.
* `cover` -> Perform coverage analysis.
* `build` -> Build the distribution.
* `generate_dockerfile` -> generate dockerfile
Expand Down Expand Up @@ -97,7 +97,7 @@ Sessions other than that use the last given Python version.
You can change it:

```bash
export NOX_PYTHONS=3.9,3.10
export NOX_PYTHONS=3.10,3.13
```

With the above setting, session `test` will run on Python 3.9 and 3.10, and all other sessions on Python 3.10.
Expand Down
2 changes: 1 addition & 1 deletion b2.spec.template
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ a = Analysis(['b2/_internal/${VERSION}/__main__.py'],
pathex=['.'],
binaries=[],
datas=datas,
hiddenimports=['pkg_resources.extern', 'pkg_resources.py2_warn'],
hiddenimports=[],
hookspath=['pyinstaller-hooks'],
runtime_hooks=[],
win_no_prefer_redirects=False,
Expand Down
1 change: 1 addition & 0 deletions changelog.d/+drop-python-3.9.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Drop support for Python 3.9 and PyPy 3.9.
1 change: 1 addition & 0 deletions changelog.d/+non-utf8-test-redesign.infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rewrite the test case verifying non-UTF-8 help rendering, replacing subtests with parametrization, cover nested commands.
1 change: 1 addition & 0 deletions changelog.d/+pyinstaller-6.infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bump PyInstaller version and remove obsolete hidden imports from the PyInstaller spec file.
1 change: 1 addition & 0 deletions changelog.d/+pytest.infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bump pytest and other test dependencies versions.
2 changes: 0 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@

PYTHON_VERSIONS = (
[
'pypy3.9',
'pypy3.10',
'3.9',
'3.10',
'3.11',
'3.12',
Expand Down
29 changes: 14 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ authors = [
{ name = "Backblaze Inc", email = "support@backblaze.com" },
]
dynamic = ["version"]
requires-python = ">=3.9"
requires-python = ">=3.10"
keywords = ["backblaze b2 cloud storage"]
license = {text = "MIT"}
readme = "README.md"
Expand All @@ -15,7 +15,6 @@ classifiers = [
"Topic :: Software Development :: Libraries",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -39,7 +38,7 @@ full = [
"pydantic>=2.0.1,<3"
]
license = [
"pip>=23.1.0",
"pip>=26.1",
"pip-licenses~=5.0",
"pipdeptree>=2.9,<3",
"prettytable~=3.9",
Expand All @@ -62,27 +61,27 @@ format = [
]
lint = [
"ruff~=0.8.4",
"pytest==8.3.3",
"tenacity>=8.2.3,<9",
"pytest>=9.0.3,<10",
"tenacity>=9.1.4,<10",
"liccheck>=0.9.2",
"setuptools>=60,<80", # required by liccheck
]
release = [
"towncrier==23.11.0",
]
test = [
"coverage==7.2.7",
"pexpect==4.9.0",
"pytest==8.3.3",
"pytest-cov==3.0.0",
"pytest-forked==1.6.0",
"pytest-xdist==2.5.0",
"pytest-watcher==0.4.3",
"tenacity>=8.2.3,<9",
"more-itertools==8.13.0",
"coverage>=7.13.5,<8",
"pexpect>=4.9.0,<5",
"pytest>=9.0.3,<10",
"pytest-cov>=7.1.0,<8",
"pytest-forked>=1.6.0,<2",
"pytest-xdist>=3.8.0,<4",
"pytest-watcher>=0.6.3,<1",
"tenacity>=9.1.4,<10",
"more-itertools>=11.0.2,<12",
]
bundle = [
"pyinstaller<6,>=5.13; python_version < \"3.13\"",
"pyinstaller>=6.15,<7",
"pyinstaller-hooks-contrib>=2023.6",
"patchelf-wrapper==1.2.0; platform_system == \"Linux\"",
"staticx~=0.14.1; platform_system == \"Linux\"",
Expand Down
78 changes: 43 additions & 35 deletions test/unit/test_arg_parser.py
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to rewrite the existing test case, because the newer version of pytest had some issues with serializing classes when using subTests. Going away from subTests seems like a sane thing to do in any case

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import argparse
import sys

import pytest

from b2._internal._cli.arg_parser_types import (
parse_comma_separated_list,
parse_millis_from_float_timestamp,
Expand Down Expand Up @@ -43,48 +45,54 @@ def test_parse_range(self):
parse_range('!@#,%^&')


class TestNonUTF8TerminalSupport(TestBase):
class ASCIIEncodedStream:
def __init__(self, original_stream):
self.original_stream = original_stream
self.encoding = 'ascii'
class _ASCIIEncodedStream:
def __init__(self, original_stream):
self.original_stream = original_stream
self.encoding = 'ascii'

def write(self, data):
if isinstance(data, str):
data = data.encode(self.encoding, 'strict')
self.original_stream.buffer.write(data)

def flush(self):
self.original_stream.flush()


def _build_command_names_classes_mapping() -> dict[str, type]:
"""
Recursively build a dictionary of all command names and corresponding classes, including all level subcommands
"""

def write(self, data):
if isinstance(data, str):
data = data.encode(self.encoding, 'strict')
self.original_stream.buffer.write(data)
command_classes = {}

def flush(self):
self.original_stream.flush()
def _walk_command_classes(command_name: str, command_class: type) -> None:
assert command_name not in command_classes
command_classes[command_name] = command_class

def check_help_string(self, command_class, command_name):
help_string = command_class.__doc__
registry = getattr(command_class, 'subcommands_registry', None)
if registry:
for subcommand_name, subcommand_class in registry.items():
_walk_command_classes(f'{command_name} {subcommand_name}', subcommand_class)

# create a parser with a help message that is based on the command_class.__doc__ string
parser = B2ArgumentParser(description=help_string)
_walk_command_classes('b2', B2)
return command_classes

try:
old_stdout = sys.stdout
old_stderr = sys.stderr
sys.stdout = TestNonUTF8TerminalSupport.ASCIIEncodedStream(sys.stdout)
sys.stderr = TestNonUTF8TerminalSupport.ASCIIEncodedStream(sys.stderr)

parser.print_help()
COMMAND_NAMES_CLASSES_MAPPING = _build_command_names_classes_mapping()

except UnicodeEncodeError as e:
self.fail(
f'Failed to encode help message for command "{command_name}" on a non-UTF-8 terminal: {e}'
)

finally:
# Restore original stdout and stderr
sys.stdout = old_stdout
sys.stderr = old_stderr
@pytest.mark.parametrize('command_name', COMMAND_NAMES_CLASSES_MAPPING)
def test_help_in_non_utf8_terminal(command_name: str, monkeypatch):
command_class = COMMAND_NAMES_CLASSES_MAPPING[command_name]
parser = B2ArgumentParser(description=command_class.__doc__)

def test_help_in_non_utf8_terminal(self):
command_classes = dict(B2.subcommands_registry.items())
command_classes['b2'] = B2
monkeypatch.setattr(sys, 'stdout', _ASCIIEncodedStream(sys.stdout))
monkeypatch.setattr(sys, 'stderr', _ASCIIEncodedStream(sys.stderr))

for command_name, command_class in command_classes.items():
with self.subTest(command_class=command_class, command_name=command_name):
self.check_help_string(command_class, command_name)
try:
parser.print_help()
except UnicodeEncodeError as e:
pytest.fail(
f'Failed to encode help message for command "{command_name}" on a non-UTF-8 terminal: {e}'
)
Loading
Loading