Repository: codespell-project/codespell

Issue:
mut is a keyword in rust
I have an app which is partly written in rust, but the keyword `mut` (used to tell that a variable is not constant) is recognised as `mut ==> must, mutt, moot`.

Are there any way to disable it on `.rs` files ?


Discussion:
Can you see if any of the usage options would work?

https://github.com/codespell-project/codespell#usage
@nils-van-zuijlen using --ignore-words should do the job, or the exclude list:
https://github.com/codespell-project/codespell/blob/master/codespell_lib/_codespell.py#L248-L250

Resolving patch:
diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py
--- a/codespell_lib/_codespell.py
+++ b/codespell_lib/_codespell.py
@@ -35,8 +35,18 @@
 
 # Users might want to link this file into /usr/local/bin, so we resolve the
 # symbolic link path to the real path if necessary.
-default_dictionary = os.path.join(os.path.dirname(os.path.realpath(__file__)),
-                                  'data', 'dictionary.txt')
+_data_root = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data')
+_builtin_dictionaries = (  # name, desc, name, err in aspell, correction in aspell  # noqa: E501
+# The aspell tests here aren't the ideal state, but the None's are realistic
+# for obscure words
+    ('clear', 'for unambiguous errors', '', False, None),
+    ('rare', 'for rare but valid words', '_rare', None, None),
+    ('informal', 'for informal words', '_informal', True, True),
+    ('code', 'for words common to code and/or mathematics', '_code', None, None),  # noqa: E501
+    ('names', 'for valid proper names that might be typos', '_names', None, None),  # noqa: E501
+    ('en-GB_to_en-US', 'for corrections from en-GB to en-US', '_en-GB_to_en-US', True, True),  # noqa: E501
+)
+_builtin_default = 'clear,rare'
 
 # OPTIONS:
 #
@@ -216,11 +226,21 @@ def parse_options(args):
                         help='write changes in place if possible')
 
     parser.add_argument('-D', '--dictionary',
-                        action='append', metavar='FILE',
+                        action='append',
                         help='Custom dictionary file that contains spelling '
                              'corrections. If this flag is not specified or '
                              'equals "-" then the default dictionary is used. '
                              'This option can be specified multiple times.')
+    builtin_opts = ', '.join(
+        '%r %s' % (d[0], d[1]) for d in _builtin_dictionaries)
+    parser.add_argument('--builtin',
+                        dest='builtin', default=_builtin_default,
+                        metavar='BUILTIN-LIST',
+                        help='Comma-separated list of builtin dictionaries '
+                        'to include (when "-D -" or no "-D" is passed). '
+                        'Current options are:\n%s. The default is '
+                        '"--builtin %s".'
+                        % (builtin_opts, _builtin_default))
     parser.add_argument('-I', '--ignore-words',
                         action='append', metavar='FILE',
                         help='File that contains words which will be ignored '
@@ -603,7 +623,7 @@ def main(*args):
     ignore_words_files = options.ignore_words or []
     ignore_words = set()
     for ignore_words_file in ignore_words_files:
-        if not os.path.exists(ignore_words_file):
+        if not os.path.isfile(ignore_words_file):
             print('ERROR: cannot find ignore-words file: %s' %
                   ignore_words_file, file=sys.stderr)
             parser.print_help()
@@ -615,16 +635,36 @@ def main(*args):
         for word in comma_separated_words.split(','):
             ignore_words.add(word.strip())
 
-    dictionaries = options.dictionary or [default_dictionary]
-    misspellings = dict()
+    if options.dictionary:
+        dictionaries = options.dictionary
+    else:
+        dictionaries = ['-']
+    use_dictionaries = list()
     for dictionary in dictionaries:
         if dictionary == "-":
-            dictionary = default_dictionary
-        if not os.path.exists(dictionary):
-            print('ERROR: cannot find dictionary file: %s' % dictionary,
-                  file=sys.stderr)
-            parser.print_help()
-            return 1
+            # figure out which builtin dictionaries to use
+            use = sorted(set(options.builtin.split(',')))
+            for u in use:
+                for builtin in _builtin_dictionaries:
+                    if builtin[0] == u:
+                        use_dictionaries.append(
+                            os.path.join(_data_root, 'dictionary%s.txt'
+                                         % (builtin[2],)))
+                        break
+                else:
+                    print('ERROR: Unknown builtin dictionary: %s' % (u,),
+                          file=sys.stderr)
+                    parser.print_help()
+                    return 1
+        else:
+            if not os.path.isfile(dictionary):
+                print('ERROR: cannot find dictionary file: %s' % dictionary,
+                      file=sys.stderr)
+                parser.print_help()
+                return 1
+            use_dictionaries.append(dictionary)
+    misspellings = dict()
+    for dictionary in use_dictionaries:
         build_dict(dictionary, misspellings, ignore_words)
     colors = TermColors()
     if not options.colors or sys.platform == 'win32':
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -48,7 +48,7 @@
               'codespell_lib.data',
           ],
           package_data={'codespell_lib': [
-              op.join('data', 'dictionary.txt'),
+              op.join('data', 'dictionary*.txt'),
               op.join('data', 'linux-kernel.exclude'),
           ]},
           entry_points={


Test patch:
diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py
--- a/codespell_lib/tests/test_basic.py
+++ b/codespell_lib/tests/test_basic.py
@@ -31,24 +31,35 @@ def test_command(tmpdir):
 def test_basic(tmpdir, capsys):
     """Test some basic functionality"""
     assert cs.main('_does_not_exist_') == 0
-    with open(op.join(str(tmpdir), 'tmp'), 'w') as f:
+    fname = op.join(str(tmpdir), 'tmp')
+    with open(fname, 'w') as f:
         pass
     assert cs.main('-D', 'foo', f.name) == 1, 'missing dictionary'
-    try:
-        assert 'cannot find dictionary' in capsys.readouterr()[1]
-        assert cs.main(f.name) == 0, 'empty file'
-        with open(f.name, 'a') as f:
-            f.write('this is a test file\n')
-        assert cs.main(f.name) == 0, 'good'
-        with open(f.name, 'a') as f:
-            f.write('abandonned\n')
-        assert cs.main(f.name) == 1, 'bad'
-        with open(f.name, 'a') as f:
-            f.write('abandonned\n')
-        assert cs.main(f.name) == 2, 'worse'
-    finally:
-        os.remove(f.name)
+    assert 'cannot find dictionary' in capsys.readouterr()[1]
+    assert cs.main(fname) == 0, 'empty file'
+    with open(fname, 'a') as f:
+        f.write('this is a test file\n')
+    assert cs.main(fname) == 0, 'good'
+    with open(fname, 'a') as f:
+        f.write('abandonned\n')
+    assert cs.main(fname) == 1, 'bad'
+    with open(fname, 'a') as f:
+        f.write('abandonned\n')
+    assert cs.main(fname) == 2, 'worse'
+    with open(fname, 'a') as f:
+        f.write('tim\ngonna\n')
+    assert cs.main(fname) == 2, 'with a name'
+    assert cs.main('--builtin', 'clear,rare,names,informal', fname) == 4
+    capsys.readouterr()
+    assert cs.main(fname, '--builtin', 'foo') == 1  # bad type sys.exit(1)
+    stdout = capsys.readouterr()[1]
+    assert 'Unknown builtin dictionary' in stdout
     d = str(tmpdir)
+    assert cs.main(fname, '-D', op.join(d, 'foo')) == 1  # bad dict
+    stdout = capsys.readouterr()[1]
+    assert 'cannot find dictionary' in stdout
+    os.remove(fname)
+
     with open(op.join(d, 'bad.txt'), 'w') as f:
         f.write('abandonned\nAbandonned\nABANDONNED\nAbAnDoNnEd')
     assert cs.main(d) == 4
diff --git a/codespell_lib/tests/test_dictionary.py b/codespell_lib/tests/test_dictionary.py
--- a/codespell_lib/tests/test_dictionary.py
+++ b/codespell_lib/tests/test_dictionary.py
@@ -1,61 +1,174 @@
 # -*- coding: utf-8 -*-
 
+import glob
 import os.path as op
+import os
 import re
+import warnings
 
+import pytest
 
-def test_dictionary_formatting():
+from codespell_lib._codespell import _builtin_dictionaries
+
+try:
+    import aspell
+    speller = aspell.Speller('lang', 'en')
+except Exception as exp:  # probably ImportError, but maybe also language
+    speller = None
+    if os.getenv('REQUIRE_ASPELL', 'false').lower() == 'true':
+        raise RuntimeError(
+            'Cannot run complete tests without aspell when '
+            'REQUIRE_ASPELL=true. Got error during import:\n%s'
+            % (exp,))
+    else:
+        warnings.warn(
+            'aspell not found, but not required, skipping aspell tests. Got '
+            'error during import:\n%s' % (exp,))
+
+ws = re.compile(r'.*\s.*')  # whitespace
+comma = re.compile(r'.*,.*')  # comma
+
+
+# Filename, should be seen as errors in aspell or not
+_data_dir = op.join(op.dirname(__file__), '..', 'data')
+_fnames_in_aspell = [
+    (op.join(_data_dir, 'dictionary%s.txt' % d[2]), d[3:5])
+    for d in _builtin_dictionaries]
+fname_params = pytest.mark.parametrize('fname, in_aspell', _fnames_in_aspell)
+
+
+def test_dictionaries_exist():
+    """Test consistency of dictionaries."""
+    doc_fnames = set(op.basename(f[0]) for f in _fnames_in_aspell)
+    got_fnames = set(op.basename(f)
+                     for f in glob.glob(op.join(_data_dir, '*.txt')))
+    assert doc_fnames == got_fnames
+
+
+@fname_params
+def test_dictionary_formatting(fname, in_aspell):
     """Test that all dictionary entries are valid."""
-    err_dict = dict()
-    ws = re.compile(r'.*\s.*')  # whitespace
-    comma = re.compile(r'.*,.*')  # comma
-    with open(op.join(op.dirname(__file__), '..', 'data',
-                      'dictionary.txt'), 'rb') as fid:
+    errors = list()
+    with open(fname, 'rb') as fid:
         for line in fid:
             err, rep = line.decode('utf-8').split('->')
             err = err.lower()
             rep = rep.rstrip('\n')
-            assert err != rep.lower(), 'error %r corrects to itself' % err
+            try:
+                _check_err_rep(err, rep, in_aspell, fname)
+            except AssertionError as exp:
+                errors.append(str(exp).split('\n')[0])
+    if len(errors):
+        raise AssertionError('\n' + '\n'.join(errors))
+
+
+def _check_aspell(word, msg, in_aspell, fname):
+    if speller is None:
+        return  # cannot check
+    if in_aspell is None:
+        return  # don't check
+    if ' ' in word:
+        return  # can't check (easily)
+    this_in_aspell = speller.check(
+        word.encode(speller.ConfigKeys()['encoding'][1]))
+    end = 'be in aspell for dictionary %s' % (fname,)
+    if in_aspell:  # should be an error in aspell
+        assert this_in_aspell, '%s should %s' % (msg, end)
+    else:  # shouldn't be
+        assert not this_in_aspell, '%s should not %s' % (msg, end)
+
+
+def _check_err_rep(err, rep, in_aspell, fname):
+    assert ws.match(err) is None, 'error %r has whitespace' % err
+    assert comma.match(err) is None, 'error %r has a comma' % err
+    assert len(rep) > 0, ('error %s: correction %r must be non-empty'
+                          % (err, rep))
+    assert not re.match(r'^\s.*', rep), ('error %s: correction %r '
+                                         'cannot start with whitespace'
+                                         % (err, rep))
+    _check_aspell(err, 'error %r' % (err,), in_aspell[0], fname)
+    prefix = 'error %s: correction %r' % (err, rep)
+    for (r, msg) in [
+            (r'^,',
+             '%s starts with a comma'),
+            (r'\s,',
+             '%s contains a whitespace character followed by a comma'),
+            (r',\s\s',
+             '%s contains a comma followed by multiple whitespace characters'),
+            (r',[^ ]',
+             '%s contains a comma *not* followed by a space'),
+            (r'\s+$',
+             '%s has a trailing space'),
+            (r'^[^,]*,\s*$',
+             '%s has a single entry but contains a trailing comma')]:
+        assert not re.search(r, rep), (msg % (prefix,))
+    del msg
+    if rep.count(','):
+        assert rep.endswith(','), ('error %s: multiple corrections must end '
+                                   'with trailing ","' % (err,))
+    reps = [r.strip() for r in rep.lower().split(',')]
+    reps = [r for r in reps if len(r)]
+    for r in reps:
+        assert err != r.lower(), ('error %r corrects to itself amongst others'
+                                  % (err,))
+        _check_aspell(
+            r, 'error %s: correction %r' % (err, r), in_aspell[1], fname)
+    assert len(set(reps)) == len(reps), 'entries are not (lower-case) unique'
+
+
+@pytest.mark.parametrize('err, rep, match', [
+    ('a a', 'bar', 'has whitespace'),
+    ('a,a', 'bar', 'has a comma'),
+    ('a', '', 'non-empty'),
+    ('a', ' bar', 'start with whitespace'),
+    ('a', ',bar', 'starts with a comma'),
+    ('a', 'bar,bat', '.*not.*followed by a space'),
+    ('a', 'bar ', 'trailing space'),
+    ('a', 'b ,ar', 'contains a whitespace.*followed by a comma'),
+    ('a', 'bar,', 'single entry.*comma'),
+    ('a', 'bar, bat', 'must end with trailing ","'),
+    ('a', 'a, bar,', 'corrects to itself amongst others'),
+    ('a', 'a', 'corrects to itself'),
+    ('a', 'bar, bar,', 'unique'),
+])
+def test_error_checking(err, rep, match):
+    """Test that our error checking works."""
+    with pytest.raises(AssertionError, match=match):
+        _check_err_rep(err, rep, (None, None), 'dummy')
+
+
+@pytest.mark.skipif(speller is None, reason='requires aspell')
+@pytest.mark.parametrize('err, rep, err_aspell, rep_aspell, match', [
+    # This doesn't raise any exceptions, so skip for now:
+    # pytest.param('a', 'uvw, bar,', None, None, 'should be in aspell'),
+    ('abc', 'uvw, bar,', True, None, 'should be in aspell'),
+    ('a', 'uvw, bar,', False, None, 'should not be in aspell'),
+    ('a', 'abc, uvw,', None, True, 'should be in aspell'),
+    ('abc', 'uvw, bar,', True, True, 'should be in aspell'),
+    ('abc', 'uvw, bar,', False, True, 'should be in aspell'),
+    ('a', 'bar, back,', None, False, 'should not be in aspell'),
+    ('abc', 'uvw, xyz,', True, False, 'should be in aspell'),
+    ('abc', 'uvw, bar,', False, False, 'should not be in aspell'),
+])
+def test_error_checking_in_aspell(err, rep, err_aspell, rep_aspell, match):
+    """Test that our error checking works with aspell."""
+    with pytest.raises(AssertionError, match=match):
+        _check_err_rep(err, rep, (err_aspell, rep_aspell), 'dummy')
+
+
+@fname_params
+def test_dictionary_looping(fname, in_aspell):
+    """Test that all dictionary entries are valid."""
+    err_dict = dict()
+    with open(fname, 'rb') as fid:
+        for line in fid:
+            err, rep = line.decode('utf-8').split('->')
+            err = err.lower()
             assert err not in err_dict, 'error %r already exists' % err
-            assert ws.match(err) is None, 'error %r has whitespace' % err
-            assert comma.match(err) is None, 'error %r has a comma' % err
-            assert len(rep) > 0, ('error %s: correction %r must be non-empty'
-                                  % (err, rep))
-            assert not re.match(r'^\s.*', rep), ('error %s: correction %r '
-                                                 'cannot start with whitespace'
-                                                 % (err, rep))
-            prefix = 'error %s: correction %r' % (err, rep)
-            for (r, msg) in [
-                (r'^,',
-                 '%s starts with a comma'),
-                (r'\s,',
-                 '%s contains a whitespace character followed by a comma'),
-                (r',\s\s',
-                 '%s contains a comma followed by multiple whitespace '
-                 'characters'),
-                (r',[^ ]',
-                 '%s contains a comma *not* followed by a space'),
-                (r'\s+$',
-                 '%s has a trailing space'),
-                (r'^[^,]*,\s*$',
-                 '%s has a single entry but contains a trailing comma'),
-            ]:
-                assert not re.search(r, rep), (msg % (prefix,))
-            del msg
-            rep_count = rep.count(',')
-            if rep_count and not rep.endswith(','):
-                assert 'disabled' in rep.split(',')[-1], \
-                    ('currently corrections must end with trailing "," (if '
-                     ' multiple corrections are available) or have "disabled" '
-                     'in the comment')
+            rep = rep.rstrip('\n')
             reps = [r.strip() for r in rep.lower().split(',')]
             reps = [r for r in reps if len(r)]
             err_dict[err] = reps
-            unique = list()
-            for r in reps:
-                if r not in unique:
-                    unique.append(r)
-            assert reps == unique, 'entries are not (lower-case) unique'
     # check for corrections that are errors (but not self replacements)
     for err in err_dict:
         for r in err_dict[err]: