Merge pull request #2512 from tkoyama010/patch-6

interepolation->interpolation
diff --git a/.github/workflows/codespell-private.yml b/.github/workflows/codespell-private.yml
index fa9c5fa..1ad4e3b 100644
--- a/.github/workflows/codespell-private.yml
+++ b/.github/workflows/codespell-private.yml
@@ -1,21 +1,26 @@
 # GitHub Action to check our dictionary, this should only be used by the codespell project itself
 # For general usage in your repo, see the example in codespell.yml
 # https://github.com/codespell-project/codespell
-name: codespell Private Actions
+# Concurrency cancels an action on a given PR once a new commit is pushed
+name: Test Codespell
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }}
+  cancel-in-progress: true
 on: [push, pull_request]
 jobs:
   test:
     env:
       REQUIRE_ASPELL: true
     # Make sure we're using the latest aspell dictionary
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-22.04
     strategy:
+      fail-fast: false
       matrix:
         python-version:
-          - 3.6
-          - 3.7
-          - 3.8
-          - 3.9
+          - '3.7'
+          - '3.8'
+          - '3.9'
+          - '3.10'
     name: Python ${{ matrix.python-version }} test
     steps:
       - uses: actions/checkout@v3
@@ -24,14 +29,15 @@
         with:
           python-version: ${{ matrix.python-version }}
       - run: sudo apt-get install libaspell-dev aspell-en
-      - run: |
+      - name: Install dependencies
+        run: |
           python --version  # just to check
           pip install -U pip wheel # upgrade to latest pip find 3.5 wheels; wheel to avoid errors
-          pip install codecov chardet "setuptools!=47.2.0" docutils
+          pip install --upgrade codecov chardet "setuptools!=47.2.0" docutils setuptools_scm[toml]
           pip install aspell-python-py3
           pip install -e ".[dev]" # install the codespell dev packages
-      - run: python setup.py install
       - run: codespell --help
+      - run: codespell --version
       - run: make check
       - run: codespell --check-filenames --skip="./.git/*,*.pyc,./codespell_lib/tests/test_basic.py,./codespell_lib/data/*,./example/code.c,./build/lib/codespell_lib/tests/test_basic.py,./build/lib/codespell_lib/data/*,README.rst,*.egg-info/*"
       # this file has an error
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 32bfc59..5858912 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -13,4 +13,4 @@
         with:
           check_filenames: true
           # When using this Action in other repos, the --skip option below can be removed
-          skip: ./.git,./codespell_lib/data,./example/code.c,test_basic.py,*.pyc,README.rst
+          skip: "./.git,./codespell_lib/data,./example/code.c,test_basic.py,*.pyc,README.rst"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..66f131c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,56 @@
+# Upload a Python Package using Twine when a release is created
+
+name: Build
+on:
+  release:
+    types: [published]
+  push:
+    branches:
+      - master
+  pull_request:
+    branches:
+      - master
+
+permissions:
+  contents: read
+
+jobs:
+  package:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up Python
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.10'
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install build twine
+    - name: Build package
+      run: python -m build
+    - name: Check package
+      run: twine check --strict dist/*
+    - name: Check env vars
+      run: |
+        echo "Triggered by: ${{ github.event_name }}"
+    - uses: actions/upload-artifact@v3
+      with:
+        name: dist
+        path: dist
+
+  # PyPI on release
+  pypi:
+    needs: package
+    runs-on: ubuntu-latest
+    if: github.event_name == 'release'
+    steps:
+    - uses: actions/download-artifact@v3
+      with:
+        name: dist
+        path: dist
+    - name: Publish to PyPI
+      uses: pypa/gh-action-pypi-publish@release/v1
+      with:
+        user: __token__
+        password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index a0a7f4a..8b5d201 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
 *.orig
 .cache/
 .pytest_cache/
+codespell_lib/_version.py
diff --git a/Makefile b/Makefile
index c297fb0..5cbe5e0 100644
--- a/Makefile
+++ b/Makefile
@@ -45,7 +45,7 @@
 	done
 
 check-manifest:
-	check-manifest
+	check-manifest --no-build-isolation
 
 check-distutils:
 	python setup.py check --restructuredtext --strict
@@ -56,8 +56,5 @@
 pytest:
 	pytest codespell_lib
 
-pypi:
-	python setup.py sdist register upload
-
 clean:
 	rm -rf codespell.1
diff --git a/README.rst b/README.rst
index 9a3f7c7..fb5702e 100644
--- a/README.rst
+++ b/README.rst
@@ -23,7 +23,7 @@
 Requirements
 ------------
 
-Python 3.6 or above.
+Python 3.7 or above.
 
 Installation
 ------------
@@ -103,12 +103,24 @@
     count =
     quiet-level = 3
 
-This is equivalent to running::
+Codespell will also check in the current directory for a ``pyproject.toml``
+(or a path can be specified via ``--toml <filename>``) file, and the
+``[tool.codespell]`` entry will be used as long as the tomli_ package
+is installed, for example::
+
+    [tool.codespell]
+    skip = '*.po,*.ts,./src/3rdParty,./src/Test'
+    count = ''
+    quiet-level = 3
+
+These are both equivalent to running::
 
     codespell --quiet-level 3 --count --skip "*.po,*.ts,./src/3rdParty,./src/Test"
 
 Any options specified in the command line will *override* options from the
-config file.
+config files.
+
+.. _tomli: https://pypi.org/project/tomli/
 
 Dictionary format
 -----------------
diff --git a/codespell_lib/__init__.py b/codespell_lib/__init__.py
index c354bd7..019d227 100644
--- a/codespell_lib/__init__.py
+++ b/codespell_lib/__init__.py
@@ -1 +1,2 @@
-from ._codespell import main, _script_main, VERSION as __version__  # noqa
+from ._codespell import main, _script_main  # noqa
+from ._version import __version__  # noqa
diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py
index fdad69d..598aa72 100644
--- a/codespell_lib/_codespell.py
+++ b/codespell_lib/_codespell.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -26,17 +25,19 @@
 import sys
 import textwrap
 
-word_regex_def = u"[\\w\\-'’`]+"
+# autogenerated by setuptools_scm
+from ._version import __version__ as VERSION
+
+word_regex_def = "[\\w\\-'’`]+"
 # While we want to treat characters like ( or " as okay for a starting break,
 # these may occur unescaped in URIs, and so we are more restrictive on the
 # endpoint.  Emails are more restrictive, so the endpoint remains flexible.
-uri_regex_def = (u"(\\b(?:https?|[ts]?ftp|file|git|smb)://[^\\s]+(?=$|\\s)|"
-                 u"\\b[\\w.%+-]+@[\\w.-]+\\b)")
+uri_regex_def = ("(\\b(?:https?|[ts]?ftp|file|git|smb)://[^\\s]+(?=$|\\s)|"
+                 "\\b[\\w.%+-]+@[\\w.-]+\\b)")
 encodings = ('utf-8', 'iso-8859-1')
 USAGE = """
 \t%prog [OPTIONS] [file1 file2 ... fileN]
 """
-VERSION = '2.3.0.dev0'
 
 supported_languages_en = ('en', 'en_GB', 'en_US', 'en_CA', 'en_AU')
 supported_languages = supported_languages_en
@@ -81,7 +82,7 @@
 #    file1 .. fileN      Files to check spelling
 
 
-class QuietLevels(object):
+class QuietLevels:
     NONE = 0
     ENCODING = 1
     BINARY_FILE = 2
@@ -90,7 +91,7 @@
     FIXES = 16
 
 
-class GlobMatch(object):
+class GlobMatch:
     def __init__(self, pattern):
         if pattern:
             # Pattern might be a list of comma-delimited strings
@@ -109,14 +110,14 @@
         return False
 
 
-class Misspelling(object):
+class Misspelling:
     def __init__(self, data, fix, reason):
         self.data = data
         self.fix = fix
         self.reason = reason
 
 
-class TermColors(object):
+class TermColors:
     def __init__(self):
         self.FILE = '\033[33m'
         self.WWORD = '\033[31m'
@@ -130,7 +131,7 @@
         self.DISABLE = ''
 
 
-class Summary(object):
+class Summary:
     def __init__(self):
         self.summary = {}
 
@@ -150,7 +151,7 @@
                          width=15 - len(key)) for key in keys])
 
 
-class FileOpener(object):
+class FileOpener:
     def __init__(self, use_chardet, quiet_level):
         self.use_chardet = use_chardet
         if use_chardet:
@@ -200,30 +201,27 @@
         return lines, encoding
 
     def open_with_internal(self, filename):
-        curr = 0
-        while True:
-            try:
-                f = codecs.open(filename, 'r', encoding=encodings[curr])
-            except UnicodeDecodeError:
-                if not self.quiet_level & QuietLevels.ENCODING:
-                    print("WARNING: Decoding file using encoding=%s failed: %s"
-                          % (encodings[curr], filename,), file=sys.stderr)
-                    try:
-                        print("WARNING: Trying next encoding %s"
-                              % encodings[curr + 1], file=sys.stderr)
-                    except IndexError:
-                        pass
-
-                curr += 1
-            else:
-                lines = f.readlines()
-                f.close()
-                break
-        if not lines:
+        encoding = None
+        first_try = True
+        for encoding in encodings:
+            if first_try:
+                first_try = False
+            elif not self.quiet_level & QuietLevels.ENCODING:
+                print("WARNING: Trying next encoding %s"
+                      % encoding, file=sys.stderr)
+            with codecs.open(filename, 'r', encoding=encoding) as f:
+                try:
+                    lines = f.readlines()
+                except UnicodeDecodeError:
+                    if not self.quiet_level & QuietLevels.ENCODING:
+                        print("WARNING: Decoding file using encoding=%s "
+                              "failed: %s" % (encoding, filename,),
+                              file=sys.stderr)
+                else:
+                    break
+        else:
             raise Exception('Unknown encoding')
 
-        encoding = encodings[curr]
-
         return lines, encoding
 
 # -.-:-.-:-.-:-.:-.-:-.-:-.-:-.-:-.:-.-:-.-:-.-:-.-:-.:-.-:-
@@ -238,7 +236,7 @@
     def _split_lines(self, text, width):
         parts = text.split('\n')
         out = list()
-        for pi, part in enumerate(parts):
+        for part in parts:
             # Eventually we could allow others...
             indent_start = '- '
             if part.startswith(indent_start):
@@ -392,7 +390,8 @@
                         help='print LINES of surrounding context')
     parser.add_argument('--config', type=str,
                         help='path to config file.')
-
+    parser.add_argument('--toml', type=str,
+                        help='path to a pyproject.toml file.')
     parser.add_argument('files', nargs='*',
                         help='files or directories to check')
 
@@ -404,6 +403,26 @@
     if options.config:
         cfg_files.append(options.config)
     config = configparser.ConfigParser()
+
+    # Read toml before other config files.
+    toml_files_errors = list()
+    if os.path.isfile('pyproject.toml'):
+        toml_files_errors.append(('pyproject.toml', False))
+    if options.toml:
+        toml_files_errors.append((options.toml, True))
+    for toml_file, raise_error in toml_files_errors:
+        try:
+            import tomli
+        except Exception as exc:
+            if raise_error:
+                raise ImportError(
+                    f'tomli is required to read pyproject.toml but could not '
+                    f'be imported, got: {exc}') from None
+            else:
+                continue
+        with open(toml_file, 'rb') as f:
+            data = tomli.load(f).get('tool', {})
+        config.read_dict(data)
     config.read(cfg_files)
 
     if config.has_section('codespell'):
@@ -439,7 +458,7 @@
 
 
 def build_exclude_hashes(filename, exclude_lines):
-    with codecs.open(filename, 'r') as f:
+    with codecs.open(filename, mode='r', encoding='utf-8') as f:
         for line in f:
             exclude_lines.add(line)
 
diff --git a/codespell_lib/data/dictionary.txt b/codespell_lib/data/dictionary.txt
index c964f3e..80e6a3a 100644
--- a/codespell_lib/data/dictionary.txt
+++ b/codespell_lib/data/dictionary.txt
@@ -497,7 +497,6 @@
 accrediation->accreditation
 accredidation->accreditation
 accress->access
-accreting->accrediting
 accroding->according
 accrodingly->accordingly
 accronym->acronym
@@ -1923,6 +1922,10 @@
 anaylses->analyses
 anaylsis->analysis
 anaylsises->analysises
+anayltic->analytic
+anayltical->analytical
+anayltically->analytically
+anayltics->analytics
 anaylze->analyze
 anaylzed->analyzed
 anaylzer->analyzer
@@ -6679,6 +6682,8 @@
 combind->combined
 combinded->combined
 combinine->combine
+combintaion->combination
+combintaions->combinations
 combusion->combustion
 comceptually->conceptually
 comdemnation->condemnation
@@ -15330,7 +15335,6 @@
 falshes->flashes
 falshing->flashing
 falsly->falsely
-falsy->falsely, false,
 falt->fault
 falure->failure
 familar->familiar
@@ -19213,8 +19217,12 @@
 instutionalized->institutionalized
 instutions->intuitions
 insue->ensue, insure,
+insufficency->insufficiency
 insufficent->insufficient
+insufficently->insufficiently
 insuffiency->insufficiency
+insuffient->insufficient
+insuffiently->insufficiently
 insurasnce->insurance
 insurence->insurance
 intaces->instance
@@ -27973,6 +27981,7 @@
 regons->regions
 regorded->recorded
 regresion->regression
+regresison->regression
 regresssion->regression
 regrigerator->refrigerator
 regsion->region
@@ -31741,6 +31750,7 @@
 spefiying->specifying
 spefy->specify
 spefying->specifying
+speherical->spherical
 speical->special
 speices->species
 speicfied->specified
@@ -31766,6 +31776,7 @@
 speperats->separates
 sperate->separate
 sperately->separately
+sperhical->spherical
 spermatozoan->spermatozoon
 speshal->special
 speshally->specially, especially,
@@ -32471,6 +32482,8 @@
 substancial->substantial
 substantialy->substantially
 substantivly->substantively
+substask->subtask
+substasks->subtasks
 substatial->substantial
 substential->substantial
 substentially->substantially
@@ -32508,6 +32521,8 @@
 subsytem->subsystem
 subsytems->subsystems
 subtabels->subtables
+subtak->subtask
+subtaks->subtask, subtasks,
 subtances->substances
 subtarger->subtarget, sub-target,
 subtargers->subtargets, sub-targets,
@@ -32519,6 +32534,8 @@
 subtitution->substitution
 subtitutions->substitutions
 subtrafuge->subterfuge
+subtrate->substrate
+subtrates->substrates
 subtring->substring
 subtrings->substrings
 subtsitutable->substitutable
@@ -32655,11 +32672,17 @@
 sufficates->suffocates
 sufficating->suffocating
 suffication->suffocation
+sufficency->sufficiency
 sufficent->sufficient
 sufficently->sufficiently
+sufficiancy->sufficiency
 sufficiant->sufficient
+sufficiantly->sufficiently
 sufficiennt->sufficient
 sufficienntly->sufficiently
+suffiency->sufficiency
+suffient->sufficient
+suffiently->sufficiently
 suffisticated->sophisticated
 suficate->suffocate
 suficated->suffocated
@@ -34087,7 +34110,7 @@
 toolar->toolbar
 toolsbox->toolbox
 toom->tomb
-tooo->todo
+tooo->todo, too, tool, took,
 toos->tools
 tootonic->teutonic
 topicaizer->topicalizer
diff --git a/codespell_lib/data/dictionary_code.txt b/codespell_lib/data/dictionary_code.txt
index 29ee050..174a414 100644
--- a/codespell_lib/data/dictionary_code.txt
+++ b/codespell_lib/data/dictionary_code.txt
@@ -21,6 +21,7 @@
 endcode->encode
 errorstring->error string
 exitst->exits, exists,
+falsy->falsely, false,
 files'->file's
 gae->game, Gael, gale,
 hist->heist, his,
diff --git a/codespell_lib/data/dictionary_rare.txt b/codespell_lib/data/dictionary_rare.txt
index a6e2074..814348e 100644
--- a/codespell_lib/data/dictionary_rare.txt
+++ b/codespell_lib/data/dictionary_rare.txt
@@ -1,4 +1,5 @@
 ablet->able, tablet,
+accreting->accrediting
 afterwords->afterwards
 amination->animation, lamination,
 aminations->animations, laminations,
@@ -16,7 +17,7 @@
 bloc->block
 blocs->blocks
 bodgy->body
-bu->by
+bu->by, be, but, bug, bun, bud, buy, bum,
 buss->bus
 busses->buses
 calculatable->calculable
diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py
index 64beddd..8b73584 100644
--- a/codespell_lib/tests/test_basic.py
+++ b/codespell_lib/tests/test_basic.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import contextlib
 import inspect
 import os
@@ -22,7 +20,7 @@
     assert EX_DATAERR == 65
 
 
-class MainWrapper(object):
+class MainWrapper:
     """Compatibility wrapper for when we used to return the count."""
 
     def main(self, *args, count=True, std=False, **kwargs):
@@ -174,7 +172,7 @@
         with FakeStdin('0\n'):  # blank input -> nothing
             assert cs.main('-w', '-i', '3', f.name) == 0
         assert cs.main(f.name) == 0
-        with open(f.name, 'r') as f_read:
+        with open(f.name) as f_read:
             assert f_read.read() == 'awkward\n'
         with open(f.name, 'w') as f:
             f.write('ackward\n')
@@ -184,7 +182,7 @@
             assert code == 0
         assert 'a valid option' in stdout
         assert cs.main(f.name) == 0
-        with open(f.name, 'r') as f:
+        with open(f.name) as f:
             assert f.read() == 'backward\n'
     finally:
         os.remove(f.name)
@@ -249,11 +247,11 @@
     """Test exclude file functionality."""
     d = str(tmpdir)
     with open(op.join(d, 'bad.txt'), 'wb') as f:
-        f.write('1 abandonned 1\n2 abandonned 2\n'.encode('utf-8'))
+        f.write(b'1 abandonned 1\n2 abandonned 2\n')
     bad_name = f.name
     assert cs.main(bad_name) == 2
     with open(op.join(d, 'tmp.txt'), 'wb') as f:
-        f.write('1 abandonned 1\n'.encode('utf-8'))
+        f.write(b'1 abandonned 1\n')
     assert cs.main(bad_name) == 2
     assert cs.main('-x', f.name, bad_name) == 1
 
@@ -266,12 +264,25 @@
     # with CaptureStdout() as sio:
     assert cs.main(f.name) == 0
     with open(f.name, 'wb') as f:
-        f.write(u'naïve\n'.encode('utf-8'))
+        f.write('naïve\n'.encode())
     assert cs.main(f.name) == 0
     assert cs.main('-e', f.name) == 0
     with open(f.name, 'ab') as f:
-        f.write(u'naieve\n'.encode('utf-8'))
+        f.write(b'naieve\n')
     assert cs.main(f.name) == 1
+    # Encoding detection (only try ISO 8859-1 because UTF-8 is the default)
+    with open(f.name, 'wb') as f:
+        f.write(b'Speling error, non-ASCII: h\xe9t\xe9rog\xe9n\xe9it\xe9\n')
+    # check warnings about wrong encoding are enabled with "-q 0"
+    code, stdout, stderr = cs.main('-q', '0', f.name, std=True, count=True)
+    assert code == 1
+    assert 'Speling' in stdout
+    assert 'iso-8859-1' in stderr
+    # check warnings about wrong encoding are disabled with "-q 1"
+    code, stdout, stderr = cs.main('-q', '1', f.name, std=True, count=True)
+    assert code == 1
+    assert 'Speling' in stdout
+    assert 'iso-8859-1' not in stderr
     # Binary file warning
     with open(f.name, 'wb') as f:
         f.write(b'\x00\x00naiive\x00\x00')
@@ -382,7 +393,7 @@
     # with CaptureStdout() as sio:
     assert cs.main(f.name) == 0
     with open(f.name, 'wb') as f:
-        f.write('this has an ACII error'.encode('utf-8'))
+        f.write(b'this has an ACII error')
     code, stdout, _ = cs.main(f.name, std=True)
     assert code == 1
     assert 'ASCII' in stdout
@@ -791,35 +802,55 @@
         assert not uri_regex.findall(boilerplate % uri), uri
 
 
-def test_config(tmpdir, capsys):
-    """
-    Tests loading options from a config file.
-    """
-    d = str(tmpdir)
-
-    # Create sample files.
-    with open(op.join(d, 'bad.txt'), 'w') as f:
+@pytest.mark.parametrize('kind', ('toml', 'cfg'))
+def test_config_toml(tmp_path, capsys, kind):
+    """Test loading options from a config file or toml."""
+    d = tmp_path / 'files'
+    d.mkdir()
+    with open(d / 'bad.txt', 'w') as f:
         f.write('abandonned donn\n')
-    with open(op.join(d, 'good.txt'), 'w') as f:
+    with open(d / 'good.txt', 'w') as f:
         f.write("good")
 
-    # Create a config file.
-    conffile = op.join(d, 'config.cfg')
-    with open(conffile, 'w') as f:
-        f.write(
-            '[codespell]\n'
-            'skip = bad.txt\n'
-            'count = \n'
-        )
-
     # Should fail when checking both.
-    code, stdout, _ = cs.main(d, count=True, std=True)
+    code, stdout, _ = cs.main(str(d), count=True, std=True)
     # Code in this case is not exit code, but count of misspellings.
     assert code == 2
     assert 'bad.txt' in stdout
 
+    if kind == 'cfg':
+        conffile = str(tmp_path / 'setup.cfg')
+        args = ('--config', conffile)
+        with open(conffile, 'w') as f:
+            f.write("""\
+[codespell]
+skip = bad.txt, whatever.txt
+count =
+""")
+    else:
+        assert kind == 'toml'
+        pytest.importorskip('tomli')
+        tomlfile = str(tmp_path / 'pyproject.toml')
+        args = ('--toml', tomlfile)
+        with open(tomlfile, 'w') as f:
+            f.write("""\
+[tool.codespell]
+skip = 'bad.txt,whatever.txt'
+count = false
+""")
+
     # Should pass when skipping bad.txt
-    code, stdout, _ = cs.main('--config', conffile, d, count=True, std=True)
+    code, stdout, _ = cs.main(str(d), *args, count=True, std=True)
+    assert code == 0
+    assert 'bad.txt' not in stdout
+
+    # And both should automatically work if they're in cwd
+    cwd = os.getcwd()
+    try:
+        os.chdir(tmp_path)
+        code, stdout, _ = cs.main(str(d), count=True, std=True)
+    finally:
+        os.chdir(cwd)
     assert code == 0
     assert 'bad.txt' not in stdout
 
diff --git a/codespell_lib/tests/test_dictionary.py b/codespell_lib/tests/test_dictionary.py
index 6d246d3..9f3dec0 100644
--- a/codespell_lib/tests/test_dictionary.py
+++ b/codespell_lib/tests/test_dictionary.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
 import glob
 import os.path as op
 import os
@@ -41,9 +39,9 @@
 
 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')))
+    doc_fnames = {op.basename(f[0]) for f in _fnames_in_aspell}
+    got_fnames = {op.basename(f)
+                  for f in glob.glob(op.join(_data_dir, '*.txt'))}
     assert doc_fnames == got_fnames
 
 
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..431834f
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,68 @@
+# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
+
+[project]
+name = "codespell"
+description = "Codespell"
+readme = { file = "README.rst", content-type = "text/x-rst" }
+requires-python = ">=3.7"
+license = {text = "GPL v2"}
+authors = [
+    {name = "Lucas De Marchi", email = "lucas.de.marchi@gmail.com"},
+]
+classifiers = [
+    "Intended Audience :: Developers",
+    "License :: OSI Approved",
+    "Programming Language :: Python",
+    "Topic :: Software Development",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS"
+]
+dependencies = []
+dynamic = ["version"]
+
+[project.optional-dependencies]
+dev = [
+    "check-manifest",
+    "flake8",
+    "pytest",
+    "pytest-cov",
+    "pytest-dependency",
+    "tomli"
+]
+hard-encoding-detection = [
+    "chardet"
+]
+toml = [
+    "tomli; python_version < '3.11'"
+]
+
+[project.scripts]
+codespell = "codespell_lib:_script_main"
+
+[project.urls]
+homepage = "https://github.com/codespell-project/codespell"
+repository = "https://github.com/codespell-project/codespell"
+
+[build-system]
+requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "codespell_lib/_version.py"
+
+[tool.setuptools.packages.find]
+exclude = [
+    "snap",
+    "dist"
+]
+
+[tool.setuptools.package-data]
+codespell_lib = [
+    "data/dictionary*.txt",
+    "data/linux-kernel.exclude"
+]
+
+[tool.check-manifest]
+ignore = ["codespell_lib/_version.py"]
diff --git a/setup.cfg b/setup.cfg
index d46869c..7074b31 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [tool:pytest]
-addopts = --cov=codespell_lib --showlocals -rs --cov-report=
+addopts = --cov=codespell_lib -rs --cov-report= --tb=short
 
 [flake8]
 exclude = build, ci-helpers
-ignore =
+ignore = W503
diff --git a/setup.py b/setup.py
index 8a6c8c7..4fe2f90 100755
--- a/setup.py
+++ b/setup.py
@@ -1,66 +1,6 @@
 #! /usr/bin/env python
 
-# adapted from mne-python
-
-import os
-
 from setuptools import setup
 
-from codespell_lib import __version__
-
-DISTNAME = 'codespell'
-DESCRIPTION = """Codespell"""
-MAINTAINER = 'Lucas De Marchi'
-MAINTAINER_EMAIL = 'lucas.de.marchi@gmail.com'
-URL = 'https://github.com/codespell-project/codespell/'
-LICENSE = 'GPL v2'
-DOWNLOAD_URL = 'https://github.com/codespell-project/codespell/'
-with open('README.rst', 'r') as f:
-    LONG_DESCRIPTION = f.read()
-
 if __name__ == "__main__":
-    if os.path.exists('MANIFEST'):
-        os.remove('MANIFEST')
-
-    setup(name=DISTNAME,
-          maintainer=MAINTAINER,
-          include_package_data=True,
-          maintainer_email=MAINTAINER_EMAIL,
-          description=DESCRIPTION,
-          license=LICENSE,
-          url=URL,
-          version=__version__,
-          download_url=DOWNLOAD_URL,
-          long_description=LONG_DESCRIPTION,
-          long_description_content_type='text/x-rst',
-          zip_safe=False,
-          classifiers=['Intended Audience :: Developers',
-                       'License :: OSI Approved',
-                       'Programming Language :: Python',
-                       'Topic :: Software Development',
-                       'Operating System :: Microsoft :: Windows',
-                       'Operating System :: POSIX',
-                       'Operating System :: Unix',
-                       'Operating System :: MacOS'],
-          platforms='any',
-          python_requires='>=3.6',
-          packages=[
-              'codespell_lib',
-              'codespell_lib.tests',
-              'codespell_lib.data',
-          ],
-          package_data={'codespell_lib': [
-              os.path.join('data', 'dictionary*.txt'),
-              os.path.join('data', 'linux-kernel.exclude'),
-          ]},
-          entry_points={
-              'console_scripts': [
-                  'codespell = codespell_lib:_script_main'
-              ],
-          },
-          extras_require={
-              "dev": ["check-manifest", "flake8", "pytest", "pytest-cov",
-                      "pytest-dependency"],
-              "hard-encoding-detection": ["chardet"],
-          }
-          )
+    setup()