diff --git a/b2/_internal/arg_parser.py b/b2/_internal/arg_parser.py index 52a27245..b661d9fc 100644 --- a/b2/_internal/arg_parser.py +++ b/b2/_internal/arg_parser.py @@ -12,13 +12,15 @@ import argparse import contextlib import functools +import io import locale import re import sys import textwrap import unittest.mock -from rst2ansi import rst2ansi +from rich.console import Console +from rich_rst import RestructuredText try: getencoding = locale.getencoding @@ -127,15 +129,14 @@ def _encode_description(self, value: str): return textwrap.dedent(value) else: encoding = self._get_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 + if encoding == 'ascii': return textwrap.dedent(value) + buf = io.StringIO() + console = Console(file=buf, color_system='standard') + console.print(RestructuredText(textwrap.dedent(value))) + return buf.getvalue().rstrip() + def _get_short_description(self) -> str: if not self._raw_description: return '' diff --git a/b2/_internal/console_tool.py b/b2/_internal/console_tool.py index b3a931cf..dbfd13e7 100644 --- a/b2/_internal/console_tool.py +++ b/b2/_internal/console_tool.py @@ -52,7 +52,6 @@ import b2sdk import requests -import rst2ansi from b2sdk.v3 import ( ALL_CAPABILITIES, B2_ACCOUNT_INFO_DEFAULT_FILE, @@ -421,7 +420,7 @@ class DestinationSseMixin(Described): Using SSE-C requires providing ``{B2_DESTINATION_SSE_C_KEY_B64_ENV_VAR}`` environment variable, containing the base64 encoded encryption key. If ``{B2_DESTINATION_SSE_C_KEY_ID_ENV_VAR}`` environment variable is provided, - it's value will be saved as ``{SSE_C_KEY_ID_FILE_INFO_KEY_NAME}`` in the + its value will be saved as ``{SSE_C_KEY_ID_FILE_INFO_KEY_NAME}`` in the uploaded file's fileInfo. """ @@ -4292,7 +4291,7 @@ class License(Command): # pragma: no cover # overrides to the license text extracted by piplicenses. # Thanks to this set, we make sure the module is still used # PTable is used on versions below Python 3.11 - MODULES_TO_OVERRIDE_LICENSE_TEXT = {'rst2ansi', 'b2sdk'} + MODULES_TO_OVERRIDE_LICENSE_TEXT = {'b2sdk'} LICENSES = { 'argcomplete': 'https://raw.githubusercontent.com/kislyuk/argcomplete/develop/LICENSE.rst', @@ -4445,12 +4444,7 @@ def _fetch_license_from_url(self, url: str) -> str: def _get_single_license(self, module_dict: dict): license_ = module_dict['LicenseText'] module_name = module_dict['Name'] - if module_name == 'rst2ansi': - # this one module is problematic, we need to extract the license text from its docstring - assert license_ == piplicenses.LICENSE_UNKNOWN # let's make sure they didn't fix it - license_ = rst2ansi.__doc__ - assert 'MIT License' in license_ # let's make sure the license is still there - elif module_name == 'b2sdk': + if module_name == 'b2sdk': license_ = (pathlib.Path(b2sdk.__file__).parent / 'LICENSE').read_text() else: license_url = self.LICENSES.get(module_name) or self.LICENSES.get( diff --git a/changelog.d/+migrate-from-rst2ansi-to-rich.md b/changelog.d/+migrate-from-rst2ansi-to-rich.md new file mode 100644 index 00000000..9b0fa7cb --- /dev/null +++ b/changelog.d/+migrate-from-rst2ansi-to-rich.md @@ -0,0 +1 @@ +Replace rst2ansi man-page rendering with Rich, fixing Python 3.14+ compatibility issues. diff --git a/noxfile.py b/noxfile.py index 0a9626e8..3446a3a1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -235,8 +235,6 @@ def run_integration_test(session, pytest_posargs): '2' if CI else 'auto', '--log-level', 'INFO', - '-W', - 'ignore::DeprecationWarning:rst2ansi.visitor:', *PYTEST_GLOBAL_ARGS, *pytest_posargs, ] diff --git a/pyproject.toml b/pyproject.toml index 923d48cb..4f55a58b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ dependencies = [ "tabulate==0.9.0", "tqdm>=4.65.0,<5", "platformdirs>=3.11.0,<5", + "rich>=15.0.0", + "rich-rst>=2.0.1", ] [project.optional-dependencies] @@ -116,6 +118,7 @@ authorized_licenses = [ "bsd license", "new bsd license", "simplified bsd", + "bsd-2-clause", "bsd-3-clause", "apache", "apache 2.0", diff --git a/uv.lock b/uv.lock index 7ad8b396..6c4efaec 100644 --- a/uv.lock +++ b/uv.lock @@ -88,6 +88,8 @@ dependencies = [ { name = "docutils" }, { name = "idna", marker = "platform_system == 'Java'" }, { name = "platformdirs" }, + { name = "rich" }, + { name = "rich-rst" }, { name = "rst2ansi" }, { name = "tabulate" }, { name = "tqdm" }, @@ -162,6 +164,8 @@ requires-dist = [ { name = "platformdirs", specifier = ">=3.11.0,<5" }, { name = "prettytable", marker = "extra == 'license'", specifier = "~=3.9" }, { name = "pydantic", marker = "extra == 'full'", specifier = ">=2.0.1,<3" }, + { name = "rich", specifier = ">=15.0.0" }, + { name = "rich-rst", specifier = ">=2.0.1" }, { name = "rst2ansi", specifier = "==0.1.5" }, { name = "tabulate", specifier = "==0.9.0" }, { name = "tqdm", specifier = ">=4.65.0,<5" }, @@ -698,6 +702,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -783,6 +799,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "more-itertools" version = "11.0.2" @@ -1242,6 +1267,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-rst" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/56/3191bae66b08ccc637ea8120426068bcb361cc323c96404c310886937067/rich_rst-2.0.1.tar.gz", hash = "sha256:cbe236ed0901d1ec8427cc6a50bf0a34353ba28ad014dc24def68bfe7f3b9e68", size = 300570, upload-time = "2026-05-16T00:47:57.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3d/55c17d3ebdf3cd81356002afe5bef9bb8af631db2819785b6eac845b925b/rich_rst-2.0.1-py3-none-any.whl", hash = "sha256:7ee15f345ce25fa02b582c272a6cdbaf0c21243e38061cea273cff659bf3ef61", size = 272922, upload-time = "2026-05-16T00:47:55.508Z" }, +] + [[package]] name = "rst2ansi" version = "0.1.5"