Skip to content

Commit 290e705

Browse files
Copilotbact
andcommitted
Move download/numpy tests from core to compact tier; fix CI failure
Co-authored-by: bact <128572+bact@users.noreply.github.com>
1 parent f3bedd7 commit 290e705

5 files changed

Lines changed: 114 additions & 83 deletions

File tree

tests/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The CI/CD test workflow is at
5353
- Run `unittest tests.core`
5454
- Focus on core functionalities.
5555
- Do not rely on external dependencies beyond the standard library.
56+
- Do not perform network access or corpus downloads.
5657
- Tested on all supported operating systems and all active Python versions.
5758
- Test case class suffix: `TestCase`
5859

@@ -64,6 +65,8 @@ The CI/CD test workflow is at
6465
and small set of dependencies.
6566
- These dependencies are `PyYAML`, `nlpo3`, `numpy`, `pyicu`,
6667
`python-crfsuite`, and `requests`.
68+
- Includes corpus download/remove tests (require network access and `requests`)
69+
and any tests that depend on `numpy` or `python-crfsuite`.
6770
- Tested on:
6871
- All OSes: earliest and second-latest supported Python versions
6972
- Ubuntu: additionally tested on the latest version

tests/compact/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# Names of module to be tested
1212
test_packages: list[str] = [
1313
"tests.compact.testc_classify",
14+
"tests.compact.testc_corpus",
1415
"tests.compact.testc_el",
1516
"tests.compact.testc_parse",
1617
"tests.compact.testc_tokenize",

tests/compact/testc_corpus.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# SPDX-FileCopyrightText: 2016-2026 PyThaiNLP Project
2+
# SPDX-FileType: SOURCE
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
# Tests for corpus download/remove functions.
6+
# These tests require network access and the "compact" dependency set.
7+
8+
import os
9+
import tempfile
10+
import unittest
11+
from unittest.mock import patch
12+
13+
from pythainlp.corpus import (
14+
download,
15+
find_synonyms,
16+
get_corpus_db_detail,
17+
get_corpus_path,
18+
remove,
19+
)
20+
21+
22+
class CorpusDownloadTestCaseC(unittest.TestCase):
23+
def test_download_and_remove(self):
24+
"""Test the full download/remove lifecycle for a test corpus."""
25+
# Invalid-URL / name-not-found cases
26+
self.assertFalse(
27+
download(name="test", url="wrongurl00AAfcX2df")
28+
) # URL not exist
29+
self.assertFalse(
30+
download(name="XxxXXxxx817d37sf")
31+
) # corpus name not exist
32+
33+
# Full lifecycle: download, re-download, query, remove
34+
self.assertTrue(download("test")) # download the first time
35+
self.assertTrue(download(name="test", force=True)) # force download
36+
self.assertTrue(download(name="test")) # try download existing
37+
self.assertIsNotNone(get_corpus_db_detail("test")) # corpus exists
38+
self.assertIsNotNone(get_corpus_path("test")) # corpus exists
39+
self.assertTrue(remove("test")) # remove existing
40+
self.assertFalse(remove("test")) # remove non-existing
41+
42+
# Corpus version not supported in this PyThaiNLP version
43+
# test 0.0.1 is for PyThaiNLP version <2.0
44+
self.assertFalse(download(name="test", version="0.0.1"))
45+
46+
def test_download_ignores_offline_mode(self):
47+
"""download() must work even when PYTHAINLP_OFFLINE=1.
48+
49+
Explicit calls to download() are deliberate user actions and must
50+
not be blocked by the PYTHAINLP_OFFLINE environment variable.
51+
That variable only prevents the *automatic* download triggered by
52+
get_corpus_path() when a corpus is missing locally.
53+
"""
54+
with patch.dict(os.environ, {"PYTHAINLP_OFFLINE": "1"}):
55+
result = download("test")
56+
self.assertTrue(result)
57+
58+
def test_zip(self):
59+
"""Test download and extraction of a zip corpus."""
60+
self.assertTrue(download("test_zip")) # download first
61+
p = get_corpus_path("test_zip")
62+
self.assertTrue(os.path.isdir(p))
63+
self.assertTrue(remove("test_zip"))
64+
65+
def test_find_synonyms(self):
66+
self.assertEqual(
67+
find_synonyms("หมู"), ["จรุก", "วราหะ", "วราห์", "ศูกร", "สุกร"]
68+
)
69+
self.assertEqual(find_synonyms("1"), [])
70+
71+
72+
class ReadOnlyModeExplicitSaveTestCaseC(unittest.TestCase):
73+
"""Test that user-initiated saves are allowed in read-only mode.
74+
75+
Uses numpy and python-crfsuite (both in the compact dependency set).
76+
"""
77+
78+
def test_explicit_save_allowed_in_read_only(self):
79+
"""Read-only mode must not block saves to a user-specified path.
80+
81+
Read-only mode only blocks implicit background writes to PyThaiNLP's
82+
internal data directory. Operations where the user explicitly
83+
specifies an output path must proceed normally.
84+
"""
85+
import numpy as np
86+
87+
from pythainlp.classify.param_free import GzipModel
88+
from pythainlp.tag._tag_perceptron import PerceptronTagger
89+
90+
with patch.dict(
91+
os.environ,
92+
{"PYTHAINLP_READ_ONLY": "1"},
93+
clear=False,
94+
):
95+
os.environ.pop("PYTHAINLP_READ_MODE", None)
96+
with tempfile.TemporaryDirectory() as tmpdir:
97+
# GzipModel.save — user explicitly provides the path
98+
model = object.__new__(GzipModel)
99+
model.training_data = np.array([("text", "label")])
100+
model.cx2_list = [4]
101+
out = os.path.join(tmpdir, "model.json")
102+
model.save(out)
103+
self.assertTrue(os.path.isfile(out))
104+
105+
# PerceptronTagger.train — user explicitly provides save_loc
106+
tagger = PerceptronTagger()
107+
sentences = [[("กิน", "VV"), ("ข้าว", "NN")]]
108+
tagger_out = os.path.join(tmpdir, "tagger.json")
109+
tagger.train(sentences, save_loc=tagger_out, nr_iter=1)
110+
self.assertTrue(os.path.isfile(tagger_out))

tests/core/test_corpus.py

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@
88

99
from pythainlp.corpus import (
1010
countries,
11-
download,
12-
find_synonyms,
1311
get_corpus_db,
1412
get_corpus_db_detail,
1513
get_corpus_default_db,
1614
get_corpus_path,
1715
oscar,
1816
phupha,
1917
provinces,
20-
remove,
2118
thai_family_names,
2219
thai_female_names,
2320
thai_icu_words,
@@ -89,29 +86,8 @@ def test_corpus(self):
8986
get_corpus_db_detail("XXXmx3KSXX", version="0.2"), {}
9087
) # corpus does not exist
9188
self.assertIsNone(get_corpus_path("XXXkdjfBzc")) # query non-existing
92-
self.assertFalse(
93-
download(name="test", url="wrongurl00AAfcX2df")
94-
) # URL not exist
95-
self.assertFalse(
96-
download(name="XxxXXxxx817d37sf")
97-
) # corpus name not exist
9889
# END - Test non-exists
9990

100-
# BEGIN - Test download
101-
self.assertTrue(download("test")) # download the first time
102-
self.assertTrue(download(name="test", force=True)) # force download
103-
self.assertTrue(download(name="test")) # try download existing
104-
self.assertIsNotNone(get_corpus_db_detail("test")) # corpus exists
105-
self.assertIsNotNone(get_corpus_path("test")) # corpus exists
106-
self.assertIsNone(get_corpus_default_db("test"))
107-
self.assertTrue(remove("test")) # remove existing
108-
self.assertFalse(remove("test")) # remove non-existing
109-
# END - Test download
110-
111-
# Corpus version does not support in this PyThaiNLP version
112-
# test 0.0.1 is for PyThaiNLP version <2.0
113-
self.assertFalse(download(name="test", version="0.0.1"))
114-
11591
def test_oscar(self):
11692
# Mock the oscar corpus file to avoid slow download and parsing
11793
# Format: word,count
@@ -301,20 +277,6 @@ def test_get_corpus_path_offline_mode(self):
301277
self.assertIsNotNone(result)
302278
self.assertNotEqual(result, "")
303279

304-
def test_download_ignores_offline_mode(self):
305-
"""download() must work even when PYTHAINLP_OFFLINE=1.
306-
307-
Explicit calls to download() are deliberate user actions and must
308-
not be blocked by the PYTHAINLP_OFFLINE environment variable.
309-
That variable only prevents the *automatic* download triggered by
310-
get_corpus_path() when a corpus is missing locally.
311-
"""
312-
# Use the real "test" corpus so the download actually goes through
313-
with patch.dict(os.environ, {"PYTHAINLP_OFFLINE": "1"}):
314-
result = download("test")
315-
# Should succeed (returns True), not be blocked
316-
self.assertTrue(result)
317-
318280
def test_revise_wordset(self):
319281
training_data = [
320282
["ถวิล อุดล", " ", "เป็น", "นักการเมือง", "หนึ่ง", "ใน"],
@@ -324,15 +286,3 @@ def test_revise_wordset(self):
324286
["ที่", "ถูก", "สังหาร", "เมื่อ", "ปี", " ", "พ.ศ.", " ", "2492"],
325287
]
326288
self.assertIsInstance(revise_newmm_default_wordset(training_data), set)
327-
328-
def test_zip(self):
329-
self.assertTrue(download("test_zip")) # download first
330-
p = get_corpus_path("test_zip")
331-
self.assertTrue(os.path.isdir(p))
332-
self.assertTrue(remove("test_zip"))
333-
334-
def test_find_synonyms(self):
335-
self.assertEqual(
336-
find_synonyms("หมู"), ["จรุก", "วราหะ", "วราห์", "ศูกร", "สุกร"]
337-
)
338-
self.assertEqual(find_synonyms("1"), [])

tests/core/test_tools.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -272,36 +272,3 @@ def test_get_pythainlp_data_path_no_makedirs_in_read_only(self):
272272
"Data directory should not be created in read-only mode",
273273
)
274274

275-
def test_explicit_save_allowed_in_read_only(self):
276-
"""Test that user-explicit saves are allowed in read-only mode.
277-
278-
Read-only mode only blocks implicit background writes to PyThaiNLP's
279-
internal data directory. Operations where the user explicitly
280-
specifies an output path must proceed normally.
281-
"""
282-
from pythainlp.classify.param_free import GzipModel
283-
from pythainlp.tag._tag_perceptron import PerceptronTagger
284-
285-
with patch.dict(
286-
os.environ,
287-
{"PYTHAINLP_READ_ONLY": "1"},
288-
clear=False,
289-
):
290-
os.environ.pop("PYTHAINLP_READ_MODE", None)
291-
with tempfile.TemporaryDirectory() as tmpdir:
292-
# GzipModel.save — user explicitly provides the path
293-
model = object.__new__(GzipModel)
294-
import numpy as np
295-
296-
model.training_data = np.array([("text", "label")])
297-
model.cx2_list = [4]
298-
out = os.path.join(tmpdir, "model.json")
299-
model.save(out)
300-
self.assertTrue(os.path.isfile(out))
301-
302-
# PerceptronTagger.train — user explicitly provides save_loc
303-
tagger = PerceptronTagger()
304-
sentences = [[("กิน", "VV"), ("ข้าว", "NN")]]
305-
tagger_out = os.path.join(tmpdir, "tagger.json")
306-
tagger.train(sentences, save_loc=tagger_out, nr_iter=1)
307-
self.assertTrue(os.path.isfile(tagger_out))

0 commit comments

Comments
 (0)