diff --git a/flow/record/fieldtypes/__init__.py b/flow/record/fieldtypes/__init__.py index 8f0029c..0cbeb16 100644 --- a/flow/record/fieldtypes/__init__.py +++ b/flow/record/fieldtypes/__init__.py @@ -32,6 +32,7 @@ UTC = timezone.utc PY_311 = sys.version_info >= (3, 11, 0) +PY_312 = sys.version_info >= (3, 12, 0) PATH_POSIX = 0 PATH_WINDOWS = 1 @@ -645,28 +646,31 @@ def __new__(cls, *args): for path_part in args: if isinstance(path_part, pathlib.PureWindowsPath): cls = windows_path - # The (string) representation of a pathlib.PureWindowsPath is not round trip equivalent if a - # path starts with a \ or / followed by a drive letter, e.g.: \C:\... - # Meaning: - # - # str(PureWindowsPath(r"\C:\WINDOWS/Temp")) != - # str(PureWindowsPath(PureWindowsPath(r"\C:\WINDOWS/Temp"))), - # - # repr(PureWindowsPath(r"\C:\WINDOWS/Temp")) != - # repr(PureWindowsPath(PureWindowsPath(r"\C:\WINDOWS/Temp"))), - # - # This would be the case though when using PurePosixPath instead. - # - # This construction works around that by converting all path parts - # to strings first. - args = (str(arg) for arg in args) + if not PY_312: + # For Python < 3.12, the (string) representation of a + # pathlib.PureWindowsPath is not round trip equivalent if a path + # starts with a \ or / followed by a drive letter, e.g.: \C:\... + # Meaning: + # + # str(PureWindowsPath(r"\C:\WINDOWS/Temp")) != + # str(PureWindowsPath(PureWindowsPath(r"\C:\WINDOWS/Temp"))), + # + # repr(PureWindowsPath(r"\C:\WINDOWS/Temp")) != + # repr(PureWindowsPath(PureWindowsPath(r"\C:\WINDOWS/Temp"))), + # + # This would be the case though when using PurePosixPath instead. + # + # This construction works around that by converting all path parts + # to strings first. + args = (str(arg) for arg in args) elif isinstance(path_part, pathlib.PurePosixPath): cls = posix_path elif _is_windowslike_path(path_part): # This handles any custom PurePath based implementations that have a windows # like path separator (\). cls = windows_path - args = (str(arg) for arg in args) + if not PY_312: + args = (str(arg) for arg in args) elif _is_posixlike_path(path_part): # This handles any custom PurePath based implementations that don't have a # windows like path separator (\). @@ -675,7 +679,11 @@ def __new__(cls, *args): continue break - return cls._from_parts(args) + if PY_312: + obj = super().__new__(cls) + else: + obj = cls._from_parts(args) + return obj def __eq__(self, other: Any) -> bool: if isinstance(other, str): diff --git a/tests/test_fieldtypes.py b/tests/test_fieldtypes.py index 19c6cbe..f12f3d8 100644 --- a/tests/test_fieldtypes.py +++ b/tests/test_fieldtypes.py @@ -3,6 +3,8 @@ import hashlib import os import pathlib +import posixpath +import types from datetime import datetime, timedelta, timezone import pytest @@ -12,6 +14,7 @@ from flow.record.fieldtypes import ( PATH_POSIX, PATH_WINDOWS, + PY_312, _is_posixlike_path, _is_windowslike_path, ) @@ -527,12 +530,29 @@ def test_digest(): def custom_pure_path(sep, altsep): - class CustomFlavour(pathlib._PosixFlavour): - def __new__(cls): - instance = pathlib._PosixFlavour.__new__(cls) - instance.sep = sep - instance.altsep = altsep - return instance + # Starting from Python 3.12, pathlib._Flavours are removed as you can + # now properly subclass pathlib.Path + # The flavour property of Path's is replaced by a link to e.g. + # posixpath or ntpath. + # See also: https://github.com/python/cpython/issues/88302 + if PY_312: + + class CustomFlavour: + def __new__(cls, *args, **kwargs): + flavour = types.ModuleType("mockpath") + flavour.__dict__.update(posixpath.__dict__) + flavour.sep = sep + flavour.altsep = altsep + return flavour + + else: + + class CustomFlavour(pathlib._PosixFlavour): + def __new__(cls): + instance = super().__new__(cls) + instance.sep = sep + instance.altsep = altsep + return instance class PureCustomPath(pathlib.PurePath): _flavour = CustomFlavour()