Search code examples
pythonpython-3.xpathlib

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.


Solution

  • 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.