feat: initial Dropbox tag sync tool
CI / Tests (Linux, Python 3.12) (push) Failing after 4m51s
CI / Tests (Linux, Python 3.11) (push) Failing after 5m19s
CI / Tests (Linux, Python 3.9) (push) Failing after 5m3s
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled
CI / Tests (Linux, Python 3.12) (push) Failing after 4m51s
CI / Tests (Linux, Python 3.11) (push) Failing after 5m19s
CI / Tests (Linux, Python 3.9) (push) Failing after 5m3s
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled
Cross-platform CLI that mirrors Dropbox web tags to OS-native tags: - macOS: Finder xattrs (content-preserving, no re-upload) - Windows: XMP:Subject via ExifTool (re-upload expected once per file) Includes an interactive wizard that auto-detects the OS and Dropbox folder, walks the user through read-only token creation, previews tagged files, applies tags, and installs a background scheduler (launchd on macOS, Task Scheduler on Windows). Ships with: - 47-test suite (pytest) covering config, state DB, run_sync, writers - Gitea Actions CI for Linux / macOS / Windows with coverage reporting
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""Shared pytest fixtures and helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# --- Isolate persistent state paths from the real user's home ---------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_user_state(tmp_path, monkeypatch):
|
||||
"""Redirect all per-user state files into a tmp dir.
|
||||
|
||||
Ensures tests never clobber real settings, tokens, or state DB on the
|
||||
developer's machine.
|
||||
"""
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
|
||||
# Re-import config so its module-level paths re-evaluate against the
|
||||
# patched home. We clear and re-import to reset cached module constants.
|
||||
import importlib
|
||||
from tagsync import config
|
||||
|
||||
importlib.reload(config)
|
||||
yield
|
||||
# Leave things as-is after the test; tmp_path cleanup handles disk.
|
||||
|
||||
|
||||
# --- Fake Dropbox API objects ------------------------------------------------
|
||||
#
|
||||
# The Dropbox SDK may or may not be installed in the test env. We don't want
|
||||
# to require it, and the parts of core.py that need it are only reached via
|
||||
# helpers we can monkeypatch out.
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeFileMetadata:
|
||||
"""Stand-in for dropbox.files.FileMetadata."""
|
||||
name: str
|
||||
path_lower: str
|
||||
path_display: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeUserTag:
|
||||
tag_text: str
|
||||
|
||||
|
||||
class FakeTag:
|
||||
"""Stand-in for dropbox.files.Tag (union type)."""
|
||||
def __init__(self, tag_text: str) -> None:
|
||||
self._user = FakeUserTag(tag_text)
|
||||
|
||||
def get_user_generated_tag(self) -> FakeUserTag:
|
||||
return self._user
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakePathToTags:
|
||||
path: str
|
||||
tags: list
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeTagsResult:
|
||||
paths_to_tags: list
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patched_core(monkeypatch):
|
||||
"""Monkeypatch core's Dropbox-dependent helpers to controllable fakes.
|
||||
|
||||
Tests receive a `state` dict where they can configure:
|
||||
- `files`: list of FakeFileMetadata returned by _list_all_files
|
||||
- `tags_by_path`: dict[path_lower -> list[str]] returned by _fetch_tags
|
||||
"""
|
||||
from tagsync import core
|
||||
|
||||
state = {
|
||||
"files": [],
|
||||
"tags_by_path": {},
|
||||
"fetch_calls": 0,
|
||||
}
|
||||
|
||||
def fake_client(_token):
|
||||
return object() # placeholder; never used because list/fetch are faked
|
||||
|
||||
def fake_list(_dbx):
|
||||
return list(state["files"])
|
||||
|
||||
def fake_fetch(_dbx, files):
|
||||
state["fetch_calls"] += 1
|
||||
for f in files:
|
||||
tag_names = sorted(state["tags_by_path"].get(f.path_lower, []))
|
||||
yield f, tag_names
|
||||
|
||||
monkeypatch.setattr(core, "_dropbox_client", fake_client)
|
||||
monkeypatch.setattr(core, "_list_all_files", fake_list)
|
||||
monkeypatch.setattr(core, "_fetch_tags", fake_fetch)
|
||||
return state
|
||||
|
||||
|
||||
# --- Marker-based skips ------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
platform_skip = {
|
||||
"macos": sys.platform != "darwin",
|
||||
"windows": sys.platform != "win32",
|
||||
}
|
||||
for item in items:
|
||||
for mark_name, should_skip in platform_skip.items():
|
||||
if mark_name in item.keywords and should_skip:
|
||||
item.add_marker(
|
||||
pytest.mark.skip(reason=f"requires {mark_name}")
|
||||
)
|
||||
Reference in New Issue
Block a user