From 5daf48d5cb20b27e74a221cfcd7b88135ed9276e Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:52:15 +0800 Subject: [PATCH 1/4] fix: parse favorite total from labeled HTML --- src/jmcomic/jm_toolkit.py | 2 +- tests/test_jmcomic/test_jm_client.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/jmcomic/jm_toolkit.py b/src/jmcomic/jm_toolkit.py index 6dc8101e7..ba9017910 100644 --- a/src/jmcomic/jm_toolkit.py +++ b/src/jmcomic/jm_toolkit.py @@ -653,7 +653,7 @@ class JmPageTool: ) # 收藏夹的收藏总数 - pattern_html_favorite_total = compile(r' : (\d+)[^/]*/\D*(\d+)') + pattern_html_favorite_total = compile(r'(?:總數|总数)\s*:\s*(\d+)\s*/\s*(\d+)') # 所有的收藏夹 pattern_html_favorite_folder_list = [ diff --git a/tests/test_jmcomic/test_jm_client.py b/tests/test_jmcomic/test_jm_client.py index e84814dc0..aefe69467 100644 --- a/tests/test_jmcomic/test_jm_client.py +++ b/tests/test_jmcomic/test_jm_client.py @@ -427,6 +427,25 @@ def test_html_forum_comment_id_parsing(self): self.assertEqual(page[1].user_id, '201') self.assertEqual(page[1].album_id, '301') + def test_html_favorite_total_uses_labeled_count(self): + for label, expected_total in [('總數', 6), ('总数', 12)]: + with self.subTest(label=label): + html = f''' + +
{label} : {expected_total}\n / 600
+ + ''' + + page = JmPageTool.parse_html_to_favorite_page(html, page_number=1) + + self.assertEqual(page.total, expected_total) + self.assertEqual(page.page_number, 1) + def test_get_detail(self): client = self.client From eb8aaea1e7c424f19f43bbd58bba00116fbdb461 Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:20:12 +0800 Subject: [PATCH 2/4] release: prepare v2.7.5 --- CHANGELOG.md | 13 +++ src/jmcomic/__init__.py | 2 +- src/jmcomic/jm_async_client.py | 13 ++- src/jmcomic/jm_client_impl.py | 27 +++++-- src/jmcomic/jm_config.py | 2 +- src/jmcomic/jm_exception.py | 23 ++++++ src/jmcomic/jm_plugin.py | 10 ++- tests/test_jmcomic/test_jm_exception.py | 100 ++++++++++++++++++++++++ 8 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 tests/test_jmcomic/test_jm_exception.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 76c203da2..67b9680fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 条目分类参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [2.7.5] - 2026-08-25 + +### Summary + +本次更新修复网页端收藏夹总数误解析,并同步禁漫 APP 2.1.2 的移动端版本号。 + +### Fixed +- 修复收藏夹页面改版后,总数正则优先命中 CSS 尺寸、导致 `JmHtmlClient.favorite_folder().total` 错误返回 `50` 的问题。 + +### Changed +- 更新禁漫移动端版本号至 `2.1.2`。 +- `RequestRetryAllFailException` 现在会保留并输出各域名、各次重试的原始异常,便于定位真实失败原因。 + ## [2.7.4] - 2026-08-09 ### Summary diff --git a/src/jmcomic/__init__.py b/src/jmcomic/__init__.py index 48eb8ef28..fdf04d29a 100644 --- a/src/jmcomic/__init__.py +++ b/src/jmcomic/__init__.py @@ -2,7 +2,7 @@ # 被依赖方 <--- 使用方 # config <--- entity <--- toolkit <--- client <--- option <--- downloader -__version__ = '2.7.4' +__version__ = '2.7.5' from .jm_task_context import * from .api import * diff --git a/src/jmcomic/jm_async_client.py b/src/jmcomic/jm_async_client.py index c1dfe834d..d7ac2b27e 100644 --- a/src/jmcomic/jm_async_client.py +++ b/src/jmcomic/jm_async_client.py @@ -260,6 +260,7 @@ async def _request_with_retry(self, if not domain_list: ExceptionTool.raises("无可用 API 域名列表") + retry_errors = [] for domain_index, domain in enumerate(domain_list): url = self._build_api_url(url_path, domain) @@ -289,11 +290,21 @@ async def _request_with_retry(self, return resp except Exception as e: self.before_retry(e, url, retry, domain_index) + retry_errors.append({ + 'domain': domain, + 'url': url, + 'retry': retry, + 'error': e, + }) # 所有域名都失败 msg = f"请求重试全部失败: [{url_path}], {domain_list}" jm_log('req.fallback', msg) - ExceptionTool.raises(msg, {}, RequestRetryAllFailException) + ExceptionTool.raises( + msg, + {ExceptionTool.CONTEXT_KEY_RETRY_ERRORS: retry_errors}, + RequestRetryAllFailException, + ) # noinspection PyMethodMayBeStatic,PyUnusedLocal def before_retry(self, e, url, retry, domain_index): diff --git a/src/jmcomic/jm_client_impl.py b/src/jmcomic/jm_client_impl.py index 597148371..c183a2624 100644 --- a/src/jmcomic/jm_client_impl.py +++ b/src/jmcomic/jm_client_impl.py @@ -59,6 +59,7 @@ def request_with_retry(self, domain_index=0, retry_count=0, is_image=False, + _retry_errors=None, **kwargs, ): """ @@ -83,8 +84,12 @@ def request_with_retry(self, **kwargs, ) + if _retry_errors is None: + _retry_errors = [] + if domain_index >= len(self.domain_list): - return self.fallback(request, url, domain_index, retry_count, is_image, **kwargs) + return self.fallback(request, url, domain_index, retry_count, is_image, + retry_errors=_retry_errors, **kwargs) url_backup = url @@ -120,11 +125,19 @@ def request_with_retry(self, raise e self.before_retry(e, kwargs, retry_count, url) + _retry_errors.append({ + 'domain': self.domain_list[domain_index] if url_backup.startswith('/') else None, + 'url': url, + 'retry': retry_count, + 'error': e, + }) if retry_count < self.retry_times: - return self.request_with_retry(request, url_backup, domain_index, retry_count + 1, is_image, **kwargs) + return self.request_with_retry(request, url_backup, domain_index, retry_count + 1, is_image, + _retry_errors, **kwargs) else: - return self.request_with_retry(request, url_backup, domain_index + 1, 0, is_image, **kwargs) + return self.request_with_retry(request, url_backup, domain_index + 1, 0, is_image, + _retry_errors, **kwargs) # noinspection PyMethodMayBeStatic def raise_if_resp_should_retry(self, resp, is_image): @@ -212,10 +225,14 @@ def set_domain_list(self, domain_list: List[str]): self.domain_list = domain_list # noinspection PyUnusedLocal - def fallback(self, request, url, domain_index, retry_count, is_image, **kwargs): + def fallback(self, request, url, domain_index, retry_count, is_image, retry_errors=None, **kwargs): msg = f"请求重试全部失败: [{url}], {self.domain_list}" jm_log('req.fallback', msg) - ExceptionTool.raises(msg, {}, RequestRetryAllFailException) + ExceptionTool.raises( + msg, + {ExceptionTool.CONTEXT_KEY_RETRY_ERRORS: retry_errors or []}, + RequestRetryAllFailException, + ) # noinspection PyMethodMayBeStatic def append_params_to_url(self, url, params): diff --git a/src/jmcomic/jm_config.py b/src/jmcomic/jm_config.py index 4dd5442b8..05380d4df 100644 --- a/src/jmcomic/jm_config.py +++ b/src/jmcomic/jm_config.py @@ -142,7 +142,7 @@ class JmMagicConstants: APP_TOKEN_SECRET_2 = '18comicAPPContent' APP_DATA_SECRET = '185Hcomic3PAPP7R' API_DOMAIN_SERVER_SECRET = 'diosfjckwpqpdfjkvnqQjsik' - APP_VERSION = '2.0.30' + APP_VERSION = '2.1.2' # 模块级别共用配置 diff --git a/src/jmcomic/jm_exception.py b/src/jmcomic/jm_exception.py index cb15a9234..f88392e44 100644 --- a/src/jmcomic/jm_exception.py +++ b/src/jmcomic/jm_exception.py @@ -63,6 +63,28 @@ def error_jmid(self) -> str: class RequestRetryAllFailException(JmcomicException): description = '请求重试全部失败异常' + @property + def errors(self) -> list: + """返回每次请求失败的记录,其中 error 为原始异常对象。""" + return self.context.get(ExceptionTool.CONTEXT_KEY_RETRY_ERRORS, []) + + def __str__(self): + if not self.errors: + return super().__str__() + + details = [] + for index, item in enumerate(self.errors, 1): + error = item['error'] + location = item.get('url') or item.get('domain') or '未知地址' + retry = item.get('retry') + retry_text = '' if retry is None else f', retry={retry}' + details.append( + f' {index}. {location}{retry_text}: ' + f'{type(error).__name__}: {error}' + ) + + return f'{super().__str__()}\n失败详情:\n' + '\n'.join(details) + class PartialDownloadFailedException(JmcomicException): description = '部分章节或图片下载失败异常' @@ -84,6 +106,7 @@ class ExceptionTool: CONTEXT_KEY_RE_PATTERN = 'pattern' CONTEXT_KEY_MISSING_JM_ID = 'missing_jm_id' CONTEXT_KEY_DOWNLOADER = 'downloader' + CONTEXT_KEY_RETRY_ERRORS = 'retry_errors' @classmethod def raises(cls, diff --git a/src/jmcomic/jm_plugin.py b/src/jmcomic/jm_plugin.py index 188cab0e9..ad2442424 100644 --- a/src/jmcomic/jm_plugin.py +++ b/src/jmcomic/jm_plugin.py @@ -1785,6 +1785,7 @@ def do_request(domain): retry_domain_max_times: int = self.retry_config['retry_domain_max_times'] retry_rounds: int = self.retry_config['retry_rounds'] + retry_errors = [] for rindex in range(retry_rounds): domain_list = self.get_sorted_domain(client, retry_domain_max_times) for i, domain in enumerate(domain_list): @@ -1795,9 +1796,16 @@ def do_request(domain): return do_request(domain) except Exception as e: jm_log('req.error', e) + retry_errors.append({ + 'domain': domain, + 'url': client.of_api_url(url, domain) if url.startswith('/') else url, + 'retry': rindex, + 'error': e, + }) self.update_failed_count(client, domain) - return client.fallback(request, url, 0, 0, is_image, **kwargs) + return client.fallback(request, url, 0, 0, is_image, + retry_errors=retry_errors, **kwargs) def get_sorted_domain(self, client: JmcomicClient, times): domain_list = client.get_domain_list() diff --git a/tests/test_jmcomic/test_jm_exception.py b/tests/test_jmcomic/test_jm_exception.py new file mode 100644 index 000000000..cc4450a1f --- /dev/null +++ b/tests/test_jmcomic/test_jm_exception.py @@ -0,0 +1,100 @@ +from test_jmcomic import * +import asyncio + + +class Test_RequestRetryAllFailException(unittest.TestCase): + + def test_sync_client_collects_each_failed_request(self): + client = object.__new__(AbstractJmClient) + client.domain_list = ['api-one.example', 'api-two.example'] + client.retry_times = 1 + client.domain_retry_strategy = None + + def request(url, **kwargs): + raise TimeoutError(url) + + with self.assertRaises(RequestRetryAllFailException) as cm: + client.request_with_retry(request, '/search') + + errors = cm.exception.errors + self.assertEqual(4, len(errors)) + self.assertEqual(['api-one.example', 'api-one.example', + 'api-two.example', 'api-two.example'], + [item['domain'] for item in errors]) + self.assertTrue(all(isinstance(item['error'], TimeoutError) for item in errors)) + + def test_async_client_collects_each_failed_request(self): + class FailingSession: + async def get(self, url, **kwargs): + raise ConnectionError(url) + + client = object.__new__(AsyncJmApiClient) + client._domain_list = ['api-one.example', 'api-two.example'] + client._retry_times = 0 + client._session = FailingSession() + + async def request(): + return await client._request_with_retry('/search', {}) + + with self.assertRaises(RequestRetryAllFailException) as cm: + asyncio.run(request()) + + errors = cm.exception.errors + self.assertEqual(2, len(errors)) + self.assertEqual(['api-one.example', 'api-two.example'], + [item['domain'] for item in errors]) + self.assertTrue(all(isinstance(item['error'], ConnectionError) for item in errors)) + + def test_advanced_retry_collects_each_failed_request(self): + client = object.__new__(AbstractJmClient) + client.domain_list = ['api-one.example', 'api-two.example'] + client.retry_times = 1 + client.domain_retry_strategy = None + + plugin = object.__new__(AdvancedRetryPlugin) + plugin.retry_config = { + 'retry_rounds': 1, + 'retry_domain_max_times': 1, + } + plugin(client) + + def request(url, **kwargs): + raise OSError(url) + + with self.assertRaises(RequestRetryAllFailException) as cm: + plugin(client, request, '/search', False) + + errors = cm.exception.errors + self.assertEqual(2, len(errors)) + self.assertEqual(['api-one.example', 'api-two.example'], + [item['domain'] for item in errors]) + self.assertTrue(all(isinstance(item['error'], OSError) for item in errors)) + + def test_preserves_and_formats_retry_errors(self): + error_500 = ResponseUnexpectedException('禁漫API异常响应, 500', {}) + error_timeout = TimeoutError('connection timed out') + errors = [ + { + 'domain': 'api-one.example', + 'url': 'https://api-one.example/search', + 'retry': 0, + 'error': error_500, + }, + { + 'domain': 'api-two.example', + 'url': 'https://api-two.example/search', + 'retry': 1, + 'error': error_timeout, + }, + ] + exception = RequestRetryAllFailException( + '请求重试全部失败', + {ExceptionTool.CONTEXT_KEY_RETRY_ERRORS: errors}, + ) + + self.assertIs(exception.errors[0]['error'], error_500) + self.assertIs(exception.errors[1]['error'], error_timeout) + text = str(exception) + self.assertIn('ResponseUnexpectedException: 禁漫API异常响应, 500', text) + self.assertIn('TimeoutError: connection timed out', text) + self.assertIn('https://api-one.example/search, retry=0', text) From e4c73b5c87c3726953ba960ef2ce2d8882554f6a Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:25:38 +0800 Subject: [PATCH 3/4] ci: update supported Python versions --- .github/workflows/benchmark.yml | 6 +++--- .github/workflows/test_api.yml | 2 +- .github/workflows/test_html.yml | 2 +- README.md | 7 ++++--- assets/readme/README-en.md | 7 ++++--- assets/readme/README-jp.md | 7 ++++--- assets/readme/README-kr.md | 7 ++++--- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f2b7bbc99..e534db734 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -2,12 +2,12 @@ name: Async Benchmark on: push: - branches: [ main, master ] + branches: [ master ] paths: - '**/*async*.py' - '.github/workflows/benchmark.yml' pull_request: - branches: [ main, master ] + branches: [ master ] paths: - '**/*async*.py' - '.github/workflows/benchmark.yml' @@ -24,7 +24,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.14' - name: Install dependencies run: | diff --git a/.github/workflows/test_api.yml b/.github/workflows/test_api.yml index 7f109cfbd..e0e5d7558 100644 --- a/.github/workflows/test_api.yml +++ b/.github/workflows/test_api.yml @@ -17,7 +17,7 @@ jobs: test: # This code is based on https://github.com/gaogaotiantian/viztracer/blob/master/.github/workflows/python-package.yml strategy: matrix: - python-version: ['3.9', '3.10', '3.13', '3.14'] # 3.9 is kept as the minimum supported version test (EOL) + python-version: ['3.10', '3.11', '3.13', '3.14'] os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} timeout-minutes: 5 diff --git a/.github/workflows/test_html.yml b/.github/workflows/test_html.yml index 9e7bb049e..8f44d7c9b 100644 --- a/.github/workflows/test_html.yml +++ b/.github/workflows/test_html.yml @@ -17,7 +17,7 @@ jobs: test: # This code is based on https://github.com/gaogaotiantian/viztracer/blob/master/.github/workflows/python-package.yml strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.13'] + python-version: ['3.10', '3.11', '3.13', '3.14'] os: [ ubuntu-latest ] runs-on: ${{ matrix.os }} timeout-minutes: 5 diff --git a/README.md b/README.md index b2b06b416..aa4355cc7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ ## 安装教程 > ⚠如果你没有安装过 Python,需要先前往 [Python 官网下载](https://www.python.org/downloads/) 再执行以下步骤。 ->**推荐使用 Python 3.12及以上版本** +>**推荐使用 Python 3.14** * 通过pip官方源安装(推荐,并且更新也是这个命令) @@ -255,8 +255,9 @@ jmv 350234 -y ## 使用小说明 -* 推荐使用 **Python 3.12+**,目前最低兼容版本为3.9。 - > 注意:Python 3.9 及更早版本皆已于 2025 年彻底结束官方生命周期 (EOL),使用3.9及以下随时有可能遇到第三方库不兼容的问题。 +* 推荐使用 **Python 3.14**,目前 CI 只覆盖 Python 3.10 及以上版本。 + > [!NOTE] + > Python 3.9 及更早版本皆已于 2025 年彻底结束官方生命周期 (EOL),使用3.9及以下随时有可能遇到第三方库不兼容的问题。Python 3.9 仍保留安装兼容,但不再纳入 CI。 * 个人项目,文档和示例会有不及时之处,可以Issue提问。 diff --git a/assets/readme/README-en.md b/assets/readme/README-en.md index 358299817..49c49ac12 100644 --- a/assets/readme/README-en.md +++ b/assets/readme/README-en.md @@ -63,7 +63,7 @@ In addition to downloading, other JM interfaces are also implemented on demand. ## Installation Guide > ⚠ If you have not installed Python, you must install Python before executing the following steps. [Download from Python Official Site](https://www.python.org/downloads/) -> **Version 3.12+ is recommended.** +> **Python 3.14 is recommended.** * Install via official pip source (recommended, the update command is identical) @@ -249,8 +249,9 @@ Please check the documentation homepage → [jmcomic.readthedocs.io (Chinese lan ## Prerequisites -* Version **3.12+** is recommended, with a minimum compatible version of 3.9. - > Note: Python 3.9 and earlier versions reached their End Of Life (EOL) in 2025. You may encounter third-party library incompatibilities at any time if you use version 3.9 or below. +* **Python 3.14** is recommended. CI currently only covers Python 3.10 and later. + > [!NOTE] + > Python 3.9 and earlier versions reached their End Of Life (EOL) in 2025. You may encounter third-party library incompatibilities at any time if you use version 3.9 or below. Python 3.9 remains install-compatible, but is no longer included in CI. * Since this is a personal project, the documentation/examples may occasionally be out of sync. Please feel free to open an Issue for any clarifications. diff --git a/assets/readme/README-jp.md b/assets/readme/README-jp.md index 04a057b2f..7e494885a 100644 --- a/assets/readme/README-jp.md +++ b/assets/readme/README-jp.md @@ -63,7 +63,7 @@ ## インストール手順 > ⚠ まだPythonをインストールしていない場合は、先に [公式のPythonページからダウンロード](https://www.python.org/downloads/) してインストールをお願いします。 -> **Python 3.12以上の使用を推奨します** +> **Python 3.14の使用を推奨します** * 公式 pip ソースからインストール(推奨。アップデートもこのコマンドを使用します) @@ -244,8 +244,9 @@ jmv 350234 -y ## ご利用上の注意点 -* **Python 3.12以上**を推奨します。現在の最小互換バージョンは3.9です。 - > 注意: Python 3.9 およびそれ以前のバージョンは2025年に完全にサポート終了 (EOL) となっており、3.9以下のバージョンを使用すると、サードパーティ製ライブラリの非互換性の問題がいつでも発生する可能性があります。 +* **Python 3.14**を推奨します。現在、CI は Python 3.10 以降のみを対象としています。 + > [!NOTE] + > Python 3.9 およびそれ以前のバージョンは2025年に完全にサポート終了 (EOL) となっており、3.9以下のバージョンを使用すると、サードパーティ製ライブラリの非互換性の問題がいつでも発生する可能性があります。Python 3.9 はインストール互換性を維持しますが、CI の対象外です。 * 個人のプロジェクトであるため、ドキュメントやサンプルコードの更新が遅れることがあります。ご不明な点はIssueにてご質問ください。 diff --git a/assets/readme/README-kr.md b/assets/readme/README-kr.md index 03f90f417..649702d37 100644 --- a/assets/readme/README-kr.md +++ b/assets/readme/README-kr.md @@ -63,7 +63,7 @@ ## 설치 가이드 > ⚠ Python이 시스템에 설치되어 있지 않다면, 다음 단계를 진행하기 전에 먼저 [Python 공식 사이트](https://www.python.org/downloads/) 에서 다운로드하여 설치해주시기 바랍니다. -> **Python 3.12 이상 버전을 권장합니다** +> **Python 3.14 사용을 권장합니다** * 공식 pip 저장소를 통한 설치 (추천. 업데이트 명령어도 동일합니다) @@ -244,8 +244,9 @@ jmv 350234 -y ## 사용 팁 -* **Python 3.12 이상**을 권장하며, 현재 최소 호환 버전은 3.9입니다. - > 참고: Python 3.9 및 이전 버전은 모두 2025년에 공식 지원 종료(EOL)되었으므로 3.9 이하 버전을 사용할 경우 언제든 서드파티 라이브러리 호환성 문제에 부딪힐 수 있습니다. +* **Python 3.14**를 권장하며, 현재 CI는 Python 3.10 이상만 대상으로 합니다. + > [!NOTE] + > Python 3.9 및 이전 버전은 모두 2025년에 공식 지원 종료(EOL)되었으므로 3.9 이하 버전을 사용할 경우 언제든 서드파티 라이브러리 호환성 문제에 부딪힐 수 있습니다. Python 3.9는 설치 호환성을 유지하지만 더 이상 CI 대상에는 포함되지 않습니다. * 여유 시간에 만들어 지는 프로젝트이기에 정보나 활용 코드의 늦은 갱신이 다분합니다. 이슈(Issue)페이지로 연락주시기 바랍니다! From f4bc4effc5d21796eb0165365081228e86f03fac Mon Sep 17 00:00:00 2001 From: hect0x7 <93357912+hect0x7@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:08:25 +0800 Subject: [PATCH 4/4] docs: correct Python EOL wording [SKIP CI] --- README.md | 2 +- assets/readme/README-en.md | 2 +- assets/readme/README-jp.md | 2 +- assets/readme/README-kr.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa4355cc7..2d80417d7 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ jmv 350234 -y * 推荐使用 **Python 3.14**,目前 CI 只覆盖 Python 3.10 及以上版本。 > [!NOTE] - > Python 3.9 及更早版本皆已于 2025 年彻底结束官方生命周期 (EOL),使用3.9及以下随时有可能遇到第三方库不兼容的问题。Python 3.9 仍保留安装兼容,但不再纳入 CI。 + > Python 3.9 及更早版本均已结束官方支持 (EOL),使用3.9及以下随时有可能遇到第三方库不兼容的问题。Python 3.9 仍保留安装兼容,但不再纳入 CI。 * 个人项目,文档和示例会有不及时之处,可以Issue提问。 diff --git a/assets/readme/README-en.md b/assets/readme/README-en.md index 49c49ac12..f4238f6b9 100644 --- a/assets/readme/README-en.md +++ b/assets/readme/README-en.md @@ -251,7 +251,7 @@ Please check the documentation homepage → [jmcomic.readthedocs.io (Chinese lan * **Python 3.14** is recommended. CI currently only covers Python 3.10 and later. > [!NOTE] - > Python 3.9 and earlier versions reached their End Of Life (EOL) in 2025. You may encounter third-party library incompatibilities at any time if you use version 3.9 or below. Python 3.9 remains install-compatible, but is no longer included in CI. + > Python 3.9 and earlier versions are no longer officially supported (EOL). You may encounter third-party library incompatibilities at any time if you use version 3.9 or below. Python 3.9 remains install-compatible, but is no longer included in CI. * Since this is a personal project, the documentation/examples may occasionally be out of sync. Please feel free to open an Issue for any clarifications. diff --git a/assets/readme/README-jp.md b/assets/readme/README-jp.md index 7e494885a..5ffca77fb 100644 --- a/assets/readme/README-jp.md +++ b/assets/readme/README-jp.md @@ -246,7 +246,7 @@ jmv 350234 -y * **Python 3.14**を推奨します。現在、CI は Python 3.10 以降のみを対象としています。 > [!NOTE] - > Python 3.9 およびそれ以前のバージョンは2025年に完全にサポート終了 (EOL) となっており、3.9以下のバージョンを使用すると、サードパーティ製ライブラリの非互換性の問題がいつでも発生する可能性があります。Python 3.9 はインストール互換性を維持しますが、CI の対象外です。 + > Python 3.9 およびそれ以前のバージョンは公式サポートが終了 (EOL) しており、3.9以下のバージョンを使用すると、サードパーティ製ライブラリの非互換性の問題がいつでも発生する可能性があります。Python 3.9 はインストール互換性を維持しますが、CI の対象外です。 * 個人のプロジェクトであるため、ドキュメントやサンプルコードの更新が遅れることがあります。ご不明な点はIssueにてご質問ください。 diff --git a/assets/readme/README-kr.md b/assets/readme/README-kr.md index 649702d37..34375bf5f 100644 --- a/assets/readme/README-kr.md +++ b/assets/readme/README-kr.md @@ -246,7 +246,7 @@ jmv 350234 -y * **Python 3.14**를 권장하며, 현재 CI는 Python 3.10 이상만 대상으로 합니다. > [!NOTE] - > Python 3.9 및 이전 버전은 모두 2025년에 공식 지원 종료(EOL)되었으므로 3.9 이하 버전을 사용할 경우 언제든 서드파티 라이브러리 호환성 문제에 부딪힐 수 있습니다. Python 3.9는 설치 호환성을 유지하지만 더 이상 CI 대상에는 포함되지 않습니다. + > Python 3.9 및 이전 버전은 모두 공식 지원이 종료(EOL)되었으므로 3.9 이하 버전을 사용할 경우 언제든 서드파티 라이브러리 호환성 문제에 부딪힐 수 있습니다. Python 3.9는 설치 호환성을 유지하지만 더 이상 CI 대상에는 포함되지 않습니다. * 여유 시간에 만들어 지는 프로젝트이기에 정보나 활용 코드의 늦은 갱신이 다분합니다. 이슈(Issue)페이지로 연락주시기 바랍니다!