Repository: pyparsing/pyparsing

Issue:
Add a Caseless version of CloseMatch
I notice that theres classes for `CaselessLiteral` and a `CloseMatch` but there doesnt seem to be a class combining the two for situations where you want a `CaselessLiteral` but also want to be tolerant of a certain number of incorrect characters (i.e. to catch typos) as is the behavior with `CloseMatch`. Whis would have the behavior of a less-stringent `CloseMatch` that will only consider a character to be nonmatching if it is a different character, rather than just a differently-cased version of the same character.

After poking around the code, it would seem to me that this could essentially be some light refeatoring to move the string comparison from [here](https://github.com/pyparsing/pyparsing/blob/b5b1fdd1c0a3dfffbbad206e885d59e6015015a8/pyparsing/core.py#L2154) to a new function within `CloseMatch` and then create a subclass of `CloseMatch` that changes the comparison function to use `src.lower() != mat.lower()) as the comparison instead.

Would this be something that would be useful to submit a pull request for? or would this be better to create in a separate package/module?

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#281](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (31532f7) into [master](https://codecov.io/gh/pyparsing/pyparsing/commit/d108a29db062c9250f50c978e3a86381d1b0746b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d108a29) will **decrease** coverage by `0.04%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/pyparsing/pyparsing/pull/281/graphs/tree.svg?width=650&height=150&src=pr&token=57TPjygijp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master     #281      +/-   ##
==========================================
- Coverage   91.14%   91.10%   -0.05%     
==========================================
  Files          11       11              
  Lines        3253     3236      -17     
  Branches      713      714       +1     
==========================================
- Hits         2965     2948      -17     
  Misses        176      176              
  Partials      112      112              
```


| [Impacted Files](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [pyparsing/core.py](https://codecov.io/gh/pyparsing/pyparsing/pull/281/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHlwYXJzaW5nL2NvcmUucHk=) | `89.38% <100.00%> (-0.04%)` | :arrow_down: |
| [pyparsing/unicode.py](https://codecov.io/gh/pyparsing/pyparsing/pull/281/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHlwYXJzaW5nL3VuaWNvZGUucHk=) | `96.34% <0.00%> (-0.22%)` | :arrow_down: |
| [pyparsing/exceptions.py](https://codecov.io/gh/pyparsing/pyparsing/pull/281/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHlwYXJzaW5nL2V4Y2VwdGlvbnMucHk=) | `97.64% <0.00%> (-0.03%)` | :arrow_down: |
| [pyparsing/util.py](https://codecov.io/gh/pyparsing/pyparsing/pull/281/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHlwYXJzaW5nL3V0aWwucHk=) | `100.00% <0.00%> (ø)` | |
| [pyparsing/common.py](https://codecov.io/gh/pyparsing/pyparsing/pull/281/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cHlwYXJzaW5nL2NvbW1vbi5weQ==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [d108a29...31532f7](https://codecov.io/gh/pyparsing/pyparsing/pull/281?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

MoralCode: @ptmcg is this something that you think aligns well with the goals of pyparsing? happy to do my best to fix the merge conflicts if this would be a helpful merge
ptmcg: My apologies - I just reviewed the issue you posted, and you submitted this PR around May!

Since then there have been quite a number of commits. Try rebasing your code off the latest master, or if that fails, probably just start a new PR with a fresh branch of latest master. I am concerned that resolving merge conflicts will leave out needed changes (this just happened to me last week at work!).  I'll work with your PR as soon as you let me know its ready.
MoralCode: @ptmcg just did those updates. looks like most of it was due to me committing autoformatting changes from linters which seems to have been why there were so many conflicts.

The only question I have is if I put the entry to CHANGES in the right spot (3.3.3c1)

Patch:
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index bb2a3da..4abec8a 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -1,6 +1,6 @@
 # Security Policy
 
-Pyparsing itself has no known security security vulnerabilities. It does not 
+Pyparsing itself has no known security vulnerabilities. It does not 
 itself access any risk-inherent methods like `exec` or `eval`, nor does it import 
 any modules not part of the Python standard library.
 
diff --git a/CHANGES b/CHANGES
index c074435..55147ee 100644
--- a/CHANGES
+++ b/CHANGES
@@ -56,6 +56,9 @@ Version 3.0.0c1 -
 - Fixed bug in QuotedString class when the escaped quote string is not a
   repeated character. (Issue #263)
 
+- Added a caseless parameter to the `CloseMatch` class to allow for casing to be ignored when checking for close matches
+
+
 
 Version 3.0.0b3 - August, 2021
 ------------------------------
diff --git a/docs/HowToUsePyparsing.rst b/docs/HowToUsePyparsing.rst
index 95178e4..61a0580 100644
--- a/docs/HowToUsePyparsing.rst
+++ b/docs/HowToUsePyparsing.rst
@@ -6,7 +6,7 @@ Using the pyparsing module
 :address: ptmcg@users.sourceforge.net
 
 :revision: 3.0.1
-:date: August, 2021
+:date: September, 2021
 
 :copyright: Copyright |copy| 2003-2021 Paul McGuire.
 
@@ -31,12 +31,19 @@ using the Python interpreter's built-in ``help()`` function). You will also
 find many example scripts in the `examples <https://github.com/pyparsing/pyparsing/tree/master/examples>`_
 directory of the pyparsing GitHub repo.
 
-Note: In pyparsing 3.0, many method and function names which were
+*Note: In pyparsing 3.0, many method and function names which were
 originally written using camelCase have been converted to PEP8-compatible
-snake_case. So ``parseString()`` is being renamed to ``parse_string()``, 
+snake_case. So ``parseString()`` is being renamed to ``parse_string()``,
 ``delimitedList`` to ``delimited_list``, and so on. You may see the old
 names in legacy parsers, and they will be supported for a time with
-synonyms, but the synonyms will be removed in a future release.
+synonyms, but the synonyms will be removed in a future release.*
+
+*If you are using this documentation, but working with a 2.4.x version of pyparsing,
+you'll need to convert methods and arguments from the documented snake_case
+names to the legacy camelCase names. In pyparsing 3.0.x, both forms are
+supported, but the legacy forms are deprecated; they will be dropped in a
+future release.*
+
 
 Steps to follow
 ===============
diff --git a/pyparsing/__init__.py b/pyparsing/__init__.py
index 9b20db0..daf4bd1 100644
--- a/pyparsing/__init__.py
+++ b/pyparsing/__init__.py
@@ -103,7 +103,7 @@ __version__ = (
         __version_info__.release_level == "final"
     ]
 )
-__version_time__ = "16 August 2021 05:31 UTC"
+__version_time__ = "2 September 2021 17:43 UTC"
 __versionTime__ = __version_time__
 __author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"
 
@@ -125,9 +125,12 @@ from .common import (
 )
 
 # define backward compat synonyms
-pyparsing_unicode = unicode
-pyparsing_common = common
-pyparsing_test = testing
+if "pyparsing_unicode" not in globals():
+    pyparsing_unicode = unicode
+if "pyparsing_common" not in globals():
+    pyparsing_common = common
+if "pyparsing_test" not in globals():
+    pyparsing_test = testing
 
 core_builtin_exprs += common_builtin_exprs + helper_builtin_exprs
 
diff --git a/pyparsing/core.py b/pyparsing/core.py
index 7163559..d0d36e5 100644
--- a/pyparsing/core.py
+++ b/pyparsing/core.py
@@ -407,7 +407,7 @@ class ParserElement(ABC):
 
     def __init__(self, savelist: bool = False):
         self.parseAction: List[ParseAction] = list()
-        self.failAction = None
+        self.failAction: OptionalType[ParseFailAction] = None
         self.customName = None
         self._defaultName = None
         self.resultsName = None
@@ -428,7 +428,9 @@ class ParserElement(ABC):
         self.modalResults = True
         # custom debug actions
         self.debugActions: Tuple[
-            DebugStartAction, DebugSuccessAction, DebugExceptionAction
+            OptionalType[DebugStartAction],
+            OptionalType[DebugSuccessAction],
+            OptionalType[DebugExceptionAction],
         ] = (None, None, None)
         self.re = None
         # avoid redundant calls to preParse
@@ -461,7 +463,7 @@ class ParserElement(ABC):
         cpy.parseAction = self.parseAction[:]
         cpy.ignoreExprs = self.ignoreExprs[:]
         if self.copyDefaultWhiteChars:
-            cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
+            cpy.whiteChars = set(ParserElement.DEFAULT_WHITE_CHARS)
         return cpy
 
     def set_results_name(
@@ -855,7 +857,7 @@ class ParserElement(ABC):
     _parse = _parseNoCache
 
     @staticmethod
-    def reset_cache() -> NoReturn:
+    def reset_cache() -> None:
         ParserElement.packrat_cache.clear()
         ParserElement.packrat_cache_stats[:] = [0] * len(
             ParserElement.packrat_cache_stats
@@ -866,7 +868,7 @@ class ParserElement(ABC):
     _left_recursion_enabled = False
 
     @staticmethod
-    def disable_memoization() -> NoReturn:
+    def disable_memoization() -> None:
         """
         Disables active Packrat or Left Recursion parsing and their memoization
 
@@ -882,7 +884,7 @@ class ParserElement(ABC):
     @staticmethod
     def enable_left_recursion(
         cache_size_limit: OptionalType[int] = None, *, force=False
-    ) -> NoReturn:
+    ) -> None:
         """
         Enables "bounded recursion" parsing, which allows for both direct and indirect
         left-recursion. During parsing, left-recursive :class:`Forward` elements are
@@ -928,7 +930,7 @@ class ParserElement(ABC):
         ParserElement._left_recursion_enabled = True
 
     @staticmethod
-    def enable_packrat(cache_size_limit: int = 128, *, force: bool = False) -> NoReturn:
+    def enable_packrat(cache_size_limit: int = 128, *, force: bool = False) -> None:
         """
         Enables "packrat" parsing, which adds memoizing to the parsing logic.
         Repeated parse attempts at the same string location (which happens
@@ -1169,7 +1171,7 @@ class ParserElement(ABC):
 
     def search_string(
         self, instring: str, max_matches: int = _MAX_INT, *, maxMatches: int = _MAX_INT
-    ) -> List[ParseResults]:
+    ) -> ParseResults:
         """
         Another extension to :class:`scan_string`, simplifying the access to the tokens found
         to match the given parse expression.  May be called with optional
@@ -1205,7 +1207,7 @@ class ParserElement(ABC):
     def split(
         self,
         instring: str,
-        maxsplit: bool = _MAX_INT,
+        maxsplit: int = _MAX_INT,
         include_separators: bool = False,
         *,
         includeSeparators=False,
@@ -2030,7 +2032,8 @@ class ParserElement(ABC):
             from .diagram import to_railroad, railroad_to_html
         except ImportError as ie:
             raise Exception(
-                "must install 'Railroad-Diagram Generator' from https://pypi.org/project/railroad-diagrams to generate parser railroad diagrams"
+                "must install 'Railroad-Diagram Generator' from https://pypi.org/project/railroad-diagrams"
+                "and jinja2 from https://pypi.org/project/jinja2 to generate parser railroad diagrams"
             ) from ie
 
         railroad = to_railroad(self, vertical=vertical, diagram_kwargs=kwargs)
@@ -2380,6 +2383,7 @@ class CloseMatch(Token):
     :class:`CloseMatch` takes parameters:
 
      - ``match_string`` - string to be matched
+     - ``caseless`` - a boolean indicating whether to ignore casing when comparing characters
      - ``max_mismatches`` - (``default=1``) maximum number of
        mismatches allowed to count as a match
 
@@ -2409,7 +2413,7 @@ class CloseMatch(Token):
     """
 
     def __init__(
-        self, match_string: str, max_mismatches: int = None, *, maxMismatches: int = 1
+        self, match_string: str, max_mismatches: int = None, *, maxMismatches: int = 1, caseless=False
     ):
         maxMismatches = max_mismatches if max_mismatches is not None else maxMismatches
         super().__init__()
@@ -2418,6 +2422,7 @@ class CloseMatch(Token):
         self.errmsg = "Expected {!r} (with up to {} mismatches)".format(
             self.match_string, self.maxMismatches
         )
+        self.caseless = caseless
         self.mayIndexError = False
         self.mayReturnEmpty = False
 
@@ -2439,6 +2444,9 @@ class CloseMatch(Token):
                 zip(instring[loc:maxloc], match_string)
             ):
                 src, mat = s_m
+                if self.caseless:
+                    src, mat = src.lower(), mat.lower()
+
                 if src != mat:
                     mismatches.append(match_stringloc)
                     if len(mismatches) > maxMismatches:
@@ -3564,8 +3572,8 @@ class And(ParseExpression):
         def _generateDefaultName(self):
             return "-"
 
-    def __init__(self, exprs: IterableType[ParserElement], savelist: bool = True):
-        exprs: List["ParserElement"] = list(exprs)
+    def __init__(self, exprs_arg: IterableType[ParserElement], savelist: bool = True):
+        exprs: List["ParserElement"] = list(exprs_arg)
         if exprs and Ellipsis in exprs:
             tmp = []
             for i, expr in enumerate(exprs):
diff --git a/pyparsing/diagram/__init__.py b/pyparsing/diagram/__init__.py
index 03264fa..e96d49c 100644
--- a/pyparsing/diagram/__init__.py
+++ b/pyparsing/diagram/__init__.py
@@ -342,7 +342,7 @@ def _to_diagram_element(
             ret = EditablePartial.from_call(railroad.Choice, 0, items=[])
         else:
             ret = EditablePartial.from_call(railroad.HorizontalChoice, items=[])
-    elif isinstance(element, pyparsing.Optional):
+    elif isinstance(element, pyparsing.Opt):
         ret = EditablePartial.from_call(railroad.Optional, item="")
     elif isinstance(element, pyparsing.OneOrMore):
         ret = EditablePartial.from_call(railroad.OneOrMore, item="")
diff --git a/pyparsing/helpers.py b/pyparsing/helpers.py
index 03cf695..6b7b909 100644
--- a/pyparsing/helpers.py
+++ b/pyparsing/helpers.py
@@ -665,7 +665,12 @@ class OpAssoc(Enum):
 
 
 InfixNotationOperatorSpec = Tuple[
-    Union[ParserElement, str], int, OpAssoc, OptionalType[ParseAction]
+    Union[
+        ParserElement, str, Tuple[Union[ParserElement, str], Union[ParserElement, str]]
+    ],
+    int,
+    OpAssoc,
+    OptionalType[ParseAction],
 ]
 
 
@@ -765,6 +770,9 @@ def infix_notation(
                     "if numterms=3, opExpr must be a tuple or list of two expressions"
                 )
             opExpr1, opExpr2 = opExpr
+            term_name = "{}{} term".format(opExpr1, opExpr2)
+        else:
+            term_name = "{} term".format(opExpr)
 
         if not 1 <= arity <= 3:
             raise ValueError("operator must be unary (1), binary (2), or ternary (3)")
@@ -772,8 +780,7 @@ def infix_notation(
         if rightLeftAssoc not in (OpAssoc.LEFT, OpAssoc.RIGHT):
             raise ValueError("operator must indicate right or left associativity")
 
-        termName = "%s term" % opExpr if arity < 3 else "%s%s term" % opExpr
-        thisExpr = Forward().set_name(termName)
+        thisExpr = Forward().set_name(term_name)
         if rightLeftAssoc is OpAssoc.LEFT:
             if arity == 1:
                 matchExpr = _FB(lastExpr + opExpr) + Group(
@@ -816,7 +823,7 @@ def infix_notation(
                 matchExpr.set_parse_action(*pa)
             else:
                 matchExpr.set_parse_action(pa)
-        thisExpr <<= matchExpr.set_name(termName) | lastExpr
+        thisExpr <<= matchExpr.set_name(term_name) | lastExpr
         lastExpr = thisExpr
     ret <<= lastExpr
     return ret
diff --git a/pyparsing/results.py b/pyparsing/results.py
index 172074d..3d52fbb 100644
--- a/pyparsing/results.py
+++ b/pyparsing/results.py
@@ -3,9 +3,10 @@
 from collections.abc import MutableMapping, Mapping, MutableSequence
 import pprint
 from weakref import ref as wkref
+from typing import Tuple, Any
 
 str_type = (str, bytes)
-_generator_type = type((x for x in ()))
+_generator_type = type((_ for _ in ()))
 
 
 class _ParseResultsWithOffset:
@@ -70,7 +71,7 @@ class ParseResults:
         - year: 1999
     """
 
-    _null_values = (None, "", [], ())
+    _null_values: Tuple[Any, ...] = (None, "", [], ())
 
     __slots__ = [
         "_name",
diff --git a/pyparsing/unicode.py b/pyparsing/unicode.py
index f41d2a0..cbf6865 100644
--- a/pyparsing/unicode.py
+++ b/pyparsing/unicode.py
@@ -2,6 +2,7 @@
 
 import sys
 from itertools import filterfalse
+from typing import List, Tuple
 
 
 class _lazyclassproperty:
@@ -39,7 +40,7 @@ class unicode_set:
             pass
     """
 
-    _ranges = []
+    _ranges: List[Tuple[int, ...]] = []
 
     @classmethod
     def _get_chars_for_ranges(cls):
@@ -95,30 +96,30 @@ class pyparsing_unicode(unicode_set):
     A namespace class for defining common language unicode_sets.
     """
 
-    _ranges = [(32, sys.maxunicode)]
+    _ranges: List[Tuple[int, ...]] = [(32, sys.maxunicode)]
 
     class Latin1(unicode_set):
         "Unicode set for Latin-1 Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0020, 0x007E),
             (0x00A0, 0x00FF),
         ]
 
     class LatinA(unicode_set):
         "Unicode set for Latin-A Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0100, 0x017F),
         ]
 
     class LatinB(unicode_set):
         "Unicode set for Latin-B Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0180, 0x024F),
         ]
 
     class Greek(unicode_set):
         "Unicode set for Greek Unicode Character Ranges"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0342, 0x0345),
             (0x0370, 0x0377),
             (0x037A, 0x037F),
@@ -158,7 +159,7 @@ class pyparsing_unicode(unicode_set):
 
     class Cyrillic(unicode_set):
         "Unicode set for Cyrillic Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0400, 0x052F),
             (0x1C80, 0x1C88),
             (0x1D2B,),
@@ -171,7 +172,7 @@ class pyparsing_unicode(unicode_set):
 
     class Chinese(unicode_set):
         "Unicode set for Chinese Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x2E80, 0x2E99),
             (0x2E9B, 0x2EF3),
             (0x31C0, 0x31E3),
@@ -194,18 +195,18 @@ class pyparsing_unicode(unicode_set):
 
     class Japanese(unicode_set):
         "Unicode set for Japanese Unicode Character Range, combining Kanji, Hiragana, and Katakana ranges"
-        _ranges = []
+        _ranges: List[Tuple[int, ...]] = []
 
         class Kanji(unicode_set):
             "Unicode set for Kanji Unicode Character Range"
-            _ranges = [
+            _ranges: List[Tuple[int, ...]] = [
                 (0x4E00, 0x9FBF),
                 (0x3000, 0x303F),
             ]
 
         class Hiragana(unicode_set):
             "Unicode set for Hiragana Unicode Character Range"
-            _ranges = [
+            _ranges: List[Tuple[int, ...]] = [
                 (0x3041, 0x3096),
                 (0x3099, 0x30A0),
                 (0x30FC,),
@@ -217,7 +218,7 @@ class pyparsing_unicode(unicode_set):
 
         class Katakana(unicode_set):
             "Unicode set for Katakana  Unicode Character Range"
-            _ranges = [
+            _ranges: List[Tuple[int, ...]] = [
                 (0x3099, 0x309C),
                 (0x30A0, 0x30FF),
                 (0x31F0, 0x31FF),
@@ -231,7 +232,7 @@ class pyparsing_unicode(unicode_set):
 
     class Hangul(unicode_set):
         "Unicode set for Hangul (Korean) Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x1100, 0x11FF),
             (0x302E, 0x302F),
             (0x3131, 0x318E),
@@ -251,17 +252,17 @@ class pyparsing_unicode(unicode_set):
 
     Korean = Hangul
 
-    class CJK(Chinese, Japanese, Korean):
+    class CJK(Chinese, Japanese, Hangul):
         "Unicode set for combined Chinese, Japanese, and Korean (CJK) Unicode Character Range"
         pass
 
     class Thai(unicode_set):
         "Unicode set for Thai Unicode Character Range"
-        _ranges = [(0x0E01, 0x0E3A), (0x0E3F, 0x0E5B)]
+        _ranges: List[Tuple[int, ...]] = [(0x0E01, 0x0E3A), (0x0E3F, 0x0E5B)]
 
     class Arabic(unicode_set):
         "Unicode set for Arabic Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0600, 0x061B),
             (0x061E, 0x06FF),
             (0x0700, 0x077F),
@@ -269,7 +270,7 @@ class pyparsing_unicode(unicode_set):
 
     class Hebrew(unicode_set):
         "Unicode set for Hebrew Unicode Character Range"
-        _ranges = [
+        _ranges: List[Tuple[int, ...]] = [
             (0x0591, 0x05C7),
             (0x05D0, 0x05EA),
             (0x05EF, 0x05F4),
@@ -283,7 +284,7 @@ class pyparsing_unicode(unicode_set):
 
     class Devanagari(unicode_set):
         "Unicode set for Devanagari Unicode Character Range"
-        _ranges = [(0x0900, 0x097F), (0xA8E0, 0xA8FF)]
+        _ranges: List[Tuple[int, ...]] = [(0x0900, 0x097F), (0xA8E0, 0xA8FF)]
 
 
 pyparsing_unicode.Japanese._ranges = (
diff --git a/pyparsing/util.py b/pyparsing/util.py
index bbbfcd5..004476d 100644
--- a/pyparsing/util.py
+++ b/pyparsing/util.py
@@ -4,6 +4,8 @@ import types
 import collections
 import itertools
 from functools import lru_cache
+from typing import List
+
 
 _bslash = chr(92)
 
@@ -11,8 +13,8 @@ _bslash = chr(92)
 class __config_flags:
     """Internal class for defining compatibility and debugging flags"""
 
-    _all_names = []
-    _fixed_names = []
+    _all_names: List[str] = []
+    _fixed_names: List[str] = []
     _type_desc = "configuration"
 
     @classmethod


Test patch:
diff --git a/tests/test_simple_unit.py b/tests/test_simple_unit.py
index 0e9c25f..6d06b34 100644
--- a/tests/test_simple_unit.py
+++ b/tests/test_simple_unit.py
@@ -422,6 +422,8 @@ class TestParseAction(PyparsingExpressionTestCase):
 
 
 class TestResultsModifyingParseAction(PyparsingExpressionTestCase):
+    # do not make staticmethod
+    # @staticmethod
     def compute_stats_parse_action(t):
         # by the time this parse action is called, parsed numeric words
         # have been converted to ints by a previous parse action, so
@@ -486,6 +488,8 @@ class TestTransformStringUsingParseActions(PyparsingExpressionTestCase):
         "/": "I",
     }
 
+    # do not make staticmethod
+    # @staticmethod
     def markup_convert(t):
         htmltag = TestTransformStringUsingParseActions.markup_convert_map[
             t.markup_symbol
diff --git a/tests/test_unit.py b/tests/test_unit.py
index e633d64..57322ec 100644
--- a/tests/test_unit.py
+++ b/tests/test_unit.py
@@ -5857,6 +5857,38 @@ class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase):
                 else ("no match", "match")[r[1].mismatches == exp],
             )
 
+    def testCloseMatchCaseless(self):
+
+        searchseq = pp.CloseMatch("ATCATCGAATGGA", 2, caseless=True)
+
+        _, results = searchseq.runTests(
+            """
+            atcatcgaatgga
+            xtcatcgaatggx
+            atcatcgaaxgga
+            atcaxxgaatgga
+            atcaxxgaatgxa
+            atcaxxgaatgg
+            """
+        )
+        expected = ([], [0, 12], [9], [4, 5], None, None)
+
+        for r, exp in zip(results, expected):
+            if exp is not None:
+                self.assertEqual(
+                    exp,
+                    r[1].mismatches,
+                    "fail CaselessCloseMatch between {!r} and {!r}".format(
+                        searchseq.match_string, r[0]
+                    ),
+                )
+            print(
+                r[0],
+                "exc: %s" % r[1]
+                if exp is None and isinstance(r[1], Exception)
+                else ("no match", "match")[r[1].mismatches == exp],
+            )
+
     def testDefaultKeywordChars(self):
 
         with self.assertRaisesParseException(