From OpenLP
Jump to: navigation, search
Please note: This is a work in progess

OpenLP currently uses string objects to represent file and directory paths. From Python 3.4 pathlib, a new module introducing a Path object, was included in the standard library.

Switching to this Path object will allow us to deal with file paths on different platforms easier. In some cases it also reduces LOC and in my opinion makes the code cleaner and easier to read.

Naming Convention

At this point I would like to propose a naming convention.

  • All variables that reference a Path object end with '_path' i.e (save_path, media_path)
  • Variables that reference a string representation of a part of a path end with '_name' i.e (file_name, directory_name)

Lists of the afore mentioned type shall be plurals, i.e

  • save_paths, media_paths
  • file_names, directory_names

The Path object

Here are some examples to help get started using pathlib.

All of these code samples are from work I've done on refactoring OpenLP to use Path objects. (Some have been simplified to provide a concise example)

Creating paths

The existing way using strings:

path = os.path.join(AppLocation.get_section_data_path('themes'), 'theme_name')

Using a Path object. (Note once OpenLP has been converted to using Path objects AppLocation.get_section_data_path will return a Path object)

# Using the Path constructor (If you're creating a Path object from scratch)
path = Path(AppLocation.get_section_data_path('themes'), 'theme_name')

# Creating a new Path object from an existing Path object 
path = AppLocation.get_section_data_path('themes') / 'theme_name'

# -- or --
path = AppLocation.get_section_data_path('themes').joinpath('theme_name')

The '/' is used to join Paths, or a Path and a string object regardless if the operating system uses forward or backward slashes.

Formatting strings

Nothing special needs to be done when using a Path object as an argument to the format method of a string.

from pathlib import PurePosixPath, PureWindowsPath
'Directory: {path}'.format(path=PurePosixPath('test', 'path')) == 'Directory: test/path'
'Directory: {path}'.format(path=PureWindowsPath('test', 'path')) == 'Directory: test\\path'

Using Paths

The Path object is divided in to ConcretePath objects (ones who's methods access the file system) and PurePath objects (ones who's methods provide their functionally with out accessing the file system). These are objects are sub classed to provide the Path object. See the pathlib documentation for more details.

PurePath Methods

These are methods that do not access the file system, consequently, PurePosixPath can be imported in Windows and PureWindowsPath can be imported on Posix systems. The same cannot be said for the ConcretePath objects

name (File / Directory Names)

Used to access the name of the last part of the path (anything after the last slash)

With os.path:

filename = os.path.split(self.theme.background_filename)[1]

# -- or --
filename = os.path.basename(self.theme.background_filename)

With pathlib:

file_name =

Note: See Path object removes the trailing / for a difference between how os.path and pathlib.Path handle trailing slashes

with_name (File / Directory Names)

Same as above, but allows the file / directory name to be easily changed when using a Path object

With os.path:

data_folder_backup_path = data_folder_path + '-' + timestamp

With pathlib:

data_folder_backup_path = data_folder_path.with_name( + '-' + timestamp)
suffix (File extensions)

In pathlib the extension is known (correctly) as the suffix.

With os.path:

extension = os.path.splitext(file_name)[1].lower()

With pathlib:

extension = file_path.suffix.lower()
with_suffix (File extensions)

As with file names, pathlib makes replacing the extension/suffix a breeze.

With os.path:

if os.path.splitext(file_name)[1] == '':
    file_name += '.osz'
    ext = os.path.splitext(file_name)[1]
    file_name.replace(ext, '.osz')

With pathlib:

stem (File name with out extension)

pathlib.stem gets the file name with out extensions.

This involved a two step process with os.path of 'splitting' the name and then 'splitting' the extension.

With os.path:

path_file_name = self.file_name()
path, file_name = os.path.split(path_file_name)
base_name = os.path.splitext(file_name)[0]

With pathlib:

base_name = self.file_name().stem

Get the parent directory name.

With os.path:

last_dir = os.path.split(file)[0]

# -- or --
last_dir = os.path.dirname(file)

With pathlib:

last_dir_path = file_path.parent

ConcretePath Methods

Concrete Path methods preform reads or writes to the file system. Because of this the ConcretePath implementations can only be used on the system for which they were written for.


With os.path:

os.path.getsize(file_name) == 0

With pathlib:

file_path.stat().st_size == 0

With os.path:

image_date = os.stat(file_path).st_mtime

With pathlib:

image_date = file_path.stat().st_mtime

Does the path exist regardless if it is a file or directory.

With os.path:

if os.path.exists(thumb_path):

With pathlib:

if thumb_path.exists():

Is the path a directory?

With os.path:

if os.path.isdir(local_file):

With pathlib:

if local_path.is_dir():

Is the path a file?

With os.path:

if not os.path.isfile(text_file):

With pathlib:

if not text_file_path.is_file():

Returns a list of absolute paths, so iterating through results and joins are not required.

With os.path:

listing = os.listdir(local_file)
    for file_name in listing:
        files.append(os.path.join(local_file, file_name))

With pathlib:

file_paths = local_path.iterdir()

When using os.walk, and only expecting results from the source directory (i.e. no sub directories).

With os.path:

for files in os.walk(source):
    for name in files[2]:

With pathlib:

for file_path in source_path.iterdir():

With os.path:

with open(filename, 'rb') as detect_file:

With pathlib:

with'rb') as detect_file:

Open the file and read out the text.

With os.path:

song_file = open(self.import_source, 'rt', encoding='utf-8-sig')
file_content =

# -- or --
with open(self.import_source, 'rt', encoding='utf-8-sig') as song_file:
    file_content =

With pathlib:

file_content = self.import_source.read_text(encoding='utf-8-sig')
fn = open(notes_file, mode='wt', encoding='utf-8')

# -- or --
with open(notes_file, mode='wt', encoding='utf-8') as fn:

Wrappers and Utility functions


No such thing as a Falsey path

Perhaps the biggest annoyance of the Path object is that the Path object is assumed to be relative to the current working directory. If its instantiated with out any arguments, or an empty string, its still a object with a path relative to the current working directory.

Path() == Path('') == Path('.')

Previously in OpenLP there would be cases where we did things like:

file_name = ''
# some code ...
if file_name:

We could do this because an empty string is a Falsey value. However all Path objects are Truthy

To work round this empty path variables should be defined as None. This leads to extra effort when handling things like QFileDialogs, as they return an empty string if the user cancels the dialog box. Meaning we can't just wrap the return value with a Path object. Instead the return needs evaluating and if equal to a Falsey value we need to return None.

file_name = ''
# some code ..
if file_name == '':
    file_path = None
    file_path = Path(file_name)

Of course it goes the other way too. We cannot just call str() on a variable which stores a Path object, as it could be None, and str(None) == 'None'. So something like the following is needed.

file_path = None
# some code ..
if file_path is None:
    file_name = ''
    file_name = str(file_path)

To simplify this I have implemented a version of both the above code samples as utilities path_to_str and str_to_path.

Path object removes the trailing \

Another feature to look out for is that the Path object removes the trailing slash. For example:

str(Path('a/')) == 'a'

Path('a/') == Path('a')

This kind of makes sense. Drop in to a terminal and try the following (should work on Windows too)

:~$ cd Documents/
:~/Documents$ cd ..

:~$ cd Documents

However this leads to some inconsistencies between the os.path module and the pathlib module. Here are some (but not exhaustive examples):

a_name = 'user/desktop'
b_name = 'user/desktop/'

a_path = Path(a_name)
b_path = Path(b_name)

(a_path == b_path) == True

# Get the file / directory name
os.path.basename(a_name) ==  'desktop'
os.path.basename(b_name) == '' == 'desktop'

# Get the parent directory
os.path.dirname(a_name) == 'user'
os.path.dirname(b_name) == 'user/desktop'

a_path.parent == Path('user')

Saving Paths

To save a Path in a cross platform way, you should consider using relative Paths, i.e. relative to the service file, theme file, data folder and so on.

To facilitate the above a couple modules have been implemented.


This module has been designed with the future in mind, whilst implementing the minimum required for the current use. With the addition of a function to register custom objects this module will be able to en/decode objects that the json standard library cannot. The Path object has been re-implemented (openlp.core.common.path) to provide methods to facilitate this. Saving a path is as simple as:

import json

from openlp.core.common.path import Path
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder

orig_path = Path('/', 'home', 'user', 'desktop', 'file.ext')
json_encoded_path = json.dumps(orig_path, cls=OpenLPJsonEncoder)
json_encoded_path == '{"__Path__": ["/", "home", "user", "desktop", "file.ext"]}'

new_path = json.loads(json_encoded_path, cls=OpenLPJsonDecoder)
new_path == Path('/home/user/desktop/file.ext')

When the json methods are passed with the additional arguments, the arguments are passed to the json en/decode methods of the custom object. The custom Path object accepts a 'base_path' parameter, which allows it to automatically convert the Path to a relative path (if possible) for storage. The above code then becomes:

import json

from openlp.core.common.path import Path
from openlp.core.common.json import OpenLPJsonDecoder, OpenLPJsonEncoder

base_path = Path('/', 'home', 'user', 'desktop')

orig_path = Path('/', 'home', 'user', 'desktop', 'file.ext')
json_encoded_path = json.dumps(orig_path, cls=OpenLPJsonEncoder, base_path=base_path)
json_encoded_path == '{"__Path__": ["file.ext"]}'

See how json_encoded_path is now relative to the base path? Any relative paths stored in this way will automatically be converted to an absolute path if a base_path parameter is also supplied when the json object is decoded:

differnet_base_path = Path('/', 'home', 'annother_user', 'desktop')

new_path = json.loads(json_encoded_path, cls=OpenLPJsonDecoder, base_path=differnet_base_path )
new_path == Path('/home/annother_user/desktop/file.ext')

Whilst PureWindowsPath accepts forward and back slashes, PurePoisixPath only supports forward slashes.

For ultimate portability, we should save the value of parts on the Path object. That way they can be used in a Path object constructor. See the example that follows:

orig_path = Path ('user/desktop') == ('user', 'desktop')

orig_parts =

new_path = Path(*orig_parts)
new_path == Path('user/desktop')


A 'PathType' (openlp.core.lib.db) has also been created to wrap the json en/decoding of path objects to allow them to be stored as plain text in the database. As OpenLP uses 'open' formats such as OpenLyrics to export data, it is expected that the sqlite databases are kept internal. For this reason Paths stored using the 'PathType' are made relative to the data folder. This allows for easy changing of the data path.

For an example of the 'PathType' in use see the song and image pugins' database code.