Skip to content

Commit 5ce7793

Browse files
authored
Merge pull request #1317 from PyThaiNLP/copilot/fix-db-json-creation-issue
Lazy create db.json; Introduce PYTHAINLP_READ_ONLY with scoped write protection
2 parents 0b9e4ae + 0f24d7d commit 5ce7793

15 files changed

Lines changed: 479 additions & 87 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ The minimum requirement is now Python 3.9.
3535
(`dependency_parsing`, engine="attaparse") (#1303)
3636
- `pythainlp.is_offline_mode()` helper function (#1306)
3737
- Thai consonant cluster detection (`check_khuap_klam`) (#1308)
38+
- `pythainlp.is_read_only_mode()` helper function;
39+
use `PYTHAINLP_READ_ONLY=1` to prevent all write operations
3840

3941
### Changed
4042

@@ -59,6 +61,8 @@ The minimum requirement is now Python 3.9.
5961
- `PYTHAINLP_DATA_DIR` env var; use `PYTHAINLP_DATA` instead
6062
(follows `NLTK_DATA` convention from NLTK)
6163
`PYTHAINLP_DATA_DIR` will be removed in future version (#1306)
64+
- `PYTHAINLP_READ_MODE` env var; use `PYTHAINLP_READ_ONLY` instead.
65+
Setting both at the same time raises `ValueError`.
6266

6367
### Removed
6468

@@ -75,6 +79,8 @@ The minimum requirement is now Python 3.9.
7579
- Kho Khon alphabet issue in `tltk` transliteration (#1187)
7680
- Suppress Gensim duplicate-word warnings when loading word2vec
7781
binary files (#1316)
82+
- `db.json` is no longer created on import; it is created lazily only
83+
when a corpus is first downloaded (#1317)
7884

7985
## [5.2.0] - 2025-12-20
8086

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,47 @@ inside the function that will be distributed to worker nodes.
8686
See details in
8787
[the documentation](https://pythainlp.org/dev-docs/notes/installation.html).
8888

89+
## Environment variables
90+
91+
| Variable | Description | Status |
92+
|---|---|---|
93+
| `PYTHAINLP_DATA` | Path to the data directory (default: `~/pythainlp-data`). | Current |
94+
| `PYTHAINLP_DATA_DIR` | Legacy alias for `PYTHAINLP_DATA`. Emits a `DeprecationWarning`. Setting both raises `ValueError`. | Deprecated; use `PYTHAINLP_DATA` |
95+
| `PYTHAINLP_OFFLINE` | Set to `1` to disable automatic corpus downloads. Explicit `download()` calls still work. | Current |
96+
| `PYTHAINLP_READ_ONLY` | Set to `1` to enable read-only mode, which prevents implicit background writes to PyThaiNLP's internal data directory (corpus downloads, catalog updates, directory creation). Explicit user-initiated saves to user-specified paths are unaffected. | Current |
97+
| `PYTHAINLP_READ_MODE` | Legacy alias for `PYTHAINLP_READ_ONLY`. Emits a `DeprecationWarning`. Setting both raises `ValueError`. | Deprecated; use `PYTHAINLP_READ_ONLY` |
98+
8999
### Offline mode
90100

91-
Set `PYTHAINLP_OFFLINE=1` to disable automatic corpus downloads.
101+
Set `PYTHAINLP_OFFLINE=1` to disable **automatic** corpus downloads.
92102
When this variable is set and a corpus is not already cached locally,
93103
a `FileNotFoundError` is raised instead of attempting a network download.
104+
Explicit calls to `pythainlp.corpus.download()` are unaffected.
94105
Use `pythainlp.is_offline_mode()` to check the current state programmatically.
95106

96107
```python
97108
import pythainlp
98109
print(pythainlp.is_offline_mode()) # True if PYTHAINLP_OFFLINE=1
99110
```
100111

112+
### Read-only mode
113+
114+
Set `PYTHAINLP_READ_ONLY=1` to prevent implicit background writes to PyThaiNLP's
115+
internal data directory. This blocks corpus downloads, catalog updates, and
116+
automatic data directory creation — writes that happen as side effects the user
117+
may not be aware of.
118+
119+
Operations where the user explicitly specifies an output path are unaffected
120+
(e.g., `model.save("path")`, `tagger.train(..., save_loc="path")`,
121+
`thainlp misspell --output myfile.txt`).
122+
123+
Use `pythainlp.is_read_only_mode()` to check the current state programmatically.
124+
125+
```python
126+
import pythainlp
127+
print(pythainlp.is_read_only_mode()) # True if PYTHAINLP_READ_ONLY=1
128+
```
129+
101130
## Testing
102131

103132
We test core functionalities on all officially supported Python versions.

README_TH.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,46 @@ PyThaiNLP ดาวน์โหลดข้อมูล (ดูแค็ตต
117117
ภายในฟังก์ชันที่จะถูกกระจายไปยัง worker nodes
118118
ดูรายละเอียดใน[เอกสาร](https://pythainlp.org/dev-docs/notes/installation.html)
119119

120+
## Environment variables
121+
122+
| ตัวแปร | คำอธิบาย | สถานะ |
123+
|---|---|---|
124+
| `PYTHAINLP_DATA` | พาธของไดเรกทอรีข้อมูล (ค่าเริ่มต้น: `~/pythainlp-data`) | ปัจจุบัน |
125+
| `PYTHAINLP_DATA_DIR` | ชื่อเดิมของ `PYTHAINLP_DATA` แสดง `DeprecationWarning` และหากตั้งค่าทั้งสองพร้อมกันจะเกิด `ValueError` | เลิกใช้แล้ว; ใช้ `PYTHAINLP_DATA` แทน |
126+
| `PYTHAINLP_OFFLINE` | ตั้งเป็น `1` เพื่อปิดการดาวน์โหลด corpus อัตโนมัติ การเรียก `download()` โดยตรงยังคงใช้งานได้ | ปัจจุบัน |
127+
| `PYTHAINLP_READ_ONLY` | ตั้งเป็น `1` เพื่อเปิดโหมดอ่านอย่างเดียว ป้องกันการเขียนพื้นหลังที่ผู้ใช้อาจไม่ทราบ (ดาวน์โหลด corpus, อัปเดตแค็ตตาล็อก, สร้างไดเรกทอรี) การบันทึกไฟล์ที่ผู้ใช้ระบุเองไม่ได้รับผลกระทบ | ปัจจุบัน |
128+
| `PYTHAINLP_READ_MODE` | ชื่อเดิมของ `PYTHAINLP_READ_ONLY` แสดง `DeprecationWarning` และหากตั้งค่าทั้งสองพร้อมกันจะเกิด `ValueError` | เลิกใช้แล้ว; ใช้ `PYTHAINLP_READ_ONLY` แทน |
129+
130+
### โหมดออฟไลน์
131+
132+
ตั้งค่า `PYTHAINLP_OFFLINE=1` เพื่อปิดการดาวน์โหลด corpus **อัตโนมัติ**
133+
เมื่อตั้งค่าตัวแปรนี้และ corpus ไม่มีในเครื่อง จะเกิด `FileNotFoundError`
134+
แทนที่จะพยายามดาวน์โหลดจากเครือข่าย
135+
การเรียก `pythainlp.corpus.download()` โดยตรงยังคงทำงานได้ตามปกติ
136+
ใช้ `pythainlp.is_offline_mode()` เพื่อตรวจสอบสถานะปัจจุบันในโค้ด
137+
138+
```python
139+
import pythainlp
140+
print(pythainlp.is_offline_mode()) # True ถ้า PYTHAINLP_OFFLINE=1
141+
```
142+
143+
### โหมดอ่านอย่างเดียว
144+
145+
ตั้งค่า `PYTHAINLP_READ_ONLY=1` เพื่อป้องกันการเขียนพื้นหลังที่เกิดขึ้นโดยอัตโนมัติ
146+
ในไดเรกทอรีข้อมูลภายในของ PyThaiNLP ได้แก่ การดาวน์โหลด corpus, การอัปเดตแค็ตตาล็อก
147+
และการสร้างไดเรกทอรีข้อมูล — การเขียนเหล่านี้เกิดขึ้นเป็นผลข้างเคียงที่ผู้ใช้อาจไม่ทราบ
148+
149+
การดำเนินการที่ผู้ใช้ระบุ path เอาไว้อย่างชัดเจนจะไม่ได้รับผลกระทบ
150+
เช่น `model.save("path")`, `tagger.train(..., save_loc="path")`,
151+
`thainlp misspell --output myfile.txt`
152+
153+
ใช้ `pythainlp.is_read_only_mode()` เพื่อตรวจสอบสถานะปัจจุบันในโค้ด
154+
155+
```python
156+
import pythainlp
157+
print(pythainlp.is_read_only_mode()) # True ถ้า PYTHAINLP_READ_ONLY=1
158+
```
159+
120160
## การทดสอบ
121161

122162
เราทดสอบฟังก์ชันหลักบน Python ทุกเวอร์ชันที่รองรับอย่างเป็นทางการ

docs/notes/installation.rst

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,58 @@ Runtime configurations
160160
.. envvar:: PYTHAINLP_OFFLINE
161161

162162
When set to a truthy value (``1``, ``true``, ``yes``, ``on``), PyThaiNLP operates in
163-
*offline mode*: corpus downloads are disabled, and :func:`pythainlp.corpus.get_corpus_path`
164-
raises :exc:`FileNotFoundError` for any corpus that is not already cached locally.
163+
*offline mode*: automatic corpus downloads are disabled, and
164+
:func:`pythainlp.corpus.get_corpus_path` raises :exc:`FileNotFoundError`
165+
for any corpus that is not already cached locally.
166+
167+
Explicit calls to :func:`pythainlp.corpus.download` or ``thainlp data get``
168+
still work normally, because those are deliberate user actions.
165169

166170
Use :func:`pythainlp.is_offline_mode` to check the current state programmatically.
167171

168172
This follows the same convention as ``HF_HUB_OFFLINE`` in `huggingface_hub`.
169173

174+
.. envvar:: PYTHAINLP_READ_ONLY
175+
176+
When set to a truthy value (``1``, ``true``, ``yes``, ``on``), PyThaiNLP operates in
177+
*read-only mode*: implicit background writes to PyThaiNLP's internal data directory
178+
are disabled.
179+
180+
**What read-only mode blocks** (implicit writes the user may not be aware of):
181+
182+
- Creating the PyThaiNLP data directory (``~/pythainlp-data`` or
183+
as configured by :envvar:`PYTHAINLP_DATA`).
184+
- :func:`pythainlp.corpus.download` — corpus file downloads and catalog
185+
(``db.json``) updates.
186+
- :func:`pythainlp.corpus.remove` — corpus file and catalog deletions.
187+
188+
**What read-only mode does NOT block** (explicit user-initiated writes):
189+
190+
- Saving trained models or vocabularies to a user-specified path
191+
(e.g., ``model.save("my_model.json")``, ``tagger.train(..., save_loc="...")``,
192+
``tokenizer.save_vocabulary("my_dir/")``) — the user explicitly provided the
193+
destination path.
194+
- CLI output files written to a user-specified location
195+
(e.g., ``thainlp benchmark --save-details``,
196+
``thainlp misspell --output myfile.txt``).
197+
198+
Use :func:`pythainlp.is_read_only_mode` to check the current state programmatically.
199+
200+
.. note::
201+
To disable only *automatic* background downloads while keeping explicit
202+
``download()`` calls working, use :envvar:`PYTHAINLP_OFFLINE` instead.
203+
204+
If both :envvar:`PYTHAINLP_READ_ONLY` and :envvar:`PYTHAINLP_READ_MODE` are set at the
205+
same time, PyThaiNLP raises :exc:`ValueError`.
206+
170207
.. envvar:: PYTHAINLP_READ_MODE
171208

172-
Configures PyThaiNLP to operate in read-only mode (0 = False, 1 = True).
209+
.. deprecated::
210+
Use :envvar:`PYTHAINLP_READ_ONLY` instead. Setting ``PYTHAINLP_READ_MODE`` triggers a
211+
:class:`DeprecationWarning` at runtime. If both ``PYTHAINLP_READ_ONLY`` and
212+
``PYTHAINLP_READ_MODE`` are set simultaneously, PyThaiNLP raises :exc:`ValueError`.
213+
214+
``PYTHAINLP_READ_MODE=1`` is equivalent to ``PYTHAINLP_READ_ONLY=1``.
173215

174216
Installation FAQ
175217
----------------
@@ -194,4 +236,5 @@ For more discussion, see `PermissionError: [Errno 13] Permission denied: /home/p
194236

195237
Q: How do I enable read-only mode for PyThaiNLP?
196238

197-
A: Set the environment variable ``PYTHAINLP_READ_MODE`` to ``1``.
239+
A: Set the environment variable ``PYTHAINLP_READ_ONLY`` to ``1``.
240+
The legacy ``PYTHAINLP_READ_MODE=1`` is still accepted but deprecated.

pythainlp/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"collate",
5757
"correct",
5858
"is_offline_mode",
59+
"is_read_only_mode",
5960
"pos_tag",
6061
"romanize",
6162
"spell",
@@ -77,6 +78,6 @@
7778
subword_tokenize,
7879
word_tokenize,
7980
)
80-
from pythainlp.tools.path import is_offline_mode
81+
from pythainlp.tools.path import is_offline_mode, is_read_only_mode
8182
from pythainlp.transliterate import romanize, transliterate
8283
from pythainlp.util import collate, thai_strftime

pythainlp/corpus/__init__.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,13 @@
4747
]
4848

4949
import os
50-
from typing import Optional
5150

5251
from pythainlp.tools import get_full_data_path, get_pythainlp_path
5352

5453
# Remote and local corpus databases
5554

5655
_CORPUS_DIRNAME: str = "corpus"
5756
_CORPUS_PATH: str = os.path.join(get_pythainlp_path(), _CORPUS_DIRNAME)
58-
_CHECK_MODE: Optional[str] = os.getenv("PYTHAINLP_READ_MODE")
59-
60-
# URL of remote corpus catalog
6157
_CORPUS_DB_URL: str = "https://pythainlp.org/pythainlp-corpus/db.json"
6258

6359
# filename of local corpus catalog
@@ -66,11 +62,6 @@
6662
# full path of local corpus catalog
6763
_CORPUS_DB_PATH: str = get_full_data_path(_CORPUS_DB_FILENAME)
6864

69-
# create a local corpus database if it does not already exist
70-
if not os.path.exists(_CORPUS_DB_PATH) and _CHECK_MODE != "1":
71-
with open(_CORPUS_DB_PATH, "w", encoding="utf-8") as f:
72-
f.write(r'{"_default": {}}')
73-
7465

7566
def corpus_path() -> str:
7667
"""Get path where corpus files are kept locally."""

pythainlp/corpus/core.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
from pythainlp import __version__
1818
from pythainlp.corpus import corpus_db_path, corpus_db_url, corpus_path
1919
from pythainlp.tools import get_full_data_path
20-
from pythainlp.tools.path import is_offline_mode
20+
from pythainlp.tools.path import is_offline_mode, is_read_only_mode
2121

2222
if TYPE_CHECKING:
2323
from http.client import HTTPMessage, HTTPResponse
2424
from typing import Any, Optional
2525

26-
_CHECK_MODE: Optional[str] = os.getenv("PYTHAINLP_READ_MODE")
2726
_USER_AGENT: str = (
2827
f"PyThaiNLP/{__version__} "
2928
f"(Python/{sys.version_info.major}.{sys.version_info.minor}; "
@@ -85,7 +84,10 @@ def get_corpus_db_detail(name: str, version: str = "") -> dict[str, Any]:
8584
:return: details about corpus
8685
:rtype: dict
8786
"""
88-
with open(corpus_db_path(), encoding="utf-8-sig") as f:
87+
db_path = corpus_db_path()
88+
if not os.path.exists(db_path):
89+
return {}
90+
with open(db_path, encoding="utf-8-sig") as f:
8991
local_db = json.load(f)
9092

9193
if not version:
@@ -658,8 +660,8 @@ def download(
658660
``$HOME/pythainlp-data/``
659661
(e.g. ``/Users/bact/pythainlp-data/wiki_lm_lstm.pth``).
660662
"""
661-
if _CHECK_MODE == "1":
662-
print("PyThaiNLP is read-only mode. It can't download.")
663+
if is_read_only_mode():
664+
print("PyThaiNLP is in read-only mode. It cannot download.")
663665
return False
664666

665667
if not url:
@@ -674,8 +676,12 @@ def download(
674676

675677
# check if corpus is available
676678
if name in corpus_db_dict:
677-
with open(corpus_db_path(), encoding="utf-8-sig") as f:
678-
local_db = json.load(f)
679+
db_path = corpus_db_path()
680+
if os.path.exists(db_path):
681+
with open(db_path, encoding="utf-8-sig") as f:
682+
local_db = json.load(f)
683+
else:
684+
local_db = {"_default": {}}
679685

680686
corpus = corpus_db_dict[name]
681687
print("Corpus:", name)
@@ -802,10 +808,13 @@ def remove(name: str) -> bool:
802808
# FileNotFoundError: [Errno 2] No such file or directory:
803809
# '/usr/local/lib/python3.6/dist-packages/pythainlp/corpus/ttc'
804810
"""
805-
if _CHECK_MODE == "1":
806-
print("PyThaiNLP is read-only mode. It can't download.")
811+
if is_read_only_mode():
812+
print("PyThaiNLP is in read-only mode. It cannot remove corpus.")
813+
return False
814+
db_path = corpus_db_path()
815+
if not os.path.exists(db_path):
807816
return False
808-
with open(corpus_db_path(), encoding="utf-8-sig") as f:
817+
with open(db_path, encoding="utf-8-sig") as f:
809818
db = json.load(f)
810819
data = [
811820
corpus for corpus in db["_default"].values() if corpus["name"] == name

pythainlp/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"get_pythainlp_data_path",
88
"get_pythainlp_path",
99
"is_offline_mode",
10+
"is_read_only_mode",
1011
"safe_print",
1112
"warn_deprecation",
1213
]
@@ -18,4 +19,5 @@
1819
get_pythainlp_data_path,
1920
get_pythainlp_path,
2021
is_offline_mode,
22+
is_read_only_mode,
2123
)

0 commit comments

Comments
 (0)