Skip to content

Commit 937ef54

Browse files
Copilotbact
andcommitted
Guard all PyThaiNLP write operations with is_read_only_mode()
Co-authored-by: bact <128572+bact@users.noreply.github.com>
1 parent 5f58b5a commit 937ef54

7 files changed

Lines changed: 99 additions & 1 deletion

File tree

pythainlp/classify/param_free.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ def predict(self, x1: str, k: int = 1) -> str:
9696

9797
def save(self, path: str) -> None:
9898
""":param str path: path to save model"""
99+
from pythainlp.tools.path import is_read_only_mode
100+
101+
if is_read_only_mode():
102+
raise PermissionError(
103+
"PyThaiNLP is in read-only mode. Cannot save model."
104+
)
99105
with open(path, "w", encoding="utf-8") as f:
100106
json.dump(
101107
{

pythainlp/cli/benchmark.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ def __init__(self, name: str, argv: Sequence[str]) -> None:
157157
dir_name = os.path.dirname(args.input_file)
158158
file_name = args.input_file.split("/")[-1].split(".")[0]
159159

160+
from pythainlp.tools.path import is_read_only_mode
161+
162+
if is_read_only_mode():
163+
safe_print(
164+
"PyThaiNLP is in read-only mode. "
165+
"Benchmark details cannot be saved."
166+
)
167+
return
168+
160169
res_path = "%s/eval-%s.yml" % (dir_name, file_name)
161170
safe_print("Evaluation result is saved to %s" % res_path)
162171

pythainlp/cli/misspell.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,14 @@ def __init__(self, argv: Sequence[str]) -> None:
7070
base, ext = os.path.splitext(args.file)
7171
args.output = f"{base}-misspelled-r{args.misspell_ratio}-seed{args.seed}{ext}"
7272

73+
from pythainlp.tools.path import is_read_only_mode
74+
75+
if is_read_only_mode():
76+
print(
77+
"PyThaiNLP is in read-only mode. "
78+
f"Cannot write output to {args.output!r}."
79+
)
80+
return
81+
7382
with open(args.output, "w", encoding="utf-8") as f:
7483
f.writelines(misspelled_lines)

pythainlp/tag/_tag_perceptron.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,12 @@ def train(
202202

203203
# save the model
204204
if save_loc is not None:
205+
from pythainlp.tools.path import is_read_only_mode
206+
207+
if is_read_only_mode():
208+
raise PermissionError(
209+
"PyThaiNLP is in read-only mode. Cannot save model."
210+
)
205211
data: dict[str, Union[dict, list]] = {}
206212
data["weights"] = self.model.weights
207213
data["tagdict"] = self.tagdict

pythainlp/tools/path.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ def get_pythainlp_data_path() -> str:
202202

203203
resolved = data_dir or os.path.join("~", PYTHAINLP_DEFAULT_DATA_DIR)
204204
path = os.path.expanduser(resolved)
205-
os.makedirs(path, exist_ok=True)
205+
if not is_read_only_mode():
206+
os.makedirs(path, exist_ok=True)
206207
return path
207208

208209

pythainlp/translate/tokenization_small100.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,12 @@ def __setstate__(self, d: dict) -> None:
375375
def save_vocabulary(
376376
self, save_directory: str, filename_prefix: Optional[str] = None
377377
) -> tuple[str, str]:
378+
from pythainlp.tools.path import is_read_only_mode
379+
380+
if is_read_only_mode():
381+
raise PermissionError(
382+
"PyThaiNLP is in read-only mode. Cannot save vocabulary."
383+
)
378384
save_dir = Path(save_directory)
379385
if not save_dir.is_dir():
380386
raise OSError(f"{save_directory} should be a directory")
@@ -468,5 +474,11 @@ def load_json(path: str) -> Union[dict[str, str], list[str]]:
468474
def save_json(
469475
data: Union[Mapping[str, Union[str, int]], list[str]], path: str
470476
) -> None:
477+
from pythainlp.tools.path import is_read_only_mode
478+
479+
if is_read_only_mode():
480+
raise PermissionError(
481+
"PyThaiNLP is in read-only mode. Cannot save file."
482+
)
471483
with open(path, "w") as f:
472484
json.dump(data, f, indent=2)

tests/core/test_tools.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,58 @@ def test_is_read_only_mode_conflict(self):
248248
is_read_only_mode()
249249
self.assertIn("PYTHAINLP_READ_ONLY", str(ctx.exception))
250250
self.assertIn("PYTHAINLP_READ_MODE", str(ctx.exception))
251+
252+
def test_get_pythainlp_data_path_no_makedirs_in_read_only(self):
253+
"""Test that get_pythainlp_data_path skips makedirs in read-only mode."""
254+
with tempfile.TemporaryDirectory() as tmpdir:
255+
new_dir = os.path.join(tmpdir, "new-pythainlp-data")
256+
with patch.dict(
257+
os.environ,
258+
{"PYTHAINLP_DATA": new_dir, "PYTHAINLP_READ_ONLY": "1"},
259+
clear=False,
260+
):
261+
os.environ.pop("PYTHAINLP_DATA_DIR", None)
262+
os.environ.pop("PYTHAINLP_READ_MODE", None)
263+
path = get_pythainlp_data_path()
264+
self.assertEqual(path, new_dir)
265+
# Directory must NOT be created in read-only mode
266+
self.assertFalse(
267+
os.path.exists(new_dir),
268+
"Data directory should not be created in read-only mode",
269+
)
270+
271+
def test_param_free_save_blocked_in_read_only(self):
272+
"""Test that GzipModel.save raises PermissionError in read-only mode."""
273+
from pythainlp.classify.param_free import GzipModel
274+
275+
# Bypass __init__ (which needs numpy) — only the guard is under test
276+
model = object.__new__(GzipModel)
277+
with patch.dict(
278+
os.environ,
279+
{"PYTHAINLP_READ_ONLY": "1"},
280+
clear=False,
281+
):
282+
os.environ.pop("PYTHAINLP_READ_MODE", None)
283+
with tempfile.TemporaryDirectory() as tmpdir:
284+
with self.assertRaises(PermissionError):
285+
model.save(os.path.join(tmpdir, "model.json"))
286+
287+
def test_perceptron_train_save_blocked_in_read_only(self):
288+
"""Test that PerceptronTagger.train raises PermissionError when saving in read-only mode."""
289+
from pythainlp.tag._tag_perceptron import PerceptronTagger
290+
291+
tagger = PerceptronTagger()
292+
sentences = [[("กิน", "VV"), ("ข้าว", "NN")]]
293+
with patch.dict(
294+
os.environ,
295+
{"PYTHAINLP_READ_ONLY": "1"},
296+
clear=False,
297+
):
298+
os.environ.pop("PYTHAINLP_READ_MODE", None)
299+
with tempfile.TemporaryDirectory() as tmpdir:
300+
with self.assertRaises(PermissionError):
301+
tagger.train(
302+
sentences,
303+
save_loc=os.path.join(tmpdir, "tagger.json"),
304+
nr_iter=1,
305+
)

0 commit comments

Comments
 (0)