Skip to content

Fix to_finals dropping the 'n' from syllabic 'ng' (嗯/呣/唔) in non-strict mode#371

Open
gaoflow wants to merge 1 commit into
mozillazg:masterfrom
gaoflow:fix-finals-syllabic-ng
Open

Fix to_finals dropping the 'n' from syllabic 'ng' (嗯/呣/唔) in non-strict mode#371
gaoflow wants to merge 1 commit into
mozillazg:masterfrom
gaoflow:fix-finals-syllabic-ng

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 30, 2026

Copy link
Copy Markdown

简述:非严格模式下,to_finals 会把鼻音节 ng(嗯/兀/唔 等读音 ńg/ňg/ǹg)切成声母 n + 韵母 g,丢掉了 n,返回了不存在的韵母 g。本 PR 让整个 ng 作为韵母返回,与单独的 m/n 保持一致;严格模式(无韵母)行为不变。

The bug

In non-strict mode, to_finals drops the n from the syllabic velar-nasal reading ng and returns the invalid final 'g':

>>> from pypinyin.contrib.tone_convert import to_finals
>>> to_finals('ńg', strict=False)   # 嗯/兀/唔 etc. (also 'ňg', 'ǹg', 'ng')
'g'                                  # expected 'ng'
>>> to_finals('ń', strict=False)     # sibling bare nasal — correct
'n'

It propagates to every style and to the high-level API:

>>> from pypinyin import pinyin, Style
>>> pinyin('嗯', style=Style.FINALS, heteronym=True, strict=False)
[['n', 'g']]                         # the 'g' is corrupt; should be 'ng'
>>> pinyin('嗯', style=Style.FINALS_TONE3, heteronym=True, strict=False)
[['n2', 'g2', 'g3', 'g4', 'n3', 'n4']]   # should be ng2/ng3/ng4

Why 'ng' is the correct result

Root cause

pypinyin/style/_utils.py::get_finals: for ng, get_initials('ng', strict=False) greedily matches the valid initial n, so finals = 'ng'[len('n'):] = 'g'. The existing guard if not finals and not strict: return pinyin only rescues the bare m/n (where the remainder is empty); for ng the remainder 'g' is non-empty, so the guard is skipped and the invalid 'g' is returned. The fix adds the ng case (returns the whole syllable in non-strict mode), leaving the strict path and the general empty-final path untouched.

Tests

tests/contrib/test_tone_convert.py: added ng cases (ńg/ňg/ǹg/ng) to the to_finals, to_finals_tone, to_finals_tone2, to_finals_tone3 parametrize blocks — including strict=True → '' controls and the ń → n / ḿ → m sibling controls. tests/test_others.py::test_finals_syllabic_ng_keeps_n covers the high-level pinyin('嗯', …, heteronym=True, strict=False) blast radius and asserts the strict-default [['']] is preserved.

The added positive cases fail on master (assert 'g' == 'ng') and pass with the fix; full suite stays green (the only failures here are the pre-existing tests/test_cmd.py pty/OSError ones, unrelated to this change).

Summary by CodeRabbit

  • Bug Fixes
    • Improved lenient pinyin parsing for syllabic nasal endings, so ng is now preserved as a whole final in non-strict mode.
    • Refined tone conversion behavior for nasal syllables, including clearer handling of m, n, and ng forms.
    • Updated results for syllables like so non-strict conversions keep ng-based finals, while strict mode remains unchanged.

In non-strict mode get_finals() splits the syllabic velar nasal 'ng'
(readings of 嗯/兀/唔, e.g. ńg/ňg/ǹg) into initial 'n' + final 'g', so it
returns the invalid final 'g' and silently drops the 'n'. The existing
m/n guard only fires when the remaining final is empty, which never
happens for 'ng' ('g' is left over). Return the whole 'ng' as the final,
matching the sibling bare m/n behaviour. Strict mode (no final, by
design) is left unchanged.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

In get_finals, a guard is added so that when pinyin == 'ng' in non-strict mode, the whole string 'ng' is returned as the final instead of being split into initial n plus an invalid g remainder. Tests are added for tone conversion and pinyin() decomposition of to verify the new behavior.

ng Finals Non-strict Fix

Layer / File(s) Summary
get_finals ng guard
pypinyin/style/_utils.py
Adds an early return for pinyin == 'ng' in the non-strict branch, returning 'ng' as the full final.
Tests for ng finals
tests/contrib/test_tone_convert.py, tests/test_others.py
Extends parameterized tests for ńg/ňg/ǹg/ng in test_to_initials, test_to_finals, test_to_finals_tone, test_to_finals_tone2, and adds test_finals_syllabic_ng_keeps_n for decomposition in strict vs non-strict mode.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A bunny hops past 'n' and 'g',
refusing to let them split and flee.
"Stay together!" the finals say,
嗯 sounds whole — hip hip hooray! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 clearly describes the main fix: non-strict syllabic 'ng' finals no longer lose the 'n'.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@pypinyin/style/_utils.py`:
- Around line 63-64: The new comment in the helper around pinyin splitting uses
fullwidth punctuation that triggers Ruff RUF003; update the comment in the
_utils.py logic near the handling of “ńg/嗯” to use ASCII punctuation only,
replacing the fullwidth parentheses and comma with standard ASCII equivalents
while keeping the meaning unchanged.

In `@tests/contrib/test_tone_convert.py`:
- Around line 292-293: Normalize the added test comment punctuation in
test_tone_convert so Ruff no longer flags RUF003; update the comment text around
the nasal-syllable note and the later note at the referenced spot to use ASCII
punctuation instead of fullwidth marks, keeping the same meaning while matching
the project's linting rules.

In `@tests/test_others.py`:
- Around line 77-78: Normalize the new test comments in test_others.py to use
ASCII punctuation only, since Ruff flags the fullwidth characters as RUF003.
Update the affected comment text around the assertions in the test module so the
same meaning remains but all punctuation is ASCII, and check the nearby comment
referenced by the same review to keep the file lint-clean.
🪄 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: 71993d7d-13c9-47d9-b8fd-0bd8358cc3a3

📥 Commits

Reviewing files that changed from the base of the PR and between 8595294 and 55709f0.

📒 Files selected for processing (3)
  • pypinyin/style/_utils.py
  • tests/contrib/test_tone_convert.py
  • tests/test_others.py

Comment thread pypinyin/style/_utils.py
Comment on lines +63 to +64
# ńg(嗯 等)会被切出声母 n,剩下的 'g' 不是有效韵母,
# 非严格模式下应将整个 ng 作为韵母返回。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the fullwidth punctuation in this new comment.

Lines 63-64 use fullwidth , , and , which Ruff reports as RUF003. Please normalize them to ASCII so this change stays lint-clean.

🧰 Tools
🪛 Ruff (0.15.20)

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

(RUF003)


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

(RUF003)


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

(RUF003)


[warning] 63-63: 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 `@pypinyin/style/_utils.py` around lines 63 - 64, The new comment in the helper
around pinyin splitting uses fullwidth punctuation that triggers Ruff RUF003;
update the comment in the _utils.py logic near the handling of “ńg/嗯” to use
ASCII punctuation only, replacing the fullwidth parentheses and comma with
standard ASCII equivalents while keeping the meaning unchanged.

Source: Linters/SAST tools

Comment on lines +292 to +293
# 嗯/呣/唔 等鼻音节:非严格模式下整个 m/n/ng 作为韵母,
# 严格模式下没有韵母(保持原有设计)。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Normalize the punctuation in the added test comments.

Lines 292-293 and Line 300 contain fullwidth punctuation that Ruff flags as RUF003. Swapping those marks to ASCII avoids a needless lint failure here.

Also applies to: 300-300

🧰 Tools
🪛 Ruff (0.15.20)

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

(RUF003)


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

(RUF003)


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

(RUF003)


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

(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/contrib/test_tone_convert.py` around lines 292 - 293, Normalize the
added test comment punctuation in test_tone_convert so Ruff no longer flags
RUF003; update the comment text around the nasal-syllable note and the later
note at the referenced spot to use ASCII punctuation instead of fullwidth marks,
keeping the same meaning while matching the project's linting rules.

Source: Linters/SAST tools

Comment thread tests/test_others.py
Comment on lines +77 to +78
# 嗯 等字的 ńg/ňg/ǹg 读音在非严格模式下,韵母应是完整的 ``ng``,
# 而不是把声母 n 切掉后剩下的无效韵母 ``g``。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use ASCII punctuation in these new comments.

Lines 77-78 and Line 88 introduce fullwidth punctuation that Ruff reports as RUF003. Please normalize those characters so the test file remains lint-clean.

Also applies to: 88-88

🧰 Tools
🪛 Ruff (0.15.20)

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

(RUF003)


[warning] 77-77: 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_others.py` around lines 77 - 78, Normalize the new test comments
in test_others.py to use ASCII punctuation only, since Ruff flags the fullwidth
characters as RUF003. Update the affected comment text around the assertions in
the test module so the same meaning remains but all punctuation is ASCII, and
check the nearby comment referenced by the same review to keep the file
lint-clean.

Source: Linters/SAST tools

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