Passed
Push — main ( c9ac86...a4501a )
by Douglas
02:00
created

pocketutils.tools.path_tools   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 251
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 154
dl 0
loc 251
rs 9.1199
c 0
b 0
f 0
wmc 41

5 Methods

Rating   Name   Duplication   Size   Complexity  
A PathTools.updir() 0 15 3
B PathTools.sanitize_path() 0 41 7
A PathTools.sanitize_nodes() 0 24 4
A PathTools.guess_trash() 0 15 3
F PathTools.sanitize_node() 0 136 24

How to fix   Complexity   

Complexity

Complex classes like pocketutils.tools.path_tools often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import sys
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
from typing import Callable, Optional, Sequence
3
4
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
5
6
from pocketutils.core.exceptions import *
0 ignored issues
show
Unused Code introduced by
ConstructionError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
AlgorithmError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DataIntegrityError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
UnrecognizedKeyError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ParsingError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
InexactRoundError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
NumericConversionError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
NullValueError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ZeroDistanceError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
OutOfRangeError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
StringPatternError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
XValueError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
EmptyCollectionError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DeviceConnectionError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
XTypeError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
HardwareError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
PathError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
MissingDeviceError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
CalledProcessError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
Collection was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
wraps was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
BadWriteError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
PathExistsError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
LengthMismatchError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DownloadError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
CacheSaveError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
CacheLoadError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
SaveError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
LoadError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
WrongDimensionError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
UploadError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
LengthError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DeprecatedWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DataWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
AlreadyUsedError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ConfigWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ImportFailedWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
IgnoringRequestWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
StrangeRequestWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DangerousRequestWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ImmatureWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ObsoleteWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
UnsupportedOpError was imported with wildcard, but is not used.
Loading history...
Coding Style introduced by
The usage of wildcard imports like pocketutils.core.exceptions should generally be avoided.
Loading history...
Unused Code introduced by
CodeIncompleteError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
NaturalExpectedError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
KeyLike was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ErrorUtils was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
XException was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
XWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
Error was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
IllegalStateError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
AlgorithmWarning was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
NotConstructableError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
LockedError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
MismatchedDataError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
MissingResourceError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
LookupFailedError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ImmutableError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
AmbiguousRequestError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
MissingEnvVarError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ResourceError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
IncompatibleDataError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
RefusingRequestError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
MissingConfigKeyError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ConfigError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
BadCommandError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
ReservedError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
UserError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
MultipleMatchesError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
OpStateError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
HashValidationError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
PathIsNotADirError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
PathIsNotAFileError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
XFileExistsError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
RequestError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
RefusingError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
XKeyError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
AbstractWrappedError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
InjectionError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DbLookupError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DownloadTimeoutError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
FileDoesNotExistError was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
DirDoesNotExistError was imported with wildcard, but is not used.
Loading history...
7
from pocketutils.tools.base_tools import BaseTools
8
9
logger = logging.getLogger("pocketutils")
10
11
12
class PathTools(BaseTools):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
13
    @classmethod
14
    def updir(cls, n: int, *parts) -> Path:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "n" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
15
        """
16
        Get an absolute path ``n`` parents from ``os.getcwd()``.
17
        Does not sanitize.
18
19
        Ex: In dir '/home/john/dir_a/dir_b':
20
            updir(2, 'dir1', 'dir2')  # returns Path('/home/john/dir1/dir2')
21
        """
22
        base = Path(os.getcwd())
23
        for _ in range(n):
24
            base = base.parent
25
        for part in parts:
26
            base = base / part
27
        return base.resolve()
28
29
    @classmethod
30
    def guess_trash(cls) -> Path:
31
        """
32
        Chooses a reasonable path for trash based on the OS.
33
        This is not reliable. For a more sophisticated solution,
34
        see https://github.com/hsoft/send2trash
35
        However, even that can fail.
36
        """
37
        plat = sys.platform.lower()
38
        if "darwin" in plat:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
39
            return Path.home() / ".Trash"
40
        elif "win" in plat:
41
            return Path(Path.home().root) / "$Recycle.Bin"
42
        else:
43
            return Path.home() / ".trash"
44
45
    @classmethod
46
    def sanitize_path(
47
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
48
        path: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
49
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
50
        is_file: Optional[bool] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
51
        fat: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
52
        trim: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
53
        warn: Union[bool, Callable[[str], Any]] = True,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
54
    ) -> Path:
55
        r"""
56
        Sanitizes a path for major OSes and filesystems.
57
        Also see sanitize_path_nodes and sanitize_path_node.
58
        Mostly platform-independent.
59
60
        The idea is to sanitize for both Windows and Posix, regardless of the platform in use.
61
        The sanitization should be as uniform as possible for both platforms.
62
        This works for at least Windows+NTFS.
63
        Tilde substitution for long filenames in Windows is unsupported.
64
65
        A corner case is drive letters in Linux:
66
        "C:\\Users\\john" is converted to '/C:/users/john' if os.name=='posix'
67
        """
68
        w = {True: logger.warning, False: lambda _: None}.get(warn, warn)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "w" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
69
        path = str(path)
70
        if path.startswith("\\\\?"):
71
            raise IllegalPathError(
72
                f"Long UNC Windows paths (\\\\? prefix) are not supported (path '{path}')"
73
            )
74
        bits = str(path).strip().replace("\\", "/").split("/")
75
        new_nodes = list(cls.sanitize_nodes(bits, is_file=is_file, fat=fat, trim=trim))
76
        # unfortunately POSIX turns Path('C:\', '5') into C:\/5
77
        # this isn't an ideal way to fix it, but it works
78
        pat = regex.compile(r"^([A-Z]:)(?:\\)?$", flags=regex.V1)
79
        if os.name == "posix" and len(new_nodes) > 0 and pat.fullmatch(new_nodes[0]):
80
            new_nodes[0] = new_nodes[0].rstrip("\\")
81
            new_nodes.insert(0, "/")
82
        new_path = Path(*new_nodes)
83
        if new_path != path:
84
            w(f"Sanitized filename {path} → {new_path}")
85
        return Path(new_path)
86
87
    @classmethod
88
    def sanitize_nodes(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
89
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
90
        bits: Sequence[PathLike],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
91
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
92
        is_file: Optional[bool] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
93
        fat: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
94
        trim: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
95
    ) -> Sequence[str]:
96
        fixed_bits = [
97
            bit + os.sep
98
            if i == 0 and bit.strip() in ["", ".", ".."]
99
            else cls.sanitize_node(
100
                bit,
101
                is_file=(False if i < len(bits) - 1 else is_file),
102
                trim=trim,
103
                fat=fat,
104
                is_root_or_drive=(None if i == 0 else False),
105
            )
106
            for i, bit in enumerate(bits)
107
            if bit.strip() not in ["", "."]
108
            or i == 0  # ignore // (empty) just like Path does (but fail on sanitize_path_node(' '))
109
        ]
110
        return [bit for i, bit in enumerate(fixed_bits) if i == 0 or bit not in ["", "."]]
111
112
    @classmethod
113
    def sanitize_node(
114
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
115
        bit: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
116
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
117
        is_file: Optional[bool] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
118
        is_root_or_drive: Optional[bool] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
119
        fat: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
120
        trim: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
121
    ) -> str:
122
        r"""
123
        Sanitizes a path node such that it will be fine for major OSes and filesystems.
124
        For example:
125
        - 'plums;and/or;apples' becomes 'plums_and_or_apples' (escaped ; and /)
126
        - 'null.txt' becomes '_null_.txt' ('null' is forbidden in Windows)
127
        - 'abc  ' becomes 'abc' (no trailing spaces)
128
        The behavior is platform-independent -- os, sys, and pathlib are not used.
129
        For ex, calling sanitize_path_node(r'C:\') returns r'C:\' on both Windows and Linux
130
        If you want to sanitize a whole path, see sanitize_path instead.
131
132
        Args:
133
            bit: The node
134
            is_file: False for directories, True otherwise, None if unknown
135
            is_root_or_drive: True if known to be the root ('/') or a drive ('C:\'), None if unknown
136
            fat: Also make compatible with FAT filesystems
137
            trim: Truncate to 254 chars (otherwise fails)
138
139
        Returns:
140
            A string
141
        """
142
        # since is_file and is_root_or_drive are both Optional[bool], let's be explicit and use 'is' for clarity
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (112/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
143
        if is_file is True and is_root_or_drive is True:
144
            raise ContradictoryRequestError("is_file and is_root_or_drive are both true")
145
        if is_file is True and is_root_or_drive is None:
146
            is_root_or_drive = False
147
        if is_root_or_drive is True and is_file is None:
148
            is_file = False
149
        source_bit = copy(str(bit))
150
        bit = str(bit).strip()
151
        # first, catch root or drive as long as is_root_or_drive is not false
152
        # if is_root_or_drive is True (which is a weird call), then fail if it's not
153
        # otherwise, it's not a root or drive letter, so keep going
154
        if is_root_or_drive is not False:
155
            # \ is allowed in Windows
156
            if bit in ["/", "\\"]:
157
                return bit
158
            m = regex.compile(r"^([A-Z]:)(?:\\)?$", flags=regex.V1).fullmatch(bit)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "m" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
159
            # this is interesting
160
            # for bit=='C:' and is_root_or_drive=None,
161
            # it could be either a drive letter
162
            # or a file path that should be corrected to 'C_'
163
            # I guess here we're going with a drive letter
164
            if m is not None:
165
                # we need C:\ and not C: because:
166
                # Path('C:\\', '5').is_absolute() is True
167
                # but Path('C:', '5').is_absolute() is False
168
                # unfortunately, doing Path('C:\\', '5') on Linux gives 'C:\\/5'
169
                # I can't handle that here, but sanitize_path() will account for it
170
                return m.group(1) + "\\"
171
            if is_root_or_drive is True:
172
                raise IllegalPathError(f"Node '{bit}' is not the root or a drive letter")
173
        # note that we can't call WindowsPath.is_reserved because it can't be instantiated on non-Linux
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (103/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
174
        # also, these appear to be different from the ones defined there
175
        bad_chars = {
176
            "<",
177
            ">",
178
            ":",
179
            '"',
180
            "|",
181
            "?",
182
            "*",
183
            "\\",
184
            "/",
185
            *{chr(c) for c in range(128, 128 + 33)},
186
            *{chr(c) for c in range(0, 32)},
187
            "\t",
188
        }
189
        # don't handle Long UNC paths
190
        # also cannot be blank or whitespace
191
        # the $ suffixed ones are for FAT
192
        # no CLOCK$, even with an ext
193
        # also no SCREEN$
194
        bad_strs = {
195
            "CON",
196
            "PRN",
197
            "AUX",
198
            "NUL",
199
            "COM1",
200
            "COM2",
201
            "COM3",
202
            "COM4",
203
            "COM5",
204
            "COM6",
205
            "COM7",
206
            "COM8",
207
            "COM9",
208
            "LPT1",
209
            "LPT2",
210
            "LPT3",
211
            "LPT4",
212
            "LPT5",
213
            "LPT6",
214
            "LPT7",
215
            "LPT8",
216
            "LPT9",
217
        }
218
        if fat:
219
            bad_strs += {"$IDLE$", "CONFIG$", "KEYBD$", "SCREEN$", "CLOCK$", "LST"}
220
        # just dots is invalid
221
        if set(bit.replace(" ", "")) == "." and bit not in ["..", "."]:
222
            bit = "_" + bit + "_"
223
            # raise IllegalPathError(f"Node '{source_bit}' is invalid")
224
        for q in bad_chars:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "q" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
225
            bit = bit.replace(q, "_")
226
        if bit.upper() in bad_strs:
227
            # arbitrary decision
228
            bit = "_" + bit + "_"
229
        else:
230
            stub, ext = os.path.splitext(bit)
231
            if stub.upper() in bad_strs:
232
                bit = "_" + stub + "_" + ext
233
        if bit.strip() == "":
234
            bit = "_" + bit + "_"
235
            # raise IllegalPathError(f"Node '{source_bit}' is empty or contains only whitespace")
236
        # "." cannot end a node
237
        bit = bit.rstrip()
238
        if is_file is not True and (bit == "." or bit == ".."):
0 ignored issues
show
Unused Code introduced by
Consider merging these comparisons with "in" to "bit in ('.', '..')"
Loading history...
239
            return bit
240
        # never allow '.' or ' ' to end a filename
241
        bit = bit.rstrip(". ")
242
        # do this after
243
        if len(bit) > 254 and trim:
244
            bit = bit[:254]
245
        elif len(bit) > 254:
246
            raise IllegalPathError(f"Node '{source_bit}' has more than 254 characters")
247
        return bit
248
249
250
__all__ = ["PathTools"]
251