Alois Alois - 16 days ago 7
Python Question

pathlib.path allowing other types for joining apart from string and Path

How can I change

pathlib.Path._parse_args
, so that I cannot only have other types (like
LocalPath
) as arguments to
Path
but also use other types as parameter for
/
based joining, etc..?

from pathlib import Path
Path(tmplib) / 25 / True


With
tmplib
a
LocalPath
from
py._path.local
, and the others automatically converted to their
str()
representations?

I tried sub-classing and monkey-patching as shown in this (pathlib Path and py.test LocalPath) question but that did not work.

Path
looks very resistant to extending.

Answer

pathlib (and pathlib2 for Python versions < 3.5) primarily consists of four classes directly related to paths Path, PosixPath, WindowsPath and PurePath (BasePath in pathlib2). If you subclass each of these and copy and adapt the code for Path.__new__() and PurePath._parse_args() in the following way:

import os
import sys
if sys.version_info < (3, 5):
    import pathlib2 as pathlib
else:
    import pathlib


class PurePath(pathlib.Path):
    __slots__ = ()
    types_to_stringify = [int]

    @classmethod
    def _parse_args(cls, args):
        # This is useful when you don't want to create an instance, just
        # canonicalize some constructor arguments.
        parts = []
        for a in args:
            if isinstance(a, pathlib.PurePath):
                parts += a._parts
            elif sys.version_info < (3,) and isinstance(a, basestring):
                # Force-cast str subclasses to str (issue #21127)
                parts.append(str(a))
            elif sys.version_info >= (3, 4) and isinstance(a, str):
                # Force-cast str subclasses to str (issue #21127)
                parts.append(str(a))
            elif isinstance(a, tuple(PurePath.types_to_stringify)):
                parts.append(str(a))
            else:
                try:
                    parts.append(a)
                except:
                    raise TypeError(
                        "argument should be a path or str object, not %r"
                        % type(a))
        return cls._flavour.parse_parts(parts)


class WindowsPath(PurePath, pathlib.PureWindowsPath):
    __slots__ = ()


class PosixPath(PurePath, pathlib.PurePosixPath):
    __slots__ = ()


class Path(pathlib.Path):
    __slots__ = ()

    def __new__(cls, *args, **kwargs):
        if cls is Path:
            cls = WindowsPath if os.name == 'nt' else PosixPath
        self = cls._from_parts(args, init=False)
        if not self._flavour.is_supported:
            raise NotImplementedError("cannot instantiate %r on your system"
                                      % (cls.__name__,))
        self._init()
        return self

you will have a Path that already understand int and can be used to do:

from py._path.local import LocalPath

# extend the types to be converted to string on the fly
PurePath.types_to_stringify.extend([LocalPath, bool])

tmpdir = LocalPath('/var/tmp/abc')

p = Path(tmpdir) / 14 / False / 'testfile.yaml'
print(p)

to get:

/var/tmp/abc/14/False/testfile.yaml

(you'll need to install package pathlib2 for versions < 3.5 for this to work on those).

The above Path can be used as open(p) in Python 3.6.

Adapting _parse_args gives you automatic support for / (__truediv__) as well as methods like joinpath(), relative_to() etc.