Skip to content

Commit f0d2f71

Browse files
authored
Merge pull request #1318 from PyThaiNLP/copilot/check-download-read-only-mode
Check and test download() failure in read-only mode; document env var interactions
2 parents a6267c9 + 0b77833 commit f0d2f71

3 files changed

Lines changed: 92 additions & 0 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ internal data directory. This blocks corpus downloads, catalog updates, and
116116
automatic data directory creation — writes that happen as side effects the user
117117
may not be aware of.
118118

119+
> **Note:** Read-only mode is more restrictive than offline mode.
120+
> `PYTHAINLP_OFFLINE=1` blocks only *automatic* downloads triggered by
121+
> `get_corpus_path()`; explicit `pythainlp.corpus.download()` calls still work.
122+
> `PYTHAINLP_READ_ONLY=1` also blocks explicit `download()` calls, because any
123+
> download requires writing to the data directory.
124+
> Use `PYTHAINLP_READ_ONLY` when the data directory is on a read-only file system
125+
> (e.g., a read-only Docker volume or a shared cluster mount).
126+
119127
Operations where the user explicitly specifies an output path are unaffected
120128
(e.g., `model.save("path")`, `tagger.train(..., save_loc="path")`,
121129
`thainlp misspell --output myfile.txt`).

docs/notes/installation.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,60 @@ Runtime configurations
213213

214214
``PYTHAINLP_READ_MODE=1`` is equivalent to ``PYTHAINLP_READ_ONLY=1``.
215215

216+
Interaction between environment variables
217+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
218+
219+
The table below shows how :envvar:`PYTHAINLP_OFFLINE` and :envvar:`PYTHAINLP_READ_ONLY`
220+
affect the two main corpus operations:
221+
222+
.. list-table::
223+
:header-rows: 1
224+
:widths: 35 32 33
225+
226+
* - Operation
227+
- ``PYTHAINLP_OFFLINE=1``
228+
- ``PYTHAINLP_READ_ONLY=1``
229+
* - :func:`pythainlp.corpus.get_corpus_path` — corpus already cached locally
230+
- Succeeds (returns path)
231+
- Succeeds (no write needed)
232+
* - :func:`pythainlp.corpus.get_corpus_path` — corpus **not** cached locally
233+
- Fails (:exc:`FileNotFoundError`)
234+
- Fails (:exc:`FileNotFoundError`)
235+
* - :func:`pythainlp.corpus.download` — corpus already in local catalog
236+
- Succeeds (download is an explicit user action)
237+
- Fails (returns ``False``; all writes are blocked)
238+
* - :func:`pythainlp.corpus.download` — corpus **not** in local catalog
239+
- Succeeds (downloads the corpus)
240+
- Fails (returns ``False``; all writes are blocked)
241+
242+
Key differences:
243+
244+
- :envvar:`PYTHAINLP_OFFLINE` blocks only **automatic** downloads.
245+
Explicit calls to :func:`~pythainlp.corpus.download` (or the
246+
``thainlp data get`` CLI command) still work, because those are
247+
deliberate user actions.
248+
249+
- :envvar:`PYTHAINLP_READ_ONLY` is **more restrictive**: it blocks
250+
*all* writes to the data directory, including explicit
251+
:func:`~pythainlp.corpus.download` calls.
252+
Use this when the data directory is on a read-only file system
253+
(e.g., a read-only Docker volume or a shared cluster mount).
254+
255+
- :envvar:`PYTHAINLP_DATA` sets the path of the data directory used by
256+
both modes. In read-only mode the directory is not created if it
257+
does not already exist.
258+
259+
Typical use cases:
260+
261+
- **Offline laptop / air-gapped system**: set ``PYTHAINLP_OFFLINE=1``
262+
after downloading all required corpora. You can still call
263+
``download()`` manually if you have network access.
264+
265+
- **Read-only container image with pre-bundled corpora**:
266+
set ``PYTHAINLP_READ_ONLY=1`` so that no writes occur at all.
267+
Any attempt to download a corpus that is missing from the image
268+
will return ``False`` instead of raising a permission error.
269+
216270
Installation FAQ
217271
----------------
218272

tests/core/test_corpus.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pythainlp.corpus import (
1010
countries,
11+
download,
1112
get_corpus_db,
1213
get_corpus_db_detail,
1314
get_corpus_default_db,
@@ -277,6 +278,35 @@ def test_get_corpus_path_offline_mode(self):
277278
self.assertIsNotNone(result)
278279
self.assertNotEqual(result, "")
279280

281+
def test_download_read_only_mode(self):
282+
"""Test that download() returns False when read-only mode is active.
283+
284+
When the data directory is read-only (e.g. a read-only mounted volume),
285+
download() must not attempt any network or file-system operations and
286+
must return False immediately. This is stricter than offline mode:
287+
PYTHAINLP_OFFLINE only prevents *automatic* downloads; PYTHAINLP_READ_ONLY
288+
also prevents explicit download() calls.
289+
"""
290+
# Corpus not in cache — must fail in read-only mode.
291+
with patch(
292+
"pythainlp.corpus.core.is_read_only_mode", return_value=True
293+
):
294+
result = download("not_a_real_corpus_xyz123")
295+
self.assertFalse(result)
296+
297+
# Sanity-check: with read-only mode off, the call is at least
298+
# attempted (returns False because the corpus does not exist remotely,
299+
# not because of read-only mode).
300+
with patch(
301+
"pythainlp.corpus.core.is_read_only_mode", return_value=False
302+
):
303+
with patch(
304+
"pythainlp.corpus.core.get_corpus_db",
305+
return_value=None,
306+
):
307+
result = download("not_a_real_corpus_xyz123")
308+
self.assertFalse(result)
309+
280310
def test_revise_wordset(self):
281311
training_data = [
282312
["ถวิล อุดล", " ", "เป็น", "นักการเมือง", "หนึ่ง", "ใน"],

0 commit comments

Comments
 (0)