scriptconv 0.0.3a1__tar.gz → 0.0.3a3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/PKG-INFO +7 -1
  2. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/pyproject.toml +3 -1
  3. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/__init__.py +16 -1
  4. scriptconv-0.0.3a3/scriptconv/readings.py +119 -0
  5. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/version.py +1 -1
  6. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv.egg-info/PKG-INFO +7 -1
  7. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv.egg-info/SOURCES.txt +3 -0
  8. scriptconv-0.0.3a3/scriptconv.egg-info/requires.txt +12 -0
  9. scriptconv-0.0.3a3/tests/test_readings.py +73 -0
  10. scriptconv-0.0.3a3/tests/test_readings_zh.py +78 -0
  11. scriptconv-0.0.3a1/scriptconv.egg-info/requires.txt +0 -4
  12. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/LICENSE +0 -0
  13. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/README.md +0 -0
  14. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/requirements.txt +0 -0
  15. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/__main__.py +0 -0
  16. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/notation.py +0 -0
  17. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/py.typed +0 -0
  18. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/scripts.py +0 -0
  19. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv/translit.py +0 -0
  20. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv.egg-info/dependency_links.txt +0 -0
  21. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/scriptconv.egg-info/top_level.txt +0 -0
  22. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/setup.cfg +0 -0
  23. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/tests/test_cli.py +0 -0
  24. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/tests/test_examples.py +0 -0
  25. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/tests/test_notation.py +0 -0
  26. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/tests/test_scripts.py +0 -0
  27. {scriptconv-0.0.3a1 → scriptconv-0.0.3a3}/tests/test_translit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.3a1
3
+ Version: 0.0.3a3
4
4
  Summary: Zero-dependency script & phoneme-notation core — ISO-15924 detection & metadata, IPA↔ARPABET/X-SAMPA/Lexique/Kirshenbaum/Cotovía/RFE, Buckwalter↔Arabic, Hangul→jamo, kana
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/TigreGotico/scriptconv
@@ -20,9 +20,15 @@ Classifier: Operating System :: OS Independent
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
+ Provides-Extra: ja
24
+ Requires-Dist: pykakasi<3,>=2.3; extra == "ja"
25
+ Provides-Extra: zh
26
+ Requires-Dist: pypinyin<1,>=0.50; extra == "zh"
23
27
  Provides-Extra: test
24
28
  Requires-Dist: pytest; extra == "test"
25
29
  Requires-Dist: pytest-timeout; extra == "test"
30
+ Requires-Dist: pykakasi<3,>=2.3; extra == "test"
31
+ Requires-Dist: pypinyin<1,>=0.50; extra == "test"
26
32
  Dynamic: license-file
27
33
 
28
34
  # scriptconv
@@ -33,7 +33,9 @@ version = {attr = "scriptconv.version.VERSION_STR"}
33
33
  dependencies = {file = ["requirements.txt"]}
34
34
 
35
35
  [project.optional-dependencies]
36
- test = ["pytest", "pytest-timeout"]
36
+ ja = ["pykakasi>=2.3,<3"]
37
+ zh = ["pypinyin>=0.50,<1"]
38
+ test = ["pytest", "pytest-timeout", "pykakasi>=2.3,<3", "pypinyin>=0.50,<1"]
37
39
 
38
40
  [project.urls]
39
41
  Homepage = "https://github.com/TigreGotico/scriptconv"
@@ -17,8 +17,12 @@ notation:
17
17
  translit:
18
18
  Script-level decomposition and transliteration (Hangul → jamo,
19
19
  Hiragana ↔ Katakana).
20
+ readings:
21
+ Dictionary-backed respelling (Japanese kanji → kana via the ``ja``
22
+ extra; Chinese hanzi → pinyin/bopomofo via the ``zh`` extra).
20
23
 
21
- Zero runtime dependencies (stdlib only).
24
+ Zero required runtime dependencies (stdlib only); the readings module
25
+ needs the optional ``ja``/``zh`` extras.
22
26
  """
23
27
 
24
28
  from scriptconv.scripts import (
@@ -61,6 +65,12 @@ from scriptconv.translit import (
61
65
  hira_to_kana,
62
66
  kana_to_hira,
63
67
  )
68
+ from scriptconv.readings import (
69
+ to_hiragana,
70
+ to_katakana,
71
+ to_pinyin,
72
+ to_bopomofo,
73
+ )
64
74
 
65
75
  __all__ = [
66
76
  # scripts
@@ -100,4 +110,9 @@ __all__ = [
100
110
  "decompose_hangul",
101
111
  "hira_to_kana",
102
112
  "kana_to_hira",
113
+ # readings (dictionary-backed, needs scriptconv[ja])
114
+ "to_hiragana",
115
+ "to_katakana",
116
+ "to_pinyin",
117
+ "to_bopomofo",
103
118
  ]
@@ -0,0 +1,119 @@
1
+ """Dictionary-backed script respelling (Japanese kanji → kana, Chinese hanzi → pinyin/bopomofo).
2
+
3
+ Unlike the table-driven modules (:mod:`scriptconv.translit`,
4
+ :mod:`scriptconv.notation`), converting kanji to kana is not a codepoint
5
+ mapping: a kanji's kana spelling is a *lexical* property resolved against a
6
+ reading dictionary, and genuinely ambiguous readings are decided by the
7
+ dictionary's segmentation. This is still orthography — the output is how the
8
+ word is written in kana, exactly as an IME or furigana would render it — not
9
+ phonemization; no sound rules are applied.
10
+
11
+ The same reasoning covers Chinese: a hanzi's pinyin or bopomofo spelling is a
12
+ lexical property with genuinely ambiguous readings (heteronyms such as 行
13
+ xíng/háng) resolved by a phrase dictionary — standard orthographic respelling,
14
+ not phonemization.
15
+
16
+ Because it needs reading dictionaries, this module requires optional
17
+ dependencies: ``pykakasi`` for Japanese (``pip install scriptconv[ja]``) and
18
+ ``pypinyin`` for Chinese (``pip install scriptconv[zh]``). Importing the
19
+ module is always safe; the dependency is resolved on first conversion and a
20
+ missing dictionary raises :class:`ImportError` rather than silently returning
21
+ unconverted text.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ __all__ = ["to_hiragana", "to_katakana", "to_pinyin", "to_bopomofo"]
26
+
27
+ _kakasi = None
28
+
29
+ _HIRA = range(0x3040, 0x30A0)
30
+ _KANA = range(0x30A0, 0x3100)
31
+
32
+
33
+ def _converter():
34
+ global _kakasi
35
+ if _kakasi is None:
36
+ try:
37
+ import pykakasi
38
+ except ImportError:
39
+ raise ImportError(
40
+ "kanji→kana conversion needs the reading dictionary from "
41
+ "pykakasi — install with `pip install scriptconv[ja]`"
42
+ ) from None
43
+ _kakasi = pykakasi.kakasi()
44
+ return _kakasi
45
+
46
+
47
+ def to_hiragana(text: str, keep_katakana: bool = False) -> str:
48
+ """Respell Japanese text in hiragana.
49
+
50
+ Kanji are resolved to their dictionary reading; existing kana and any
51
+ non-Japanese characters pass through. With ``keep_katakana=True``,
52
+ katakana tokens keep their original script (useful when katakana marks
53
+ loanwords deliberately) instead of being folded into hiragana.
54
+ """
55
+ out = []
56
+ for token in _converter().convert(text):
57
+ orig = token["orig"]
58
+ if keep_katakana and orig and all(ord(c) in _KANA or not _is_japanese(c)
59
+ for c in orig):
60
+ out.append(orig)
61
+ else:
62
+ out.append(token["hira"])
63
+ return "".join(out)
64
+
65
+
66
+ def to_katakana(text: str) -> str:
67
+ """Respell Japanese text in katakana.
68
+
69
+ Kanji are resolved to their dictionary reading; existing kana are
70
+ transposed to katakana and any non-Japanese characters pass through.
71
+ """
72
+ return "".join(token["kana"] for token in _converter().convert(text))
73
+
74
+
75
+ def _is_japanese(ch: str) -> bool:
76
+ cp = ord(ch)
77
+ return cp in _HIRA or cp in _KANA or 0x4E00 <= cp <= 0x9FFF
78
+
79
+
80
+ _PINYIN_STYLES = None
81
+
82
+
83
+ def _pinyin(text: str, style_name: str) -> str:
84
+ global _PINYIN_STYLES
85
+ if _PINYIN_STYLES is None:
86
+ try:
87
+ from pypinyin import Style, lazy_pinyin
88
+ except ImportError:
89
+ raise ImportError(
90
+ "hanzi→pinyin/bopomofo conversion needs the phrase dictionary "
91
+ "from pypinyin — install with `pip install scriptconv[zh]`"
92
+ ) from None
93
+ _PINYIN_STYLES = {"mark": Style.TONE, "number": Style.TONE3,
94
+ "none": Style.NORMAL, "bopomofo": Style.BOPOMOFO,
95
+ "_fn": lazy_pinyin}
96
+ if style_name not in _PINYIN_STYLES:
97
+ raise ValueError(f"tone must be one of 'mark', 'number', 'none' — got {style_name!r}")
98
+ tokens = _PINYIN_STYLES["_fn"](text, style=_PINYIN_STYLES[style_name])
99
+ return " ".join(" ".join(tokens).split())
100
+
101
+
102
+ def to_pinyin(text: str, tone: str = "mark") -> str:
103
+ """Respell Chinese text in pinyin, syllables space-separated.
104
+
105
+ Hanzi are resolved to their dictionary reading (heteronyms decided by
106
+ pypinyin's phrase dictionary); non-Chinese characters pass through.
107
+ ``tone`` selects the tone spelling: ``"mark"`` (``zhōng``, default),
108
+ ``"number"`` (``zhong1``) or ``"none"`` (``zhong``).
109
+ """
110
+ return _pinyin(text, tone)
111
+
112
+
113
+ def to_bopomofo(text: str) -> str:
114
+ """Respell Chinese text in bopomofo (zhuyin), syllables space-separated.
115
+
116
+ Hanzi are resolved to their dictionary reading; non-Chinese characters
117
+ pass through. Tone marks follow zhuyin convention (first tone unmarked).
118
+ """
119
+ return _pinyin(text, "bopomofo")
@@ -2,7 +2,7 @@
2
2
  VERSION_MAJOR = 0
3
3
  VERSION_MINOR = 0
4
4
  VERSION_BUILD = 3
5
- VERSION_ALPHA = 1
5
+ VERSION_ALPHA = 3
6
6
  # END_VERSION_BLOCK
7
7
 
8
8
  VERSION_STR = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.3a1
3
+ Version: 0.0.3a3
4
4
  Summary: Zero-dependency script & phoneme-notation core — ISO-15924 detection & metadata, IPA↔ARPABET/X-SAMPA/Lexique/Kirshenbaum/Cotovía/RFE, Buckwalter↔Arabic, Hangul→jamo, kana
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/TigreGotico/scriptconv
@@ -20,9 +20,15 @@ Classifier: Operating System :: OS Independent
20
20
  Requires-Python: >=3.10
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
+ Provides-Extra: ja
24
+ Requires-Dist: pykakasi<3,>=2.3; extra == "ja"
25
+ Provides-Extra: zh
26
+ Requires-Dist: pypinyin<1,>=0.50; extra == "zh"
23
27
  Provides-Extra: test
24
28
  Requires-Dist: pytest; extra == "test"
25
29
  Requires-Dist: pytest-timeout; extra == "test"
30
+ Requires-Dist: pykakasi<3,>=2.3; extra == "test"
31
+ Requires-Dist: pypinyin<1,>=0.50; extra == "test"
26
32
  Dynamic: license-file
27
33
 
28
34
  # scriptconv
@@ -6,6 +6,7 @@ scriptconv/__init__.py
6
6
  scriptconv/__main__.py
7
7
  scriptconv/notation.py
8
8
  scriptconv/py.typed
9
+ scriptconv/readings.py
9
10
  scriptconv/scripts.py
10
11
  scriptconv/translit.py
11
12
  scriptconv/version.py
@@ -17,5 +18,7 @@ scriptconv.egg-info/top_level.txt
17
18
  tests/test_cli.py
18
19
  tests/test_examples.py
19
20
  tests/test_notation.py
21
+ tests/test_readings.py
22
+ tests/test_readings_zh.py
20
23
  tests/test_scripts.py
21
24
  tests/test_translit.py
@@ -0,0 +1,12 @@
1
+
2
+ [ja]
3
+ pykakasi<3,>=2.3
4
+
5
+ [test]
6
+ pytest
7
+ pytest-timeout
8
+ pykakasi<3,>=2.3
9
+ pypinyin<1,>=0.50
10
+
11
+ [zh]
12
+ pypinyin<1,>=0.50
@@ -0,0 +1,73 @@
1
+ import sys
2
+ import unittest
3
+ from unittest import mock
4
+
5
+ from scriptconv import to_hiragana, to_katakana
6
+ from scriptconv import readings
7
+
8
+
9
+ class TestToHiragana(unittest.TestCase):
10
+ def test_kanji_resolved_to_reading(self):
11
+ self.assertEqual(to_hiragana("日本語"), "にほんご")
12
+
13
+ def test_katakana_folded_by_default(self):
14
+ self.assertEqual(to_hiragana("コーヒー"), "こーひー")
15
+
16
+ def test_keep_katakana_flag_preserves_loanwords(self):
17
+ self.assertEqual(to_hiragana("コーヒー", keep_katakana=True), "コーヒー")
18
+
19
+ def test_keep_katakana_still_converts_kanji(self):
20
+ out = to_hiragana("私はコーヒーが好き", keep_katakana=True)
21
+ self.assertIn("コーヒー", out)
22
+ self.assertNotIn("私", out)
23
+ self.assertNotIn("好", out)
24
+
25
+ def test_hiragana_passes_through(self):
26
+ self.assertEqual(to_hiragana("こんにちは"), "こんにちは")
27
+
28
+ def test_non_japanese_passes_through(self):
29
+ self.assertEqual(to_hiragana("OVOS 123!"), "OVOS 123!")
30
+
31
+ def test_mixed_script_sentence(self):
32
+ out = to_hiragana("東京タワーは333mです")
33
+ self.assertIn("とうきょう", out)
34
+ self.assertIn("333m", out)
35
+
36
+ def test_empty_string(self):
37
+ self.assertEqual(to_hiragana(""), "")
38
+
39
+
40
+ class TestToKatakana(unittest.TestCase):
41
+ def test_kanji_resolved_to_reading(self):
42
+ self.assertEqual(to_katakana("日本語"), "ニホンゴ")
43
+
44
+ def test_hiragana_transposed(self):
45
+ self.assertEqual(to_katakana("こんにちは"), "コンニチハ")
46
+
47
+ def test_katakana_passes_through(self):
48
+ self.assertEqual(to_katakana("コーヒー"), "コーヒー")
49
+
50
+ def test_non_japanese_passes_through(self):
51
+ self.assertEqual(to_katakana("OVOS 123!"), "OVOS 123!")
52
+
53
+ def test_empty_string(self):
54
+ self.assertEqual(to_katakana(""), "")
55
+
56
+
57
+ class TestDependencyBoundary(unittest.TestCase):
58
+ def test_missing_pykakasi_raises_with_install_hint(self):
59
+ with mock.patch.object(readings, "_kakasi", None), \
60
+ mock.patch.dict(sys.modules, {"pykakasi": None}):
61
+ with self.assertRaises(ImportError) as ctx:
62
+ to_hiragana("日本語")
63
+ self.assertIn("scriptconv[ja]", str(ctx.exception))
64
+
65
+ def test_converter_instance_is_cached(self):
66
+ to_hiragana("日本語")
67
+ first = readings._kakasi
68
+ to_katakana("日本語")
69
+ self.assertIs(readings._kakasi, first)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ unittest.main()
@@ -0,0 +1,78 @@
1
+ import sys
2
+ import unittest
3
+ from unittest import mock
4
+
5
+ from scriptconv import to_pinyin, to_bopomofo
6
+ from scriptconv import readings
7
+
8
+
9
+ class TestToPinyin(unittest.TestCase):
10
+ def test_hanzi_resolved_with_tone_marks(self):
11
+ self.assertEqual(to_pinyin("中国人"), "zhōng guó rén")
12
+
13
+ def test_tone_number_style(self):
14
+ self.assertEqual(to_pinyin("中国人", tone="number"), "zhong1 guo2 ren2")
15
+
16
+ def test_tone_none_style(self):
17
+ self.assertEqual(to_pinyin("中国人", tone="none"), "zhong guo ren")
18
+
19
+ def test_heteronym_resolved_by_phrase_context(self):
20
+ # 行 reads háng in 银行 (bank) but xíng in 行走 (to walk)
21
+ self.assertIn("háng", to_pinyin("银行"))
22
+ self.assertIn("xíng", to_pinyin("行走"))
23
+
24
+ def test_non_chinese_passes_through(self):
25
+ self.assertEqual(to_pinyin("OVOS 123!"), "OVOS 123!")
26
+
27
+ def test_mixed_script_sentence(self):
28
+ out = to_pinyin("我有3个apple")
29
+ self.assertIn("wǒ", out)
30
+ self.assertIn("apple", out)
31
+
32
+ def test_invalid_tone_raises_value_error(self):
33
+ with self.assertRaises(ValueError):
34
+ to_pinyin("中", tone="fancy")
35
+
36
+ def test_empty_string(self):
37
+ self.assertEqual(to_pinyin(""), "")
38
+
39
+
40
+ class TestToBopomofo(unittest.TestCase):
41
+ def test_hanzi_resolved_to_zhuyin(self):
42
+ self.assertEqual(to_bopomofo("中国"), "ㄓㄨㄥ ㄍㄨㄛˊ")
43
+
44
+ def test_first_tone_unmarked_second_tone_marked(self):
45
+ out = to_bopomofo("中国")
46
+ self.assertNotIn("ˉ", out)
47
+ self.assertIn("ˊ", out)
48
+
49
+ def test_non_chinese_passes_through(self):
50
+ self.assertEqual(to_bopomofo("OVOS 123!"), "OVOS 123!")
51
+
52
+ def test_empty_string(self):
53
+ self.assertEqual(to_bopomofo(""), "")
54
+
55
+
56
+ class TestDependencyBoundary(unittest.TestCase):
57
+ def test_missing_pypinyin_raises_with_install_hint(self):
58
+ with mock.patch.object(readings, "_PINYIN_STYLES", None), \
59
+ mock.patch.dict(sys.modules, {"pypinyin": None}):
60
+ with self.assertRaises(ImportError) as ctx:
61
+ to_pinyin("中国")
62
+ self.assertIn("scriptconv[zh]", str(ctx.exception))
63
+
64
+ def test_style_table_is_cached(self):
65
+ to_pinyin("中国")
66
+ first = readings._PINYIN_STYLES
67
+ to_bopomofo("中国")
68
+ self.assertIs(readings._PINYIN_STYLES, first)
69
+
70
+ def test_chinese_and_japanese_backends_independent(self):
71
+ # pypinyin absence must not break Japanese conversion
72
+ with mock.patch.object(readings, "_PINYIN_STYLES", None), \
73
+ mock.patch.dict(sys.modules, {"pypinyin": None}):
74
+ self.assertEqual(readings.to_hiragana("日本語"), "にほんご")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ unittest.main()
@@ -1,4 +0,0 @@
1
-
2
- [test]
3
- pytest
4
- pytest-timeout
File without changes
File without changes
File without changes