feat(app): limit scope of notes with one or more filters (#13)

* style: rename `VaultMetadata.add_metadata` to `VaultMetadata.index_metadata`

* refactor(vault): refactor filtering notes

* fix(application): improve usage display

* fix(application): improve colors of questions

* feat(application): limit the scope of notes to be processed with one or more filters

* build(deps): update identify
This commit is contained in:
Nathaniel Landau
2023-02-01 15:00:57 -05:00
committed by GitHub
parent 6909738218
commit 4a29945de2
15 changed files with 418 additions and 171 deletions

View File

@@ -22,36 +22,46 @@ Run `obsidian-metadata` from the command line to invoke the script. Add `--help
Obsidian-metadata provides a menu of sub-commands. Obsidian-metadata provides a menu of sub-commands.
**Vault Actions** **Vault Actions**
Create or delete a backup of your vault.
- Backup: Create a backup of the vault. - Backup: Create a backup of the vault.
- Delete Backup: Delete a backup of the vault. - Delete Backup: Delete a backup of the vault.
**Inspect Metadata** **Inspect Metadata**
Inspect the metadata in your vault.
- View all metadata in the vault - View all metadata in the vault
**Filter Notes in Scope**: **Filter Notes in Scope**:
Limit the scope of notes to be processed with a regex. Limit the scope of notes to be processed with one or more filters.
- Apply regex: Set a regex to limit scope - Path filter (regex): Limit scope based on the path or filename
- List notes in scope: List notes that will be processed. - Metadata Filter: Limit scope based on a key or key/value pair
- Tag Filter: Limit scope based on an in-text tag
- List and Clear Filters List all current filters and clear one or all
- List notes in scope: List notes that will be processed.
**Add Metadata** **Add Metadata**
Add new metadata to your vault.
- Add metadata to the frontmatter - Add metadata to the frontmatter
- Add to inline metadata (Not yet implemented) - Add to inline metadata (Not yet implemented)
- Add to inline tag (Not yet implemented) - Add to inline tag (Not yet implemented)
**Rename Metadata** **Rename Metadata**
Rename either a key and all associated values, a specific value within a key. or an in-text tag.
- Rename a key - Rename a key
- Rename a value - Rename a value
- rename an inline tag - rename an inline tag
**Delete Metadata** **Delete Metadata**
Delete either a key and all associated values, or a specific value.
- Delete a key and associated values - Delete a key and associated values
- Delete a value from a key - Delete a value from a key
- Delete an inline tag - Delete an inline tag
**Review Changes** **Review Changes**
Prior to committing changes, review all changes that will be made.
- View a diff of the changes that will be made - View a diff of the changes that will be made
**Commit Changes** **Commit Changes**
Write the changes to disk. This step is not undoable.
- Commit changes to the vault - Commit changes to the vault
### Configuration ### Configuration

12
poetry.lock generated
View File

@@ -188,7 +188,7 @@ pyflakes = ">=3.0.0,<3.1.0"
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.5.16" version = "2.5.17"
description = "File identification library for Python" description = "File identification library for Python"
category = "dev" category = "dev"
optional = false optional = false
@@ -657,7 +657,7 @@ python-versions = ">=3.7"
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "67.0.0" version = "67.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev" category = "dev"
optional = false optional = false
@@ -952,8 +952,8 @@ flake8 = [
{file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
] ]
identify = [ identify = [
{file = "identify-2.5.16-py2.py3-none-any.whl", hash = "sha256:832832a58ecc1b8f33d5e8cb4f7d3db2f5c7fbe922dfee5f958b48fed691501a"}, {file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"},
{file = "identify-2.5.16.tar.gz", hash = "sha256:c47acedfe6495b1c603ed7e93469b26e839cab38db4155113f36f718f8b3dc47"}, {file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"},
] ]
iniconfig = [ iniconfig = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
@@ -1267,8 +1267,8 @@ ruff = [
{file = "ruff-0.0.217.tar.gz", hash = "sha256:39b2b1de9330fcf60643bdd6c4c660b457390c686b4ba7101bea019a01446494"}, {file = "ruff-0.0.217.tar.gz", hash = "sha256:39b2b1de9330fcf60643bdd6c4c660b457390c686b4ba7101bea019a01446494"},
] ]
setuptools = [ setuptools = [
{file = "setuptools-67.0.0-py3-none-any.whl", hash = "sha256:9d790961ba6219e9ff7d9557622d2fe136816a264dd01d5997cfc057d804853d"}, {file = "setuptools-67.1.0-py3-none-any.whl", hash = "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378"},
{file = "setuptools-67.0.0.tar.gz", hash = "sha256:883131c5b6efa70b9101c7ef30b2b7b780a4283d5fc1616383cdf22c83cbefe6"}, {file = "setuptools-67.1.0.tar.gz", hash = "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"},
] ]
shellingham = [ shellingham = [
{file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"},

View File

@@ -1,6 +1,7 @@
"""Logging and alerts.""" """Logging and alerts."""
import sys import sys
from pathlib import Path from pathlib import Path
from textwrap import wrap
import rich.repr import rich.repr
import typer import typer
@@ -62,6 +63,29 @@ def info(msg: str) -> None:
print(f"INFO | {msg}") print(f"INFO | {msg}")
def usage(msg: str, width: int = 80) -> None:
"""Print a usage message without using logging.
Args:
msg: Message to print
width (optional): Width of the message
"""
for _n, line in enumerate(wrap(msg, width=width)):
if _n == 0:
print(f"[dim]USAGE | {line}")
else:
print(f"[dim] | {line}")
def debug(msg: str) -> None:
"""Print a debug message without using logging.
Args:
msg: Message to print
"""
print(f"[blue]DEBUG | {msg}[/blue]")
def dim(msg: str) -> None: def dim(msg: str) -> None:
"""Print a message in dimmed color. """Print a message in dimmed color.

View File

@@ -82,36 +82,46 @@ def main(
[tan]Obsidian-metadata[/] provides a menu of sub-commands. [tan]Obsidian-metadata[/] provides a menu of sub-commands.
[bold underline]Vault Actions[/] [bold underline]Vault Actions[/]
Create or delete a backup of your vault.
• Backup: Create a backup of the vault. • Backup: Create a backup of the vault.
• Delete Backup: Delete a backup of the vault. • Delete Backup: Delete a backup of the vault.
[bold underline]Inspect Metadata[/] [bold underline]Inspect Metadata[/]
Inspect the metadata in your vault.
• View all metadata in the vault • View all metadata in the vault
[bold underline]Filter Notes in Scope[/] [bold underline]Filter Notes in Scope[/]
Limit the scope of notes to be processed with a regex. Limit the scope of notes to be processed with one or more filters.
Apply regex: Set a regex to limit scope Path filter (regex): Limit scope based on the path or filename
List notes in scope: List notes that will be processed. Metadata Filter: Limit scope based on a key or key/value pair
• Tag Filter: Limit scope based on an in-text tag
• List and Clear Filters: List all current filters and clear one or all
• List notes in scope: List notes that will be processed.
[bold underline]Add Metadata[/] [bold underline]Add Metadata[/]
Add new metadata to your vault.
• Add metadata to the frontmatter • Add metadata to the frontmatter
• [dim]Add to inline metadata (Not yet implemented)[/] • [dim]Add to inline metadata (Not yet implemented)[/]
• [dim]Add to inline tag (Not yet implemented)[/] • [dim]Add to inline tag (Not yet implemented)[/]
[bold underline]Rename Metadata[/] [bold underline]Rename Metadata[/]
Rename either a key and all associated values, a specific value within a key. or an in-text tag.
• Rename a key • Rename a key
• Rename a value • Rename a value
• rename an inline tag • rename an inline tag
[bold underline]Delete Metadata[/] [bold underline]Delete Metadata[/]
Delete either a key and all associated values, or a specific value.
• Delete a key and associated values • Delete a key and associated values
• Delete a value from a key • Delete a value from a key
• Delete an inline tag • Delete an inline tag
[bold underline]Review Changes[/] [bold underline]Review Changes[/]
Prior to committing changes, review all changes that will be made.
• View a diff of the changes that will be made • View a diff of the changes that will be made
[bold underline]Commit Changes[/] [bold underline]Commit Changes[/]
Write the changes to disk. This step is not undoable.
• Commit changes to the vault • Commit changes to the vault
""" """

View File

@@ -8,7 +8,7 @@ from obsidian_metadata.models.metadata import (
VaultMetadata, VaultMetadata,
) )
from obsidian_metadata.models.notes import Note from obsidian_metadata.models.notes import Note
from obsidian_metadata.models.vault import Vault from obsidian_metadata.models.vault import Vault, VaultFilter
from obsidian_metadata.models.application import Application # isort: skip from obsidian_metadata.models.application import Application # isort: skip
@@ -23,4 +23,5 @@ __all__ = [
"Patterns", "Patterns",
"Vault", "Vault",
"VaultMetadata", "VaultMetadata",
"VaultFilter",
] ]

View File

@@ -5,10 +5,12 @@ from typing import Any
import questionary import questionary
from rich import print from rich import print
from textwrap import dedent from rich import box
from rich.console import Console
from rich.table import Table
from obsidian_metadata._config import VaultConfig from obsidian_metadata._config import VaultConfig
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, VaultFilter
from obsidian_metadata._utils import alerts from obsidian_metadata._utils import alerts
from obsidian_metadata.models.questions import Questions from obsidian_metadata.models.questions import Questions
from obsidian_metadata.models.enums import MetadataType from obsidian_metadata.models.enums import MetadataType
@@ -28,10 +30,11 @@ class Application:
self.config = config self.config = config
self.dry_run = dry_run self.dry_run = dry_run
self.questions = Questions() self.questions = Questions()
self.filters: list[VaultFilter] = []
def application_main(self) -> None: def application_main(self) -> None:
"""Questions for the main application.""" """Questions for the main application."""
self.load_vault() self._load_vault()
while True: while True:
self.vault.info() self.vault.info()
@@ -65,12 +68,9 @@ class Application:
def application_add_metadata(self) -> None: def application_add_metadata(self) -> None:
"""Add metadata.""" """Add metadata."""
help_text = """ alerts.usage(
USAGE | Add Metadata "Add new metadata to your vault. Currently only supports adding to the frontmatter of a note."
[dim]Add new metadata to your vault. Currently only supports )
adding to the frontmatter of a note.[/]
"""
print(dedent(help_text))
area = self.questions.ask_area() area = self.questions.ask_area()
match area: match area:
@@ -103,41 +103,109 @@ class Application:
def application_filter(self) -> None: def application_filter(self) -> None:
"""Filter notes.""" """Filter notes."""
help_text = """ alerts.usage("Limit the scope of notes to be processed with one or more filters.")
USAGE | Filter Notes
[dim]Enter a regex to filter notes by path. This allows you to
specify a subset of notes to update. Leave empty to include
all markdown files.[/]
"""
print(dedent(help_text))
choices = [ choices = [
{"name": "Apply regex filter", "value": "apply_filter"}, {"name": "Apply new regex path filter", "value": "apply_path_filter"},
{"name": "Apply new metadata filter", "value": "apply_metadata_filter"},
{"name": "Apply new in-text tag filter", "value": "apply_tag_filter"},
{"name": "List and clear filters", "value": "list_filters"},
{"name": "List notes in scope", "value": "list_notes"}, {"name": "List notes in scope", "value": "list_notes"},
questionary.Separator(), questionary.Separator(),
{"name": "Back", "value": "back"}, {"name": "Back", "value": "back"},
] ]
while True: while True:
match self.questions.ask_selection(choices=choices, question="Select an action"): match self.questions.ask_selection(choices=choices, question="Select an action"):
case "apply_filter": case "apply_path_filter":
path_filter = self.questions.ask_filter_path() path = self.questions.ask_filter_path()
if path_filter is None: if path is None or path == "":
return return
if path_filter == "": self.filters.append(VaultFilter(path_filter=path))
path_filter = None self._load_vault()
self.load_vault(path_filter=path_filter) case "apply_metadata_filter":
key = self.questions.ask_existing_key()
if key is None:
return
total_notes = self.vault.num_notes() + self.vault.num_excluded_notes() questions2 = Questions(vault=self.vault, key=key)
value = questions2.ask_existing_value(
if path_filter is None: question="Enter the value for the metadata filter",
alerts.success(f"Loaded all {total_notes} total notes") )
if value is None:
return
if value == "":
self.filters.append(VaultFilter(key_filter=key))
else: else:
alerts.success( self.filters.append(VaultFilter(key_filter=key, value_filter=value))
f"Loaded {self.vault.num_notes()} notes from {total_notes} total notes" self._load_vault()
)
case "apply_tag_filter":
tag = self.questions.ask_existing_inline_tag()
if tag is None or tag == "":
return
self.filters.append(VaultFilter(tag_filter=tag))
self._load_vault()
case "list_filters":
if len(self.filters) == 0:
alerts.notice("No filters have been applied")
return
print("")
table = Table(
"Opt",
"Filter",
"Type",
title="Current Filters",
show_header=False,
box=box.HORIZONTALS,
)
for _n, filter in enumerate(self.filters, start=1):
if filter.path_filter is not None:
table.add_row(
str(_n),
f"Path regex: [tan bold]{filter.path_filter}",
end_section=bool(_n == len(self.filters)),
)
elif filter.tag_filter is not None:
table.add_row(
str(_n),
f"Tag filter: [tan bold]{filter.tag_filter}",
end_section=bool(_n == len(self.filters)),
)
elif filter.key_filter is not None and filter.value_filter is None:
table.add_row(
str(_n),
f"Key filter: [tan bold]{filter.key_filter}",
end_section=bool(_n == len(self.filters)),
)
elif filter.key_filter is not None and filter.value_filter is not None:
table.add_row(
str(_n),
f"Key/Value : [tan bold]{filter.key_filter}={filter.value_filter}",
end_section=bool(_n == len(self.filters)),
)
table.add_row(f"{len(self.filters) + 1}", "Clear All")
table.add_row(f"{len(self.filters) + 2}", "Return to Main Menu")
Console().print(table)
num = self.questions.ask_number(
question="Enter the number of the filter to clear"
)
if num is None:
return
if int(num) <= len(self.filters):
self.filters.pop(int(num) - 1)
self._load_vault()
return
if int(num) == len(self.filters) + 1:
self.filters = []
self._load_vault()
return
case "list_notes": case "list_notes":
self.vault.list_editable_notes() self.vault.list_editable_notes()
@@ -147,12 +215,9 @@ class Application:
def application_inspect_metadata(self) -> None: def application_inspect_metadata(self) -> None:
"""View metadata.""" """View metadata."""
help_text = """ alerts.usage(
USAGE | View Metadata "Inspect the metadata in your vault. Note, uncommitted changes will be reflected in these reports"
[dim]Inspect the metadata in your vault. Note, uncommitted )
changes will be reflected in these reports[/]
"""
print(dedent(help_text))
choices = [ choices = [
{"name": "View all metadata", "value": "all_metadata"}, {"name": "View all metadata", "value": "all_metadata"},
@@ -168,11 +233,7 @@ class Application:
def application_vault(self) -> None: def application_vault(self) -> None:
"""Vault actions.""" """Vault actions."""
help_text = """ alerts.usage("Create or delete a backup of your vault.")
USAGE | Vault Actions
[dim]Create or delete a backup of your vault.[/]
"""
print(dedent(help_text))
choices = [ choices = [
{"name": "Backup vault", "value": "backup_vault"}, {"name": "Backup vault", "value": "backup_vault"},
@@ -191,12 +252,7 @@ class Application:
return return
def application_delete_metadata(self) -> None: def application_delete_metadata(self) -> None:
help_text = """ alerts.usage("Delete either a key and all associated values, or a specific value.")
USAGE | Delete Metadata
[dim]Delete either a key and all associated values,
or a specific value.[/]
"""
print(dedent(help_text))
choices = [ choices = [
{"name": "Delete key", "value": "delete_key"}, {"name": "Delete key", "value": "delete_key"},
@@ -219,11 +275,7 @@ class Application:
def application_rename_metadata(self) -> None: def application_rename_metadata(self) -> None:
"""Rename metadata.""" """Rename metadata."""
help_text = """ alerts.usage("Select the type of metadata to rename.")
USAGE | Rename Metadata
[dim]Select the type of metadata to rename.[/]
"""
print(dedent(help_text))
choices = [ choices = [
{"name": "Rename key", "value": "rename_key"}, {"name": "Rename key", "value": "rename_key"},
@@ -324,14 +376,17 @@ class Application:
return return
def load_vault(self, path_filter: str = None) -> None: def _load_vault(self) -> None:
"""Load the vault. """Load the vault."""
Args: if len(self.filters) == 0:
path_filter (str, optional): Regex to filter notes by path. self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run)
""" else:
self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run, path_filter=path_filter) self.vault = Vault(config=self.config, dry_run=self.dry_run, filters=self.filters)
log.info(f"Indexed {self.vault.num_notes()} notes from {self.vault.vault_path}")
alerts.success(
f"Loaded {len(self.vault.notes_in_scope)} notes from {len(self.vault.all_notes)} total notes"
)
self.questions = Questions(vault=self.vault) self.questions = Questions(vault=self.vault)
def rename_key(self) -> None: def rename_key(self) -> None:

View File

@@ -31,8 +31,8 @@ class VaultMetadata:
"""Representation of all metadata.""" """Representation of all metadata."""
return str(self.dict) return str(self.dict)
def add_metadata(self, metadata: dict[str, list[str]]) -> None: def index_metadata(self, metadata: dict[str, list[str]]) -> None:
"""Add metadata to the vault. Takes a dictionary as input and merges it with the existing metadata. Does not overwrite existing keys. """Index pre-existing metadata in the vault. Takes a dictionary as input and merges it with the existing metadata. Does not overwrite existing keys.
Args: Args:
metadata (dict): Metadata to add. metadata (dict): Metadata to add.

View File

@@ -64,13 +64,13 @@ class Questions:
""" """
self.style = questionary.Style( self.style = questionary.Style(
[ [
("qmark", "fg:#808080 bold"), ("qmark", "fg:#729fcf bold"),
("question", "bold"), ("question", "fg:#729fcf bold"),
("separator", "fg:#808080"), ("separator", "fg:#808080"),
("instruction", "fg:#808080"), ("instruction", "fg:#808080"),
("highlighted", "fg:#c0c0c0 bold reverse"), ("highlighted", "fg:#729fcf bold underline"),
("text", ""), ("text", ""),
("pointer", "bold"), ("pointer", "fg:#729fcf bold"),
] ]
) )
self.vault = vault self.vault = vault
@@ -174,6 +174,20 @@ class Questions:
return True return True
def _validate_number(self, text: str) -> bool | str:
"""Validate a number.
Args:
text (str): The number to validate.
Returns:
bool | str: True if the number is valid, otherwise a string with the error message.
"""
if not text.isdigit():
return "Must be an integer"
return True
def _validate_valid_vault_regex(self, text: str) -> bool | str: def _validate_valid_vault_regex(self, text: str) -> bool | str:
"""Validates a valid regex. """Validates a valid regex.
@@ -202,8 +216,8 @@ class Questions:
Returns: Returns:
bool | str: True if the value is valid, otherwise a string with the error message. bool | str: True if the value is valid, otherwise a string with the error message.
""" """
if len(text) < 1: if len(text) == 0:
return "Value cannot be empty" return True
if self.key is not None and not self.vault.metadata.contains(self.key, text): if self.key is not None and not self.vault.metadata.contains(self.key, text):
return f"{self.key}:{text} does not exist" return f"{self.key}:{text} does not exist"
@@ -408,6 +422,19 @@ class Questions:
question, validate=self._validate_new_value, style=self.style, qmark="INPUT |" question, validate=self._validate_new_value, style=self.style, qmark="INPUT |"
).ask() ).ask()
def ask_number(self, question: str = "Enter a number") -> int:
"""Ask the user for a number.
Args:
question (str, optional): The question to ask. Defaults to "Enter a number".
Returns:
int: A number.
"""
return questionary.text(
question, validate=self._validate_number, style=self.style, qmark="INPUT |"
).ask()
def ask_selection( def ask_selection(
self, choices: list[Any], question: str = "Select an option" self, choices: list[Any], question: str = "Select an option"
) -> Any: # pragma: no cover ) -> Any: # pragma: no cover

View File

@@ -2,6 +2,7 @@
import re import re
import shutil import shutil
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
import rich.repr import rich.repr
@@ -17,6 +18,16 @@ from obsidian_metadata._utils.alerts import logger as log
from obsidian_metadata.models import MetadataType, Note, VaultMetadata from obsidian_metadata.models import MetadataType, Note, VaultMetadata
@dataclass
class VaultFilter:
"""Vault filters."""
path_filter: str = None
key_filter: str = None
value_filter: str = None
tag_filter: str = None
@rich.repr.auto @rich.repr.auto
class Vault: class Vault:
"""Representation of the Obsidian vault. """Representation of the Obsidian vault.
@@ -28,7 +39,12 @@ 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: VaultConfig, dry_run: bool = False, path_filter: str = None): def __init__(
self,
config: VaultConfig,
dry_run: bool = False,
filters: list[VaultFilter] = [],
):
self.vault_path: Path = config.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"
@@ -37,8 +53,8 @@ class Vault:
for p in config.exclude_paths: for p in config.exclude_paths:
self.exclude_paths.append(Path(self.vault_path / p)) self.exclude_paths.append(Path(self.vault_path / p))
self.path_filter = path_filter self.filters = filters
self.note_paths = self._find_markdown_notes(path_filter) self.all_note_paths = self._find_markdown_notes()
with Progress( with Progress(
SpinnerColumn(), SpinnerColumn(),
@@ -46,9 +62,10 @@ class Vault:
transient=True, transient=True,
) as progress: ) as progress:
progress.add_task(description="Processing notes...", total=None) progress.add_task(description="Processing notes...", total=None)
self.notes: list[Note] = [ self.all_notes: list[Note] = [
Note(note_path=p, dry_run=self.dry_run) for p in self.note_paths Note(note_path=p, dry_run=self.dry_run) for p in self.all_note_paths
] ]
self.notes_in_scope = self._filter_notes()
self._rebuild_vault_metadata() self._rebuild_vault_metadata()
@@ -57,33 +74,54 @@ class Vault:
yield "vault_path", self.vault_path yield "vault_path", self.vault_path
yield "dry_run", self.dry_run yield "dry_run", self.dry_run
yield "backup_path", self.backup_path yield "backup_path", self.backup_path
yield "num_notes", self.num_notes() yield "num_notes", len(self.all_notes)
yield "num_notes_in_scope", len(self.notes_in_scope)
yield "exclude_paths", self.exclude_paths yield "exclude_paths", self.exclude_paths
def _find_markdown_notes(self, path_filter: str = None) -> list[Path]: def _filter_notes(self) -> list[Note]:
"""Build list of all markdown files in the vault. """Filter notes by path and metadata using the filters defined in self.filters.
Args: Returns:
path_filter (str, optional): Regex to filter notes by path. list[Note]: List of notes matching the filters.
"""
notes_list = self.all_notes.copy()
for _filter in self.filters:
if _filter.path_filter is not None:
notes_list = [
n
for n in notes_list
if re.search(_filter.path_filter, str(n.note_path.relative_to(self.vault_path)))
]
if _filter.tag_filter is not None:
notes_list = [n for n in notes_list if n.contains_inline_tag(_filter.tag_filter)]
if _filter.key_filter is not None and _filter.value_filter is not None:
notes_list = [
n
for n in notes_list
if n.contains_metadata(_filter.key_filter, _filter.value_filter)
]
if _filter.key_filter is not None and _filter.value_filter is None:
notes_list = [n for n in notes_list if n.contains_metadata(_filter.key_filter)]
return notes_list
def _find_markdown_notes(self) -> list[Path]:
"""Build list of all markdown files in the vault.
Returns: Returns:
list[Path]: List of paths to all matching files in the vault. list[Path]: List of paths to all matching files in the vault.
""" """
notes_list = [ return [
p.resolve() p.resolve()
for p in self.vault_path.glob("**/*") for p in self.vault_path.glob("**/*")
if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"] if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"]
and not any(item in p.parents for item in self.exclude_paths) and not any(item in p.parents for item in self.exclude_paths)
] ]
if path_filter is not None:
notes_list = [
p for p in notes_list if re.search(path_filter, str(p.relative_to(self.vault_path)))
]
return notes_list
def _rebuild_vault_metadata(self) -> None: def _rebuild_vault_metadata(self) -> None:
"""Rebuild vault metadata.""" """Rebuild vault metadata."""
self.metadata = VaultMetadata() self.metadata = VaultMetadata()
@@ -93,10 +131,12 @@ class Vault:
transient=True, transient=True,
) as progress: ) as progress:
progress.add_task(description="Processing notes...", total=None) progress.add_task(description="Processing notes...", total=None)
for _note in self.notes: for _note in self.notes_in_scope:
self.metadata.add_metadata(_note.frontmatter.dict) self.metadata.index_metadata(_note.frontmatter.dict)
self.metadata.add_metadata(_note.inline_metadata.dict) self.metadata.index_metadata(_note.inline_metadata.dict)
self.metadata.add_metadata({_note.inline_tags.metadata_key: _note.inline_tags.list}) self.metadata.index_metadata(
{_note.inline_tags.metadata_key: _note.inline_tags.list}
)
def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> int: def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> int:
"""Add metadata to all notes in the vault. """Add metadata to all notes in the vault.
@@ -111,7 +151,7 @@ class Vault:
""" """
num_changed = 0 num_changed = 0
for _note in self.notes: for _note in self.notes_in_scope:
if _note.add_metadata(area, key, value): if _note.add_metadata(area, key, value):
num_changed += 1 num_changed += 1
@@ -153,7 +193,7 @@ class Vault:
Returns: Returns:
bool: True if tag is found in vault. bool: True if tag is found in vault.
""" """
return any(_note.contains_inline_tag(tag) for _note in self.notes) return any(_note.contains_inline_tag(tag) for _note in self.notes_in_scope)
def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool: def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool:
"""Check if vault contains the given metadata. """Check if vault contains the given metadata.
@@ -193,7 +233,7 @@ class Vault:
""" """
num_changed = 0 num_changed = 0
for _note in self.notes: for _note in self.notes_in_scope:
if _note.delete_inline_tag(tag): if _note.delete_inline_tag(tag):
num_changed += 1 num_changed += 1
@@ -214,7 +254,7 @@ class Vault:
""" """
num_changed = 0 num_changed = 0
for _note in self.notes: for _note in self.notes_in_scope:
if _note.delete_metadata(key, value): if _note.delete_metadata(key, value):
num_changed += 1 num_changed += 1
@@ -230,7 +270,7 @@ class Vault:
list[Note]: List of notes that have changes. list[Note]: List of notes that have changes.
""" """
changed_notes = [] changed_notes = []
for _note in self.notes: for _note in self.notes_in_scope:
if _note.has_changes(): if _note.has_changes():
changed_notes.append(_note) changed_notes.append(_note)
@@ -245,36 +285,23 @@ class Vault:
table.add_row("Backup path", str(self.backup_path)) table.add_row("Backup path", str(self.backup_path))
else: else:
table.add_row("Backup", "None") table.add_row("Backup", "None")
table.add_row("Notes in scope", str(self.num_notes())) table.add_row("Notes in scope", str(len(self.notes_in_scope)))
table.add_row("Notes excluded from scope", str(self.num_excluded_notes())) table.add_row("Notes excluded from scope", str(self.num_excluded_notes()))
table.add_row("Active path filter", str(self.path_filter)) table.add_row("Active filters", str(len(self.filters)))
table.add_row("Notes with updates", str(len(self.get_changed_notes()))) table.add_row("Notes with changes", str(len(self.get_changed_notes())))
Console().print(table) Console().print(table)
def list_editable_notes(self) -> None: def list_editable_notes(self) -> None:
"""Print a list of notes within the scope that are being edited.""" """Print a list of notes within the scope that are being edited."""
table = Table(title="Notes in current scope", show_header=False, box=box.HORIZONTALS) table = Table(title="Notes in current scope", show_header=False, box=box.HORIZONTALS)
for _n, _note in enumerate(self.notes, start=1): for _n, _note in enumerate(self.notes_in_scope, start=1):
table.add_row(str(_n), str(_note.note_path.relative_to(self.vault_path))) table.add_row(str(_n), str(_note.note_path.relative_to(self.vault_path)))
Console().print(table) Console().print(table)
def num_excluded_notes(self) -> int: def num_excluded_notes(self) -> int:
"""Count number of excluded notes.""" """Count number of excluded notes."""
excluded_notes = [ return len(self.all_notes) - len(self.notes_in_scope)
p.resolve()
for p in self.vault_path.glob("**/*")
if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"] and p not in self.note_paths
]
return len(excluded_notes)
def num_notes(self) -> int:
"""Number of notes in the vault.
Returns:
int: Number of notes in the vault.
"""
return len(self.notes)
def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> int: def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> int:
"""Renames a key or key-value pair in the note's metadata. """Renames a key or key-value pair in the note's metadata.
@@ -291,7 +318,7 @@ class Vault:
""" """
num_changed = 0 num_changed = 0
for _note in self.notes: for _note in self.notes_in_scope:
if _note.rename_metadata(key, value_1, value_2): if _note.rename_metadata(key, value_1, value_2):
num_changed += 1 num_changed += 1
@@ -312,7 +339,7 @@ class Vault:
""" """
num_changed = 0 num_changed = 0
for _note in self.notes: for _note in self.notes_in_scope:
if _note.rename_inline_tag(old_tag, new_tag): if _note.rename_inline_tag(old_tag, new_tag):
num_changed += 1 num_changed += 1
@@ -325,6 +352,6 @@ class Vault:
"""Write changes to the vault.""" """Write changes to the vault."""
log.debug("Writing changes to vault...") log.debug("Writing changes to vault...")
if self.dry_run is False: if self.dry_run is False:
for _note in self.notes: for _note in self.notes_in_scope:
log.trace(f"writing to {_note.note_path}") log.trace(f"writing to {_note.note_path}")
_note.write() _note.write()

View File

@@ -44,6 +44,36 @@ def test_notice(capsys):
assert captured.out == "NOTICE | This prints in notice\n" assert captured.out == "NOTICE | This prints in notice\n"
def test_alerts_debug(capsys):
"""Test debug."""
alerts.debug("This prints in debug")
captured = capsys.readouterr()
assert captured.out == "DEBUG | This prints in debug\n"
def test_usage(capsys):
"""Test usage."""
alerts.usage("This prints in usage")
captured = capsys.readouterr()
assert captured.out == "USAGE | This prints in usage\n"
alerts.usage(
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
)
captured = capsys.readouterr()
assert "USAGE | Lorem ipsum dolor sit amet" in captured.out
assert " | incididunt ut labore et dolore magna aliqua" in captured.out
alerts.usage(
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
width=20,
)
captured = capsys.readouterr()
assert "USAGE | Lorem ipsum dolor" in captured.out
assert " | sit amet," in captured.out
assert " | adipisicing elit," in captured.out
def test_info(capsys): def test_info(capsys):
"""Test info.""" """Test info."""
alerts.info("This prints in info") alerts.info("This prints in info")

View File

@@ -19,19 +19,19 @@ from tests.helpers import Regex
def test_instantiate_application(test_application) -> None: def test_instantiate_application(test_application) -> None:
"""Test application.""" """Test application."""
app = test_application app = test_application
app.load_vault() app._load_vault()
assert app.dry_run is False assert app.dry_run is False
assert app.config.name == "command_line_vault" assert app.config.name == "command_line_vault"
assert app.config.exclude_paths == [".git", ".obsidian"] assert app.config.exclude_paths == [".git", ".obsidian"]
assert app.dry_run is False assert app.dry_run is False
assert app.vault.num_notes() == 13 assert len(app.vault.all_notes) == 13
def test_abort(test_application, mocker, capsys) -> None: def test_abort(test_application, mocker, capsys) -> None:
"""Test renaming a key.""" """Test renaming a key."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
return_value="abort", return_value="abort",
@@ -45,7 +45,7 @@ def test_abort(test_application, mocker, capsys) -> None:
def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> None: def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> None:
"""Test adding new metadata to the vault.""" """Test adding new metadata to the vault."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["add_metadata", KeyError], side_effect=["add_metadata", KeyError],
@@ -72,7 +72,7 @@ def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> N
def test_delete_inline_tag(test_application, mocker, capsys) -> None: def test_delete_inline_tag(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag.""" """Test renaming an inline tag."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["delete_metadata", KeyError], side_effect=["delete_metadata", KeyError],
@@ -113,7 +113,7 @@ def test_delete_inline_tag(test_application, mocker, capsys) -> None:
def test_delete_key(test_application, mocker, capsys) -> None: def test_delete_key(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag.""" """Test renaming an inline tag."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["delete_metadata", KeyError], side_effect=["delete_metadata", KeyError],
@@ -156,7 +156,7 @@ def test_delete_key(test_application, mocker, capsys) -> None:
def test_delete_value(test_application, mocker, capsys) -> None: def test_delete_value(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag.""" """Test renaming an inline tag."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["delete_metadata", KeyError], side_effect=["delete_metadata", KeyError],
@@ -202,17 +202,17 @@ def test_delete_value(test_application, mocker, capsys) -> None:
) )
def test_filter_notes_filter(test_application, mocker, capsys) -> None: def test_filter_notes(test_application, mocker, capsys) -> None:
"""Test renaming a key.""" """Test renaming a key."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["filter_notes", KeyError], side_effect=["filter_notes", KeyError],
) )
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_selection", "obsidian_metadata.models.application.Questions.ask_selection",
side_effect=["apply_filter", "list_notes", "back"], side_effect=["apply_path_filter", "list_notes", "back"],
) )
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_filter_path", "obsidian_metadata.models.application.Questions.ask_filter_path",
@@ -232,25 +232,61 @@ def test_filter_notes_filter(test_application, mocker, capsys) -> None:
) )
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_selection", "obsidian_metadata.models.application.Questions.ask_selection",
side_effect=["apply_filter", "list_notes", "back"], side_effect=["apply_metadata_filter", "list_notes", "back"],
) )
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_filter_path", "obsidian_metadata.models.application.Questions.ask_existing_key",
return_value="on_one_note",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_existing_value",
return_value="", return_value="",
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.out == Regex(r"SUCCESS +\| Loaded all.*\d+.*total notes", re.DOTALL) assert captured.out == Regex(r"SUCCESS +\| Loaded.*1.*notes from.*\d+.*total", re.DOTALL)
assert "02 inline/inline 2.md" in captured.out
assert "03 mixed/mixed 1.md" not in captured.out
def test_filter_clear(test_application, mocker, capsys) -> None:
"""Test clearing filters."""
app = test_application
app._load_vault()
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["filter_notes", "filter_notes", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_selection",
side_effect=["apply_metadata_filter", "list_filters", "list_notes", "back"],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_existing_key",
return_value="on_one_note",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_existing_value",
return_value="",
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_number",
return_value="1",
)
with pytest.raises(KeyError):
app.application_main()
captured = capsys.readouterr()
assert "02 inline/inline 2.md" in captured.out assert "02 inline/inline 2.md" in captured.out
assert "03 mixed/mixed 1.md" in captured.out assert "03 mixed/mixed 1.md" in captured.out
assert "01 frontmatter/frontmatter 4.md" in captured.out
assert "04 no metadata/no_metadata_1.md " in captured.out
def test_inspect_metadata_all(test_application, mocker, capsys) -> None: def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
"""Test backing up a vault.""" """Test backing up a vault."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["inspect_metadata", KeyError], side_effect=["inspect_metadata", KeyError],
@@ -269,7 +305,7 @@ def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
def test_rename_inline_tag(test_application, mocker, capsys) -> None: def test_rename_inline_tag(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag.""" """Test renaming an inline tag."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["rename_metadata", KeyError], side_effect=["rename_metadata", KeyError],
@@ -318,7 +354,7 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
def test_rename_key(test_application, mocker, capsys) -> None: def test_rename_key(test_application, mocker, capsys) -> None:
"""Test renaming a key.""" """Test renaming a key."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["rename_metadata", KeyError], side_effect=["rename_metadata", KeyError],
@@ -367,7 +403,7 @@ def test_rename_key(test_application, mocker, capsys) -> None:
def test_rename_value_fail(test_application, mocker, capsys) -> None: def test_rename_value_fail(test_application, mocker, capsys) -> None:
"""Test renaming an inline tag.""" """Test renaming an inline tag."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["rename_metadata", KeyError], side_effect=["rename_metadata", KeyError],
@@ -425,7 +461,7 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
def test_review_no_changes(test_application, mocker, capsys) -> None: def test_review_no_changes(test_application, mocker, capsys) -> None:
"""Review changes when no changes to vault.""" """Review changes when no changes to vault."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["review_changes", KeyError], side_effect=["review_changes", KeyError],
@@ -439,7 +475,7 @@ def test_review_no_changes(test_application, mocker, capsys) -> None:
def test_review_changes(test_application, mocker, capsys) -> None: def test_review_changes(test_application, mocker, capsys) -> None:
"""Review changes when no changes to vault.""" """Review changes when no changes to vault."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["rename_metadata", "review_changes", KeyError], side_effect=["rename_metadata", "review_changes", KeyError],
@@ -471,7 +507,7 @@ def test_review_changes(test_application, mocker, capsys) -> None:
def test_vault_backup(test_application, mocker, capsys) -> None: def test_vault_backup(test_application, mocker, capsys) -> None:
"""Test backing up a vault.""" """Test backing up a vault."""
app = test_application app = test_application
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["vault_actions", KeyError], side_effect=["vault_actions", KeyError],
@@ -492,7 +528,7 @@ def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
app = test_application app = test_application
backup_path = Path(tmp_path / "application.bak") backup_path = Path(tmp_path / "application.bak")
backup_path.mkdir() backup_path.mkdir()
app.load_vault() app._load_vault()
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["vault_actions", KeyError], side_effect=["vault_actions", KeyError],

View File

@@ -6,6 +6,7 @@ author:: John Doe
status:: new status:: new
type:: book type:: book
type:: article type:: article
on_one_note:: one
#food/fruit/apple #food/fruit/apple
#food/fruit/pear #food/fruit/pear
#dinner #lunch #breakfast #dinner #lunch #breakfast

View File

@@ -69,7 +69,7 @@ def test_vault_metadata(capsys) -> None:
vm = VaultMetadata() vm = VaultMetadata()
assert vm.dict == {} assert vm.dict == {}
vm.add_metadata(METADATA) vm.index_metadata(METADATA)
assert vm.dict == { assert vm.dict == {
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
@@ -97,7 +97,7 @@ def test_vault_metadata(capsys) -> None:
assert captured.out == Regex(r"│ frontmatter_Key1 +│ author name +│") assert captured.out == Regex(r"│ frontmatter_Key1 +│ author name +│")
new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]} new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]}
vm.add_metadata(new_metadata) vm.index_metadata(new_metadata)
assert vm.dict == { assert vm.dict == {
"added_key": ["added_value"], "added_key": ["added_value"],
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
@@ -115,7 +115,7 @@ def test_vault_metadata(capsys) -> None:
def test_vault_metadata_contains() -> None: def test_vault_metadata_contains() -> None:
"""Test contains method.""" """Test contains method."""
vm = VaultMetadata() vm = VaultMetadata()
vm.add_metadata(METADATA) vm.index_metadata(METADATA)
assert vm.dict == { assert vm.dict == {
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
@@ -141,7 +141,7 @@ def test_vault_metadata_contains() -> None:
def test_vault_metadata_delete() -> None: def test_vault_metadata_delete() -> None:
"""Test delete method.""" """Test delete method."""
vm = VaultMetadata() vm = VaultMetadata()
vm.add_metadata(METADATA) vm.index_metadata(METADATA)
assert vm.dict == { assert vm.dict == {
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
@@ -165,7 +165,7 @@ def test_vault_metadata_delete() -> None:
def test_vault_metadata_rename() -> None: def test_vault_metadata_rename() -> None:
"""Test rename method.""" """Test rename method."""
vm = VaultMetadata() vm = VaultMetadata()
vm.add_metadata(METADATA) vm.index_metadata(METADATA)
assert vm.dict == { assert vm.dict == {
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],

View File

@@ -60,6 +60,14 @@ def test_validate_new_tag() -> None:
assert questions._validate_new_tag("new_tag") is True assert questions._validate_new_tag("new_tag") is True
def test_validate_number() -> None:
"""Test number validation."""
questions = Questions(vault=VAULT)
assert "Must be an integer" in questions._validate_number("test")
assert "Must be an integer" in questions._validate_number("1.1")
assert questions._validate_number("1") is True
def test_validate_existing_inline_tag() -> None: def test_validate_existing_inline_tag() -> None:
"""Test existing tag validation.""" """Test existing tag validation."""
questions = Questions(vault=VAULT) questions = Questions(vault=VAULT)
@@ -80,12 +88,10 @@ def test_validate_key_exists_regex() -> None:
def test_validate_value() -> None: def test_validate_value() -> None:
"""Test value validation.""" """Test value validation."""
questions = Questions(vault=VAULT) questions = Questions(vault=VAULT)
assert questions._validate_value("test") is True
assert "Value cannot be empty" in questions._validate_value("")
assert questions._validate_value("test") is True
questions2 = Questions(vault=VAULT, key="frontmatter_Key1") questions2 = Questions(vault=VAULT, key="frontmatter_Key1")
assert questions2._validate_value("test") == "frontmatter_Key1:test does not exist" assert questions2._validate_value("test") == "frontmatter_Key1:test does not exist"
assert "Value cannot be empty" in questions2._validate_value("")
assert questions2._validate_value("author name") is True assert questions2._validate_value("author name") is True

View File

@@ -4,7 +4,7 @@
from pathlib import Path from pathlib import Path
from obsidian_metadata._config import Config from obsidian_metadata._config import Config
from obsidian_metadata.models import Vault from obsidian_metadata.models import Vault, VaultFilter
from obsidian_metadata.models.enums import MetadataType from obsidian_metadata.models.enums import MetadataType
from tests.helpers import Regex from tests.helpers import Regex
@@ -20,7 +20,7 @@ def test_vault_creation(test_vault):
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() == 3 assert len(vault.all_notes) == 3
assert vault.metadata.dict == { assert vault.metadata.dict == {
"Inline Tags": [ "Inline Tags": [
@@ -64,16 +64,36 @@ def test_get_filtered_notes(sample_vault) -> None:
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_config = config.vaults[0] vault_config = config.vaults[0]
vault = Vault(config=vault_config, path_filter="front")
assert vault.num_notes() == 4 filters = [VaultFilter(path_filter="front")]
vault = Vault(config=vault_config, filters=filters)
assert len(vault.all_notes) == 13
assert len(vault.notes_in_scope) == 4
vault_path = sample_vault filters = [VaultFilter(path_filter="mixed")]
config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) vault = Vault(config=vault_config, filters=filters)
vault_config = config.vaults[0] assert len(vault.all_notes) == 13
vault2 = Vault(config=vault_config, path_filter="mixed") assert len(vault.notes_in_scope) == 1
assert vault2.num_notes() == 1 filters = [VaultFilter(key_filter="on_one_note")]
vault = Vault(config=vault_config, filters=filters)
assert len(vault.all_notes) == 13
assert len(vault.notes_in_scope) == 1
filters = [VaultFilter(key_filter="type", value_filter="book")]
vault = Vault(config=vault_config, filters=filters)
assert len(vault.all_notes) == 13
assert len(vault.notes_in_scope) == 10
filters = [VaultFilter(tag_filter="brunch")]
vault = Vault(config=vault_config, filters=filters)
assert len(vault.all_notes) == 13
assert len(vault.notes_in_scope) == 1
filters = [VaultFilter(tag_filter="brunch"), VaultFilter(path_filter="inbox")]
vault = Vault(config=vault_config, filters=filters)
assert len(vault.all_notes) == 13
assert len(vault.notes_in_scope) == 0
def test_backup(test_vault, capsys): def test_backup(test_vault, capsys):