47

I would like to enhance the class pathlib.Path but the simple example above dose not work.

from pathlib import Path

class PPath(Path):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

test = PPath("dir", "test.txt")

Here is the error message I have.

Traceback (most recent call last):
  File "/Users/projetmbc/test.py", line 14, in <module>
    test = PPath("dir", "test.txt")
  File "/anaconda/lib/python3.4/pathlib.py", line 907, in __new__
    self = cls._from_parts(args, init=False)
  File "/anaconda/lib/python3.4/pathlib.py", line 589, in _from_parts
    drv, root, parts = self._parse_args(args)
  File "/anaconda/lib/python3.4/pathlib.py", line 582, in _parse_args
    return cls._flavour.parse_parts(parts)
AttributeError: type object 'PPath' has no attribute '_flavour'

What I am doing wrong ?

2
  • Possible solutions to the problem have been outlined on CodeReview. You can follow the discussion of the issue. Maybe one day they will post a solution we haven't encountered yet. Commented Oct 23, 2017 at 10:52
  • Thanks a lot for this ! Commented Oct 23, 2017 at 16:44

10 Answers 10

45

You can subclass the concrete implementation, so this works:

class Path(type(pathlib.Path())):

Here's what I did with this:

import pathlib

class Path(type(pathlib.Path())):
    def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None):
        if encoding is None and 'b' not in mode:
            encoding = 'utf-8'
        return super().open(mode, buffering, encoding, errors, newline)

Path('/tmp/a.txt').write_text("я")
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks. It is ugly but far less than my previous method.
Unfortunately mypy does not allow dynamic base classes, so you can't type check this
Calling Path() creates either a WindowsPath or a PosixPath object, so the only "magic" that this is doing is determining which flavor to use, then getting that type for the inheritance. See the docs.
This just works, but adding an __init__ (and calling super().__init__()) is not obvious. It's probably not required, though.
14

Combining some of the previous answers you could also just write:

class MyPath(pathlib.Path):
    _flavour = type(pathlib.Path())._flavour

UPDATE for Python 3.12

In Python 3.12 the pathlib.Path class now supports subclassing (see What’s New In Python 3.12). Therefore, you can simply access the _flavour attribute from your subclass without any workaround.

3 Comments

It seems the most elegant solution to me
This doesn't work with 3.12.3.
It does with Python 3.12.5. Can you please provide more information?
12

Here is the definition of the Path class. It does something rather clever. Rather than directly returning an instance of Path from its __new__(), it returns an instance of a subclass, but only if it's been invoked directly as Path() (and not as a subclass).

Otherwise, it expects to have been invoked via either WindowsPath() or PosixPath(), which both provide a _flavour class attribute via multiple inheritance. You must also provide this attribute when subclassing. You'll probably need to instantiate and/or subclass the _Flavour class to do this. This is not a supported part of the API, so your code might break in a future version of Python.

TL;DR: This idea is fraught with peril, and I fear that my answers to your questions will be interpreted as approval rather than reluctant assistance.

4 Comments

What a weird choice regarding to the traditional Python API. I think that I have to do what you suggest. This needs to read the source. Not a problem but just a little boring thing.
@projetmbc: IMHO this is poor design; a superclass should not display knowledge of its subclasses, much less depend on them in this fashion. Path() ought to be a factory function instead. It might be worth a bug report, but since it's a matter of subjective opinion, I'm not sure the bug tracker is the best place to start. You might have a more productive conversation on Python-Dev or another mailing list.
I think like you and I have posted a message on the Python-dev mailing list. On the other the challenge in my answer is a funny thing to do.
I wouldn't call this clever
8

You may be able to simplify your life depending on why you want to extend Path (or PosixPath, or WindowsPath). In my case, I wanted to implement a File class that had all the methods of Path, and a few others. However, I didn't actually care if isinstance(File(), Path).

Delegation works beautifully:

class File:

    def __init__(self, path):
        self.path = pathlib.Path(path)
        ...

    def __getattr__(self, attr):
        return getattr(self.path, attr)

    def foobar(self):
        ...

Now, if file = File('/a/b/c'), I can use the entire Path interface on file, and also do file.foobar().

4 Comments

Don't you need a self.path = pathlib.Path(path) in __init__ if you're going to instantiate with file = File("a/b/c"), i.e. a str argument to __init__'s path argument?
Sure, self.path needs to be a pathlib.Path.
Okay, you should adjust that in your answer (doesn't work as it stands right now).
Okay, I fixed init.
6

I have been struggling with this too.

Here is what i did, studying from the pathlib module. Seems to me that is the cleaner way to do it, but if the pathlib module changes its implementation, it probably won't hold.

from pathlib import Path
import os
import pathlib

class PPath(Path):

    _flavour = pathlib._windows_flavour if os.name == 'nt' else pathlib._posix_flavour

    def __new__(cls, *args):
        return super(PPath, cls).__new__(cls, *args)

    def __init__(self, *args):
        super().__init__() #Path.__init__ does not take any arg (all is done in new)
        self._some_instance_ppath_value = self.exists() #Path method

    def some_ppath_method(self, *args):
        pass

test = PPath("dir", "test.txt")

Comments

3

Note

I have opened a bug track here after a little discussion on the Python dev. list.

A temporary solution

Sorry for this double answer but here is a way to achieve what I want. Thanks to Kevin that points me to the source of pathlib and the fact we have here constructors.

import pathlib
import os

def _extramethod(cls, n):
    print("=== "*n)

class PathPlus(pathlib.Path):
    def __new__(cls, *args):
        if cls is PathPlus:
            cls = pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath

        setattr(cls, "extramethod", _extramethod)

        return cls._from_parts(args)

test = PathPlus("C:", "Users", "projetmbc", "onefile.ext")

print("File ?", test.is_file())
print("Dir  ?", test.is_dir())
print("New name:", test.with_name("new.name"))
print("Drive ?", test.drive)

test.extramethod(4)

This prints the following lines.

File ? False
Dir  ? False
New name: C:/Users/projetmbc/new.name
Drive ? 
=== === === === 

2 Comments

I'm not sure this is a great idea. You're basically attaching the extra method to the WindowsPath or PosixPath classes directly, meaning it'll be available to all instances of those classes, not just the instances created via PathPlus.
I think like you but for the moment this not so perfect solution is the one I use. Why ? If I use my enhanced version of the class Path, I do not care of the Windowsäth or PosixPath. Indeed, on the Python-dev mailing list, Guido himself saids that the subclassing of the class Path must be easier to do that it is now. As a conclusion, my solution is very temporary patch before Path become more pythonic.
1

With the latest Python version, _flavour, _posix_flavour are deprecated and we can pass flavour keyword in our class.

import os
from pathlib import Path


class PPath(Path):
    def __init__(self, *args) -> None:
        # Determine the flavor based on the operating system
        flavour = 'posix' if os.name == 'posix' else 'windows'

        # Pass the flavor to the superclass constructor
        super().__init__(*args, flavour=flavour)

2 Comments

I searched in the changelog but could not find anything on _flavour being deprecated. Here it's only noted that some performance issues have been dealt with: docs.python.org/release/3.12.0/whatsnew/changelog.html Here you can even see _flavour is still available in Python 3.12: github.com/python/cpython/blob/…
Also from Python 3.12 the pathlib.Path class now supports subclassing (docs.python.org/3.12/whatsnew/3.12.html). Therefore, this workaround is obsolete for Python 3.12 onwards.
1

It's work too.

from pathlib import Path

class SystemConfigPath(type(Path())):
    def __new__(cls, **kwargs):
        path = cls._std_etc()
        return super().__new__(cls, path, **kwargs)

    @staticmethod
    def _std_etc():
        return '/etc'

name = SystemConfigPath()
name = name / 'apt'
print(name)

Printed:

/etc/apt

@staticmethod can be replaced by @classmethod

Comments

1

In order to inherit from pathlib.Path, you need to specify which OS, or "flavour" you're representing. All you need to do is specify that you are using either Windows or Unix (seems to be Unix based on your traceback) by inheriting from pathlib.PosixPath or pathlib.WindowsPath.

import pathlib

class PPath(pathlib.PosixPath):
    pass

test = PPath("dir", "test.txt")
print(test)

Which outputs:

dir\test.txt

Using type(pathlib.Path()) as proposed in this answer does the exact same thing as directly inheriting from pathlib.PosixPath or pathlib.WindowsPath since instantiating pathlib.Path "creates either a PosixPath or a WindowsPath" (pathlib documentation).

If you know your application will not be cross-platform, it is simpler to directly inherit from the flavor Path that represents your OS.

Comments

0

Here is a simple way to do things regarding to the observation made by Kevin.

class PPath():
    def __init__(self, *args, **kwargs):
        self.path = Path(*args, **kwargs)

Then I will need to use a trick so as to automatically bind all the Path's methods to my PPpath class. I think that will be funny to do.

5 Comments

Re automatic binding: Try overriding __getattr__. This won't catch magic methods, so you'll also need to override __div__ and (possibly) __rdiv__ to get the foo / bar syntax.
__div__ isn't a thing in 3.x. I meant to say __truediv__ and maybe __rtruediv__.
I have updated my code so as to automatically have de div syntax. My main problem becomes now to find an automatic way to bind all the public method of Path like is_file to my class.
Again, use __getattr__ to redirect attribute access to the Path object. This will affect normal methods as well.
I will try this during the coming week. If I find something useful, I'll put it here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.