Search code examples
pythonpython-3.xoopsubclassingpathlib

Subclass of pathlib.Path doesn't support "/" operator


I'm attempting to create a subclass of pathlib.Path that will do some manipulation to the passed string path value before passing it along to the base class.

class MyPath(Path):
    def __init__(self, str_path):
        str_path = str_path.upper() # just representative, not what I'm actually doing

        super().__init__(str_path)

However, when I try to use this:

foo = MyPath("/path/to/my/file.txt")
bar = foo / "bar"

I get the following error: TypeError: unsupported operand type(s) for /: 'MyPath' and 'str'

I am using Python 3.12 which I understand to have better support for subclassing Path


Solution

  • MyPath("foo") / "bar' goes through several steps:

    1. It invokes MyPath("foo").__truediv__("bar"), which
    2. Invokes MyPath("foo").with_segments("bar"), which
    3. Invokes MyPath(MyPath("foo"), "bar"), which
    4. Raises a TypeError because you overrode MyPath.__init__ to only accept a single additional positional argument, which
    5. Causes MyPath.__truediv__ to catch a TypeError and subsequently return NotImplemented

    Your __init__ method must be careful to accept multiple path components, not just a single string, and it must not assume that each argument is an actual string, but rather objects that implement the os.PathLike interface. (In particular, don't assume that a path component has an upper method. Call os.fspath on the component first to retrieve a str representation that does have an upper method.)

    Something like

    import os
    
    class MyPath(Path):
        def __init__(self, *paths):
            super().__init__(*(os.fspath(x).upper() for x in paths))
    

    and thus

    % py312/bin/python -i tmp.py
    >>> bar
    MyPath('/PATH/TO/MY/FILE.TXT/BAR')
    

    Alternately, as others have alluded to, you can override with_segments to, for example, combine the segments yourself and pass the single result to MyPath, but I see no reason to restrict MyPath.__init__'s signature as you currently do.