Marco Ricci commited on 2025-01-20 18:00:12
              Zeige 10 geänderte Dateien mit 175 Einfügungen und 203 Löschungen.
            
The interface was already supported by all the user-facing code (interestingly enough) and by almost all of the internal code. Using `pathlib.Path` objects directly instead of manually dealing with string or byte string collections does cut down on a lot of the otherwise explicit bookkeeping and context management. Some changes to the tests are necessary because other (or more) functions need to be mocked, or other ways of testing successful or ensuring unsuccessful operations are needed.
| ... | ... | 
                      @@ -247,9 +247,7 @@ select = [  | 
                  
| 247 | 247 | 
                        'CPY', 'C4', 'DTZ', 'T10', 'DJ', 'EM', 'EXE', 'FA',  | 
                    
| 248 | 248 | 
                        'ISC', 'ICN', 'LOG', 'G', 'INP', 'PIE', 'T20', 'PYI',  | 
                    
| 249 | 249 | 
                        'PT', 'Q', 'RET', 'SLF', 'SLOT', 'SIM', 'TID', 'TC',  | 
                    
| 250 | 
                        - 'INT', 'ARG',  | 
                    |
| 251 | 
                        - # We currently do not use pathlib. Disable 'PTH'.  | 
                    |
| 252 | 
                        - 'TD',  | 
                    |
| 250 | 
                        + 'INT', 'ARG', 'PTH', 'TD',  | 
                    |
| 253 | 251 | 
                        # We use TODOs and FIXMEs as notes for later, and don't want the  | 
                    
| 254 | 252 | 
                        # linter to nag about every occurrence. Disable 'FIX'.  | 
                    
| 255 | 253 | 
                        #  | 
                    
| ... | ... | 
                      @@ -24,6 +24,7 @@ import functools  | 
                  
| 24 | 24 | 
                        import gettext  | 
                    
| 25 | 25 | 
                        import inspect  | 
                    
| 26 | 26 | 
                        import os  | 
                    
| 27 | 
                        +import pathlib  | 
                    |
| 27 | 28 | 
                        import string  | 
                    
| 28 | 29 | 
                        import sys  | 
                    
| 29 | 30 | 
                        import textwrap  | 
                    
| ... | ... | 
                      @@ -48,7 +49,7 @@ PROG_NAME = 'derivepassphrase'  | 
                  
| 48 | 49 | 
                         | 
                    
| 49 | 50 | 
                         | 
                    
| 50 | 51 | 
                        def load_translations(  | 
                    
| 51 | 
                        - localedirs: list[str] | None = None,  | 
                    |
| 52 | 
                        + localedirs: list[str | bytes | os.PathLike] | None = None,  | 
                    |
| 52 | 53 | 
                        languages: Sequence[str] | None = None,  | 
                    
| 53 | 54 | 
                        class_: type[gettext.NullTranslations] | None = None,  | 
                    
| 54 | 55 | 
                        ) -> gettext.NullTranslations: # pragma: no cover  | 
                    
| ... | ... | 
                      @@ -71,29 +72,36 @@ def load_translations(  | 
                  
| 71 | 72 | 
                        Returns:  | 
                    
| 72 | 73 | 
                        A (potentially dummy) translation catalog.  | 
                    
| 73 | 74 | 
                         | 
                    
| 75 | 
                        + Raises:  | 
                    |
| 76 | 
                        + RuntimeError:  | 
                    |
| 77 | 
                        + `APPDATA` (on Windows) or `XDG_DATA_HOME` (otherwise) is not  | 
                    |
| 78 | 
                        + set. We attempted to compute the default value, but failed  | 
                    |
| 79 | 
                        + to determine the home directory.  | 
                    |
| 80 | 
                        +  | 
                    |
| 74 | 81 | 
                        """  | 
                    
| 75 | 82 | 
                        if localedirs is None:  | 
                    
| 76 | 83 | 
                                 if sys.platform.startswith('win'):
                       | 
                    
| 77 | 
                        - xdg_data_home = os.environ.get(  | 
                    |
| 78 | 
                        - 'APPDATA',  | 
                    |
| 79 | 
                        -                os.path.expanduser('~'),
                       | 
                    |
| 84 | 
                        + xdg_data_home = (  | 
                    |
| 85 | 
                        + pathlib.Path(os.environ['APPDATA'])  | 
                    |
| 86 | 
                        +                if os.environ.get('APPDATA')
                       | 
                    |
| 87 | 
                        +                else pathlib.Path('~').expanduser()
                       | 
                    |
| 80 | 88 | 
                        )  | 
                    
| 81 | 89 | 
                                 elif os.environ.get('XDG_DATA_HOME'):
                       | 
                    
| 82 | 
                        - xdg_data_home = os.environ['XDG_DATA_HOME']  | 
                    |
| 90 | 
                        + xdg_data_home = pathlib.Path(os.environ['XDG_DATA_HOME'])  | 
                    |
| 83 | 91 | 
                        else:  | 
                    
| 84 | 
                        - xdg_data_home = os.path.join(  | 
                    |
| 85 | 
                        -                os.path.expanduser('~'), '.local', 'share'
                       | 
                    |
| 92 | 
                        + xdg_data_home = (  | 
                    |
| 93 | 
                        +                pathlib.Path('~').expanduser() / '.local' / '.share'
                       | 
                    |
| 86 | 94 | 
                        )  | 
                    
| 87 | 95 | 
                        localedirs = [  | 
                    
| 88 | 
                        - os.path.join(xdg_data_home, 'locale'),  | 
                    |
| 89 | 
                        - os.path.join(sys.prefix, 'share', 'locale'),  | 
                    |
| 90 | 
                        - os.path.join(sys.base_prefix, 'share', 'locale'),  | 
                    |
| 96 | 
                        + pathlib.Path(xdg_data_home, 'locale'),  | 
                    |
| 97 | 
                        + pathlib.Path(sys.prefix, 'share', 'locale'),  | 
                    |
| 98 | 
                        + pathlib.Path(sys.base_prefix, 'share', 'locale'),  | 
                    |
| 91 | 99 | 
                        ]  | 
                    
| 92 | 100 | 
                        for localedir in localedirs:  | 
                    
| 93 | 101 | 
                        with contextlib.suppress(OSError):  | 
                    
| 94 | 102 | 
                        return gettext.translation(  | 
                    
| 95 | 103 | 
                        PROG_NAME,  | 
                    
| 96 | 
                        - localedir=localedir,  | 
                    |
| 104 | 
                        + localedir=os.fsdecode(localedir),  | 
                    |
| 97 | 105 | 
                        languages=languages,  | 
                    
| 98 | 106 | 
                        class_=class_,  | 
                    
| 99 | 107 | 
                        )  | 
                    
| ... | ... | 
                      @@ -17,6 +17,7 @@ import inspect  | 
                  
| 17 | 17 | 
                        import json  | 
                    
| 18 | 18 | 
                        import logging  | 
                    
| 19 | 19 | 
                        import os  | 
                    
| 20 | 
                        +import pathlib  | 
                    |
| 20 | 21 | 
                        import shlex  | 
                    
| 21 | 22 | 
                        import sys  | 
                    
| 22 | 23 | 
                        import unicodedata  | 
                    
| ... | ... | 
                      @@ -50,7 +51,6 @@ else:  | 
                  
| 50 | 51 | 
                        import tomli as tomllib  | 
                    
| 51 | 52 | 
                         | 
                    
| 52 | 53 | 
                        if TYPE_CHECKING:  | 
                    
| 53 | 
                        - import pathlib  | 
                    |
| 54 | 54 | 
                        import socket  | 
                    
| 55 | 55 | 
                        import types  | 
                    
| 56 | 56 | 
                        from collections.abc import (  | 
                    
| ... | ... | 
                      @@ -1468,7 +1468,7 @@ def derivepassphrase_export_vault(  | 
                  
| 1468 | 1468 | 
                         | 
                    
| 1469 | 1469 | 
                        def _config_filename(  | 
                    
| 1470 | 1470 | 
                        subsystem: str | None = 'old settings.json',  | 
                    
| 1471 | 
                        -) -> str | bytes | pathlib.Path:  | 
                    |
| 1471 | 
                        +) -> pathlib.Path:  | 
                    |
| 1472 | 1472 | 
                        """Return the filename of the configuration file for the subsystem.  | 
                    
| 1473 | 1473 | 
                         | 
                    
| 1474 | 1474 | 
                        The (implicit default) file is currently named `settings.json`,  | 
                    
| ... | ... | 
                      @@ -1495,9 +1495,9 @@ def _config_filename(  | 
                  
| 1495 | 1495 | 
                        The subsystem will be mandatory to specify.  | 
                    
| 1496 | 1496 | 
                         | 
                    
| 1497 | 1497 | 
                        """  | 
                    
| 1498 | 
                        - path: str | bytes | pathlib.Path  | 
                    |
| 1499 | 
                        - path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(  | 
                    |
| 1500 | 
                        - PROG_NAME, force_posix=True  | 
                    |
| 1498 | 
                        + path = pathlib.Path(  | 
                    |
| 1499 | 
                        + os.getenv(PROG_NAME.upper() + '_PATH')  | 
                    |
| 1500 | 
                        + or click.get_app_dir(PROG_NAME, force_posix=True)  | 
                    |
| 1501 | 1501 | 
                        )  | 
                    
| 1502 | 1502 | 
                        # Use match/case here once Python 3.9 becomes unsupported.  | 
                    
| 1503 | 1503 | 
                        if subsystem is None:  | 
                    
| ... | ... | 
                      @@ -1511,7 +1511,7 @@ def _config_filename(  | 
                  
| 1511 | 1511 | 
                        else: # pragma: no cover  | 
                    
| 1512 | 1512 | 
                                 msg = f'Unknown configuration subsystem: {subsystem!r}'
                       | 
                    
| 1513 | 1513 | 
                        raise AssertionError(msg)  | 
                    
| 1514 | 
                        - return os.path.join(path, filename)  | 
                    |
| 1514 | 
                        + return path / filename  | 
                    |
| 1515 | 1515 | 
                         | 
                    
| 1516 | 1516 | 
                         | 
                    
| 1517 | 1517 | 
                        def _load_config() -> _types.VaultConfig:  | 
                    
| ... | ... | 
                      @@ -1532,7 +1532,7 @@ def _load_config() -> _types.VaultConfig:  | 
                  
| 1532 | 1532 | 
                         | 
                    
| 1533 | 1533 | 
                        """  | 
                    
| 1534 | 1534 | 
                        filename = _config_filename(subsystem='vault')  | 
                    
| 1535 | 
                        - with open(filename, 'rb') as fileobj:  | 
                    |
| 1535 | 
                        +    with filename.open('rb') as fileobj:
                       | 
                    |
| 1536 | 1536 | 
                        data = json.load(fileobj)  | 
                    
| 1537 | 1537 | 
                        if not _types.is_vault_config(data):  | 
                    
| 1538 | 1538 | 
                        raise ValueError(_INVALID_VAULT_CONFIG)  | 
                    
| ... | ... | 
                      @@ -1563,12 +1563,12 @@ def _migrate_and_load_old_config() -> tuple[  | 
                  
| 1563 | 1563 | 
                        """  | 
                    
| 1564 | 1564 | 
                        new_filename = _config_filename(subsystem='vault')  | 
                    
| 1565 | 1565 | 
                        old_filename = _config_filename(subsystem='old settings.json')  | 
                    
| 1566 | 
                        - with open(old_filename, 'rb') as fileobj:  | 
                    |
| 1566 | 
                        +    with old_filename.open('rb') as fileobj:
                       | 
                    |
| 1567 | 1567 | 
                        data = json.load(fileobj)  | 
                    
| 1568 | 1568 | 
                        if not _types.is_vault_config(data):  | 
                    
| 1569 | 1569 | 
                        raise ValueError(_INVALID_VAULT_CONFIG)  | 
                    
| 1570 | 1570 | 
                        try:  | 
                    
| 1571 | 
                        - os.replace(old_filename, new_filename)  | 
                    |
| 1571 | 
                        + old_filename.rename(new_filename)  | 
                    |
| 1572 | 1572 | 
                        except OSError as exc:  | 
                    
| 1573 | 1573 | 
                        return data, exc  | 
                    
| 1574 | 1574 | 
                        else:  | 
                    
| ... | ... | 
                      @@ -1591,17 +1591,13 @@ def _save_config(config: _types.VaultConfig, /) -> None:  | 
                  
| 1591 | 1591 | 
                        ValueError:  | 
                    
| 1592 | 1592 | 
                        The data cannot be stored as a vault(1)-compatible config.  | 
                    
| 1593 | 1593 | 
                         | 
                    
| 1594 | 
                        - """ # noqa: DOC501  | 
                    |
| 1594 | 
                        + """  | 
                    |
| 1595 | 1595 | 
                        if not _types.is_vault_config(config):  | 
                    
| 1596 | 1596 | 
                        raise ValueError(_INVALID_VAULT_CONFIG)  | 
                    
| 1597 | 1597 | 
                        filename = _config_filename(subsystem='vault')  | 
                    
| 1598 | 
                        - filedir = os.path.dirname(os.path.abspath(filename))  | 
                    |
| 1599 | 
                        - try:  | 
                    |
| 1600 | 
                        - os.makedirs(filedir, exist_ok=False)  | 
                    |
| 1601 | 
                        - except FileExistsError:  | 
                    |
| 1602 | 
                        - if not os.path.isdir(filedir):  | 
                    |
| 1603 | 
                        - raise  | 
                    |
| 1604 | 
                        - with open(filename, 'w', encoding='UTF-8') as fileobj:  | 
                    |
| 1598 | 
                        + filedir = filename.resolve().parent  | 
                    |
| 1599 | 
                        + filedir.mkdir(parents=True, exist_ok=True)  | 
                    |
| 1600 | 
                        +    with filename.open('w', encoding='UTF-8') as fileobj:
                       | 
                    |
| 1605 | 1601 | 
                        json.dump(config, fileobj)  | 
                    
| 1606 | 1602 | 
                         | 
                    
| 1607 | 1603 | 
                         | 
                    
| ... | ... | 
                      @@ -1622,7 +1618,7 @@ def _load_user_config() -> dict[str, Any]:  | 
                  
| 1622 | 1618 | 
                         | 
                    
| 1623 | 1619 | 
                        """  | 
                    
| 1624 | 1620 | 
                        filename = _config_filename(subsystem='user configuration')  | 
                    
| 1625 | 
                        - with open(filename, 'rb') as fileobj:  | 
                    |
| 1621 | 
                        +    with filename.open('rb') as fileobj:
                       | 
                    |
| 1626 | 1622 | 
                        return tomllib.load(fileobj)  | 
                    
| 1627 | 1623 | 
                         | 
                    
| 1628 | 1624 | 
                         | 
                    
| ... | ... | 
                      @@ -2489,8 +2485,8 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 2489 | 2485 | 
                        delete_service_settings: bool = False,  | 
                    
| 2490 | 2486 | 
                        delete_globals: bool = False,  | 
                    
| 2491 | 2487 | 
                        clear_all_settings: bool = False,  | 
                    
| 2492 | 
                        - export_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,  | 
                    |
| 2493 | 
                        - import_settings: TextIO | pathlib.Path | os.PathLike[str] | None = None,  | 
                    |
| 2488 | 
                        + export_settings: TextIO | os.PathLike[str] | None = None,  | 
                    |
| 2489 | 
                        + import_settings: TextIO | os.PathLike[str] | None = None,  | 
                    |
| 2494 | 2490 | 
                        overwrite_config: bool = False,  | 
                    
| 2495 | 2491 | 
                        unset_settings: Sequence[str] = (),  | 
                    
| 2496 | 2492 | 
                        export_as: Literal['json', 'sh'] = 'json',  | 
                    
| ... | ... | 
                      @@ -2676,10 +2672,8 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 2676 | 2672 | 
                        backup_config, exc = _migrate_and_load_old_config()  | 
                    
| 2677 | 2673 | 
                        except FileNotFoundError:  | 
                    
| 2678 | 2674 | 
                                         return {'services': {}}
                       | 
                    
| 2679 | 
                        - old_name = os.path.basename(  | 
                    |
| 2680 | 
                        - _config_filename(subsystem='old settings.json')  | 
                    |
| 2681 | 
                        - )  | 
                    |
| 2682 | 
                        - new_name = os.path.basename(_config_filename(subsystem='vault'))  | 
                    |
| 2675 | 
                        + old_name = _config_filename(subsystem='old settings.json').name  | 
                    |
| 2676 | 
                        + new_name = _config_filename(subsystem='vault').name  | 
                    |
| 2683 | 2677 | 
                        deprecation.warning(  | 
                    
| 2684 | 2678 | 
                        _msg.TranslatedString(  | 
                    
| 2685 | 2679 | 
                        _msg.WarnMsgTemplate.V01_STYLE_CONFIG,  | 
                    
| ... | ... | 
                      @@ -8,6 +8,7 @@ from __future__ import annotations  | 
                  
| 8 | 8 | 
                         | 
                    
| 9 | 9 | 
                        import importlib  | 
                    
| 10 | 10 | 
                        import os  | 
                    
| 11 | 
                        +import pathlib  | 
                    |
| 11 | 12 | 
                        from typing import TYPE_CHECKING, Protocol  | 
                    
| 12 | 13 | 
                         | 
                    
| 13 | 14 | 
                        import derivepassphrase as dpp  | 
                    
| ... | ... | 
                      @@ -34,10 +35,10 @@ class NotAVaultConfigError(ValueError):  | 
                  
| 34 | 35 | 
                         | 
                    
| 35 | 36 | 
                        def __init__(  | 
                    
| 36 | 37 | 
                        self,  | 
                    
| 37 | 
                        - path: str | bytes,  | 
                    |
| 38 | 
                        + path: str | bytes | os.PathLike,  | 
                    |
| 38 | 39 | 
                        format: str | None = None, # noqa: A002  | 
                    
| 39 | 40 | 
                        ) -> None:  | 
                    
| 40 | 
                        - self.path = path  | 
                    |
| 41 | 
                        + self.path = os.fspath(path)  | 
                    |
| 41 | 42 | 
                        self.format = format  | 
                    
| 42 | 43 | 
                         | 
                    
| 43 | 44 | 
                        def __str__(self) -> str: # pragma: no cover  | 
                    
| ... | ... | 
                      @@ -79,7 +80,7 @@ def get_vault_key() -> bytes:  | 
                  
| 79 | 80 | 
                        return username  | 
                    
| 80 | 81 | 
                         | 
                    
| 81 | 82 | 
                         | 
                    
| 82 | 
                        -def get_vault_path() -> str | bytes | os.PathLike:  | 
                    |
| 83 | 
                        +def get_vault_path() -> pathlib.Path:  | 
                    |
| 83 | 84 | 
                        """Automatically determine the vault(1) configuration path.  | 
                    
| 84 | 85 | 
                         | 
                    
| 85 | 86 | 
                        Query the `VAULT_PATH` environment variable, or default to  | 
                    
| ... | ... | 
                      @@ -96,13 +97,9 @@ def get_vault_path() -> str | bytes | os.PathLike:  | 
                  
| 96 | 97 | 
                        manually to the correct value.  | 
                    
| 97 | 98 | 
                         | 
                    
| 98 | 99 | 
                        """  | 
                    
| 99 | 
                        - result = os.path.join(  | 
                    |
| 100 | 
                        -        os.path.expanduser('~'), os.environ.get('VAULT_PATH', '.vault')
                       | 
                    |
| 101 | 
                        - )  | 
                    |
| 102 | 
                        -    if result.startswith('~'):
                       | 
                    |
| 103 | 
                        - msg = 'Cannot determine home directory'  | 
                    |
| 104 | 
                        - raise RuntimeError(msg)  | 
                    |
| 105 | 
                        - return result  | 
                    |
| 100 | 
                        + return pathlib.Path(  | 
                    |
| 101 | 
                        +        '~', os.environ.get('VAULT_PATH', '.vault')
                       | 
                    |
| 102 | 
                        + ).expanduser()  | 
                    |
| 106 | 103 | 
                         | 
                    
| 107 | 104 | 
                         | 
                    
| 108 | 105 | 
                        class ExportVaultConfigDataFunction(Protocol): # pragma: no cover  | 
                    
| ... | ... | 
                      @@ -25,12 +25,12 @@ should *not* be used or relied on.  | 
                  
| 25 | 25 | 
                        from __future__ import annotations  | 
                    
| 26 | 26 | 
                         | 
                    
| 27 | 27 | 
                        import base64  | 
                    
| 28 | 
                        -import fnmatch  | 
                    |
| 29 | 28 | 
                        import importlib  | 
                    
| 30 | 29 | 
                        import json  | 
                    
| 31 | 30 | 
                        import logging  | 
                    
| 32 | 31 | 
                        import os  | 
                    
| 33 | 32 | 
                        import os.path  | 
                    
| 33 | 
                        +import pathlib  | 
                    |
| 34 | 34 | 
                        import struct  | 
                    
| 35 | 35 | 
                        from typing import TYPE_CHECKING, Any  | 
                    
| 36 | 36 | 
                         | 
                    
| ... | ... | 
                      @@ -107,6 +107,8 @@ def export_storeroom_data( # noqa: C901,D417,PLR0912,PLR0914,PLR0915  | 
                  
| 107 | 107 | 
                             importlib.import_module('cryptography')
                       | 
                    
| 108 | 108 | 
                        if path is None:  | 
                    
| 109 | 109 | 
                        path = exporter.get_vault_path()  | 
                    
| 110 | 
                        + else:  | 
                    |
| 111 | 
                        + path = pathlib.Path(os.fsdecode(path))  | 
                    |
| 110 | 112 | 
                        if key is None:  | 
                    
| 111 | 113 | 
                        key = exporter.get_vault_key()  | 
                    
| 112 | 114 | 
                        if format != 'storeroom': # pragma: no cover  | 
                    
| ... | ... | 
                      @@ -115,15 +117,11 @@ def export_storeroom_data( # noqa: C901,D417,PLR0912,PLR0914,PLR0915  | 
                  
| 115 | 117 | 
                        )  | 
                    
| 116 | 118 | 
                        raise ValueError(msg)  | 
                    
| 117 | 119 | 
                        try:  | 
                    
| 118 | 
                        - master_keys_file = open( # noqa: SIM115  | 
                    |
| 119 | 
                        - os.path.join(os.fsdecode(path), '.keys'),  | 
                    |
| 120 | 
                        + master_keys_file = pathlib.Path(path, '.keys').open( # noqa: SIM115  | 
                    |
| 120 | 121 | 
                        encoding='utf-8',  | 
                    
| 121 | 122 | 
                        )  | 
                    
| 122 | 123 | 
                        except FileNotFoundError as exc:  | 
                    
| 123 | 
                        - raise exporter.NotAVaultConfigError(  | 
                    |
| 124 | 
                        - os.fsdecode(path),  | 
                    |
| 125 | 
                        - format='storeroom',  | 
                    |
| 126 | 
                        - ) from exc  | 
                    |
| 124 | 
                        + raise exporter.NotAVaultConfigError(path, format='storeroom') from exc  | 
                    |
| 127 | 125 | 
                        with master_keys_file:  | 
                    
| 128 | 126 | 
                        header = json.loads(master_keys_file.readline())  | 
                    
| 129 | 127 | 
                                 if header != {'version': 1}:
                       | 
                    
| ... | ... | 
                      @@ -149,14 +147,7 @@ def export_storeroom_data( # noqa: C901,D417,PLR0912,PLR0914,PLR0915  | 
                  
| 149 | 147 | 
                         | 
                    
| 150 | 148 | 
                             config_structure: dict[str, Any] = {}
                       | 
                    
| 151 | 149 | 
                             json_contents: dict[str, bytes] = {}
                       | 
                    
| 152 | 
                        - # Use glob.glob(..., root_dir=...) here once Python 3.9 becomes  | 
                    |
| 153 | 
                        - # unsupported.  | 
                    |
| 154 | 
                        - storeroom_path_str = os.fsdecode(path)  | 
                    |
| 155 | 
                        - valid_hashdirs = [  | 
                    |
| 156 | 
                        - hashdir_name  | 
                    |
| 157 | 
                        - for hashdir_name in os.listdir(storeroom_path_str)  | 
                    |
| 158 | 
                        - if fnmatch.fnmatch(hashdir_name, '[01][0-9a-f]')  | 
                    |
| 159 | 
                        - ]  | 
                    |
| 150 | 
                        +    valid_hashdirs = list(path.glob('[01][0-9a-f]'))
                       | 
                    |
| 160 | 151 | 
                        for file in valid_hashdirs:  | 
                    
| 161 | 152 | 
                        logger.info(  | 
                    
| 162 | 153 | 
                        _msg.TranslatedString(  | 
                    
| ... | ... | 
                      @@ -165,8 +156,7 @@ def export_storeroom_data( # noqa: C901,D417,PLR0912,PLR0914,PLR0915  | 
                  
| 165 | 156 | 
                        )  | 
                    
| 166 | 157 | 
                        )  | 
                    
| 167 | 158 | 
                        bucket_contents = [  | 
                    
| 168 | 
                        - bytes(item)  | 
                    |
| 169 | 
                        - for item in _decrypt_bucket_file(file, master_keys, root_dir=path)  | 
                    |
| 159 | 
                        + bytes(item) for item in _decrypt_bucket_file(file, master_keys)  | 
                    |
| 170 | 160 | 
                        ]  | 
                    
| 171 | 161 | 
                        bucket_index = json.loads(bucket_contents.pop(0))  | 
                    
| 172 | 162 | 
                        for pos, item in enumerate(bucket_index):  | 
                    
| ... | ... | 
                      @@ -669,7 +659,7 @@ def _decrypt_bucket_item(  | 
                  
| 669 | 659 | 
                         | 
                    
| 670 | 660 | 
                         | 
                    
| 671 | 661 | 
                        def _decrypt_bucket_file(  | 
                    
| 672 | 
                        - filename: str,  | 
                    |
| 662 | 
                        + filename: str | bytes | os.PathLike,  | 
                    |
| 673 | 663 | 
                        master_keys: _types.StoreroomMasterKeys,  | 
                    
| 674 | 664 | 
                        *,  | 
                    
| 675 | 665 | 
                        root_dir: str | bytes | os.PathLike = '.',  | 
                    
| ... | ... | 
                      @@ -705,9 +695,9 @@ def _decrypt_bucket_file(  | 
                  
| 705 | 695 | 
                         | 
                    
| 706 | 696 | 
                        """  | 
                    
| 707 | 697 | 
                        master_keys = master_keys.toreadonly()  | 
                    
| 708 | 
                        - with open(  | 
                    |
| 709 | 
                        - os.path.join(os.fsdecode(root_dir), filename), 'rb'  | 
                    |
| 710 | 
                        - ) as bucket_file:  | 
                    |
| 698 | 
                        + root_dir = pathlib.Path(os.fsdecode(root_dir))  | 
                    |
| 699 | 
                        + filename = pathlib.Path(os.fsdecode(filename))  | 
                    |
| 700 | 
                        +    with (root_dir / filename).open('rb') as bucket_file:
                       | 
                    |
| 711 | 701 | 
                        header_line = bucket_file.readline()  | 
                    
| 712 | 702 | 
                        try:  | 
                    
| 713 | 703 | 
                        header = json.loads(header_line)  | 
                    
| ... | ... | 
                      @@ -31,6 +31,7 @@ import importlib  | 
                  
| 31 | 31 | 
                        import json  | 
                    
| 32 | 32 | 
                        import logging  | 
                    
| 33 | 33 | 
                        import os  | 
                    
| 34 | 
                        +import pathlib  | 
                    |
| 34 | 35 | 
                        import warnings  | 
                    
| 35 | 36 | 
                        from typing import TYPE_CHECKING  | 
                    
| 36 | 37 | 
                         | 
                    
| ... | ... | 
                      @@ -107,7 +108,9 @@ def export_vault_native_data( # noqa: D417  | 
                  
| 107 | 108 | 
                             importlib.import_module('cryptography')
                       | 
                    
| 108 | 109 | 
                        if path is None:  | 
                    
| 109 | 110 | 
                        path = exporter.get_vault_path()  | 
                    
| 110 | 
                        - with open(path, 'rb') as infile:  | 
                    |
| 111 | 
                        + else:  | 
                    |
| 112 | 
                        + path = pathlib.Path(os.fsdecode(path))  | 
                    |
| 113 | 
                        +    with path.open('rb') as infile:
                       | 
                    |
| 111 | 114 | 
                        contents = base64.standard_b64decode(infile.read())  | 
                    
| 112 | 115 | 
                        if key is None:  | 
                    
| 113 | 116 | 
                        key = exporter.get_vault_key()  | 
                    
| ... | ... | 
                      @@ -123,10 +126,7 @@ def export_vault_native_data( # noqa: D417  | 
                  
| 123 | 126 | 
                        try:  | 
                    
| 124 | 127 | 
                        return parser_class(contents, key)()  | 
                    
| 125 | 128 | 
                        except ValueError as exc:  | 
                    
| 126 | 
                        - raise exporter.NotAVaultConfigError(  | 
                    |
| 127 | 
                        - os.fsdecode(path),  | 
                    |
| 128 | 
                        - format=format,  | 
                    |
| 129 | 
                        - ) from exc  | 
                    |
| 129 | 
                        + raise exporter.NotAVaultConfigError(path, format=format) from exc  | 
                    |
| 130 | 130 | 
                         | 
                    
| 131 | 131 | 
                         | 
                    
| 132 | 132 | 
                        def _h(bs: Buffer) -> str:  | 
                    
| ... | ... | 
                      @@ -615,10 +615,7 @@ class VaultNativeV02ConfigParser(VaultNativeConfigParser):  | 
                  
| 615 | 615 | 
                        value (if any):  | 
                    
| 616 | 616 | 
                         | 
                    
| 617 | 617 | 
                        ~~~~ python  | 
                    
| 618 | 
                        -  | 
                    |
| 619 | 
                        - data = block_input = b''.join([  | 
                    |
| 620 | 
                        - previous_block, input_string, salt  | 
                    |
| 621 | 
                        - ])  | 
                    |
| 618 | 
                        + data = block_input = b''.join([previous_block, input_string, salt])  | 
                    |
| 622 | 619 | 
                        for i in range(iteration_count):  | 
                    
| 623 | 620 | 
                        data = message_digest(data)  | 
                    
| 624 | 621 | 
                        block = data  | 
                    
| ... | ... | 
                      @@ -728,7 +724,7 @@ if __name__ == '__main__':  | 
                  
| 728 | 724 | 
                        import os  | 
                    
| 729 | 725 | 
                         | 
                    
| 730 | 726 | 
                             logging.basicConfig(level=('DEBUG' if os.getenv('DEBUG') else 'WARNING'))
                       | 
                    
| 731 | 
                        - with open(exporter.get_vault_path(), 'rb') as infile:  | 
                    |
| 727 | 
                        +    with exporter.get_vault_path().open('rb') as infile:
                       | 
                    |
| 732 | 728 | 
                        contents = base64.standard_b64decode(infile.read())  | 
                    
| 733 | 729 | 
                        password = exporter.get_vault_key()  | 
                    
| 734 | 730 | 
                        try:  | 
                    
| ... | ... | 
                      @@ -12,6 +12,7 @@ import importlib.util  | 
                  
| 12 | 12 | 
                        import json  | 
                    
| 13 | 13 | 
                        import logging  | 
                    
| 14 | 14 | 
                        import os  | 
                    
| 15 | 
                        +import pathlib  | 
                    |
| 15 | 16 | 
                        import re  | 
                    
| 16 | 17 | 
                        import shlex  | 
                    
| 17 | 18 | 
                        import stat  | 
                    
| ... | ... | 
                      @@ -1440,18 +1441,16 @@ def isolated_config(  | 
                  
| 1440 | 1441 | 
                        stack.enter_context(  | 
                    
| 1441 | 1442 | 
                        cli.StandardCLILogging.ensure_standard_warnings_logging()  | 
                    
| 1442 | 1443 | 
                        )  | 
                    
| 1443 | 
                        -        monkeypatch.setenv('HOME', os.getcwd())
                       | 
                    |
| 1444 | 
                        -        monkeypatch.setenv('USERPROFILE', os.getcwd())
                       | 
                    |
| 1444 | 
                        + cwd = str(pathlib.Path.cwd().resolve())  | 
                    |
| 1445 | 
                        +        monkeypatch.setenv('HOME', cwd)
                       | 
                    |
| 1446 | 
                        +        monkeypatch.setenv('USERPROFILE', cwd)
                       | 
                    |
| 1445 | 1447 | 
                        monkeypatch.delenv(env_name, raising=False)  | 
                    
| 1446 | 1448 | 
                        config_dir = cli._config_filename(subsystem=None)  | 
                    
| 1447 | 
                        - os.makedirs(config_dir, exist_ok=True)  | 
                    |
| 1449 | 
                        + config_dir.mkdir(parents=True, exist_ok=True)  | 
                    |
| 1448 | 1450 | 
                        if isinstance(main_config_str, str):  | 
                    
| 1449 | 
                        - with open(  | 
                    |
| 1450 | 
                        -                cli._config_filename('user configuration'),
                       | 
                    |
| 1451 | 
                        - 'w',  | 
                    |
| 1452 | 
                        - encoding='UTF-8',  | 
                    |
| 1453 | 
                        - ) as outfile:  | 
                    |
| 1454 | 
                        - outfile.write(main_config_str)  | 
                    |
| 1451 | 
                        +            cli._config_filename('user configuration').write_text(
                       | 
                    |
| 1452 | 
                        + main_config_str, encoding='UTF-8'  | 
                    |
| 1453 | 
                        + )  | 
                    |
| 1455 | 1454 | 
                        yield  | 
                    
| 1456 | 1455 | 
                         | 
                    
| 1457 | 1456 | 
                         | 
                    
| ... | ... | 
                      @@ -1466,7 +1465,7 @@ def isolated_vault_config(  | 
                  
| 1466 | 1465 | 
                        monkeypatch=monkeypatch, runner=runner, main_config_str=main_config_str  | 
                    
| 1467 | 1466 | 
                        ):  | 
                    
| 1468 | 1467 | 
                        config_filename = cli._config_filename(subsystem='vault')  | 
                    
| 1469 | 
                        - with open(config_filename, 'w', encoding='UTF-8') as outfile:  | 
                    |
| 1468 | 
                        +        with config_filename.open('w', encoding='UTF-8') as outfile:
                       | 
                    |
| 1470 | 1469 | 
                        json.dump(vault_config, outfile)  | 
                    
| 1471 | 1470 | 
                        yield  | 
                    
| 1472 | 1471 | 
                         | 
                    
| ... | ... | 
                      @@ -1486,15 +1485,18 @@ def isolated_vault_exporter_config(  | 
                  
| 1486 | 1485 | 
                        except AttributeError:  | 
                    
| 1487 | 1486 | 
                         | 
                    
| 1488 | 1487 | 
                        @contextlib.contextmanager  | 
                    
| 1489 | 
                        - def chdir(newpath: str) -> Iterator[None]: # pragma: no branch  | 
                    |
| 1490 | 
                        - oldpath = os.getcwd()  | 
                    |
| 1488 | 
                        + def chdir(  | 
                    |
| 1489 | 
                        + newpath: str | bytes | os.PathLike,  | 
                    |
| 1490 | 
                        + ) -> Iterator[None]: # pragma: no branch  | 
                    |
| 1491 | 
                        + oldpath = pathlib.Path.cwd().resolve()  | 
                    |
| 1491 | 1492 | 
                        os.chdir(newpath)  | 
                    
| 1492 | 1493 | 
                        yield  | 
                    
| 1493 | 1494 | 
                        os.chdir(oldpath)  | 
                    
| 1494 | 1495 | 
                         | 
                    
| 1495 | 1496 | 
                        with runner.isolated_filesystem():  | 
                    
| 1496 | 
                        -        monkeypatch.setenv('HOME', os.getcwd())
                       | 
                    |
| 1497 | 
                        -        monkeypatch.setenv('USERPROFILE', os.getcwd())
                       | 
                    |
| 1497 | 
                        + cwd = str(pathlib.Path.cwd().resolve())  | 
                    |
| 1498 | 
                        +        monkeypatch.setenv('HOME', cwd)
                       | 
                    |
| 1499 | 
                        +        monkeypatch.setenv('USERPROFILE', cwd)
                       | 
                    |
| 1498 | 1500 | 
                                 monkeypatch.delenv('VAULT_PATH', raising=False)
                       | 
                    
| 1499 | 1501 | 
                                 monkeypatch.delenv('VAULT_KEY', raising=False)
                       | 
                    
| 1500 | 1502 | 
                                 monkeypatch.delenv('LOGNAME', raising=False)
                       | 
                    
| ... | ... | 
                      @@ -1502,16 +1504,16 @@ def isolated_vault_exporter_config(  | 
                  
| 1502 | 1504 | 
                                 monkeypatch.delenv('USERNAME', raising=False)
                       | 
                    
| 1503 | 1505 | 
                        if vault_key is not None:  | 
                    
| 1504 | 1506 | 
                                     monkeypatch.setenv('VAULT_KEY', vault_key)
                       | 
                    
| 1507 | 
                        +        vault_config_path = pathlib.Path('.vault').resolve()
                       | 
                    |
| 1505 | 1508 | 
                        # Use match/case here once Python 3.9 becomes unsupported.  | 
                    
| 1506 | 1509 | 
                        if isinstance(vault_config, str):  | 
                    
| 1507 | 
                        -            with open('.vault', 'w', encoding='UTF-8') as outfile:
                       | 
                    |
| 1508 | 
                        - print(vault_config, file=outfile)  | 
                    |
| 1510 | 
                        +            vault_config_path.write_text(f'{vault_config}\n', encoding='UTF-8')
                       | 
                    |
| 1509 | 1511 | 
                        elif isinstance(vault_config, bytes):  | 
                    
| 1510 | 
                        -            os.makedirs('.vault', mode=0o700, exist_ok=True)
                       | 
                    |
| 1512 | 
                        + vault_config_path.mkdir(parents=True, mode=0o700, exist_ok=True)  | 
                    |
| 1511 | 1513 | 
                        # Use parenthesized context manager expressions here once  | 
                    
| 1512 | 1514 | 
                        # Python 3.9 becomes unsupported.  | 
                    
| 1513 | 1515 | 
                        with contextlib.ExitStack() as stack:  | 
                    
| 1514 | 
                        -                stack.enter_context(chdir('.vault'))
                       | 
                    |
| 1516 | 
                        + stack.enter_context(chdir(vault_config_path))  | 
                    |
| 1515 | 1517 | 
                        tmpzipfile = stack.enter_context(  | 
                    
| 1516 | 1518 | 
                        tempfile.NamedTemporaryFile(suffix='.zip')  | 
                    
| 1517 | 1519 | 
                        )  | 
                    
| ... | ... | 
                      @@ -1562,7 +1564,7 @@ def make_file_readonly(  | 
                  
| 1562 | 1564 | 
                        and are susceptible to race conditions.  | 
                    
| 1563 | 1565 | 
                         | 
                    
| 1564 | 1566 | 
                        """  | 
                    
| 1565 | 
                        - fname: int | str | bytes | os.PathLike[str]  | 
                    |
| 1567 | 
                        + fname: int | str | bytes | os.PathLike  | 
                    |
| 1566 | 1568 | 
                             if try_race_free_implementation and {os.stat, os.chmod} <= os.supports_fd:
                       | 
                    
| 1567 | 1569 | 
                        fname = os.open(  | 
                    
| 1568 | 1570 | 
                        pathname,  | 
                    
| ... | ... | 
                      @@ -1573,13 +1575,13 @@ def make_file_readonly(  | 
                  
| 1573 | 1575 | 
                        else:  | 
                    
| 1574 | 1576 | 
                        fname = pathname  | 
                    
| 1575 | 1577 | 
                        try:  | 
                    
| 1576 | 
                        - orig_mode = os.stat(fname).st_mode  | 
                    |
| 1578 | 
                        + orig_mode = os.stat(fname).st_mode # noqa: PTH116  | 
                    |
| 1577 | 1579 | 
                        new_mode = (  | 
                    
| 1578 | 1580 | 
                        orig_mode & ~stat.S_IWUSR & ~stat.S_IWGRP & ~stat.S_IWOTH  | 
                    
| 1579 | 1581 | 
                        | stat.S_IREAD  | 
                    
| 1580 | 1582 | 
                        )  | 
                    
| 1581 | 
                        - os.chmod(fname, stat.S_IREAD)  | 
                    |
| 1582 | 
                        - os.chmod(fname, new_mode)  | 
                    |
| 1583 | 
                        + os.chmod(fname, stat.S_IREAD) # noqa: PTH101  | 
                    |
| 1584 | 
                        + os.chmod(fname, new_mode) # noqa: PTH101  | 
                    |
| 1583 | 1585 | 
                        finally:  | 
                    
| 1584 | 1586 | 
                        if isinstance(fname, int):  | 
                    
| 1585 | 1587 | 
                        os.close(fname)  | 
                    
| ... | ... | 
                      @@ -12,6 +12,7 @@ import io  | 
                  
| 12 | 12 | 
                        import json  | 
                    
| 13 | 13 | 
                        import logging  | 
                    
| 14 | 14 | 
                        import os  | 
                    
| 15 | 
                        +import pathlib  | 
                    |
| 15 | 16 | 
                        import shlex  | 
                    
| 16 | 17 | 
                        import shutil  | 
                    
| 17 | 18 | 
                        import socket  | 
                    
| ... | ... | 
                      @@ -926,8 +927,8 @@ class TestCLI:  | 
                  
| 926 | 927 | 
                        input=json.dumps(config),  | 
                    
| 927 | 928 | 
                        catch_exceptions=False,  | 
                    
| 928 | 929 | 
                        )  | 
                    
| 929 | 
                        - with open(  | 
                    |
| 930 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 930 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 931 | 
                        + encoding='UTF-8'  | 
                    |
| 931 | 932 | 
                        ) as infile:  | 
                    
| 932 | 933 | 
                        config2 = json.load(infile)  | 
                    
| 933 | 934 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| ... | ... | 
                      @@ -965,8 +966,8 @@ class TestCLI:  | 
                  
| 965 | 966 | 
                        input=json.dumps(config),  | 
                    
| 966 | 967 | 
                        catch_exceptions=False,  | 
                    
| 967 | 968 | 
                        )  | 
                    
| 968 | 
                        - with open(  | 
                    |
| 969 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 969 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 970 | 
                        + encoding='UTF-8'  | 
                    |
| 970 | 971 | 
                        ) as infile:  | 
                    
| 971 | 972 | 
                        config3 = json.load(infile)  | 
                    
| 972 | 973 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| ... | ... | 
                      @@ -1020,10 +1021,9 @@ class TestCLI:  | 
                  
| 1020 | 1021 | 
                        # configuration file ourselves afterwards, inside the context.  | 
                    
| 1021 | 1022 | 
                        # We also might as well use `isolated_config` instead.  | 
                    
| 1022 | 1023 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 1023 | 
                        - with open(  | 
                    |
| 1024 | 
                        - cli._config_filename(subsystem='vault'), 'w', encoding='UTF-8'  | 
                    |
| 1025 | 
                        - ) as outfile:  | 
                    |
| 1026 | 
                        -                print('This string is not valid JSON.', file=outfile)
                       | 
                    |
| 1024 | 
                        + cli._config_filename(subsystem='vault').write_text(  | 
                    |
| 1025 | 
                        + 'This string is not valid JSON.\n', encoding='UTF-8'  | 
                    |
| 1026 | 
                        + )  | 
                    |
| 1027 | 1027 | 
                        dname = cli._config_filename(subsystem=None)  | 
                    
| 1028 | 1028 | 
                        result_ = runner.invoke(  | 
                    
| 1029 | 1029 | 
                        cli.derivepassphrase_vault,  | 
                    
| ... | ... | 
                      @@ -1049,8 +1049,7 @@ class TestCLI:  | 
                  
| 1049 | 1049 | 
                        ) -> None:  | 
                    
| 1050 | 1050 | 
                        runner = click.testing.CliRunner(mix_stderr=False)  | 
                    
| 1051 | 1051 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 1052 | 
                        - with contextlib.suppress(FileNotFoundError):  | 
                    |
| 1053 | 
                        - os.remove(cli._config_filename(subsystem='vault'))  | 
                    |
| 1052 | 
                        + cli._config_filename(subsystem='vault').unlink(missing_ok=True)  | 
                    |
| 1054 | 1053 | 
                        result_ = runner.invoke(  | 
                    
| 1055 | 1054 | 
                        # Test parent context navigation by not calling  | 
                    
| 1056 | 1055 | 
                        # `cli.derivepassphrase_vault` directly. Used e.g. in  | 
                    
| ... | ... | 
                      @@ -1104,9 +1103,9 @@ class TestCLI:  | 
                  
| 1104 | 1103 | 
                        ) -> None:  | 
                    
| 1105 | 1104 | 
                        runner = click.testing.CliRunner(mix_stderr=False)  | 
                    
| 1106 | 1105 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 1107 | 
                        - with contextlib.suppress(FileNotFoundError):  | 
                    |
| 1108 | 
                        - os.remove(cli._config_filename(subsystem='vault'))  | 
                    |
| 1109 | 
                        - os.makedirs(cli._config_filename(subsystem='vault'))  | 
                    |
| 1106 | 
                        + config_file = cli._config_filename(subsystem='vault')  | 
                    |
| 1107 | 
                        + config_file.unlink(missing_ok=True)  | 
                    |
| 1108 | 
                        + config_file.mkdir(parents=True, exist_ok=True)  | 
                    |
| 1110 | 1109 | 
                        result_ = runner.invoke(  | 
                    
| 1111 | 1110 | 
                        cli.derivepassphrase_vault,  | 
                    
| 1112 | 1111 | 
                        ['--export', '-', *export_options],  | 
                    
| ... | ... | 
                      @@ -1158,10 +1157,10 @@ class TestCLI:  | 
                  
| 1158 | 1157 | 
                        ) -> None:  | 
                    
| 1159 | 1158 | 
                        runner = click.testing.CliRunner(mix_stderr=False)  | 
                    
| 1160 | 1159 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 1160 | 
                        + config_dir = cli._config_filename(subsystem=None)  | 
                    |
| 1161 | 1161 | 
                        with contextlib.suppress(FileNotFoundError):  | 
                    
| 1162 | 
                        -                shutil.rmtree('.derivepassphrase')
                       | 
                    |
| 1163 | 
                        -            with open('.derivepassphrase', 'w', encoding='UTF-8') as outfile:
                       | 
                    |
| 1164 | 
                        -                print('Obstruction!!', file=outfile)
                       | 
                    |
| 1162 | 
                        + shutil.rmtree(config_dir)  | 
                    |
| 1163 | 
                        +            config_dir.write_text('Obstruction!!\n')
                       | 
                    |
| 1165 | 1164 | 
                        result_ = runner.invoke(  | 
                    
| 1166 | 1165 | 
                        cli.derivepassphrase_vault,  | 
                    
| 1167 | 1166 | 
                        ['--export', '-', *export_options],  | 
                    
| ... | ... | 
                      @@ -1197,8 +1196,8 @@ contents go here  | 
                  
| 1197 | 1196 | 
                        )  | 
                    
| 1198 | 1197 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| 1199 | 1198 | 
                        assert result.clean_exit(empty_stderr=True), 'expected clean exit'  | 
                    
| 1200 | 
                        - with open(  | 
                    |
| 1201 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 1199 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 1200 | 
                        + encoding='UTF-8'  | 
                    |
| 1202 | 1201 | 
                        ) as infile:  | 
                    
| 1203 | 1202 | 
                        config = json.load(infile)  | 
                    
| 1204 | 1203 | 
                                     assert config == {
                       | 
                    
| ... | ... | 
                      @@ -1223,8 +1222,8 @@ contents go here  | 
                  
| 1223 | 1222 | 
                        )  | 
                    
| 1224 | 1223 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| 1225 | 1224 | 
                        assert result.clean_exit(empty_stderr=True), 'expected clean exit'  | 
                    
| 1226 | 
                        - with open(  | 
                    |
| 1227 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 1225 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 1226 | 
                        + encoding='UTF-8'  | 
                    |
| 1228 | 1227 | 
                        ) as infile:  | 
                    
| 1229 | 1228 | 
                        config = json.load(infile)  | 
                    
| 1230 | 1229 | 
                                     assert config == {'global': {'phrase': 'abc'}, 'services': {}}
                       | 
                    
| ... | ... | 
                      @@ -1246,8 +1245,8 @@ contents go here  | 
                  
| 1246 | 1245 | 
                        )  | 
                    
| 1247 | 1246 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| 1248 | 1247 | 
                        assert result.clean_exit(empty_stderr=True), 'expected clean exit'  | 
                    
| 1249 | 
                        - with open(  | 
                    |
| 1250 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 1248 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 1249 | 
                        + encoding='UTF-8'  | 
                    |
| 1251 | 1250 | 
                        ) as infile:  | 
                    
| 1252 | 1251 | 
                        config = json.load(infile)  | 
                    
| 1253 | 1252 | 
                                     assert config == {
                       | 
                    
| ... | ... | 
                      @@ -1274,8 +1273,8 @@ contents go here  | 
                  
| 1274 | 1273 | 
                        assert result.error_exit(error='the user aborted the request'), (  | 
                    
| 1275 | 1274 | 
                        'expected known error message'  | 
                    
| 1276 | 1275 | 
                        )  | 
                    
| 1277 | 
                        - with open(  | 
                    |
| 1278 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 1276 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 1277 | 
                        + encoding='UTF-8'  | 
                    |
| 1279 | 1278 | 
                        ) as infile:  | 
                    
| 1280 | 1279 | 
                        config = json.load(infile)  | 
                    
| 1281 | 1280 | 
                                     assert config == {'global': {'phrase': 'abc'}, 'services': {}}
                       | 
                    
| ... | ... | 
                      @@ -1346,8 +1345,8 @@ contents go here  | 
                  
| 1346 | 1345 | 
                        )  | 
                    
| 1347 | 1346 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| 1348 | 1347 | 
                        assert result.clean_exit(), 'expected clean exit'  | 
                    
| 1349 | 
                        - with open(  | 
                    |
| 1350 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 1348 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 1349 | 
                        + encoding='UTF-8'  | 
                    |
| 1351 | 1350 | 
                        ) as infile:  | 
                    
| 1352 | 1351 | 
                        config = json.load(infile)  | 
                    
| 1353 | 1352 | 
                        assert config == result_config, (  | 
                    
| ... | ... | 
                      @@ -1457,7 +1456,8 @@ contents go here  | 
                  
| 1457 | 1456 | 
                        runner=runner,  | 
                    
| 1458 | 1457 | 
                                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
                       | 
                    
| 1459 | 1458 | 
                        ):  | 
                    
| 1460 | 
                        -            monkeypatch.setenv('SSH_AUTH_SOCK', os.getcwd())
                       | 
                    |
| 1459 | 
                        + cwd = pathlib.Path.cwd().resolve()  | 
                    |
| 1460 | 
                        +            monkeypatch.setenv('SSH_AUTH_SOCK', str(cwd))
                       | 
                    |
| 1461 | 1461 | 
                        result_ = runner.invoke(  | 
                    
| 1462 | 1462 | 
                        cli.derivepassphrase_vault,  | 
                    
| 1463 | 1463 | 
                        ['--key', '--config'],  | 
                    
| ... | ... | 
                      @@ -1674,16 +1674,8 @@ contents go here  | 
                  
| 1674 | 1674 | 
                        monkeypatch=monkeypatch,  | 
                    
| 1675 | 1675 | 
                        runner=runner,  | 
                    
| 1676 | 1676 | 
                        ):  | 
                    
| 1677 | 
                        -            shutil.rmtree('.derivepassphrase')
                       | 
                    |
| 1678 | 
                        - os_makedirs_called = False  | 
                    |
| 1679 | 
                        - real_os_makedirs = os.makedirs  | 
                    |
| 1680 | 
                        -  | 
                    |
| 1681 | 
                        - def makedirs(*args: Any, **kwargs: Any) -> Any:  | 
                    |
| 1682 | 
                        - nonlocal os_makedirs_called  | 
                    |
| 1683 | 
                        - os_makedirs_called = True  | 
                    |
| 1684 | 
                        - return real_os_makedirs(*args, **kwargs)  | 
                    |
| 1685 | 
                        -  | 
                    |
| 1686 | 
                        - monkeypatch.setattr(os, 'makedirs', makedirs)  | 
                    |
| 1677 | 
                        + with contextlib.suppress(FileNotFoundError):  | 
                    |
| 1678 | 
                        + shutil.rmtree(cli._config_filename(subsystem=None))  | 
                    |
| 1687 | 1679 | 
                        result_ = runner.invoke(  | 
                    
| 1688 | 1680 | 
                        cli.derivepassphrase_vault,  | 
                    
| 1689 | 1681 | 
                        ['--config', '-p'],  | 
                    
| ... | ... | 
                      @@ -1695,9 +1687,8 @@ contents go here  | 
                  
| 1695 | 1687 | 
                        assert result.stderr == 'Passphrase:', (  | 
                    
| 1696 | 1688 | 
                        'program unexpectedly failed?!'  | 
                    
| 1697 | 1689 | 
                        )  | 
                    
| 1698 | 
                        - assert os_makedirs_called, 'os.makedirs has not been called?!'  | 
                    |
| 1699 | 
                        - with open(  | 
                    |
| 1700 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 1690 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 1691 | 
                        + encoding='UTF-8'  | 
                    |
| 1701 | 1692 | 
                        ) as infile:  | 
                    
| 1702 | 1693 | 
                        config_readback = json.load(infile)  | 
                    
| 1703 | 1694 | 
                                     assert config_readback == {
                       | 
                    
| ... | ... | 
                      @@ -1717,12 +1708,10 @@ contents go here  | 
                  
| 1717 | 1708 | 
                        save_config_ = cli._save_config  | 
                    
| 1718 | 1709 | 
                         | 
                    
| 1719 | 1710 | 
                        def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any:  | 
                    
| 1711 | 
                        + config_dir = cli._config_filename(subsystem=None)  | 
                    |
| 1720 | 1712 | 
                        with contextlib.suppress(FileNotFoundError):  | 
                    
| 1721 | 
                        -                    shutil.rmtree('.derivepassphrase')
                       | 
                    |
| 1722 | 
                        - with open(  | 
                    |
| 1723 | 
                        - '.derivepassphrase', 'w', encoding='UTF-8'  | 
                    |
| 1724 | 
                        - ) as outfile:  | 
                    |
| 1725 | 
                        -                    print('Obstruction!!', file=outfile)
                       | 
                    |
| 1713 | 
                        + shutil.rmtree(config_dir)  | 
                    |
| 1714 | 
                        +                config_dir.write_text('Obstruction!!\n')
                       | 
                    |
| 1726 | 1715 | 
                        monkeypatch.setattr(cli, '_save_config', save_config_)  | 
                    
| 1727 | 1716 | 
                        return save_config_(*args, **kwargs)  | 
                    
| 1728 | 1717 | 
                         | 
                    
| ... | ... | 
                      @@ -2089,7 +2078,7 @@ class TestCLIUtils:  | 
                  
| 2089 | 2078 | 
                        monkeypatch=monkeypatch, runner=runner, vault_config=config  | 
                    
| 2090 | 2079 | 
                        ):  | 
                    
| 2091 | 2080 | 
                        config_filename = cli._config_filename(subsystem='vault')  | 
                    
| 2092 | 
                        - with open(config_filename, encoding='UTF-8') as fileobj:  | 
                    |
| 2081 | 
                        + with config_filename.open(encoding='UTF-8') as fileobj:  | 
                    |
| 2093 | 2082 | 
                        assert json.load(fileobj) == config  | 
                    
| 2094 | 2083 | 
                        assert cli._load_config() == config  | 
                    
| 2095 | 2084 | 
                         | 
                    
| ... | ... | 
                      @@ -2600,8 +2589,8 @@ Boo.  | 
                  
| 2600 | 2589 | 
                        assert result.clean_exit(empty_stderr=True), (  | 
                    
| 2601 | 2590 | 
                        'expected clean exit'  | 
                    
| 2602 | 2591 | 
                        )  | 
                    
| 2603 | 
                        - with open(  | 
                    |
| 2604 | 
                        - cli._config_filename(subsystem='vault'), encoding='UTF-8'  | 
                    |
| 2592 | 
                        + with cli._config_filename(subsystem='vault').open(  | 
                    |
| 2593 | 
                        + encoding='UTF-8'  | 
                    |
| 2605 | 2594 | 
                        ) as infile:  | 
                    
| 2606 | 2595 | 
                        config_readback = json.load(infile)  | 
                    
| 2607 | 2596 | 
                        assert config_readback == result_config  | 
                    
| ... | ... | 
                      @@ -2844,11 +2833,9 @@ class TestCLITransition:  | 
                  
| 2844 | 2833 | 
                        ) -> None:  | 
                    
| 2845 | 2834 | 
                        runner = click.testing.CliRunner()  | 
                    
| 2846 | 2835 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 2847 | 
                        - config_filename = cli._config_filename(  | 
                    |
| 2848 | 
                        - subsystem='old settings.json'  | 
                    |
| 2836 | 
                        + cli._config_filename(subsystem='old settings.json').write_text(  | 
                    |
| 2837 | 
                        + json.dumps(config, indent=2) + '\n', encoding='UTF-8'  | 
                    |
| 2849 | 2838 | 
                        )  | 
                    
| 2850 | 
                        - with open(config_filename, 'w', encoding='UTF-8') as fileobj:  | 
                    |
| 2851 | 
                        - print(json.dumps(config, indent=2), file=fileobj)  | 
                    |
| 2852 | 2839 | 
                        assert cli._migrate_and_load_old_config()[0] == config  | 
                    
| 2853 | 2840 | 
                         | 
                    
| 2854 | 2841 | 
                        @pytest.mark.parametrize(  | 
                    
| ... | ... | 
                      @@ -2875,11 +2862,9 @@ class TestCLITransition:  | 
                  
| 2875 | 2862 | 
                        ) -> None:  | 
                    
| 2876 | 2863 | 
                        runner = click.testing.CliRunner()  | 
                    
| 2877 | 2864 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 2878 | 
                        - config_filename = cli._config_filename(  | 
                    |
| 2879 | 
                        - subsystem='old settings.json'  | 
                    |
| 2865 | 
                        + cli._config_filename(subsystem='old settings.json').write_text(  | 
                    |
| 2866 | 
                        + json.dumps(config, indent=2) + '\n', encoding='UTF-8'  | 
                    |
| 2880 | 2867 | 
                        )  | 
                    
| 2881 | 
                        - with open(config_filename, 'w', encoding='UTF-8') as fileobj:  | 
                    |
| 2882 | 
                        - print(json.dumps(config, indent=2), file=fileobj)  | 
                    |
| 2883 | 2868 | 
                        assert cli._migrate_and_load_old_config() == (config, None)  | 
                    
| 2884 | 2869 | 
                         | 
                    
| 2885 | 2870 | 
                        @pytest.mark.parametrize(  | 
                    
| ... | ... | 
                      @@ -2906,12 +2891,12 @@ class TestCLITransition:  | 
                  
| 2906 | 2891 | 
                        ) -> None:  | 
                    
| 2907 | 2892 | 
                        runner = click.testing.CliRunner()  | 
                    
| 2908 | 2893 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 2909 | 
                        - config_filename = cli._config_filename(  | 
                    |
| 2910 | 
                        - subsystem='old settings.json'  | 
                    |
| 2894 | 
                        + cli._config_filename(subsystem='old settings.json').write_text(  | 
                    |
| 2895 | 
                        + json.dumps(config, indent=2) + '\n', encoding='UTF-8'  | 
                    |
| 2896 | 
                        + )  | 
                    |
| 2897 | 
                        + cli._config_filename(subsystem='vault').mkdir(  | 
                    |
| 2898 | 
                        + parents=True, exist_ok=True  | 
                    |
| 2911 | 2899 | 
                        )  | 
                    
| 2912 | 
                        - with open(config_filename, 'w', encoding='UTF-8') as fileobj:  | 
                    |
| 2913 | 
                        - print(json.dumps(config, indent=2), file=fileobj)  | 
                    |
| 2914 | 
                        - os.mkdir(cli._config_filename(subsystem='vault'))  | 
                    |
| 2915 | 2900 | 
                        config2, err = cli._migrate_and_load_old_config()  | 
                    
| 2916 | 2901 | 
                        assert config2 == config  | 
                    
| 2917 | 2902 | 
                        assert isinstance(err, OSError)  | 
                    
| ... | ... | 
                      @@ -2941,11 +2926,9 @@ class TestCLITransition:  | 
                  
| 2941 | 2926 | 
                        ) -> None:  | 
                    
| 2942 | 2927 | 
                        runner = click.testing.CliRunner()  | 
                    
| 2943 | 2928 | 
                        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):  | 
                    
| 2944 | 
                        - config_filename = cli._config_filename(  | 
                    |
| 2945 | 
                        - subsystem='old settings.json'  | 
                    |
| 2929 | 
                        + cli._config_filename(subsystem='old settings.json').write_text(  | 
                    |
| 2930 | 
                        + json.dumps(config, indent=2) + '\n', encoding='UTF-8'  | 
                    |
| 2946 | 2931 | 
                        )  | 
                    
| 2947 | 
                        - with open(config_filename, 'w', encoding='UTF-8') as fileobj:  | 
                    |
| 2948 | 
                        - print(json.dumps(config, indent=2), file=fileobj)  | 
                    |
| 2949 | 2932 | 
                        with pytest.raises(ValueError, match=cli._INVALID_VAULT_CONFIG):  | 
                    
| 2950 | 2933 | 
                        cli._migrate_and_load_old_config()  | 
                    
| 2951 | 2934 | 
                         | 
                    
| ... | ... | 
                      @@ -3077,17 +3060,13 @@ class TestCLITransition:  | 
                  
| 3077 | 3060 | 
                        monkeypatch=monkeypatch,  | 
                    
| 3078 | 3061 | 
                        runner=runner,  | 
                    
| 3079 | 3062 | 
                        ):  | 
                    
| 3080 | 
                        - with open(  | 
                    |
| 3081 | 
                        - cli._config_filename(subsystem='old settings.json'),  | 
                    |
| 3082 | 
                        - 'w',  | 
                    |
| 3083 | 
                        - encoding='UTF-8',  | 
                    |
| 3084 | 
                        - ) as fileobj:  | 
                    |
| 3085 | 
                        - print(  | 
                    |
| 3063 | 
                        + cli._config_filename(subsystem='old settings.json').write_text(  | 
                    |
| 3086 | 3064 | 
                        json.dumps(  | 
                    
| 3087 | 3065 | 
                                             {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    
| 3088 | 3066 | 
                        indent=2,  | 
                    
| 3089 | 
                        - ),  | 
                    |
| 3090 | 
                        - file=fileobj,  | 
                    |
| 3067 | 
                        + )  | 
                    |
| 3068 | 
                        + + '\n',  | 
                    |
| 3069 | 
                        + encoding='UTF-8',  | 
                    |
| 3091 | 3070 | 
                        )  | 
                    
| 3092 | 3071 | 
                        result_ = runner.invoke(  | 
                    
| 3093 | 3072 | 
                        cli.derivepassphrase_vault,  | 
                    
| ... | ... | 
                      @@ -3113,17 +3092,13 @@ class TestCLITransition:  | 
                  
| 3113 | 3092 | 
                        monkeypatch=monkeypatch,  | 
                    
| 3114 | 3093 | 
                        runner=runner,  | 
                    
| 3115 | 3094 | 
                        ):  | 
                    
| 3116 | 
                        - with open(  | 
                    |
| 3117 | 
                        - cli._config_filename(subsystem='old settings.json'),  | 
                    |
| 3118 | 
                        - 'w',  | 
                    |
| 3119 | 
                        - encoding='UTF-8',  | 
                    |
| 3120 | 
                        - ) as fileobj:  | 
                    |
| 3121 | 
                        - print(  | 
                    |
| 3095 | 
                        + cli._config_filename(subsystem='old settings.json').write_text(  | 
                    |
| 3122 | 3096 | 
                        json.dumps(  | 
                    
| 3123 | 3097 | 
                                             {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    
| 3124 | 3098 | 
                        indent=2,  | 
                    
| 3125 | 
                        - ),  | 
                    |
| 3126 | 
                        - file=fileobj,  | 
                    |
| 3099 | 
                        + )  | 
                    |
| 3100 | 
                        + + '\n',  | 
                    |
| 3101 | 
                        + encoding='UTF-8',  | 
                    |
| 3127 | 3102 | 
                        )  | 
                    
| 3128 | 3103 | 
                         | 
                    
| 3129 | 3104 | 
                        def raiser(*_args: Any, **_kwargs: Any) -> None:  | 
                    
| ... | ... | 
                      @@ -3134,6 +3109,7 @@ class TestCLITransition:  | 
                  
| 3134 | 3109 | 
                        )  | 
                    
| 3135 | 3110 | 
                         | 
                    
| 3136 | 3111 | 
                        monkeypatch.setattr(os, 'replace', raiser)  | 
                    
| 3112 | 
                        + monkeypatch.setattr(pathlib.Path, 'rename', raiser)  | 
                    |
| 3137 | 3113 | 
                        result_ = runner.invoke(  | 
                    
| 3138 | 3114 | 
                        cli.derivepassphrase_vault,  | 
                    
| 3139 | 3115 | 
                        ['--export', '-'],  | 
                    
| ... | ... | 
                      @@ -3161,9 +3137,8 @@ class TestCLITransition:  | 
                  
| 3161 | 3137 | 
                        ):  | 
                    
| 3162 | 3138 | 
                        old_name = cli._config_filename(subsystem='old settings.json')  | 
                    
| 3163 | 3139 | 
                        new_name = cli._config_filename(subsystem='vault')  | 
                    
| 3164 | 
                        - with contextlib.suppress(FileNotFoundError):  | 
                    |
| 3165 | 
                        - os.remove(old_name)  | 
                    |
| 3166 | 
                        - os.rename(new_name, old_name)  | 
                    |
| 3140 | 
                        + old_name.unlink(missing_ok=True)  | 
                    |
| 3141 | 
                        + new_name.rename(old_name)  | 
                    |
| 3167 | 3142 | 
                        assert cli._shell_complete_service(  | 
                    
| 3168 | 3143 | 
                        click.Context(cli.derivepassphrase),  | 
                    
| 3169 | 3144 | 
                        click.Argument(['some_parameter']),  | 
                    
| ... | ... | 
                      @@ -4183,7 +4158,7 @@ class TestShellCompletion:  | 
                  
| 4183 | 4158 | 
                        runner=runner,  | 
                    
| 4184 | 4159 | 
                                     vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    
| 4185 | 4160 | 
                        ):  | 
                    
| 4186 | 
                        - os.remove(cli._config_filename(subsystem='vault'))  | 
                    |
| 4161 | 
                        + cli._config_filename(subsystem='vault').unlink(missing_ok=True)  | 
                    |
| 4187 | 4162 | 
                        assert not cli._shell_complete_service(  | 
                    
| 4188 | 4163 | 
                        click.Context(cli.derivepassphrase),  | 
                    
| 4189 | 4164 | 
                        click.Argument(['some_parameter']),  | 
                    
| ... | ... | 
                      @@ -7,7 +7,7 @@ from __future__ import annotations  | 
                  
| 7 | 7 | 
                        import base64  | 
                    
| 8 | 8 | 
                        import contextlib  | 
                    
| 9 | 9 | 
                        import json  | 
                    
| 10 | 
                        -import os  | 
                    |
| 10 | 
                        +import pathlib  | 
                    |
| 11 | 11 | 
                        from typing import TYPE_CHECKING  | 
                    
| 12 | 12 | 
                         | 
                    
| 13 | 13 | 
                        import click.testing  | 
                    
| ... | ... | 
                      @@ -180,11 +180,12 @@ class TestCLI:  | 
                  
| 180 | 180 | 
                        vault_config='',  | 
                    
| 181 | 181 | 
                        vault_key=tests.VAULT_MASTER_KEY,  | 
                    
| 182 | 182 | 
                        ):  | 
                    
| 183 | 
                        -            os.remove('.vault')
                       | 
                    |
| 184 | 
                        -            os.mkdir('.vault')
                       | 
                    |
| 183 | 
                        +            p = pathlib.Path('.vault')
                       | 
                    |
| 184 | 
                        + p.unlink()  | 
                    |
| 185 | 
                        + p.mkdir()  | 
                    |
| 185 | 186 | 
                        result_ = runner.invoke(  | 
                    
| 186 | 187 | 
                        cli.derivepassphrase_export_vault,  | 
                    
| 187 | 
                        - ['.vault'],  | 
                    |
| 188 | 
                        + [str(p)],  | 
                    |
| 188 | 189 | 
                        )  | 
                    
| 189 | 190 | 
                        result = tests.ReadableResult.parse(result_)  | 
                    
| 190 | 191 | 
                        assert result.error_exit(  | 
                    
| ... | ... | 
                      @@ -322,10 +323,11 @@ class TestStoreroom:  | 
                  
| 322 | 323 | 
                        runner=runner,  | 
                    
| 323 | 324 | 
                        vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,  | 
                    
| 324 | 325 | 
                        ):  | 
                    
| 325 | 
                        -            with open('.vault/20', 'w', encoding='UTF-8') as outfile:
                       | 
                    |
| 326 | 
                        +            p = pathlib.Path('.vault', '20')
                       | 
                    |
| 327 | 
                        +            with p.open('w', encoding='UTF-8') as outfile:
                       | 
                    |
| 326 | 328 | 
                        print(config, file=outfile)  | 
                    
| 327 | 329 | 
                        with pytest.raises(ValueError, match='Invalid bucket file: '):  | 
                    
| 328 | 
                        -                list(storeroom._decrypt_bucket_file('.vault/20', master_keys))
                       | 
                    |
| 330 | 
                        + list(storeroom._decrypt_bucket_file(p, master_keys))  | 
                    |
| 329 | 331 | 
                         | 
                    
| 330 | 332 | 
                        @pytest.mark.parametrize(  | 
                    
| 331 | 333 | 
                        ['data', 'err_msg'],  | 
                    
| ... | ... | 
                      @@ -356,7 +358,8 @@ class TestStoreroom:  | 
                  
| 356 | 358 | 
                        vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,  | 
                    
| 357 | 359 | 
                        vault_key=tests.VAULT_MASTER_KEY,  | 
                    
| 358 | 360 | 
                        ):  | 
                    
| 359 | 
                        -            with open('.vault/.keys', 'w', encoding='UTF-8') as outfile:
                       | 
                    |
| 361 | 
                        +            p = pathlib.Path('.vault', '.keys')
                       | 
                    |
| 362 | 
                        +            with p.open('w', encoding='UTF-8') as outfile:
                       | 
                    |
| 360 | 363 | 
                        print(data, file=outfile)  | 
                    
| 361 | 364 | 
                        with pytest.raises(RuntimeError, match=err_msg):  | 
                    
| 362 | 365 | 
                        handler(format='storeroom')  | 
                    
| ... | ... | 
                      @@ -5,6 +5,7 @@  | 
                  
| 5 | 5 | 
                        from __future__ import annotations  | 
                    
| 6 | 6 | 
                         | 
                    
| 7 | 7 | 
                        import os  | 
                    
| 8 | 
                        +import pathlib  | 
                    |
| 8 | 9 | 
                        from typing import TYPE_CHECKING, Any  | 
                    
| 9 | 10 | 
                         | 
                    
| 10 | 11 | 
                        import click.testing  | 
                    
| ... | ... | 
                      @@ -65,26 +66,29 @@ class Test001ExporterUtils:  | 
                  
| 65 | 66 | 
                        @pytest.mark.parametrize(  | 
                    
| 66 | 67 | 
                        ['expected', 'path'],  | 
                    
| 67 | 68 | 
                        [  | 
                    
| 68 | 
                        -            ('/tmp', '/tmp'),
                       | 
                    |
| 69 | 
                        -            ('~', os.path.curdir),
                       | 
                    |
| 70 | 
                        -            ('~/.vault', None),
                       | 
                    |
| 69 | 
                        +            (pathlib.Path('/tmp'), pathlib.Path('/tmp')),
                       | 
                    |
| 70 | 
                        +            (pathlib.Path('~'), pathlib.Path()),
                       | 
                    |
| 71 | 
                        +            (pathlib.Path('~/.vault'), None),
                       | 
                    |
| 71 | 72 | 
                        ],  | 
                    
| 72 | 73 | 
                        )  | 
                    
| 73 | 74 | 
                        def test_210_get_vault_path(  | 
                    
| 74 | 75 | 
                        self,  | 
                    
| 75 | 76 | 
                        monkeypatch: pytest.MonkeyPatch,  | 
                    
| 76 | 
                        - expected: str,  | 
                    |
| 77 | 
                        - path: str | None,  | 
                    |
| 77 | 
                        + expected: pathlib.Path,  | 
                    |
| 78 | 
                        + path: str | os.PathLike[str] | None,  | 
                    |
| 78 | 79 | 
                        ) -> None:  | 
                    
| 79 | 80 | 
                        runner = click.testing.CliRunner(mix_stderr=False)  | 
                    
| 80 | 81 | 
                        with tests.isolated_vault_exporter_config(  | 
                    
| 81 | 82 | 
                        monkeypatch=monkeypatch, runner=runner  | 
                    
| 82 | 83 | 
                        ):  | 
                    
| 83 | 84 | 
                        if path:  | 
                    
| 84 | 
                        -                monkeypatch.setenv('VAULT_PATH', path)
                       | 
                    |
| 85 | 
                        - assert os.fsdecode(  | 
                    |
| 86 | 
                        - os.path.realpath(exporter.get_vault_path())  | 
                    |
| 87 | 
                        - ) == os.path.realpath(os.path.expanduser(expected))  | 
                    |
| 85 | 
                        + monkeypatch.setenv(  | 
                    |
| 86 | 
                        + 'VAULT_PATH', os.fspath(path) if path is not None else None  | 
                    |
| 87 | 
                        + )  | 
                    |
| 88 | 
                        + assert (  | 
                    |
| 89 | 
                        + exporter.get_vault_path().resolve()  | 
                    |
| 90 | 
                        + == expected.expanduser().resolve()  | 
                    |
| 91 | 
                        + )  | 
                    |
| 88 | 92 | 
                         | 
                    
| 89 | 93 | 
                        def test_220_register_export_vault_config_data_handler(  | 
                    
| 90 | 94 | 
                        self, monkeypatch: pytest.MonkeyPatch  | 
                    
| ... | ... | 
                      @@ -126,7 +130,11 @@ class Test001ExporterUtils:  | 
                  
| 126 | 130 | 
                        def test_310_get_vault_path_without_home(  | 
                    
| 127 | 131 | 
                        self, monkeypatch: pytest.MonkeyPatch  | 
                    
| 128 | 132 | 
                        ) -> None:  | 
                    
| 129 | 
                        - monkeypatch.setattr(os.path, 'expanduser', lambda x: x)  | 
                    |
| 133 | 
                        + def raiser(*_args: Any, **_kwargs: Any) -> Any:  | 
                    |
| 134 | 
                        +            raise RuntimeError('Cannot determine home directory.')  # noqa: EM101,TRY003
                       | 
                    |
| 135 | 
                        +  | 
                    |
| 136 | 
                        + monkeypatch.setattr(pathlib.Path, 'expanduser', raiser)  | 
                    |
| 137 | 
                        + monkeypatch.setattr(os.path, 'expanduser', raiser)  | 
                    |
| 130 | 138 | 
                        with pytest.raises(  | 
                    
| 131 | 139 | 
                        RuntimeError, match=r'[Cc]annot determine home directory'  | 
                    
| 132 | 140 | 
                        ):  | 
                    
| 133 | 141 |