4

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.

Community
  • 1
  • 1
Alois
  • 197
  • 2
  • 9

2 Answers2

1

pathlib (and pathlib2 for Python versions < 3.4) 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, 4):
    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.4 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.

Anthon
  • 69,918
  • 32
  • 186
  • 246
1

You can do something like this if you want to keep the platform independence magic:

from py._path.local import LocalPath
import os
import pathlib


class Path(pathlib.Path):

    def __new__(cls, *args, **kwargs):
        if cls is Path:
            cls = WindowsPath if os.name == 'nt' else PosixPath
        return cls._from_parts(map(str, args))

    def __truediv__(self, other):
        return super().__truediv__(str(other))

class WindowsPath(Path, pathlib.WindowsPath):
    pass
class PosixPath(Path, pathlib.PosixPath):
    pass

p = Path(LocalPath())
print(p / 25 / True)

Or just this if it's ok to be platform specific:

from py._path.local import LocalPath
import pathlib


class Path(pathlib.PosixPath):

    def __new__(cls, *args, **kwargs):
        return cls._from_parts(map(str, args))

    def __truediv__(self, other):
        return super().__truediv__(str(other))


p = Path(LocalPath())
print(p / 25 / True)
eigil
  • 465
  • 10
  • 17
  • 1
    Although it looks simpler than my solution, this will not complain about **any** type that can be represented as a string. And if you want to use `joinpath()`, `realtive_to()`, etc., you have to bring in each of those definitions as well. – Anthon Nov 24 '16 at 20:32