Skip to content

Commit 4ce1cae

Browse files
authored
Accept names as well as numbers for the -i and -q bitmasks (#4062)
1 parent 3de237f commit 4ce1cae

3 files changed

Lines changed: 110 additions & 18 deletions

File tree

‎README.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ Display them without terminal colors and with a quiet level of 3.
107107
108108
Run interactive mode level 3 and write changes to file.
109109

110+
.. code-block:: sh
111+
112+
codespell -i ask,choose -q encoding,binary-file
113+
114+
The ``-i`` and ``-q`` bitmasks can also be given as comma-separated names;
115+
this is the same as ``-i 3 -q 3``. See ``codespell --help`` for the names.
116+
110117
We ship a collection of dictionaries that are an improved version of the one available
111118
`on Wikipedia <https://en.wikipedia.org/wiki/Wikipedia:Lists_of_common_misspellings/For_machines>`_
112119
after applying them in projects like Linux Kernel, EFL, oFono among others.

‎codespell_lib/_codespell.py‎

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import sys
2929
import tempfile
3030
import textwrap
31-
from collections.abc import Iterable, Sequence
31+
from collections.abc import Callable, Iterable, Sequence
3232
from re import Match, Pattern
3333
from typing import (
3434
Any,
@@ -176,6 +176,44 @@ class QuietLevels:
176176
CONFIG_FILES = 32
177177

178178

179+
_INTERACTIVE_NAMES = {"ask": 1, "choose": 2}
180+
_QUIET_LEVEL_NAMES = {
181+
"encoding": QuietLevels.ENCODING,
182+
"binary-file": QuietLevels.BINARY_FILE,
183+
"disabled-fixes": QuietLevels.DISABLED_FIXES,
184+
"non-automatic-fixes": QuietLevels.NON_AUTOMATIC_FIXES,
185+
"fixes": QuietLevels.FIXES,
186+
"config-files": QuietLevels.CONFIG_FILES,
187+
}
188+
189+
190+
def _bitmask(names: dict[str, int]) -> Callable[[str], int]:
191+
"""Return an argparse type for a bitmask option.
192+
193+
The value is either the number as before (``3``) or a comma-separated
194+
list of names (``ask,choose``) that are ORed together (GH-2526).
195+
"""
196+
all_bits = sum(names.values())
197+
198+
def parse(value: str) -> int:
199+
if value.isdigit():
200+
level = int(value)
201+
if level > all_bits:
202+
msg = f"invalid choice: {value} (choose from 0-{all_bits})"
203+
raise argparse.ArgumentTypeError(msg)
204+
return level
205+
level = 0
206+
for name in value.split(","):
207+
try:
208+
level |= names[name.strip()]
209+
except KeyError:
210+
msg = f"invalid choice: {name!r} (choose from {', '.join(names)})"
211+
raise argparse.ArgumentTypeError(msg) from None
212+
return level
213+
214+
return parse
215+
216+
179217
class GlobMatch:
180218
def __init__(self, pattern: list[str]) -> None:
181219
self.pattern_list: list[str] = pattern
@@ -592,35 +630,35 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]:
592630
"-i",
593631
"--interactive",
594632
action="store",
595-
type=int,
633+
type=_bitmask(_INTERACTIVE_NAMES),
596634
default=0,
597-
choices=range(0, 4),
598-
help="set interactive mode when writing changes:\n"
635+
help="set interactive mode when writing changes, as a number "
636+
"or as a comma-separated list of names:\n"
599637
"- 0: no interactivity.\n"
600-
"- 1: ask for confirmation; 'a'/'s' answer for the rest of the file.\n"
601-
"- 2: ask user to choose one fix when more than one is available;"
638+
"- 1 or ask: ask for confirmation; 'a'/'s' answer for the rest of the file.\n"
639+
"- 2 or choose: ask user to choose one fix when more than one is available;"
602640
" 'Na'/'s' answer for the rest of the file.\n"
603-
"- 3: both 1 and 2",
641+
"- 3 or ask,choose: both 1 and 2",
604642
metavar="MODE",
605643
)
606644

607645
parser.add_argument(
608646
"-q",
609647
"--quiet-level",
610648
action="store",
611-
type=int,
649+
type=_bitmask(_QUIET_LEVEL_NAMES),
612650
default=34,
613-
choices=range(0, 64),
614-
help="bitmask that allows suppressing messages:\n"
651+
help="bitmask that allows suppressing messages, as a number "
652+
"or as a comma-separated list of names:\n"
615653
"- 0: print all messages.\n"
616-
"- 1: disable warnings about wrong encoding.\n"
617-
"- 2: disable warnings about binary files.\n"
618-
"- 4: omit warnings about automatic fixes that were disabled in the dictionary.\n" # noqa: E501
619-
"- 8: don't print anything for non-automatic fixes.\n"
620-
"- 16: don't print the list of fixed files.\n"
621-
"- 32: don't print configuration files.\n"
654+
"- 1 or encoding: disable warnings about wrong encoding.\n"
655+
"- 2 or binary-file: disable warnings about binary files.\n"
656+
"- 4 or disabled-fixes: omit warnings about automatic fixes that were disabled in the dictionary.\n" # noqa: E501
657+
"- 8 or non-automatic-fixes: don't print anything for non-automatic fixes.\n"
658+
"- 16 or fixes: don't print the list of fixed files.\n"
659+
"- 32 or config-files: don't print configuration files.\n"
622660
"As usual with bitmasks, these levels can be "
623-
"combined; e.g. use 3 for levels 1+2, 7 for "
661+
"combined; e.g. use 3 or encoding,binary-file for levels 1+2, 7 for "
624662
"1+2+4, 23 for 1+2+4+16, etc. "
625663
"The default mask is %(default)s.",
626664
metavar="LEVEL",

‎codespell_lib/tests/test_basic.py‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,26 @@ def test_quiet_level_32(
14921492
assert "Used config files:" in stdout
14931493
assert "setup.cfg" in stdout
14941494

1495+
# Names are the same as the numbers, on the command line and in a
1496+
# config file (GH-2526).
1497+
result = cs.main(str(d), *args, "--quiet-level=config-files", std=True)
1498+
assert isinstance(result, tuple)
1499+
code, stdout, _ = result
1500+
assert code == 0
1501+
assert "Used config files:" not in stdout
1502+
result = cs.main(str(d), *args, "--quiet-level=encoding,binary-file", std=True)
1503+
assert isinstance(result, tuple)
1504+
code, stdout, _ = result
1505+
assert code == 0
1506+
assert "Used config files:" in stdout
1507+
with open(conf, "a", encoding="utf-8") as f:
1508+
f.write("quiet-level = config-files\n")
1509+
result = cs.main(str(d), *args, std=True)
1510+
assert isinstance(result, tuple)
1511+
code, stdout, _ = result
1512+
assert code == 0
1513+
assert "Used config files:" not in stdout
1514+
14951515

14961516
def test_ill_formed_ini_config_file(
14971517
tmp_path: Path,
@@ -1958,16 +1978,43 @@ def test_interactive_level_2_no_prompt_for_single_fix(
19581978
assert f.read_text() == "abandoned\n"
19591979

19601980

1981+
@pytest.mark.parametrize("level", ["3", "ask,choose"])
19611982
def test_interactive_level_3_rejection_keeps_yn_prompt(
19621983
tmp_path: Path,
1984+
level: str,
19631985
) -> None:
19641986
"""A rejected word must stay a Y/n question, not degrade to an option list."""
19651987
f1 = tmp_path / "f1.txt"
19661988
f2 = tmp_path / "f2.txt"
19671989
f1.write_text("abandonned\n")
19681990
f2.write_text("abandonned\n")
1969-
proc = run_codespell_interactive(("-w", "-i", "3", f1, f2), answers="n\nn\n")
1991+
proc = run_codespell_interactive(("-w", "-i", level, f1, f2), answers="n\nn\n")
19701992
assert proc.stdout.count(PROMPT) == 2
19711993
assert "Choose an option" not in proc.stdout
19721994
assert f1.read_text() == "abandonned\n"
19731995
assert f2.read_text() == "abandonned\n"
1996+
1997+
1998+
@pytest.mark.parametrize(
1999+
("option", "value", "choices"),
2000+
[
2001+
("-i", "4", "0-3"),
2002+
("-i", "ask,confirm", "ask, choose"),
2003+
("-q", "64", "0-63"),
2004+
("-q", "binary", "encoding, binary-file"),
2005+
],
2006+
)
2007+
def test_bitmask_option_invalid_value(
2008+
tmp_path: Path,
2009+
option: str,
2010+
value: str,
2011+
choices: str,
2012+
) -> None:
2013+
"""A number out of range or an unknown name is rejected (GH-2526)."""
2014+
f = tmp_path / "f.txt"
2015+
f.write_text("abandonned\n")
2016+
proc = run_codespell_interactive((option, value, f), answers="")
2017+
assert proc.returncode == 2
2018+
assert "invalid choice" in proc.stderr
2019+
assert choices in proc.stderr
2020+
assert f.read_text() == "abandonned\n"

0 commit comments

Comments
 (0)