Passed
Push — main ( 520e83...b06663 )
by Douglas
01:43
created

FilesysTools.check_expired()   C

Complexity

Conditions 9

Size

Total Lines 49
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 23
nop 8
dl 0
loc 49
rs 6.6666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
from contextlib import contextmanager
16
from dataclasses import dataclass
17
from datetime import datetime, timezone, timedelta
18
from getpass import getuser
19
from pathlib import Path, PurePath
20
from typing import (
21
    Any,
22
    Generator,
23
    Iterable,
24
    Mapping,
25
    Optional,
26
    Sequence,
27
    SupportsBytes,
28
    Type,
29
    Union,
30
    Tuple,
31
    Callable,
32
)
33
34
import numpy as np
0 ignored issues
show
introduced by
Unable to import 'numpy'
Loading history...
35
import orjson
0 ignored issues
show
introduced by
Unable to import 'orjson'
Loading history...
36
import pandas as pd
0 ignored issues
show
introduced by
Unable to import 'pandas'
Loading history...
37
import regex
0 ignored issues
show
introduced by
Unable to import 'regex'
Loading history...
38
from defusedxml import ElementTree
0 ignored issues
show
introduced by
Unable to import 'defusedxml'
Loading history...
39
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...
40
41
from pocketutils.tools.unit_tools import UnitTools
42
43
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...
44
    AlreadyUsedError,
45
    ContradictoryRequestError,
46
    DirDoesNotExistError,
47
    FileDoesNotExistError,
48
    ParsingError,
49
)
50
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...
51
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...
52
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...
Unused Code introduced by
enum was imported with wildcard, but is not used.
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...
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
53
from pocketutils.tools.base_tools import BaseTools
54
from pocketutils.tools.path_tools import PathTools
55
56
logger = logging.getLogger("pocketutils")
57
COMPRESS_LEVEL = 9
58
59
60
@dataclass(frozen=True, repr=True)
0 ignored issues
show
best-practice introduced by
Too many instance attributes (8/7)
Loading history...
61
class PathInfo:
62
    """
63
    Info about an extant or nonexistent path as it was at some time.
64
    Use this to avoid making repeated filesystem calls (e.g. ``.is_dir()``):
65
    None of the properties defined here make OS calls.
66
67
    Attributes:
68
        source: The original path used for lookup; may be a symlink
69
        resolved: The fully resolved path, or None if it does not exist
70
        as_of: A datetime immediately before the system calls (system timezone)
71
        real_stat: ``os.stat_result``, or None if the path does not exist
72
        link_stat: ``os.stat_result``, or None if the path is not a symlink
73
        has_access: Path exists and has the 'a' flag set
74
        has_read: Path exists and has the 'r' flag set
75
        has_write: Path exists and has the 'w' flag set
76
77
    All of the additional properties refer to the resolved path,
78
    except for :meth:`is_symlink`, :meth:`is_valid_symlink`,
79
    and :meth:`is_broken_symlink`.
80
    """
81
82
    source: Path
83
    resolved: Optional[Path]
84
    as_of: datetime
85
    real_stat: Optional[os.stat_result]
86
    link_stat: Optional[os.stat_result]
87
    has_access: bool
88
    has_read: bool
89
    has_write: bool
90
91
    @property
92
    def mod_or_create_dt(self) -> Optional[datetime]:
93
        """
94
        Returns the modification or access datetime.
95
        Uses whichever is available: creation on Windows and modification on Unix-like.
96
        """
97
        if os.name == "nt":
98
            return self._get_dt("st_ctime")
99
        # will work on posix; on java try anyway
100
        return self._get_dt("st_mtime")
101
102
    @property
103
    def mod_dt(self) -> Optional[datetime]:
104
        """
105
        Returns the modification datetime, if known.
106
        Returns None on Windows or if the path does not exist.
107
        """
108
        if os.name == "nt":
109
            return None
110
        return self._get_dt("st_mtime")
111
112
    @property
113
    def create_dt(self) -> Optional[datetime]:
114
        """
115
        Returns the creation datetime, if known.
116
        Returns None on Unix-like systems or if the path does not exist.
117
        """
118
        if os.name == "posix":
119
            return None
120
        return self._get_dt("st_ctime")
121
122
    @property
123
    def access_dt(self) -> Optional[datetime]:
124
        """
125
        Returns the access datetime.
126
        *Should* never return None if the path exists, but not guaranteed.
127
        """
128
        return self._get_dt("st_atime")
129
130
    @property
131
    def exists(self) -> bool:
132
        """
133
        Returns whether the resolved path exists.
134
        """
135
        return self.real_stat is not None
136
137
    @property
138
    def is_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
139
        return self.exists and stat.S_ISREG(self.real_stat.st_mode)
140
141
    @property
142
    def is_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
143
        return self.exists and stat.S_ISDIR(self.real_stat.st_mode)
144
145
    @property
146
    def is_readable_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
147
        return self.is_file and self.has_access and self.has_read
148
149
    @property
150
    def is_writeable_dir(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
151
        return self.is_dir and self.has_access and self.has_write
152
153
    @property
154
    def is_readable_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
155
        return self.is_file and self.has_access and self.has_read
156
157
    @property
158
    def is_writeable_file(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
159
        return self.is_file and self.has_access and self.has_write
160
161
    @property
162
    def is_block_device(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
163
        return self.exists and stat.S_ISBLK(self.real_stat.st_mode)
164
165
    @property
166
    def is_char_device(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
167
        return self.exists and stat.S_ISCHR(self.real_stat.st_mode)
168
169
    @property
170
    def is_socket(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
171
        return self.exists and stat.S_ISSOCK(self.real_stat.st_mode)
172
173
    @property
174
    def is_fifo(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
175
        return self.exists and stat.S_ISFIFO(self.real_stat.st_mode)
176
177
    @property
178
    def is_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
179
        return self.link_stat is not None
180
181
    @property
182
    def is_valid_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
183
        return self.is_symlink and self.exists
184
185
    @property
186
    def is_broken_symlink(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
187
        return self.is_symlink and not self.exists
188
189
    def _get_dt(self, attr: str) -> Optional[datetime]:
190
        if self.real_stat is None:
191
            return None
192
        sec = getattr(self.real_stat, attr)
193
        return datetime.fromtimestamp(sec).astimezone()
194
195
196
class FilesysTools(BaseTools):
0 ignored issues
show
best-practice introduced by
Too many public methods (28/20)
Loading history...
197
    """
198
    Tools for file/directory creation, etc.
199
200
    .. caution::
201
        Some functions may be insecure.
202
    """
203
204
    @classmethod
205
    def read_compressed_text(cls, path: PathLike) -> str:
206
        """
207
        Reads text from a text file, optionally gzipped or bz2-ed.
208
        Recognized suffixes for compression are ``.gz``, ``.gzip``, ``.bz2``, and ``.bzip2``.
209
        """
210
        return read_txt_or_gz(path)
211
212
    @classmethod
213
    def write_compressed_text(cls, txt: str, path: PathLike, *, mkdirs: bool = False) -> None:
214
        """
215
        Writes text to a text file, optionally gzipped or bz2-ed.
216
        Recognized suffixes for compression are ``.gz``, ``.gzip``, ``.bz2``, and ``.bzip2``.
217
        """
218
        write_txt_or_gz(txt, path, mkdirs=mkdirs)
219
220
    @classmethod
221
    def new_webresource(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
222
        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...
223
    ) -> WebResource:
224
        return WebResource(url, archive_member, local_path)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable WebResource does not seem to be defined.
Loading history...
225
226
    @classmethod
227
    def is_linux(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
228
        return sys.platform == "linux"
229
230
    @classmethod
231
    def is_windows(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
232
        return sys.platform == "win32"
233
234
    @classmethod
235
    def is_macos(cls) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
236
        return sys.platform == "darwin"
237
238
    @classmethod
239
    def get_info(
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
240
        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...
241
    ) -> PathInfo:
242
        path = Path(path)
243
        has_ignore_error = hasattr(pathlib, "_ignore_error")
244
        if not has_ignore_error:
245
            logger.debug("No _ignore_error found; some OSErrors may be suppressed")
246
        resolved = None
247
        real_stat = None
248
        has_access = False
249
        has_read = False
250
        has_write = False
251
        link_stat = None
252
        as_of = datetime.now().astimezone()
253
        if has_ignore_error or path.is_symlink() or path.exists():
254
            link_stat = cls.__stat_raw(path)
255
        if link_stat is not None:
256
            if expand_user:
257
                resolved = path.expanduser().resolve(strict=strict)
258
            else:
259
                resolved = path.resolve(strict=strict)
260
            if stat.S_ISLNK(link_stat.st_mode):
261
                real_stat = cls.__stat_raw(resolved)
262
            else:
263
                real_stat = link_stat
264
            has_access = os.access(path, os.X_OK, follow_symlinks=True)
265
            has_read = os.access(path, os.R_OK, follow_symlinks=True)
266
            has_write = os.access(path, os.W_OK, follow_symlinks=True)
267
            if not stat.S_ISLNK(link_stat.st_mode):
268
                link_stat = None
269
        return PathInfo(
270
            source=path,
271
            resolved=resolved,
272
            as_of=as_of,
273
            real_stat=real_stat,
274
            link_stat=link_stat,
275
            has_access=has_access,
276
            has_read=has_read,
277
            has_write=has_write,
278
        )
279
280
    @classmethod
281
    def prep_dir(cls, path: PathLike, *, exist_ok: bool = True) -> bool:
282
        """
283
        Prepares a directory by making it if it doesn't exist.
284
        If exist_ok is False, calls logger.warning it already exists
285
        """
286
        path = Path(path)
287
        exists = path.exists()
288
        # On some platforms we get generic exceptions like permissions errors,
289
        # so these are better
290
        if exists and not path.is_dir():
291
            raise DirDoesNotExistError(f"Path {path} exists but is not a file")
292
        if exists and not exist_ok:
293
            logger.warning(f"Directory {path} already exists")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
294
        if not exists:
295
            # NOTE! exist_ok in mkdir throws an error on Windows
296
            path.mkdir(parents=True)
297
        return exists
298
299
    @classmethod
300
    def prep_file(cls, path: PathLike, *, exist_ok: bool = True) -> None:
301
        """
302
        Prepares a file path by making its parent directory.
303
        Same as ``pathlib.Path.mkdir`` but makes sure ``path`` is a file if it exists.
304
        """
305
        # On some platforms we get generic exceptions like permissions errors, so these are better
306
        path = Path(path)
307
        # check for errors first; don't make the dirs and then fail
308
        if path.exists() and not path.is_file() and not path.is_symlink():
309
            raise FileDoesNotExistError(f"Path {path} exists but is not a file")
310
        Path(path.parent).mkdir(parents=True, exist_ok=exist_ok)
311
312
    @classmethod
313
    def get_env_info(cls, *, include_insecure: bool = False) -> Mapping[str, str]:
314
        """
315
        Get a dictionary of some system and environment information.
316
        Includes os_release, hostname, username, mem + disk, shell, etc.
317
318
        Args:
319
            include_insecure: Include data like hostname and username
320
321
        .. caution ::
322
            Even with ``include_insecure=False``, avoid exposing this data to untrusted
323
            sources. For example, this includes the specific OS release, which could
324
            be used in attack.
325
        """
326
        try:
327
            import psutil
0 ignored issues
show
introduced by
Import outside toplevel (psutil)
Loading history...
328
        except ImportError:
329
            psutil = None
330
            logger.warning("psutil is not installed, so cannot get extended env info")
331
332
        now = datetime.now(timezone.utc).astimezone().isoformat()
333
        uname = platform.uname()
334
        language_code, encoding = locale.getlocale()
0 ignored issues
show
Unused Code introduced by
The variable language_code seems to be unused.
Loading history...
335
        # build up this dict:
336
        data = {}
337
338
        def _try(os_fn, k: str, *args):
339
            if any((a is None for a in args)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable a does not seem to be defined.
Loading history...
340
                return None
341
            try:
342
                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...
343
                data[k] = v
344
                return v
345
            except (OSError, ImportError):
346
                return None
347
348
        data.update(
349
            dict(
350
                platform=platform.platform(),
351
                python=".".join(str(i) for i in sys.version_info),
352
                os=uname.system,
353
                os_release=uname.release,
354
                os_version=uname.version,
355
                machine=uname.machine,
356
                byte_order=sys.byteorder,
357
                processor=uname.processor,
358
                build=sys.version,
359
                python_bits=8 * struct.calcsize("P"),
360
                environment_info_capture_datetime=now,
361
                encoding=encoding,
362
                locale=locale,
363
                recursion_limit=sys.getrecursionlimit(),
364
                float_info=sys.float_info,
365
                int_info=sys.int_info,
366
                flags=sys.flags,
367
                hash_info=sys.hash_info,
368
                implementation=sys.implementation,
369
                switch_interval=sys.getswitchinterval(),
370
                filesystem_encoding=sys.getfilesystemencoding(),
371
            )
372
        )
373
        if "LANG" in os.environ:
374
            data["lang"] = os.environ["LANG"]
375
        if "SHELL" in os.environ:
376
            data["shell"] = os.environ["SHELL"]
377
        if "LC_ALL" in os.environ:
378
            data["lc_all"] = os.environ["LC_ALL"]
379
        if hasattr(sys, "winver"):
380
            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...
381
        if hasattr(sys, "mac_ver"):
382
            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...
383
        if hasattr(sys, "linux_distribution"):
384
            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...
385
        if include_insecure:
386
            _try(getuser, "username")
387
            _try(os.getlogin, "login")
388
            _try(socket.gethostname, "hostname")
389
            _try(os.getcwd, "cwd")
390
            pid = _try(os.getpid, "pid")
391
            ppid = _try(os.getppid, "parent_pid")
392
            if hasattr(os, "getpriority"):
393
                _try(os.getpriority, "priority", os.PRIO_PROCESS, pid)
394
                _try(os.getpriority, "parent_priority", os.PRIO_PROCESS, ppid)
395
        if psutil is not None:
396
            data.update(
397
                dict(
398
                    disk_used=psutil.disk_usage(".").used,
399
                    disk_free=psutil.disk_usage(".").free,
400
                    memory_used=psutil.virtual_memory().used,
401
                    memory_available=psutil.virtual_memory().available,
402
                )
403
            )
404
        return {k: str(v) for k, v in dict(data).items()}
405
406
    @classmethod
407
    def list_package_versions(cls) -> Mapping[str, str]:
408
        """
409
        Returns installed packages and their version numbers.
410
        Reliable; uses importlib (Python 3.8+).
411
        """
412
        # calling .metadata reads the metadata file
413
        # and .version is an alias to .metadata["version"]
414
        # so make sure to only read once
415
        # TODO: get installed extras?
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
416
        dct = {}
417
        for d in importlib.metadata.distributions():
0 ignored issues
show
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...
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...
418
            meta = d.metadata
419
            dct[meta["name"]] = meta["version"]
420
        return dct
421
422
    @classmethod
423
    def delete_surefire(cls, path: PathLike) -> Optional[Exception]:
424
        """
425
        Deletes files or directories cross-platform, but working around multiple issues in Windows.
426
427
        Returns:
428
            None, or an Exception for minor warnings
429
430
        Raises:
431
            IOError: If it can't delete
432
        """
433
        # we need this because of Windows
434
        path = Path(path)
435
        logger.debug(f"Permanently deleting {path} ...")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
436
        chmod_err = None
437
        try:
438
            os.chmod(str(path), stat.S_IRWXU)
439
        except Exception 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...
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...
440
            chmod_err = e
441
        # another reason for returning exception:
442
        # We don't want to interrupt the current line being printed like in slow_delete
443
        if path.is_dir():
444
            shutil.rmtree(str(path), ignore_errors=True)  # ignore_errors because of Windows
445
            try:
446
                path.unlink(missing_ok=True)  # again, because of Windows
447
            except IOError:
448
                pass  # almost definitely because it doesn't exist
449
        else:
450
            path.unlink(missing_ok=True)
451
        logger.debug(f"Permanently deleted {path}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
452
        return chmod_err
453
454
    @classmethod
455
    def trash(cls, path: PathLike, trash_dir: Optional[PathLike] = None) -> None:
456
        """
457
        Trash a file or directory.
458
459
        Args:
460
            path: The path to move to the trash
461
            trash_dir: If None, uses :meth:`pocketutils.tools.path_tools.PathTools.guess_trash`
462
        """
463
        if trash_dir is None:
464
            trash_dir = PathTools.guess_trash()
465
        logger.debug(f"Trashing {path} to {trash_dir} ...")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
466
        shutil.move(str(path), str(trash_dir))
467
        logger.debug(f"Trashed {path} to {trash_dir}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
468
469
    @classmethod
470
    def try_cleanup(cls, path: Path, *, bound: Type[Exception] = PermissionError) -> None:
471
        """
472
        Try to delete a file (probably temp file), if it exists, and log any PermissionError.
473
        """
474
        path = Path(path)
475
        # noinspection PyBroadException
476
        try:
477
            path.unlink(missing_ok=True)
478
        except bound:
479
            logger.error(f"Permission error preventing deleting {path}")
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
480
481
    @classmethod
482
    def read_lines_file(cls, path: PathLike, *, ignore_comments: bool = False) -> Sequence[str]:
483
        """
484
        Returns a list of lines in the file.
485
        Optionally skips lines starting with '#' or that only contain whitespace.
486
        """
487
        lines = []
488
        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...
489
            for line in f.readlines():
490
                line = line.strip()
491
                if not ignore_comments or not line.startswith("#") and not len(line.strip()) == 0:
492
                    lines.append(line)
493
        return lines
494
495
    @classmethod
496
    def read_properties_file(cls, path: PathLike) -> Mapping[str, str]:
497
        """
498
        Reads a .properties file.
499
        A list of lines with key=value pairs (with an equals sign).
500
        Lines beginning with # are ignored.
501
        Each line must contain exactly 1 equals sign.
502
503
        .. caution::
504
            The escaping is not compliant with the standard
505
506
        Args:
507
            path: Read the file at this local path
508
509
        Returns:
510
            A dict mapping keys to values, both with surrounding whitespace stripped
511
        """
512
        dct = {}
513
        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...
514
            for i, line in enumerate(f.readlines()):
515
                line = line.strip()
516
                if len(line) == 0 or line.startswith("#"):
517
                    continue
518
                if line.count("=") != 1:
519
                    raise ParsingError(f"Bad line {i} in {path}", resource=path)
520
                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...
521
                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...
522
                if k in dct:
523
                    raise AlreadyUsedError(f"Duplicate property {k} (line {i})", key=k)
524
                dct[k] = v
525
        return dct
526
527
    @classmethod
528
    def write_properties_file(
529
        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...
530
    ) -> None:
531
        """
532
        Writes a .properties file.
533
534
        .. caution::
535
            The escaping is not compliant with the standard
536
        """
537
        if not OpenMode(mode).write:
538
            raise ContradictoryRequestError(f"Cannot write text to {path} in mode {mode}")
539
        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...
540
            bads = []
541
            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...
542
                if "=" in k or "=" in v or "\n" in k or "\n" in v:
543
                    bads.append(k)
544
                f.write(
545
                    str(k).replace("=", "--").replace("\n", "\\n")
546
                    + "="
547
                    + str(v).replace("=", "--").replace("\n", "\\n")
548
                    + "\n"
549
                )
550
            if 0 < len(bads) <= 10:
551
                logger.warning(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
552
                    "At least one properties entry contains an equals sign or newline (\\n)."
553
                    f"These were escaped: {', '.join(bads)}"
554
                )
555
            elif len(bads) > 0:
556
                logger.warning(
557
                    "At least one properties entry contains an equals sign or newline (\\n),"
558
                    "which were escaped."
559
                )
560
561
    @classmethod
562
    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...
563
        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...
564
            f.write(orjson.dumps(data).decode(encoding="utf8"))
565
566
    @classmethod
567
    def load_json(cls, path: PathLike) -> Union[dict, list]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
568
        return orjson.loads(Path(path).read_text(encoding="utf8"))
569
570
    @classmethod
571
    def read_any(
0 ignored issues
show
best-practice introduced by
Too many return statements (10/6)
Loading history...
572
        cls, path: PathLike
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
573
    ) -> Union[
574
        str,
575
        bytes,
576
        Sequence[str],
577
        pd.DataFrame,
578
        Sequence[int],
579
        Sequence[float],
580
        Sequence[str],
581
        Mapping[str, str],
582
    ]:
583
        """
584
        Reads a variety of simple formats based on filename extension.
585
        Includes '.txt', 'csv', .xml', '.properties', '.json'.
586
        Also reads '.data' (binary), '.lines' (text lines).
587
        And formatted lists: '.strings', '.floats', and '.ints' (ex: "[1, 2, 3]").
588
        """
589
        path = Path(path)
590
        ext = path.suffix.lstrip(".")
591
592
        def load_list(dtype):
593
            return [
594
                dtype(s)
595
                for s in FilesysTools.read_lines_file(path)[0]
596
                .replace(" ", "")
597
                .replace("[", "")
598
                .replace("]", "")
599
                .split(",")
600
            ]
601
602
        if ext == "lines":
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
603
            return cls.read_lines_file(path)
604
        elif ext == "txt":
605
            return path.read_text(encoding="utf-8")
606
        elif ext == "data":
607
            return path.read_bytes()
608
        elif ext == "json":
609
            return cls.load_json(path)
610
        elif ext in ["npy", "npz"]:
611
            return np.load(str(path), allow_pickle=False, encoding="utf8")
612
        elif ext == "properties":
613
            return cls.read_properties_file(path)
614
        elif ext == "csv":
615
            return pd.read_csv(path, encoding="utf8")
616
        elif ext == "ints":
617
            return load_list(int)
618
        elif ext == "floats":
619
            return load_list(float)
620
        elif ext == "strings":
621
            return load_list(str)
622
        elif ext == "xml":
623
            ElementTree.parse(path).getroot()
624
        else:
625
            raise TypeError(f"Did not recognize resource file type for file {path}")
626
627
    @classmethod
628
    @contextmanager
629
    def open_file(cls, path: PathLike, mode: Union[OpenMode, str], *, mkdir: bool = False):
630
        """
631
        Opens a text file, always using utf8, optionally gzipped.
632
633
        See Also:
634
            :class:`pocketutils.core.input_output.OpenMode`
635
        """
636
        path = Path(path)
637
        mode = OpenMode(mode)
638
        if mode.write and mkdir:
639
            path.parent.mkdir(exist_ok=True, parents=True)
640
        if not mode.read:
641
            cls.prep_file(path, exist_ok=mode.overwrite or mode.append)
642
        if mode.gzipped:
643
            yield gzip.open(path, mode.internal, compresslevel=COMPRESS_LEVEL, encoding="utf8")
644
        elif mode.binary:
645
            yield open(path, mode.internal, encoding="utf8")
646
        else:
647
            yield open(path, mode.internal, encoding="utf8")
648
649
    @classmethod
650
    def write_lines(cls, iterable: Iterable[Any], path: PathLike, mode: str = "w") -> int:
651
        """
652
        Just writes an iterable line-by-line to a file, using '\n'.
653
        Makes the parent directory if needed.
654
        Checks that the iterable is a "true iterable" (not a string or bytes).
655
656
        Returns:
657
            The number of lines written (the same as len(iterable) if iterable has a length)
658
659
        Raises:
660
            FileExistsError: If the path exists and append is False
661
            PathIsNotFileError: If append is True, and the path exists but is not a file
662
        """
663
        if not cls.is_true_iterable(iterable):
664
            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...
665
        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...
666
        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...
667
            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...
668
                f.write(str(x) + "\n")
669
            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...
670
        return n
671
672
    @classmethod
673
    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...
674
        """
675
        Returns the hex-encoded hash of the object (converted to bytes).
676
        """
677
        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...
678
        m.update(bytes(x))
679
        return m.hexdigest()
680
681
    @classmethod
682
    def replace_in_file(cls, path: PathLike, changes: Mapping[str, str]) -> None:
683
        """
684
        Uses re.sub repeatedly to modify (AND REPLACE) a file's content.
685
        """
686
        path = Path(path)
687
        data = path.read_text(encoding="utf-8")
688
        for key, value in changes.items():
689
            data = regex.sub(key, value, data, flags=regex.V1 | regex.MULTILINE | regex.DOTALL)
690
        path.write_text(data, encoding="utf-8")
691
692
    @classmethod
693
    def tmppath(cls, path: Optional[PathLike] = None, **kwargs) -> Generator[Path, None, None]:
694
        """
695
        Makes a temporary Path. Won't create ``path`` but will delete it at the end.
696
        If ``path`` is None, will use ``tempfile.mkstemp``.
697
        """
698
        if path is None:
699
            _, path = tempfile.mkstemp()
700
        try:
701
            yield Path(path, **kwargs)
702
        finally:
703
            Path(path).unlink()
704
705
    @classmethod
706
    def tmpfile(
707
        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...
708
    ) -> Generator[Writeable, None, None]:
709
        """
710
        Simple wrapper around tempfile functions.
711
        Wraps ``TemporaryFile``, ``NamedTemporaryFile``, and ``SpooledTemporaryFile``.
712
        """
713
        if spooled:
714
            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...
715
                yield x
716
        elif path is None:
717
            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...
718
                yield x
719
        else:
720
            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...
721
                yield x
722
723
    @classmethod
724
    def tmpdir(cls, **kwargs) -> Generator[Path, None, None]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
725
        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...
726
            yield Path(x)
727
728
    @classmethod
729
    def check_expired(
730
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
731
        path: PathLike,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
732
        max_sec: Union[timedelta, float],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
733
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
734
        parent: Optional[PathLike] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
735
        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...
736
        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...
737
        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...
738
    ) -> Optional[bool]:
739
        """
740
        Warns and returns True if ``path`` mod date is more than ``max_sec`` in the past.
741
        Returns None if it could not be determined.
742
743
        The formatting strings can refer to any of these (will be empty if unknown):
744
        - path: Full path
745
        - name: File/dir name
746
        - path_rel: Path relative to ``self._dir``, or full path if not under
747
        - now: formatted current datetime
748
        - [mod/create]_dt: Formatted mod/creation datetime
749
        - [mod/create]_rel: Mod/create datetime in terms of offset from now
750
        - [mod/create]_delta: Formatted timedelta from now
751
        - [mod/create]_delta_sec: Number of seconds from now (negative if now < mod/create dt)
752
753
        Args:
754
            path: A specific path to check
755
            max_sec: Max seconds, or a timedelta
756
            parent: If provided, path_rel is relative to this directory (to simplify warnings)
757
            warn_expired_fmt: Formatting string in terms of the variables listed above
758
            warn_unknown_fmt: Formatting string in terms of the variables listed above
759
            log: Log about problems
760
761
        Returns:
762
            Whether it is expired, or None if it could not be determined
763
        """
764
        path = Path(path)
765
        if log is None:
766
            log = lambda _: None
767
        limit = max_sec if isinstance(max_sec, timedelta) else timedelta(seconds=max_sec)
768
        now = datetime.now().astimezone()
769
        info = FilesysTools.get_info(path)
770
        if info.mod_dt and now - info.mod_dt > limit:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
771
            cls._warn_expired(now, info.mod_dt, info.create_dt, path, parent, warn_expired_fmt, log)
772
            return True
773
        elif not info.mod_dt and (not info.create_dt or (now - info.create_dt) > limit):
774
            cls._warn_expired(now, info.mod_dt, info.create_dt, path, parent, warn_unknown_fmt, log)
775
            return None
776
        return False
777
778
    @classmethod
779
    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...
780
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
781
        now: datetime,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
782
        mod: Optional[datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
783
        created: Optional[datetime],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
784
        path: Path,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
785
        parent: Optional[Path],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
786
        fmt: Optional[str],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
787
        log: Callable[[str], Any],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
788
    ):
789
        if isinstance(fmt, str):
790
            fmt = fmt.format
791
        if parent is not None and path.is_relative_to(parent):
792
            path_rel = str(path.relative_to(parent))
793
        else:
794
            path_rel = str(path)
795
        now_str, mod_str, mod_rel, mod_delta, mod_delta_sec = cls._expire_warning_info(now, mod)
796
        _, create_str, create_rel, create_delta, create_delta_sec = cls._expire_warning_info(
797
            now, created
798
        )
799
        msg = fmt(
800
            path=path,
801
            path_rel=path_rel,
802
            name=path.name,
803
            now=now_str,
804
            mod_dt=mod_str,
805
            mod_rel=mod_rel,
806
            mod_delta=mod_delta,
807
            mod_sec=mod_delta_sec,
808
            create_dt=create_str,
809
            create_rel=create_rel,
810
            create_delta=create_delta,
811
            create_sec=create_delta_sec,
812
        )
813
        log(msg)
814
815
    @classmethod
816
    def _expire_warning_info(
817
        cls, now: datetime, then: Optional[datetime]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
818
    ) -> Tuple[str, str, str, str, str]:
819
        now_str = now.strftime("%Y-%m-%d %H:%M:%S")
820
        if then is None:
821
            return now_str, "", "", "", ""
822
        delta = now - then
823
        then_str = then.strftime("%Y-%m-%d %H:%M:%S")
824
        then_rel = UnitTools.approx_time_wrt(now, then)
825
        delta_str = UnitTools.delta_time_to_str(delta, space=Chars.narrownbsp)
826
        return now_str, then_str, then_rel, delta_str, str(delta.total_seconds())
827
828
    @classmethod
829
    def __stat_raw(cls, path: Path) -> Optional[os.stat_result]:
830
        try:
831
            return path.lstat()
832
        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...
833
            if hasattr(pathlib, "_ignore_error") and not pathlib._ignore_error(e):
0 ignored issues
show
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...
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...
834
                raise
835
        return None
836
837
838
__all__ = ["FilesysTools", "PathInfo"]
839