Skip to content

v2.6.19: 新增 Feature 机制,支持下载时通过 extra 参数附加导出 PDF/ZIP/长图等行为; Feature 参数根据 download_album/download_photo 来源自适应; 新增 Feature 教程文档#533

Open
hect0x7 wants to merge 5 commits intomasterfrom
dev
Open

v2.6.19: 新增 Feature 机制,支持下载时通过 extra 参数附加导出 PDF/ZIP/长图等行为; Feature 参数根据 download_album/download_photo 来源自适应; 新增 Feature 教程文档#533
hect0x7 wants to merge 5 commits intomasterfrom
dev

Conversation

@hect0x7
Copy link
Copy Markdown
Owner

@hect0x7 hect0x7 commented May 5, 2026

  1. 新增 Feature 机制,支持下载时通过 extra 参数附加导出 PDF/ZIP/长图等行为
  2. Feature 参数根据 download_album/download_photo 来源自适应
  3. 新增 Feature 教程文档
  4. 升级版本号至 v2.6.19

Summary by CodeRabbit

  • New Features

    • Attachable export features for downloads: export to PDF, ZIP, or long-image; combine and parameterize behaviors per download.
    • Optional colored "pretty" console logging.
  • Improvements

    • Download API forwards extra context to per-item operations; packaging now auto-infers album vs photo granularity when unspecified.
  • Documentation

    • New step-by-step tutorial and updated navigation describing exports and migration guidance.
  • Tests

    • Added tests for feature composition, invocation rules, parameter adaptation, and export output naming.

Version: 2.6.19

…根据 download_album/download_photo 来源自适应; 新增 Feature 教程文档
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Feature system for post-download export behaviors, integrates feature registration/invocation into downloader hooks, forwards an extra parameter through download APIs, introduces pretty console logging, migrates legacy zip level configs, bumps package version, and adds tests and documentation/tutorial entries.

Changes

Feature System + Integration

Layer / File(s) Summary
Data / API shape
src/jmcomic/api.py
download_batch, download_album, download_photo gain extra=None and forward it; batch/photo paths register features via dler.add_features(extra, 'download_album'/'download_photo').
Core Feature types
src/jmcomic/jm_feature.py
New Feature, PluginFeature, FeatureChain classes, operator combinators, kwargs adaptation (filename_rule/level) and predefined Feature.export_pdf, Feature.export_zip, Feature.export_long_img.
Plugin change / backward compat
src/jmcomic/jm_plugin.py
ZipPlugin.invoke signature adds level=None and infers level from presence of album when unspecified.
Downloader wiring
src/jmcomic/jm_downloader.py
Adds self._feature_list, add_features(features, feature_from), _invoke_features_for(when, **context), and invokes features from after_album/after_photo.
Options, logging & migration
src/jmcomic/jm_option.py, src/jmcomic/jm_config.py
Adds PrettyFormatter and enable_pretty_log(); JmOption.construct supports log='pretty'; adds _migrate_zip_level for legacy zip level migration.
Exports & package metadata
src/jmcomic/__init__.py
Bumps __version__ to 2.6.19 and re-exports from .jm_feature import *.
Docs / Tutorials / Nav
assets/docs/sources/tutorial/13_export_and_feature.md, assets/docs/mkdocs.yml, assets/docs/sources/index.md, README.md, assets/docs/sources/option_file_syntax.md
Adds tutorial 13_export_and_feature.md, updates mkdocs nav and README quick links, and documents zip level deprecation/migration.
Tests
tests/test_jmcomic/test_jm_feature.py
New unit tests covering Feature composition, PluginFeature call/adaptation, invocation via extra in downloads, and export filename assertions.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant API as download_album/photo
    participant Downloader as JmDownloader
    participant Hook as after_album/after_photo
    participant Feature as Feature/PluginFeature
    participant Plugin as ExportPlugin

    User->>API: download_album(..., extra=Feature.export_pdf)
    API->>Downloader: add_features(extra, "download_album")
    Downloader->>Downloader: store feature in _feature_list
    API->>Downloader: start download
    Downloader->>Hook: after_album(album)
    Hook->>Downloader: _invoke_features_for("after_album", album=..., downloader=...)
    Downloader->>Feature: should_invoke(feature_from="download_album", when="after_album")?
    Feature-->>Downloader: True
    Downloader->>Feature: invoke(option, feature_from="download_album", when="after_album", album=..., downloader=...)
    Feature->>Plugin: option.invoke_plugin(... adapted kwargs ...)
    Plugin-->>Feature: export complete
    Feature-->>Downloader: done
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibble code and twitch my nose,
Features bloom where download flows.
PDFs, ZIPs, long pictures glide—
One extra= hop, they join the ride.
Burrow done, I dance with pride.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: version bump to 2.6.19, introduction of Feature mechanism, support for extra parameter in download functions, adaptive parameter handling, and new tutorial documentation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
tests/test_jmcomic/test_jm_feature.py (1)

89-112: ⚡ Quick win

Add a batch-ID coverage case for extra execution.

Current tests validate scalar IDs, but not iterable batch input. A small batch test would guard against extra being skipped in download_batch delegation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_jmcomic/test_jm_feature.py` around lines 89 - 112, Add a test that
exercises the `extra` Feature when using batch input: create a
`MyCounterFeature` (subclassing `Feature`) that increments a counter in
`invoke`, then call `jmcomic.download_batch([...], self.option,
extra=counter_feature)` with an iterable of IDs and assert the counter increased
for each expected `after_photo`/`after_album` invocation; this mirrors the
existing `test_download_use_feature` flow (which calls `download_album` and
`download_photo`) but targets `download_batch` to ensure `extra` is not skipped
when handling iterable inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@assets/docs/sources/tutorial/13_export_and_feature.md`:
- Around line 88-90: The link fragment for the inline tip doesn't match the
generated heading anchor for the target heading "### 2.5 智能适配规则"; update the
fragment in the tip (the [智能适配规则](#...) link) to match the heading's generated
anchor (include the "2.5" portion, e.g. change to "#2-5-智能适配规则" or the exact
generated anchor your docs tool emits) so the internal link points to the "###
2.5 智能适配规则" heading.

In `@README.md`:
- Line 33: Update the README entry for the tutorial so the link text reflects
the full coverage ("PDF/ZIP/长图"); specifically change the markdown link label
"[教程:下载后转为 PDF / ZIP]" to "[教程:下载后转为 PDF / ZIP / 长图]" (the target URL
./assets/docs/sources/tutorial/13_export_and_feature.md remains the same) so the
README matches the actual tutorial scope.

In `@src/jmcomic/api.py`:
- Around line 68-69: The batch branch in download_album and download_photo
ignores the extra parameter so features in extra never run for iterable IDs;
update the calls to download_batch(...) to forward the extra argument (e.g.,
change download_batch(download_album, jm_album_id, option, downloader) to
download_batch(download_album, jm_album_id, option, downloader, extra)) and do
the same for the corresponding call in download_photo so download_batch receives
and passes extra through to individual downloads.

In `@src/jmcomic/jm_config.py`:
- Around line 597-599: The code currently calls _os.system('') under the
sys.platform == 'win32' branch, which spawns a shell; replace this with direct
Win32 console mode API calls: use GetStdHandle, GetConsoleMode, SetConsoleMode
and enable the ENABLE_VIRTUAL_TERMINAL_PROCESSING flag (0x0004) for the
STD_OUTPUT_HANDLE to turn on ANSI processing. Locate the platform check
(sys.platform == 'win32') and the _os.system('') call in jm_config.py and swap
it for a ctypes-based implementation that retrieves the stdout handle, reads the
current console mode, ORs in ENABLE_VIRTUAL_TERMINAL_PROCESSING, and writes it
back, handling errors/return codes appropriately.

In `@src/jmcomic/jm_downloader.py`:
- Around line 290-298: The registration code in add_features/feature handling
appends unknown `features` types to self._feature_list and defers errors until
hook invocation; validate types at registration time by checking if `features`
is a list, a FeatureChain (inspect FeatureChain._features), or a supported
single feature type (e.g., the expected Feature class/interface) and raise a
clear TypeError for any unsupported `features` value instead of appending it;
update the branch that currently does self._feature_list.append((features,
feature_from)) to perform an explicit isinstance/issubclass check (or
duck-typing validation) and raise with a helpful message referencing the
offending value.

In `@src/jmcomic/jm_feature.py`:
- Around line 96-103: The current __call__ in PluginFeature overwrites
_user_keys with only the latest kwargs keys, losing previously recorded explicit
keys; change it to merge the previous instance's _user_keys with the new kwargs
keys (e.g., new_instance._user_keys = (getattr(self, "_user_keys", set()) |
set(kwargs.keys()))) so earlier explicit parameters are preserved across chained
PluginFeature(...) calls; ensure you reference __call__, PluginFeature,
_user_keys and kwargs when making this change.

---

Nitpick comments:
In `@tests/test_jmcomic/test_jm_feature.py`:
- Around line 89-112: Add a test that exercises the `extra` Feature when using
batch input: create a `MyCounterFeature` (subclassing `Feature`) that increments
a counter in `invoke`, then call `jmcomic.download_batch([...], self.option,
extra=counter_feature)` with an iterable of IDs and assert the counter increased
for each expected `after_photo`/`after_album` invocation; this mirrors the
existing `test_download_use_feature` flow (which calls `download_album` and
`download_photo`) but targets `download_batch` to ensure `extra` is not skipped
when handling iterable inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 397052b3-440e-4769-968d-68868f23af61

📥 Commits

Reviewing files that changed from the base of the PR and between b063ce5 and b5d9858.

📒 Files selected for processing (11)
  • README.md
  • assets/docs/mkdocs.yml
  • assets/docs/sources/index.md
  • assets/docs/sources/tutorial/13_export_and_feature.md
  • src/jmcomic/__init__.py
  • src/jmcomic/api.py
  • src/jmcomic/jm_config.py
  • src/jmcomic/jm_downloader.py
  • src/jmcomic/jm_feature.py
  • src/jmcomic/jm_option.py
  • tests/test_jmcomic/test_jm_feature.py

Comment thread assets/docs/sources/tutorial/13_export_and_feature.md Outdated
Comment thread README.md Outdated
Comment thread src/jmcomic/api.py Outdated
Comment thread src/jmcomic/jm_config.py
Comment thread src/jmcomic/jm_downloader.py
Comment thread src/jmcomic/jm_feature.py
Comment on lines +96 to +103
def __call__(self, **kwargs):
"""带自定义参数,返回新实例(继承默认参数)"""
new_kwargs = self.kwargs.copy()
new_kwargs.update(kwargs)
new_instance = PluginFeature(self.plugin_key, **new_kwargs)
# 记录用户显式传入的参数名,这些参数不被动态适配
new_instance._user_keys = set(kwargs.keys())
return new_instance
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve explicit user keys across chained PluginFeature(...) calls.

Line 102 currently tracks only keys from the latest call, so earlier explicit keys can be lost and later auto-adapted unexpectedly.

🔧 Proposed fix
     def __call__(self, **kwargs):
         """带自定义参数,返回新实例(继承默认参数)"""
         new_kwargs = self.kwargs.copy()
         new_kwargs.update(kwargs)
         new_instance = PluginFeature(self.plugin_key, **new_kwargs)
         # 记录用户显式传入的参数名,这些参数不被动态适配
-        new_instance._user_keys = set(kwargs.keys())
+        new_instance._user_keys = set(self._user_keys) | set(kwargs.keys())
         return new_instance
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 97-97: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


[warning] 97-97: Docstring contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF002)


[warning] 97-97: Docstring contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF002)


[warning] 101-101: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_feature.py` around lines 96 - 103, The current __call__ in
PluginFeature overwrites _user_keys with only the latest kwargs keys, losing
previously recorded explicit keys; change it to merge the previous instance's
_user_keys with the new kwargs keys (e.g., new_instance._user_keys =
(getattr(self, "_user_keys", set()) | set(kwargs.keys()))) so earlier explicit
parameters are preserved across chained PluginFeature(...) calls; ensure you
reference __call__, PluginFeature, _user_keys and kwargs when making this
change.

修复内容:
1. [文档修正] 修正 13_export_and_feature.md 中的失效锚点链接
2. [文档完善] 补全 README.md 中的教程支持范围描述(增加长图)
3. [核心修复] 修复批量下载时没有透传 extra 参数导致 Feature 无法执行的严重 Bug
4. [性能优化] 改用 ctypes 启用 Windows ANSI 控制台支持,移除低效且不安全的 os.system 进程调用
5. [错误捕获] 提前在 jm_downloader.py 的 add_features 方法中对 extra 进行类型校验
6. [认知升级] 更新代码中的 docstring,强化 Feature 作为“基于生命周期和调用来源的动态自适应机制”的定位

未修改内容及原因:
1. jm_feature.py 链式调用丢失 _user_keys 问题
   - 经与开发者确认,暂时先不修改此逻辑。
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/jmcomic/api.py (1)

6-10: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reserve callback parameter explicitly before forwarding **kwargs.

Line 10 accepts **kwargs which are forwarded to download_api() on line 41. However, line 40 already passes callback=callback to the same function. If a caller passes callback= through download_batch(), Python raises TypeError: got multiple values for keyword argument 'callback' before the download runs, making this forwarding surface unsafe for that parameter.

🔧 Proposed fix
 def download_batch(download_api,
                    jm_id_iter: Union[Iterable, Generator],
                    option=None,
                    downloader=None,
+                   callback=None,
                    **kwargs,
                    ) -> Set[__DOWNLOAD_API_RET]:
@@
-    def callback(*ret):
+    def collect_result(*ret):
         result.add(ret)
+        if callback is not None:
+            callback(*ret)
@@
-        apply_each_obj_func=lambda aid: download_api(aid,
-                                                     option,
-                                                     downloader,
-                                                     callback=callback,
-                                                     **kwargs,
-                                                     ),
+        apply_each_obj_func=lambda aid: download_api(
+            aid,
+            option,
+            downloader,
+            callback=collect_result,
+            **kwargs,
+        ),
         wait_finish=True
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/api.py` around lines 6 - 10, The download_batch function
currently accepts **kwargs and also passes callback=callback to download_api,
which can cause a TypeError if caller supplies callback in **kwargs; update the
download_batch signature to explicitly accept callback (e.g., def
download_batch(..., callback=None, **kwargs)) and when forwarding to
download_api use callback=callback (or ensure you pop 'callback' from kwargs
before calling) so there are no duplicate keyword arguments; reference the
download_batch function and the download_api call when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@assets/docs/sources/tutorial/13_export_and_feature.md`:
- Around line 35-38: Add explicit fence language "text" to the plain-text code
blocks in this markdown so they no longer trigger MD040: replace each
triple-backtick fence (```) around the directory/tree and flow
examples—specifically the blocks containing the "./" tree, the "[章节标题].pdf"
example, the "Feature (基类)" block, and the "api.download_album(...)" flow—so
each opens with ```text; do the same for the other occurrences referenced
(blocks around lines 83-86, 127-131, 140-161) to keep rendering unchanged while
satisfying the linter.

---

Outside diff comments:
In `@src/jmcomic/api.py`:
- Around line 6-10: The download_batch function currently accepts **kwargs and
also passes callback=callback to download_api, which can cause a TypeError if
caller supplies callback in **kwargs; update the download_batch signature to
explicitly accept callback (e.g., def download_batch(..., callback=None,
**kwargs)) and when forwarding to download_api use callback=callback (or ensure
you pop 'callback' from kwargs before calling) so there are no duplicate keyword
arguments; reference the download_batch function and the download_api call when
making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e060789-bafd-4c19-9756-e717a2c75a29

📥 Commits

Reviewing files that changed from the base of the PR and between b5d9858 and db09b8b.

📒 Files selected for processing (6)
  • README.md
  • assets/docs/sources/tutorial/13_export_and_feature.md
  • src/jmcomic/api.py
  • src/jmcomic/jm_config.py
  • src/jmcomic/jm_downloader.py
  • src/jmcomic/jm_feature.py

Comment thread assets/docs/sources/tutorial/13_export_and_feature.md
- ZipPlugin: level 默认值改为 None,根据上下文自动推导(有 album 则合并,只有 photo 则单章)
- Feature: export_zip 不再传递 level,_adapt_kwargs 移除 level 适配逻辑
- JmOption: compatible_with_old_versions 中新增 _migrate_zip_level,自动将旧 yaml 中的 level 配置等价迁移到正确的钩子位置
- 文档: 更新 option_file_syntax.md 和 13_export_and_feature.md 中 level 相关说明
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
assets/docs/sources/option_file_syntax.md (1)

237-253: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The after_album ZIP examples still use photo-scoped placeholders.

This section is configuring zip under after_album, but filename_rule: Ptitle and the dir_rule example with Pid / Ptitle both require a photo context. With the new hook-driven behavior there is no per-photo naming context here, so these examples now contradict the added guidance on Lines 239-241 and will break if users copy them verbatim. Use Axxx placeholders in this section, or move the photo-scoped examples under after_photo.

🔧 Proposed fix
-        filename_rule: Ptitle # 压缩文件的命名规则
+        filename_rule: Atitle # 压缩文件的命名规则

 ...
-        #   rule: 'Bd / {Atitle} / [{Pid}]-{Ptitle}.zip'  # 设置压缩文件夹规则,中间Atitle表示创建一层文件夹,名称是本子标题。[{Pid}]-{Ptitle}.zip 表示压缩文件的命名规则(需显式写出后缀名)
-        # 使用此方法指定压缩包存储路径则无需和所在钩子对应
+        #   rule: 'Bd / {Atitle} / [{Aid}]-{Atitle}.zip'  # 设置压缩文件夹规则,中间Atitle表示创建一层文件夹,名称是本子标题。[{Aid}]-{Atitle}.zip 表示压缩文件的命名规则(需显式写出后缀名)
+        # 使用此方法指定压缩包存储路径时,DSL 里的 A/P 变量仍需和所在钩子对应
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/docs/sources/option_file_syntax.md` around lines 237 - 253, The
examples under the after_album section use photo-scoped placeholders
(filename_rule: Ptitle and dir_rule examples with {Pid}/{Ptitle}) which are
invalid in album scope; update those examples to use album-scoped placeholders
(Axxx) instead or move the photo-scoped examples into the after_photo section;
specifically, change filename_rule Ptitle to an Axxx variant (e.g., Atitle/Aid),
update the dir_rule example to use {Atitle}/{Aid} and an album-level filename
pattern, and ensure the comments mention that filename_rule, zip_dir and suffix
are ignored when dir_rule is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@assets/docs/sources/tutorial/13_export_and_feature.md`:
- Around line 56-62: The example using download_album passes
filename_rule='Ptitle' which PluginFeature.__call__ treats as user-owned and
will be evaluated without a photo context in album scope, causing a runtime
error; update the snippet to use an album-scoped rule (e.g.,
filename_rule='Atitle') or move this example into download_photo so the Pxxx
rule has a photo context, referencing the download_album call and
Feature.export_pdf usage to locate the code to change.
- Around line 134-166: The docs incorrectly state that
PluginFeature/_adapt_kwargs adapt the `level`; update the prose and sequence
diagram to reflect that `level` is now inferred inside ZipPlugin.invoke() and
that PluginFeature._adapt_kwargs() only rewrites `filename_rule` (not `level`),
and adjust the example flow where `_adapt_kwargs('download_album')` is shown to
only remap filename prefixes (A/P) while `ZipPlugin.invoke()` determines
`level`; mention that user-provided params are still preserved and keep
references to PluginFeature, _adapt_kwargs, ZipPlugin.invoke, should_invoke,
`filename_rule`, and `level` so readers know where to look in the code.

In `@src/jmcomic/jm_option.py`:
- Around line 364-377: The migration currently only moves zip with level=photo
from after_album→after_photo but fails to handle after_photo entries with
level='album'; update the migration in jm_option.py (the block handling
group/level, plugin_list, plugins, pinfo, i, and jm_log) to detect when group ==
'after_photo' and level == 'album' and perform the opposite migration: append
pinfo to plugins.setdefault('after_album', []), remove the entry from
plugin_list (plugin_list.pop(i)), emit a jm_log message describing the migration
to after_album, and avoid simply deleting the level; ensure the index i is only
incremented when you do not pop the current item so loop behavior remains
correct.

---

Outside diff comments:
In `@assets/docs/sources/option_file_syntax.md`:
- Around line 237-253: The examples under the after_album section use
photo-scoped placeholders (filename_rule: Ptitle and dir_rule examples with
{Pid}/{Ptitle}) which are invalid in album scope; update those examples to use
album-scoped placeholders (Axxx) instead or move the photo-scoped examples into
the after_photo section; specifically, change filename_rule Ptitle to an Axxx
variant (e.g., Atitle/Aid), update the dir_rule example to use {Atitle}/{Aid}
and an album-level filename pattern, and ensure the comments mention that
filename_rule, zip_dir and suffix are ignored when dir_rule is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bf0bd43-acd7-4ed9-b8a8-58c95eb5ea48

📥 Commits

Reviewing files that changed from the base of the PR and between db09b8b and a88d83d.

📒 Files selected for processing (5)
  • assets/docs/sources/option_file_syntax.md
  • assets/docs/sources/tutorial/13_export_and_feature.md
  • src/jmcomic/jm_feature.py
  • src/jmcomic/jm_option.py
  • src/jmcomic/jm_plugin.py

Comment thread assets/docs/sources/tutorial/13_export_and_feature.md
Comment thread assets/docs/sources/tutorial/13_export_and_feature.md
Comment thread src/jmcomic/jm_option.py
Comment on lines +364 to +377
if group == 'after_album' and level != 'album':
# after_album + level=photo → 等价迁移到 after_photo
plugins.setdefault('after_photo', []).append(pinfo)
plugin_list.pop(i)
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,打包粒度由所在钩子自动推导。'
f'已自动将 after_album 下的 zip(level={level!r}) 等价迁移到 after_photo。'
f'等价写法:将 zip 插件从 after_album 移至 after_photo 并删除 level 配置项。')
else:
if level != 'photo':
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,已自动移除。'
f'打包粒度由所在钩子自动推导({group} → {level})。')
i += 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

after_photo + level: album is not migrated to after_album.

The docstring says album-level ZIP configs should “确保在 after_album”, but this branch only handles the opposite direction. For an old config like after_photo: [{plugin: zip, kwargs: {level: album}}], the code just removes level and keeps the plugin in after_photo, silently changing behavior from one merged archive to per-photo archives.

🔧 Proposed fix
-                if group == 'after_album' and level != 'album':
+                if group == 'after_album' and level != 'album':
                     # after_album + level=photo → 等价迁移到 after_photo
                     plugins.setdefault('after_photo', []).append(pinfo)
                     plugin_list.pop(i)
                     jm_log('option.migrate',
                            f'[zip 插件迁移] level 参数已废弃,打包粒度由所在钩子自动推导。'
                            f'已自动将 after_album 下的 zip(level={level!r}) 等价迁移到 after_photo。'
                            f'等价写法:将 zip 插件从 after_album 移至 after_photo 并删除 level 配置项。')
+                elif group == 'after_photo' and level == 'album':
+                    # after_photo + level=album → 等价迁移到 after_album
+                    plugins.setdefault('after_album', []).append(pinfo)
+                    plugin_list.pop(i)
+                    jm_log('option.migrate',
+                           f'[zip 插件迁移] level 参数已废弃,打包粒度由所在钩子自动推导。'
+                           f'已自动将 after_photo 下的 zip(level={level!r}) 等价迁移到 after_album。'
+                           f'等价写法:将 zip 插件从 after_photo 移至 after_album 并删除 level 配置项。')
                 else:
                     if level != 'photo':
                         jm_log('option.migrate',
                                f'[zip 插件迁移] level 参数已废弃,已自动移除。'
                                f'打包粒度由所在钩子自动推导({group} → {level})。')
                     i += 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if group == 'after_album' and level != 'album':
# after_album + level=photo → 等价迁移到 after_photo
plugins.setdefault('after_photo', []).append(pinfo)
plugin_list.pop(i)
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,打包粒度由所在钩子自动推导。'
f'已自动将 after_album 下的 zip(level={level!r}) 等价迁移到 after_photo。'
f'等价写法:将 zip 插件从 after_album 移至 after_photo 并删除 level 配置项。')
else:
if level != 'photo':
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,已自动移除。'
f'打包粒度由所在钩子自动推导({group}{level})。')
i += 1
if group == 'after_album' and level != 'album':
# after_album + level=photo → 等价迁移到 after_photo
plugins.setdefault('after_photo', []).append(pinfo)
plugin_list.pop(i)
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,打包粒度由所在钩子自动推导。'
f'已自动将 after_album 下的 zip(level={level!r}) 等价迁移到 after_photo。'
f'等价写法:将 zip 插件从 after_album 移至 after_photo 并删除 level 配置项。')
elif group == 'after_photo' and level == 'album':
# after_photo + level=album → 等价迁移到 after_album
plugins.setdefault('after_album', []).append(pinfo)
plugin_list.pop(i)
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,打包粒度由所在钩子自动推导。'
f'已自动将 after_photo 下的 zip(level={level!r}) 等价迁移到 after_album。'
f'等价写法:将 zip 插件从 after_photo 移至 after_album 并删除 level 配置项。')
else:
if level != 'photo':
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已废弃,已自动移除。'
f'打包粒度由所在钩子自动推导({group}{level})。')
i += 1
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 369-369: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


[warning] 371-371: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


[warning] 375-375: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


[warning] 376-376: String contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF001)


[warning] 376-376: String contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_option.py` around lines 364 - 377, The migration currently
only moves zip with level=photo from after_album→after_photo but fails to handle
after_photo entries with level='album'; update the migration in jm_option.py
(the block handling group/level, plugin_list, plugins, pinfo, i, and jm_log) to
detect when group == 'after_photo' and level == 'album' and perform the opposite
migration: append pinfo to plugins.setdefault('after_album', []), remove the
entry from plugin_list (plugin_list.pop(i)), emit a jm_log message describing
the migration to after_album, and avoid simply deleting the level; ensure the
index i is only incremented when you do not pop the current item so loop
behavior remains correct.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (4)
src/jmcomic/jm_feature.py (1)

98-105: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Typo _user_0keys silently breaks the explicit-kwargs tracking; also merge prior keys.

Two issues stacked here:

  1. new_instance._user_0keys = ... is a typo for _user_keys (note the stray 0). The attribute set on __init__ (self._user_keys: set = set()) is never overwritten, so _user_keys always stays empty and any “explicit user kwargs” logic that relies on it (now or in future) is dead.
  2. Even after fixing the typo, this assignment overwrites previously recorded user keys when PluginFeature(...) is chained, which the previous review already raised. Merge with self._user_keys.
🔧 Proposed fix
     def __call__(self, **kwargs):
         """带自定义参数,返回新实例(继承默认参数)"""
         new_kwargs = self.kwargs.copy()
         new_kwargs.update(kwargs)
         new_instance = PluginFeature(self.plugin_key, **new_kwargs)
-        # 记录用户显式传入的参数名,这些参数不被动态适配
-        new_instance._user_0keys = set(kwargs.keys())
+        # 记录用户显式传入的参数名,这些参数不被动态适配
+        new_instance._user_keys = set(self._user_keys) | set(kwargs.keys())
         return new_instance
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_feature.py` around lines 98 - 105, In PluginFeature.__call__,
fix the typo that sets new_instance._user_0keys (should be _user_keys) and merge
existing keys instead of overwriting them: compute the union of self._user_keys
and the keys from kwargs and assign that to new_instance._user_keys so chained
calls preserve prior explicit-kwargs tracking; keep the rest of the new_kwargs
logic the same.
assets/docs/sources/tutorial/13_export_and_feature.md (2)

73-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

filename_rule='Ptitle' in a download_album example will fail at runtime.

Current PluginFeature._adapt_plugin_kwargs only fills a default via setdefault, so the explicit Ptitle is forwarded unchanged into img2pdf invoked from the after_album hook — no photo is in scope there, so the rule resolution will blow up. Use an Axxx rule here, or move the snippet to download_photo.

🔧 Proposed fix
 download_album('123', option, extra=Feature.export_pdf(
     # 下面是自定义参数
     pdf_dir='D:/my_pdfs',          # PDF 保存到 D:/my_pdfs 文件夹
-    filename_rule='Ptitle',        # 用章节标题作为文件名
+    filename_rule='Atitle',        # 用本子标题作为文件名
     delete_original_file=True,     # 合并完 PDF 后删除原图
 ))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/docs/sources/tutorial/13_export_and_feature.md` around lines 73 - 80,
The example uses filename_rule='Ptitle' inside download_album which will fail
because PluginFeature._adapt_plugin_kwargs only setdefaults and the after_album
hook calls img2pdf without a photo in scope; change the example to use an
album-level rule (e.g. an Axxx rule) or move the snippet into download_photo,
and/or update PluginFeature._adapt_plugin_kwargs to resolve/translate
filename_rule for album contexts before forwarding to after_album; specifically
adjust the example referencing Feature.export_pdf and filename_rule or alter
PluginFeature._adapt_plugin_kwargs so it resolves Ptitle when used in
after_album or rejects/rewrites it to an A-prefixed rule.

177-184: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Flow diagram and bullet still claim level is adapted by _adapt_kwargs.

level is no longer adapted by PluginFeature._adapt_plugin_kwargs (it was migrated out, and ZipPlugin infers granularity at runtime). Also, _adapt_plugin_kwargs only sets a default filename_rule; it does not actually swap Ptitle→Atitle / Pid→Aid as the comment in the diagram (# Atitle 不变, Ptitle→Atitle, Pid→Aid) suggests. Either align the prose to “fills a default filename_rule if not provided; ZIP packaging level is inferred by the plugin” or add the prefix-swap logic in code (see related comment on jm_feature.py).

src/jmcomic/jm_option.py (1)

364-380: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

after_photo + level: album is still not migrated.

The else-branch on Line 376 just removes level and leaves the plugin in after_photo, silently turning a legacy after_photo: [{plugin: zip, kwargs: {level: album}}] config from “one merged archive per album” into “one archive per photo”. Mirror the after_album → after_photo migration so the inverse case rehomes to after_album instead of changing semantics.

🔧 Proposed fix
                 if group == 'after_album' and level != 'album':
                     # after_album + level=photo → 等价迁移到 after_photo
                     plugins.setdefault('after_photo', []).append(pinfo)
                     plugin_list.pop(i)
                     jm_log('option.migrate', ...)
+                elif group == 'after_photo' and level == 'album':
+                    # after_photo + level=album → 等价迁移到 after_album
+                    plugins.setdefault('after_album', []).append(pinfo)
+                    plugin_list.pop(i)
+                    jm_log('option.migrate',
+                           f'[zip 插件迁移] level 参数已过时,已自动将 after_photo 下的 '
+                           f'zip(level={level!r}) 等价迁移到 after_album。')
                 else:
                     if level != 'photo':
                         jm_log('option.migrate', ...)
                     i += 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_option.py` around lines 364 - 380, The migration currently
only moves plugins when group == 'after_album' and level != 'album', but it
fails to handle the inverse legacy case where group == 'after_photo' and level
== 'album'; instead it leaves the plugin in plugin_list and logs removing the
level which changes semantics. Update the logic in jm_option.py around the block
using variables group, level, plugins, plugin_list, pinfo and i so that when
group == 'after_photo' and level == 'album' you rehome the plugin to
plugins.setdefault('after_album', []).append(pinfo), remove it from plugin_list
(plugin_list.pop(i)), emit a jm_log message mirroring the existing '[zip 插件迁移]'
guidance (suggest the equivalent after_album config) and do not increment i for
that branch; keep the existing behavior for other cases (including logging and i
+= 1) intact.
🧹 Nitpick comments (1)
src/jmcomic/jm_feature.py (1)

75-96: 💤 Low value

PluginFeature.kwargs is shared mutable state between instances created via __call__.

__init__ keeps a reference to the caller-supplied kwargs dict (self.kwargs = kwargs). For the predefined module-level instances on Lines 170-172 (Feature.export_pdf etc.), the dict is the literal passed in at module import. __call__ does self.kwargs.copy() so derived instances are safe, but anyone mutating Feature.export_pdf.kwargs directly (or via _adapt_plugin_kwargs if it stopped copying) would scribble on the singleton. Recommend defensive-copying in __init__ and using MappingProxyType if you want immutability.

🔧 Suggested hardening
     def __init__(self, plugin_key, **kwargs):
         self.plugin_key = plugin_key
-        self.kwargs = kwargs
+        self.kwargs = dict(kwargs)
         # 用户通过 __call__ 显式传入的参数名,这些参数不会被 _adapt_kwargs 动态适配
         self._user_keys: set = set()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_feature.py` around lines 75 - 96, PluginFeature currently
stores a direct reference to the caller-supplied kwargs in
PluginFeature.__init__ (self.kwargs = kwargs), which creates shared mutable
state for module-level feature singletons like Feature.export_pdf and can be
mutated by callers or helpers such as _adapt_plugin_kwargs; change __init__ to
defensively copy the mapping (e.g., use kwargs.copy()) or wrap it in an
immutable MappingProxyType to prevent external mutations, and ensure the
existing __call__ and _adapt_plugin_kwargs behavior still operate on copies
rather than the original stored dict.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@assets/docs/sources/tutorial/13_export_and_feature.md`:
- Around line 55-58: The OR-syntax example uses a fullwidth vertical bar (|)
which is not Python's bitwise-or operator; update the example in the
download_album call to use the ASCII pipe character so it reads
extra=Feature.export_pdf | Feature.export_zip (refer to the download_album(...)
example and the symbols Feature.export_pdf and Feature.export_zip).

In `@src/jmcomic/jm_feature.py`:
- Around line 19-21: Remove the problematic unused import "LiteralString" from
the top of src/jmcomic/jm_feature.py (the line "from typing import
LiteralString") because it breaks Python 3.9/3.10 CI; simply delete that import
so the file only imports the plugin via "from .jm_plugin import *". If you later
actually need LiteralString, import it from typing_extensions and add a
version/gating check instead of importing it directly from typing.
- Around line 125-132: _adapt_plugin_kwargs currently forces full filename_rule
strings and ignores user overrides; update it to use bare rule defaults (e.g.,
'Atitle' for albums, 'Ptitle' for photos) and perform prefix swapping only for
keys not in self._user_keys: if feature_from == 'download_album' set default
filename_rule to 'Atitle' (and map any default rules by replacing leading
'P'→'A'), if feature_from == 'download_photo' set default to 'Ptitle' (and
replace leading 'A'→'P'); do not modify filename_rule (or any other key) when
that key exists in self._user_keys, and continue to base defaults on
self.kwargs.copy().

In `@src/jmcomic/jm_option.py`:
- Around line 378-379: In jm_option.py remove the unnecessary f-string prefix in
the jm_log call: replace jm_log('option.migrate', f'[zip 插件迁移] level
参数已过时,你可以直接删除该参数,不会有任何影响') with a plain string literal (jm_log('option.migrate',
'[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')) so the f prefix is dropped and
Ruff F541 is avoided.

In `@tests/test_jmcomic/test_jm_feature.py`:
- Around line 63-85: The test references an undefined symbol "when" and asserts
filename_rule values that don't match PluginFeature._adapt_plugin_kwargs
behavior; define or import a valid when value (e.g., when='after_album' or
import the constant used by jm_feature) before calling
Feature.export_pdf/_adapt_plugin_kwargs and then update the assertions to match
the current implementation's outputs (e.g., expect '[JM{Aid}]{Atitle}' or
'[JM{Pid}]{Ptitle}' etc) OR alternatively change
jm_feature.PluginFeature._adapt_plugin_kwargs to return the bare adapted tokens
(like 'Atitle'/'Ptitle'/'Aid') if you prefer to keep the original
assertions—pick one approach and make the change consistently for
Feature.export_pdf, Feature.export_zip, Feature.export_long_img and the custom
Feature.export_zip(filename_rule=...) test.

---

Duplicate comments:
In `@assets/docs/sources/tutorial/13_export_and_feature.md`:
- Around line 73-80: The example uses filename_rule='Ptitle' inside
download_album which will fail because PluginFeature._adapt_plugin_kwargs only
setdefaults and the after_album hook calls img2pdf without a photo in scope;
change the example to use an album-level rule (e.g. an Axxx rule) or move the
snippet into download_photo, and/or update PluginFeature._adapt_plugin_kwargs to
resolve/translate filename_rule for album contexts before forwarding to
after_album; specifically adjust the example referencing Feature.export_pdf and
filename_rule or alter PluginFeature._adapt_plugin_kwargs so it resolves Ptitle
when used in after_album or rejects/rewrites it to an A-prefixed rule.

In `@src/jmcomic/jm_feature.py`:
- Around line 98-105: In PluginFeature.__call__, fix the typo that sets
new_instance._user_0keys (should be _user_keys) and merge existing keys instead
of overwriting them: compute the union of self._user_keys and the keys from
kwargs and assign that to new_instance._user_keys so chained calls preserve
prior explicit-kwargs tracking; keep the rest of the new_kwargs logic the same.

In `@src/jmcomic/jm_option.py`:
- Around line 364-380: The migration currently only moves plugins when group ==
'after_album' and level != 'album', but it fails to handle the inverse legacy
case where group == 'after_photo' and level == 'album'; instead it leaves the
plugin in plugin_list and logs removing the level which changes semantics.
Update the logic in jm_option.py around the block using variables group, level,
plugins, plugin_list, pinfo and i so that when group == 'after_photo' and level
== 'album' you rehome the plugin to plugins.setdefault('after_album',
[]).append(pinfo), remove it from plugin_list (plugin_list.pop(i)), emit a
jm_log message mirroring the existing '[zip 插件迁移]' guidance (suggest the
equivalent after_album config) and do not increment i for that branch; keep the
existing behavior for other cases (including logging and i += 1) intact.

---

Nitpick comments:
In `@src/jmcomic/jm_feature.py`:
- Around line 75-96: PluginFeature currently stores a direct reference to the
caller-supplied kwargs in PluginFeature.__init__ (self.kwargs = kwargs), which
creates shared mutable state for module-level feature singletons like
Feature.export_pdf and can be mutated by callers or helpers such as
_adapt_plugin_kwargs; change __init__ to defensively copy the mapping (e.g., use
kwargs.copy()) or wrap it in an immutable MappingProxyType to prevent external
mutations, and ensure the existing __call__ and _adapt_plugin_kwargs behavior
still operate on copies rather than the original stored dict.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ac8b097-2eec-408c-9b14-09650ab81b2a

📥 Commits

Reviewing files that changed from the base of the PR and between 26a830d and 09233da.

📒 Files selected for processing (6)
  • assets/docs/sources/option_file_syntax.md
  • assets/docs/sources/tutorial/13_export_and_feature.md
  • src/jmcomic/jm_downloader.py
  • src/jmcomic/jm_feature.py
  • src/jmcomic/jm_option.py
  • tests/test_jmcomic/test_jm_feature.py

Comment on lines +55 to +58
# 也支持列表语法,|语法
download_album('123', option, extra=[Feature.export_pdf, Feature.export_zip])
download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Replace fullwidth with ASCII | in the OR-syntax example.

Line 57 uses (U+FF5C, FULLWIDTH VERTICAL LINE), which is not the Python bitwise-or operator. A user copy-pasting this snippet will hit a SyntaxError.

🔧 Proposed fix
 # 也支持列表语法,|语法
 download_album('123', option, extra=[Feature.export_pdf, Feature.export_zip])
-download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
+download_album('123', option, extra=Feature.export_pdf | Feature.export_zip)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@assets/docs/sources/tutorial/13_export_and_feature.md` around lines 55 - 58,
The OR-syntax example uses a fullwidth vertical bar (|) which is not Python's
bitwise-or operator; update the example in the download_album call to use the
ASCII pipe character so it reads extra=Feature.export_pdf | Feature.export_zip
(refer to the download_album(...) example and the symbols Feature.export_pdf and
Feature.export_zip).

Comment thread src/jmcomic/jm_feature.py
Comment on lines +19 to +21
from typing import LiteralString

from .jm_plugin import *
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Drop the LiteralString import — it breaks Python 3.9/3.10 CI and is unused.

typing.LiteralString was added in 3.11; on 3.9/3.10 the existing from typing import LiteralString raises ImportError at import time, which is exactly what the pipeline run logs show for Run Test (API) / test (3.9, …) and (3.10, …). The symbol is also not referenced anywhere in this file.

🔧 Proposed fix
-from typing import LiteralString
-
 from .jm_plugin import *

If you actually need LiteralString somewhere later, import it from typing_extensions and gate by version.

🧰 Tools
🪛 GitHub Actions: Run Test (API) / 2_test (3.10, ubuntu-latest).txt

[error] 19-19: ImportError: cannot import name 'LiteralString' from 'typing'. The code uses 'from typing import LiteralString', which is not available in Python 3.10; use 'typing_extensions.LiteralString' or remove usage.

🪛 GitHub Actions: Run Test (API) / 3_test (3.9, ubuntu-latest).txt

[error] 19-19: ImportError: cannot import name 'LiteralString' from 'typing'. In Python 3.9, LiteralString is not available; consider using typing_extensions.LiteralString or refactor to a compatible approach. This occurred while running 'python -m unittest'.

🪛 GitHub Actions: Run Test (API) / test (3.10, ubuntu-latest)

[error] 19-19: ImportError: cannot import name 'LiteralString' from 'typing' during unit tests ('python -m unittest'). In Python 3.10, LiteralString is not available in typing. Use 'typing_extensions.LiteralString' or replace with an appropriate type (e.g., str).

🪛 GitHub Actions: Run Test (API) / test (3.9, ubuntu-latest)

[error] 19-19: ImportError: cannot import name 'LiteralString' from 'typing' (Python 3.9). Use typing_extensions.LiteralString or adjust type usage.

🪛 Ruff (0.15.12)

[error] 21-21: from .jm_plugin import * used; unable to detect undefined names

(F403)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_feature.py` around lines 19 - 21, Remove the problematic
unused import "LiteralString" from the top of src/jmcomic/jm_feature.py (the
line "from typing import LiteralString") because it breaks Python 3.9/3.10 CI;
simply delete that import so the file only imports the plugin via "from
.jm_plugin import *". If you later actually need LiteralString, import it from
typing_extensions and add a version/gating check instead of importing it
directly from typing.

Comment thread src/jmcomic/jm_feature.py
Comment on lines +125 to +132
def _adapt_plugin_kwargs(self, feature_from: str, when: str) -> dict:
"""
根据feature_from和when动态确定以下插件参数:
filename_rule
"""
kwargs = self.kwargs.copy()
kwargs.setdefault('filename_rule', '[JM{Aid}]{Atitle}' if feature_from == 'download_album' else '[JM{Pid}]{Ptitle}')
return kwargs
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm there's no other prefix-adaptation logic we might be missing.
rg -n -C2 'filename_rule|_adapt_plugin_kwargs|_user_keys'

Repository: hect0x7/JMComic-Crawler-Python

Length of output: 15148


_adapt_plugin_kwargs doesn't match the documented contract or tests.

Current behavior: unconditionally sets 'filename_rule' to '[JM{Aid}]{Atitle}' or '[JM{Pid}]{Ptitle}'.

Expected behavior (per tests):

  • Use bare rule names ('Atitle', 'Ptitle', 'Aid', 'Pid')
  • Swap P↔A prefixes when switching context: 'Ptitle''Atitle' in download_album mode, 'Atitle''Ptitle' in download_photo mode
  • Skip adaptation for rules explicitly passed by the user (check if k not in self._user_keys)

The proposed implementation in the review correctly addresses this by:

  • Using bare defaults aligned with plugin signatures (e.g., 'Atitle' for album, 'Ptitle' for photo)
  • Swapping prefixes only for non-user-supplied rules
  • Honoring user-explicit parameters via the _user_keys check
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_feature.py` around lines 125 - 132, _adapt_plugin_kwargs
currently forces full filename_rule strings and ignores user overrides; update
it to use bare rule defaults (e.g., 'Atitle' for albums, 'Ptitle' for photos)
and perform prefix swapping only for keys not in self._user_keys: if
feature_from == 'download_album' set default filename_rule to 'Atitle' (and map
any default rules by replacing leading 'P'→'A'), if feature_from ==
'download_photo' set default to 'Ptitle' (and replace leading 'A'→'P'); do not
modify filename_rule (or any other key) when that key exists in self._user_keys,
and continue to base defaults on self.kwargs.copy().

Comment thread src/jmcomic/jm_option.py
Comment on lines +378 to +379
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop the unused f prefix.

The string has no placeholders, so the f prefix is dead and trips Ruff F541. Plain string literal is enough.

🔧 Proposed fix
-                        jm_log('option.migrate',
-                               f'[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')
+                        jm_log('option.migrate',
+                               '[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jm_log('option.migrate',
f'[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')
jm_log('option.migrate',
'[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')
🧰 Tools
🪛 Ruff (0.15.12)

[error] 379-379: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 379-379: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


[warning] 379-379: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jmcomic/jm_option.py` around lines 378 - 379, In jm_option.py remove the
unnecessary f-string prefix in the jm_log call: replace jm_log('option.migrate',
f'[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响') with a plain string literal
(jm_log('option.migrate', '[zip 插件迁移] level 参数已过时,你可以直接删除该参数,不会有任何影响')) so the f
prefix is dropped and Ruff F541 is avoided.

Comment on lines +63 to +85
def test_adapt_kwargs(self):
"""测试 PluginFeature 参数动态适配"""
# download_album 模式:P前缀 → A前缀, level → album
pdf = Feature.export_pdf
adapted = pdf._adapt_plugin_kwargs('download_album', when)
self.assertEqual(adapted['filename_rule'], 'Atitle') # A开头不变

zip_f = Feature.export_zip
adapted = zip_f._adapt_plugin_kwargs('download_album', when)
self.assertEqual(adapted['filename_rule'], 'Atitle') # Ptitle → Atitle

long_img = Feature.export_long_img
adapted = long_img._adapt_plugin_kwargs('download_album', when)
self.assertEqual(adapted['filename_rule'], 'Aid') # Pid → Aid

# download_photo 模式:A前缀 → P前缀, level → photo
adapted = pdf._adapt_plugin_kwargs('download_photo', when)
self.assertEqual(adapted['filename_rule'], 'Ptitle') # Atitle → Ptitle

# 用户显式传入的参数不被动态适配
custom = Feature.export_zip(filename_rule='Ptitle')
adapted = custom._adapt_plugin_kwargs('download_album', when)
self.assertEqual(adapted['filename_rule'], 'Ptitle') # 用户显式指定,不适配
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

when is undefined; assertions also disagree with the current _adapt_plugin_kwargs implementation.

Two compounding problems:

  1. when is referenced in every _adapt_plugin_kwargs(...) call but never defined or imported (the file only does from . import *). The test will fail with NameError: name 'when' is not defined before any assertion runs.
  2. Even after passing when (e.g. 'after_album'), PluginFeature._adapt_plugin_kwargs in jm_feature.py returns filename_rule='[JM{Aid}]{Atitle}' (or '[JM{Pid}]{Ptitle}') — not the bare 'Atitle' / 'Ptitle' / 'Aid' these tests assert. Either the implementation needs to add the documented P↔A prefix swap, or these expectations need to be rewritten to match the actual default rule. See the related comment on jm_feature.py:_adapt_plugin_kwargs.
🔧 Minimal fix for the undefined name (assertions still need to be reconciled with the implementation)
     def test_adapt_kwargs(self):
         """测试 PluginFeature 参数动态适配"""
         # download_album 模式:P前缀 → A前缀, level → album
         pdf = Feature.export_pdf
-        adapted = pdf._adapt_plugin_kwargs('download_album', when)
+        adapted = pdf._adapt_plugin_kwargs('download_album', 'after_album')
         self.assertEqual(adapted['filename_rule'], 'Atitle')  # A开头不变
         ...
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 65-65: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 78-78: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


[warning] 85-85: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_jmcomic/test_jm_feature.py` around lines 63 - 85, The test
references an undefined symbol "when" and asserts filename_rule values that
don't match PluginFeature._adapt_plugin_kwargs behavior; define or import a
valid when value (e.g., when='after_album' or import the constant used by
jm_feature) before calling Feature.export_pdf/_adapt_plugin_kwargs and then
update the assertions to match the current implementation's outputs (e.g.,
expect '[JM{Aid}]{Atitle}' or '[JM{Pid}]{Ptitle}' etc) OR alternatively change
jm_feature.PluginFeature._adapt_plugin_kwargs to return the bare adapted tokens
(like 'Atitle'/'Ptitle'/'Aid') if you prefer to keep the original
assertions—pick one approach and make the change consistently for
Feature.export_pdf, Feature.export_zip, Feature.export_long_img and the custom
Feature.export_zip(filename_rule=...) test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant