feat(configuration): support multiple vaults in the configuration file (#6)

When multiple vaults are added to the configuration file, the script will prompt 
you to select one at runtime
This commit is contained in:
Nathaniel Landau
2023-01-24 10:32:56 -05:00
committed by GitHub
parent 5abab2ad20
commit 1e4fbcb4e2
23 changed files with 350 additions and 171 deletions

View File

@@ -5,7 +5,7 @@ default_stages: [commit, manual]
fail_fast: true fail_fast: true
repos: repos:
- repo: "https://github.com/commitizen-tools/commitizen" - repo: "https://github.com/commitizen-tools/commitizen"
rev: v2.39.1 rev: v2.40.0
hooks: hooks:
- id: commitizen - id: commitizen
- id: commitizen-branch - id: commitizen-branch
@@ -39,6 +39,7 @@ repos:
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
- id: check-symlinks - id: check-symlinks
- id: check-toml - id: check-toml
exclude: broken_config_file\.toml
- id: check-vcs-permalinks - id: check-vcs-permalinks
- id: check-xml - id: check-xml
- id: check-yaml - id: check-yaml
@@ -60,7 +61,7 @@ repos:
entry: yamllint --strict --config-file .yamllint.yml entry: yamllint --strict --config-file .yamllint.yml
- repo: "https://github.com/charliermarsh/ruff-pre-commit" - repo: "https://github.com/charliermarsh/ruff-pre-commit"
rev: "v0.0.229" rev: "v0.0.230"
hooks: hooks:
- id: ruff - id: ruff
args: ["--extend-ignore", "I001,D301,D401,PLR2004"] args: ["--extend-ignore", "I001,D301,D401,PLR2004"]

View File

@@ -33,16 +33,27 @@ The script provides a menu of available actions. Make as many changes as you req
### Configuration ### 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 ```toml
# Path to your obsidian vault ["Vault One"] # Name of the vault.
vault = "/path/to/vault"
# Folders within the vault to ignore when indexing metadata # Path to your obsidian vault
exclude_paths = [".git", ".obsidian"] 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 # Contributing

6
poetry.lock generated
View File

@@ -699,7 +699,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "main" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@@ -707,7 +707,7 @@ python-versions = ">=3.7"
name = "tomlkit" name = "tomlkit"
version = "0.11.6" version = "0.11.6"
description = "Style preserving TOML library" description = "Style preserving TOML library"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@@ -814,7 +814,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "db4a41f0b94c3bf84fa548a6733da1cced1fedc2bdfdc6f80396dddbd5619bfd" content-hash = "acbde0d9374261931e4f12f4ed8fcbc543008f68c4aed0a6748280a3999b3394"
[metadata.files] [metadata.files]
absolufy-imports = [ absolufy-imports = [

View File

@@ -23,7 +23,7 @@
rich = "^13.2.0" rich = "^13.2.0"
ruamel-yaml = "^0.17.21" ruamel-yaml = "^0.17.21"
shellingham = "^1.4.0" shellingham = "^1.4.0"
tomli = "^2.0.1" tomlkit = "^0.11.6"
typer = "^0.7.0" typer = "^0.7.0"
[tool.poetry.group.test.dependencies] [tool.poetry.group.test.dependencies]

View File

@@ -1,5 +1,5 @@
"""Config module for obsidian frontmatter.""" """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"]

View File

@@ -1,61 +1,52 @@
"""Instantiate the configuration object.""" """Instantiate the configuration object."""
import re
import shutil
from pathlib import Path from pathlib import Path
from textwrap import dedent
from typing import Any from typing import Any
import questionary
import rich.repr import rich.repr
import tomlkit
import typer 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 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 @rich.repr.auto
class Config: class Config:
"""Configuration class.""" """Representation of a configuration file."""
def __init__(self, config_path: Path = None, vault_path: Path = None) -> None: 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() if vault_path is None:
self.config_content: str = self.config_path.read_text() self.config_path: Path = self._validate_config_path(Path(config_path))
self.vault_path: Path = self._validate_vault_path(vault_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: try:
self.exclude_paths: list[Any] = self.config["exclude_paths"] self.vaults: list[VaultConfig] = [
except KeyError: VaultConfig(vault_name=key, vault_config=self.config[key]) for key in self.config
self.exclude_paths = [] ]
except TypeError as e:
try: log.error(f"Configuration file is invalid: '{self.config_path}'")
self.metadata_location: str = self.config["metadata"]["metadata_location"] raise typer.Exit(code=1) from e
except KeyError:
self.metadata_location = "frontmatter"
try:
self.tags_location: str = self.config["metadata"]["tags_location"]
except KeyError:
self.tags_location = "top"
log.debug(f"Loaded configuration from '{self.config_path}'") log.debug(f"Loaded configuration from '{self.config_path}'")
log.trace(self.config) log.trace(self.config)
def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover 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_path", self.config_path
yield "config_content", yield "vaults", self.vaults
yield "vault_path", self.vault_path
yield "metadata_location", self.metadata_location
yield "tags_location", self.tags_location
yield "exclude_paths", self.exclude_paths
def _validate_config_path(self, config_path: Path | None) -> Path: def _validate_config_path(self, config_path: Path | None) -> Path:
"""Load the configuration path.""" """Load the configuration path."""
@@ -63,7 +54,7 @@ class Config:
config_path = Path(Path.home() / f".{__package__.split('.')[0]}.toml") config_path = Path(Path.home() / f".{__package__.split('.')[0]}.toml")
if not config_path.exists(): 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}'") alerts.info(f"Created default configuration file at '{config_path}'")
return config_path.expanduser().resolve() return config_path.expanduser().resolve()
@@ -71,46 +62,67 @@ class Config:
def _load_config(self) -> dict[str, Any]: def _load_config(self) -> dict[str, Any]:
"""Load the configuration file.""" """Load the configuration file."""
try: try:
with self.config_path.open("rb") as f: with open(self.config_path, encoding="utf-8") as fp:
return tomllib.load(f) return tomlkit.load(fp)
except tomllib.TOMLDecodeError as e: except tomlkit.exceptions.TOMLKitError as e:
alerts.error(f"Could not parse '{self.config_path}'") alerts.error(f"Could not parse '{self.config_path}'")
raise typer.Exit(code=1) from e 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: def _validate_vault_path(self, vault_path: Path | None) -> Path:
"""Validate the vault path.""" """Validate the vault path."""
if vault_path is None: vault_path = Path(vault_path).expanduser().resolve()
try:
vault_path = Path(self.config["vault"]).expanduser().resolve()
except KeyError:
vault_path = Path("/I/Do/Not/Exist")
if not vault_path.exists(): # pragma: no cover if not vault_path.exists():
alerts.error(f"Vault path not found: '{vault_path}'") alerts.error(f"Vault path not found: '{vault_path}'")
raise typer.Exit(code=1)
vault_path = questionary.path( if not vault_path.is_dir():
"Enter a path to Obsidian vault:", alerts.error(f"Vault path is not a directory: '{vault_path}'")
only_directories=True, raise typer.Exit(code=1)
validate=vault_validation,
).ask()
if vault_path is None:
raise typer.Exit(code=1)
vault_path = Path(vault_path).expanduser().resolve()
self.write_config_value("vault", str(vault_path))
return 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)

View File

@@ -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"]

View File

@@ -2,6 +2,7 @@
from obsidian_metadata._utils import alerts from obsidian_metadata._utils import alerts
from obsidian_metadata._utils.alerts import LoggerManager from obsidian_metadata._utils.alerts import LoggerManager
from obsidian_metadata._utils.questions import Questions
from obsidian_metadata._utils.utilities import ( from obsidian_metadata._utils.utilities import (
clean_dictionary, clean_dictionary,
clear_screen, clear_screen,
@@ -9,7 +10,6 @@ from obsidian_metadata._utils.utilities import (
dict_values_to_lists_strings, dict_values_to_lists_strings,
docstring_parameter, docstring_parameter,
remove_markdown_sections, remove_markdown_sections,
vault_validation,
version_callback, version_callback,
) )
@@ -21,6 +21,7 @@ __all__ = [
"dict_contains", "dict_contains",
"docstring_parameter", "docstring_parameter",
"LoggerManager", "LoggerManager",
"Questions",
"remove_markdown_sections", "remove_markdown_sections",
"vault_validation", "vault_validation",
"version_callback", "version_callback",

View File

@@ -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

View File

@@ -1,7 +1,6 @@
"""Utility functions.""" """Utility functions."""
import re import re
from os import name, system from os import name, system
from pathlib import Path
from typing import Any from typing import Any
import typer import typer
@@ -83,17 +82,6 @@ def version_callback(value: bool) -> None:
raise typer.Exit() 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: def docstring_parameter(*sub: Any) -> Any:
"""Decorator to replace variables within docstrings. """Decorator to replace variables within docstrings.

View File

@@ -4,11 +4,17 @@
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import questionary
import typer import typer
from rich import print from rich import print
from obsidian_metadata._config import Config 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 from obsidian_metadata.models import Application
app = typer.Typer(add_completion=False, no_args_is_help=True, rich_markup_mode="rich") app = typer.Typer(add_completion=False, no_args_is_help=True, rich_markup_mode="rich")
@@ -95,9 +101,6 @@ def main(
log_to_file, log_to_file,
) )
config: Config = Config(config_path=config_file, vault_path=vault_path)
application = Application(dry_run=dry_run, config=config)
banner = r""" banner = r"""
___ _ _ _ _ ___ _ _ _ _
/ _ \| |__ ___(_) __| (_) __ _ _ __ / _ \| |__ ___(_) __| (_) __ _ _ __
@@ -109,7 +112,28 @@ def main(
| | | | __/ || (_| | (_| | (_| | || (_| | | | | | __/ || (_| | (_| | (_| | || (_| |
|_| |_|\___|\__\__,_|\__,_|\__,_|\__\__,_| |_| |_|\___|\__\__,_|\__,_|\__,_|\__\__,_|
""" """
clear_screen()
print(banner) 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() application.main_app()

View File

@@ -6,8 +6,8 @@ from typing import Any
import questionary import questionary
from rich import print from rich import print
from obsidian_metadata._config import Config from obsidian_metadata._config import VaultConfig
from obsidian_metadata._utils import alerts, clear_screen from obsidian_metadata._utils import alerts
from obsidian_metadata._utils.alerts import logger as log from obsidian_metadata._utils.alerts import logger as log
from obsidian_metadata.models import Patterns, Vault 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 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.config = config
self.dry_run = dry_run self.dry_run = dry_run
self.custom_style = questionary.Style( self.custom_style = questionary.Style(
@@ -45,7 +45,6 @@ class Application:
def main_app(self) -> None: # noqa: C901 def main_app(self) -> None: # noqa: C901
"""Questions for the main application.""" """Questions for the main application."""
clear_screen()
self.load_vault() self.load_vault()
while True: while True:

View File

@@ -10,7 +10,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.prompt import Confirm from rich.prompt import Confirm
from rich.table import Table 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 import alerts
from obsidian_metadata._utils.alerts import logger as log from obsidian_metadata._utils.alerts import logger as log
from obsidian_metadata.models import Note, VaultMetadata from obsidian_metadata.models import Note, VaultMetadata
@@ -27,8 +27,8 @@ class Vault:
notes (list[Note]): List of all notes in the vault. notes (list[Note]): List of all notes in the vault.
""" """
def __init__(self, config: Config, dry_run: bool = False, path_filter: str = None): def __init__(self, config: VaultConfig, dry_run: bool = False, path_filter: str = None):
self.vault_path: Path = config.vault_path self.vault_path: Path = config.path
self.dry_run: bool = dry_run self.dry_run: bool = dry_run
self.backup_path: Path = self.vault_path.parent / f"{self.vault_path.name}.bak" self.backup_path: Path = self.vault_path.parent / f"{self.vault_path.name}.bak"
self.exclude_paths: list[Path] = [] self.exclude_paths: list[Path] = []

View File

@@ -10,9 +10,10 @@ def test_load_vault(test_vault) -> None:
"""Test application.""" """Test application."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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() app.load_vault()
assert app.dry_run is False assert app.dry_run is False
assert app.config == config assert app.config == vault_config
assert app.vault.num_notes() == 2 assert app.vault.num_notes() == 3

View File

@@ -1,28 +1,109 @@
# type: ignore # type: ignore
"""Tests for the configuration module.""" """Tests for the configuration module."""
import re
from pathlib import Path from pathlib import Path
from textwrap import dedent
import pytest
import typer
from obsidian_metadata._config import Config from obsidian_metadata._config import Config
def test_first_run(tmp_path): def test_broken_config_file(capsys) -> None:
"""Test creating a config on first run.""" """Test loading a broken config file."""
config_file = Path(tmp_path / "config.toml") config_file = Path("tests/fixtures/broken_config_file.toml")
vault_path = Path(tmp_path / "vault/")
vault_path.mkdir()
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 assert config_file.exists() is True
config.write_config_value("vault", str(vault_path)) assert content == dedent(sample_config)
content = config_file.read_text()
assert config.vault_path == vault_path
assert re.search(str(vault_path), content) is not None
new_config = Config(config_path=config_file)
def test_parse_config(): assert new_config.config == {
"""Test parsing a config file.""" "Vault 1": {
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=None) "path": str(fake_vault),
assert config.vault_path == Path(Path.cwd() / "tests/fixtures/test_vault") "exclude_paths": [".git", ".obsidian"],
}
}

View File

@@ -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"

6
tests/fixtures/multiple_vaults.toml vendored Normal file
View File

@@ -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"

View File

@@ -1,8 +1,3 @@
vault = "tests/fixtures/sample_vault" ["Sample Vault"]
exclude_paths = [".git", ".obsidian", "ignore_folder"]
# folders to ignore when parsing content path = "tests/fixtures/sample_vault"
exclude_paths = [".git", ".obsidian", "ignore_folder"]
[metadata]
metadata_location = "frontmatter" # "frontmatter", "top", "bottom"
tags_location = "top" # "frontmatter", "top", "bottom"

View File

@@ -8,6 +8,7 @@ tags:
- ignored_file_tag1 - ignored_file_tag1
author: author name author: author name
type: ["article", "note"] type: ["article", "note"]
ignored_frontmatter: ignore_me
--- ---
#inline_tag_top1 #inline_tag_top2 #inline_tag_top1 #inline_tag_top2
#ignored_file_tag2 #ignored_file_tag2

View File

@@ -1,8 +1,3 @@
vault = "tests/fixtures/test_vault" ["Test Vault"]
exclude_paths = [".git", ".obsidian", "ignore_folder"]
# folders to ignore when parsing content path = "tests/fixtures/test_vault"
exclude_paths = [".git", ".obsidian", "ignore_folder"]
[metadata]
metadata_location = "frontmatter" # "frontmatter", "top", "bottom"
tags_location = "top" # "frontmatter", "top", "bottom"

12
tests/questions_test.py Normal file
View File

@@ -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")

View File

@@ -7,7 +7,6 @@ from obsidian_metadata._utils import (
dict_contains, dict_contains,
dict_values_to_lists_strings, dict_values_to_lists_strings,
remove_markdown_sections, 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(): def test_remove_markdown_sections():
"""Test removing markdown sections.""" """Test removing markdown sections."""
text: str = """ text: str = """

View File

@@ -12,16 +12,18 @@ def test_vault_creation(test_vault):
"""Test creating a Vault object.""" """Test creating a Vault object."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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.vault_path == vault_path
assert vault.backup_path == Path(f"{vault_path}.bak") assert vault.backup_path == Path(f"{vault_path}.bak")
assert vault.dry_run is False assert vault.dry_run is False
assert str(vault.exclude_paths[0]) == Regex(r".*\.git") assert str(vault.exclude_paths[0]) == Regex(r".*\.git")
assert vault.num_notes() == 2 assert vault.num_notes() == 3
assert vault.metadata.dict == { assert vault.metadata.dict == {
"Inline Tags": [ "Inline Tags": [
"ignored_file_tag2",
"inline_tag_bottom1", "inline_tag_bottom1",
"inline_tag_bottom2", "inline_tag_bottom2",
"inline_tag_top1", "inline_tag_top1",
@@ -30,24 +32,29 @@ def test_vault_creation(test_vault):
"intext_tag2", "intext_tag2",
"shared_tag", "shared_tag",
], ],
"author": ["author name"],
"bottom_key1": ["bottom_key1_value"], "bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"], "bottom_key2": ["bottom_key2_value"],
"date_created": ["2022-12-22"], "date_created": ["2022-12-22"],
"emoji_📅_key": ["emoji_📅_key_value"], "emoji_📅_key": ["emoji_📅_key_value"],
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"],
"intext_key": ["intext_value"], "intext_key": ["intext_value"],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value"],
"shared_key2": ["shared_key2_value1", "shared_key2_value2"], "shared_key2": ["shared_key2_value1", "shared_key2_value2"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
"frontmatter_tag2", "frontmatter_tag2",
"frontmatter_tag3",
"ignored_file_tag1",
"shared_tag", "shared_tag",
"📅/frontmatter_tag3", "📅/frontmatter_tag3",
], ],
"top_key1": ["top_key1_value"], "top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"], "top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"], "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.""" """Test filtering notes."""
vault_path = sample_vault vault_path = sample_vault
config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) 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 assert vault.num_notes() == 4
vault_path = sample_vault vault_path = sample_vault
config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) 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 assert vault2.num_notes() == 1
@@ -70,7 +79,8 @@ def test_backup(test_vault, capsys):
"""Test backing up the vault.""" """Test backing up the vault."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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.backup()
@@ -88,7 +98,8 @@ def test_backup_dryrun(test_vault, capsys):
"""Test backing up the vault.""" """Test backing up the vault."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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}") print(f"vault.dry_run: {vault.dry_run}")
vault.backup() vault.backup()
@@ -102,7 +113,8 @@ def test_delete_backup(test_vault, capsys):
"""Test deleting the vault backup.""" """Test deleting the vault backup."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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.backup()
vault.delete_backup() vault.delete_backup()
@@ -121,7 +133,8 @@ def test_delete_backup_dryrun(test_vault, capsys):
"""Test deleting the vault backup.""" """Test deleting the vault backup."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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) Path.mkdir(vault.backup_path)
vault.delete_backup() vault.delete_backup()
@@ -135,7 +148,8 @@ def test_info(test_vault, capsys):
"""Test printing vault information.""" """Test printing vault information."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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() vault.info()
@@ -149,7 +163,8 @@ def test_contains_inline_tag(test_vault) -> None:
"""Test if the vault contains an inline tag.""" """Test if the vault contains an inline tag."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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("tag") is False
assert vault.contains_inline_tag("intext_tag2") is True 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.""" """Test if the vault contains a metadata key."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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("key") is False
assert vault.contains_metadata("top_key1") is True 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.""" """Test deleting an inline tag."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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("no tag") is False
assert vault.delete_inline_tag("intext_tag2") is True assert vault.delete_inline_tag("intext_tag2") is True
assert vault.metadata.dict["Inline Tags"] == [ assert vault.metadata.dict["Inline Tags"] == [
"ignored_file_tag2",
"inline_tag_bottom1", "inline_tag_bottom1",
"inline_tag_bottom2", "inline_tag_bottom2",
"inline_tag_top1", "inline_tag_top1",
@@ -189,15 +207,16 @@ def test_delete_metadata(test_vault) -> None:
"""Test deleting a metadata key/value.""" """Test deleting a metadata key/value."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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("no key") == 0
assert vault.delete_metadata("top_key1", "no_value") == 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.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 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.""" """Test renaming an inline tag."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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("no tag", "new_tag") is False
assert vault.rename_inline_tag("intext_tag2", "new_tag") is True assert vault.rename_inline_tag("intext_tag2", "new_tag") is True
assert vault.metadata.dict["Inline Tags"] == [ assert vault.metadata.dict["Inline Tags"] == [
"ignored_file_tag2",
"inline_tag_bottom1", "inline_tag_bottom1",
"inline_tag_bottom2", "inline_tag_bottom2",
"inline_tag_top1", "inline_tag_top1",
@@ -224,7 +245,8 @@ def test_rename_metadata(test_vault) -> None:
"""Test renaming a metadata key/value.""" """Test renaming a metadata key/value."""
vault_path = test_vault vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) 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("no key", "new_key") is False
assert vault.rename_metadata("tags", "nonexistent_value", "new_vaule") 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.rename_metadata("tags", "frontmatter_tag1", "new_vaule") is True
assert vault.metadata.dict["tags"] == [ assert vault.metadata.dict["tags"] == [
"frontmatter_tag2", "frontmatter_tag2",
"frontmatter_tag3",
"ignored_file_tag1",
"new_vaule", "new_vaule",
"shared_tag", "shared_tag",
"📅/frontmatter_tag3", "📅/frontmatter_tag3",
@@ -241,6 +265,8 @@ def test_rename_metadata(test_vault) -> None:
assert "tags" not in vault.metadata.dict assert "tags" not in vault.metadata.dict
assert vault.metadata.dict["new_key"] == [ assert vault.metadata.dict["new_key"] == [
"frontmatter_tag2", "frontmatter_tag2",
"frontmatter_tag3",
"ignored_file_tag1",
"new_vaule", "new_vaule",
"shared_tag", "shared_tag",
"📅/frontmatter_tag3", "📅/frontmatter_tag3",