Skip to content

API documentation

PathRerooter

Class to facilitate converting paths with old-root to new-root.

This is not a replacement for shutil.copytree. This does not actually rename any files. It just creates the updated path, and so is useful for updating data files containing paths that have been changed.

Source code in src/path_tools_med/tools.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class PathRerooter:
    """Class to facilitate converting paths with old-root to new-root.

    This is not a replacement for `shutil.copytree`.  This does not actually rename
    any files.  It just creates the updated path, and so is useful for updating data
    files containing paths that have been changed.
    """

    def __init__(
        self,
        old_path_root: Path | str | None,
        new_path_root: Path | str | None,
        *_: Any,
        ignore: bool = False,
    ):
        """
        Constructor

        If both roots are None, then the fix operation quietly does nothing.

        If ignore is true, then the fix operation quietly does nothing for
        paths that do not start with the old root.
        This applies to all invocations of `fix(path)` called on this
        `PathRerooter` object.
        To only selectively ignore these situations leave `ignore=False`
        in the constructor,
        but use `fix(path, ignore=True)` on the pertinant invocations.

        :param old_path_root: the old root path to be converted from
        :param new_path_root: the new root path to be converted to
        :param ignore: if true, quietly just return paths that do not
                       start with the old root.
        """
        self.noop = old_path_root is None and new_path_root is None
        self.old_path_root_parts = list(Path(old_path_root).parts) if old_path_root else []
        self.new_path_root_parts = list(Path(new_path_root).parts) if new_path_root else []
        self.ignore = ignore

    def fix(self, path: Path | str, *_: Any, ignore: bool = False) -> Path:
        """
        Apply the defined root conversion to the specified path returning
        the new path object.

        No files are renamed, the new `pathlib.Path` object is just created.

        A RuntimeError is raised if the path does not start with the old-path,
        unless either `ignore=True` is passed on this invocation,
        or `ignore=True` was set in the constructor.

        :param path: path to be converted
        :param ignore: if true, quietly just return paths that do not start
                       with the old root.
        :return: the converted path
        """
        this_path = Path(path)
        if self.noop:
            return this_path

        old_path_parts = list(this_path.parts)
        for part in self.old_path_root_parts:
            if part == old_path_parts[0]:
                old_path_parts.pop(0)
            elif ignore or self.ignore:
                return this_path
            else:
                msg = (
                    f"old_path_part '{part}' does not match "
                    f"'{old_path_parts[0]}' in path '{this_path}'"
                )
                logger.error(msg)
                raise RuntimeError(msg)
        new_path_parts = [*self.new_path_root_parts, *old_path_parts]
        return Path().joinpath(*new_path_parts)

__init__(old_path_root: Path | str | None, new_path_root: Path | str | None, *_: Any, ignore: bool = False)

Constructor

If both roots are None, then the fix operation quietly does nothing.

If ignore is true, then the fix operation quietly does nothing for paths that do not start with the old root. This applies to all invocations of fix(path) called on this PathRerooter object. To only selectively ignore these situations leave ignore=False in the constructor, but use fix(path, ignore=True) on the pertinant invocations.

:param old_path_root: the old root path to be converted from :param new_path_root: the new root path to be converted to :param ignore: if true, quietly just return paths that do not start with the old root.

Source code in src/path_tools_med/tools.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def __init__(
    self,
    old_path_root: Path | str | None,
    new_path_root: Path | str | None,
    *_: Any,
    ignore: bool = False,
):
    """
    Constructor

    If both roots are None, then the fix operation quietly does nothing.

    If ignore is true, then the fix operation quietly does nothing for
    paths that do not start with the old root.
    This applies to all invocations of `fix(path)` called on this
    `PathRerooter` object.
    To only selectively ignore these situations leave `ignore=False`
    in the constructor,
    but use `fix(path, ignore=True)` on the pertinant invocations.

    :param old_path_root: the old root path to be converted from
    :param new_path_root: the new root path to be converted to
    :param ignore: if true, quietly just return paths that do not
                   start with the old root.
    """
    self.noop = old_path_root is None and new_path_root is None
    self.old_path_root_parts = list(Path(old_path_root).parts) if old_path_root else []
    self.new_path_root_parts = list(Path(new_path_root).parts) if new_path_root else []
    self.ignore = ignore

fix(path: Path | str, *_: Any, ignore: bool = False) -> Path

Apply the defined root conversion to the specified path returning the new path object.

No files are renamed, the new pathlib.Path object is just created.

A RuntimeError is raised if the path does not start with the old-path, unless either ignore=True is passed on this invocation, or ignore=True was set in the constructor.

:param path: path to be converted :param ignore: if true, quietly just return paths that do not start with the old root. :return: the converted path

Source code in src/path_tools_med/tools.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def fix(self, path: Path | str, *_: Any, ignore: bool = False) -> Path:
    """
    Apply the defined root conversion to the specified path returning
    the new path object.

    No files are renamed, the new `pathlib.Path` object is just created.

    A RuntimeError is raised if the path does not start with the old-path,
    unless either `ignore=True` is passed on this invocation,
    or `ignore=True` was set in the constructor.

    :param path: path to be converted
    :param ignore: if true, quietly just return paths that do not start
                   with the old root.
    :return: the converted path
    """
    this_path = Path(path)
    if self.noop:
        return this_path

    old_path_parts = list(this_path.parts)
    for part in self.old_path_root_parts:
        if part == old_path_parts[0]:
            old_path_parts.pop(0)
        elif ignore or self.ignore:
            return this_path
        else:
            msg = (
                f"old_path_part '{part}' does not match "
                f"'{old_path_parts[0]}' in path '{this_path}'"
            )
            logger.error(msg)
            raise RuntimeError(msg)
    new_path_parts = [*self.new_path_root_parts, *old_path_parts]
    return Path().joinpath(*new_path_parts)

backup_path(path: Path | str, backup_extension: str = '.bak') -> Path

Rename the path object specified by path to path.backup_extension. If there is an existing path.backup_extension, file it will be renamed with a version number extension using make_versioned_backup.

WARNING: The operations are not atomic and so it is possible for another process to create another path.backup_extension or path.backup_extension.~#~ file in between the time that this process determines the file to create doesn't exist and when it renames the file. If this happens the earlier created file will be overwritten, see pathlib.Path.replace.

:param path: the path to the file to be renamed :param backup_extension: the backup extension to be appended :return: the new path object

Source code in src/path_tools_med/tools.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def backup_path(path: Path | str, backup_extension: str = ".bak") -> Path:
    """
    Rename the path object specified by `path` to `path.backup_extension`.
    If there is an existing `path.backup_extension`, file it will be renamed
    with a version number extension using `make_versioned_backup`.

    WARNING: The operations are not atomic and so it is possible for another
    process to create another `path.backup_extension` or `path.backup_extension.~#~`
    file in between the time that this process determines the file to create doesn't
    exist and when it renames the file.  If this happens the earlier created file
    will be overwritten, see `pathlib.Path.replace`.

    :param path: the path to the file to be renamed
    :param backup_extension: the backup extension to be appended
    :return: the new path object
    """
    this_path = Path(path)
    path_backup = Path(str(this_path) + backup_extension)
    if path_backup.exists():
        make_versioned_backup(path_backup)
    return this_path.replace(path_backup)

get_highest_backup_version(path: Path | str) -> int

Return the highest existing backup version number for existing path.~#~ files. If no such files exist return 0.

:param path: :return: hightest existing backup version number

Source code in src/path_tools_med/tools.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def get_highest_backup_version(path: Path | str) -> int:
    """
    Return the highest existing backup version number for existing `path.~#~` files.
    If no such files exist return 0.

    :param path:
    :return: hightest existing backup version number
    """
    versions = [0]
    this_path = Path(path)
    versions.extend(
        [
            int(x[x.rindex("~", 0, -1) + 1 : -1])
            for x in map(str, this_path.parent.glob(f"{this_path.name}.~*~"))
        ],
    )
    return max(versions)

make_versioned_backup(path: Path) -> Path

Rename the path object specified with a version number extension that will be greater than any existing version number.

WARNING: The operations are not atomic and so it is possible for another process to create another path.backup_extension or path.backup_extension.~#~ file in between the time that this process determines the file to create doesn't exist and when it renames the file. If this happens the earlier created file will be overwritten, see pathlib.Path.replace.

:param path: the path to the file to be renamed :return: the new path object

Source code in src/path_tools_med/tools.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def make_versioned_backup(path: Path) -> Path:
    """
    Rename the path object specified with a version number extension
    that will be greater than any existing version number.

    WARNING: The operations are not atomic and so it is possible for another
    process to create another `path.backup_extension` or `path.backup_extension.~#~`
    file in between the time that this process determines the file to create doesn't
    exist and when it renames the file.  If this happens the earlier created file
    will be overwritten, see `pathlib.Path.replace`.

    :param path: the path to the file to be renamed
    :return: the new path object
    """
    this_path = Path(path)
    return this_path.replace(
        f"{this_path}.~{get_highest_backup_version(this_path) + 1}~",
    )

tools

PathRerooter

Class to facilitate converting paths with old-root to new-root.

This is not a replacement for shutil.copytree. This does not actually rename any files. It just creates the updated path, and so is useful for updating data files containing paths that have been changed.

Source code in src/path_tools_med/tools.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class PathRerooter:
    """Class to facilitate converting paths with old-root to new-root.

    This is not a replacement for `shutil.copytree`.  This does not actually rename
    any files.  It just creates the updated path, and so is useful for updating data
    files containing paths that have been changed.
    """

    def __init__(
        self,
        old_path_root: Path | str | None,
        new_path_root: Path | str | None,
        *_: Any,
        ignore: bool = False,
    ):
        """
        Constructor

        If both roots are None, then the fix operation quietly does nothing.

        If ignore is true, then the fix operation quietly does nothing for
        paths that do not start with the old root.
        This applies to all invocations of `fix(path)` called on this
        `PathRerooter` object.
        To only selectively ignore these situations leave `ignore=False`
        in the constructor,
        but use `fix(path, ignore=True)` on the pertinant invocations.

        :param old_path_root: the old root path to be converted from
        :param new_path_root: the new root path to be converted to
        :param ignore: if true, quietly just return paths that do not
                       start with the old root.
        """
        self.noop = old_path_root is None and new_path_root is None
        self.old_path_root_parts = list(Path(old_path_root).parts) if old_path_root else []
        self.new_path_root_parts = list(Path(new_path_root).parts) if new_path_root else []
        self.ignore = ignore

    def fix(self, path: Path | str, *_: Any, ignore: bool = False) -> Path:
        """
        Apply the defined root conversion to the specified path returning
        the new path object.

        No files are renamed, the new `pathlib.Path` object is just created.

        A RuntimeError is raised if the path does not start with the old-path,
        unless either `ignore=True` is passed on this invocation,
        or `ignore=True` was set in the constructor.

        :param path: path to be converted
        :param ignore: if true, quietly just return paths that do not start
                       with the old root.
        :return: the converted path
        """
        this_path = Path(path)
        if self.noop:
            return this_path

        old_path_parts = list(this_path.parts)
        for part in self.old_path_root_parts:
            if part == old_path_parts[0]:
                old_path_parts.pop(0)
            elif ignore or self.ignore:
                return this_path
            else:
                msg = (
                    f"old_path_part '{part}' does not match "
                    f"'{old_path_parts[0]}' in path '{this_path}'"
                )
                logger.error(msg)
                raise RuntimeError(msg)
        new_path_parts = [*self.new_path_root_parts, *old_path_parts]
        return Path().joinpath(*new_path_parts)

__init__(old_path_root: Path | str | None, new_path_root: Path | str | None, *_: Any, ignore: bool = False)

Constructor

If both roots are None, then the fix operation quietly does nothing.

If ignore is true, then the fix operation quietly does nothing for paths that do not start with the old root. This applies to all invocations of fix(path) called on this PathRerooter object. To only selectively ignore these situations leave ignore=False in the constructor, but use fix(path, ignore=True) on the pertinant invocations.

:param old_path_root: the old root path to be converted from :param new_path_root: the new root path to be converted to :param ignore: if true, quietly just return paths that do not start with the old root.

Source code in src/path_tools_med/tools.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def __init__(
    self,
    old_path_root: Path | str | None,
    new_path_root: Path | str | None,
    *_: Any,
    ignore: bool = False,
):
    """
    Constructor

    If both roots are None, then the fix operation quietly does nothing.

    If ignore is true, then the fix operation quietly does nothing for
    paths that do not start with the old root.
    This applies to all invocations of `fix(path)` called on this
    `PathRerooter` object.
    To only selectively ignore these situations leave `ignore=False`
    in the constructor,
    but use `fix(path, ignore=True)` on the pertinant invocations.

    :param old_path_root: the old root path to be converted from
    :param new_path_root: the new root path to be converted to
    :param ignore: if true, quietly just return paths that do not
                   start with the old root.
    """
    self.noop = old_path_root is None and new_path_root is None
    self.old_path_root_parts = list(Path(old_path_root).parts) if old_path_root else []
    self.new_path_root_parts = list(Path(new_path_root).parts) if new_path_root else []
    self.ignore = ignore

fix(path: Path | str, *_: Any, ignore: bool = False) -> Path

Apply the defined root conversion to the specified path returning the new path object.

No files are renamed, the new pathlib.Path object is just created.

A RuntimeError is raised if the path does not start with the old-path, unless either ignore=True is passed on this invocation, or ignore=True was set in the constructor.

:param path: path to be converted :param ignore: if true, quietly just return paths that do not start with the old root. :return: the converted path

Source code in src/path_tools_med/tools.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def fix(self, path: Path | str, *_: Any, ignore: bool = False) -> Path:
    """
    Apply the defined root conversion to the specified path returning
    the new path object.

    No files are renamed, the new `pathlib.Path` object is just created.

    A RuntimeError is raised if the path does not start with the old-path,
    unless either `ignore=True` is passed on this invocation,
    or `ignore=True` was set in the constructor.

    :param path: path to be converted
    :param ignore: if true, quietly just return paths that do not start
                   with the old root.
    :return: the converted path
    """
    this_path = Path(path)
    if self.noop:
        return this_path

    old_path_parts = list(this_path.parts)
    for part in self.old_path_root_parts:
        if part == old_path_parts[0]:
            old_path_parts.pop(0)
        elif ignore or self.ignore:
            return this_path
        else:
            msg = (
                f"old_path_part '{part}' does not match "
                f"'{old_path_parts[0]}' in path '{this_path}'"
            )
            logger.error(msg)
            raise RuntimeError(msg)
    new_path_parts = [*self.new_path_root_parts, *old_path_parts]
    return Path().joinpath(*new_path_parts)

backup_path(path: Path | str, backup_extension: str = '.bak') -> Path

Rename the path object specified by path to path.backup_extension. If there is an existing path.backup_extension, file it will be renamed with a version number extension using make_versioned_backup.

WARNING: The operations are not atomic and so it is possible for another process to create another path.backup_extension or path.backup_extension.~#~ file in between the time that this process determines the file to create doesn't exist and when it renames the file. If this happens the earlier created file will be overwritten, see pathlib.Path.replace.

:param path: the path to the file to be renamed :param backup_extension: the backup extension to be appended :return: the new path object

Source code in src/path_tools_med/tools.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def backup_path(path: Path | str, backup_extension: str = ".bak") -> Path:
    """
    Rename the path object specified by `path` to `path.backup_extension`.
    If there is an existing `path.backup_extension`, file it will be renamed
    with a version number extension using `make_versioned_backup`.

    WARNING: The operations are not atomic and so it is possible for another
    process to create another `path.backup_extension` or `path.backup_extension.~#~`
    file in between the time that this process determines the file to create doesn't
    exist and when it renames the file.  If this happens the earlier created file
    will be overwritten, see `pathlib.Path.replace`.

    :param path: the path to the file to be renamed
    :param backup_extension: the backup extension to be appended
    :return: the new path object
    """
    this_path = Path(path)
    path_backup = Path(str(this_path) + backup_extension)
    if path_backup.exists():
        make_versioned_backup(path_backup)
    return this_path.replace(path_backup)

get_highest_backup_version(path: Path | str) -> int

Return the highest existing backup version number for existing path.~#~ files. If no such files exist return 0.

:param path: :return: hightest existing backup version number

Source code in src/path_tools_med/tools.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def get_highest_backup_version(path: Path | str) -> int:
    """
    Return the highest existing backup version number for existing `path.~#~` files.
    If no such files exist return 0.

    :param path:
    :return: hightest existing backup version number
    """
    versions = [0]
    this_path = Path(path)
    versions.extend(
        [
            int(x[x.rindex("~", 0, -1) + 1 : -1])
            for x in map(str, this_path.parent.glob(f"{this_path.name}.~*~"))
        ],
    )
    return max(versions)

make_versioned_backup(path: Path) -> Path

Rename the path object specified with a version number extension that will be greater than any existing version number.

WARNING: The operations are not atomic and so it is possible for another process to create another path.backup_extension or path.backup_extension.~#~ file in between the time that this process determines the file to create doesn't exist and when it renames the file. If this happens the earlier created file will be overwritten, see pathlib.Path.replace.

:param path: the path to the file to be renamed :return: the new path object

Source code in src/path_tools_med/tools.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def make_versioned_backup(path: Path) -> Path:
    """
    Rename the path object specified with a version number extension
    that will be greater than any existing version number.

    WARNING: The operations are not atomic and so it is possible for another
    process to create another `path.backup_extension` or `path.backup_extension.~#~`
    file in between the time that this process determines the file to create doesn't
    exist and when it renames the file.  If this happens the earlier created file
    will be overwritten, see `pathlib.Path.replace`.

    :param path: the path to the file to be renamed
    :return: the new path object
    """
    this_path = Path(path)
    return this_path.replace(
        f"{this_path}.~{get_highest_backup_version(this_path) + 1}~",
    )