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
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ Display them without terminal colors and with a quiet level of 3.

Run interactive mode level 3 and write changes to file.

.. code-block:: sh

codespell -i ask,choose -q encoding,binary-file

The ``-i`` and ``-q`` bitmasks can also be given as comma-separated names;
this is the same as ``-i 3 -q 3``. See ``codespell --help`` for the names.

We ship a collection of dictionaries that are an improved version of the one available
`on Wikipedia <https://en.wikipedia.org/wiki/Wikipedia:Lists_of_common_misspellings/For_machines>`_
after applying them in projects like Linux Kernel, EFL, oFono among others.
Expand Down
72 changes: 55 additions & 17 deletions codespell_lib/_codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import sys
import tempfile
import textwrap
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
from re import Match, Pattern
from typing import (
Any,
Expand Down Expand Up @@ -176,6 +176,44 @@ class QuietLevels:
CONFIG_FILES = 32


_INTERACTIVE_NAMES = {"ask": 1, "choose": 2}
_QUIET_LEVEL_NAMES = {
"encoding": QuietLevels.ENCODING,
"binary-file": QuietLevels.BINARY_FILE,
"disabled-fixes": QuietLevels.DISABLED_FIXES,
"non-automatic-fixes": QuietLevels.NON_AUTOMATIC_FIXES,
"fixes": QuietLevels.FIXES,
"config-files": QuietLevels.CONFIG_FILES,
}


def _bitmask(names: dict[str, int]) -> Callable[[str], int]:
"""Return an argparse type for a bitmask option.

The value is either the number as before (``3``) or a comma-separated
list of names (``ask,choose``) that are ORed together (GH-2526).
"""
all_bits = sum(names.values())

def parse(value: str) -> int:
if value.isdigit():
level = int(value)
if level > all_bits:
msg = f"invalid choice: {value} (choose from 0-{all_bits})"
raise argparse.ArgumentTypeError(msg)
return level
level = 0
for name in value.split(","):
try:
level |= names[name.strip()]
except KeyError:
msg = f"invalid choice: {name!r} (choose from {', '.join(names)})"
raise argparse.ArgumentTypeError(msg) from None
return level

return parse


class GlobMatch:
def __init__(self, pattern: list[str]) -> None:
self.pattern_list: list[str] = pattern
Expand Down Expand Up @@ -592,35 +630,35 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]:
"-i",
"--interactive",
action="store",
type=int,
type=_bitmask(_INTERACTIVE_NAMES),
default=0,
choices=range(0, 4),
help="set interactive mode when writing changes:\n"
help="set interactive mode when writing changes, as a number "
"or as a comma-separated list of names:\n"
"- 0: no interactivity.\n"
"- 1: ask for confirmation; 'a'/'s' answer for the rest of the file.\n"
"- 2: ask user to choose one fix when more than one is available;"
"- 1 or ask: ask for confirmation; 'a'/'s' answer for the rest of the file.\n"
"- 2 or choose: ask user to choose one fix when more than one is available;"
" 'Na'/'s' answer for the rest of the file.\n"
"- 3: both 1 and 2",
"- 3 or ask,choose: both 1 and 2",
metavar="MODE",
)

parser.add_argument(
"-q",
"--quiet-level",
action="store",
type=int,
type=_bitmask(_QUIET_LEVEL_NAMES),
default=34,
choices=range(0, 64),
help="bitmask that allows suppressing messages:\n"
help="bitmask that allows suppressing messages, as a number "
"or as a comma-separated list of names:\n"
"- 0: print all messages.\n"
"- 1: disable warnings about wrong encoding.\n"
"- 2: disable warnings about binary files.\n"
"- 4: omit warnings about automatic fixes that were disabled in the dictionary.\n" # noqa: E501
"- 8: don't print anything for non-automatic fixes.\n"
"- 16: don't print the list of fixed files.\n"
"- 32: don't print configuration files.\n"
"- 1 or encoding: disable warnings about wrong encoding.\n"
"- 2 or binary-file: disable warnings about binary files.\n"
"- 4 or disabled-fixes: omit warnings about automatic fixes that were disabled in the dictionary.\n" # noqa: E501
"- 8 or non-automatic-fixes: don't print anything for non-automatic fixes.\n"
"- 16 or fixes: don't print the list of fixed files.\n"
"- 32 or config-files: don't print configuration files.\n"
"As usual with bitmasks, these levels can be "
"combined; e.g. use 3 for levels 1+2, 7 for "
"combined; e.g. use 3 or encoding,binary-file for levels 1+2, 7 for "
"1+2+4, 23 for 1+2+4+16, etc. "
"The default mask is %(default)s.",
metavar="LEVEL",
Expand Down
49 changes: 48 additions & 1 deletion codespell_lib/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,26 @@ def test_quiet_level_32(
assert "Used config files:" in stdout
assert "setup.cfg" in stdout

# Names are the same as the numbers, on the command line and in a
# config file (GH-2526).
result = cs.main(str(d), *args, "--quiet-level=config-files", std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 0
assert "Used config files:" not in stdout
result = cs.main(str(d), *args, "--quiet-level=encoding,binary-file", std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 0
assert "Used config files:" in stdout
with open(conf, "a", encoding="utf-8") as f:
f.write("quiet-level = config-files\n")
result = cs.main(str(d), *args, std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 0
assert "Used config files:" not in stdout


def test_ill_formed_ini_config_file(
tmp_path: Path,
Expand Down Expand Up @@ -1958,16 +1978,43 @@ def test_interactive_level_2_no_prompt_for_single_fix(
assert f.read_text() == "abandoned\n"


@pytest.mark.parametrize("level", ["3", "ask,choose"])
def test_interactive_level_3_rejection_keeps_yn_prompt(
tmp_path: Path,
level: str,
) -> None:
"""A rejected word must stay a Y/n question, not degrade to an option list."""
f1 = tmp_path / "f1.txt"
f2 = tmp_path / "f2.txt"
f1.write_text("abandonned\n")
f2.write_text("abandonned\n")
proc = run_codespell_interactive(("-w", "-i", "3", f1, f2), answers="n\nn\n")
proc = run_codespell_interactive(("-w", "-i", level, f1, f2), answers="n\nn\n")
assert proc.stdout.count(PROMPT) == 2
assert "Choose an option" not in proc.stdout
assert f1.read_text() == "abandonned\n"
assert f2.read_text() == "abandonned\n"


@pytest.mark.parametrize(
("option", "value", "choices"),
[
("-i", "4", "0-3"),
("-i", "ask,confirm", "ask, choose"),
("-q", "64", "0-63"),
("-q", "binary", "encoding, binary-file"),
],
)
def test_bitmask_option_invalid_value(
tmp_path: Path,
option: str,
value: str,
choices: str,
) -> None:
"""A number out of range or an unknown name is rejected (GH-2526)."""
f = tmp_path / "f.txt"
f.write_text("abandonned\n")
proc = run_codespell_interactive((option, value, f), answers="")
assert proc.returncode == 2
assert "invalid choice" in proc.stderr
assert choices in proc.stderr
assert f.read_text() == "abandonned\n"
Loading