Repository: facelessuser/wcmatch

Issue:
Provide a method to determine if a pattern is magic
It would be useful to have a function that returns a bool value of whether or not the pattern provided is "magic" or not.

An example (which I need) where this would be useful is in determining the concrete base path of a pattern:
- `path/to/something/**/test*` > `path/to/something`.

As a work around, I can just use the regex included in the library, or compare the pattern with its glob-escaped counterpart, but ideally this would be addressed by a dedicated function in the package.


Discussion:
facelessuser: @gir-bot lgtm

Patch:
diff --git a/docs/src/dictionary/en-custom.txt b/docs/src/dictionary/en-custom.txt
index fd4c7f9..dc81408 100644
--- a/docs/src/dictionary/en-custom.txt
+++ b/docs/src/dictionary/en-custom.txt
@@ -37,6 +37,7 @@ configurability
 deMorgan
 deduping
 deprecations
+dev
 falsy
 filename
 filenames
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index ed313f1..946bb9c 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,7 +1,10 @@
 # Changelog
 
-## 8.0.2
+## 8.1.dev
 
+- **NEW**: Add `is_magic` function to the `glob` and `fnamtch` library.
+- **NEW**: `fnmatch` now has `escape` and `raw_escape` available via its API. `fnmatch` escape variants will not use
+  path/drive logic when evaluating a file name.
 - **FIX**: Use `os.fspath` to convert path-like objects to string, whatever the preferred return for `os.path` is what
   Wildcard Match will accept. Don't try to convert paths via `__str__` or `__bytes__`.
 - **FIX**: Better checking of types to ensure consistent failure if the path, pattern, or root directory of are not all
diff --git a/docs/src/markdown/fnmatch.md b/docs/src/markdown/fnmatch.md
index 0d24445..cf29ed7 100644
--- a/docs/src/markdown/fnmatch.md
+++ b/docs/src/markdown/fnmatch.md
@@ -165,6 +165,108 @@ we wrap the entire group to be captured: `#!py3 '+(a)'` --> `#!py3 r'((a)+)'`.
 !!! new "New 7.1"
     Translate patterns now provide capturing groups for [`EXTMATCH`](#extmatch) groups.
 
+#### `fnmatch.escape` {: #escape}
+
+```py3
+def escape(pattern):
+```
+
+This escapes special glob meta characters so they will be treated as literal characters. It escapes using backslashes.
+It will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`. and `\` regardless of what feature
+is or is not enabled. Its intended use is escaping a filename for use in a pattern.
+
+```pycon3
+>>> from wcmatch import fnmatch
+>>> fnmatch.escape('**file**{}.txt')
+'\\*\\*file\\*\\*\\{\\}.txt'
+>>> fnmatch.fnmatch('**file**{}.txt', fnmatch.escape('**file**{}.txt'))
+True
+```
+
+!!! new "New 8.1"
+    An `escape` variant for `fnmatch` was made available in 8.1.
+
+#### `glob.raw_escape` {: #raw_escape}
+
+```py3
+def raw_escape(pattern, raw_chars=True):
+```
+
+`raw_escape` is kind of a niche function and 99% of the time, it is recommended to use [`escape`](#escape).
+
+The big difference between `raw_escape` and [`escape`](#escape) is how `\` are handled. `raw_escape` is mainly for
+filenames provided to Python via an interface that doesn't process Python strings like they normally are, for instance
+an input in a GUI.
+
+To illustrate, you may have an interface to input path names, but may want to take advantage of Python Unicode
+references. Normally, on a python command line, you can do this:
+
+```pycon3
+>>> 'El Ni\u00f1o'
+'El Niño'
+```
+
+But when in a GUI interface, if a user inputs the same, it's like getting a raw string.
+
+```pycon3
+>>> r'El Ni\u00f1o'
+'El Ni\\u00f1o'
+```
+
+`raw_escape` will take a raw string in the above format and resolve character escapes and escape the filename as if it
+was a normal string.  Though backslashes aren't normally used in filenames, notice to do this, we must treat literal
+backslashes as an escaped backslash.
+
+```pycon3
+>>> fnmatch.escape('folder\\El Ni\u00f1o', unix=False)
+'folder\\\\El Niño'
+>>> fnmatch.raw_escape(r'folder\\El Ni\u00f1o')
+'folder\\\\El Niño'
+```
+
+Handling of raw character references can be turned off if desired:
+
+```pycon3
+>>> glob.raw_escape(r'my\\file-\x31.txt', unix=False)
+'my\\\\file\\-1.txt'
+>>> glob.raw_escape(r'my\\file-\x31.txt', unix=False, raw_chars=False)
+'my\\\\file\\-\\\\x31.txt'
+```
+
+Outside of the treatment of `\`, `raw_escape` will function just like [`escape`](#escape):
+
+!!! new "New 8.1"
+    A `raw_escape` variant for `fnmatch` was made available in 8.1.
+
+### `fnmatch.is_magic` {: #is_magic}
+
+```py3
+def is_magic(pattern, *, flags=0):
+    """Check if the pattern is likely to be magic."""
+```
+
+This checks a given `pattern` to see if it is "magic" or not. The check is based on the enabled features via `flags`.
+
+```pycon3
+>>> fnmatch.is_magic('test')
+False
+>>> fnmatch.is_magic('[test]ing?')
+True
+```
+
+The table below illustrates which symbols are searched for based on the given feature, the first row being the base
+symbols that are checked. In the case of [`NEGATE`](#negate), if [`MINUSNEGATE`](#minusnegate) is also enabled,
+[`MINUSNEGATE`](#minusnegate)'s symbols will be searched instead of [`NEGATE`](#negate)'s symbols.
+
+Features                      | Symbols
+----------------------------- | -------
+                              | `?*[]\`
+[`EXTMATCH`](#extmatch)       | `()`
+[`BRACE`](#brace)             | `{}`
+[`NEGATE`](#negate)           | `!`
+[`MINUSNEGATE`](#minusnegate) | `-`
+[`SPLIT`](#split)             | `|`
+
 ## Flags
 
 #### `fnmatch.CASE, fnmatch.C` {: #case}
diff --git a/docs/src/markdown/glob.md b/docs/src/markdown/glob.md
index 89d05ae..01f239d 100644
--- a/docs/src/markdown/glob.md
+++ b/docs/src/markdown/glob.md
@@ -492,8 +492,8 @@ def escape(pattern, unix=None):
 ```
 
 This escapes special glob meta characters so they will be treated as literal characters. It escapes using backslashes.
-It will escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `^`, `{`, `}`. and `\`. Its intended use is escaping paths
-  or path parts for use in patterns.
+It will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`. and `\` regardless of what feature
+is or is not enabled. Its intended use is escaping paths or path parts for use in patterns.
 
 ```pycon3
 >>> from wcmatch import glob
@@ -503,10 +503,9 @@ It will escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `^`, `{`, `}`. and `
 True
 ```
 
-Raw strings can be used as well, but if you wish to use raw strings that represent a Python string (where `\` are
-actually denoted by `\\`), then you may wish to use [`raw_escape`](#raw_escape).
-
-`escape` can also handle Windows style with `/` or `\\` path separators:
+`escape` can also handle Windows style paths with `/` or `\` path separators. It is usually recommended to use `/` as
+Windows backslashes are only supported via a special escape, but `\` will be expanded to an escaped backslash
+(represented in a raw string as `#!py3 r'\\'` or a normal string as `#!py3 '\\\\'`).
 
 ```pycon3
 >>> from wmcatch import glob
@@ -520,10 +519,10 @@ True
 True
 ```
 
-On a Windows system, meta characters are not processed in drives/mounts except pattern expansion characters such `{`,
-`}`, and `|` when using [`BRACE`](#brace) and/or [`SPLIT`](#split). These pattern expansion happens in an earlier step
-and are handled a bit differently. These characters are the only ones that need to be escaped in Windows drives and will
-be properly escaped by `escape`.
+On a Windows system, meta characters are not processed in drives/UNC mounts except for pattern expansion characters.
+`{`, `}`, and `|`, when using [`BRACE`](#brace) and/or [`SPLIT`](#split), are the only meta characters that can affect
+drives/UNC mounts; therefore, they are the only characters that need to be escaped. `escape`, when it detects or is
+informed that it is processing a Windows path, will properly find and handle these drives/UNC mounts.
 
 ```pycon3
 >>> from wmcatch import glob
@@ -555,25 +554,38 @@ escaping will be used.
 def raw_escape(pattern, unix=None, raw_chars=True):
 ```
 
-This is like [`escape`](#escape) except it will escape paths provided as raw strings. Or more specifically, strings
-whose content is a representation of a Python string. Simply using Python raw strings does not mean you need to use
-`raw_escape` as `escape` can handle raw strings well enough:
+`raw_escape` is kind of a niche function and 99% of the time, it is recommended to use [`escape`](#escape).
+
+The big difference between `raw_escape` and [`escape`](#escape) is how `\` are handled. `raw_escape` is mainly for paths
+provided to Python via an interface that doesn't process Python strings like they normally are, for instance an input
+in a GUI.
+
+To illustrate, you may have an interface to input path names, but may want to take advantage of Python Unicode
+references. Normally, on a python command line, you can do this:
 
 ```pycon3
->>> glob.escape(r'my\file-[work].txt', unix=False)
-'my\\\\file\\-\\[work\\].txt'
+>>> 'folder\\El Ni\u00f1o'
+'folder\\El Niño'
 ```
 
-But if you are accepting an input from a source that is giving you a representation of a Python string (where `\` is
-represented by two `\`), then `raw_escape` is what you want:
+But when in a GUI interface, if a user inputs the same, it's like getting a raw string.
 
 ```pycon3
->>> glob.raw_escape(r'my\\file-[work].txt', unix=False)
-'my\\\\file\\-\\[work\\].txt'
+>>> r'folder\\El Ni\u00f1o'
+'folder\\\\El Ni\\u00f1o'
 ```
 
-By default `raw_escape` always translates Python character back references into actual characters, but if this is not
-needed, a new option called `raw_chars` (`True` by default) has been added so this behavior can be disabled:
+`raw_escape` will take a raw string in the above format and resolve character escapes and escape the path as if it was
+a normal string.  Notice to do this, we must treat literal Windows' path backslashes as an escaped backslash.
+
+```pycon3
+>>> glob.escape('folder\\El Ni\u00f1o', unix=False)
+'folder\\\\El Niño'
+>>> glob.raw_escape(r'folder\\El Ni\u00f1o')
+'folder\\\\El Niño'
+```
+
+Handling of raw character references can be turned off if desired:
 
 ```pycon3
 >>> glob.raw_escape(r'my\\file-\x31.txt', unix=False)
@@ -582,19 +594,7 @@ needed, a new option called `raw_chars` (`True` by default) has been added so th
 'my\\\\file\\-\\\\x31.txt'
 ```
 
-It also handles Windows style paths with `/` or `\\` path separators:
-
-```pycon3
->>> from wcmatch import glob
->>> glob.raw_escape(r'some\\path?\\\x2a\x2afile\x2a\x2a{}.txt', unix=False)
-'some\\\\path\\?\\\\\\*\\*file\\*\\*\\{\\}.txt'
->>> glob.globmatch('some\\path?\\**file**{}.txt', glob.raw_escape(r'some\\path?\\\x2a\x2afile\x2a\x2a{}.txt', unix=False), flags=glob.RAWCHARS | glob.FORCEWIN)
-True
->>> glob.raw_escape(r'some/path?/\x2a\x2afile\x2a\x2a{}.txt', unix=False)
-'some/path\\?/\\*\\*file\\*\\*\\{\\}.txt'
->>> glob.globmatch('some\\path?\\**file**{}.txt', glob.raw_escape(r'some/path?/\x2a\x2afile\x2a\x2a{}.txt', unix=False), flags=glob.RAWCHARS | glob.FORCEWIN)
-True
-```
+Outside of the treatment of `\`, `raw_escape` will function just like [`escape`](#escape):
 
 `raw_escape` will detect the system it is running on and pick Windows escape logic or Linux/Unix logic. Since
 [`globmatch`](#globmatch) allows you to match Unix style paths on a Windows system, and vice versa, you can force
@@ -616,6 +616,58 @@ escaping will be used.
 
     `raw_chars` option was added.
 
+### `glob.is_magic` {: #is_magic}
+
+```py3
+def is_magic(pattern, *, flags=0):
+    """Check if the pattern is likely to be magic."""
+```
+
+This checks a given `pattern` to see if "magic" symbols are present or not. The check is based on the enabled features
+via `flags`.
+
+```pycon3
+>>> glob.is_magic('test')
+False
+>>> glob.is_magic('[test]ing?')
+True
+```
+
+Wildcard Match treats `\` as special/magic as it is used to escape characters. In general, even though Wildcard Match
+will treat an escaped backslash as a directory separator on Windows, it is recommend to use `/` for directory
+separators. If you just want to check a Windows path for magic characters, you can covert separators to `/`. And if you
+are on Linux/Unix and for some reason chose to use `\` in your file name, it will also be considered magic.
+
+When `is_magic` is called, the system it is called on is detected automatically and/or inferred from flags such as
+[`FORCEUNIX`](#forceunix) or [`FORCEWIN`](#forcewin). If the pattern is checked against a Windows system, UNC server/mount
+names will be detected and treated differently. Wildcard Match cannot detect and glob all possible connected server
+names, so they are treated differently and cannot contain magic except in three cases:
+
+1. The drive is using backslashes as backslashes are treated as magic.
+2. [`BRACE`](#brace) is enabled and either `{` or `}` are found in the drive name.
+3. [`SPLIT`](#split) is enabled and `|` is found in the drive name.
+
+```pycon3
+>>> glob.is_magic('//?/UNC/server/mount{}/', flags=glob.FORCEWIN)
+False
+>>> glob.is_magic('//?/UNC/server/mount{}/', flags=glob.FORCEWIN | glob.BRACE)
+True
+```
+
+The table below illustrates which symbols are searched for based on the given feature, the first row being the base
+symbols that are checked. In the case of [`NEGATE`](#negate), if [`MINUSNEGATE`](#minusnegate) is also enabled,
+[`MINUSNEGATE`](#minusnegate)'s symbols will be searched instead of [`NEGATE`](#negate)'s symbols.
+
+Features                      | Symbols
+----------------------------- | -------
+                              | `?*[]\`
+[`EXTMATCH`](#extmatch)       | `()`
+[`BRACE`](#brace)             | `{}`
+[`NEGATE`](#negate)           | `!`
+[`MINUSNEGATE`](#minusnegate) | `-`
+[`SPLIT`](#split)             | `|`
+[`GLOBTILDE`](#globtilde)     | `~`
+
 ## Flags
 
 #### `glob.CASE, glob.C` {: #case}
diff --git a/wcmatch/__meta__.py b/wcmatch/__meta__.py
index 1cb94c9..14754b4 100644
--- a/wcmatch/__meta__.py
+++ b/wcmatch/__meta__.py
@@ -189,5 +189,5 @@ def parse_version(ver):
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(8, 0, 2, "final")
+__version_info__ = Version(8, 1, 0, ".dev")
 __version__ = __version_info__._get_canonical()
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index ce3a862..aaed5db 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -74,12 +74,42 @@ RE_WIN_DRIVE = (
 )
 
 RE_MAGIC_ESCAPE = (
-    re.compile(r'([-!~*?()\[\]|^{}]|(?<!\\)(?:(?:[\\]{2})*)\\(?!\\))'),
-    re.compile(br'([-!~*?()\[\]|^{}]|(?<!\\)(?:(?:[\\]{2})*)\\(?!\\))')
+    re.compile(r'([-!~*?()\[\]|{}]|(?<!\\)(?:(?:[\\]{2})*)\\(?!\\))'),
+    re.compile(br'([-!~*?()\[\]|{}]|(?<!\\)(?:(?:[\\]{2})*)\\(?!\\))')
 )
+
+MAGIC_DEF = (
+    frozenset("*?[]\\"),
+    frozenset(b"*?[]\\")
+)
+MAGIC_SPLIT = (
+    frozenset("|"),
+    frozenset(b"|")
+)
+MAGIC_NEGATE = (
+    frozenset('!'),
+    frozenset(b'!')
+)
+MAGIC_MINUS_NEGATE = (
+    frozenset('-'),
+    frozenset(b'-')
+)
+MAGIC_TILDE = (
+    frozenset('~'),
+    frozenset(b'~')
+)
+MAGIC_EXTMATCH = (
+    frozenset('()'),
+    frozenset(b'()')
+)
+MAGIC_BRACE = (
+    frozenset("{}"),
+    frozenset(b"{}")
+)
+
 RE_MAGIC = (
-    re.compile(r'([-!~*?(\[|^{\\])'),
-    re.compile(br'([-!~*?(\[|^{\\])')
+    re.compile(r'([-!~*?(\[|{\\])'),
+    re.compile(br'([-!~*?(\[|{\\])')
 )
 RE_WIN_DRIVE_MAGIC = (
     re.compile(r'([{}|]|(?<!\\)(?:(?:[\\]{2})*)\\(?!\\))'),
@@ -284,19 +314,19 @@ class PatternLimitException(Exception):
     """Pattern limit exception."""
 
 
-def raw_escape(pattern, unix=None, raw_chars=True):
+def raw_escape(pattern, unix=None, raw_chars=True, pathname=False):
     """Apply raw character transform before applying escape."""
 
-    return _escape(util.norm_pattern(pattern, False, raw_chars, True), unix, True)
+    return _escape(util.norm_pattern(pattern, False, raw_chars, True), unix, True, pathname=pathname)
 
 
-def escape(pattern, unix=None):
+def escape(pattern, unix=None, pathname=False):
     """Normal escape."""
 
-    return _escape(pattern, unix)
+    return _escape(pattern, unix, pathname=pathname)
 
 
-def _escape(pattern, unix=None, raw=False):
+def _escape(pattern, unix=None, raw=False, pathname=False):
     """Escape."""
 
     if isinstance(pattern, bytes):
@@ -324,7 +354,7 @@ def _escape(pattern, unix=None, raw=False):
     # So we shouldn't escape them as we'll just have to
     # detect and undo it later.
     length = 0
-    if ((unix is None and util.platform() == "windows") or unix is False):
+    if pathname and ((unix is None and util.platform() == "windows") or unix is False):
         m = drive_pat.match(pattern)
         if m:
             # Replace splitting magic chars
@@ -383,6 +413,55 @@ def _get_win_drive(pattern, regex=False, case_sensitive=False):
     return root_specified, drive, slash, end
 
 
+def is_magic(pattern, flags=0):
+    """Check if pattern is magic."""
+
+    magical = False
+    unix = is_unix_style(flags)
+
+    ptype = BYTES if isinstance(pattern, bytes) else UNICODE
+    drive_pat = RE_WIN_DRIVE[ptype]
+
+    if ptype == BYTES:
+        slash = b'\\'
+    else:
+        slash = '\\'
+
+    magic = set()
+    magic_drive = set() if unix else set(slash)
+
+    magic |= MAGIC_DEF[ptype]
+    if flags & BRACE:
+        magic |= MAGIC_BRACE[ptype]
+        magic_drive |= MAGIC_BRACE[ptype]
+    if flags & SPLIT:
+        magic |= MAGIC_SPLIT[ptype]
+        magic_drive |= MAGIC_SPLIT[ptype]
+    if flags & GLOBTILDE:
+        magic |= MAGIC_TILDE[ptype]
+    if flags & EXTMATCH:
+        magic |= MAGIC_EXTMATCH[ptype]
+    if flags & NEGATE:
+        if flags & MINUSNEGATE:
+            magic |= MAGIC_MINUS_NEGATE[ptype]
+        else:
+            magic |= MAGIC_NEGATE[ptype]
+
+    length = 0
+    if ((unix is None and util.platform() == "windows") or unix is False):
+        m = drive_pat.match(pattern)
+        if m:
+            drive = m.group(0)
+            length = len(drive)
+            if set(drive) & magic_drive:
+                magical = True
+
+    if not magical and set(pattern[length:]) & magic:
+        magical = True
+
+    return magical
+
+
 def is_negative(pattern, flags):
     """Check if negative pattern."""
 
diff --git a/wcmatch/fnmatch.py b/wcmatch/fnmatch.py
index 95fe242..d0c9f1e 100644
--- a/wcmatch/fnmatch.py
+++ b/wcmatch/fnmatch.py
@@ -27,7 +27,7 @@ __all__ = (
     "NEGATE", "MINUSNEGATE", "DOTMATCH", "BRACE", "SPLIT",
     "NEGATEALL", "FORCEWIN", "FORCEUNIX",
     "C", "I", "R", "N", "M", "D", "E", "S", "B", "A", "W", "U",
-    "translate", "fnmatch", "filter"
+    "translate", "fnmatch", "filter", "escape", "raw_escape", "is_magic"
 )
 
 C = CASE = _wcparse.CASE
@@ -103,3 +103,22 @@ def filter(filenames, patterns, *, flags=0, limit=_wcparse.PATTERN_LIMIT):  # no
         if obj.match(_wcparse.norm_slash(filename, flags) if not unix else filename):
             matches.append(filename)
     return matches
+
+
+def raw_escape(pattern, raw_chars=True):
+    """Apply raw character transform before applying escape."""
+
+    return _wcparse.raw_escape(pattern, raw_chars=raw_chars)
+
+
+def escape(pattern):
+    """Escape."""
+
+    return _wcparse.escape(pattern)
+
+
+def is_magic(pattern, *, flags=0):
+    """Check if the pattern is likely to be magic."""
+
+    flags = _flag_transform(flags)
+    return _wcparse.is_magic(pattern, flags)
diff --git a/wcmatch/glob.py b/wcmatch/glob.py
index 04dbcc2..3bbbcd9 100644
--- a/wcmatch/glob.py
+++ b/wcmatch/glob.py
@@ -33,7 +33,7 @@ __all__ = (
     "REALPATH", "FOLLOW", "MATCHBASE", "MARK", "NEGATEALL", "NODIR", "FORCEWIN", "FORCEUNIX", "GLOBTILDE",
     "NODOTDIR", "SCANDOTDIR",
     "C", "I", "R", "D", "E", "G", "N", "M", "B", "P", "L", "S", "X", 'K', "O", "A", "W", "U", "T", "Q", "Z", "SD",
-    "iglob", "glob", "globmatch", "globfilter", "escape", "raw_escape"
+    "iglob", "glob", "globmatch", "globfilter", "escape", "raw_escape", "is_magic"
 )
 
 # We don't use `util.platform` only because we mock it in tests,
@@ -606,10 +606,17 @@ def globfilter(filenames, patterns, *, flags=0, root_dir=None, limit=_wcparse.PA
 def raw_escape(pattern, unix=None, raw_chars=True):
     """Apply raw character transform before applying escape."""
 
-    return _wcparse.raw_escape(pattern, unix, raw_chars)
+    return _wcparse.raw_escape(pattern, unix, raw_chars, pathname=True)
 
 
 def escape(pattern, unix=None):
     """Escape."""
 
-    return _wcparse.escape(pattern, unix)
+    return _wcparse.escape(pattern, unix, pathname=True)
+
+
+def is_magic(pattern, *, flags=0):
+    """Check if the pattern is likely to be magic."""
+
+    flags = _flag_transform(flags)
+    return _wcparse.is_magic(pattern, flags)


Test patch:
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index 6abbfec..0891e76 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -2,6 +2,8 @@
 """Tests for `fnmatch`."""
 import unittest
 import re
+import sys
+import os
 import pytest
 import wcmatch.fnmatch as fnmatch
 from unittest import mock
@@ -481,6 +483,253 @@ class TestFnMatchTranslate(unittest.TestCase):
         self.assertTrue(len(fnmatch.translate(b'!test', flags=fnmatch.N | fnmatch.A)[0]) == 1)
 
 
+class TestIsMagic(unittest.TestCase):
+    """Test "is magic" logic."""
+
+    def test_default(self):
+        """Test default magic."""
+
+        self.assertTrue(fnmatch.is_magic("test*"))
+        self.assertTrue(fnmatch.is_magic("test["))
+        self.assertTrue(fnmatch.is_magic("test]"))
+        self.assertTrue(fnmatch.is_magic("test?"))
+        self.assertTrue(fnmatch.is_magic("test\\"))
+
+        self.assertFalse(fnmatch.is_magic("test~!()-/|{}"))
+
+    def test_extmatch(self):
+        """Test extended match magic."""
+
+        self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.EXTMATCH))
+        self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.EXTMATCH))
+        self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.EXTMATCH))
+        self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.EXTMATCH))
+        self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.EXTMATCH))
+        self.assertTrue(fnmatch.is_magic("test(", flags=fnmatch.EXTMATCH))
+        self.assertTrue(fnmatch.is_magic("test)", flags=fnmatch.EXTMATCH))
+
+        self.assertFalse(fnmatch.is_magic("test~!-/|{}", flags=fnmatch.EXTMATCH))
+
+    def test_negate(self):
+        """Test negate magic."""
+
+        self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.NEGATE))
+        self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.NEGATE))
+        self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.NEGATE))
+        self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.NEGATE))
+        self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.NEGATE))
+        self.assertTrue(fnmatch.is_magic("test!", flags=fnmatch.NEGATE))
+
+        self.assertFalse(fnmatch.is_magic("test~()-/|{}", flags=fnmatch.NEGATE))
+
+    def test_minusnegate(self):
+        """Test minus negate magic."""
+
+        self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+        self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+        self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+        self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+        self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+        self.assertTrue(fnmatch.is_magic("test-", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+
+        self.assertFalse(fnmatch.is_magic("test~()!/|{}", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE))
+
+    def test_brace(self):
+        """Test brace magic."""
+
+        self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.BRACE))
+        self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.BRACE))
+        self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.BRACE))
+        self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.BRACE))
+        self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.BRACE))
+        self.assertTrue(fnmatch.is_magic("test{", flags=fnmatch.BRACE))
+        self.assertTrue(fnmatch.is_magic("test}", flags=fnmatch.BRACE))
+
+        self.assertFalse(fnmatch.is_magic("test~!-/|", flags=fnmatch.BRACE))
+
+    def test_split(self):
+        """Test split magic."""
+
+        self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.SPLIT))
+        self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.SPLIT))
+        self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.SPLIT))
+        self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.SPLIT))
+        self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.SPLIT))
+        self.assertTrue(fnmatch.is_magic("test|", flags=fnmatch.SPLIT))
+
+        self.assertFalse(fnmatch.is_magic("test~()-!/", flags=fnmatch.SPLIT))
+
+    def test_all(self):
+        """Test tilde magic."""
+
+        flags = (
+            fnmatch.EXTMATCH |
+            fnmatch.NEGATE |
+            fnmatch.BRACE |
+            fnmatch.SPLIT
+        )
+
+        self.assertTrue(fnmatch.is_magic("test*", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test[", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test]", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test?", flags=flags))
+        self.assertTrue(fnmatch.is_magic(r"te\\st", flags=flags))
+        self.assertTrue(fnmatch.is_magic(r"te\st", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test!", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test|", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test(", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test)", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test{", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test}", flags=flags))
+        self.assertTrue(fnmatch.is_magic("test-", flags=flags | fnmatch.MINUSNEGATE))
+
+        self.assertFalse(fnmatch.is_magic("test-~", flags=flags))
+        self.assertFalse(fnmatch.is_magic("test!~", flags=flags | fnmatch.MINUSNEGATE))
+
+    def test_all_bytes(self):
+        """Test tilde magic."""
+
+        flags = (
+            fnmatch.EXTMATCH |
+            fnmatch.NEGATE |
+            fnmatch.BRACE |
+            fnmatch.SPLIT
+        )
+
+        self.assertTrue(fnmatch.is_magic(b"test*", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test[", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test]", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test?", flags=flags))
+        self.assertTrue(fnmatch.is_magic(rb"te\\st", flags=flags))
+        self.assertTrue(fnmatch.is_magic(rb"te\st", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test!", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test|", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test(", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test)", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test{", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test}", flags=flags))
+        self.assertTrue(fnmatch.is_magic(b"test-", flags=flags | fnmatch.MINUSNEGATE))
+
+        self.assertFalse(fnmatch.is_magic(b"test-~", flags=flags))
+        self.assertFalse(fnmatch.is_magic(b"test!~", flags=flags | fnmatch.MINUSNEGATE))
+
+
+class TestFnMatchEscapes(unittest.TestCase):
+    """Test escaping."""
+
+    def check_escape(self, arg, expected, raw=False, unix=None, raw_chars=True):
+        """Verify escapes."""
+
+        flags = 0
+        if unix is False:
+            flags = fnmatch.FORCEWIN
+        elif unix is True:
+            flags = fnmatch.FORCEUNIX
+
+        if raw:
+            self.assertEqual(fnmatch.raw_escape(arg, raw_chars=raw_chars), expected)
+            self.assertEqual(fnmatch.raw_escape(os.fsencode(arg), raw_chars=raw_chars), os.fsencode(expected))
+            file = (util.norm_pattern(arg, False, True) if raw_chars else arg).replace('\\\\', '\\')
+            self.assertTrue(
+                fnmatch.fnmatch(
+                    file,
+                    fnmatch.raw_escape(arg, raw_chars=raw_chars),
+                    flags=flags
+                )
+            )
+        else:
+            self.assertEqual(fnmatch.escape(arg), expected)
+            self.assertEqual(fnmatch.escape(os.fsencode(arg)), os.fsencode(expected))
+            self.assertTrue(
+                fnmatch.fnmatch(
+                    arg,
+                    fnmatch.escape(arg),
+                    flags=flags
+                )
+            )
+
+    def test_escape(self):
+        """Test path escapes."""
+
+        check = self.check_escape
+        check('abc', 'abc')
+        check('[', r'\[')
+        check('?', r'\?')
+        check('*', r'\*')
+        check('[[_/*?*/_]]', r'\[\[_/\*\?\*/_\]\]')
+        check('/[[_/*?*/_]]/', r'/\[\[_/\*\?\*/_\]\]/')
+
+    def test_raw_escape(self):
+        """Test path escapes."""
+
+        check = self.check_escape
+        check(r'abc', 'abc', raw=True)
+        check(r'[', r'\[', raw=True)
+        check(r'?', r'\?', raw=True)
+        check(r'*', r'\*', raw=True)
+        check(r'[[_/*?*/_]]', r'\[\[_/\*\?\*/_\]\]', raw=True)
+        check(r'/[[_/*?*/_]]/', r'/\[\[_/\*\?\*/_\]\]/', raw=True)
+        check(r'\x3f', r'\?', raw=True)
+        # `fnmatch` doesn't care about drives
+        check(r'\\\\[^what]\\name\\temp', r'\\\\\[^what\]\\name\\temp', raw=True, unix=False)
+        check('//[^what]/name/temp', r'//\[^what\]/name/temp', raw=True, unix=False)
+
+    def test_raw_escape_no_raw_chars(self):
+        """Test path escapes with no raw character translations."""
+
+        check = self.check_escape
+        check(r'abc', 'abc', raw=True, raw_chars=False)
+        check(r'[', r'\[', raw=True, raw_chars=False)
+        check(r'?', r'\?', raw=True, raw_chars=False)
+        check(r'*', r'\*', raw=True, raw_chars=False)
+        check(r'[[_/*?*/_]]', r'\[\[_/\*\?\*/_\]\]', raw=True, raw_chars=False)
+        check(r'/[[_/*?*/_]]/', r'/\[\[_/\*\?\*/_\]\]/', raw=True, raw_chars=False)
+        check(r'\x3f', r'\\x3f', raw=True, raw_chars=False)
+        # `fnmatch` doesn't care about drives
+        check(r'\\\\[^what]\\name\\temp', r'\\\\\[^what\]\\name\\temp', raw=True, raw_chars=False, unix=False)
+        check('//[^what]/name/temp', r'//\[^what\]/name/temp', raw=True, raw_chars=False, unix=False)
+
+    @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test")
+    def test_escape_windows(self):
+        """Test windows escapes."""
+
+        check = self.check_escape
+        # `fnmatch` doesn't care about drives
+        check('a:\\?', r'a:\\\?')
+        check('b:\\*', r'b:\\\*')
+        check('\\\\?\\c:\\?', r'\\\\\?\\c:\\\?')
+        check('\\\\*\\*\\*', r'\\\\\*\\\*\\\*')
+        check('//?/c:/?', r'//\?/c:/\?')
+        check('//*/*/*', r'//\*/\*/\*')
+        check('//[^what]/name/temp', r'//\[^what\]/name/temp')
+
+    def test_escape_forced_windows(self):
+        """Test forced windows escapes."""
+
+        check = self.check_escape
+        # `fnmatch` doesn't care about drives
+        check('a:\\?', r'a:\\\?', unix=False)
+        check('b:\\*', r'b:\\\*', unix=False)
+        check('\\\\?\\c:\\?', r'\\\\\?\\c:\\\?', unix=True)
+        check('\\\\*\\*\\*', r'\\\\\*\\\*\\\*', unix=True)
+        check('//?/c:/?', r'//\?/c:/\?', unix=True)
+        check('//*/*/*', r'//\*/\*/\*', unix=True)
+        check('//[^what]/name/temp', r'//\[^what\]/name/temp', unix=True)
+
+    def test_escape_forced_unix(self):
+        """Test forced windows Unix."""
+
+        check = self.check_escape
+        # `fnmatch` doesn't care about drives
+        check('a:\\?', r'a:\\\?', unix=True)
+        check('b:\\*', r'b:\\\*', unix=True)
+        check('\\\\?\\c:\\?', r'\\\\\?\\c:\\\?', unix=True)
+        check('\\\\*\\*\\*', r'\\\\\*\\\*\\\*', unix=True)
+        check('//?/c:/?', r'//\?/c:/\?', unix=True)
+        check('//*/*/*', r'//\*/\*/\*', unix=True)
+        check('//[^what]/name/temp', r'//\[^what\]/name/temp', unix=True)
+
+
 class TestExpansionLimit(unittest.TestCase):
     """Test expansion limits."""
 
diff --git a/tests/test_glob.py b/tests/test_glob.py
index aca82f7..702794e 100644
--- a/tests/test_glob.py
+++ b/tests/test_glob.py
@@ -1287,7 +1287,7 @@ class TestGlobEscapes(unittest.TestCase):
         check('\\\\*\\*\\*', r'\\\\\*\\\*\\\*', unix=True)
         check('//?/c:/?', r'//\?/c:/\?', unix=True)
         check('//*/*/*', r'//\*/\*/\*', unix=True)
-        check('//[^what]/name/temp', r'//\[\^what\]/name/temp', unix=True)
+        check('//[^what]/name/temp', r'//\[^what\]/name/temp', unix=True)
 
 
 @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test")
diff --git a/tests/test_globmatch.py b/tests/test_globmatch.py
index 965d8cc..2146bec 100644
--- a/tests/test_globmatch.py
+++ b/tests/test_globmatch.py
@@ -1697,6 +1697,171 @@ class TestTilde(unittest.TestCase):
         self.assertNotEqual(len(files), len(gfiles))
 
 
+class TestIsMagic(unittest.TestCase):
+    """Test "is magic" logic."""
+
+    def test_default(self):
+        """Test default magic."""
+
+        self.assertTrue(glob.is_magic("test*"))
+        self.assertTrue(glob.is_magic("test["))
+        self.assertTrue(glob.is_magic("test]"))
+        self.assertTrue(glob.is_magic("test?"))
+        self.assertTrue(glob.is_magic("test\\"))
+
+        self.assertFalse(glob.is_magic("test~!()-/|{}"))
+
+    def test_extmatch(self):
+        """Test extended match magic."""
+
+        self.assertTrue(glob.is_magic("test*", flags=glob.EXTGLOB))
+        self.assertTrue(glob.is_magic("test[", flags=glob.EXTGLOB))
+        self.assertTrue(glob.is_magic("test]", flags=glob.EXTGLOB))
+        self.assertTrue(glob.is_magic("test?", flags=glob.EXTGLOB))
+        self.assertTrue(glob.is_magic("test\\", flags=glob.EXTGLOB))
+        self.assertTrue(glob.is_magic("test(", flags=glob.EXTGLOB))
+        self.assertTrue(glob.is_magic("test)", flags=glob.EXTGLOB))
+
+        self.assertFalse(glob.is_magic("test~!-/|{}", flags=glob.EXTGLOB))
+
+    def test_negate(self):
+        """Test negate magic."""
+
+        self.assertTrue(glob.is_magic("test*", flags=glob.NEGATE))
+        self.assertTrue(glob.is_magic("test[", flags=glob.NEGATE))
+        self.assertTrue(glob.is_magic("test]", flags=glob.NEGATE))
+        self.assertTrue(glob.is_magic("test?", flags=glob.NEGATE))
+        self.assertTrue(glob.is_magic("test\\", flags=glob.NEGATE))
+        self.assertTrue(glob.is_magic("test!", flags=glob.NEGATE))
+
+        self.assertFalse(glob.is_magic("test~()-/|{}", flags=glob.NEGATE))
+
+    def test_minusnegate(self):
+        """Test minus negate magic."""
+
+        self.assertTrue(glob.is_magic("test*", flags=glob.NEGATE | glob.MINUSNEGATE))
+        self.assertTrue(glob.is_magic("test[", flags=glob.NEGATE | glob.MINUSNEGATE))
+        self.assertTrue(glob.is_magic("test]", flags=glob.NEGATE | glob.MINUSNEGATE))
+        self.assertTrue(glob.is_magic("test?", flags=glob.NEGATE | glob.MINUSNEGATE))
+        self.assertTrue(glob.is_magic("test\\", flags=glob.NEGATE | glob.MINUSNEGATE))
+        self.assertTrue(glob.is_magic("test-", flags=glob.NEGATE | glob.MINUSNEGATE))
+
+        self.assertFalse(glob.is_magic("test~()!/|{}", flags=glob.NEGATE | glob.MINUSNEGATE))
+
+    def test_brace(self):
+        """Test brace magic."""
+
+        self.assertTrue(glob.is_magic("test*", flags=glob.BRACE))
+        self.assertTrue(glob.is_magic("test[", flags=glob.BRACE))
+        self.assertTrue(glob.is_magic("test]", flags=glob.BRACE))
+        self.assertTrue(glob.is_magic("test?", flags=glob.BRACE))
+        self.assertTrue(glob.is_magic("test\\", flags=glob.BRACE))
+        self.assertTrue(glob.is_magic("test{", flags=glob.BRACE))
+        self.assertTrue(glob.is_magic("test}", flags=glob.BRACE))
+
+        self.assertFalse(glob.is_magic("test~!-/|", flags=glob.BRACE))
+
+    def test_split(self):
+        """Test split magic."""
+
+        self.assertTrue(glob.is_magic("test*", flags=glob.SPLIT))
+        self.assertTrue(glob.is_magic("test[", flags=glob.SPLIT))
+        self.assertTrue(glob.is_magic("test]", flags=glob.SPLIT))
+        self.assertTrue(glob.is_magic("test?", flags=glob.SPLIT))
+        self.assertTrue(glob.is_magic("test\\", flags=glob.SPLIT))
+        self.assertTrue(glob.is_magic("test|", flags=glob.SPLIT))
+
+        self.assertFalse(glob.is_magic("test~()-!/", flags=glob.SPLIT))
+
+    def test_tilde(self):
+        """Test tilde magic."""
+
+        self.assertTrue(glob.is_magic("test*", flags=glob.GLOBTILDE))
+        self.assertTrue(glob.is_magic("test[", flags=glob.GLOBTILDE))
+        self.assertTrue(glob.is_magic("test]", flags=glob.GLOBTILDE))
+        self.assertTrue(glob.is_magic("test?", flags=glob.GLOBTILDE))
+        self.assertTrue(glob.is_magic("test\\", flags=glob.GLOBTILDE))
+        self.assertTrue(glob.is_magic("test~", flags=glob.GLOBTILDE))
+
+        self.assertFalse(glob.is_magic("test|()-!/", flags=glob.GLOBTILDE))
+
+    def test_all(self):
+        """Test tilde magic."""
+
+        flags = (
+            glob.EXTGLOB |
+            glob.NEGATE |
+            glob.BRACE |
+            glob.SPLIT |
+            glob.GLOBTILDE
+        )
+
+        self.assertTrue(glob.is_magic("test*", flags=flags))
+        self.assertTrue(glob.is_magic("test[", flags=flags))
+        self.assertTrue(glob.is_magic("test]", flags=flags))
+        self.assertTrue(glob.is_magic("test?", flags=flags))
+        self.assertTrue(glob.is_magic(r"te\\st", flags=flags))
+        self.assertTrue(glob.is_magic(r"te\st", flags=flags))
+        self.assertTrue(glob.is_magic("test!", flags=flags))
+        self.assertTrue(glob.is_magic("test|", flags=flags))
+        self.assertTrue(glob.is_magic("test(", flags=flags))
+        self.assertTrue(glob.is_magic("test)", flags=flags))
+        self.assertTrue(glob.is_magic("test{", flags=flags))
+        self.assertTrue(glob.is_magic("test}", flags=flags))
+        self.assertTrue(glob.is_magic("test~", flags=flags))
+        self.assertTrue(glob.is_magic("test-", flags=flags | glob.MINUSNEGATE))
+
+        self.assertFalse(glob.is_magic("test-", flags=flags))
+        self.assertFalse(glob.is_magic("test!", flags=flags | glob.MINUSNEGATE))
+
+    def test_all_bytes(self):
+        """Test tilde magic."""
+
+        flags = (
+            glob.EXTGLOB |
+            glob.NEGATE |
+            glob.BRACE |
+            glob.SPLIT |
+            glob.GLOBTILDE
+        )
+
+        self.assertTrue(glob.is_magic(b"test*", flags=flags))
+        self.assertTrue(glob.is_magic(b"test[", flags=flags))
+        self.assertTrue(glob.is_magic(b"test]", flags=flags))
+        self.assertTrue(glob.is_magic(b"test?", flags=flags))
+        self.assertTrue(glob.is_magic(rb"te\\st", flags=flags))
+        self.assertTrue(glob.is_magic(rb"te\st", flags=flags))
+        self.assertTrue(glob.is_magic(b"test!", flags=flags))
+        self.assertTrue(glob.is_magic(b"test|", flags=flags))
+        self.assertTrue(glob.is_magic(b"test(", flags=flags))
+        self.assertTrue(glob.is_magic(b"test)", flags=flags))
+        self.assertTrue(glob.is_magic(b"test{", flags=flags))
+        self.assertTrue(glob.is_magic(b"test}", flags=flags))
+        self.assertTrue(glob.is_magic(b"test~", flags=flags))
+        self.assertTrue(glob.is_magic(b"test-", flags=flags | glob.MINUSNEGATE))
+
+        self.assertFalse(glob.is_magic(b"test-", flags=flags))
+        self.assertFalse(glob.is_magic(b"test!", flags=flags | glob.MINUSNEGATE))
+
+    def test_win_path(self):
+        """Test windows path."""
+
+        flags = (
+            glob.EXTGLOB |
+            glob.NEGATE |
+            glob.FORCEWIN |
+            glob.GLOBTILDE
+        )
+
+        self.assertFalse(glob.is_magic('//?/UNC/server/*[]!|(){}~-/', flags=flags))
+        self.assertFalse(glob.is_magic('//?/UNC/server/*[]!|()~-/', flags=flags | glob.BRACE))
+        self.assertFalse(glob.is_magic('//?/UNC/server/*[]!(){}~-/', flags=flags | glob.SPLIT))
+
+        self.assertTrue(glob.is_magic('//?/UNC/server/*[]!|(){}|~-/', flags=flags | glob.BRACE))
+        self.assertTrue(glob.is_magic('//?/UNC/server/*[]!(){}|~-/', flags=flags | glob.SPLIT))
+        self.assertTrue(glob.is_magic(r'\\\\server\\mount\\', flags=flags))
+
+
 class TestExpansionLimit(unittest.TestCase):
     """Test expansion limits."""