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
92 changes: 92 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# AGENTS.md

This file provides guidance to AI coding agents when working with code in
this repository.

## AI-assistance policy (read first)

codespell is maintained by a small number of volunteers. Code is reviewed
and maintained by humans, so interactions about it should be between
humans, too.

- **The human opening the PR is the sole author.** Do not add AI
co-authorship trailers (`Co-Authored-By:` for Claude / Copilot / Cursor /
any other tool) to commits, and do not use a bot as the commit author.
The author takes full responsibility for the contents and must be able to
explain and defend every line on request.
- **Do not write issues, PR descriptions, or comments for the human.**
Pasted AI-generated text makes it harder for reviewers to assess a
contribution. The human writes those in their own words, and discloses in
the PR description which AI tools were used and the scope of their
assistance.
- **Optimize for the reviewer and for long-term maintainability, not for
output volume.** Every line you add is read by a volunteer reviewer and
then maintained by humans in perpetuity.

## Keep changes small

A big diff is a cost, not an accomplishment. Aim for the smallest change
that delivers the user-visible behavior; anything else can be added later,
when something actually needs it.

- **Sketch before you generate.** For anything beyond a local fix,
summarize the design in a few sentences for the human you are working
with and confirm it is the smallest one that works before writing code.
- **YAGNI.** Do not add options, config knobs, fallbacks, or "robustness"
the change does not need. Every new command-line option is documented,
tested, and supported forever, so it needs a demonstrated user need, not
a hypothetical one.
- **Reuse before writing something new.** Look for an existing helper in
`codespell_lib/_codespell.py` and build on it rather than adding a
near-duplicate.
- **Do not add dependencies** without asking first.
- **Add to an existing test before writing a new one.** Extend a test in
`codespell_lib/tests/test_basic.py` that already sets up what you need,
then add a new test function there, and only then consider a new file. A
few compact assertions beat a long test that re-creates setup.
- **For a bug fix, confirm the test fails without the fix and passes with
it.** A regression test that passes without the fix guards nothing.

## Commands

```bash
pip install -e . --group dev # editable install with test/lint deps
pre-commit install --install-hooks # ruff, mypy, codespell, file sorting, ...
pytest codespell_lib/tests/test_basic.py -k write_changes # CLI tests
make check-dictionaries # sort + validate dictionaries (test_dictionary.py)
pre-commit run --all-files # all linters, auto-fixing where possible
make check # everything CI checks: dictionaries, build, pytest, ruff
```

## Project layout

Pure Python (>= 3.9), no runtime dependencies.

- `codespell_lib/_codespell.py`: the CLI (`main()`), option parsing, and
file checking/fixing.
- `codespell_lib/data/dictionary*.txt`: the misspelling dictionaries, one
per `--builtin` set.
- `codespell_lib/tests/test_basic.py`: CLI behavior tests, mostly calling
`cs.main(...)` on files in `tmp_path`.
- `codespell_lib/tests/test_dictionary.py`: dictionary format and
consistency checks.

Dictionary entries are `typo->fix`; with more than one fix, end the line
with a comma so none is applied automatically:

```text
calulated->calculated
fiel->feel, field, file, phial,
```

See "Dictionary format" in `README.rst` for the full rules.

## Boundaries

- **Always:** run the relevant tests and
`pre-commit run --files <changed files>` before handing work back.
- **Ask first:** new command-line options, new dependencies, and changes to
CI (`.github/`), `pyproject.toml`, or `.pre-commit-config.yaml`.
- **Never:** hand-edit `dictionary_en_to_en-OX_AUTOGENERATED.txt` (it is
generated from `dictionary_en-GB_to_en-US.txt` by `make`), delete or
weaken a failing test to make it pass, or commit secrets.
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CLAUDE.md

See @AGENTS.md for guidance on working with this repository.
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ Some noteworthy flags:
The ``-w`` flag will actually implement the changes recommended by codespell. Running without the ``-w`` flag is the same as doing a dry run. It is recommended to run this with the ``-i`` or ``--interactive`` flag.

.. code-block:: sh
codespell -w --exit-non-zero-on-fix
By default, ``-w`` exits with status 0 when every misspelling it finds was fixed. The ``--exit-non-zero-on-fix`` flag makes codespell exit with a non-zero status whenever it writes changes, which is useful in CI so that a run that modified files is reported as a failure.

.. code-block:: sh
codespell -I FILE, --ignore-words=FILE
Expand Down
34 changes: 24 additions & 10 deletions codespell_lib/_codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,14 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]:
default=False,
help="write changes in place if possible",
)
parser.add_argument(
"--exit-non-zero-on-fix",
action="store_true",
default=False,
help="exit with a non-zero status if any changes were written, "
"even if every misspelling was fixed. Has no effect without "
"-w/--write-changes or -i/--interactive.",
)

parser.add_argument(
"-D",
Expand Down Expand Up @@ -1200,7 +1208,7 @@ def parse_file(
uri_ignore_words: set[str],
context: Optional[tuple[int, int]],
options: argparse.Namespace,
) -> int:
) -> tuple[int, bool]:
bad_count = 0
fragments = None

Expand Down Expand Up @@ -1243,24 +1251,24 @@ def parse_file(

# ignore irregular files
if not os.path.isfile(filename):
return bad_count
return bad_count, False

try:
text = is_text_file(filename)
except PermissionError as e:
print(f"WARNING: {e.strerror}: {filename}", file=sys.stderr)
return bad_count
return bad_count, False
except OSError:
return bad_count
return bad_count, False

if not text:
if not options.quiet_level & QuietLevels.BINARY_FILE:
print(f"WARNING: Binary file: {filename}", file=sys.stderr)
return bad_count
return bad_count, False
try:
fragments, encoding = file_opener.open(filename)
except OSError:
return bad_count
return bad_count, False

# Parse lines.
changed = False
Expand Down Expand Up @@ -1317,7 +1325,7 @@ def parse_file(
)
_write_file_atomically(filename, encoding, fragments)

return bad_count
return bad_count, changed


def _write_file_atomically(
Expand Down Expand Up @@ -1569,6 +1577,7 @@ def main(*args: str) -> int:
)

bad_count = 0
any_changed = False
for filename in sorted(options.files):
# ignore hidden files
if is_hidden(filename, options.check_hidden):
Expand All @@ -1590,7 +1599,7 @@ def main(*args: str) -> int:
fname = os.path.join(root, file_)
if glob_match.match(fname): # skip paths
continue
bad_count += parse_file(
bad_count_update, changed = parse_file(
fname,
colors,
summary,
Expand All @@ -1605,6 +1614,8 @@ def main(*args: str) -> int:
context,
options,
)
bad_count += bad_count_update
any_changed = any_changed or changed

# skip (relative) directories
dirs[:] = [
Expand All @@ -1615,7 +1626,7 @@ def main(*args: str) -> int:
]

elif not glob_match.match(filename): # skip files
bad_count += parse_file(
bad_count_update, changed = parse_file(
filename,
colors,
summary,
Expand All @@ -1630,10 +1641,13 @@ def main(*args: str) -> int:
context,
options,
)
bad_count += bad_count_update
any_changed = any_changed or changed

if summary:
print("\n-------8<-------\nSUMMARY:")
print(summary)
if options.count:
print(bad_count, file=sys.stderr)
return EX_DATAERR if bad_count else EX_OK
failed = bad_count or (any_changed and options.exit_non_zero_on_fix)
return EX_DATAERR if failed else EX_OK
8 changes: 8 additions & 0 deletions codespell_lib/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ def test_write_changes_lists_changes(
corrected = fname.read_text()
assert corrected == "This is abandoned\nAnd this is occurred\nAlso the typo\n"

# --exit-non-zero-on-fix fails only when changes were written
fname.write_text("abandonned\n")
assert cs_.main("-w", str(fname)) == EX_OK
fname.write_text("abandonned\n")
assert cs_.main("-w", "--exit-non-zero-on-fix", str(fname)) == EX_DATAERR
assert fname.read_text() == "abandoned\n"
assert cs_.main("-w", "--exit-non-zero-on-fix", str(fname)) == EX_OK


def test_default_word_parsing(
tmp_path: Path,
Expand Down
Loading