diff --git a/instawow-gui/frontend/src/api.ts b/instawow-gui/frontend/src/api.ts index 1aff6e86..8ec9e2bc 100644 --- a/instawow-gui/frontend/src/api.ts +++ b/instawow-gui/frontend/src/api.ts @@ -38,7 +38,7 @@ export type Profiles = Profile[]; export type GlobalConfig = { config_dir: string; auto_update_check: boolean; - temp_dir: string; + cache_dir: string; access_tokens: { cfcore: string | null; github: string | null; diff --git a/src/instawow/cli/__init__.py b/src/instawow/cli/__init__.py index 722fba87..494ecdc8 100644 --- a/src/instawow/cli/__init__.py +++ b/src/instawow/cli/__init__.py @@ -1246,9 +1246,23 @@ async def github_oauth_flow(): return config +@cli.group('cache') +def _cache_group(): + "Manage the cache." + + +@_cache_group.command('clear') +@click.pass_obj +def cache_clear(config_ctx: ConfigBoundCtxProxy): + "Clear the instawow cache." + import shutil + + shutil.rmtree(config_ctx.config.global_config.cache_dir) + + @cli.group('debug') def _debug_group(): - "Retrieve debugging information." + "Debug instawow." @_debug_group.command('config') diff --git a/src/instawow/config/__init__.py b/src/instawow/config/__init__.py index 5c5c8ef4..35cd2773 100644 --- a/src/instawow/config/__init__.py +++ b/src/instawow/config/__init__.py @@ -5,8 +5,7 @@ from collections.abc import Callable, Iterator, Mapping, Sized from functools import lru_cache from pathlib import Path -from tempfile import gettempdir -from typing import NewType, TypeAlias, TypeVar +from typing import NewType, TypeVar import attrs import cattrs @@ -96,50 +95,58 @@ def convert_global_config(global_config: GlobalConfig): def _get_default_config_dir(): - config_parent_dir = os.environ.get('XDG_CONFIG_HOME') + parent_dir = os.environ.get('XDG_CONFIG_HOME') - if not config_parent_dir: + if not parent_dir: if sys.platform == 'darwin': - config_parent_dir = Path.home() / 'Library' / 'Application Support' + parent_dir = Path.home() / 'Library' / 'Application Support' elif sys.platform == 'win32': - config_parent_dir = os.environ.get('APPDATA') + parent_dir = os.environ.get('APPDATA') - if not config_parent_dir: - config_parent_dir = Path.home() / '.config' + if not parent_dir: + parent_dir = Path.home() / '.config' - return Path(config_parent_dir, _BOTTOM_DIR_NAME) + return Path(parent_dir, _BOTTOM_DIR_NAME) -def _get_default_temp_dir(): - return Path(gettempdir(), f'{_BOTTOM_DIR_NAME}t') +def _get_default_cache_dir(): + parent_dir = os.environ.get('XDG_CACHE_HOME') + if not parent_dir: + if sys.platform == 'darwin': + parent_dir = Path.home() / 'Library' / 'Caches' + elif sys.platform == 'win32': + parent_dir = os.environ.get('LOCALAPPDATA') -def _get_default_state_dir(): - state_parent_dir = os.environ.get('XDG_STATE_HOME') + if not parent_dir: + parent_dir = Path.home() / '.cache' - if not state_parent_dir and sys.platform not in {'darwin', 'win32'}: - state_parent_dir = Path.home() / '.local' / 'state' + return Path(parent_dir, _BOTTOM_DIR_NAME) - if not state_parent_dir: - return _get_default_config_dir() - return Path(state_parent_dir, _BOTTOM_DIR_NAME) +def _get_default_state_dir(): + parent_dir = os.environ.get('XDG_STATE_HOME') + if not parent_dir and sys.platform not in {'darwin', 'win32'}: + parent_dir = Path.home() / '.local' / 'state' -_AccessToken: TypeAlias = SecretStr | None + if not parent_dir: + return _get_default_config_dir() + + return Path(parent_dir, _BOTTOM_DIR_NAME) @fauxfrozen class _AccessTokens: - cfcore: _AccessToken = attrs.field( + cfcore: SecretStr | None = attrs.field( default=None, metadata=FieldMetadata(store=True), ) - github: _AccessToken = attrs.field( + github: SecretStr | None = attrs.field( default=None, metadata=FieldMetadata(store=True), ) - wago_addons: _AccessToken = attrs.field( + wago_addons: SecretStr | None = attrs.field( default=None, metadata=FieldMetadata(store=True), ) @@ -152,8 +159,8 @@ class GlobalConfig: converter=_expand_path, metadata=FieldMetadata(env=True), ) - temp_dir: Path = attrs.field( - factory=_get_default_temp_dir, + cache_dir: Path = attrs.field( + factory=_get_default_cache_dir, converter=_expand_path, metadata=FieldMetadata(env=True), ) @@ -203,9 +210,8 @@ def ensure_dirs(self) -> Self: ensure_dirs( [ self.config_dir, - self.temp_dir, - self.state_dir, self.cache_dir, + self.state_dir, self.http_cache_dir, self.install_cache_dir, ] @@ -217,21 +223,17 @@ def write(self) -> Self: write_config(self, self.config_file) return self - @property - def cache_dir(self) -> Path: - return self.temp_dir / 'cache' - @property def http_cache_dir(self) -> Path: - return self.temp_dir / 'cache' / '_http' + return self.cache_dir / '_http' @property def install_cache_dir(self) -> Path: - return self.temp_dir / 'cache' / '_install' + return self.cache_dir / '_install' @property def plugins_cache_dir(self) -> Path: - return self.temp_dir / 'cache' / 'plugins' + return self.cache_dir / 'plugins' @property def config_file(self) -> Path: @@ -310,7 +312,7 @@ def write(self) -> Self: return self def delete(self) -> None: - trash((self.config_dir,), dest=self.global_config.temp_dir, missing_ok=True) + trash((self.config_dir,), dest=self.global_config.cache_dir, missing_ok=True) @property def config_dir(self) -> Path: diff --git a/src/instawow/pkg_management.py b/src/instawow/pkg_management.py index bb3197cc..0df42570 100644 --- a/src/instawow/pkg_management.py +++ b/src/instawow/pkg_management.py @@ -306,7 +306,7 @@ def _install_pkg( if replace_folders: trash( (config_ctx.config.addon_dir / f for f in top_level_folders), - dest=config_ctx.config.global_config.temp_dir, + dest=config_ctx.config.global_config.cache_dir, missing_ok=True, ) else: @@ -362,7 +362,7 @@ def _update_pkg( trash( (config_ctx.config.addon_dir / f.name for f in old_pkg.folders), - dest=config_ctx.config.global_config.temp_dir, + dest=config_ctx.config.global_config.cache_dir, missing_ok=True, ) extract(config_ctx.config.addon_dir) @@ -382,7 +382,7 @@ def _remove_pkg(config_ctx: shared_ctx.ConfigBoundCtx, pkg: pkg_models.Pkg, *, k if not keep_folders: trash( (config_ctx.config.addon_dir / f.name for f in pkg.folders), - dest=config_ctx.config.global_config.temp_dir, + dest=config_ctx.config.global_config.cache_dir, missing_ok=True, ) diff --git a/tests/conftest.py b/tests/conftest.py index 8652d063..87bcd7ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import Any +from unittest import mock import pytest from _pytest.fixtures import SubRequest @@ -45,7 +46,7 @@ async def iw_aresponses( ): async with ResponsesMockServer() as server: monkeypatch.setattr('aiohttp.TCPConnector', server.tcp_connector_class) - monkeypatch.setattr('aiohttp.ClientRequest.is_ssl', lambda _: False) + monkeypatch.setattr('aiohttp.ClientRequest.is_ssl', mock.Mock(return_value=False)) yield server @@ -53,7 +54,7 @@ async def iw_aresponses( def iw_global_config_values(request: pytest.FixtureRequest, tmp_path: Path): return { 'config_dir': tmp_path / '__config__' / 'config', - 'temp_dir': tmp_path / '__config__' / 'temp', + 'cache_dir': tmp_path / '__config__' / 'cache', 'state_dir': tmp_path / '__config__' / 'state', 'access_tokens': { 'cfcore': request.param, @@ -103,7 +104,7 @@ def _iw_global_config_defaults( monkeypatch: pytest.MonkeyPatch, iw_global_config_values: dict[str, Any], ): - for key in 'config_dir', 'temp_dir', 'state_dir': + for key in 'config_dir', 'cache_dir', 'state_dir': monkeypatch.setenv(f'INSTAWOW_{key.upper()}', str(iw_global_config_values[key])) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9f2990cf..f94805e9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,7 +16,7 @@ import pytest from instawow.cli import cli -from instawow.config import ProfileConfig +from instawow.config import GlobalConfig, ProfileConfig Run: TypeAlias = Callable[[str], click.testing.Result] @@ -610,7 +610,7 @@ def test_exit_codes_with_substr_match( command: str, exit_code: int, ): - monkeypatch.setattr('instawow._utils.file.reveal_folder', lambda *_, **__: ...) + monkeypatch.setattr('instawow._utils.file.reveal_folder', mock.MagicMock()) assert install_molinari_and_run(command).exit_code == exit_code @@ -644,3 +644,12 @@ def test_plugin_hook_command_can_be_invoked( ): pytest.importorskip('instawow_test_plugin') assert run('plugins foo').output == 'success!\n' + + +def test_clear_cache( + iw_global_config: GlobalConfig, + run: Run, +): + assert iw_global_config.cache_dir.is_dir() + assert run('cache clear').exit_code == 0 + assert not iw_global_config.cache_dir.is_dir()