diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66148dd..1db3d93 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_stages: [commit, manual] fail_fast: true repos: - repo: "https://github.com/commitizen-tools/commitizen" - rev: v2.39.1 + rev: v2.40.0 hooks: - id: commitizen - id: commitizen-branch @@ -39,6 +39,7 @@ repos: - id: check-shebang-scripts-are-executable - id: check-symlinks - id: check-toml + exclude: broken_config_file\.toml - id: check-vcs-permalinks - id: check-xml - id: check-yaml @@ -60,7 +61,7 @@ repos: entry: yamllint --strict --config-file .yamllint.yml - repo: "https://github.com/charliermarsh/ruff-pre-commit" - rev: "v0.0.229" + rev: "v0.0.230" hooks: - id: ruff args: ["--extend-ignore", "I001,D301,D401,PLR2004"] diff --git a/README.md b/README.md index f73a1d7..66abb37 100644 --- a/README.md +++ b/README.md @@ -33,16 +33,27 @@ The script provides a menu of available actions. Make as many changes as you req ### Configuration -`obsidian-metadata` requires a configuration file at `~/.obsidian_metadata.toml`. On first run, this file will be created. Read the comments in this file to configure your preferences. This configuration file contains the following information. +`obsidian-metadata` requires a configuration file at `~/.obsidian_metadata.toml`. On first run, this file will be created. You can specify a new location for the configuration file with the `--config-file` option. + +To add additional vaults, copy the default section and add the appropriate information. The script will prompt you to select a vault if multiple exist in the configuration file + +Below is an example with two vaults. ```toml -# Path to your obsidian vault -vault = "/path/to/vault" +["Vault One"] # Name of the vault. -# Folders within the vault to ignore when indexing metadata -exclude_paths = [".git", ".obsidian"] + # Path to your obsidian vault + path = "/path/to/vault" + + # Folders within the vault to ignore when indexing metadata + exclude_paths = [".git", ".obsidian"] + +["Vault Two"] + path = "/path/to/second_vault" + exclude_paths = [".git", ".obsidian"] ``` +To bypass the configuration file and specify a vault to use at runtime use the `--vault-path` option. # Contributing diff --git a/poetry.lock b/poetry.lock index 6882a94..9391c20 100644 --- a/poetry.lock +++ b/poetry.lock @@ -699,7 +699,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -707,7 +707,7 @@ python-versions = ">=3.7" name = "tomlkit" version = "0.11.6" description = "Style preserving TOML library" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -814,7 +814,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "db4a41f0b94c3bf84fa548a6733da1cced1fedc2bdfdc6f80396dddbd5619bfd" +content-hash = "acbde0d9374261931e4f12f4ed8fcbc543008f68c4aed0a6748280a3999b3394" [metadata.files] absolufy-imports = [ diff --git a/pyproject.toml b/pyproject.toml index 97ebf6c..aa16354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ rich = "^13.2.0" ruamel-yaml = "^0.17.21" shellingham = "^1.4.0" - tomli = "^2.0.1" + tomlkit = "^0.11.6" typer = "^0.7.0" [tool.poetry.group.test.dependencies] diff --git a/src/obsidian_metadata/_config/__init__.py b/src/obsidian_metadata/_config/__init__.py index d03444b..7674d15 100644 --- a/src/obsidian_metadata/_config/__init__.py +++ b/src/obsidian_metadata/_config/__init__.py @@ -1,5 +1,5 @@ """Config module for obsidian frontmatter.""" -from obsidian_metadata._config.config import Config +from obsidian_metadata._config.config import Config, VaultConfig -__all__ = ["Config"] +__all__ = ["Config", "VaultConfig"] diff --git a/src/obsidian_metadata/_config/config.py b/src/obsidian_metadata/_config/config.py index e70d771..16fe2d1 100644 --- a/src/obsidian_metadata/_config/config.py +++ b/src/obsidian_metadata/_config/config.py @@ -1,61 +1,52 @@ """Instantiate the configuration object.""" -import re -import shutil + from pathlib import Path +from textwrap import dedent from typing import Any -import questionary import rich.repr +import tomlkit import typer -from obsidian_metadata._utils import alerts, vault_validation +from obsidian_metadata._utils import Questions, alerts from obsidian_metadata._utils.alerts import logger as log -try: - import tomllib -except ModuleNotFoundError: - import tomli as tomllib # type: ignore [no-redef] - -DEFAULT_CONFIG_FILE: Path = Path(__file__).parent / "default.toml" - @rich.repr.auto class Config: - """Configuration class.""" + """Representation of a configuration file.""" def __init__(self, config_path: Path = None, vault_path: Path = None) -> None: - self.config_path: Path = self._validate_config_path(Path(config_path)) - self.config: dict[str, Any] = self._load_config() - self.config_content: str = self.config_path.read_text() - self.vault_path: Path = self._validate_vault_path(vault_path) + + if vault_path is None: + self.config_path: Path = self._validate_config_path(Path(config_path)) + self.config: dict[str, Any] = self._load_config() + + if self.config == {}: + log.error(f"Configuration file is empty: '{self.config_path}'") + raise typer.Exit(code=1) + else: + self.config_path = None + self.config = { + "command_line_vault": {"path": vault_path, "exclude_paths": [".git", ".obsidian"]} + } try: - self.exclude_paths: list[Any] = self.config["exclude_paths"] - except KeyError: - self.exclude_paths = [] - - try: - self.metadata_location: str = self.config["metadata"]["metadata_location"] - except KeyError: - self.metadata_location = "frontmatter" - - try: - self.tags_location: str = self.config["metadata"]["tags_location"] - except KeyError: - self.tags_location = "top" + self.vaults: list[VaultConfig] = [ + VaultConfig(vault_name=key, vault_config=self.config[key]) for key in self.config + ] + except TypeError as e: + log.error(f"Configuration file is invalid: '{self.config_path}'") + raise typer.Exit(code=1) from e log.debug(f"Loaded configuration from '{self.config_path}'") log.trace(self.config) def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover - """Define rich representation of Vault.""" + """Define rich representation of the Config object.""" yield "config_path", self.config_path - yield "config_content", - yield "vault_path", self.vault_path - yield "metadata_location", self.metadata_location - yield "tags_location", self.tags_location - yield "exclude_paths", self.exclude_paths + yield "vaults", self.vaults def _validate_config_path(self, config_path: Path | None) -> Path: """Load the configuration path.""" @@ -63,7 +54,7 @@ class Config: config_path = Path(Path.home() / f".{__package__.split('.')[0]}.toml") if not config_path.exists(): - shutil.copy(DEFAULT_CONFIG_FILE, config_path) + self._write_default_config(config_path) alerts.info(f"Created default configuration file at '{config_path}'") return config_path.expanduser().resolve() @@ -71,46 +62,67 @@ class Config: def _load_config(self) -> dict[str, Any]: """Load the configuration file.""" try: - with self.config_path.open("rb") as f: - return tomllib.load(f) - except tomllib.TOMLDecodeError as e: + with open(self.config_path, encoding="utf-8") as fp: + return tomlkit.load(fp) + except tomlkit.exceptions.TOMLKitError as e: alerts.error(f"Could not parse '{self.config_path}'") raise typer.Exit(code=1) from e + def _write_default_config(self, path_to_config: Path) -> None: + """Write the default configuration file when no config file is found.""" + vault_path = Questions.ask_for_vault_path() + + config_text = f"""\ + # Add another vault by replicating this section and changing the name + ["Vault 1"] # Name of the vault. + + # Path to your obsidian vault + path = "{vault_path}" + + # Folders within the vault to ignore when indexing metadata + exclude_paths = [".git", ".obsidian"]""" + + path_to_config.write_text(dedent(config_text)) + + +@rich.repr.auto +class VaultConfig: + """Representation of a vault configuration.""" + + def __init__(self, vault_name: str, vault_config: dict) -> None: + """Initialize the vault configuration.""" + self.name: str = vault_name + self.config: dict = vault_config + + try: + self.path = self._validate_vault_path(self.config["path"]) + + Path(self.config["path"]).expanduser().resolve() + except KeyError: + self.path = None + + try: + self.exclude_paths = self.config["exclude_paths"] + except KeyError: + self.exclude_paths = [] + + def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover + """Define rich representation of a vault config.""" + yield "name", self.name + yield "config", self.config + yield "path", self.path + yield "exclude_paths", self.exclude_paths + def _validate_vault_path(self, vault_path: Path | None) -> Path: """Validate the vault path.""" - if vault_path is None: - try: - vault_path = Path(self.config["vault"]).expanduser().resolve() - except KeyError: - vault_path = Path("/I/Do/Not/Exist") + vault_path = Path(vault_path).expanduser().resolve() - if not vault_path.exists(): # pragma: no cover + if not vault_path.exists(): alerts.error(f"Vault path not found: '{vault_path}'") + raise typer.Exit(code=1) - vault_path = questionary.path( - "Enter a path to Obsidian vault:", - only_directories=True, - validate=vault_validation, - ).ask() - if vault_path is None: - raise typer.Exit(code=1) + if not vault_path.is_dir(): + alerts.error(f"Vault path is not a directory: '{vault_path}'") + raise typer.Exit(code=1) - vault_path = Path(vault_path).expanduser().resolve() - - self.write_config_value("vault", str(vault_path)) return vault_path - - def write_config_value(self, key: str, value: str | int) -> None: - """Write a new value to the configuration file. - - Args: - key (str): The key to write. - value (str|int): The value to write. - """ - self.config_content = re.sub( - rf"( *{key} = ['\"])[^'\"]*(['\"].*)", rf"\1{value}\2", self.config_content - ) - - alerts.notice(f"Writing new configuration for '{key}' to '{self.config_path}'") - self.config_path.write_text(self.config_content) diff --git a/src/obsidian_metadata/_config/default.toml b/src/obsidian_metadata/_config/default.toml deleted file mode 100644 index a6f94da..0000000 --- a/src/obsidian_metadata/_config/default.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Path to your obsidian vault -vault = "/path/to/vault" - -# Folders within the vault to ignore when indexing metadata -exclude_paths = [".git", ".obsidian"] diff --git a/src/obsidian_metadata/_utils/__init__.py b/src/obsidian_metadata/_utils/__init__.py index b00ef24..82b44b7 100644 --- a/src/obsidian_metadata/_utils/__init__.py +++ b/src/obsidian_metadata/_utils/__init__.py @@ -2,6 +2,7 @@ from obsidian_metadata._utils import alerts from obsidian_metadata._utils.alerts import LoggerManager +from obsidian_metadata._utils.questions import Questions from obsidian_metadata._utils.utilities import ( clean_dictionary, clear_screen, @@ -9,7 +10,6 @@ from obsidian_metadata._utils.utilities import ( dict_values_to_lists_strings, docstring_parameter, remove_markdown_sections, - vault_validation, version_callback, ) @@ -21,6 +21,7 @@ __all__ = [ "dict_contains", "docstring_parameter", "LoggerManager", + "Questions", "remove_markdown_sections", "vault_validation", "version_callback", diff --git a/src/obsidian_metadata/_utils/questions.py b/src/obsidian_metadata/_utils/questions.py new file mode 100644 index 0000000..01ad6ee --- /dev/null +++ b/src/obsidian_metadata/_utils/questions.py @@ -0,0 +1,33 @@ +"""Functions for asking questions to the user and validating responses.""" +from pathlib import Path + +import questionary +import typer + + +class Questions: + """Class for asking questions to the user and validating responses.""" + + @staticmethod + def ask_for_vault_path() -> Path: # pragma: no cover + """Ask the user for the path to their vault.""" + vault_path = questionary.path( + "Enter a path to Obsidian vault:", + only_directories=True, + validate=Questions._validate_vault, + ).ask() + if vault_path is None: + raise typer.Exit(code=1) + + return Path(vault_path).expanduser().resolve() + + @staticmethod + def _validate_vault(path: str) -> bool | str: + """Validates the vault path.""" + path_to_validate: Path = Path(path).expanduser().resolve() + if not path_to_validate.exists(): + return f"Path does not exist: {path_to_validate}" + if not path_to_validate.is_dir(): + return f"Path is not a directory: {path_to_validate}" + + return True diff --git a/src/obsidian_metadata/_utils/utilities.py b/src/obsidian_metadata/_utils/utilities.py index 8bedcf8..5667355 100644 --- a/src/obsidian_metadata/_utils/utilities.py +++ b/src/obsidian_metadata/_utils/utilities.py @@ -1,7 +1,6 @@ """Utility functions.""" import re from os import name, system -from pathlib import Path from typing import Any import typer @@ -83,17 +82,6 @@ def version_callback(value: bool) -> None: raise typer.Exit() -def vault_validation(path: str) -> bool | str: - """Validates the vault path.""" - path_to_validate: Path = Path(path).expanduser().resolve() - if not path_to_validate.exists(): - return f"Path does not exist: {path_to_validate}" - if not path_to_validate.is_dir(): - return f"Path is not a directory: {path_to_validate}" - - return True - - def docstring_parameter(*sub: Any) -> Any: """Decorator to replace variables within docstrings. diff --git a/src/obsidian_metadata/cli.py b/src/obsidian_metadata/cli.py index e3cf36f..1d1636b 100644 --- a/src/obsidian_metadata/cli.py +++ b/src/obsidian_metadata/cli.py @@ -4,11 +4,17 @@ from pathlib import Path from typing import Optional +import questionary import typer from rich import print from obsidian_metadata._config import Config -from obsidian_metadata._utils import alerts, docstring_parameter, version_callback +from obsidian_metadata._utils import ( + alerts, + clear_screen, + docstring_parameter, + version_callback, +) from obsidian_metadata.models import Application app = typer.Typer(add_completion=False, no_args_is_help=True, rich_markup_mode="rich") @@ -95,9 +101,6 @@ def main( log_to_file, ) - config: Config = Config(config_path=config_file, vault_path=vault_path) - application = Application(dry_run=dry_run, config=config) - banner = r""" ___ _ _ _ _ / _ \| |__ ___(_) __| (_) __ _ _ __ @@ -109,7 +112,28 @@ def main( | | | | __/ || (_| | (_| | (_| | || (_| | |_| |_|\___|\__\__,_|\__,_|\__,_|\__\__,_| """ + clear_screen() print(banner) + + config: Config = Config(config_path=config_file, vault_path=vault_path) + if len(config.vaults) == 0: + typer.echo("No vaults configured. Exiting.") + raise typer.Exit(1) + + if len(config.vaults) == 1: + application = Application(dry_run=dry_run, config=config.vaults[0]) + else: + vault_names = [vault.name for vault in config.vaults] + vault_name = questionary.select( + "Select a vault to process:", + choices=vault_names, + ).ask() + if vault_name is None: + raise typer.Exit(code=1) + + vault_to_use = next(vault for vault in config.vaults if vault.name == vault_name) + application = Application(dry_run=dry_run, config=vault_to_use) + application.main_app() diff --git a/src/obsidian_metadata/models/application.py b/src/obsidian_metadata/models/application.py index de25aa0..f2d7c1d 100644 --- a/src/obsidian_metadata/models/application.py +++ b/src/obsidian_metadata/models/application.py @@ -6,8 +6,8 @@ from typing import Any import questionary from rich import print -from obsidian_metadata._config import Config -from obsidian_metadata._utils import alerts, clear_screen +from obsidian_metadata._config import VaultConfig +from obsidian_metadata._utils import alerts from obsidian_metadata._utils.alerts import logger as log from obsidian_metadata.models import Patterns, Vault @@ -22,7 +22,7 @@ class Application: More info: https://questionary.readthedocs.io/en/stable/pages/advanced.html#create-questions-from-dictionaries """ - def __init__(self, config: Config, dry_run: bool) -> None: + def __init__(self, config: VaultConfig, dry_run: bool) -> None: self.config = config self.dry_run = dry_run self.custom_style = questionary.Style( @@ -45,7 +45,6 @@ class Application: def main_app(self) -> None: # noqa: C901 """Questions for the main application.""" - clear_screen() self.load_vault() while True: diff --git a/src/obsidian_metadata/models/vault.py b/src/obsidian_metadata/models/vault.py index dbe99bf..1891728 100644 --- a/src/obsidian_metadata/models/vault.py +++ b/src/obsidian_metadata/models/vault.py @@ -10,7 +10,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from rich.prompt import Confirm from rich.table import Table -from obsidian_metadata._config import Config +from obsidian_metadata._config import VaultConfig from obsidian_metadata._utils import alerts from obsidian_metadata._utils.alerts import logger as log from obsidian_metadata.models import Note, VaultMetadata @@ -27,8 +27,8 @@ class Vault: notes (list[Note]): List of all notes in the vault. """ - def __init__(self, config: Config, dry_run: bool = False, path_filter: str = None): - self.vault_path: Path = config.vault_path + def __init__(self, config: VaultConfig, dry_run: bool = False, path_filter: str = None): + self.vault_path: Path = config.path self.dry_run: bool = dry_run self.backup_path: Path = self.vault_path.parent / f"{self.vault_path.name}.bak" self.exclude_paths: list[Path] = [] diff --git a/tests/application_test.py b/tests/application_test.py index 6f528ea..976d43d 100644 --- a/tests/application_test.py +++ b/tests/application_test.py @@ -10,9 +10,10 @@ def test_load_vault(test_vault) -> None: """Test application.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - app = Application(config=config, dry_run=False) + vault_config = config.vaults[0] + app = Application(config=vault_config, dry_run=False) app.load_vault() assert app.dry_run is False - assert app.config == config - assert app.vault.num_notes() == 2 + assert app.config == vault_config + assert app.vault.num_notes() == 3 diff --git a/tests/config_test.py b/tests/config_test.py index 3873bc6..39dd8b6 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -1,28 +1,109 @@ # type: ignore """Tests for the configuration module.""" -import re from pathlib import Path +from textwrap import dedent + +import pytest +import typer from obsidian_metadata._config import Config -def test_first_run(tmp_path): - """Test creating a config on first run.""" - config_file = Path(tmp_path / "config.toml") - vault_path = Path(tmp_path / "vault/") - vault_path.mkdir() +def test_broken_config_file(capsys) -> None: + """Test loading a broken config file.""" + config_file = Path("tests/fixtures/broken_config_file.toml") - config = Config(config_path=config_file, vault_path=vault_path) + with pytest.raises(typer.Exit): + Config(config_path=config_file) + captured = capsys.readouterr() + assert "Could not parse" in captured.out + + +def test_vault_path_errors(tmp_path, capsys) -> None: + """Test loading a config file with a vault path that doesn't exist.""" + config_file = Path(tmp_path / "config.toml") + with pytest.raises(typer.Exit): + Config(config_path=config_file, vault_path=Path("tests/fixtures/does_not_exist")) + captured = capsys.readouterr() + assert "Vault path not found" in captured.out + + with pytest.raises(typer.Exit): + Config(config_path=config_file, vault_path=Path("tests/fixtures/sample_note.md")) + captured = capsys.readouterr() + assert "Vault path is not a directory" in captured.out + + +def test_multiple_vaults_okay() -> None: + """Test multiple vaults.""" + config_file = Path("tests/fixtures/multiple_vaults.toml") + + config = Config(config_path=config_file) + assert config.config == { + "Sample Vault": { + "exclude_paths": [".git", ".obsidian", "ignore_folder"], + "path": "tests/fixtures/sample_vault", + }, + "Test Vault": { + "exclude_paths": [".git", ".obsidian", "ignore_folder"], + "path": "tests/fixtures/test_vault", + }, + } + assert len(config.vaults) == 2 + assert config.vaults[0].name == "Sample Vault" + assert config.vaults[0].path == Path("tests/fixtures/sample_vault").expanduser().resolve() + assert config.vaults[0].exclude_paths == [".git", ".obsidian", "ignore_folder"] + assert config.vaults[1].name == "Test Vault" + assert config.vaults[1].path == Path("tests/fixtures/test_vault").expanduser().resolve() + assert config.vaults[1].exclude_paths == [".git", ".obsidian", "ignore_folder"] + + +def test_single_vault() -> None: + """Test multiple vaults.""" + config_file = Path("tests/fixtures/test_vault_config.toml") + + config = Config(config_path=config_file) + assert config.config == { + "Test Vault": { + "exclude_paths": [".git", ".obsidian", "ignore_folder"], + "path": "tests/fixtures/test_vault", + } + } + assert len(config.vaults) == 1 + assert config.vaults[0].name == "Test Vault" + assert config.vaults[0].path == Path("tests/fixtures/test_vault").expanduser().resolve() + assert config.vaults[0].exclude_paths == [".git", ".obsidian", "ignore_folder"] + + +def test_no_config_no_vault(tmp_path, mocker) -> None: + """Test creating a config on first run.""" + fake_vault = Path(tmp_path / "vault") + fake_vault.mkdir() + mocker.patch( + "obsidian_metadata._config.config.Questions.ask_for_vault_path", return_value=fake_vault + ) + + config_file = Path(tmp_path / "config.toml") + Config(config_path=config_file) + + content = config_file.read_text() + sample_config = f"""\ + # Add another vault by replicating this section and changing the name + ["Vault 1"] # Name of the vault. + + # Path to your obsidian vault + path = "{str(fake_vault)}" + + # Folders within the vault to ignore when indexing metadata + exclude_paths = [".git", ".obsidian"]""" assert config_file.exists() is True - config.write_config_value("vault", str(vault_path)) - content = config_file.read_text() - assert config.vault_path == vault_path - assert re.search(str(vault_path), content) is not None + assert content == dedent(sample_config) - -def test_parse_config(): - """Test parsing a config file.""" - config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=None) - assert config.vault_path == Path(Path.cwd() / "tests/fixtures/test_vault") + new_config = Config(config_path=config_file) + assert new_config.config == { + "Vault 1": { + "path": str(fake_vault), + "exclude_paths": [".git", ".obsidian"], + } + } diff --git a/tests/fixtures/broken_config_file.toml b/tests/fixtures/broken_config_file.toml new file mode 100644 index 0000000..29568cb --- /dev/null +++ b/tests/fixtures/broken_config_file.toml @@ -0,0 +1,6 @@ +["Sample Vault] + exclude_paths = [".git", ".obsidian", "ignore_folder"] + path = "tests/fixtures/sample_vault" +["Test Vault"] + exclude_paths = [".git", ".obsidian", "ignore_folder"] + path = "tests/fixtures/test_vault" diff --git a/tests/fixtures/multiple_vaults.toml b/tests/fixtures/multiple_vaults.toml new file mode 100644 index 0000000..034276e --- /dev/null +++ b/tests/fixtures/multiple_vaults.toml @@ -0,0 +1,6 @@ +["Sample Vault"] + exclude_paths = [".git", ".obsidian", "ignore_folder"] + path = "tests/fixtures/sample_vault" +["Test Vault"] + exclude_paths = [".git", ".obsidian", "ignore_folder"] + path = "tests/fixtures/test_vault" diff --git a/tests/fixtures/sample_vault_config.toml b/tests/fixtures/sample_vault_config.toml index 208e62f..a904439 100644 --- a/tests/fixtures/sample_vault_config.toml +++ b/tests/fixtures/sample_vault_config.toml @@ -1,8 +1,3 @@ -vault = "tests/fixtures/sample_vault" - -# folders to ignore when parsing content -exclude_paths = [".git", ".obsidian", "ignore_folder"] - -[metadata] - metadata_location = "frontmatter" # "frontmatter", "top", "bottom" - tags_location = "top" # "frontmatter", "top", "bottom" +["Sample Vault"] + exclude_paths = [".git", ".obsidian", "ignore_folder"] + path = "tests/fixtures/sample_vault" diff --git a/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md b/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md index 971f792..5aa71d3 100644 --- a/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md +++ b/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md @@ -8,6 +8,7 @@ tags: - ignored_file_tag1 author: author name type: ["article", "note"] +ignored_frontmatter: ignore_me --- #inline_tag_top1 #inline_tag_top2 #ignored_file_tag2 diff --git a/tests/fixtures/test_vault_config.toml b/tests/fixtures/test_vault_config.toml index de72145..044336e 100644 --- a/tests/fixtures/test_vault_config.toml +++ b/tests/fixtures/test_vault_config.toml @@ -1,8 +1,3 @@ -vault = "tests/fixtures/test_vault" - -# folders to ignore when parsing content -exclude_paths = [".git", ".obsidian", "ignore_folder"] - -[metadata] - metadata_location = "frontmatter" # "frontmatter", "top", "bottom" - tags_location = "top" # "frontmatter", "top", "bottom" +["Test Vault"] + exclude_paths = [".git", ".obsidian", "ignore_folder"] + path = "tests/fixtures/test_vault" diff --git a/tests/questions_test.py b/tests/questions_test.py new file mode 100644 index 0000000..71b4ace --- /dev/null +++ b/tests/questions_test.py @@ -0,0 +1,12 @@ +# type: ignore +"""Test the questions class.""" + + +from obsidian_metadata._utils import Questions + + +def test_vault_validation(): + """Test vault validation.""" + assert Questions._validate_vault("tests/") is True + assert "Path is not a directory" in Questions._validate_vault("pyproject.toml") + assert "Path does not exist" in Questions._validate_vault("tests/vault2") diff --git a/tests/utilities_test.py b/tests/utilities_test.py index 2ea83ed..d487c0f 100644 --- a/tests/utilities_test.py +++ b/tests/utilities_test.py @@ -7,7 +7,6 @@ from obsidian_metadata._utils import ( dict_contains, dict_values_to_lists_strings, remove_markdown_sections, - vault_validation, ) @@ -67,13 +66,6 @@ def test_dict_values_to_lists_strings(): } -def test_vault_validation(): - """Test vault validation.""" - assert vault_validation("tests/") is True - assert "Path is not a directory" in vault_validation("pyproject.toml") - assert "Path does not exist" in vault_validation("tests/vault2") - - def test_remove_markdown_sections(): """Test removing markdown sections.""" text: str = """ diff --git a/tests/vault_test.py b/tests/vault_test.py index 461a0a4..226fd7e 100644 --- a/tests/vault_test.py +++ b/tests/vault_test.py @@ -12,16 +12,18 @@ def test_vault_creation(test_vault): """Test creating a Vault object.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.vault_path == vault_path assert vault.backup_path == Path(f"{vault_path}.bak") assert vault.dry_run is False assert str(vault.exclude_paths[0]) == Regex(r".*\.git") - assert vault.num_notes() == 2 + assert vault.num_notes() == 3 assert vault.metadata.dict == { "Inline Tags": [ + "ignored_file_tag2", "inline_tag_bottom1", "inline_tag_bottom2", "inline_tag_top1", @@ -30,24 +32,29 @@ def test_vault_creation(test_vault): "intext_tag2", "shared_tag", ], + "author": ["author name"], "bottom_key1": ["bottom_key1_value"], "bottom_key2": ["bottom_key2_value"], "date_created": ["2022-12-22"], "emoji_📅_key": ["emoji_📅_key_value"], "frontmatter_Key1": ["author name"], "frontmatter_Key2": ["article", "note"], + "ignored_frontmatter": ["ignore_me"], "intext_key": ["intext_value"], "shared_key1": ["shared_key1_value"], "shared_key2": ["shared_key2_value1", "shared_key2_value2"], "tags": [ "frontmatter_tag1", "frontmatter_tag2", + "frontmatter_tag3", + "ignored_file_tag1", "shared_tag", "📅/frontmatter_tag3", ], "top_key1": ["top_key1_value"], "top_key2": ["top_key2_value"], "top_key3": ["top_key3_value_as_link"], + "type": ["article", "note"], } @@ -55,13 +62,15 @@ def test_get_filtered_notes(sample_vault) -> None: """Test filtering notes.""" vault_path = sample_vault config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config, path_filter="front") + vault_config = config.vaults[0] + vault = Vault(config=vault_config, path_filter="front") assert vault.num_notes() == 4 vault_path = sample_vault config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) - vault2 = Vault(config=config, path_filter="mixed") + vault_config = config.vaults[0] + vault2 = Vault(config=vault_config, path_filter="mixed") assert vault2.num_notes() == 1 @@ -70,7 +79,8 @@ def test_backup(test_vault, capsys): """Test backing up the vault.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config, dry_run=False) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) vault.backup() @@ -88,7 +98,8 @@ def test_backup_dryrun(test_vault, capsys): """Test backing up the vault.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config, dry_run=True) + vault_config = config.vaults[0] + vault = Vault(config=vault_config, dry_run=True) print(f"vault.dry_run: {vault.dry_run}") vault.backup() @@ -102,7 +113,8 @@ def test_delete_backup(test_vault, capsys): """Test deleting the vault backup.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config, dry_run=False) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) vault.backup() vault.delete_backup() @@ -121,7 +133,8 @@ def test_delete_backup_dryrun(test_vault, capsys): """Test deleting the vault backup.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config, dry_run=True) + vault_config = config.vaults[0] + vault = Vault(config=vault_config, dry_run=True) Path.mkdir(vault.backup_path) vault.delete_backup() @@ -135,7 +148,8 @@ def test_info(test_vault, capsys): """Test printing vault information.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) vault.info() @@ -149,7 +163,8 @@ def test_contains_inline_tag(test_vault) -> None: """Test if the vault contains an inline tag.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.contains_inline_tag("tag") is False assert vault.contains_inline_tag("intext_tag2") is True @@ -159,7 +174,8 @@ def test_contains_metadata(test_vault) -> None: """Test if the vault contains a metadata key.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.contains_metadata("key") is False assert vault.contains_metadata("top_key1") is True @@ -171,11 +187,13 @@ def test_delete_inline_tag(test_vault) -> None: """Test deleting an inline tag.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.delete_inline_tag("no tag") is False assert vault.delete_inline_tag("intext_tag2") is True assert vault.metadata.dict["Inline Tags"] == [ + "ignored_file_tag2", "inline_tag_bottom1", "inline_tag_bottom2", "inline_tag_top1", @@ -189,15 +207,16 @@ def test_delete_metadata(test_vault) -> None: """Test deleting a metadata key/value.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.delete_metadata("no key") == 0 assert vault.delete_metadata("top_key1", "no_value") == 0 - assert vault.delete_metadata("top_key1", "top_key1_value") == 1 + assert vault.delete_metadata("top_key1", "top_key1_value") == 2 assert vault.metadata.dict["top_key1"] == [] - assert vault.delete_metadata("top_key2") == 1 + assert vault.delete_metadata("top_key2") == 2 assert "top_key2" not in vault.metadata.dict @@ -205,11 +224,13 @@ def test_rename_inline_tag(test_vault) -> None: """Test renaming an inline tag.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.rename_inline_tag("no tag", "new_tag") is False assert vault.rename_inline_tag("intext_tag2", "new_tag") is True assert vault.metadata.dict["Inline Tags"] == [ + "ignored_file_tag2", "inline_tag_bottom1", "inline_tag_bottom2", "inline_tag_top1", @@ -224,7 +245,8 @@ def test_rename_metadata(test_vault) -> None: """Test renaming a metadata key/value.""" vault_path = test_vault config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) - vault = Vault(config=config) + vault_config = config.vaults[0] + vault = Vault(config=vault_config) assert vault.rename_metadata("no key", "new_key") is False assert vault.rename_metadata("tags", "nonexistent_value", "new_vaule") is False @@ -232,6 +254,8 @@ def test_rename_metadata(test_vault) -> None: assert vault.rename_metadata("tags", "frontmatter_tag1", "new_vaule") is True assert vault.metadata.dict["tags"] == [ "frontmatter_tag2", + "frontmatter_tag3", + "ignored_file_tag1", "new_vaule", "shared_tag", "📅/frontmatter_tag3", @@ -241,6 +265,8 @@ def test_rename_metadata(test_vault) -> None: assert "tags" not in vault.metadata.dict assert vault.metadata.dict["new_key"] == [ "frontmatter_tag2", + "frontmatter_tag3", + "ignored_file_tag1", "new_vaule", "shared_tag", "📅/frontmatter_tag3",