Metadata-Version: 2.4
Name: pygrammar-fix
Version: 1.0.0
Summary: Fast, offline long-paragraph grammar correction that orchestrates harper-py and nlprule.
Author-email: Ankit Arora <you@example.com>
License: MIT
Project-URL: Homepage, https://github.com/ankitarora05/pygrammar-fix
Project-URL: Repository, https://github.com/ankitarora05/pygrammar-fix
Project-URL: Issues, https://github.com/ankitarora05/pygrammar-fix/issues
Keywords: grammar,spelling,nlp,harper,nlprule,correction
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: harper-py>=0.1.1
Requires-Dist: pyspellchecker>=0.8
Provides-Extra: nlprule
Requires-Dist: nlprule>=0.6.3; (sys_platform != "darwin" or platform_machine != "arm64") and extra == "nlprule"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Requires-Dist: nlprule>=0.6.3; (sys_platform != "darwin" or platform_machine != "arm64") and extra == "dev"
Dynamic: license-file

# pygrammar-fix

Fast, offline long-paragraph grammar and spelling correction that orchestrates
[`harper-py`](https://pypi.org/project/harper-py/),
[`nlprule`](https://pypi.org/project/nlprule/) (optional), and
[`pyspellchecker`](https://pypi.org/project/pyspellchecker/).

- **Fast and offline** — no server, no API key, no JVM. Everything runs locally.
- **Returns fixed text** — `fix()` gives you the corrected string directly.
- **Handles long paragraphs** — sentence-aware chunking keeps each engine call reliable.
- **Three-stage pipeline** — spelling pass → grammar pass → join, each independently tunable.
- **Honest about limits** — see [Known limitations](#known-limitations) below.

## Install

```bash
pip install pygrammar-fix
```

For multilingual correction (German, Spanish), install the `nlprule` extra.
`nlprule` has no macOS ARM64 wheel, so this works on Linux, Windows, and
Intel Macs:

```bash
pip install "pygrammar-fix[nlprule]"
```

On Apple Silicon you can still use `pygrammar-fix` for English — spelling and
grammar are handled by `pyspellchecker` and `harper-py`, both of which ship
native ARM64 wheels. `nlprule`-dependent features raise a clear `RuntimeError`
instead of failing silently.

## Quick start

```python
from pygrammar_fix import GrammarFixer

fixer = GrammarFixer(prefer="harper")
print(fixer.fix("I definately recieve to many seperate emails becuase my inbox is allways full."))
# → I definitely receive to many separate emails because my inbox is always full.
```

That example shows the pipeline working end-to-end:

- `definately`, `recieve`, `seperate`, `becuase`, `allways` — fixed by the spelling pass
- `inbox` — **not** mangled (distance guard rejects `inbox` → `into`)
- `to many` — left alone (homophone; grammar-engine territory, see limitations)

## Usage

### Fix text

```python
from pygrammar_fix import GrammarFixer

fixer = GrammarFixer()
fixed = fixer.fix("This is an test of the the linter.")
print(fixed)  # → This is a test of the linter.
```

### Structured suggestions

```python
for suggestion in fixer.suggest("She was not been here since Monday."):
    print(suggestion["start"], suggestion["end"], suggestion["message"], suggestion["replacements"])
```

Note: `suggest()` exposes the **grammar engines** only. Spelling corrections are
applied by `fix()`. Use `suggest()` when you want to present the raw grammar
diagnostics to a user and let them choose.

### Engine selection

```python
# Fastest path for English (harper-py)
fixer = GrammarFixer(prefer="harper")

# Default: nlprule where available, falls back to a clear error on macOS ARM64
fixer = GrammarFixer(prefer="nlprule")

# German or Spanish (requires the nlprule extra)
fixer = GrammarFixer(lang="de")
```

### Disable spelling

```python
# Per call
fixer.fix(text, spellcheck=False)

# Globally
fixer = GrammarFixer(spellcheck=False)
```

### Tune chunk size

```python
# Smaller chunks are more reliable on tricky text; larger chunks are faster.
fixer = GrammarFixer(max_chunk_chars=3000)
```

### Protect domain-specific words

`pyspellchecker` will flag unusual proper nouns as misspellings. Register them:

```python
from pygrammar_fix import add_to_whitelist

add_to_whitelist("Kubernetes", "Ankit", "Arora", "pygrammar")

fixer = GrammarFixer(prefer="harper")
fixer.fix("Ankit deployed to Kubernetes.")  # neither name is touched
```

## How it works

The package is a thin orchestrator, not a merged binding. Three independent
layers run in sequence:

```
                ┌─────────────────────┐
input text  ──▶ │  1. Spelling pass   │  pyspellchecker + Damerau-Levenshtein guard
                └─────────────────────┘
                            │
                            ▼
                ┌─────────────────────┐
                │  2. Chunker         │  sentence-aware split at ≤ max_chunk_chars
                └─────────────────────┘
                            │
                            ▼
                ┌─────────────────────┐
                │  3. Grammar engine  │  harper-py  (English, fast)
                └─────────────────────┘  or nlprule (English/DE/ES, correct())
                            │
                            ▼
                       corrected text
```

| Layer | File | Purpose |
| :--- | :--- | :--- |
| Spelling | `src/pygrammar_fix/_spellcheck.py` | Fix misspellings conservatively; skip acronyms, camelCase, and distance-2 candidates |
| Chunker | `src/pygrammar_fix/_chunker.py` | Split long paragraphs into sentence-aware chunks |
| Adapters | `src/pygrammar_fix/_adapters.py` | Uniform `suggest` / `correct` interface over harper-py and nlprule |
| Orchestrator | `src/pygrammar_fix/_orchestrator.py` | Public `GrammarFixer` API; engine selection; lazy loading |

### Why three layers instead of two

`harper-py` is fast and catches both grammar and spelling. But it doesn't know
every word: `allways` gets split into `all ways` (valid English, wrong meaning)
rather than corrected to `always`. `nlprule` catches some grammar-adjacent
spelling issues via rules but has no ARM64 wheel. A dedicated spellchecker that
runs *before* the grammar engine solves both problems:

- `allways` → `always` reliably
- No dependency on `nlprule` for common typos
- The distance guard prevents the spellchecker from making things worse

### Why the distance guard matters

`pyspellchecker`'s default dictionary (~82K words) doesn't include `inbox`.
Left to its own devices, it corrects `inbox` → `into` (distance 2, both valid
words). The distance guard in `_spellcheck.py` rejects any correction more than
1 edit away for short words, and more than 2 for long words — so `inbox` stays
`inbox`, while `recieve` → `receive` and `becuase` → `because` still work.

## Performance

Measured on an M-series MacBook Air, Python 3.12:

| Workload | Time |
| :--- | :--- |
| 1 sentence (~60 chars) | ~5 ms |
| 8,500 chars (500 sentences) | ~370 ms |
| First call (dictionary load) | adds ~50–100 ms one-time |

Grammar correction dominates the cost — Harper itself lints ~1,000 words in
~23 ms, but the orchestrator re-chunks and re-lints each chunk. For most use
cases this is more than fast enough. If you need raw speed on a single
sentence, use `GrammarFixer(max_chunk_chars=10_000, spellcheck=False)` to skip
spellchecking and minimize chunking overhead.

## Known limitations

- **Homophones are not corrected.** `to`/`too`, `its`/`it's`, `your`/`you're`
  are valid words individually — no spellchecker can fix them without grammar
  context. `nlprule` catches many of these (Linux/Windows/Intel Mac); on
  macOS ARM64 they pass through unchanged. This is the honest boundary of the
  spellchecker layer.
- **Rare misspellings may be missed.** `pyspellchecker` uses a frequency-based
  dictionary. Very rare words that aren't in it and have no close neighbors
  stay uncorrected.
- **Proper nouns are flagged as misspellings** unless whitelisted. Use
  `add_to_whitelist("YourName", "YourProduct")`.
- **`nlprule` has no macOS ARM64 wheel.** `GrammarFixer(lang="de")` and
  `GrammarFixer(prefer="nlprule")` on Apple Silicon raise a clear
  `RuntimeError`. English works fine without it via `prefer="harper"`.
- **`suggest()` does not include spelling corrections.** That method exposes
  the grammar engines only. Spelling runs exclusively in `fix()`.

## Development

```bash
git clone https://github.com/ankitarora05/pygrammar-fix
cd pygrammar-fix

python -m venv .venv
source .venv/bin/activate

pip install -e ".[dev]"
pytest -v
```

On macOS ARM64 the `nlprule`-dependent tests are automatically skipped (the
`dev` extra uses a PEP 508 environment marker). On Linux and Windows they run.

### Project layout

```
pygrammar-fix/
├── src/pygrammar_fix/
│   ├── __init__.py
│   ├── _adapters.py         # harper-py and nlprule wrappers
│   ├── _chunker.py          # sentence-aware splitter
│   ├── _orchestrator.py     # GrammarFixer
│   ├── _spellcheck.py       # pyspellchecker + distance guard
│   └── py.typed             # PEP 561 marker
├── tests/
│   ├── test_adapters.py
│   ├── test_chunker.py
│   ├── test_orchestrator.py
│   └── test_spellcheck.py
├── pyproject.toml
└── README.md
```

### Running the tests

```bash
pytest -v
```

Expected on macOS ARM64:

```
tests/test_chunker.py          ...              [PASS]
tests/test_orchestrator.py     .ss              [PASS + 2 skip]
tests/test_spellcheck.py       .........        [PASS]
tests/test_adapters.py         s                [SKIP — nlprule absent]
```

### Building the wheel

```bash
python -m build
ls -lh dist/
```

Produces `pygrammar_fix-1.0.0-py3-none-any.whl` and
`pygrammar_fix-1.0.0.tar.gz`. Both are pure-Python — they install on any
platform that has wheels for the binary dependencies (`harper-py`,
`pyspellchecker`, and optionally `nlprule`).

## License

MIT — see [LICENSE](LICENSE).
