pathlib 路径和 py.test LocalPath

pathlib Path and py.test LocalPath

我已经开始使用 pathlib.Path 一段时间了,我喜欢使用它。现在习惯了,马虎了,忘记给str.

投参了

这经常发生在使用 tox + py.test 和基于 tmpdir 的临时目录(这是一个 py._path.local.LocalPath)时:

from pathlib import Path
import pytest

def test_tmpdir(tmpdir):
    p = Path(tmpdir) / 'testfile.csv'

我不是每次都插入 str(),而是考虑更一般地解决这个问题,但没能成功。

首先,我尝试创建自己的路径 class,该路径具有经过改编的 _parse_args:

import pytest
from py._path.local import LocalPath
from pathlib import Path, PurePath

def Path(Path):
    @classmethod
    def _parse_args(cls, args):
        parts = []
        for a in args:
            if isinstance(a, PurePath):
                parts += a._parts
            elif isinstance(a, str):
                # Force-cast str subclasses to str (issue #21127)
                parts.append(str(a))
            elif isinstance(a, LocalPath):
                parts.append(str(a))
            else:
                raise TypeError(
                    "argument should be a path or str object, not %r"
                    % type(a))
        return cls._flavour.parse_parts(parts)

def test_subclass(tmpdir):
    p = Path(tmpdir) / 'testfile.csv'

这会抛出一个 TypeError: unsupported operand type(s) for /: 'NoneType' and 'str'(也尝试使用 PosixPath,结果相同,但不希望是 Linux 特定的)。

我试过猴子补丁 Path:

import pytest
from pathlib import Path

def add_tmpdir():
    from py._path.local import LocalPath

    org_attr = '_parse_args'
    stow_attr = '_org_parse_args'

    def parse_args_localpath(cls, args):
        args = list(args)
        for idx, a in enumerate(args):
            if isinstance(a, LocalPath):
                args[idx] = str(a)
        return getattr(cls, stow_attr)(args)

    if hasattr(Path, stow_attr):
        return  # already done
    setattr(Path, stow_attr, getattr(Path, org_attr))
    setattr(Path, org_attr, parse_args_localpath)

add_tmpdir()

def test_monkeypatch_path(tmpdir):
    p = Path(tmpdir) / 'testfile.csv'

这会抛出 AttributeError: type object 'Path' has no attribute '_flavour'(在猴子修补 PurePath 时)。

最后我尝试只包装 Path:

import pytest
import pathlib

def Path(*args):
    from py._path.local import LocalPath
    args = list(args)
    for idx, a in enumerate(args):
        if isinstance(a, LocalPath):
            args[idx] = str(a)
    return pathlib.Path(*args)

def test_tmpdir_path(tmpdir):
    p = Path(tmpdir) / 'testfile.csv'

这也给出了 AttributeError: type object 'Path' has no attribute '_flavour'

我认为最后一个在某些时候有效,但我无法重现。
我做错了什么吗?为什么这么难?

最后一个(包装)应该有效,我怀疑你实际上在一个 py.test/tox 运行 中测试了所有这些并且那个猴子补丁仍然有效(这可能解释了为什么它在某些时候起作用,如果你开始改变全局 类).

上的东西,测试文件的顺序等很重要

这很难,因为 Path 本质上是一个生成器,它会即时决定你是在 Windows 还是 Linux,并创建一个 WindowsPath 分别。 PosixPath 相应地。

BDFL Guido van Rossum 在 2015 年 5 月已经 indicated

It does sound like subclassing Path should be made easier.

但什么也没发生。其他标准库中 3.6 中对 pathlib 的支持有所增加,但 pathlib 本身仍然存在同样的问题。

以防其他人正在研究 pytest 的 tmpdir 路径是否与 pathlib.Path 兼容:

使用 python 3.6.5pytest 3.2.1,问题中发布的代码在没有显式转换为 str:

的情况下工作得很好
from pathlib import Path

def test_tmpdir(tmpdir):
    p = Path(tmpdir) / 'testfile.csv'

为方便起见,您可以创建一个夹具来自动进行包装:

@pytest.fixture
def tmppath(tmpdir):
    return Path(tmpdir)

那么您可以在测试中使用 tmppath 而不是 tmpdir