Passed
Push — main ( b62157...75b58f )
by Douglas
01:42
created

FilesysTools.dump_error()   B

Complexity

Conditions 6

Size

Total Lines 29
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 22
nop 3
dl 0
loc 29
rs 8.4186
c 0
b 0
f 0
1
import gzip
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import hashlib
3
import importlib.metadata
0 ignored issues
show
Bug introduced by
The name metadata does not seem to exist in module importlib.
Loading history...
introduced by
Unable to import 'importlib.metadata'
Loading history...
4
import locale
5
import logging
6
import os
7
import pathlib
8
import platform
9
import shutil
10
import socket
11
import stat
12
import struct
13
import sys
14
import tempfile
15
import traceback
16
from contextlib import contextmanager
17
from dataclasses import dataclass
18
from datetime import datetime, timezone, timedelta
19
from getpass import getuser
20
from pathlib import Path, PurePath
21
from typing import (
22
    Any,
23
    Generator,
24
    Iterable,
25
    Mapping,
26
    Optional,
27
    Sequence,
28
    SupportsBytes,
29
    Type,
30
    Union,
31
    Tuple,
32
    Callable,
33
)
34
35
import numpy as np
0 ignored issues
show
introduced by
Unable to import 'numpy'
Loading history...
36
import orjson
0 ignored issues
show
introduced by
Unable to import 'orjson'
Loading history...
37
import pandas as pd
0 ignored issues
show
introduced by
Unable to import 'pandas'
Loading history...
38
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
39
from defusedxml import ElementTree
0 ignored issues
show
introduced by
Unable to import 'defusedxml'
Loading history...
40
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...
41
42
from pocketutils.tools.unit_tools import UnitTools
43
44
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...
45
    AlreadyUsedError,
46
    ContradictoryRequestError,
47
    DirDoesNotExistError,
48
    FileDoesNotExistError,
49
    ParsingError,
50
)
51
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...
52
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...
53
from pocketutils.core.web_resource import *
0 ignored issues
show
Coding Style introduced by
The usage of wildcard imports like pocketutils.core.web_resource should generally be avoided.
Loading history...
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
Unused Code introduced by
zipfile was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
request was imported with wildcard, but is not used.
Loading history...
Unused Code introduced by
enum was imported with wildcard, but is not used.
Loading history...
54
from pocketutils.tools.base_tools import BaseTools
55
from pocketutils.tools.path_tools import PathTools
56
57
logger = logging.getLogger("pocketutils")
58
COMPRESS_LEVEL = 9
59
60
61
@dataclass(frozen=True, repr=True)
0 ignored issues
show
best-practice introduced by
Too many instance attributes (8/7)
Loading history...
62
class PathInfo:
63
    """
64
    Info about an extant or nonexistent path as it was at some time.
65
    Use this to avoid making repeated filesystem calls (e.g. ``.is_dir()``):
66
    None of the properties defined here make OS calls.
67
68
    Attributes:
69
        source: The original path used for lookup; may be a symlink
70
        resolved: The fully resolved path, or None if it does not exist
71
        as_of: A datetime immediately before the system calls (system timezone)
72
        real_stat: ``os.stat_result``, or None if the path does not exist
73
        link_stat: ``os.stat_result``, or None if the path is not a symlink
74
        has_access: Path exists and has the 'a' flag set
75
        has_read: Path exists and has the 'r' flag set
76
        has_write: Path exists and has the 'w' flag set
77
78
    All of the additional properties refer to the resolved path,
79
    except for :meth:`is_symlink`, :meth:`is_valid_symlink`,
80
    and :meth:`is_broken_symlink`.
81
    """
82
83
    source: Path
84
    resolved: Optional[Path]
85
    as_of: datetime
86
    real_stat: Optional[os.stat_result]
87
    link_stat: Optional[os.stat_result]
88
    has_access: bool
89
    has_read: bool
90
    has_write: bool
91
92
    @property
93
    def mod_or_create_dt(self) -> Optional[datetime]:
94
        """
95
        Returns the modification or access datetime.
96
        Uses whichever is available: creation on Windows and modification on Unix-like.
97
        """
98
        if os.name == "nt":
99
            return self._get_dt("st_ctime")
100
        # will work on posix; on java try anyway
101
        return self._get_dt("st_mtime")
102
103
    @property
104
    def mod_dt(self) -> Optional[datetime]:
105
        """
106
        Returns the modification datetime, if known.
107
        Returns None on Windows or if the path does not exist.
108
        """
109
        if os.name == "nt":
110
            return None
111
        return self._get_dt("st_mtime")
112
113
    @property
114
    def create_dt(self) -> Optional[datetime]:
115
        """
116
        Returns the creation datetime, if known.
117
        Returns None on Unix-like systems or if the path does not exist.
118
        """
119
        if os.name == "posix":
120
            return None
121
        return self._get_dt("st_ctime")
122
123
    @property
124
    def access_dt(self) -> Optional[datetime]:
125
        """
126
        Returns the access datetime.
127
        *Should* never return None if the path exists, but not guaranteed.
128
        """
129
        return self._get_dt("st_atime")
130
131
    @property
132
    def exists(self) -> bool:
133
        """
134
        Returns whether the resolved path exists.
135
        """
136
        return self.real_stat is not None
137
138
    @property
139
    def is_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
140
        return self.exists and stat.S_ISREG(self.real_stat.st_mode)
141
142
    @property
143
    def is_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
144
        return self.exists and stat.S_ISDIR(self.real_stat.st_mode)
145
146
    @property
147
    def is_readable_dir(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_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
152
        return self.is_dir and self.has_access and self.has_write
153
154
    @property
155
    def is_readable_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
156
        return self.is_file and self.has_access and self.has_read
157
158
    @property
159
    def is_writeable_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
160
        return self.is_file and self.has_access and self.has_write
161
162
    @property
163
    def is_block_device(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
164
        return self.exists and stat.S_ISBLK(self.real_stat.st_mode)
165
166
    @property
167
    def is_char_device(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
168
        return self.exists and stat.S_ISCHR(self.real_stat.st_mode)
169
170
    @property
171
    def is_socket(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
172
        return self.exists and stat.S_ISSOCK(self.real_stat.st_mode)
173
174
    @property
175
    def is_fifo(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
176
        return self.exists and stat.S_ISFIFO(self.real_stat.st_mode)
177
178
    @property
179
    def is_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
180
        return self.link_stat is not None
181
182
    @property
183
    def is_valid_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
184
        return self.is_symlink and self.exists
185
186
    @property
187
    def is_broken_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
188
        return self.is_symlink and not self.exists
189
190
    def _get_dt(self, attr: str) -> Optional[datetime]:
191
        if self.real_stat is None:
192
            return None
193
        sec = getattr(self.real_stat, attr)
194
        return datetime.fromtimestamp(sec).astimezone()
195
196
197
class FilesysTools(BaseTools):
0 ignored issues
show
best-practice introduced by
Too many public methods (29/20)
Loading history...
198
    """
199
    Tools for file/directory creation, etc.
200
201
    .. caution::
202
        Some functions may be insecure.
203
    """
204
205
    @classmethod
206
    def read_compressed_text(cls, path: PathLike) -> str:
207
        """
208
        Reads text from a text file, optionally gzipped or bz2-ed.
209
        Recognized suffixes for compression are ``.gz``, ``.gzip``, ``.bz2``, and ``.bzip2``.
210
        """
211
        return read_txt_or_gz(path)
212
213
    @classmethod
214
    def write_compressed_text(cls, txt: str, path: PathLike, *, mkdirs: bool = False) -> None:
215
        """
216
        Writes text to a text file, optionally gzipped or bz2-ed.
217
        Recognized suffixes for compression are ``.gz``, ``.gzip``, ``.bz2``, and ``.bzip2``.
218
        """
219
        write_txt_or_gz(txt, path, mkdirs=mkdirs)
220
221
    @classmethod
222
    def new_webresource(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
223
        cls, url: str, archive_member: Optional[str], local_path: PathLike
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
224
    ) -> WebResource:
225
        return WebResource(url, archive_member, local_path)
226
227
    @classmethod
228
    def is_linux(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
229
        return sys.platform == "linux"
230
231
    @classmethod
232
    def is_windows(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
233
        return sys.platform == "win32"
234
235
    @classmethod
236
    def is_macos(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
237
        return sys.platform == "darwin"
238
239
    @classmethod
240
    def get_info(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
241
        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...
242
    ) -> PathInfo:
243
        path = Path(path)
244
        has_ignore_error = hasattr(pathlib, "_ignore_error")
245
        if not has_ignore_error:
246
            logger.debug("No _ignore_error found; some OSErrors may be suppressed")
247
        resolved = None
248
        real_stat = None
249
        has_access = False
250
        has_read = False
251
        has_write = False
252
        link_stat = None
253
        as_of = datetime.now().astimezone()
254
        if has_ignore_error or path.is_symlink() or path.exists():
255
            link_stat = cls.__stat_raw(path)
256
        if link_stat is not None:
257
            if expand_user:
258
                resolved = path.expanduser().resolve(strict=strict)
259
            else:
260
                resolved = path.resolve(strict=strict)
261
            if stat.S_ISLNK(link_stat.st_mode):
262
                real_stat = cls.__stat_raw(resolved)
263
            else:
264
                real_stat = link_stat
265
            has_access = os.access(path, os.X_OK, follow_symlinks=True)
266
            has_read = os.access(path, os.R_OK, follow_symlinks=True)
267
            has_write = os.access(path, os.W_OK, follow_symlinks=True)
268
            if not stat.S_ISLNK(link_stat.st_mode):
269
                link_stat = None
270
        return PathInfo(
271
            source=path,
272
            resolved=resolved,
273
            as_of=as_of,
274
            real_stat=real_stat,
275
            link_stat=link_stat,
276
            has_access=has_access,
277
            has_read=has_read,
278
            has_write=has_write,
279
        )
280
281
    @classmethod
282
    def prep_dir(cls, path: PathLike, *, exist_ok: bool = True) -> bool:
283
        """
284
        Prepares a directory by making it if it doesn't exist.
285
        If exist_ok is False, calls logger.warning it already exists
286
        """
287
        path = Path(path)
288
        exists = path.exists()
289
        # On some platforms we get generic exceptions like permissions errors,
290
        # so these are better
291
        if exists and not path.is_dir():
292
            raise DirDoesNotExistError(f"Path {path} exists but is not a file")
293
        if exists and not exist_ok:
294
            logger.warning(f"Directory {path} already exists")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
295
        if not exists:
296
            # NOTE! exist_ok in mkdir throws an error on Windows
297
            path.mkdir(parents=True)
298
        return exists
299
300
    @classmethod
301
    def prep_file(cls, path: PathLike, *, exist_ok: bool = True) -> None:
302
        """
303
        Prepares a file path by making its parent directory.
304
        Same as ``pathlib.Path.mkdir`` but makes sure ``path`` is a file if it exists.
305
        """
306
        # On some platforms we get generic exceptions like permissions errors, so these are better
307
        path = Path(path)
308
        # check for errors first; don't make the dirs and then fail
309
        if path.exists() and not path.is_file() and not path.is_symlink():
310
            raise FileDoesNotExistError(f"Path {path} exists but is not a file")
311
        Path(path.parent).mkdir(parents=True, exist_ok=exist_ok)
312
313
    @classmethod
314
    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...
315
        cls, e: Optional[BaseException], to_path: Union[None, PathLike, datetime] = None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
316
    ) -> Path:
317
        """
318
        Writes a .json file containing the error message, stack trace, and sys info.
319
        System info is from :meth:`get_env_info`.
320
        """
321
        if to_path is None:
322
            now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
323
            to_path = Path(f"err-dump-{now}.json")
324
        elif isinstance(to_path, datetime):
325
            now = to_path.strftime("%Y-%m-%d_%H-%M-%S")
326
            to_path = Path(f"err-dump-{now}.json")
327
        else:
328
            to_path = Path(to_path)
329
        try:
330
            system = FilesysTools.get_env_info()
331
        except BaseException as e2:
0 ignored issues
show
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...
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...
332
            system = f"UNKNOWN << {e2} >>"
333
        m = "" if e is None else str(e)
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...
334
        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...
335
        if e is not None:
336
            tb = traceback.TracebackException.from_exception(e).stack.format()
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...
337
        info = orjson.dumps(
338
            dict(message=m, stacktrace=tb, system=system), option=orjson.OPT_INDENT_2
339
        )
340
        to_path.write_bytes(info)
341
        return to_path
342
343
    @classmethod
344
    def get_env_info(cls, *, include_insecure: bool = False) -> Mapping[str, str]:
345
        """
346
        Get a dictionary of some system and environment information.
347
        Includes os_release, hostname, username, mem + disk, shell, etc.
348
349
        Args:
350
            include_insecure: Include data like hostname and username
351
352
        .. caution ::
353
            Even with ``include_insecure=False``, avoid exposing this data to untrusted
354
            sources. For example, this includes the specific OS release, which could
355
            be used in attack.
356
        """
357
        try:
358
            import psutil
0 ignored issues
show
introduced by
Import outside toplevel (psutil)
Loading history...
359
        except ImportError:
360
            psutil = None
361
            logger.warning("psutil is not installed, so cannot get extended env info")
362
363
        now = datetime.now(timezone.utc).astimezone().isoformat()
364
        uname = platform.uname()
365
        language_code, encoding = locale.getlocale()
366
        # build up this dict:
367
        data = {}
368
369
        def _try(os_fn, k: str, *args):
370
            if any((a is None for a in args)):
371
                return None
372
            try:
373
                v = os_fn(*args)
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...
374
                data[k] = v
375
                return v
376
            except (OSError, ImportError):
377
                return None
378
379
        data.update(
380
            dict(
381
                platform=platform.platform(),
382
                python=".".join(str(i) for i in sys.version_info),
383
                os=uname.system,
384
                os_release=uname.release,
385
                os_version=uname.version,
386
                machine=uname.machine,
387
                byte_order=sys.byteorder,
388
                processor=uname.processor,
389
                build=sys.version,
390
                python_bits=8 * struct.calcsize("P"),
391
                environment_info_capture_datetime=now,
392
                encoding=encoding,
393
                lang_code=language_code,
394
                recursion_limit=sys.getrecursionlimit(),
395
                float_info=sys.float_info,
396
                int_info=sys.int_info,
397
                flags=sys.flags,
398
                hash_info=sys.hash_info,
399
                implementation=sys.implementation,
400
                switch_interval=sys.getswitchinterval(),
401
                filesystem_encoding=sys.getfilesystemencoding(),
402
            )
403
        )
404
        if "LANG" in os.environ:
405
            data["lang"] = os.environ["LANG"]
406
        if "SHELL" in os.environ:
407
            data["shell"] = os.environ["SHELL"]
408
        if "LC_ALL" in os.environ:
409
            data["lc_all"] = os.environ["LC_ALL"]
410
        if hasattr(sys, "winver"):
411
            data["win_ver"] = sys.getwindowsversion()
0 ignored issues
show
Bug introduced by
The Module sys does not seem to have a member named getwindowsversion.

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...
412
        if hasattr(sys, "mac_ver"):
413
            data["mac_ver"] = sys.mac_ver()
0 ignored issues
show
Bug introduced by
The Module sys does not seem to have a member named mac_ver.

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...
414
        if hasattr(sys, "linux_distribution"):
415
            data["linux_distribution"] = sys.linux_distribution()
0 ignored issues
show
Bug introduced by
The Module sys does not seem to have a member named linux_distribution.

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...
416
        if include_insecure:
417
            _try(getuser, "username")
418
            _try(os.getlogin, "login")
419
            _try(socket.gethostname, "hostname")
420
            _try(os.getcwd, "cwd")
421
            pid = _try(os.getpid, "pid")
422
            ppid = _try(os.getppid, "parent_pid")
423
            if hasattr(os, "getpriority"):
424
                _try(os.getpriority, "priority", os.PRIO_PROCESS, pid)
425
                _try(os.getpriority, "parent_priority", os.PRIO_PROCESS, ppid)
426
        if psutil is not None:
427
            data.update(
428
                dict(
429
                    disk_used=psutil.disk_usage(".").used,
430
                    disk_free=psutil.disk_usage(".").free,
431
                    memory_used=psutil.virtual_memory().used,
432
                    memory_available=psutil.virtual_memory().available,
433
                )
434
            )
435
        return {k: str(v) for k, v in dict(data).items()}
436
437
    @classmethod
438
    def list_package_versions(cls) -> Mapping[str, str]:
439
        """
440
        Returns installed packages and their version numbers.
441
        Reliable; uses importlib (Python 3.8+).
442
        """
443
        # calling .metadata reads the metadata file
444
        # and .version is an alias to .metadata["version"]
445
        # so make sure to only read once
446
        # TODO: get installed extras?
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
447
        dct = {}
448
        for d in importlib.metadata.distributions():
0 ignored issues
show
Coding Style Naming introduced by
Variable name "d" 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...
Bug introduced by
The Module importlib does not seem to have a member named metadata.

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...
449
            meta = d.metadata
450
            dct[meta["name"]] = meta["version"]
451
        return dct
452
453
    @classmethod
454
    def delete_surefire(cls, path: PathLike) -> Optional[Exception]:
455
        """
456
        Deletes files or directories cross-platform, but working around multiple issues in Windows.
457
458
        Returns:
459
            None, or an Exception for minor warnings
460
461
        Raises:
462
            IOError: If it can't delete
463
        """
464
        # we need this because of Windows
465
        path = Path(path)
466
        logger.debug(f"Permanently deleting {path} ...")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
467
        chmod_err = None
468
        try:
469
            os.chmod(str(path), stat.S_IRWXU)
470
        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...
471
            chmod_err = e
472
        # another reason for returning exception:
473
        # We don't want to interrupt the current line being printed like in slow_delete
474
        if path.is_dir():
475
            shutil.rmtree(str(path), ignore_errors=True)  # ignore_errors because of Windows
476
            try:
477
                path.unlink(missing_ok=True)  # again, because of Windows
478
            except IOError:
479
                pass  # almost definitely because it doesn't exist
480
        else:
481
            path.unlink(missing_ok=True)
482
        logger.debug(f"Permanently deleted {path}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
483
        return chmod_err
484
485
    @classmethod
486
    def trash(cls, path: PathLike, trash_dir: Optional[PathLike] = None) -> None:
487
        """
488
        Trash a file or directory.
489
490
        Args:
491
            path: The path to move to the trash
492
            trash_dir: If None, uses :meth:`pocketutils.tools.path_tools.PathTools.guess_trash`
493
        """
494
        if trash_dir is None:
495
            trash_dir = PathTools.guess_trash()
496
        logger.debug(f"Trashing {path} to {trash_dir} ...")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
497
        shutil.move(str(path), str(trash_dir))
498
        logger.debug(f"Trashed {path} to {trash_dir}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
499
500
    @classmethod
501
    def try_cleanup(cls, path: Path, *, bound: Type[Exception] = PermissionError) -> None:
502
        """
503
        Try to delete a file (probably temp file), if it exists, and log any PermissionError.
504
        """
505
        path = Path(path)
506
        # noinspection PyBroadException
507
        try:
508
            path.unlink(missing_ok=True)
509
        except bound:
510
            logger.error(f"Permission error preventing deleting {path}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
511
512
    @classmethod
513
    def read_lines_file(cls, path: PathLike, *, ignore_comments: bool = False) -> Sequence[str]:
514
        """
515
        Returns a list of lines in the file.
516
        Optionally skips lines starting with '#' or that only contain whitespace.
517
        """
518
        lines = []
519
        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...
520
            for line in f.readlines():
521
                line = line.strip()
522
                if not ignore_comments or not line.startswith("#") and not len(line.strip()) == 0:
523
                    lines.append(line)
524
        return lines
525
526
    @classmethod
527
    def read_properties_file(cls, path: PathLike) -> Mapping[str, str]:
528
        """
529
        Reads a .properties file.
530
        A list of lines with key=value pairs (with an equals sign).
531
        Lines beginning with # are ignored.
532
        Each line must contain exactly 1 equals sign.
533
534
        .. caution::
535
            The escaping is not compliant with the standard
536
537
        Args:
538
            path: Read the file at this local path
539
540
        Returns:
541
            A dict mapping keys to values, both with surrounding whitespace stripped
542
        """
543
        dct = {}
544
        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...
545
            for i, line in enumerate(f.readlines()):
546
                line = line.strip()
547
                if len(line) == 0 or line.startswith("#"):
548
                    continue
549
                if line.count("=") != 1:
550
                    raise ParsingError(f"Bad line {i} in {path}", resource=path)
551
                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...
552
                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...
553
                if k in dct:
554
                    raise AlreadyUsedError(f"Duplicate property {k} (line {i})", key=k)
555
                dct[k] = v
556
        return dct
557
558
    @classmethod
559
    def write_properties_file(
560
        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...
561
    ) -> None:
562
        """
563
        Writes a .properties file.
564
565
        .. caution::
566
            The escaping is not compliant with the standard
567
        """
568
        if not OpenMode(mode).write:
569
            raise ContradictoryRequestError(f"Cannot write text to {path} in mode {mode}")
570
        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...
571
            bads = []
572
            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...
573
                if "=" in k or "=" in v or "\n" in k or "\n" in v:
574
                    bads.append(k)
575
                f.write(
576
                    str(k).replace("=", "--").replace("\n", "\\n")
577
                    + "="
578
                    + str(v).replace("=", "--").replace("\n", "\\n")
579
                    + "\n"
580
                )
581
            if 0 < len(bads) <= 10:
582
                logger.warning(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
583
                    "At least one properties entry contains an equals sign or newline (\\n)."
584
                    f"These were escaped: {', '.join(bads)}"
585
                )
586
            elif len(bads) > 0:
587
                logger.warning(
588
                    "At least one properties entry contains an equals sign or newline (\\n),"
589
                    "which were escaped."
590
                )
591
592
    @classmethod
593
    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...
594
        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...
595
            f.write(orjson.dumps(data).decode(encoding="utf8"))
596
597
    @classmethod
598
    def load_json(cls, path: PathLike) -> Union[dict, list]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
599
        return orjson.loads(Path(path).read_text(encoding="utf8"))
600
601
    @classmethod
602
    def read_any(
0 ignored issues
show
best-practice introduced by
Too many return statements (10/6)
Loading history...
603
        cls, path: PathLike
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
604
    ) -> Union[
605
        str,
606
        bytes,
607
        Sequence[str],
608
        pd.DataFrame,
609
        Sequence[int],
610
        Sequence[float],
611
        Sequence[str],
612
        Mapping[str, str],
613
    ]:
614
        """
615
        Reads a variety of simple formats based on filename extension.
616
        Includes '.txt', 'csv', .xml', '.properties', '.json'.
617
        Also reads '.data' (binary), '.lines' (text lines).
618
        And formatted lists: '.strings', '.floats', and '.ints' (ex: "[1, 2, 3]").
619
        """
620
        path = Path(path)
621
        ext = path.suffix.lstrip(".")
622
623
        def load_list(dtype):
624
            return [
625
                dtype(s)
626
                for s in FilesysTools.read_lines_file(path)[0]
627
                .replace(" ", "")
628
                .replace("[", "")
629
                .replace("]", "")
630
                .split(",")
631
            ]
632
633
        if ext == "lines":
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
634
            return cls.read_lines_file(path)
635
        elif ext == "txt":
636
            return path.read_text(encoding="utf-8")
637
        elif ext == "data":
638
            return path.read_bytes()
639
        elif ext == "json":
640
            return cls.load_json(path)
641
        elif ext in ["npy", "npz"]:
642
            return np.load(str(path), allow_pickle=False, encoding="utf8")
643
        elif ext == "properties":
644
            return cls.read_properties_file(path)
645
        elif ext == "csv":
646
            return pd.read_csv(path, encoding="utf8")
647
        elif ext == "ints":
648
            return load_list(int)
649
        elif ext == "floats":
650
            return load_list(float)
651
        elif ext == "strings":
652
            return load_list(str)
653
        elif ext == "xml":
654
            ElementTree.parse(path).getroot()
655
        else:
656
            raise TypeError(f"Did not recognize resource file type for file {path}")
657
658
    @classmethod
659
    @contextmanager
660
    def open_file(cls, path: PathLike, mode: Union[OpenMode, str], *, mkdir: bool = False):
661
        """
662
        Opens a text file, always using utf8, optionally gzipped.
663
664
        See Also:
665
            :class:`pocketutils.core.input_output.OpenMode`
666
        """
667
        path = Path(path)
668
        mode = OpenMode(mode)
669
        if mode.write and mkdir:
670
            path.parent.mkdir(exist_ok=True, parents=True)
671
        if not mode.read:
672
            cls.prep_file(path, exist_ok=mode.overwrite or mode.append)
673
        if mode.gzipped:
674
            yield gzip.open(path, mode.internal, compresslevel=COMPRESS_LEVEL, encoding="utf8")
675
        elif mode.binary:
676
            yield open(path, mode.internal, encoding="utf8")
677
        else:
678
            yield open(path, mode.internal, encoding="utf8")
679
680
    @classmethod
681
    def write_lines(cls, iterable: Iterable[Any], path: PathLike, mode: str = "w") -> int:
682
        """
683
        Just writes an iterable line-by-line to a file, using '\n'.
684
        Makes the parent directory if needed.
685
        Checks that the iterable is a "true iterable" (not a string or bytes).
686
687
        Returns:
688
            The number of lines written (the same as len(iterable) if iterable has a length)
689
690
        Raises:
691
            FileExistsError: If the path exists and append is False
692
            PathIsNotFileError: If append is True, and the path exists but is not a file
693
        """
694
        if not cls.is_true_iterable(iterable):
695
            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...
696
        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...
697
        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...
698
            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...
699
                f.write(str(x) + "\n")
700
            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...
701
        return n
702
703
    @classmethod
704
    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...
705
        """
706
        Returns the hex-encoded hash of the object (converted to bytes).
707
        """
708
        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...
709
        m.update(bytes(x))
710
        return m.hexdigest()
711
712
    @classmethod
713
    def replace_in_file(cls, path: PathLike, changes: Mapping[str, str]) -> None:
714
        """
715
        Uses re.sub repeatedly to modify (AND REPLACE) a file's content.
716
        """
717
        path = Path(path)
718
        data = path.read_text(encoding="utf-8")
719
        for key, value in changes.items():
720
            data = regex.sub(key, value, data, flags=regex.V1 | regex.MULTILINE | regex.DOTALL)
721
        path.write_text(data, encoding="utf-8")
722
723
    @classmethod
724
    def tmppath(cls, path: Optional[PathLike] = None, **kwargs) -> Generator[Path, None, None]:
725
        """
726
        Makes a temporary Path. Won't create ``path`` but will delete it at the end.
727
        If ``path`` is None, will use ``tempfile.mkstemp``.
728
        """
729
        if path is None:
730
            _, path = tempfile.mkstemp()
731
        try:
732
            yield Path(path, **kwargs)
733
        finally:
734
            Path(path).unlink()
735
736
    @classmethod
737
    def tmpfile(
738
        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...
739
    ) -> Generator[Writeable, None, None]:
740
        """
741
        Simple wrapper around tempfile functions.
742
        Wraps ``TemporaryFile``, ``NamedTemporaryFile``, and ``SpooledTemporaryFile``.
743
        """
744
        if spooled:
745
            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...
746
                yield x
747
        elif path is None:
748
            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...
749
                yield x
750
        else:
751
            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...
752
                yield x
753
754
    @classmethod
755
    def tmpdir(cls, **kwargs) -> Generator[Path, None, None]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
756
        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...
757
            yield Path(x)
758
759
    @classmethod
760
    def check_expired(
761
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
762
        path: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
763
        max_sec: Union[timedelta, float],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
764
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
765
        parent: Optional[PathLike] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
766
        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...
767
        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...
768
        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...
769
    ) -> Optional[bool]:
770
        """
771
        Warns and returns True if ``path`` mod date is more than ``max_sec`` in the past.
772
        Returns None if it could not be determined.
773
774
        The formatting strings can refer to any of these (will be empty if unknown):
775
        - path: Full path
776
        - name: File/dir name
777
        - path_rel: Path relative to ``self._dir``, or full path if not under
778
        - now: formatted current datetime
779
        - [mod/create]_dt: Formatted mod/creation datetime
780
        - [mod/create]_rel: Mod/create datetime in terms of offset from now
781
        - [mod/create]_delta: Formatted timedelta from now
782
        - [mod/create]_delta_sec: Number of seconds from now (negative if now < mod/create dt)
783
784
        Args:
785
            path: A specific path to check
786
            max_sec: Max seconds, or a timedelta
787
            parent: If provided, path_rel is relative to this directory (to simplify warnings)
788
            warn_expired_fmt: Formatting string in terms of the variables listed above
789
            warn_unknown_fmt: Formatting string in terms of the variables listed above
790
            log: Log about problems
791
792
        Returns:
793
            Whether it is expired, or None if it could not be determined
794
        """
795
        path = Path(path)
796
        if log is None:
797
            log = lambda _: None
798
        limit = max_sec if isinstance(max_sec, timedelta) else timedelta(seconds=max_sec)
799
        now = datetime.now().astimezone()
800
        info = FilesysTools.get_info(path)
801
        if info.mod_dt and now - info.mod_dt > limit:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
802
            cls._warn_expired(now, info.mod_dt, info.create_dt, path, parent, warn_expired_fmt, log)
803
            return True
804
        elif not info.mod_dt and (not info.create_dt or (now - info.create_dt) > limit):
805
            cls._warn_expired(now, info.mod_dt, info.create_dt, path, parent, warn_unknown_fmt, log)
806
            return None
807
        return False
808
809
    @classmethod
810
    def _warn_expired(
0 ignored issues
show
Comprehensibility introduced by
This function exceeds the maximum number of variables (20/15).
Loading history...
best-practice introduced by
Too many arguments (8/5)
Loading history...
811
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
812
        now: datetime,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
813
        mod: Optional[datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
814
        created: Optional[datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
815
        path: Path,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
816
        parent: Optional[Path],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
817
        fmt: Optional[str],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
818
        log: Callable[[str], Any],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
819
    ):
820
        if isinstance(fmt, str):
821
            fmt = fmt.format
822
        if parent is not None and path.is_relative_to(parent):
823
            path_rel = str(path.relative_to(parent))
824
        else:
825
            path_rel = str(path)
826
        now_str, mod_str, mod_rel, mod_delta, mod_delta_sec = cls._expire_warning_info(now, mod)
827
        _, create_str, create_rel, create_delta, create_delta_sec = cls._expire_warning_info(
828
            now, created
829
        )
830
        msg = fmt(
831
            path=path,
832
            path_rel=path_rel,
833
            name=path.name,
834
            now=now_str,
835
            mod_dt=mod_str,
836
            mod_rel=mod_rel,
837
            mod_delta=mod_delta,
838
            mod_sec=mod_delta_sec,
839
            create_dt=create_str,
840
            create_rel=create_rel,
841
            create_delta=create_delta,
842
            create_sec=create_delta_sec,
843
        )
844
        log(msg)
845
846
    @classmethod
847
    def _expire_warning_info(
848
        cls, now: datetime, then: Optional[datetime]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
849
    ) -> Tuple[str, str, str, str, str]:
850
        now_str = now.strftime("%Y-%m-%d %H:%M:%S")
851
        if then is None:
852
            return now_str, "", "", "", ""
853
        delta = now - then
854
        then_str = then.strftime("%Y-%m-%d %H:%M:%S")
855
        then_rel = UnitTools.approx_time_wrt(now, then)
856
        delta_str = UnitTools.delta_time_to_str(delta, space=Chars.narrownbsp)
857
        return now_str, then_str, then_rel, delta_str, str(delta.total_seconds())
858
859
    @classmethod
860
    def __stat_raw(cls, path: Path) -> Optional[os.stat_result]:
861
        try:
862
            return path.lstat()
863
        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...
864
            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...
865
                raise
866
        return None
867
868
869
__all__ = ["FilesysTools", "PathInfo"]
870