Skip to content
Open
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
44 changes: 35 additions & 9 deletions codespell_lib/_codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,14 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]:
help="regular expression that is used to find URIs "
"and emails. A default expression is provided.",
)
parser.add_argument(
"--show-dictionary",
action="store_true",
default=False,
help="print for each typo the dictionary it was found in: "
"the name of a builtin dictionary or the path of a custom "
"dictionary file given with -D.",
)
parser.add_argument(
"-s",
"--summary",
Expand Down Expand Up @@ -961,6 +969,17 @@ def _format_colored_output(
return cfilename, cline, cwrongword, crightword


def _format_source(
source: str,
colors: TermColors,
options: argparse.Namespace,
) -> str:
"""Format the dictionary a misspelling came from for output."""
if not options.show_dictionary:
return ""
return f" (from {colors.FILE}{source}{colors.DISABLE})"


def parse_lines(
fragment: tuple[bool, int, list[str]],
filename: str,
Expand Down Expand Up @@ -1118,6 +1137,7 @@ def parse_lines(
if options.quiet_level & QuietLevels.NON_AUTOMATIC_FIXES:
continue
creason = ""
csource = _format_source(misspellings[lword].source, colors, options)

# If we get to this point (uncorrected error) we should change
# our bad_count and thus return value
Expand All @@ -1127,14 +1147,15 @@ def parse_lines(
print_context(lines, i, context)
if filename != "-":
print(
f"{cfilename}:{cline}: {cwrongword} ==> {crightword}{creason}"
f"{cfilename}:{cline}: {cwrongword} ==> "
f"{crightword}{creason}{csource}"
)
elif options.stdin_single_line:
print(f"{cline}: {cwrongword} ==> {crightword}{creason}")
print(f"{cline}: {cwrongword} ==> {crightword}{creason}{csource}")
else:
print(
f"{cline}: {line.strip()}\n\t{cwrongword} "
f"==> {crightword}{creason}"
f"==> {crightword}{creason}{csource}"
)

return bad_count, changed, changes_made
Expand Down Expand Up @@ -1190,10 +1211,11 @@ def parse_file(
if options.quiet_level & QuietLevels.NON_AUTOMATIC_FIXES:
continue
creason = ""
csource = _format_source(misspellings[lword].source, colors, options)

bad_count += 1

print(f"{cfilename}: {cwrongword} ==> {crightword}{creason}")
print(f"{cfilename}: {cwrongword} ==> {crightword}{creason}{csource}")

# ignore irregular files
if not os.path.isfile(filename):
Expand Down Expand Up @@ -1302,7 +1324,8 @@ def _usage_error(parser: argparse.ArgumentParser, message: str) -> int:
return EX_USAGE


def _select_builtin_dictionary(builtin_option: str) -> list[str]:
def _select_builtin_dictionary(builtin_option: str) -> list[tuple[str, str]]:
"""Return a list of (filename, name) builtin dictionary tuples."""
use = sorted(set(builtin_option.split(",")))
if "all" in use:
use = [u for u in use if u != "all"] + [
Expand All @@ -1315,7 +1338,10 @@ def _select_builtin_dictionary(builtin_option: str) -> list[str]:
for builtin in _builtin_dictionaries:
if builtin[0] == u:
use_dictionaries.append(
os.path.join(_data_root, f"dictionary{builtin[2]}.txt")
(
os.path.join(_data_root, f"dictionary{builtin[2]}.txt"),
builtin[0],
)
)
break
else:
Expand Down Expand Up @@ -1429,10 +1455,10 @@ def main(*args: str) -> int:
parser,
f"ERROR: cannot find dictionary file: {dictionary}",
)
use_dictionaries.append(dictionary)
use_dictionaries.append((dictionary, dictionary))
misspellings: dict[str, Misspelling] = {}
for dictionary in use_dictionaries:
build_dict(dictionary, misspellings, ignore_words)
for dictionary, source in use_dictionaries:
build_dict(dictionary, misspellings, ignore_words, source)
colors = TermColors()
if not options.colors:
colors.disable()
Expand Down
14 changes: 10 additions & 4 deletions codespell_lib/_spellchecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@


class Misspelling:
def __init__(self, data: str, fix: bool, reason: str) -> None:
def __init__(self, data: str, fix: bool, reason: str, source: str = "") -> None:
self.data = data
self.fix = fix
self.reason = reason
self.source = source


def add_misspelling(
key: str,
data: str,
misspellings: dict[str, Misspelling],
source: str = "",
) -> None:
data = data.strip()

Expand All @@ -43,14 +45,18 @@ def add_misspelling(
fix = True
reason = ""

misspellings[key] = Misspelling(data, fix, reason)
misspellings[key] = Misspelling(data, fix, reason, source)


def build_dict(
filename: str,
misspellings: dict[str, Misspelling],
ignore_words: set[str],
source: str = "",
) -> None:
# name recorded as the origin of the entries, e.g. the name of a
# builtin dictionary; defaults to the dictionary file name
source = source or filename
with open(filename, encoding="utf-8") as f:
translate_tables = [(x, str.maketrans(x, y)) for x, y in alt_chars]
for line in f:
Expand All @@ -60,11 +66,11 @@ def build_dict(
key = key.lower()
data = data.lower()
if key not in ignore_words:
add_misspelling(key, data, misspellings)
add_misspelling(key, data, misspellings, source)
# generate alternative misspellings/fixes
for x, table in translate_tables:
if x in key:
alt_key = key.translate(table)
alt_data = data.translate(table)
if alt_key not in ignore_words:
add_misspelling(alt_key, alt_data, misspellings)
add_misspelling(alt_key, alt_data, misspellings, source)
47 changes: 47 additions & 0 deletions codespell_lib/tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,53 @@ def test_ignore_word_list(
assert cs.main("-Labandonned,someword", "-Labilty", tmp_path) == 1


def test_show_dictionary(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test that --show-dictionary reports the dictionary a typo came from."""
bad_name = tmp_path / "bad.txt"
bad_name.write_text("abandonned\nacapella\n")
# no source is printed without the option
result = cs.main(bad_name, std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 2
assert "(from" not in stdout
# builtin dictionaries are reported by name
result = cs.main("--show-dictionary", bad_name, std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 2
assert "abandonned ==> abandoned (from clear)" in stdout
assert "acapella ==> a cappella (from rare)" in stdout
# custom dictionaries are reported by their path
dictionary = tmp_path / "dict.txt"
dictionary.write_text("abandonned->abandoned\n")
result = cs.main("--show-dictionary", "-D", dictionary, bad_name, std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 1
assert f"abandonned ==> abandoned (from {dictionary})" in stdout
# the last dictionary defining an entry wins
result = cs.main(
"--show-dictionary", "-D", "-", "-D", dictionary, bad_name, std=True
)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 2
assert f"abandonned ==> abandoned (from {dictionary})" in stdout
assert "acapella ==> a cappella (from rare)" in stdout
# --check-filenames reports the source as well
fname = tmp_path / "abandonned.txt"
fname.write_text("good contents\n")
result = cs.main("--show-dictionary", "-f", fname, std=True)
assert isinstance(result, tuple)
code, stdout, _ = result
assert code == 1
assert "abandonned ==> abandoned (from clear)" in stdout


@pytest.mark.parametrize(
("content", "expected_error_count"),
[
Expand Down