2025, Oct 03 15:20

Как исправить падение WindowsDowndate из‑за pathlib на Python 3.11–3.13

Почему WindowsDowndate падает из‑за pathlib и как это исправить: решение для Python 3.11 без импорта _from_parts и вариант для 3.12/3.13. Подробности в статье.

Когда вы берёте инструмент для исследований в области безопасности и он тут же спотыкается о стандартную библиотеку, причина чаще всего в несовпадении версий или случайном лишнем импорте. Именно это происходит с WindowsDowndate, когда код опирается на внутренности pathlib, которые меняются от релиза к релизу Python.

Что ломается и где

Сбой проявляется во вспомогательном модуле, где создаётся подкласс WindowsPath и из pathlib вытягивается приватный помощник конструктора. В проблемной конфигурации это выглядит так:

import os
from typing import Any, TypeVar, Type, Self
from pathlib import WindowsPath, _from_parts
class PathPlus(WindowsPath):
    """
    WindowsPath extension that expands env vars and exposes NT path.
    """
    TPathPlus = TypeVar("TPathPlus")
    def __new__(cls: Type[TPathPlus], raw_path: str, *extra: Any, **kw: Any) -> TPathPlus:
        expanded = os.path.expandvars(raw_path)
        extra = (expanded,) + extra
        obj = cls._from_parts(extra)
        return obj
    @property
    def nt_path(self: Self) -> str:
        return f"\\??\\{self.full_path}"
    @property
    def full_path(self: Self) -> str:
        return str(self)

В этой версии импортируется _from_parts из pathlib, а в __new__ вызывается cls._from_parts(...). На этом этапе всё и идёт наперекосяк.

Почему это не работает

В актуальном коде репозитория импорт такой: только from pathlib import WindowsPath. Подтягивать _from_parts из pathlib и лишне, и неправильно. В Python 3.11 _from_parts существует как внутренний метод класса PurePath, а не как доступный для импорта символ верхнего уровня. В Python 3.12 и 3.13 эта внутренняя часть изменилась, и метода там уже нет в прежнем виде. Сам проект прямо указывает, что он тестировался на Python 3.11.9, что соответствует реализации, рассчитывающей на наличие внутреннего метода в иерархии классов, а не на его импорт.

Полный traceback подтвердил бы, сталкиваетесь ли вы с ошибкой импорта или с AttributeError, но несоответствие видно уже по коду и примечаниям о версиях.

Два практических решения

Первый путь — привести окружение к ожидаемому проектом. Если вы запускаете Python 3.11, просто уберите лишний импорт и полагайтесь на унаследованный метод. Второй путь — остаться на 3.12/3.13 и вернуть недостающие части внутри своего подкласса, как показано ниже.

Исправление для Python 3.11: уберите лишний импорт

Оставьте класс без изменений и удалите _from_parts из строки импорта. В протестированном окружении внутренних механизмов по наследству достаточно.

import os
from typing import Any, TypeVar, Type, Self
from pathlib import WindowsPath
class PathPlus(WindowsPath):
    TPathPlus = TypeVar("TPathPlus")
    def __new__(cls: Type[TPathPlus], raw_path: str, *extra: Any, **kw: Any) -> TPathPlus:
        expanded = os.path.expandvars(raw_path)
        extra = (expanded,) + extra
        obj = cls._from_parts(extra)
        return obj
    @property
    def nt_path(self: Self) -> str:
        return f"\\??\\{self.full_path}"
    @property
    def full_path(self: Self) -> str:
        return str(self)

Это соответствует текущему импорту в репозитории и согласуется с пометкой «tested with python 3.11.9».

Вариант для Python 3.12/3.13 с прицелом на совместимость вперёд

Если хотите остаться на новой версии Python, добавьте недостающие вспомогательные конструкторы в свой подкласс. Это отражает то, на что опирался Python 3.11 внутри, и включает парсер аргументов, от которого зависит конструктор.

import os
from typing import Any, TypeVar, Type, Self
from pathlib import WindowsPath, PurePath
class PathPlus(WindowsPath):
    TPathPlus = TypeVar("TPathPlus")
    def __new__(cls: Type[TPathPlus], raw_path: str, *extra: Any, **kw: Any) -> TPathPlus:
        expanded = os.path.expandvars(raw_path)
        extra = (expanded,) + extra
        obj = cls._from_parts(extra)
        return obj
    @classmethod
    def _from_parts(cls, items):
        inst = object.__new__(cls)
        drv, root, parts = inst._parse_args(items)
        inst._drv = drv
        inst._root = root
        inst._parts = parts
        return inst
    @classmethod
    def _parse_args(cls, items):
        chunks = []
        for piece in items:
            if isinstance(piece, PurePath):
                chunks += piece._parts
            else:
                fs_val = os.fspath(piece)
                if isinstance(fs_val, str):
                    chunks.append(str(fs_val))
                else:
                    raise TypeError(
                        "argument should be a str object or an os.PathLike "
                        "object returning str, not %r" % type(fs_val)
                    )
        return cls._flavour.parse_parts(chunks)
    @property
    def nt_path(self: Self) -> str:
        return f"\\??\\{self.full_path}"
    @property
    def full_path(self: Self) -> str:
        return str(self)

Так вы инкапсулируете внутренний путь конструирования и не будете зависеть от того, сохранится ли приватная поверхность pathlib неизменной между версиями.

Почему это важно

Проекты, которые зависят от приватных или полуприватных частей стандартной библиотеки, становятся хрупкими при минорных апгрейдах Python. Здесь достаточно небольшого отличия между Python 3.11 и 3.12/3.13 во внутренних механизмах pathlib, чтобы инструмент падал уже на этапе импорта. Подтверждение точной причины через полный traceback и приведение окружения к заявленной базовой версии репозитория поможет избежать лишней отладки. Если нужно оставаться на новых интерпретаторах, изолируйте и «вендорьте» минимум логики, от которой зависите, — это позволит продолжать работу, оставаясь близко к поведению апстрима.

Выводы

Проверьте, что версия репозитория у вас соответствует актуальному коду, особенно импорту. Для быстрого успеха выровняйте Python под указанную проектом протестированную версию — в данном случае 3.11.9. Если понизить версию нельзя, встройте небольшой набор помощников, на который рассчитывает код (см. выше), чтобы восстановить совместимость на 3.12/3.13. И всегда держите под рукой полный traceback: с ним подобные несоответствия становятся очевидными сразу.

Статья основана на вопросе на StackOverflow от oz hagevermeleh и ответе пользователя furas.