diff --git a/README.md b/README.md index 8a9314f..96d6a39 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,16 @@ contractions.expand("myterm is great") # "my expansion is great" ``` +Load all JSON files from a folder: + +```python +# Load all *.json files from a directory (ignores non-JSON files) +contractions.load_folder("./my_contractions/") + +contractions.expand("myterm is great") +# "my expansion is great" +``` + ### Preview Contractions Before Fixing The `preview()` function lets you see all contractions in a text before expanding them: @@ -176,6 +186,18 @@ Loads custom contractions from a JSON file. - `FileNotFoundError`: If the file doesn't exist - `json.JSONDecodeError`: If the file contains invalid JSON +### `load_folder(folderpath)` + +Loads custom contractions from all JSON files in a directory. Non-JSON files are automatically ignored. + +**Parameters:** +- `folderpath` (str): Path to directory containing JSON files + +**Raises:** +- `FileNotFoundError`: If the folder doesn't exist +- `NotADirectoryError`: If the path is a file, not a directory +- `ValueError`: If no JSON files are found in the folder + ### `preview(text, context_chars)` Preview contractions in text before expanding. @@ -293,6 +315,7 @@ This fork includes several enhancements over the original `contractions` library ### 🆕 New Features - **`add_dict()`** - Bulk add custom contractions from a dictionary - **`load_file()`** - Load contractions from JSON files +- **`load_folder()`** - Load all JSON files from a directory - **Type hints** - Full type coverage with mypy validation - **Better structure** - Modular code organization with single-responsibility modules - **Facade API** - Clean, simple public API with shorthand aliases (`e()`, `p()`) diff --git a/contractions/__init__.py b/contractions/__init__.py index 89e5d25..19cbf35 100644 --- a/contractions/__init__.py +++ b/contractions/__init__.py @@ -1,7 +1,7 @@ import warnings from ._version import __version__ -from .api import add, add_dict, e, expand, load_file, p, preview +from .api import add, add_dict, e, expand, load_file, load_folder, p, preview def fix(*args: object, **kwargs: object) -> str: @@ -13,4 +13,4 @@ def fix(*args: object, **kwargs: object) -> str: return expand(*args, **kwargs) # type: ignore[arg-type] -__all__ = ["__version__", "add", "add_dict", "e", "expand", "fix", "load_file", "p", "preview"] +__all__ = ["__version__", "add", "add_dict", "e", "expand", "fix", "load_file", "load_folder", "p", "preview"] diff --git a/contractions/api.py b/contractions/api.py index 2880149..b956dc7 100644 --- a/contractions/api.py +++ b/contractions/api.py @@ -1,4 +1,4 @@ -from .extension import add_custom_contraction, add_custom_dict, load_custom_from_file +from .extension import add_custom_contraction, add_custom_dict, load_custom_from_file, load_custom_from_folder from .processor import expand as _expand from .processor import preview as _preview @@ -23,6 +23,10 @@ def load_file(filepath: str) -> None: return load_custom_from_file(filepath) +def load_folder(folderpath: str) -> None: + return load_custom_from_folder(folderpath) + + e = expand p = preview diff --git a/contractions/extension.py b/contractions/extension.py index 9f7e27f..00fa4ff 100644 --- a/contractions/extension.py +++ b/contractions/extension.py @@ -1,7 +1,4 @@ -import json - -from pathlib import Path - +from .file_loader import load_dict_from_file, load_dict_from_folder from .matcher import ( _get_basic_matcher, _get_leftovers_matcher, @@ -9,7 +6,7 @@ _get_preview_matcher, _get_slang_matcher, ) -from .validation import validate_dict_param, validate_file_contains_dict, validate_non_empty_string +from .validation import validate_dict_param, validate_non_empty_string _ALL_MATCHERS = (_get_basic_matcher, _get_leftovers_matcher, _get_slang_matcher, _get_leftovers_slang_matcher) @@ -34,11 +31,11 @@ def add_custom_dict(contractions_dict: dict[str, str]) -> None: def load_custom_from_file(filepath: str) -> None: - path = Path(filepath) - if not path.exists(): - raise FileNotFoundError(f"File not found at: {filepath}") + contractions_data = load_dict_from_file(filepath) + add_custom_dict(contractions_data) + - contractions_data = json.loads(path.read_text(encoding="utf-8")) - validate_file_contains_dict(contractions_data, filepath) +def load_custom_from_folder(folderpath: str) -> None: + contractions_data = load_dict_from_folder(folderpath) add_custom_dict(contractions_data) diff --git a/contractions/file_loader.py b/contractions/file_loader.py index f8a1ce8..0f3987e 100644 --- a/contractions/file_loader.py +++ b/contractions/file_loader.py @@ -1,9 +1,10 @@ import json import pkgutil +from pathlib import Path from typing import cast -from .validation import validate_data_type +from .validation import validate_data_type, validate_file_contains_dict def load_dict_data(filename: str) -> dict[str, str]: @@ -27,3 +28,35 @@ def load_list_data(filename: str) -> list[str]: return cast("list[str]", data) + +def load_dict_from_file(filepath: str) -> dict[str, str]: + path = Path(filepath) + if not path.exists(): + raise FileNotFoundError(f"File not found at: {filepath}") + + contractions_data = json.loads(path.read_text(encoding="utf-8")) + validate_file_contains_dict(contractions_data, filepath) + return cast("dict[str, str]", contractions_data) + + +def load_dict_from_folder(folderpath: str) -> dict[str, str]: + folder = Path(folderpath) + if not folder.exists(): + raise FileNotFoundError(f"Folder not found at: {folderpath}") + + if not folder.is_dir(): + raise NotADirectoryError(f"Path is not a directory: {folderpath}") + + json_files = sorted(folder.glob("*.json")) + + if not json_files: + raise ValueError(f"No JSON files found in folder: {folderpath}") + + merged_contractions: dict[str, str] = {} + for json_file in json_files: + contractions_data = json.loads(json_file.read_text(encoding="utf-8")) + validate_file_contains_dict(contractions_data, str(json_file)) + merged_contractions.update(contractions_data) + + return merged_contractions + diff --git a/tests/test_contractions.py b/tests/test_contractions.py index b545053..e25d688 100644 --- a/tests/test_contractions.py +++ b/tests/test_contractions.py @@ -169,6 +169,66 @@ def test_load_file_non_dict() -> None: os.unlink(temp_path) +def test_load_folder() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + file1 = os.path.join(temp_dir, "dict1.json") + file2 = os.path.join(temp_dir, "dict2.json") + + with open(file1, "w", encoding="utf-8") as f: + json.dump({"foldertest1": "folder test one", "foldertest2": "folder test two"}, f) + + with open(file2, "w", encoding="utf-8") as f: + json.dump({"foldertest3": "folder test three"}, f) + + contractions.load_folder(temp_dir) + + assert contractions.expand("foldertest1") == "folder test one" + assert contractions.expand("foldertest2") == "folder test two" + assert contractions.expand("foldertest3") == "folder test three" + + +def test_load_folder_ignores_non_json() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + json_file = os.path.join(temp_dir, "valid.json") + txt_file = os.path.join(temp_dir, "ignored.txt") + + with open(json_file, "w", encoding="utf-8") as f: + json.dump({"validkey": "valid value"}, f) + + with open(txt_file, "w", encoding="utf-8") as f: + f.write("this should be ignored") + + contractions.load_folder(temp_dir) + assert contractions.expand("validkey") == "valid value" + + +def test_load_folder_not_found() -> None: + with pytest.raises(FileNotFoundError, match="Folder not found"): + contractions.load_folder("/nonexistent/folder/path") + + +def test_load_folder_not_directory() -> None: + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as f: + json.dump({"test": "data"}, f) + temp_path = f.name + + try: + with pytest.raises(NotADirectoryError, match="not a directory"): + contractions.load_folder(temp_path) + finally: + os.unlink(temp_path) + + +def test_load_folder_no_json_files() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + txt_file = os.path.join(temp_dir, "file.txt") + with open(txt_file, "w", encoding="utf-8") as f: + f.write("no json here") + + with pytest.raises(ValueError, match="No JSON files found"): + contractions.load_folder(temp_dir) + + def test_expand_leftovers_only() -> None: text = "I'm happy you're here" result = contractions.expand(text, leftovers=True, slang=False)