Completed
Push — main ( 0501c2...b833a9 )
by Douglas
11:43
created

pocketutils.tools.filesys_tools   F

Complexity

Total Complexity 141

Size/Duplication

Total Lines 754
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 141
eloc 473
dl 0
loc 754
rs 2
c 0
b 0
f 0

50 Methods

Rating   Name   Duplication   Size   Complexity  
A PathInfo.is_socket() 0 3 1
A PathInfo.is_file() 0 3 1
A FilesysTools.is_macos() 0 3 1
A PathInfo.is_readable_file() 0 3 1
A PathInfo.mod_dt() 0 9 2
B FilesysTools.prep_dir() 0 18 6
A FilesysTools.dt_for_filesys() 0 5 2
A PathInfo.is_symlink() 0 3 1
A FilesysTools.dump_error_as_dict() 0 9 2
A FilesysTools.dump_error() 0 17 3
A FilesysTools.tmppath() 0 12 2
A PathInfo.is_readable_dir() 0 3 1
A FilesysTools._expire_warning_info() 0 12 2
B FilesysTools.open_file() 0 21 6
A FilesysTools.delete_surefire() 0 31 4
A PathInfo.is_char_device() 0 3 1
A PathInfo.is_broken_symlink() 0 3 1
A PathInfo.mod_or_create_dt() 0 10 2
A FilesysTools._warn_expired() 0 36 4
A FilesysTools.write_compressed_text() 0 7 1
B FilesysTools.tmpfile() 0 17 6
A FilesysTools.trash() 0 14 2
A FilesysTools.tmpdir() 0 4 2
A PathInfo.is_dir() 0 3 1
A PathInfo.is_block_device() 0 3 1
A PathInfo._get_dt() 0 5 2
D FilesysTools.read_any() 0 56 12
B FilesysTools.read_properties_file() 0 31 7
A FilesysTools.replace_in_file() 0 10 2
A FilesysTools.write_lines() 0 23 4
A PathInfo.access_dt() 0 7 1
A FilesysTools.__stat_raw() 0 8 4
A FilesysTools.read_compressed_text() 0 7 1
A PathInfo.is_valid_symlink() 0 3 1
A PathInfo.is_writeable_dir() 0 3 1
B FilesysTools.check_expired() 0 52 8
C FilesysTools.write_properties_file() 0 31 10
A PathInfo.is_fifo() 0 3 1
A FilesysTools.try_cleanup() 0 11 2
A PathInfo.exists() 0 6 1
A FilesysTools.save_json() 0 4 2
A FilesysTools.load_json() 0 3 1
A FilesysTools.is_linux() 0 3 1
A FilesysTools.prep_file() 0 12 4
B FilesysTools.read_lines_file() 0 13 6
A FilesysTools.hash_hex() 0 8 1
A PathInfo.create_dt() 0 9 2
C FilesysTools.get_info() 0 40 9
A FilesysTools.is_windows() 0 3 1
A PathInfo.is_writeable_file() 0 3 1

How to fix   Complexity   

Complexity

Complex classes like pocketutils.tools.filesys_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 gzip
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import hashlib
3
import logging
4
import os
5
import pathlib
6
import shutil
7
import stat
8
import sys
9
import tempfile
10
from contextlib import contextmanager
11
from dataclasses import dataclass
12
from datetime import datetime, timedelta
13
from pathlib import Path, PurePath
14
from typing import (
15
    Any,
16
    Callable,
17
    Generator,
18
    Iterable,
19
    Mapping,
20
    Optional,
21
    Sequence,
22
    SupportsBytes,
23
    Tuple,
24
    Type,
25
    Union,
26
)
27
28
import numpy as np
0 ignored issues
show
introduced by
Unable to import 'numpy'
Loading history...
29
import orjson
0 ignored issues
show
introduced by
Unable to import 'orjson'
Loading history...
30
import pandas as pd
0 ignored issues
show
introduced by
Unable to import 'pandas'
Loading history...
31
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
32
from defusedxml import ElementTree
0 ignored issues
show
introduced by
Unable to import 'defusedxml'
Loading history...
33
34
from pocketutils.core._internal import read_txt_or_gz, write_txt_or_gz
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
35
from pocketutils.core.chars import Chars
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
36
from pocketutils.core.exceptions import (
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
37
    AlreadyUsedError,
38
    ContradictoryRequestError,
39
    DirDoesNotExistError,
40
    FileDoesNotExistError,
41
    ParsingError,
42
)
43
from pocketutils.core.input_output import OpenMode, PathLike, Writeable
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
44
from pocketutils.tools.base_tools import BaseTools
45
from pocketutils.tools.path_tools import PathTools
46
from pocketutils.tools.sys_tools import SystemTools
47
from pocketutils.tools.unit_tools import UnitTools
48
49
logger = logging.getLogger("pocketutils")
50
COMPRESS_LEVEL = 9
51
52
53
@dataclass(frozen=True, repr=True)
0 ignored issues
show
best-practice introduced by
Too many instance attributes (8/7)
Loading history...
54
class PathInfo:
55
    """
56
    Info about an extant or nonexistent path as it was at some time.
57
    Use this to avoid making repeated filesystem calls (e.g. ``.is_dir()``):
58
    None of the properties defined here make OS calls.
59
60
    Attributes:
61
        source: The original path used for lookup; may be a symlink
62
        resolved: The fully resolved path, or None if it does not exist
63
        as_of: A datetime immediately before the system calls (system timezone)
64
        real_stat: ``os.stat_result``, or None if the path does not exist
65
        link_stat: ``os.stat_result``, or None if the path is not a symlink
66
        has_access: Path exists and has the 'a' flag set
67
        has_read: Path exists and has the 'r' flag set
68
        has_write: Path exists and has the 'w' flag set
69
70
    All the additional properties refer to the resolved path,
71
    except for :meth:`is_symlink`, :meth:`is_valid_symlink`,
72
    and :meth:`is_broken_symlink`.
73
    """
74
75
    source: Path
76
    resolved: Optional[Path]
77
    as_of: datetime
78
    real_stat: Optional[os.stat_result]
79
    link_stat: Optional[os.stat_result]
80
    has_access: bool
81
    has_read: bool
82
    has_write: bool
83
84
    @property
85
    def mod_or_create_dt(self) -> Optional[datetime]:
86
        """
87
        Returns the modification or access datetime.
88
        Uses whichever is available: creation on Windows and modification on Unix-like.
89
        """
90
        if os.name == "nt":
91
            return self._get_dt("st_ctime")
92
        # will work on posix; on java try anyway
93
        return self._get_dt("st_mtime")
94
95
    @property
96
    def mod_dt(self) -> Optional[datetime]:
97
        """
98
        Returns the modification datetime, if known.
99
        Returns None on Windows or if the path does not exist.
100
        """
101
        if os.name == "nt":
102
            return None
103
        return self._get_dt("st_mtime")
104
105
    @property
106
    def create_dt(self) -> Optional[datetime]:
107
        """
108
        Returns the creation datetime, if known.
109
        Returns None on Unix-like systems or if the path does not exist.
110
        """
111
        if os.name == "posix":
112
            return None
113
        return self._get_dt("st_ctime")
114
115
    @property
116
    def access_dt(self) -> Optional[datetime]:
117
        """
118
        Returns the access datetime.
119
        *Should* never return None if the path exists, but not guaranteed.
120
        """
121
        return self._get_dt("st_atime")
122
123
    @property
124
    def exists(self) -> bool:
125
        """
126
        Returns whether the resolved path exists.
127
        """
128
        return self.real_stat is not None
129
130
    @property
131
    def is_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
132
        return self.exists and stat.S_ISREG(self.real_stat.st_mode)
133
134
    @property
135
    def is_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
136
        return self.exists and stat.S_ISDIR(self.real_stat.st_mode)
137
138
    @property
139
    def is_readable_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
140
        return self.is_file and self.has_access and self.has_read
141
142
    @property
143
    def is_writeable_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
144
        return self.is_dir and self.has_access and self.has_write
145
146
    @property
147
    def is_readable_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
148
        return self.is_file and self.has_access and self.has_read
149
150
    @property
151
    def is_writeable_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
152
        return self.is_file and self.has_access and self.has_write
153
154
    @property
155
    def is_block_device(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
156
        return self.exists and stat.S_ISBLK(self.real_stat.st_mode)
157
158
    @property
159
    def is_char_device(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
160
        return self.exists and stat.S_ISCHR(self.real_stat.st_mode)
161
162
    @property
163
    def is_socket(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
164
        return self.exists and stat.S_ISSOCK(self.real_stat.st_mode)
165
166
    @property
167
    def is_fifo(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
168
        return self.exists and stat.S_ISFIFO(self.real_stat.st_mode)
169
170
    @property
171
    def is_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
172
        return self.link_stat is not None
173
174
    @property
175
    def is_valid_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
176
        return self.is_symlink and self.exists
177
178
    @property
179
    def is_broken_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
180
        return self.is_symlink and not self.exists
181
182
    def _get_dt(self, attr: str) -> Optional[datetime]:
183
        if self.real_stat is None:
184
            return None
185
        sec = getattr(self.real_stat, attr)
186
        return datetime.fromtimestamp(sec).astimezone()
187
188
189
class FilesysTools(BaseTools):
0 ignored issues
show
best-practice introduced by
Too many public methods (28/20)
Loading history...
190
    """
191
    Tools for file/directory creation, etc.
192
193
    .. caution::
194
        Some functions may be insecure.
195
    """
196
197
    @classmethod
198
    def read_compressed_text(cls, path: PathLike) -> str:
199
        """
200
        Reads text from a text file, optionally gzipped or bz2-ed.
201
        Recognized suffixes for compression are ``.gz``, ``.gzip``, ``.bz2``, and ``.bzip2``.
202
        """
203
        return read_txt_or_gz(path)
204
205
    @classmethod
206
    def write_compressed_text(cls, txt: str, path: PathLike, *, mkdirs: bool = False) -> None:
207
        """
208
        Writes text to a text file, optionally gzipped or bz2-ed.
209
        Recognized suffixes for compression are ``.gz``, ``.gzip``, ``.bz2``, and ``.bzip2``.
210
        """
211
        write_txt_or_gz(txt, path, mkdirs=mkdirs)
212
213
    @classmethod
214
    def is_linux(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
215
        return sys.platform == "linux"
216
217
    @classmethod
218
    def is_windows(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
219
        return sys.platform == "win32"
220
221
    @classmethod
222
    def is_macos(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
223
        return sys.platform == "darwin"
224
225
    @classmethod
226
    def get_info(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
227
        cls, path: PathLike, *, expand_user: bool = False, strict: bool = False
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
228
    ) -> PathInfo:
229
        path = Path(path)
230
        has_ignore_error = hasattr(pathlib, "_ignore_error")
231
        if not has_ignore_error:
232
            logger.debug("No _ignore_error found; some OSErrors may be suppressed")
233
        resolved = None
234
        real_stat = None
235
        has_access = False
236
        has_read = False
237
        has_write = False
238
        link_stat = None
239
        as_of = datetime.now().astimezone()
240
        if has_ignore_error or path.is_symlink() or path.exists():
241
            link_stat = cls.__stat_raw(path)
242
        if link_stat is not None:
243
            if expand_user:
244
                resolved = path.expanduser().resolve(strict=strict)
245
            else:
246
                resolved = path.resolve(strict=strict)
247
            if stat.S_ISLNK(link_stat.st_mode):
248
                real_stat = cls.__stat_raw(resolved)
249
            else:
250
                real_stat = link_stat
251
            has_access = os.access(path, os.X_OK, follow_symlinks=True)
252
            has_read = os.access(path, os.R_OK, follow_symlinks=True)
253
            has_write = os.access(path, os.W_OK, follow_symlinks=True)
254
            if not stat.S_ISLNK(link_stat.st_mode):
255
                link_stat = None
256
        return PathInfo(
257
            source=path,
258
            resolved=resolved,
259
            as_of=as_of,
260
            real_stat=real_stat,
261
            link_stat=link_stat,
262
            has_access=has_access,
263
            has_read=has_read,
264
            has_write=has_write,
265
        )
266
267
    @classmethod
268
    def prep_dir(cls, path: PathLike, *, exist_ok: bool = True) -> bool:
269
        """
270
        Prepares a directory by making it if it doesn't exist.
271
        If exist_ok is False, calls ``logger.warning`` if ``path`` already exists
272
        """
273
        path = Path(path)
274
        exists = path.exists()
275
        # On some platforms we get generic exceptions like permissions errors,
276
        # so these are better
277
        if exists and not path.is_dir():
278
            raise DirDoesNotExistError(f"Path {path} exists but is not a file")
279
        if exists and not exist_ok:
280
            logger.warning(f"Directory {path} already exists")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
281
        if not exists:
282
            # NOTE! exist_ok in mkdir throws an error on Windows
283
            path.mkdir(parents=True)
284
        return exists
285
286
    @classmethod
287
    def prep_file(cls, path: PathLike, *, exist_ok: bool = True) -> None:
288
        """
289
        Prepares a file path by making its parent directory.
290
        Same as ``pathlib.Path.mkdir`` but makes sure ``path`` is a file if it exists.
291
        """
292
        # On some platforms we get generic exceptions like permissions errors, so these are better
293
        path = Path(path)
294
        # check for errors first; don't make the dirs and then fail
295
        if path.exists() and not path.is_file() and not path.is_symlink():
296
            raise FileDoesNotExistError(f"Path {path} exists but is not a file")
297
        Path(path.parent).mkdir(parents=True, exist_ok=exist_ok)
298
299
    @classmethod
300
    def dump_error(
0 ignored issues
show
Coding Style Naming introduced by
Argument name "e" 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...
301
        cls, e: Optional[BaseException], path: Union[None, PathLike, datetime] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
302
    ) -> Path:
303
        """
304
        Writes a .json file containing the error message, stack trace, and sys info.
305
        System info is from :meth:`get_env_info`.
306
        """
307
        if path is None:
308
            path = f"err-dump-{cls.dt_for_filesys()}.json"
309
        elif isinstance(path, datetime):
310
            path = f"err-dump-{cls.dt_for_filesys(path)}.json"
311
        path = Path(path)
312
        data = cls.dump_error_as_dict(e)
313
        data = orjson.dumps(data, option=orjson.OPT_INDENT_2)
314
        path.write_bytes(data)
315
        return path
316
317
    @classmethod
318
    def dump_error_as_dict(cls, e: Optional[BaseException]) -> Mapping[str, Any]:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "e" 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...
introduced by
Missing function or method docstring
Loading history...
319
        try:
320
            system = SystemTools.get_env_info()
321
        except BaseException as e2:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as BaseException is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
Coding Style Naming introduced by
Variable name "e2" 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...
322
            system = f"UNKNOWN << {e2} >>"
323
        msg, tb = SystemTools.serialize_exception(e)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "tb" 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...
324
        tb = [t.as_dict() for t in tb]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "tb" 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...
325
        return dict(message=msg, stacktrace=tb, system=system)
326
327
    @classmethod
328
    def dt_for_filesys(cls, dt: Optional[datetime] = None) -> str:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "dt" 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...
introduced by
Missing function or method docstring
Loading history...
329
        if dt is None:
330
            dt = datetime.now()
331
        return dt.strftime("%Y-%m-%d_%H-%M-%S")
332
333
    @classmethod
334
    def delete_surefire(cls, path: PathLike) -> Optional[Exception]:
335
        """
336
        Deletes files or directories cross-platform, but working around multiple issues in Windows.
337
338
        Returns:
339
            None, or an Exception for minor warnings
340
341
        Raises:
342
            IOError: If it can't delete
343
        """
344
        # we need this because of Windows
345
        path = Path(path)
346
        logger.debug(f"Permanently deleting {path} ...")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
347
        chmod_err = None
348
        try:
349
            os.chmod(str(path), stat.S_IRWXU)
350
        except Exception as e:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
Coding Style Naming introduced by
Variable name "e" 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...
351
            chmod_err = e
352
        # another reason for returning exception:
353
        # We don't want to interrupt the current line being printed like in slow_delete
354
        if path.is_dir():
355
            shutil.rmtree(str(path), ignore_errors=True)  # ignore_errors because of Windows
356
            try:
357
                path.unlink(missing_ok=True)  # again, because of Windows
358
            except IOError:
359
                pass  # almost definitely because it doesn't exist
360
        else:
361
            path.unlink(missing_ok=True)
362
        logger.debug(f"Permanently deleted {path}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
363
        return chmod_err
364
365
    @classmethod
366
    def trash(cls, path: PathLike, trash_dir: Optional[PathLike] = None) -> None:
367
        """
368
        Trash a file or directory.
369
370
        Args:
371
            path: The path to move to the trash
372
            trash_dir: If None, uses :meth:`pocketutils.tools.path_tools.PathTools.guess_trash`
373
        """
374
        if trash_dir is None:
375
            trash_dir = PathTools.guess_trash()
376
        logger.debug(f"Trashing {path} to {trash_dir} ...")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
377
        shutil.move(str(path), str(trash_dir))
378
        logger.debug(f"Trashed {path} to {trash_dir}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
379
380
    @classmethod
381
    def try_cleanup(cls, path: Path, *, bound: Type[Exception] = PermissionError) -> None:
382
        """
383
        Try to delete a file (probably temp file), if it exists, and log any ``PermissionError``.
384
        """
385
        path = Path(path)
386
        # noinspection PyBroadException
387
        try:
388
            path.unlink(missing_ok=True)
389
        except bound:
390
            logger.error(f"Permission error preventing deleting {path}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
391
392
    @classmethod
393
    def read_lines_file(cls, path: PathLike, *, ignore_comments: bool = False) -> Sequence[str]:
394
        """
395
        Returns a list of lines in the file.
396
        Optionally skips lines starting with ``#`` or that only contain whitespace.
397
        """
398
        lines = []
399
        with cls.open_file(path, "r") as f:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
400
            for line in f.readlines():
401
                line = line.strip()
402
                if not ignore_comments or not line.startswith("#") and not len(line.strip()) == 0:
403
                    lines.append(line)
404
        return lines
405
406
    @classmethod
407
    def read_properties_file(cls, path: PathLike) -> Mapping[str, str]:
408
        """
409
        Reads a .properties file.
410
        A list of lines with key=value pairs (with an equals sign).
411
        Lines beginning with # are ignored.
412
        Each line must contain exactly 1 equals sign.
413
414
        .. caution::
415
            The escaping is not compliant with the standard
416
417
        Args:
418
            path: Read the file at this local path
419
420
        Returns:
421
            A dict mapping keys to values, both with surrounding whitespace stripped
422
        """
423
        dct = {}
424
        with cls.open_file(path, "r") as f:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
425
            for i, line in enumerate(f.readlines()):
426
                line = line.strip()
427
                if len(line) == 0 or line.startswith("#"):
428
                    continue
429
                if line.count("=") != 1:
430
                    raise ParsingError(f"Bad line {i} in {path}", resource=path)
431
                k, v = line.split("=")
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" 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...
432
                k, v = k.strip(), v.strip()
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" 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...
433
                if k in dct:
434
                    raise AlreadyUsedError(f"Duplicate property {k} (line {i})", key=k)
435
                dct[k] = v
436
        return dct
437
438
    @classmethod
439
    def write_properties_file(
440
        cls, properties: Mapping[Any, Any], path: Union[str, PurePath], mode: str = "o"
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
441
    ) -> None:
442
        """
443
        Writes a .properties file.
444
445
        .. caution::
446
            The escaping is not compliant with the standard
447
        """
448
        if not OpenMode(mode).write:
449
            raise ContradictoryRequestError(f"Cannot write text to {path} in mode {mode}")
450
        with FilesysTools.open_file(path, mode) as f:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
451
            bads = []
452
            for k, v in properties.items():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" 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...
453
                if "=" in k or "=" in v or "\n" in k or "\n" in v:
454
                    bads.append(k)
455
                f.write(
456
                    str(k).replace("=", "--").replace("\n", "\\n")
457
                    + "="
458
                    + str(v).replace("=", "--").replace("\n", "\\n")
459
                    + "\n"
460
                )
461
            if 0 < len(bads) <= 10:
462
                logger.warning(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
463
                    "At least one properties entry contains an equals sign or newline (\\n)."
464
                    f"These were escaped: {', '.join(bads)}"
465
                )
466
            elif len(bads) > 0:
467
                logger.warning(
468
                    "At least one properties entry contains an equals sign or newline (\\n),"
469
                    "which were escaped."
470
                )
471
472
    @classmethod
473
    def save_json(cls, data: Any, path: PathLike, mode: str = "w") -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
474
        with cls.open_file(path, mode) as f:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
475
            f.write(orjson.dumps(data).decode(encoding="utf8"))
476
477
    @classmethod
478
    def load_json(cls, path: PathLike) -> Union[dict, list]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
479
        return orjson.loads(Path(path).read_text(encoding="utf8"))
480
481
    @classmethod
482
    def read_any(
0 ignored issues
show
best-practice introduced by
Too many return statements (10/6)
Loading history...
483
        cls, path: PathLike
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
484
    ) -> Union[
485
        str,
486
        bytes,
487
        Sequence[str],
488
        pd.DataFrame,
489
        Sequence[int],
490
        Sequence[float],
491
        Sequence[str],
492
        Mapping[str, str],
493
    ]:
494
        """
495
        Reads a variety of simple formats based on filename extension.
496
        Includes '.txt', 'csv', .xml', '.properties', '.json'.
497
        Also reads '.data' (binary), '.lines' (text lines).
498
        And formatted lists: '.strings', '.floats', and '.ints' (ex: "[1, 2, 3]").
499
        """
500
        path = Path(path)
501
        ext = path.suffix.lstrip(".")
502
503
        def load_list(dtype):
504
            return [
505
                dtype(s)
506
                for s in FilesysTools.read_lines_file(path)[0]
507
                .replace(" ", "")
508
                .replace("[", "")
509
                .replace("]", "")
510
                .split(",")
511
            ]
512
513
        if ext == "lines":
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
514
            return cls.read_lines_file(path)
515
        elif ext == "txt":
516
            return path.read_text(encoding="utf-8")
517
        elif ext == "data":
518
            return path.read_bytes()
519
        elif ext == "json":
520
            return cls.load_json(path)
521
        elif ext in ["npy", "npz"]:
522
            return np.load(str(path), allow_pickle=False, encoding="utf8")
523
        elif ext == "properties":
524
            return cls.read_properties_file(path)
525
        elif ext == "csv":
526
            return pd.read_csv(path, encoding="utf8")
527
        elif ext == "ints":
528
            return load_list(int)
529
        elif ext == "floats":
530
            return load_list(float)
531
        elif ext == "strings":
532
            return load_list(str)
533
        elif ext == "xml":
534
            ElementTree.parse(path).getroot()
535
        else:
536
            raise TypeError(f"Did not recognize resource file type for file {path}")
537
538
    @classmethod
539
    @contextmanager
540
    def open_file(cls, path: PathLike, mode: Union[OpenMode, str], *, mkdir: bool = False):
541
        """
542
        Opens a text file, always using utf8, optionally gzipped.
543
544
        See Also:
545
            :class:`pocketutils.core.input_output.OpenMode`
546
        """
547
        path = Path(path)
548
        mode = OpenMode(mode)
549
        if mode.write and mkdir:
550
            path.parent.mkdir(exist_ok=True, parents=True)
551
        if not mode.read:
552
            cls.prep_file(path, exist_ok=mode.overwrite or mode.append)
553
        if mode.gzipped:
554
            yield gzip.open(path, mode.internal, compresslevel=COMPRESS_LEVEL, encoding="utf8")
555
        elif mode.binary:
556
            yield open(path, mode.internal, encoding="utf8")
557
        else:
558
            yield open(path, mode.internal, encoding="utf8")
559
560
    @classmethod
561
    def write_lines(cls, iterable: Iterable[Any], path: PathLike, mode: str = "w") -> int:
562
        r"""
563
        Just writes an iterable line-by-line to a file, using '\n'.
564
565
        Makes the parent directory if needed.
566
        Checks that the iterable is a "true iterable" (not a string or bytes).
567
568
        Returns:
569
            The number of lines written (the same as len(iterable) if iterable has a length)
570
571
        Raises:
572
            FileExistsError: If the path exists and append is False
573
            PathIsNotFileError: If append is True, and the path exists but is not a file
574
        """
575
        if not cls.is_true_iterable(iterable):
576
            raise TypeError("Not a true iterable")  # TODO include iterable if small
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
577
        n = 0
0 ignored issues
show
Coding Style Naming introduced by
Variable 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...
578
        with cls.open_file(path, mode) as f:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "f" 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...
579
            for x in iterable:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
580
                f.write(str(x) + "\n")
581
            n += 1
0 ignored issues
show
Coding Style Naming introduced by
Variable 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...
582
        return n
583
584
    @classmethod
585
    def hash_hex(cls, x: SupportsBytes, algorithm: str) -> str:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
586
        """
587
        Returns the hex-encoded hash of the object (converted to bytes).
588
        """
589
        m = hashlib.new(algorithm)
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...
590
        m.update(bytes(x))
591
        return m.hexdigest()
592
593
    @classmethod
594
    def replace_in_file(cls, path: PathLike, changes: Mapping[str, str]) -> None:
595
        """
596
        Uses ``regex.sub`` repeatedly to modify (AND REPLACE) a file's content.
597
        """
598
        path = Path(path)
599
        data = path.read_text(encoding="utf-8")
600
        for key, value in changes.items():
601
            data = regex.sub(key, value, data, flags=regex.V1 | regex.MULTILINE | regex.DOTALL)
602
        path.write_text(data, encoding="utf-8")
603
604
    @classmethod
605
    def tmppath(cls, path: Optional[PathLike] = None, **kwargs) -> Generator[Path, None, None]:
606
        """
607
        Makes a temporary Path. Won't create ``path`` but will delete it at the end.
608
        If ``path`` is None, will use ``tempfile.mkstemp``.
609
        """
610
        if path is None:
611
            _, path = tempfile.mkstemp()
612
        try:
613
            yield Path(path, **kwargs)
614
        finally:
615
            Path(path).unlink()
616
617
    @classmethod
618
    def tmpfile(
619
        cls, path: Optional[PathLike] = None, *, spooled: bool = False, **kwargs
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
620
    ) -> Generator[Writeable, None, None]:
621
        """
622
        Simple wrapper around tempfile functions.
623
        Wraps ``TemporaryFile``, ``NamedTemporaryFile``, and ``SpooledTemporaryFile``.
624
        """
625
        if spooled:
626
            with tempfile.SpooledTemporaryFile(**kwargs) as x:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
627
                yield x
628
        elif path is None:
629
            with tempfile.TemporaryFile(**kwargs) as x:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
630
                yield x
631
        else:
632
            with tempfile.NamedTemporaryFile(str(path), **kwargs) as x:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
633
                yield x
634
635
    @classmethod
636
    def tmpdir(cls, **kwargs) -> Generator[Path, None, None]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
637
        with tempfile.TemporaryDirectory(**kwargs) as x:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
638
            yield Path(x)
639
640
    @classmethod
641
    def check_expired(
642
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
643
        path: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
644
        max_sec: Union[timedelta, float],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
645
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
646
        parent: Optional[PathLike] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
647
        warn_expired_fmt: str = "{path_rel} is {delta} out of date [{mod_rel}]",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
648
        warn_unknown_fmt: str = "{path_rel} mod date is unknown [created: {create_rel}]",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
649
        log: Optional[Callable[[str], Any]] = logger.warning,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
650
    ) -> Optional[bool]:
651
        """
652
        Warns and returns True if ``path`` mod date is more than ``max_sec`` in the past.
653
        Returns None if it could not be determined.
654
655
        The formatting strings can refer to any of these (will be empty if unknown):
656
            - path: Full path
657
            - name: File/dir name
658
            - path_rel: Path relative to ``self._dir``, or full path if not under
659
            - now: formatted current datetime
660
            - [mod/create]_dt: Formatted mod/creation datetime
661
            - [mod/create]_rel: Mod/create datetime in terms of offset from now
662
            - [mod/create]_delta: Formatted timedelta from now
663
            - [mod/create]_delta_sec: Number of seconds from now (negative if now < mod/create dt)
664
665
        Args:
666
            path: A specific path to check
667
            max_sec: Max seconds, or a timedelta
668
            parent: If provided, path_rel is relative to this directory (to simplify warnings)
669
            warn_expired_fmt: Formatting string in terms of the variables listed above
670
            warn_unknown_fmt: Formatting string in terms of the variables listed above
671
            log: Log about problems
672
673
        Returns:
674
            Whether it is expired, or None if it could not be determined
675
        """
676
        path = Path(path)
677
        if log is None:
678
679
            def log(_):
680
                return None
681
682
        limit = max_sec if isinstance(max_sec, timedelta) else timedelta(seconds=max_sec)
683
        now = datetime.now().astimezone()
684
        info = FilesysTools.get_info(path)
685
        if info.mod_dt and now - info.mod_dt > limit:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
686
            cls._warn_expired(now, info.mod_dt, info.create_dt, path, parent, warn_expired_fmt, log)
687
            return True
688
        elif not info.mod_dt and (not info.create_dt or (now - info.create_dt) > limit):
689
            cls._warn_expired(now, info.mod_dt, info.create_dt, path, parent, warn_unknown_fmt, log)
690
            return None
691
        return False
692
693
    @classmethod
694
    def _warn_expired(
0 ignored issues
show
best-practice introduced by
Too many arguments (8/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (20/15).
Loading history...
695
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
696
        now: datetime,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
697
        mod: Optional[datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
698
        created: Optional[datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
699
        path: Path,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
700
        parent: Optional[Path],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
701
        fmt: Optional[str],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
702
        log: Callable[[str], Any],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
703
    ):
704
        if isinstance(fmt, str):
705
            fmt = fmt.format
706
        if parent is not None and path.is_relative_to(parent):
707
            path_rel = str(path.relative_to(parent))
708
        else:
709
            path_rel = str(path)
710
        now_str, mod_str, mod_rel, mod_delta, mod_delta_sec = cls._expire_warning_info(now, mod)
711
        _, create_str, create_rel, create_delta, create_delta_sec = cls._expire_warning_info(
712
            now, created
713
        )
714
        msg = fmt(
715
            path=path,
716
            path_rel=path_rel,
717
            name=path.name,
718
            now=now_str,
719
            mod_dt=mod_str,
720
            mod_rel=mod_rel,
721
            mod_delta=mod_delta,
722
            mod_sec=mod_delta_sec,
723
            create_dt=create_str,
724
            create_rel=create_rel,
725
            create_delta=create_delta,
726
            create_sec=create_delta_sec,
727
        )
728
        log(msg)
729
730
    @classmethod
731
    def _expire_warning_info(
732
        cls, now: datetime, then: Optional[datetime]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
733
    ) -> Tuple[str, str, str, str, str]:
734
        now_str = now.strftime("%Y-%m-%d %H:%M:%S")
735
        if then is None:
736
            return now_str, "", "", "", ""
737
        delta = now - then
738
        then_str = then.strftime("%Y-%m-%d %H:%M:%S")
739
        then_rel = UnitTools.approx_time_wrt(now, then)
740
        delta_str = UnitTools.delta_time_to_str(delta, space=Chars.narrownbsp)
741
        return now_str, then_str, then_rel, delta_str, str(delta.total_seconds())
742
743
    @classmethod
744
    def __stat_raw(cls, path: Path) -> Optional[os.stat_result]:
745
        try:
746
            return path.lstat()
747
        except OSError as e:
0 ignored issues
show
Coding Style Naming introduced by
Variable name "e" 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...
748
            if hasattr(pathlib, "_ignore_error") and not pathlib._ignore_error(e):
0 ignored issues
show
Bug introduced by
The Module pathlib does not seem to have a member named _ignore_error.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
Coding Style Best Practice introduced by
It seems like _ignore_error was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
749
                raise
750
        return None
751
752
753
__all__ = ["FilesysTools", "PathInfo"]
754