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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should clarify the behavior when multiple JSON files contain the same keys. Currently, later files (in sorted order) will overwrite values from earlier files. This merge behavior should be documented so users understand how conflicts are resolved.

Suggested change
**Note:** If multiple JSON files contain the same contraction keys, values from later files (in sorted order) will overwrite those from earlier files. This means the final mapping will reflect the values from the last file containing each key.

Copilot uses AI. Check for mistakes.
**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
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for load_folder() is incomplete. It should also document that json.JSONDecodeError can be raised when a JSON file in the folder contains invalid JSON syntax, similar to how load_file() documents this exception. This is important for users to understand all possible error conditions.

Suggested change
- `ValueError`: If no JSON files are found in the folder
- `ValueError`: If no JSON files are found in the folder
- `json.JSONDecodeError`: If any JSON file contains invalid JSON

Copilot uses AI. Check for mistakes.

### `preview(text, context_chars)`

Preview contractions in text before expanding.
Expand Down Expand Up @@ -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()`)
Expand Down
4 changes: 2 additions & 2 deletions contractions/__init__.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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"]
6 changes: 5 additions & 1 deletion contractions/api.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

17 changes: 7 additions & 10 deletions contractions/extension.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
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,
_get_leftovers_slang_matcher,
_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)

Expand All @@ -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)

35 changes: 34 additions & 1 deletion contractions/file_loader.py
Original file line number Diff line number Diff line change
@@ -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]:
Expand All @@ -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

60 changes: 60 additions & 0 deletions tests/test_contractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Comment on lines +172 to +230
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite is missing coverage for the case where a folder contains invalid JSON files. While there's a test for invalid JSON in load_file() (test_load_file_invalid_json), there's no corresponding test for load_folder() handling malformed JSON. This could lead to unclear error behavior when a folder contains files with invalid JSON syntax.

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +230
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite is missing coverage for the case where a folder contains JSON files that are not dictionaries (e.g., arrays, strings). While there's a test for non-dict JSON in load_file() (test_load_file_non_dict), there's no corresponding test for load_folder() when encountering files with valid JSON that isn't a dictionary format.

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +230
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for verifying behavior when JSON files in a folder have overlapping keys. The implementation uses dict.update() which will silently overwrite earlier values with later ones based on sorted filename order. This behavior should be explicitly tested to document the expected behavior.

Copilot uses AI. Check for mistakes.

def test_expand_leftovers_only() -> None:
text = "I'm happy you're here"
result = contractions.expand(text, leftovers=True, slang=False)
Expand Down
Loading