Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,138 @@ Please refer to our
[empty-line]: https://stackoverflow.com/questions/5813311/no-newline-at-end-of-file#5813359
[posix]: https://stackoverflow.com/questions/729692/why-should-text-files-end-with-a-newline

### Error messages, warnings, and exception handling

Clear, consistent, and parseable error and warning messages help users
debug problems and allow tooling to parse and filter output.
Follow these conventions in all PyThaiNLP code.

#### Exception types

Use the most specific built-in exception type for each situation:

| Situation | Exception type |
| --- | --- |
| Missing optional dependency at import time | `ImportError` or `ModuleNotFoundError` |
| Invalid argument value | `ValueError` |
| Wrong argument type | `TypeError` |
| Required file not found | `FileNotFoundError` |
| I/O or OS-level failure | `OSError` |
| Runtime failure with no more-specific type | `RuntimeError` |
| Feature not yet implemented | `NotImplementedError` |

#### Exception message format

Write messages as complete sentences.

- End each sentence with a period.
- Identify the offending value, name, or path explicitly.
- For a missing optional dependency, include the `pip install` command:

```text
<Package> is not installed. Install it with: pip install <package>
```

If the package is a PyThaiNLP optional-dependency group, name the extra:

```text
<Package> is required for this feature.
Install it with: pip install pythainlp[<extra>]
```

- For an invalid argument value, name the parameter and show the received value:

```text
<param> must be <description>; got {value!r}
```

#### Exception forwarding (chaining)

Always chain exceptions with `from` when raising a new exception inside an
`except` block, so the full traceback and original cause are preserved:

```python
# Correct: chain to the original exception.
try:
import some_package
except ImportError as e:
raise ImportError(
"some_package is not installed. Install it with: pip install some_package"
) from e

# Correct: suppress chain with `from None` only when the original
# exception is irrelevant noise that would confuse the user.
raise ValueError("Invalid configuration value.") from None
```

Never use `raise e` to re-raise a caught exception. Use bare `raise` instead,
which preserves the original traceback:

```python
# Correct: bare re-raise preserves traceback.
except Exception:
log_or_print_something()
raise

# Incorrect: `raise e` creates a new traceback starting at this line.
except Exception as e:
log_or_print_something()
raise e # do not do this
```

When re-raising as a *different* exception type, always supply `from`:

```python
# Correct.
except OSError as e:
raise RuntimeError(f"Failed to read model file: {e}") from e

# Incorrect: original cause is silently discarded.
except OSError as e:
raise RuntimeError(f"Failed to read model file: {e}")
```

#### Warnings

Use `warnings.warn()` for non-fatal conditions that the caller should know
about. Always pass both `category` and `stacklevel` explicitly:

```python
import warnings

warnings.warn("Message.", UserWarning, stacklevel=2)
```

Recommended categories:

| Situation | Category |
| --- | --- |
| Deprecated API that will be removed in a future version | `DeprecationWarning` |
| Skipped or degraded behavior the caller should review | `UserWarning` |

Use `stacklevel=2` so the warning points at the **caller's** line, not the
internal line that called `warnings.warn()`. Increase `stacklevel` by one for
each additional layer of indirection between the public API and the
`warnings.warn()` call.

Warning messages should be clear, concise, and parseable:

- Write messages as complete sentences ending with a period.
- For deprecations, name the deprecated symbol and its replacement:

```text
<old_symbol> is deprecated; use <new_symbol> instead.
```

- For skipped data or fallback behavior, describe what was skipped and why:

```text
Skipping <item> entry with <reason>: {value!r}
```

Do **not** rely on the default `UserWarning` by omitting `category`.
Always supply `category` explicitly for clarity and greppability.

### Version Control System

- We use [Git](https://git-scm.com/) as our
Expand Down
7 changes: 4 additions & 3 deletions pythainlp/cli/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ def __init__(self, name: str, argv: Sequence[str]) -> None:
import yaml

from pythainlp.benchmarks import word_tokenization
except ImportError:
except ImportError as e:
raise ImportError(
"Please install the extra dependencies `benchmarks` to use this command by running `pip install pythainlp[benchmarks]`"
)
"The 'benchmarks' extra dependencies are required for this command."
" Install them with: pip install pythainlp[benchmarks]"
) from e

df_raw = word_tokenization.benchmark(expected, actual)

Expand Down
12 changes: 5 additions & 7 deletions pythainlp/corpus/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def _safe_extract_tar(tar: tarfile.TarFile, path: str) -> None:
tarfile.LinkOutsideDestinationError,
) as e:
# Re-raise as ValueError for consistency with older Python versions
raise ValueError(str(e))
raise ValueError(str(e)) from e
else:
# Manual validation for older Python versions
for member in tar.getmembers():
Expand Down Expand Up @@ -900,13 +900,11 @@ def get_hf_hub(repo_id: str, filename: str = "") -> str:
"""
try:
from huggingface_hub import hf_hub_download, snapshot_download
except ModuleNotFoundError:
except ModuleNotFoundError as e:
raise ModuleNotFoundError(
"""
huggingface-hub isn't found!
Please installing the package via 'pip install huggingface-hub'.
"""
)
"huggingface-hub is not installed."
" Install it with: pip install huggingface-hub"
) from e
except Exception as e:
raise RuntimeError(f"An unexpected error occurred: {e}") from e
hf_root = get_full_data_path("hf_models")
Expand Down
6 changes: 3 additions & 3 deletions pythainlp/parse/attaparse_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

try:
from attaparse import depparse, load_model
except ImportError:
except ImportError as e:
raise ImportError(
"Import Error; Install attaparse by pip install attaparse"
)
"attaparse is not installed. Install it with: pip install attaparse"
) from e

if TYPE_CHECKING:
from stanza import Pipeline
Expand Down
6 changes: 4 additions & 2 deletions pythainlp/parse/esupar_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

try:
import esupar
except ImportError:
raise ImportError("Import Error; Install esupar by pip install esupar")
except ImportError as e:
raise ImportError(
"esupar is not installed. Install it with: pip install esupar"
) from e

if TYPE_CHECKING:
from esupar import Model
Expand Down
4 changes: 2 additions & 2 deletions pythainlp/phayathaibert/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,8 @@ def get_ner(

if pos:
warnings.warn(
"This model doesn't support output \
postag and It doesn't output the postag.",
"This model does not support POS tag output.",
UserWarning,
stacklevel=2,
)

Expand Down
6 changes: 3 additions & 3 deletions pythainlp/spell/phunspell.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@

try:
import phunspell
except ImportError:
except ImportError as e:
raise ImportError(
"Import Error; Install phunspell by pip install phunspell"
)
"phunspell is not installed. Install it with: pip install phunspell"
) from e

pspell: "phunspell.Phunspell" = phunspell.Phunspell("th_TH")

Expand Down
6 changes: 3 additions & 3 deletions pythainlp/spell/symspellpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@

try:
from symspellpy import SymSpell, Verbosity
except ImportError:
except ImportError as e:
raise ImportError(
"Import Error; Install symspellpy by pip install symspellpy"
)
"symspellpy is not installed. Install it with: pip install symspellpy"
) from e

from pythainlp.corpus import get_corpus_path

Expand Down
6 changes: 3 additions & 3 deletions pythainlp/spell/tltk.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

try:
from tltk.nlp import spell_candidates
except ImportError:
except ImportError as e:
raise ImportError(
"Not found tltk! Please install tltk by pip install tltk"
)
"tltk is not installed. Install it with: pip install tltk"
) from e


def spell(text: str) -> list[str]:
Expand Down
6 changes: 3 additions & 3 deletions pythainlp/tag/pos_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,10 @@ def pos_tag_transformers(
AutoTokenizer,
TokenClassificationPipeline,
)
except ImportError:
except ImportError as e:
raise ImportError(
"Not found transformers! Please install transformers by pip install transformers"
)
"transformers is not installed. Install it with: pip install transformers"
) from e

if not sentence:
return []
Expand Down
4 changes: 2 additions & 2 deletions pythainlp/tag/thai_nner.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ def __init__(self, path_model: Optional[str] = None) -> None:
# 3. Clear error message only when ThaiNNER class is actually instantiated
try:
from thai_nner import NNER
except ImportError:
except ImportError as e:
raise ImportError(
"thai-nner library not found. Please install it with 'pip install thai-nner'."
)
) from e
self.model: NNER = NNER(path_model=path_model)

def tag(
Expand Down
6 changes: 3 additions & 3 deletions pythainlp/tag/tltk.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

try:
from tltk import nlp
except ImportError:
except ImportError as e:
raise ImportError(
"Not found tltk! Please install tltk by pip install tltk"
)
"tltk is not installed. Install it with: pip install tltk"
) from e
from pythainlp.tokenize import word_tokenize

nlp.pos_load()
Expand Down
6 changes: 4 additions & 2 deletions pythainlp/tokenize/deepcut.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

try:
from deepcut import tokenize
except ImportError:
raise ImportError("Please install deepcut by pip install deepcut")
except ImportError as e:
raise ImportError(
"deepcut is not installed. Install it with: pip install deepcut"
) from e
from pythainlp.util import Trie


Expand Down
6 changes: 3 additions & 3 deletions pythainlp/tokenize/tltk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
try:
from tltk.nlp import syl_segment
from tltk.nlp import word_segment as tltk_segment
except ImportError:
except ImportError as e:
raise ImportError(
"Not found tltk! Please install tltk by pip install tltk"
)
"tltk is not installed. Install it with: pip install tltk"
) from e


def segment(text: str) -> list[str]:
Expand Down
4 changes: 2 additions & 2 deletions pythainlp/tools/misspell.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ def find_misspell_candidates(
printing_locations[ix] = char
except IndexError:
continue
except Exception as e:
except Exception:
print("Something wrong with: ", char)
raise e
raise

return chars

Expand Down
12 changes: 6 additions & 6 deletions pythainlp/translate/en_th.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@

try:
from fairseq.models.transformer import TransformerModel
except ImportError:
except ImportError as e:
raise ImportError(
"Not found fairseq! Please install fairseq by pip install fairseq"
)
"fairseq is not installed. Install it with: pip install fairseq"
) from e

try:
from sacremoses import MosesTokenizer
except ImportError:
except ImportError as e:
raise ImportError(
"Not found sacremoses! Please install sacremoses by pip install sacremoses"
)
"sacremoses is not installed. Install it with: pip install sacremoses"
) from e

from pythainlp.corpus import download, get_corpus_path

Expand Down
6 changes: 3 additions & 3 deletions pythainlp/transliterate/tltk.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

try:
from tltk.nlp import g2p, th2ipa, th2roman
except ImportError:
except ImportError as e:
raise ImportError(
"Not found tltk! Please install tltk by pip install tltk"
)
"tltk is not installed. Install it with: pip install tltk"
) from e


def romanize(text: str) -> str:
Expand Down
12 changes: 5 additions & 7 deletions pythainlp/util/abbreviation.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ def abbreviation_to_full_text(
"""
try:
from khamyo import replace as _replace
except ImportError:
except ImportError as e:
raise ImportError(
"""
This function needs to use khamyo.
You can install by pip install khamyo or
pip install pythainlp[abbreviation].
"""
)
"khamyo is required for this feature."
" Install it with: pip install khamyo"
" or pip install pythainlp[abbreviation]"
) from e
return cast(list[tuple[str, Optional[float]]], _replace(text, top_k=top_k))
Loading
Loading