Skip to content

Commit

Permalink
Add support for Py 3.13 (#221)
Browse files Browse the repository at this point in the history
* Add support for Py 3.13

- Support Py 3.13
- Remove deprecated `raw_escape`
- Remove casting which incurs a performance cost for little gain.
  Once that cannot easily be resolved, just ignore the mypy error.
- Update formatted strings to f-string (where possible)

* Remove some dead code
  • Loading branch information
facelessuser authored Aug 3, 2024
1 parent dd0b3c2 commit 2939d2a
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 235 deletions.
5 changes: 5 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 9.0

- **NEW**: Remove deprecated function `glob.raw_escape`.
- **NEW**: Officially support Python 3.13.

## 8.5.2

- **FIX**: Fix `pathlib` issue with inheritance on Python versions greater than 3.12.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/markdown/fnmatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Translate patterns now provide capturing groups for [`EXTMATCH`](#extmatch) grou
def escape(pattern):
```

The `escape` function will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`. and `\` with
The `escape` function will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`, and `\` with
backslashes, regardless of what feature is or is not enabled. It is meant to escape filenames.

```pycon3
Expand Down
86 changes: 1 addition & 85 deletions docs/src/markdown/glob.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ Translate patterns now provide capturing groups for [`EXTGLOB`](#extglob) groups
def escape(pattern, unix=None):
```

The `escape` function will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`. and `\` with
The `escape` function will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`, and `\` with
backslashes, regardless of what feature is or is not enabled. It is meant to escape path parts (filenames, Windows
drives, UNC sharepoints) or full paths.

Expand Down Expand Up @@ -660,90 +660,6 @@ force Windows style escaping.
drives manually in their match patterns as well.
///

#### `glob.raw_escape` {: #raw_escape}

/// warning | Deprecated 8.1
In 8.1, `raw_escape` has been deprecated. The same can be accomplished simply by using `codecs` and then using the
normal [`escape`](#escape):

```pycon3
>>> string = r"translate\\raw strings\\\u00c3\xc3\303\N{LATIN CAPITAL LETTER A WITH TILDE}"
>>> translated = codecs.decode(string, 'unicode_escape')
>>> glob.escape(translated)
'translate\\\\raw strings\\\\ÃÃÃÃ'
>>> glob.raw_escape(string)
'translate\\\\raw strings\\\\ÃÃÃÃ'
```
///

```py3
def raw_escape(pattern, unix=None, 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 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
>>> 'folder\\El Ni\u00f1o'
'folder\\El Niño'
```

But when in a GUI interface, if a user inputs the same, it's like getting a raw string.

```pycon3
>>> r'folder\\El Ni\u00f1o'
'folder\\\\El Ni\\u00f1o'
```

`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)
'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):

`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
Unix style escaping or Windows style escaping via the `unix` parameter. When `unix` is `None`, the escape style will be
detected, when `unix` is `True` Linux/Unix style escaping will be used, and when `unix` is `False` Windows style
escaping will be used.

```pycon3
>>> glob.raw_escape(r'some/path?/\x2a\x2afile\x2a\x2a{}.txt', unix=True)
```

/// new | New 5.0
The `unix` parameter is now `None` by default. Set to `True` to force Linux/Unix style escaping or set to `False` to
force Windows style escaping.
///

/// new | New 7.0
`{`, `}`, and `|` will be escaped in Windows drives. Additionally, users can escape these characters in Windows
drives manually in their match patterns as well.

`raw_chars` option was added.
///

### `glob.is_magic` {: #is_magic}

```py3
Expand Down
1 change: 1 addition & 0 deletions hatch_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def update(self, metadata):
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Topic :: Software Development :: Libraries :: Python Modules',
'Typing :: Typed'
]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ legacy_tox_ini = """
isolated_build = true
skipsdist=true
envlist=
py38,py39,py310,py311,py312,
py38,py39,py310,py311,py312,py313,
lint
[testenv]
Expand Down
69 changes: 9 additions & 60 deletions tests/test_glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,7 @@ class TestGlobCornerCaseMarked(Testglob):
class TestGlobEscapes(unittest.TestCase):
"""Test escaping."""

def check_escape(self, arg, expected, raw=False, unix=None, raw_chars=True):
def check_escape(self, arg, expected, unix=None):
"""Verify escapes."""

flags = 0
Expand All @@ -1382,38 +1382,15 @@ def check_escape(self, arg, expected, raw=False, unix=None, raw_chars=True):
elif unix is True:
flags = glob.FORCEUNIX

if raw:
self.assertEqual(glob.raw_escape(arg, unix=unix, raw_chars=raw_chars), expected)
self.assertEqual(glob.raw_escape(os.fsencode(arg), unix=unix, raw_chars=raw_chars), os.fsencode(expected))
file = (util.norm_pattern(arg, False, True) if raw_chars else arg).replace('\\\\', '\\')
self.assertTrue(
glob.globmatch(
file,
glob.raw_escape(arg, unix=unix, raw_chars=raw_chars),
flags=flags
)
)
else:
self.assertEqual(glob.escape(arg, unix=unix), expected)
self.assertEqual(glob.escape(os.fsencode(arg), unix=unix), os.fsencode(expected))
self.assertTrue(
glob.globmatch(
arg,
glob.escape(arg, unix=unix),
flags=flags
)
self.assertEqual(glob.escape(arg, unix=unix), expected)
self.assertEqual(glob.escape(os.fsencode(arg), unix=unix), os.fsencode(expected))
self.assertTrue(
glob.globmatch(
arg,
glob.escape(arg, unix=unix),
flags=flags
)

def test_raw_escape_deprecation(self):
"""Test raw escape deprecation."""

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")

glob.raw_escape(r'test\\test')

self.assertTrue(len(w) == 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
)

def test_escape(self):
"""Test path escapes."""
Expand All @@ -1426,34 +1403,6 @@ def test_escape(self):
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)
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)
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."""
Expand Down
19 changes: 9 additions & 10 deletions wcmatch/__meta__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Meta related things."""
from __future__ import annotations
from __future__ import unicode_literals
from collections import namedtuple
import re

Expand Down Expand Up @@ -94,7 +93,7 @@ def __new__(
raise ValueError("All version parts except 'release' should be integers.")

if release not in REL_MAP:
raise ValueError("'{}' is not a valid release type.".format(release))
raise ValueError(f"'{release}' is not a valid release type.")

# Ensure valid pre-release (we do not allow implicit pre-releases).
if ".dev-candidate" < release < "final":
Expand All @@ -119,7 +118,7 @@ def __new__(
elif dev:
raise ValueError("Version is not a development release.")

return super(Version, cls).__new__(cls, major, minor, micro, release, pre, post, dev)
return super().__new__(cls, major, minor, micro, release, pre, post, dev)

def _is_pre(self) -> bool:
"""Is prerelease."""
Expand All @@ -146,15 +145,15 @@ def _get_canonical(self) -> str:

# Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
if self.micro == 0:
ver = "{}.{}".format(self.major, self.minor)
ver = f"{self.major}.{self.minor}"
else:
ver = "{}.{}.{}".format(self.major, self.minor, self.micro)
ver = f"{self.major}.{self.minor}.{self.micro}"
if self._is_pre():
ver += '{}{}'.format(REL_MAP[self.release], self.pre)
ver += f'{REL_MAP[self.release]}{self.pre}'
if self._is_post():
ver += ".post{}".format(self.post)
ver += f".post{self.post}"
if self._is_dev():
ver += ".dev{}".format(self.dev)
ver += f".dev{self.dev}"

return ver

Expand All @@ -165,7 +164,7 @@ def parse_version(ver: str) -> Version:
m = RE_VER.match(ver)

if m is None:
raise ValueError("'{}' is not a valid version".format(ver))
raise ValueError(f"'{ver}' is not a valid version")

# Handle major, minor, micro
major = int(m.group('major'))
Expand Down Expand Up @@ -194,5 +193,5 @@ def parse_version(ver: str) -> Version:
return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(8, 5, 2, "final")
__version_info__ = Version(9, 0, 0, "final")
__version__ = __version_info__._get_canonical()
4 changes: 2 additions & 2 deletions wcmatch/_wcmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import stat
import copyreg
from . import util
from typing import Pattern, AnyStr, Generic, Any, cast
from typing import Pattern, AnyStr, Generic, Any

# `O_DIRECTORY` may not always be defined
DIR_FLAGS = os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0)
Expand Down Expand Up @@ -192,7 +192,7 @@ def match(self, root_dir: AnyStr | None = None, dir_fd: int | None = None) -> bo
)
)

re_mount = cast(Pattern[AnyStr], (RE_WIN_MOUNT if util.platform() == "windows" else RE_MOUNT)[self.ptype])
re_mount = (RE_WIN_MOUNT if util.platform() == "windows" else RE_MOUNT)[self.ptype] # type: Pattern[AnyStr] # type: ignore[assignment]
is_abs = re_mount.match(self.filename) is not None

if is_abs:
Expand Down
Loading

0 comments on commit 2939d2a

Please sign in to comment.