From 7dcbf4c3693c9afbc2ffcd7b9a9ec76122abfb80 Mon Sep 17 00:00:00 2001 From: Eric Berquist Date: Sat, 8 Jun 2024 14:46:32 -0400 Subject: [PATCH] Update reserved paths based on Python 3.13 Windows changes --- exdir/core/validation.py | 34 +++++++++++++++++++++++++--------- tests/test_help_functions.py | 3 ++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/exdir/core/validation.py b/exdir/core/validation.py index e12aac5..2b4b850 100644 --- a/exdir/core/validation.py +++ b/exdir/core/validation.py @@ -1,14 +1,25 @@ from enum import Enum import os -try: - import pathlib -except ImportError as e: - try: - import pathlib2 as pathlib - except ImportError: - raise e +import sys +from pathlib import Path, WindowsPath +from unicodedata import category from . import constants as exob +# `ntpath.isreserved` forbids ASCII control characters +# (https://github.com/python/cpython/blob/7c016deae62308dd1b4e2767fc6abf04857c7843/Lib/ntpath.py#L325) +# while `pathlib.PureWindowsPath.is_reserved` does not, so it is easiest to +# forbid all control characters. +if sys.version_info.minor < 13: + from pathlib import PureWindowsPath + + def _is_reserved(path): + return PureWindowsPath(path).is_reserved() or _contains_control_character(path) +else: + from ntpath import isreserved + + def _is_reserved(path): + return isreserved(path) or _contains_control_character(path) + VALID_CHARACTERS = ("abcdefghijklmnopqrstuvwxyz1234567890_-.") @@ -18,6 +29,11 @@ class NamingRule(Enum): THOROUGH = 3 NONE = 4 + +def _contains_control_character(s): + return any(ch for ch in s if category(ch)[0] == "C") + + def _assert_unique(parent_path, name): try: name_str = str(name) @@ -58,7 +74,7 @@ def _assert_nonreserved(name): "Name cannot be '{}' because it is a reserved filename in Exdir.".format(name_str) ) - if pathlib.PureWindowsPath(name_str).is_reserved(): + if _is_reserved(name_str): raise NameError( "Name cannot be '{}' because it is a reserved filename in Windows.".format(name_str) ) @@ -102,7 +118,7 @@ def thorough(parent_path, name): name_lower = name_str.lower() _assert_valid_characters(name_lower) - if isinstance(pathlib.Path(parent_path), pathlib.WindowsPath): + if isinstance(Path(parent_path), WindowsPath): # use _assert_unique if we're already on Windows, because it is much faster # than the test below _assert_unique(parent_path, name) diff --git a/tests/test_help_functions.py b/tests/test_help_functions.py index 2a79942..8297a8d 100644 --- a/tests/test_help_functions.py +++ b/tests/test_help_functions.py @@ -33,7 +33,8 @@ def test_assert_valid_name_minimal(setup_teardown_folder): exob._assert_valid_name("A", f) - exob._assert_valid_name("\n", f) + with pytest.raises(NameError): + exob._assert_valid_name("\n", f) exob._assert_valid_name(six.unichr(0x4500), f)