Repository: facelessuser/wcmatch

Issue:
Exception with EXTMATCH on pattern like '!(not)_@(this|that)'
* **OS:** Arch Linux
* **python version:** 3.12.3
* **wcmatch version:** 8.5.1

When using a pattern like `!(not)_@(this|that)` and EXTMATCH an exception `missing ), unterminated subpattern at position 5` is raised. The issue happens when a `!(pattern-list)` is followed by a `@(pattern-list)`.

As a trivialized example say I have the files `1_foo`, `2_foo` and `3_bar` and I want to match all files except the 2nd, I could use a pattern `!(2)_@(foo|bar)`. Matching the 1st file like this:
```python
from wcmatch import fnmatch
fnmatch.fnmatch('1_foo', '!(2)_@(foo|bar)', flags = fnmatch.EXTMATCH)
```
Should result `True` but instead the fnmatch call raises this exception:
```python
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.12/site-packages/wcmatch/fnmatch.py", line 86, in fnmatch
    return bool(_wcparse.compile(patterns, flags, limit, exclude=exclude).match(filename))
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/wcmatch/_wcparse.py", line 787, in compile
    positive, negative = compile_pattern(patterns, flags, limit, exclude)
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/wcmatch/_wcparse.py", line 739, in compile_pattern
    positive.append(_compile(expanded, flags))
                    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/wcmatch/_wcparse.py", line 798, in _compile
    return re.compile(WcParse(pattern, flags & FLAG_MASK).parse())
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/__init__.py", line 228, in compile
    return _compile(pattern, flags)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/__init__.py", line 307, in _compile
    p = _compiler.compile(pattern, flags)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/_compiler.py", line 745, in compile
    p = _parser.parse(p, flags)
        ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/_parser.py", line 979, in parse
    p = _parse_sub(source, state, flags & SRE_FLAG_VERBOSE, 0)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/_parser.py", line 460, in _parse_sub
    itemsappend(_parse(source, state, verbose, nested + 1,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/_parser.py", line 862, in _parse
    p = _parse_sub(source, state, sub_verbose, nested + 1)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/_parser.py", line 460, in _parse_sub
    itemsappend(_parse(source, state, verbose, nested + 1,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/re/_parser.py", line 864, in _parse
    raise source.error("missing ), unterminated subpattern",
re.error: missing ), unterminated subpattern at position 5
```
In bash it works:
```bash
$ touch 1_foo 2_foo 3_bar
$ ls !(2)_@(foo|bar)
1_foo  3_bar
```

Discussion:
@facelessuser: Yeah, the negate case in `EXTMATCH` was always a little tricky to get right. I have a fix for this though in PR #219.

```py
>>> fnmatch.fnmatch('1_foo', '!(2)_@(foo|bar)', flags = fnmatch.EXTMATCH)
True
```
@facelessuser: I've tagged a new release. It should be available shortly on PyPI.@jakedane: Nice, thank you!

Solution patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 5a71fef..d92d41e 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 8.5.2
+
+-   **FIX**: Fix `pathlib` issue with inheritance on Python versions greater than 3.12.
+-   **FIX**: Fix `EXTMATCH` case with `!(...)` patterns.
+
 ## 8.5.1
 
 -   **FIX**: Fix issue with type check failure in `wcmatch.glob`.
diff --git a/wcmatch/__meta__.py b/wcmatch/__meta__.py
index b53d065..7d6b0c0 100644
--- a/wcmatch/__meta__.py
+++ b/wcmatch/__meta__.py
@@ -194,5 +194,5 @@ def parse_version(ver: str) -> Version:
     return Version(major, minor, micro, release, pre, post, dev)
 
 
-__version_info__ = Version(8, 5, 1, "final")
+__version_info__ = Version(8, 5, 2, "final")
 __version__ = __version_info__._get_canonical()
diff --git a/wcmatch/_wcparse.py b/wcmatch/_wcparse.py
index 8a45da3..267a13d 100644
--- a/wcmatch/_wcparse.py
+++ b/wcmatch/_wcparse.py
@@ -1435,7 +1435,8 @@ class WcParse(Generic[AnyStr]):
                         extended.append(self._restrict_extended_slash())
                     extended.append(self.sep)
                 elif c == "|":
-                    self.clean_up_inverse(extended, temp_inv_nest and self.inv_nest)
+                    if self.inv_nest:
+                        self.clean_up_inverse(extended, temp_inv_nest)
                     extended.append(c)
                     if temp_after_start:
                         self.set_start_dir()


Test patch:
diff --git a/tests/test_fnmatch.py b/tests/test_fnmatch.py
index 87f33a6..96800fd 100644
--- a/tests/test_fnmatch.py
+++ b/tests/test_fnmatch.py
@@ -148,6 +148,10 @@ class TestFnMatch:
         ['!(test)', 'abc', True, fnmatch.D | fnmatch.E],
         ['!(test)', '..', False, fnmatch.D | fnmatch.E],
 
+        # Negation list followed by extended list
+        ['!(2)_@(foo|bar)', '1_foo', True, fnmatch.E],
+        ['!(!(2|3))_@(foo|bar)', '2_foo', True, fnmatch.E],
+
         # POSIX style character classes
         ['[[:alnum:]]bc', 'zbc', True, 0],
         ['[[:alnum:]]bc', '1bc', True, 0],