Passed
Push — main ( 75b58f...d2739b )
by Douglas
01:59
created

FilesysTools.dt_for_filesys()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
Coding Style Naming introduced by
Variable name "e2" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

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

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

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

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

Loading history...
338
            system = f"UNKNOWN << {e2} >>"
339
        msg, tb = SystemTools.serialize_exception(e)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "tb" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

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

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

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

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

Loading history...
340
        tb = [t.as_dict() for t in tb]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "tb" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

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

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

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

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

Loading history...
341
        return dict(message=msg, stacktrace=tb, system=system)
342
343
    @classmethod
344
    def dt_for_filesys(cls, dt: Optional[datetime] = None) -> str:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Coding Style Naming introduced by
Argument name "dt" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

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

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

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

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

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