Merge pull request #62 from Kijewski/pr-issue-61

 Fix typing for _SupportsWrite
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a9e5cb9..39d6842 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,16 +11,9 @@
           - ubuntu-latest
           - macos-latest
           - windows-latest
-        python: [
-          '3.5',
-          # '3.6', '3.7', '3.8', '3.9', # it takes too much GitHub action time to run tests on all versions in between
-          '3.10',
-        ]
-        include:
-          - os: ubuntu-latest
-            python: pypy-3.7
-        
-    name: Python ${{ matrix.python }} on ${{ matrix.os }}
+        python:
+          - '3.8'
+          - '3.11'
 
     steps:
       - uses: actions/checkout@v3
@@ -56,3 +49,38 @@
 
       - name: Run "JSON is a Minefield" suite
         run: python run-minefield-test.py
+
+  lint:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+
+      - name: Cache pip
+        uses: actions/cache@v3
+        with:
+          key: lint--${{ hashFiles('./requirements*.txt', './Makefile') }}
+          restore-keys: lint--
+          path: ~/.cache/pip
+
+      - name: Setup python
+        uses: actions/setup-python@v4
+        with:
+          python-version: '3.11'
+
+      - name: Display Python version
+        run: python -c 'import sys; print(sys.version)'
+
+      - name: Update pip
+        run: python -m pip install -U pip wheel setuptools
+
+      - name: Install requirements
+        run: python -m pip install -Ur requirements-dev.txt
+
+      - name: Compile project
+        run: make install
+
+      - name: Run black
+        run: python -m black --check ./*.py ./src/
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index a485588..8e4b9bf 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -31,7 +31,7 @@
     - name: Cache pip
       uses: actions/cache@v3
       with:
-        key: codeql-analysis--${{ github.event.inputs.os }}--${{ github.event.inputs.python }}--${{ hashFiles('./requirements.txt') }}
+        key: codeql-analysis--${{ github.event.inputs.os }}--${{ github.event.inputs.python }}--${{ hashFiles('./requirements-dev.txt') }}
         path: ~/.cache/pip
 
     - name: Setup python
@@ -48,7 +48,7 @@
       run: python -m pip install -U pip wheel setuptools
 
     - name: Install requirements
-      run: python -m pip install -Ur requirements.txt
+      run: python -m pip install -Ur requirements-dev.txt
 
     - name: Compile
       run: make bdist_wheel
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62d76dc..8a21332 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+**1.6.3**
+
+* Fix typing for `dump()` ([#61](https://github.com/Kijewski/pyjson5/issues/61))
+
 **1.6.2**
 
 * Update to Unicode 15.0.0
diff --git a/Makefile b/Makefile
index 54a0a3a..dbbc752 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,8 @@
-all: sdist bdist_wheel docs
+all: sdist wheel docs
 
 .DELETE_ON_ERROR:
 
-.PHONY: all sdist bdist_wheel clean docs prepare test install
+.PHONY: all sdist wheel clean docs prepare test install
 
 export PYTHONUTF8 := 1
 export PYTHONIOENCODING := UTF-8
@@ -34,14 +34,14 @@
 prepare: pyjson5.cpp ${FILES}
 
 sdist: prepare
-	rm -f -- dist/pyjson5-*.tar.gz
-	python setup.py sdist
+	-rm -- dist/pyjson5-*.tar.gz
+	python -m build --sdist
 
-bdist_wheel: pyjson5.cpp ${FILES} | sdist
-	rm -f -- dist/pyjson5-*.whl
-	python setup.py bdist_wheel
+wheel: prepare
+	-rm -- dist/pyjson5-*.whl
+	python -m build --wheel
 
-install: bdist_wheel
+install: wheel
 	pip install --force dist/pyjson5-*.whl
 
 docs: install $(wildcard docs/* docs/*/*)
@@ -51,9 +51,9 @@
 	[ ! -d build/ ] || rm -r -- build/
 	[ ! -d dist/ ] || rm -r -- dist/
 	[ ! -d pyjson5.egg-info/ ] || rm -r -- pyjson5.egg-info/
-	rm -f -- pyjson5.*.so python5.cpp
+	-rm -- pyjson5.*.so python5.cpp
 
-test: bdist_wheel
+test: wheel
 	pip install --force dist/pyjson5-*.whl
 	python run-minefield-test.py
 	python run-tests.py
diff --git a/docs/conf.py b/docs/conf.py
index 9f3ba59..3b9a85b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,13 +16,15 @@
     'myst_parser',
 ]
 
+language = "en"
+
 templates_path = ['_templates']
 source_suffix = '.rst'
 master_doc = 'index'
 
-project = u'PyJSON5'
-copyright = u'2018-2022, René Kijewski'
-author = u'René Kijewski'
+project = 'PyJSON5'
+copyright = '2018-2023, René Kijewski'
+author = 'René Kijewski'
 
 with open('../src/VERSION.inc', 'rt') as f:
     version = eval(f.read().strip())
@@ -33,41 +35,15 @@
 pygments_style = 'sphinx'
 todo_include_todos = False
 
-html_theme = 'sphinx_rtd_theme'
-html_theme_options = {
-    'navigation_depth': -1,
-}
-html_sidebars = {
-    '**': [
-        'localtoc.html',
-        'searchbox.html',
-    ]
-}
+html_theme = 'furo'
 htmlhelp_basename = 'PyJSON5doc'
 
-latex_elements = {}
-latex_documents = [
-    (master_doc, 'PyJSON5.tex', u'PyJSON5 Documentation',
-     u'René Kijewski', 'manual'),
-]
-
-man_pages = [
-    (master_doc, 'pyjson5', u'PyJSON5 Documentation',
-     [author], 1)
-]
-
-texinfo_documents = [
-    (master_doc, 'PyJSON5', u'PyJSON5 Documentation',
-     author, 'PyJSON5', 'One line description of project.',
-     'Miscellaneous'),
-]
-
 display_toc = True
 autodoc_default_flags = ['members']
 autosummary_generate = True
 
 intersphinx_mapping = {
-    'python': ('https://docs.python.org/3.10', None),
+    'python': ('https://docs.python.org/3.11', None),
 }
 
 inheritance_graph_attrs = {
diff --git a/docs/decoder.rst b/docs/decoder.rst
index 3f958e7..057791f 100644
--- a/docs/decoder.rst
+++ b/docs/decoder.rst
@@ -59,26 +59,26 @@
     pyjson5.Json5ExtraData
     pyjson5.Json5IllegalType
 
-.. autoclass:: pyjson5.Json5DecoderException
+.. autoexception:: pyjson5.Json5DecoderException
     :members:
     :inherited-members:
 
-.. autoclass:: pyjson5.Json5NestingTooDeep
+.. autoexception:: pyjson5.Json5NestingTooDeep
     :members:
     :inherited-members:
 
-.. autoclass:: pyjson5.Json5EOF
+.. autoexception:: pyjson5.Json5EOF
     :members:
     :inherited-members:
 
-.. autoclass:: pyjson5.Json5IllegalCharacter
+.. autoexception:: pyjson5.Json5IllegalCharacter
     :members:
     :inherited-members:
 
-.. autoclass:: pyjson5.Json5ExtraData
+.. autoexception:: pyjson5.Json5ExtraData
     :members:
     :inherited-members:
 
-.. autoclass:: pyjson5.Json5IllegalType
+.. autoexception:: pyjson5.Json5IllegalType
     :members:
     :inherited-members:
diff --git a/docs/encoder.rst b/docs/encoder.rst
index 4ca15a3..4b8f68b 100644
--- a/docs/encoder.rst
+++ b/docs/encoder.rst
@@ -64,10 +64,10 @@
     pyjson5.Json5EncoderException
     pyjson5.Json5UnstringifiableType
 
-.. autoclass:: pyjson5.Json5EncoderException
+.. autoexception:: pyjson5.Json5EncoderException
     :members:
     :inherited-members:
 
-.. autoclass:: pyjson5.Json5UnstringifiableType
+.. autoexception:: pyjson5.Json5UnstringifiableType
     :members:
     :inherited-members:
diff --git a/docs/exceptions.rst b/docs/exceptions.rst
index 21b9f34..ff50c4a 100644
--- a/docs/exceptions.rst
+++ b/docs/exceptions.rst
@@ -12,6 +12,6 @@
     pyjson5.Json5ExtraData
     pyjson5.Json5IllegalType
 
-.. autoclass:: pyjson5.Json5Exception
+.. autoexception:: pyjson5.Json5Exception
     :members:
     :inherited-members:
diff --git a/make_decoder_recursive_select.py b/make_decoder_recursive_select.py
index f26d596..3f3ab18 100755
--- a/make_decoder_recursive_select.py
+++ b/make_decoder_recursive_select.py
@@ -8,54 +8,58 @@
 
 
 def generate(out):
-    lst = ['DRS_fail'] * 128
-    lst[ord('n')] = 'DRS_null'
-    lst[ord('t')] = 'DRS_true'
-    lst[ord('f')] = 'DRS_false'
-    lst[ord('I')] = 'DRS_inf'
-    lst[ord('N')] = 'DRS_nan'
-    lst[ord('"')] = 'DRS_string'
-    lst[ord("'")] = 'DRS_string'
-    lst[ord('{')] = 'DRS_recursive'
-    lst[ord('[')] = 'DRS_recursive'
-    for c in '+-.0123456789':
-        lst[ord(c)] = 'DRS_number'
+    lst = ["DRS_fail"] * 128
+    lst[ord("n")] = "DRS_null"
+    lst[ord("t")] = "DRS_true"
+    lst[ord("f")] = "DRS_false"
+    lst[ord("I")] = "DRS_inf"
+    lst[ord("N")] = "DRS_nan"
+    lst[ord('"')] = "DRS_string"
+    lst[ord("'")] = "DRS_string"
+    lst[ord("{")] = "DRS_recursive"
+    lst[ord("[")] = "DRS_recursive"
+    for c in "+-.0123456789":
+        lst[ord(c)] = "DRS_number"
 
-    print('#ifndef JSON5EncoderCpp_decoder_recursive_select', file=out)
-    print('#define JSON5EncoderCpp_decoder_recursive_select', file=out)
+    print("#ifndef JSON5EncoderCpp_decoder_recursive_select", file=out)
+    print("#define JSON5EncoderCpp_decoder_recursive_select", file=out)
     print(file=out)
-    print('// GENERATED FILE', file=out)
-    print('// All changes will be lost.', file=out)
+    print("// GENERATED FILE", file=out)
+    print("// All changes will be lost.", file=out)
     print(file=out)
-    print('#include <cstdint>', file=out)
+    print("#include <cstdint>", file=out)
     print(file=out)
-    print('namespace JSON5EncoderCpp {', file=out)
-    print('inline namespace {', file=out)
+    print("namespace JSON5EncoderCpp {", file=out)
+    print("inline namespace {", file=out)
     print(file=out)
-    print('enum DrsKind : std::uint8_t {', file=out)
-    print('    DRS_fail, DRS_null, DRS_true, DRS_false, DRS_inf, DRS_nan, DRS_string, DRS_number, DRS_recursive', file=out)
-    print('};', file=out)
+    print("enum DrsKind : std::uint8_t {", file=out)
+    print(
+        "    DRS_fail, DRS_null, DRS_true, DRS_false, DRS_inf, DRS_nan, DRS_string, DRS_number, DRS_recursive",
+        file=out,
+    )
+    print("};", file=out)
     print(file=out)
-    print('static const DrsKind drs_lookup[128] = {', file=out)
+    print("static const DrsKind drs_lookup[128] = {", file=out)
     for chunk in chunked(lst, 8):
-        print('   ', end='', file=out)
+        print("   ", end="", file=out)
         for t in chunk:
-            print(' ', t, ',', sep='', end='', file=out)
+            print(" ", t, ",", sep="", end="", file=out)
         print(file=out)
-    print('};', file=out)
+    print("};", file=out)
     print(file=out)
-    print('}  // anonymous inline namespace', sep='', file=out)
-    print('}  // namespace JSON5EncoderCpp', sep='', file=out)
+    print("}  // anonymous inline namespace", sep="", file=out)
+    print("}  // namespace JSON5EncoderCpp", sep="", file=out)
     print(file=out)
-    print('#endif', sep='', file=out)
+    print("#endif", sep="", file=out)
 
 
-argparser = ArgumentParser(description='Generate src/_decoder_recursive_select.hpp')
-argparser.add_argument('input', nargs='?', type=Path, default=Path('src/_decoder_recursive_select.hpp'))
+argparser = ArgumentParser(description="Generate src/_decoder_recursive_select.hpp")
+argparser.add_argument(
+    "input", nargs="?", type=Path, default=Path("src/_decoder_recursive_select.hpp")
+)
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     basicConfig(level=DEBUG)
     args = argparser.parse_args()
-    with open(str(args.input.resolve()), 'wt') as out:
+    with open(str(args.input.resolve()), "wt") as out:
         generate(out)
-
diff --git a/make_escape_dct.py b/make_escape_dct.py
index 2c0669f..01ddd0d 100755
--- a/make_escape_dct.py
+++ b/make_escape_dct.py
@@ -7,47 +7,56 @@
 
 def generate(f):
     unescaped = 0
-    print('const EscapeDct::Items EscapeDct::items = {', file=f)
+    print("const EscapeDct::Items EscapeDct::items = {", file=f)
     for c in range(0x100):
-        if c == ord('\\'):
-            s = '\\\\'
-        elif c == ord('\b'):
-            s = '\\b'
-        elif c == ord('\f'):
-            s = '\\f'
-        elif c == ord('\n'):
-            s = '\\n'
-        elif c == ord('\r'):
-            s = '\\r'
-        elif c == ord('\t'):
-            s = '\\t'
+        if c == ord("\\"):
+            s = "\\\\"
+        elif c == ord("\b"):
+            s = "\\b"
+        elif c == ord("\f"):
+            s = "\\f"
+        elif c == ord("\n"):
+            s = "\\n"
+        elif c == ord("\r"):
+            s = "\\r"
+        elif c == ord("\t"):
+            s = "\\t"
         elif c == ord('"'):
             s = '\\"'
-        elif (c < 0x20) or (c >= 0x7f) or (chr(c) in "'&<>\\"):
-            s = f'\\u{c:04x}'
+        elif (c < 0x20) or (c >= 0x7F) or (chr(c) in "'&<>\\"):
+            s = f"\\u{c:04x}"
         else:
-            s = f'{c:c}'
+            s = f"{c:c}"
             if c < 128:
                 unescaped |= 1 << c
 
-        t = [str(len(s))] + [
-            f"'{c}'" if c != '\\' else f"'\\\\'"
-            for c in s
-        ] + ['0'] * 6
-        l = ', '.join(t[:8])
-        print(f'   {{ {l:35s} }},  /* 0x{c:02x} {chr(c)!r} */', file=f)
-    print('};', file=f)
+        t = (
+            [str(len(s))]
+            + [f"'{c}'" if c != "\\" else f"'\\\\'" for c in s]
+            + ["0"] * 6
+        )
+        l = ", ".join(t[:8])
+        print(f"   {{ {l:35s} }},  /* 0x{c:02x} {chr(c)!r} */", file=f)
+    print("};", file=f)
 
     escaped = unescaped ^ ((1 << 128) - 1)
-    print(f'const std::uint64_t EscapeDct::is_escaped_lo = UINT64_C(0x{(escaped & ((1 << 64) - 1)):016x});', file=f)
-    print(f'const std::uint64_t EscapeDct::is_escaped_hi = UINT64_C(0x{(escaped >> 64):016x});', file=f)
+    print(
+        f"const std::uint64_t EscapeDct::is_escaped_lo = UINT64_C(0x{(escaped & ((1 << 64) - 1)):016x});",
+        file=f,
+    )
+    print(
+        f"const std::uint64_t EscapeDct::is_escaped_hi = UINT64_C(0x{(escaped >> 64):016x});",
+        file=f,
+    )
 
 
-argparser = ArgumentParser(description='Generate src/_escape_dct.hpp')
-argparser.add_argument('input', nargs='?', type=Path, default=Path('src/_escape_dct.hpp'))
+argparser = ArgumentParser(description="Generate src/_escape_dct.hpp")
+argparser.add_argument(
+    "input", nargs="?", type=Path, default=Path("src/_escape_dct.hpp")
+)
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     basicConfig(level=DEBUG)
     args = argparser.parse_args()
-    with open(str(args.input.resolve()), 'wt') as out:
+    with open(str(args.input.resolve()), "wt") as out:
         generate(out)
diff --git a/make_unicode_categories.py b/make_unicode_categories.py
index fe8eba2..23feb63 100755
--- a/make_unicode_categories.py
+++ b/make_unicode_categories.py
@@ -16,26 +16,24 @@
     IdentifierPart = 3
 
     cat_indices = {
-        'zs': WhiteSpace,
-
-        'lc': IdentifierStart,
-        'll': IdentifierStart,
-        'lm': IdentifierStart,
-        'lo': IdentifierStart,
-        'lt': IdentifierStart,
-        'lu': IdentifierStart,
-        'nl': IdentifierStart,
-
-        'mc': IdentifierPart,
-        'mn': IdentifierPart,
-        'pc': IdentifierPart,
-        'nd': IdentifierPart,
+        "zs": WhiteSpace,
+        "lc": IdentifierStart,
+        "ll": IdentifierStart,
+        "lm": IdentifierStart,
+        "lo": IdentifierStart,
+        "lt": IdentifierStart,
+        "lu": IdentifierStart,
+        "nl": IdentifierStart,
+        "mc": IdentifierPart,
+        "mn": IdentifierPart,
+        "pc": IdentifierPart,
+        "nd": IdentifierPart,
     }
 
     planes = defaultdict(lambda: [0] * 0x10000)
 
     for input_line in input_file:
-        m = match(r'^([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+([A-Z][a-z])', input_line)
+        m = match(r"^([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s+;\s+([A-Z][a-z])", input_line)
         if not m:
             continue
         start, end, cat = m.groups()
@@ -48,75 +46,91 @@
                 planes[i // 0x10000][i % 0x10000] = idx
 
     # per: https://spec.json5.org/#white-space
-    for i in (0x9, 0xa, 0xb, 0xc, 0xd, 0x20, 0xa0, 0x2028, 0x2028, 0x2029, 0xfeff):
+    for i in (0x9, 0xA, 0xB, 0xC, 0xD, 0x20, 0xA0, 0x2028, 0x2028, 0x2029, 0xFEFF):
         planes[0][i] = WhiteSpace
 
     # per: https://www.ecma-international.org/ecma-262/5.1/#sec-7.6
-    for i in (ord('$'), ord('_'), ord('\\')):
+    for i in (ord("$"), ord("_"), ord("\\")):
         planes[0][i] = IdentifierStart
 
     # per: https://www.ecma-international.org/ecma-262/5.1/#sec-7.6
     for i in (0x200C, 0x200D):
         planes[0][i] = IdentifierPart
 
-    print('#ifndef JSON5EncoderCpp_unicode_cat_of', file=output_file)
-    print('#define JSON5EncoderCpp_unicode_cat_of', file=output_file)
+    print("#ifndef JSON5EncoderCpp_unicode_cat_of", file=output_file)
+    print("#define JSON5EncoderCpp_unicode_cat_of", file=output_file)
     print(file=output_file)
-    print('// GENERATED FILE', file=output_file)
-    print('// All changes will be lost.', file=output_file)
+    print("// GENERATED FILE", file=output_file)
+    print("// All changes will be lost.", file=output_file)
     print(file=output_file)
-    print('#include <cstdint>', file=output_file)
+    print("#include <cstdint>", file=output_file)
     print(file=output_file)
-    print('namespace JSON5EncoderCpp {', file=output_file)
-    print('inline namespace {', file=output_file)
+    print("namespace JSON5EncoderCpp {", file=output_file)
+    print("inline namespace {", file=output_file)
     print(file=output_file)
-    print('static unsigned unicode_cat_of(std::uint32_t codepoint) {', file=output_file)
+    print("static unsigned unicode_cat_of(std::uint32_t codepoint) {", file=output_file)
 
-    print('    static std::uint8_t plane_X[0x10000 / 4] = {0};', file=output_file)
+    print("    static std::uint8_t plane_X[0x10000 / 4] = {0};", file=output_file)
     print(file=output_file)
 
     for plane_idx, plane_data in planes.items():
-        print('    static std::uint8_t plane_' + str(plane_idx) + '[0x10000 / 4] = {', file=output_file)
-        for chunk in chunked(plane_data, 4*16):
-            print('        ', end='', file=output_file)
+        print(
+            "    static std::uint8_t plane_" + str(plane_idx) + "[0x10000 / 4] = {",
+            file=output_file,
+        )
+        for chunk in chunked(plane_data, 4 * 16):
+            print("        ", end="", file=output_file)
             for value in chunked(chunk, 4):
                 value = reduce(lambda a, i: ((a << 2) | i), reversed(value), 0)
-                print('0x{:02x}u'.format(value), end=', ', file=output_file)
+                print("0x{:02x}u".format(value), end=", ", file=output_file)
             print(file=output_file)
-        print('    };', file=output_file)
+        print("    };", file=output_file)
         print(file=output_file)
 
-    print('    static std::uint8_t *planes[17] = {', end='', file=output_file)
+    print("    static std::uint8_t *planes[17] = {", end="", file=output_file)
     for plane_idx in range(0, 17):
         if plane_idx % 8 == 0:
-            print('\n        ', end='', file=output_file)
+            print("\n        ", end="", file=output_file)
         if plane_idx in planes:
-            print('plane_' + str(plane_idx) + ', ', end='', file=output_file)
+            print("plane_" + str(plane_idx) + ", ", end="", file=output_file)
         else:
-            print('plane_X, ', end='', file=output_file)
+            print("plane_X, ", end="", file=output_file)
     print(file=output_file)
-    print('    };', file=output_file)
+    print("    };", file=output_file)
     print(file=output_file)
 
-    print('    std::uint16_t plane_idx = std::uint16_t(codepoint / 0x10000);', file=output_file)
-    print('    if (JSON5EncoderCpp_expect(plane_idx > 16, false)) return 1;', file=output_file)
-    print('    std::uint16_t datum_idx = std::uint16_t(codepoint & 0xffff);', file=output_file)
-    print('    const std::uint8_t *plane = planes[plane_idx];', file=output_file)
-    print('    return (plane[datum_idx / 4] >> (2 * (datum_idx % 4))) % 4;', file=output_file)
-    print('}', file=output_file)
+    print(
+        "    std::uint16_t plane_idx = std::uint16_t(codepoint / 0x10000);",
+        file=output_file,
+    )
+    print(
+        "    if (JSON5EncoderCpp_expect(plane_idx > 16, false)) return 1;",
+        file=output_file,
+    )
+    print(
+        "    std::uint16_t datum_idx = std::uint16_t(codepoint & 0xffff);",
+        file=output_file,
+    )
+    print("    const std::uint8_t *plane = planes[plane_idx];", file=output_file)
+    print(
+        "    return (plane[datum_idx / 4] >> (2 * (datum_idx % 4))) % 4;",
+        file=output_file,
+    )
+    print("}", file=output_file)
     print(file=output_file)
-    print('}', file=output_file)
-    print('}', file=output_file)
+    print("}", file=output_file)
+    print("}", file=output_file)
     print(file=output_file)
-    print('#endif', file=output_file)
+    print("#endif", file=output_file)
 
 
-argparser = ArgumentParser(description='Generate Unicode Category Matcher(s)')
-argparser.add_argument('input', nargs='?', type=Path, default=Path('/dev/stdin'))
-argparser.add_argument('output', nargs='?', type=Path, default=Path('/dev/stdout'))
+argparser = ArgumentParser(description="Generate Unicode Category Matcher(s)")
+argparser.add_argument("input", nargs="?", type=Path, default=Path("/dev/stdin"))
+argparser.add_argument("output", nargs="?", type=Path, default=Path("/dev/stdout"))
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     args = argparser.parse_args()
-    with open(str(args.input.resolve()), 'rt') as input_file, \
-         open(str(args.output.resolve()), 'wt') as output_file:
+    with open(str(args.input.resolve()), "rt") as input_file, open(
+        str(args.output.resolve()), "wt"
+    ) as output_file:
         raise SystemExit(main(input_file, output_file))
diff --git a/pyjson5.pyx b/pyjson5.pyx
index 4dd5f82..49e8d00 100644
--- a/pyjson5.pyx
+++ b/pyjson5.pyx
@@ -1,7 +1,7 @@
 # distutils: language = c++
 # cython: embedsignature = True, language_level = 3, warn.undeclared = True, warn.unreachable = True, warn.maybe_uninitialized = True
 
-# Copyright 2018-2022 René Kijewski <rene.SURNAME@fu-berlin.de>
+# Copyright 2018-2023 René Kijewski <pypi.org@k6i.de>
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
diff --git a/pyproject.toml b/pyproject.toml
index 638634f..6fa4cb2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,6 +2,5 @@
 requires = [
     "Cython",
     "setuptools",
-    "wheel",
 ]
 build-backend = "setuptools.build_meta"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f855de8..a49c84d 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,6 +1,15 @@
+black
 build
 colorama == 0.4.*
 cython == 0.*
-more_itertools == 8.*
+more_itertools == 9.*
+mypy
 setuptools
 wheel
+
+# keep synchronous to requirements-readthecocs.txt
+docutils == 0.19.*
+furo
+myst-parser == 2.*
+sphinx == 7.*
+sphinx_autodoc_typehints == 1.*
diff --git a/requirements-readthedocs.txt b/requirements-readthedocs.txt
index 9e593af..c1637c1 100644
--- a/requirements-readthedocs.txt
+++ b/requirements-readthedocs.txt
@@ -1,9 +1,10 @@
 # keep synchronous to setup.cfg
-pyjson5 == 1.6.2
+# keep synchronous to src/VERSION.inc
+pyjson5 == 1.6.3
 
-docutils == 0.17.*
-myst-parser == 0.18.*
-sphinx == 5.*
+# keep synchronous to requirements-dev.txt
+docutils == 0.19.*
+furo
+myst-parser == 2.*
+sphinx == 7.*
 sphinx_autodoc_typehints == 1.*
-sphinx_rtd_theme == 1.*
-sphinxprettysearchresults == 0.3.*
diff --git a/run-minefield-test.py b/run-minefield-test.py
index 749ee05..654ff25 100755
--- a/run-minefield-test.py
+++ b/run-minefield-test.py
@@ -11,16 +11,21 @@
 from pyjson5 import decode_io
 
 
-argparser = ArgumentParser(description='Run JSON5 parser tests')
-argparser.add_argument('tests', nargs='?', type=Path, default=Path('third-party/JSONTestSuite/test_parsing'))
+argparser = ArgumentParser(description="Run JSON5 parser tests")
+argparser.add_argument(
+    "tests",
+    nargs="?",
+    type=Path,
+    default=Path("third-party/JSONTestSuite/test_parsing"),
+)
 
 suffix_implies_success = {
-    'json': True,
-    'json5': True,
-    'txt': False,
+    "json": True,
+    "json5": True,
+    "txt": False,
 }
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     basicConfig(level=INFO)
     logger = getLogger(__name__)
 
@@ -28,48 +33,48 @@
 
     good = bad = severe = 0
 
-    if name != 'nt':
-        code_severe = Fore.RED + '😱'
-        code_good = Fore.CYAN + 'πŸ˜„'
-        code_bad = Fore.YELLOW + '😠'
-        code_ignored = Fore.BLUE + 'πŸ™…'
+    if name != "nt":
+        code_severe = Fore.RED + "😱"
+        code_good = Fore.CYAN + "πŸ˜„"
+        code_bad = Fore.YELLOW + "😠"
+        code_ignored = Fore.BLUE + "πŸ™…"
     else:
-        code_severe = Fore.RED + 'SEVERE'
-        code_good = Fore.CYAN + 'GOOD'
-        code_bad = Fore.YELLOW + 'BAD'
-        code_ignored = Fore.BLUE + 'IGNORED'
+        code_severe = Fore.RED + "SEVERE"
+        code_good = Fore.CYAN + "GOOD"
+        code_bad = Fore.YELLOW + "BAD"
+        code_ignored = Fore.BLUE + "IGNORED"
 
     args = argparser.parse_args()
     index = 0
-    for path in sorted(args.tests.glob('?_?*.json')):
-        category, name = path.stem.split('_', 1)
-        if category not in 'yni':
+    for path in sorted(args.tests.glob("?_?*.json")):
+        category, name = path.stem.split("_", 1)
+        if category not in "yni":
             continue
 
-        if category in 'ni':
+        if category in "ni":
             # ignore anything but tests that are expected to pass for now
             continue
 
         try:
             # ignore any UTF-8 errors
-            with open(str(path.resolve()), 'rt') as f:
+            with open(str(path.resolve()), "rt") as f:
                 f.read()
         except Exception:
             continue
 
         index += 1
         try:
-            p = Popen((executable, 'transcode-to-json.py', str(path)))
+            p = Popen((executable, "transcode-to-json.py", str(path)))
             outcome = p.wait(5)
         except Exception:
-            logger.error('Error while testing: %s', path, exc_info=True)
+            logger.error("Error while testing: %s", path, exc_info=True)
             errors += 1
             continue
 
         if outcome not in (0, 1):
             code = code_severe
             severe += 1
-        elif category == 'y':
+        elif category == "y":
             if outcome == 0:
                 code = code_good
                 good += 1
@@ -80,12 +85,19 @@
             code = code_ignored
 
         print(
-            '#', index, ' ', code, ' | '
-            'Category <', category, '> | '
-            'Test <', name, '> | '
-            'Actual <', 'pass' if outcome == 0 else 'FAIL', '>',
+            "#",
+            index,
+            " ",
+            code,
+            " | " "Category <",
+            category,
+            "> | " "Test <",
+            name,
+            "> | " "Actual <",
+            "pass" if outcome == 0 else "FAIL",
+            ">",
             Fore.RESET,
-            sep='',
+            sep="",
         )
 
     is_severe = severe > 0
@@ -93,11 +105,15 @@
     code = code_severe if is_severe else code_good if is_good else code_bad
     print()
     print(
-        code, ' | ',
-        good, ' correct outcomes | ',
-        bad, ' wrong outcomes | ',
-        severe, ' severe errors',
+        code,
+        " | ",
+        good,
+        " correct outcomes | ",
+        bad,
+        " wrong outcomes | ",
+        severe,
+        " severe errors",
         Fore.RESET,
-        sep=''
+        sep="",
     )
     raise SystemExit(2 if is_severe else 0 if is_good else 1)
diff --git a/run-tests.py b/run-tests.py
index c3dddc0..9b2e808 100755
--- a/run-tests.py
+++ b/run-tests.py
@@ -11,29 +11,31 @@
 from pyjson5 import decode_io
 
 
-argparser = ArgumentParser(description='Run JSON5 parser tests')
-argparser.add_argument('tests', nargs='?', type=Path, default=Path('third-party/json5-tests'))
+argparser = ArgumentParser(description="Run JSON5 parser tests")
+argparser.add_argument(
+    "tests", nargs="?", type=Path, default=Path("third-party/json5-tests")
+)
 
 suffix_implies_success = {
-    '.json': True,
-    '.json5': True,
-    '.txt': False,
+    ".json": True,
+    ".json5": True,
+    ".txt": False,
 }
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     basicConfig(level=INFO)
     logger = getLogger(__name__)
 
     init()
 
-    if name != 'nt':
-        code_severe = Fore.RED + '😱'
-        code_good = Fore.CYAN + 'πŸ˜„'
-        code_bad = Fore.YELLOW + '😠'
+    if name != "nt":
+        code_severe = Fore.RED + "😱"
+        code_good = Fore.CYAN + "πŸ˜„"
+        code_bad = Fore.YELLOW + "😠"
     else:
-        code_severe = Fore.RED + 'SEVERE'
-        code_good = Fore.CYAN + 'GOOD'
-        code_bad = Fore.YELLOW + 'BAD'
+        code_severe = Fore.RED + "SEVERE"
+        code_good = Fore.CYAN + "GOOD"
+        code_bad = Fore.YELLOW + "BAD"
 
     good = 0
     bad = 0
@@ -41,8 +43,8 @@
 
     args = argparser.parse_args()
     index = 0
-    for path in sorted(args.tests.glob('*/*.*')):
-        kind = path.suffix.split('.')[-1]
+    for path in sorted(args.tests.glob("*/*.*")):
+        kind = path.suffix.split(".")[-1]
         expect_success = suffix_implies_success.get(path.suffix)
         if expect_success is None:
             continue
@@ -51,10 +53,10 @@
         category = path.parent.name
         name = path.stem
         try:
-            p = Popen((executable, 'transcode-to-json.py', str(path)))
+            p = Popen((executable, "transcode-to-json.py", str(path)))
             outcome = p.wait(5)
         except Exception:
-            logger.error('Error while testing: %s', path, exc_info=True)
+            logger.error("Error while testing: %s", path, exc_info=True)
             severe += 1
             continue
 
@@ -64,14 +66,23 @@
         is_good = is_success if expect_success else is_failure
         code = code_severe if is_severe else code_good if is_good else code_bad
         print(
-            '#', index, ' ', code, ' '
-            'Category <', category, '> | '
-            'Test <', name, '> | '
-            'Data <', kind, '> | '
-            'Expected <', 'pass' if expect_success else 'FAIL', '> | '
-            'Actual <', 'pass' if is_success else 'FAIL', '>',
+            "#",
+            index,
+            " ",
+            code,
+            " " "Category <",
+            category,
+            "> | " "Test <",
+            name,
+            "> | " "Data <",
+            kind,
+            "> | " "Expected <",
+            "pass" if expect_success else "FAIL",
+            "> | " "Actual <",
+            "pass" if is_success else "FAIL",
+            ">",
             Fore.RESET,
-            sep='',
+            sep="",
         )
         if is_severe:
             severe += 1
@@ -85,11 +96,15 @@
     code = code_severe if is_severe else code_good if is_good else code_bad
     print()
     print(
-        code, ' ',
-        good, ' × correct outcome | ',
-        bad, ' × wrong outcome | ',
-        severe, ' × severe errors',
+        code,
+        " ",
+        good,
+        " × correct outcome | ",
+        bad,
+        " × wrong outcome | ",
+        severe,
+        " × severe errors",
         Fore.RESET,
-        sep=''
+        sep="",
     )
     raise SystemExit(2 if is_severe else 0 if is_good else 1)
diff --git a/setup.cfg b/setup.cfg
index 0aa8997..eb5a24e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,18 @@
 [metadata]
-version = 1.6.2
+# keep synchronous to requirements-readthedocs.txt
+# keep synchronous to src/VERSION.inc
+version = 1.6.3
 
 name = pyjson5
 description = JSON5 serializer and parser for Python 3 written in Cython.
 url = https://github.com/Kijewski/pyjson5
+project_urls =
+    Changelog = https://github.com/Kijewski/pyjson5/blob/main/CHANGELOG.md
+    Code = https://github.com/Kijewski/pyjson5
+    Documentation = https://pyjson5.readthedocs.io/
+    Download = https://pypi.org/project/pyjson5/
+    Homepage = https://github.com/Kijewski/pyjson5
+    Tracker = https://github.com/Kijewski/pyjson5/issues
 
 author = René Kijewski
 maintainer = René Kijewski
@@ -31,6 +40,7 @@
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.11
     Programming Language :: Python :: 3 :: Only
     Programming Language :: Python :: Implementation :: CPython
     Topic :: Text Processing :: General
@@ -42,7 +52,6 @@
 setup_requires =
     Cython
     setuptools
-    wheel
 
 include_package_data = True
 packages = pyjson5
diff --git a/setup.py b/setup.py
index cc5276d..c5202ae 100755
--- a/setup.py
+++ b/setup.py
@@ -3,17 +3,24 @@
 from setuptools import setup, Extension
 
 extra_compile_args = [
-    '-std=c++11', '-O3', '-fPIC', '-ggdb1', '-pipe',
-    '-fomit-frame-pointer', '-fstack-protector-strong',
+    "-std=c++11",
+    "-O3",
+    "-fPIC",
+    "-ggdb1",
+    "-pipe",
+    "-fomit-frame-pointer",
+    "-fstack-protector-strong",
 ]
 
 setup(
-    ext_modules=[Extension(
-        'pyjson5.pyjson5',
-        sources=['pyjson5.pyx'],
-        include_dirs=['src'],
-        extra_compile_args=extra_compile_args,
-        extra_link_args=extra_compile_args,
-        language='c++',
-    )],
+    ext_modules=[
+        Extension(
+            "pyjson5.pyjson5",
+            sources=["pyjson5.pyx"],
+            include_dirs=["src"],
+            extra_compile_args=extra_compile_args,
+            extra_link_args=extra_compile_args,
+            language="c++",
+        )
+    ],
 )
diff --git a/sha512sum.py b/sha512sum.py
index a6aad70..f73a379 100755
--- a/sha512sum.py
+++ b/sha512sum.py
@@ -7,27 +7,29 @@
 from sys import argv, exit
 
 
-argparser = ArgumentParser(description='sha512sum replacement if coreutils isn\'t installed')
-argparser.add_argument('-c', '--check', type=Path, required=True)
+argparser = ArgumentParser(
+    description="sha512sum replacement if coreutils isn't installed"
+)
+argparser.add_argument("-c", "--check", type=Path, required=True)
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     basicConfig(level=DEBUG)
     args = argparser.parse_args()
     errors = 0
-    with open(str(args.check.resolve()), 'rt') as f:
+    with open(str(args.check.resolve()), "rt") as f:
         for line in f:
-            expected_hash, filename = line.rstrip('\r\n').split('  ', 1)
-            with open(str(Path(filename).resolve()), 'rb') as f:
+            expected_hash, filename = line.rstrip("\r\n").split("  ", 1)
+            with open(str(Path(filename).resolve()), "rb") as f:
                 actual_hash = sha512(f.read()).hexdigest()
 
             if expected_hash == actual_hash:
-                print(filename + ': OK')
+                print(filename + ": OK")
             else:
                 errors += 1
-                print(filename + ': FAILED')
+                print(filename + ": FAILED")
 
     if errors:
-        print('%s: WARNING: %s computed checksum did NOT match' % (argv[0], errors))
+        print("%s: WARNING: %s computed checksum did NOT match" % (argv[0], errors))
         exit(1)
     else:
         exit(0)
diff --git a/src/VERSION.inc b/src/VERSION.inc
index b548db1..f3cbab0 100644
--- a/src/VERSION.inc
+++ b/src/VERSION.inc
@@ -1 +1 @@
-"1.6.2"
+"1.6.3"
diff --git a/src/_exports.pyx b/src/_exports.pyx
index 433a67c..af57a50 100644
--- a/src/_exports.pyx
+++ b/src/_exports.pyx
@@ -621,4 +621,4 @@
 
 __license__ = 'Apache-2.0'
 
-__author__ = '2018-2022 René Kijewski <rene.[surname]@fu-berlin.de>'
+__author__ = '2018-2023 René Kijewski <pypi.org@k6i.de>'
diff --git a/src/pyjson5/__init__.pyi b/src/pyjson5/__init__.pyi
index e041988..6751046 100644
--- a/src/pyjson5/__init__.pyi
+++ b/src/pyjson5/__init__.pyi
@@ -1,72 +1,78 @@
 from typing import (
-    Any, Callable, final, Final, Generic, Iterable, Literal, Optional,
-    overload, Protocol, Tuple, TypeVar, Union,
+    Any,
+    Callable,
+    final,
+    Final,
+    Iterable,
+    Literal,
+    Optional,
+    overload,
+    Protocol,
+    Tuple,
+    TypeVar,
+    Union,
 )
 
-
-_T = TypeVar('_T')
+_Data = TypeVar("_Data")
 
 class _SupportsRead(Protocol):
-    def read(self, size: int = ...) -> str:
-        ...
+    def read(self, size: int = ...) -> str: ...
 
-class _SupportsWrite(Generic[_T]):
-    def write(self, s: _T) -> None:
-        ...
+class _SupportsWrite(Protocol[_Data]):
+    def write(self, s: _Data) -> Any: ...
 
-_CallbackStr = TypeVar('_CallbackStr', bound=Callable[[str], None])
+_CallbackStr = TypeVar("_CallbackStr", bound=Callable[[str], None])
 
-_CallbackBytes = TypeVar('_CallbackBytes', bound=Callable[[bytes], None])
+_CallbackBytes = TypeVar("_CallbackBytes", bound=Callable[[bytes], None])
 
-_SupportsWriteBytes = TypeVar('_SupportsWriteBytes', bound=_SupportsWrite[bytes])
+_SupportsWriteBytes = TypeVar("_SupportsWriteBytes", bound=_SupportsWrite[bytes])
 
-_SupportsWriteStr = TypeVar('_SupportsWriteStr', bound=_SupportsWrite[str])
-
+_SupportsWriteStr = TypeVar("_SupportsWriteStr", bound=_SupportsWrite[str])
 
 ###############################################################################
 ### _exports.pyx
 ###############################################################################
 
-
 @final
 class Options:
-    '''Customizations for the ``encoder_*(...)`` function family.'''
+    """Customizations for the ``encoder_*(...)`` function family."""
+
     quotationmark: Final[str] = ...
     tojson: Final[Optional[str]] = ...
     mappingtypes: Final[Tuple[type, ...]] = ...
 
     def __init__(
-        self, *,
+        self,
+        *,
         quotationmark: Optional[str] = ...,
         tojson: Optional[str],
         mappingtypes: Optional[Tuple[type, ...]],
-    ) -> None:
-        ...
-
+    ) -> None: ...
     def update(
-        self, *,
+        self,
+        *,
         quotationmark: Optional[str] = ...,
         tojson: Optional[str],
         mappingtypes: Optional[Tuple[type, ...]],
     ) -> Options:
-        '''Creates a new Options instance by modifying some members.'''
-        ...
+        """Creates a new Options instance by modifying some members."""
 
 def decode(data: str, maxdepth: Optional[int] = ..., some: bool = ...) -> Any:
-    '''Decodes JSON5 serialized data from an ``str`` object.'''
-    ...
+    """Decodes JSON5 serialized data from an ``str`` object."""
 
 def decode_latin1(
-    data: bytes, maxdepth: Optional[int] = ..., some: bool = ...,
+    data: bytes,
+    maxdepth: Optional[int] = ...,
+    some: bool = ...,
 ) -> Any:
-    '''Decodes JSON5 serialized data from a ``bytes`` object.'''
-    ...
+    """Decodes JSON5 serialized data from a ``bytes`` object."""
 
 def decode_utf8(
-    data: bytes, maxdepth: Optional[int] = ..., some: bool = ...,
+    data: bytes,
+    maxdepth: Optional[int] = ...,
+    some: bool = ...,
 ) -> Any:
-    '''Decodes JSON5 serialized data from a ``bytes`` object.'''
-    ...
+    """Decodes JSON5 serialized data from a ``bytes`` object."""
 
 def decode_buffer(
     data: bytes,
@@ -74,8 +80,7 @@
     some: bool = ...,
     wordlength: Optional[int] = ...,
 ) -> Any:
-    '''Decodes JSON5 serialized data from an object that supports the buffer protocol, e.g. bytearray.'''
-    ...
+    """Decodes JSON5 serialized data from an object that supports the buffer protocol, e.g. bytearray."""
 
 def decode_callback(
     cb: Callable[..., Union[str, bytes, bytearray, int, None]],
@@ -83,223 +88,207 @@
     some: bool = ...,
     args: Optional[Iterable[Any]] = [],
 ) -> Any:
-    '''Decodes JSON5 serialized data by invoking a callback.'''
-    ...
+    """Decodes JSON5 serialized data by invoking a callback."""
 
 def decode_io(
-    fp: _SupportsRead, maxdepth: Optional[int] = ..., some: bool = ...,
+    fp: _SupportsRead,
+    maxdepth: Optional[int] = ...,
+    some: bool = ...,
 ) -> Any:
-    '''Decodes JSON5 serialized data from a file-like object.'''
-    ...
+    """Decodes JSON5 serialized data from a file-like object."""
 
 def encode(
-    data: Any, *,
+    data: Any,
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
 ) -> str:
-    '''Serializes a Python object to a JSON5 compatible string.'''
+    """Serializes a Python object to a JSON5 compatible string."""
     ...
 
 def encode_bytes(
-    data: Any, *,
+    data: Any,
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
 ) -> str:
-    '''Serializes a Python object to a JSON5 compatible bytes string.'''
-    ...
+    """Serializes a Python object to a JSON5 compatible bytes string."""
 
 @overload
 def encode_callback(
-    data: Any, cb: _CallbackStr, supply_bytes: Literal[False] = ..., *,
+    data: Any,
+    cb: _CallbackStr,
+    supply_bytes: Literal[False] = ...,
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
 ) -> _CallbackStr:
-    '''Serializes a Python object into a callback function.'''
-    ...
+    """Serializes a Python object into a callback function."""
 
 @overload
 def encode_callback(
-    data: Any, cb: _CallbackBytes, supply_bytes: Literal[True], *,
+    data: Any,
+    cb: _CallbackBytes,
+    supply_bytes: Literal[True],
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
-) -> _CallbackBytes:
-    ...
-
+) -> _CallbackBytes: ...
 @overload
 def encode_io(
-    data: Any, fp: _SupportsWriteBytes, supply_bytes: Literal[True] = ..., *,
+    data: Any,
+    fp: _SupportsWriteBytes,
+    supply_bytes: Literal[True] = ...,
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
 ) -> _SupportsWriteBytes:
-    '''Serializes a Python object into a file-object.'''
-    ...
+    """Serializes a Python object into a file-object."""
 
 @overload
 def encode_io(
-    data: Any, fp: _SupportsWriteStr, supply_bytes: Literal[False], *,
+    data: Any,
+    fp: _SupportsWriteStr,
+    supply_bytes: Literal[False],
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
-) -> _SupportsWriteStr:
-    ...
-
+) -> _SupportsWriteStr: ...
 def encode_noop(
-    data: Any, *,
+    data: Any,
+    *,
     options: Optional[Options] = ...,
     quotationmark: Optional[str] = ...,
     tojson: Optional[str],
     mappingtypes: Optional[Tuple[type, ...]],
 ) -> bool:
-    '''Test if the input is serializable.'''
-    ...
-
+    """Test if the input is serializable."""
 
 ###############################################################################
 ### _legacy.pyx
 ###############################################################################
 
-
 def loads(s: str, *, encoding: str = ...) -> Any:
-    '''Decodes JSON5 serialized data from a string.'''
-    ...
+    """Decodes JSON5 serialized data from a string."""
 
 def load(fp: _SupportsRead) -> Any:
-    '''Decodes JSON5 serialized data from a file-like object.'''
-    ...
+    """Decodes JSON5 serialized data from a file-like object."""
 
 def dumps(obj: Any) -> str:
-    '''Serializes a Python object to a JSON5 compatible string.'''
-    ...
+    """Serializes a Python object to a JSON5 compatible string."""
 
 def dump(obj: Any, fp: _SupportsWrite[str]) -> None:
-    '''Serializes a Python object to a JSON5 compatible string.'''
-    ...
-
+    """Serializes a Python object to a JSON5 compatible string."""
 
 ###############################################################################
 ### _exceptions.pyx
 ###############################################################################
 
-
 class Json5Exception(Exception):
-    '''Base class of any exception thrown by PyJSON5.'''
+    """Base class of any exception thrown by PyJSON5."""
 
-    def __init__(self, message: Optional[str] = ..., *args: Any) -> None:
-        ...
-
+    def __init__(self, message: Optional[str] = ..., *args: Any) -> None: ...
     @property
-    def message(self) -> Optional[str]:
-        ...
-
+    def message(self) -> Optional[str]: ...
 
 ###############################################################################
 ### _exceptions_encoder.pyx
 ###############################################################################
 
-
 class Json5EncoderException(Json5Exception):
-    '''Base class of any exception thrown by the serializer.'''
-    ...
+    """Base class of any exception thrown by the serializer."""
 
 @final
 class Json5UnstringifiableType(Json5EncoderException):
-    '''The encoder was not able to stringify the input, or it was told not to by the supplied ``Options``.'''
-    def __init__(
-        self, message: Optional[str] = ..., unstringifiable: Any = ...,
-    ) -> None:
-        ...
+    """The encoder was not able to stringify the input, or it was told not to by the supplied ``Options``."""
 
+    def __init__(
+        self,
+        message: Optional[str] = ...,
+        unstringifiable: Any = ...,
+    ) -> None: ...
     @property
     def unstringifiable(self) -> Any:
-        '''The value that caused the problem.'''
-        ...
-
+        """The value that caused the problem."""
 
 ###############################################################################
 ### _exceptions_decoder.pyx
 ###############################################################################
 
-
 class Json5DecoderException(Json5Exception):
-    '''Base class of any exception thrown by the parser.'''
-    def __init__(
-        self, message: Optional[str] = ..., result: Any = ..., *args: Any,
-    ) -> None:
-        ...
+    """Base class of any exception thrown by the parser."""
 
+    def __init__(
+        self,
+        message: Optional[str] = ...,
+        result: Any = ...,
+        *args: Any,
+    ) -> None: ...
     @property
     def result(self) -> Any:
-        '''Deserialized data up until now.'''
-        ...
+        """Deserialized data up until now."""
 
 @final
 class Json5NestingTooDeep(Json5DecoderException):
-    '''The maximum nesting level on the input data was exceeded.'''
-    ...
+    """The maximum nesting level on the input data was exceeded."""
 
 @final
 class Json5EOF(Json5DecoderException):
-    '''The input ended prematurely.'''
-    ...
+    """The input ended prematurely."""
 
 @final
 class Json5IllegalCharacter(Json5DecoderException):
-    '''An unexpected character was encountered.'''
+    """An unexpected character was encountered."""
+
     def __init__(
         self,
         message: Optional[str] = ...,
         result: Any = ...,
         character: Optional[str] = ...,
         *args: Any,
-    ) -> None:
-        ...
-
+    ) -> None: ...
     @property
     def character(self) -> Optional[str]:
-        '''Illegal character.'''
-        ...
+        """Illegal character."""
 
 @final
 class Json5ExtraData(Json5DecoderException):
-    '''The input contained extranous data.'''
+    """The input contained extranous data."""
+
     def __init__(
         self,
         message: Optional[str] = ...,
         result: Any = ...,
         character: Optional[str] = ...,
         *args: Any,
-    ) -> None:
-        ...
-
+    ) -> None: ...
     @property
     def character(self) -> Optional[str]:
-        '''Extranous character.'''
-        ...
+        """Extranous character."""
 
 @final
 class Json5IllegalType(Json5DecoderException):
-    '''The user supplied callback function returned illegal data.'''
+    """The user supplied callback function returned illegal data."""
+
     def __init__(
         self,
         message: Optional[str] = ...,
         result: Any = ...,
         value: Optional[Any] = ...,
         *args: Any,
-    ) -> None:
-        ...
-
+    ) -> None: ...
     @property
     def value(self) -> Optional[Any]:
-        '''Value that caused the problem.'''
-        ...
+        """Value that caused the problem."""
diff --git a/third-party/fast_double_parser b/third-party/fast_double_parser
index 07d9189..50a2ccb 160000
--- a/third-party/fast_double_parser
+++ b/third-party/fast_double_parser
@@ -1 +1 @@
-Subproject commit 07d9189a8fb815fe800cb15ca022e7a07093236e
+Subproject commit 50a2ccb5535179781a15d20ba2909c812f329cc5
diff --git a/transcode-to-json.py b/transcode-to-json.py
index 110f10a..c98ae5a 100755
--- a/transcode-to-json.py
+++ b/transcode-to-json.py
@@ -47,27 +47,27 @@
     return True
 
 
-argparser = ArgumentParser(description='Run JSON5 parser tests')
-argparser.add_argument('input', type=Path)
-argparser.add_argument('output', nargs='?', type=Path)
+argparser = ArgumentParser(description="Run JSON5 parser tests")
+argparser.add_argument("input", type=Path)
+argparser.add_argument("output", nargs="?", type=Path)
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     basicConfig(level=DEBUG)
     logger = getLogger(__name__)
 
     args = argparser.parse_args()
     try:
         # open() does not work with Paths in Python 3.5
-        with codecs_open(str(args.input.resolve()), 'r', 'UTF-8') as f:
+        with codecs_open(str(args.input.resolve()), "r", "UTF-8") as f:
             data = f.read()
     except Exception:
-        logger.error('Could not even read file: %s', args.input, exc_info=True)
+        logger.error("Could not even read file: %s", args.input, exc_info=True)
         raise SystemExit(-1)
 
     try:
         obj = decode(data)
     except Exception:
-        logger.error('Could not parse content: %s', args.input)
+        logger.error("Could not parse content: %s", args.input)
         raise SystemExit(1)
 
     try:
@@ -76,23 +76,27 @@
         pass
     else:
         if not eq_with_nans(obj, json_obj):
-            logger.error('JSON and PyJSON5 did not read the same data: %s, %r != %r', args.input, obj, json_obj)
+            logger.error(
+                "JSON and PyJSON5 did not read the same data: %s, %r != %r",
+                args.input,
+                obj,
+                json_obj,
+            )
             raise SystemExit(2)
 
     try:
         data = encode(obj)
     except Exception:
-        logger.error('Could open stringify content: %s', args.input, exc_info=True)
+        logger.error("Could open stringify content: %s", args.input, exc_info=True)
         raise SystemExit(2)
 
     if args.output is not None:
         try:
             # open() does not work with Paths in Python 3.5
-            with codecs_open(str(args.output.resolve()), 'w', 'UTF-8') as f:
+            with codecs_open(str(args.output.resolve()), "w", "UTF-8") as f:
                 f.write(data)
         except Exception:
-            logger.error('Could open output file: %s', args.output, exc_info=True)
+            logger.error("Could open output file: %s", args.output, exc_info=True)
             raise SystemExit(-1)
 
     raise SystemExit(0)
-