mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-12 15:03:48 -05:00
feat(application): add new metadata to frontmatter (#9)
* feat(frontmatter): frontmatter method to add key, values * build: add pysnooper to aid in debugging * feat(application): add new frontmatter * build: clean up dev container * fix(notes): diff now pretty prints in a table * docs(readme): update usage information * docs(readme): fix markdown lists
This commit is contained in:
@@ -39,10 +39,7 @@
|
||||
"--exclude",
|
||||
"'tests/'"
|
||||
],
|
||||
"python.linting.ignorePatterns": [
|
||||
".vscode/**/*.py",
|
||||
".venv/**/*.py"
|
||||
],
|
||||
"python.linting.ignorePatterns": [".vscode/**/*.py", ".venv/**/*.py"],
|
||||
"python.venvFolders": ["/home/vscode/.cache/pypoetry/virtualenvs"],
|
||||
"ruff.importStrategy": "fromEnvironment",
|
||||
"shellformat.path": "/home/vscode/.local/bin/shfmt",
|
||||
|
||||
60
README.md
60
README.md
@@ -1,36 +1,58 @@
|
||||
[](https://github.com/natelandau/obsidian-metadata/actions/workflows/python-code-checker.yml) [](https://codecov.io/gh/natelandau/obsidian-metadata)
|
||||
# obsidian-metadata
|
||||
A script to make batch updates to metadata in an Obsidian vault. Provides the following capabilities:
|
||||
A script to make batch updates to metadata in an Obsidian vault. No changes are
|
||||
made to the Vault until they are explicitly committed.
|
||||
|
||||
- `in-text tag`: delete every occurrence
|
||||
- `in-text tags`: Rename tag (`#tag1` -> `#tag2`)
|
||||
- `frontmatter`: Delete a key matching a regex pattern and all associated values
|
||||
- `frontmatter`: Rename a key
|
||||
- `frontmatter`: Delete a value matching a regex pattern from a specified key
|
||||
- `frontmatter`: Rename a value from a specified key
|
||||
- `inline metadata`: Delete a key matching a regex pattern and all associated values
|
||||
- `inline metadata`: Rename a key
|
||||
- `inline metadata`: Delete a value matching a regex pattern from a specified key
|
||||
- `inline metadata`: Rename a value from a specified key
|
||||
- `vault`: Create a backup of the Obsidian vault
|
||||
[](https://asciinema.org/a/555789)
|
||||
|
||||
## Important Disclaimer
|
||||
**It is strongly recommended that you back up your vault prior to committing changes.** This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed. The author of this script is not responsible for any data loss that may occur. Use at your own risk.
|
||||
|
||||
|
||||
## Install
|
||||
`obsidian-metadata` requires Python v3.10 or above.
|
||||
Requires Python v3.10 or above.
|
||||
|
||||
```bash
|
||||
pip install obsidian-metadata
|
||||
```
|
||||
|
||||
|
||||
## Important Disclaimer
|
||||
**It is strongly recommended that you back up your vault prior to committing changes.** This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed. The author of this script is not responsible for any data loss that may occur. Use at your own risk.
|
||||
|
||||
## Usage
|
||||
The script provides a menu of available actions. Make as many changes as you require and review them as you go. No changes are made to the Vault until they are explicitly committed.
|
||||
Run `obsidian-metadata` from the command line to invoke the script. Add `--help` to view additional options.
|
||||
|
||||
[](https://asciinema.org/a/553464)
|
||||
Obsidian-metadata provides a menu of sub-commands.
|
||||
|
||||
**Vault Actions**
|
||||
- Backup: Create a backup of the vault.
|
||||
- Delete Backup: Delete a backup of the vault.
|
||||
|
||||
**Inspect Metadata**
|
||||
- View all metadata in the vault
|
||||
|
||||
**Filter Notes in Scope**:
|
||||
Limit the scope of notes to be processed with a regex.
|
||||
- Apply regex: Set a regex to limit scope
|
||||
- List notes in scope: List notes that will be processed.
|
||||
|
||||
**Add Metadata**
|
||||
- Add metadata to the frontmatter
|
||||
- Add to inline metadata (Not yet implemented)
|
||||
- Add to inline tag (Not yet implemented)
|
||||
|
||||
**Rename Metadata**
|
||||
- Rename a key
|
||||
- Rename a value
|
||||
- rename an inline tag
|
||||
|
||||
**Delete Metadata**
|
||||
- Delete a key and associated values
|
||||
- Delete a value from a key
|
||||
- Delete an inline tag
|
||||
|
||||
**Review Changes**
|
||||
- View a diff of the changes that will be made
|
||||
|
||||
**Commit Changes**
|
||||
- Commit changes to the vault
|
||||
|
||||
### Configuration
|
||||
`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.
|
||||
|
||||
17
poetry.lock
generated
17
poetry.lock
generated
@@ -498,6 +498,17 @@ python-versions = ">=3.6"
|
||||
[package.extras]
|
||||
plugins = ["importlib-metadata"]
|
||||
|
||||
[[package]]
|
||||
name = "pysnooper"
|
||||
version = "1.1.1"
|
||||
description = "A poor man's debugger for Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
tests = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.1"
|
||||
@@ -814,7 +825,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "acbde0d9374261931e4f12f4ed8fcbc543008f68c4aed0a6748280a3999b3394"
|
||||
content-hash = "c2deb1e448642f9084ed1e3dfaf96ed8458bac720e63a531acf3507fd1cbe47d"
|
||||
|
||||
[metadata.files]
|
||||
absolufy-imports = [
|
||||
@@ -1124,6 +1135,10 @@ pygments = [
|
||||
{file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
|
||||
{file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"},
|
||||
]
|
||||
pysnooper = [
|
||||
{file = "PySnooper-1.1.1-py2.py3-none-any.whl", hash = "sha256:378f13d731a3e04d3d0350e5f295bdd0f1b49fc8a8b8bf2067fe1e5290bd20be"},
|
||||
{file = "PySnooper-1.1.1.tar.gz", hash = "sha256:d17dc91cca1593c10230dce45e46b1d3ff0f8910f0c38e941edf6ba1260b3820"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
|
||||
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
pep8-naming = "^0.13.3"
|
||||
poethepoet = "^0.18.0"
|
||||
pre-commit = "^2.21.0"
|
||||
pysnooper = "^1.1.1"
|
||||
ruff = "^0.0.217"
|
||||
typeguard = "^2.13.3"
|
||||
types-python-dateutil = "^2.8.19.5"
|
||||
@@ -101,7 +102,7 @@
|
||||
]
|
||||
src = ["src", "tests"]
|
||||
target-version = "py310"
|
||||
unfixable = ["ERA001", "F401", "F401", "UP007"]
|
||||
unfixable = ["ERA001", "F401", "F841", "UP007"]
|
||||
|
||||
[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report
|
||||
exclude_lines = [
|
||||
|
||||
@@ -16,8 +16,8 @@ __all__ = [
|
||||
"alerts",
|
||||
"clean_dictionary",
|
||||
"clear_screen",
|
||||
"dict_values_to_lists_strings",
|
||||
"dict_contains",
|
||||
"dict_values_to_lists_strings",
|
||||
"docstring_parameter",
|
||||
"LoggerManager",
|
||||
"remove_markdown_sections",
|
||||
|
||||
@@ -70,29 +70,50 @@ def main(
|
||||
None, "--version", help="Print version and exit", callback=version_callback, is_eager=True
|
||||
),
|
||||
) -> None:
|
||||
r"""A script to make batch updates to metadata in an Obsidian vault.
|
||||
r"""A script to make batch updates to metadata in an Obsidian vault. No changes are made to the Vault until they are explicitly committed.
|
||||
|
||||
[bold] [/]
|
||||
[bold underline]Features:[/]
|
||||
|
||||
- [code]in-text tags:[/] delete every occurrence
|
||||
- [code]in-text tags:[/] Rename tag ([dim]#tag1[/] -> [dim]#tag2[/])
|
||||
- [code]frontmatter:[/] Delete a key matching a regex pattern and all associated values
|
||||
- [code]frontmatter:[/] Rename a key
|
||||
- [code]frontmatter:[/] Delete a value matching a regex pattern from a specified key
|
||||
- [code]frontmatter:[/] Rename a value from a specified key
|
||||
- [code]inline metadata:[/] Delete a key matching a regex pattern and all associated values
|
||||
- [code]inline metadata:[/] Rename a key
|
||||
- [code]inline metadata:[/] Delete a value matching a regex pattern from a specified key
|
||||
- [code]inline metadata:[/] Rename a value from a specified key
|
||||
- [code]vault:[/] Create a backup of the Obsidian vault.
|
||||
|
||||
[bold underline]Usage:[/]
|
||||
[tan]Obsidian-metadata[/] allows you to make batch updates to metadata in an Obsidian vault. Once you have made your changes, review them prior to committing them to the vault. The script provides a menu of available actions. Make as many changes as you require and review them as you go. No changes are made to the Vault until they are explicitly committed.
|
||||
|
||||
[bold underline]It is strongly recommended that you back up your vault prior to committing changes.[/] This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed. The author of this script is not responsible for any data loss that may occur. Use at your own risk.
|
||||
|
||||
[bold underline]Configuration:[/]
|
||||
Configuration is specified in a configuration file. On First run, this file will be created at [tan]~/.{0}.env[/]. Any options specified on the command line will override the configuration file.
|
||||
|
||||
[bold underline]Usage:[/]
|
||||
[tan]Obsidian-metadata[/] provides a menu of sub-commands.
|
||||
|
||||
[bold underline]Vault Actions[/]
|
||||
• Backup: Create a backup of the vault.
|
||||
• Delete Backup: Delete a backup of the vault.
|
||||
|
||||
[bold underline]Inspect Metadata[/]
|
||||
• View all metadata in the vault
|
||||
|
||||
[bold underline]Filter Notes in Scope[/]
|
||||
Limit the scope of notes to be processed with a regex.
|
||||
• Apply regex: Set a regex to limit scope
|
||||
• List notes in scope: List notes that will be processed.
|
||||
|
||||
[bold underline]Add Metadata[/]
|
||||
• Add metadata to the frontmatter
|
||||
• [dim]Add to inline metadata (Not yet implemented)[/]
|
||||
• [dim]Add to inline tag (Not yet implemented)[/]
|
||||
|
||||
[bold underline]Rename Metadata[/]
|
||||
• Rename a key
|
||||
• Rename a value
|
||||
• rename an inline tag
|
||||
|
||||
[bold underline]Delete Metadata[/]
|
||||
• Delete a key and associated values
|
||||
• Delete a value from a key
|
||||
• Delete an inline tag
|
||||
|
||||
[bold underline]Review Changes[/]
|
||||
• View a diff of the changes that will be made
|
||||
|
||||
[bold underline]Commit Changes[/]
|
||||
• Commit changes to the vault
|
||||
|
||||
"""
|
||||
# Instantiate logger
|
||||
alerts.LoggerManager( # pragma: no cover
|
||||
@@ -134,7 +155,7 @@ def main(
|
||||
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.application_main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Shared models."""
|
||||
from obsidian_metadata.models.enums import MetadataType # isort: skip
|
||||
from obsidian_metadata.models.patterns import Patterns # isort: skip
|
||||
from obsidian_metadata.models.metadata import (
|
||||
Frontmatter,
|
||||
@@ -12,13 +13,14 @@ from obsidian_metadata.models.vault import Vault
|
||||
from obsidian_metadata.models.application import Application # isort: skip
|
||||
|
||||
__all__ = [
|
||||
"Application",
|
||||
"Frontmatter",
|
||||
"InlineMetadata",
|
||||
"InlineTags",
|
||||
"LoggerManager",
|
||||
"MetadataType",
|
||||
"Note",
|
||||
"Patterns",
|
||||
"Application",
|
||||
"Vault",
|
||||
"VaultMetadata",
|
||||
]
|
||||
|
||||
@@ -5,12 +5,13 @@ from typing import Any
|
||||
|
||||
import questionary
|
||||
from rich import print
|
||||
|
||||
from textwrap import dedent
|
||||
from obsidian_metadata._config import VaultConfig
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata.models import Patterns, Vault
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata.models.questions import Questions
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
|
||||
PATTERNS = Patterns()
|
||||
|
||||
@@ -38,102 +39,224 @@ class Application:
|
||||
log.info(f"Indexed {self.vault.num_notes()} notes from {self.vault.vault_path}")
|
||||
self.questions = Questions(vault=self.vault)
|
||||
|
||||
def main_app(self) -> None:
|
||||
def application_main(self) -> None:
|
||||
"""Questions for the main application."""
|
||||
self.load_vault()
|
||||
|
||||
while True:
|
||||
print("\n")
|
||||
self.vault.info()
|
||||
|
||||
match self.questions.ask_main_application(): # noqa: E999
|
||||
case None:
|
||||
break
|
||||
match self.questions.ask_application_main(): # noqa: E999
|
||||
case "vault_actions":
|
||||
self.application_vault()
|
||||
case "inspect_metadata":
|
||||
self.application_inspect_metadata()
|
||||
case "filter_notes":
|
||||
self.load_vault(path_filter=self.questions.ask_for_filter_path())
|
||||
case "all_metadata":
|
||||
self.vault.metadata.print_metadata()
|
||||
case "backup_vault":
|
||||
self.vault.backup()
|
||||
case "delete_backup":
|
||||
self.vault.delete_backup()
|
||||
case "list_notes":
|
||||
self.vault.list_editable_notes()
|
||||
case "rename_inline_tag":
|
||||
self.rename_inline_tag()
|
||||
case "delete_inline_tag":
|
||||
self.delete_inline_tag()
|
||||
case "rename_key":
|
||||
self.rename_key()
|
||||
case "delete_key":
|
||||
self.delete_key()
|
||||
case "rename_value":
|
||||
self.rename_value()
|
||||
case "delete_value":
|
||||
self.delete_value()
|
||||
self.application_filter()
|
||||
case "add_metadata":
|
||||
self.application_add_metadata()
|
||||
case "rename_metadata":
|
||||
self.application_rename_metadata()
|
||||
case "delete_metadata":
|
||||
self.application_delete_metadata()
|
||||
case "review_changes":
|
||||
self.review_changes()
|
||||
case "commit_changes":
|
||||
if self.commit_changes():
|
||||
break
|
||||
|
||||
log.error("Commit failed. Please run with -vvv for more info.")
|
||||
break
|
||||
|
||||
case "abort":
|
||||
case _:
|
||||
break
|
||||
|
||||
print("Done!")
|
||||
return
|
||||
|
||||
def rename_key(self) -> None:
|
||||
"""Renames a key in the vault."""
|
||||
def application_add_metadata(self) -> None:
|
||||
"""Add metadata."""
|
||||
help_text = """
|
||||
[bold underline]Add Metadata[/]
|
||||
Add new metadata to your vault. Currently only supports
|
||||
adding to the frontmatter of a note.\n
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
original_key = self.questions.ask_for_existing_key(
|
||||
question="Which key would you like to rename?"
|
||||
area = self.questions.ask_area()
|
||||
match area:
|
||||
case MetadataType.FRONTMATTER:
|
||||
key = self.questions.ask_new_key(question="Enter the key for the new metadata")
|
||||
if key is None:
|
||||
return
|
||||
|
||||
value = self.questions.ask_new_value(
|
||||
question="Enter the value for the new metadata"
|
||||
)
|
||||
if original_key is None:
|
||||
if value is None:
|
||||
return
|
||||
|
||||
new_key = self.questions.ask_for_new_key()
|
||||
if new_key is None:
|
||||
return
|
||||
|
||||
num_changed = self.vault.rename_metadata(original_key, new_key)
|
||||
num_changed = self.vault.add_metadata(area, key, value)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Added metadata to {num_changed} notes")
|
||||
|
||||
case MetadataType.INLINE:
|
||||
alerts.warning(f"Adding metadata to {area} is not supported yet")
|
||||
|
||||
case MetadataType.TAGS:
|
||||
alerts.warning(f"Adding metadata to {area} is not supported yet")
|
||||
|
||||
case _:
|
||||
return
|
||||
|
||||
def application_filter(self) -> None:
|
||||
"""Filter notes."""
|
||||
help_text = """
|
||||
[bold underline]Filter Notes[/]
|
||||
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.\n
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
choices = [
|
||||
{"name": "Apply regex filter", "value": "apply_filter"},
|
||||
{"name": "List notes in scope", "value": "list_notes"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Select an action"):
|
||||
case "apply_filter":
|
||||
|
||||
path_filter = self.questions.ask_filter_path()
|
||||
if path_filter is None:
|
||||
return
|
||||
|
||||
if path_filter == "":
|
||||
path_filter = None
|
||||
|
||||
self.load_vault(path_filter=path_filter)
|
||||
|
||||
total_notes = self.vault.num_notes() + self.vault.num_excluded_notes()
|
||||
|
||||
if path_filter is None:
|
||||
alerts.success(f"Loaded all {total_notes} total notes")
|
||||
else:
|
||||
alerts.success(
|
||||
f"Renamed [reverse]{original_key}[/] to [reverse]{new_key}[/] in {num_changed} notes"
|
||||
f"Loaded {self.vault.num_notes()} notes from {total_notes} total notes"
|
||||
)
|
||||
|
||||
def rename_inline_tag(self) -> None:
|
||||
"""Rename an inline tag."""
|
||||
case "list_notes":
|
||||
self.vault.list_editable_notes()
|
||||
print("\n")
|
||||
|
||||
original_tag = self.questions.ask_for_existing_inline_tag(question="Which tag to rename?")
|
||||
if original_tag is None:
|
||||
case _:
|
||||
return
|
||||
|
||||
new_tag = self.questions.ask_for_new_tag("New tag")
|
||||
if new_tag is None:
|
||||
def application_inspect_metadata(self) -> None:
|
||||
"""View metadata."""
|
||||
help_text = """
|
||||
[bold underline]View Metadata[/]
|
||||
Inspect the metadata in your vault. Note, uncommitted changes will be reflected in these reports\n
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
choices = [
|
||||
{"name": "View all metadata", "value": "all_metadata"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Select a vault action"):
|
||||
case "all_metadata":
|
||||
self.vault.metadata.print_metadata()
|
||||
case _:
|
||||
return
|
||||
|
||||
num_changed = self.vault.rename_inline_tag(original_tag, new_tag)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
def application_vault(self) -> None:
|
||||
"""Vault actions."""
|
||||
help_text = """
|
||||
[bold underline]Vault Actions[/]
|
||||
Create or delete a backup of your vault.\n
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
choices = [
|
||||
{"name": "Backup vault", "value": "backup_vault"},
|
||||
{"name": "Delete vault backup", "value": "delete_backup"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Select a vault action"):
|
||||
case "backup_vault":
|
||||
self.vault.backup()
|
||||
case "delete_backup":
|
||||
self.vault.delete_backup()
|
||||
case _:
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
f"Renamed [reverse]{original_tag}[/] to [reverse]{new_tag}[/] in {num_changed} notes"
|
||||
)
|
||||
def application_delete_metadata(self) -> None:
|
||||
help_text = """
|
||||
[bold underline]Delete Metadata[/]
|
||||
Delete either a key and all associated values, or a specific value.\n
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
choices = [
|
||||
{"name": "Delete key", "value": "delete_key"},
|
||||
{"name": "Delete value", "value": "delete_value"},
|
||||
{"name": "Delete inline tag", "value": "delete_inline_tag"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select a metadata type to delete"
|
||||
):
|
||||
case "delete_key":
|
||||
self.delete_key()
|
||||
case "delete_value":
|
||||
self.delete_value()
|
||||
case "delete_inline_tag":
|
||||
self.delete_inline_tag()
|
||||
case _:
|
||||
return
|
||||
|
||||
def application_rename_metadata(self) -> None:
|
||||
"""Rename metadata."""
|
||||
help_text = """
|
||||
[bold underline]Rename Metadata[/]\n
|
||||
Select the type of metadata to rename.\n
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
choices = [
|
||||
{"name": "Rename key", "value": "rename_key"},
|
||||
{"name": "Rename value", "value": "rename_value"},
|
||||
{"name": "Rename inline tag", "value": "rename_inline_tag"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
match self.questions.ask_selection(
|
||||
choices=choices, question="Select a metadata type to rename"
|
||||
):
|
||||
case "rename_key":
|
||||
self.rename_key()
|
||||
case "rename_value":
|
||||
self.rename_value()
|
||||
case "rename_inline_tag":
|
||||
self.rename_inline_tag()
|
||||
case _:
|
||||
return
|
||||
|
||||
###########################################################################
|
||||
def delete_inline_tag(self) -> None:
|
||||
"""Delete an inline tag."""
|
||||
tag = self.questions.ask_for_existing_inline_tag(
|
||||
question="Which tag would you like to delete?"
|
||||
)
|
||||
tag = self.questions.ask_existing_inline_tag(question="Which tag would you like to delete?")
|
||||
|
||||
num_changed = self.vault.delete_inline_tag(tag)
|
||||
if num_changed == 0:
|
||||
@@ -145,7 +268,7 @@ class Application:
|
||||
|
||||
def delete_key(self) -> None:
|
||||
"""Delete a key from the vault."""
|
||||
key_to_delete = self.questions.ask_for_existing_keys_regex(
|
||||
key_to_delete = self.questions.ask_existing_keys_regex(
|
||||
question="Regex for the key(s) you'd like to delete?"
|
||||
)
|
||||
if key_to_delete is None:
|
||||
@@ -162,42 +285,14 @@ class Application:
|
||||
|
||||
return
|
||||
|
||||
def rename_value(self) -> None:
|
||||
"""Rename a value in the vault."""
|
||||
key = self.questions.ask_for_existing_key(
|
||||
question="Which key contains the value to rename?"
|
||||
)
|
||||
if key is None:
|
||||
return
|
||||
|
||||
question_key = Questions(vault=self.vault, key=key)
|
||||
value = question_key.ask_for_existing_value(
|
||||
question="Which value would you like to rename?"
|
||||
)
|
||||
if value is None:
|
||||
return
|
||||
|
||||
new_value = question_key.ask_for_new_value()
|
||||
if new_value is None:
|
||||
return
|
||||
|
||||
num_changes = self.vault.rename_metadata(key, value, new_value)
|
||||
if num_changes == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Renamed '{key}:{value}' to '{key}:{new_value}' in {num_changes} notes")
|
||||
|
||||
def delete_value(self) -> None:
|
||||
"""Delete a value from the vault."""
|
||||
key = self.questions.ask_for_existing_key(
|
||||
question="Which key contains the value to delete?"
|
||||
)
|
||||
key = self.questions.ask_existing_key(question="Which key contains the value to delete?")
|
||||
if key is None:
|
||||
return
|
||||
|
||||
questions2 = Questions(vault=self.vault, key=key)
|
||||
value = questions2.ask_for_existing_value_regex(question="Regex for the value to delete")
|
||||
value = questions2.ask_existing_value_regex(question="Regex for the value to delete")
|
||||
if value is None:
|
||||
return
|
||||
|
||||
@@ -212,6 +307,71 @@ class Application:
|
||||
|
||||
return
|
||||
|
||||
def rename_key(self) -> None:
|
||||
"""Renames a key in the vault."""
|
||||
|
||||
original_key = self.questions.ask_existing_key(
|
||||
question="Which key would you like to rename?"
|
||||
)
|
||||
if original_key is None:
|
||||
return
|
||||
|
||||
new_key = self.questions.ask_new_key()
|
||||
if new_key is None:
|
||||
return
|
||||
|
||||
num_changed = self.vault.rename_metadata(original_key, new_key)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
f"Renamed [reverse]{original_key}[/] to [reverse]{new_key}[/] in {num_changed} notes"
|
||||
)
|
||||
|
||||
def rename_inline_tag(self) -> None:
|
||||
"""Rename an inline tag."""
|
||||
|
||||
original_tag = self.questions.ask_existing_inline_tag(question="Which tag to rename?")
|
||||
if original_tag is None:
|
||||
return
|
||||
|
||||
new_tag = self.questions.ask_new_tag("New tag")
|
||||
if new_tag is None:
|
||||
return
|
||||
|
||||
num_changed = self.vault.rename_inline_tag(original_tag, new_tag)
|
||||
if num_changed == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(
|
||||
f"Renamed [reverse]{original_tag}[/] to [reverse]{new_tag}[/] in {num_changed} notes"
|
||||
)
|
||||
return
|
||||
|
||||
def rename_value(self) -> None:
|
||||
"""Rename a value in the vault."""
|
||||
key = self.questions.ask_existing_key(question="Which key contains the value to rename?")
|
||||
if key is None:
|
||||
return
|
||||
|
||||
question_key = Questions(vault=self.vault, key=key)
|
||||
value = question_key.ask_existing_value(question="Which value would you like to rename?")
|
||||
if value is None:
|
||||
return
|
||||
|
||||
new_value = question_key.ask_new_value()
|
||||
if new_value is None:
|
||||
return
|
||||
|
||||
num_changes = self.vault.rename_metadata(key, value, new_value)
|
||||
if num_changes == 0:
|
||||
alerts.warning(f"No notes were changed")
|
||||
return
|
||||
|
||||
alerts.success(f"Renamed '{key}:{value}' to '{key}:{new_value}' in {num_changes} notes")
|
||||
|
||||
def review_changes(self) -> None:
|
||||
"""Review all changes in the vault."""
|
||||
changed_notes = self.vault.get_changed_notes()
|
||||
@@ -239,7 +399,7 @@ class Application:
|
||||
choices.append({"name": "Return", "value": "return"})
|
||||
|
||||
while True:
|
||||
note_to_review = self.questions.ask_for_selection(
|
||||
note_to_review = self.questions.ask_selection(
|
||||
choices=choices,
|
||||
question="Select a new to view the diff",
|
||||
)
|
||||
|
||||
11
src/obsidian_metadata/models/enums.py
Normal file
11
src/obsidian_metadata/models/enums.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Enum classes for the obsidian_metadata package."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MetadataType(Enum):
|
||||
"""Enum class for the type of metadata."""
|
||||
|
||||
FRONTMATTER = "Frontmatter"
|
||||
INLINE = "Inline Metadata"
|
||||
TAGS = "Inline Tags"
|
||||
@@ -197,6 +197,40 @@ class Frontmatter:
|
||||
|
||||
return dict_values_to_lists_strings(frontmatter, strip_null_values=True)
|
||||
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool:
|
||||
"""Add a key and value to the frontmatter.
|
||||
|
||||
Args:
|
||||
key (str): Key to add.
|
||||
value (str, optional): Value to add.
|
||||
|
||||
Returns:
|
||||
bool: True if the metadata was added
|
||||
"""
|
||||
if value is None:
|
||||
if key not in self.dict:
|
||||
self.dict[key] = []
|
||||
return True
|
||||
return False
|
||||
|
||||
if key not in self.dict:
|
||||
if isinstance(value, list):
|
||||
self.dict[key] = value
|
||||
return True
|
||||
|
||||
self.dict[key] = [value]
|
||||
return True
|
||||
|
||||
if key in self.dict and value not in self.dict[key]:
|
||||
if isinstance(value, list):
|
||||
self.dict[key].extend(value)
|
||||
return True
|
||||
|
||||
self.dict[key].append(value)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if a key or value exists in the metadata.
|
||||
|
||||
@@ -314,6 +348,19 @@ class InlineMetadata:
|
||||
"""
|
||||
return f"InlineMetadata(inline_metadata={self.dict})"
|
||||
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool:
|
||||
"""Add a key and value to the frontmatter.
|
||||
|
||||
Args:
|
||||
key (str): Key to add.
|
||||
value (str, optional): Value to add.
|
||||
|
||||
Returns:
|
||||
bool: True if the metadata was added
|
||||
"""
|
||||
# TODO: implement adding to inline metadata which requires knowing where in the note the metadata is to be added. In addition, unlike frontmatter, it is not possible to have multiple values for a key.
|
||||
pass
|
||||
|
||||
def _grab_inline_metadata(self, file_content: str) -> dict[str, list[str]]:
|
||||
"""Grab inline metadata from a note.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ from pathlib import Path
|
||||
|
||||
import rich.repr
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
@@ -14,6 +16,7 @@ from obsidian_metadata.models import (
|
||||
Frontmatter,
|
||||
InlineMetadata,
|
||||
InlineTags,
|
||||
MetadataType,
|
||||
Patterns,
|
||||
)
|
||||
|
||||
@@ -61,6 +64,31 @@ class Note:
|
||||
yield "inline_tags", self.inline_tags
|
||||
yield "inline_metadata", self.inline_metadata
|
||||
|
||||
def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> bool:
|
||||
"""Adds metadata to the note.
|
||||
|
||||
Args:
|
||||
area (MetadataType): Area to add metadata to.
|
||||
key (str): Key to add.
|
||||
value (str, optional): Value to add.
|
||||
|
||||
Returns:
|
||||
bool: Whether the metadata was added.
|
||||
"""
|
||||
if area is MetadataType.FRONTMATTER and self.frontmatter.add(key, value):
|
||||
self.replace_frontmatter()
|
||||
return True
|
||||
|
||||
if area is MetadataType.INLINE:
|
||||
# TODO: implement adding to inline metadata
|
||||
pass
|
||||
|
||||
if area is MetadataType.TAGS:
|
||||
# TODO: implement adding to intext tags
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def append(self, string_to_append: str, allow_multiple: bool = False) -> None:
|
||||
"""Appends a string to the end of a note.
|
||||
|
||||
@@ -225,11 +253,15 @@ class Note:
|
||||
|
||||
diff = difflib.Differ()
|
||||
result = list(diff.compare(a, b))
|
||||
table = Table(title=f"\nDiff of {self.note_path.name}", show_header=False, min_width=50)
|
||||
|
||||
for line in result:
|
||||
if line.startswith("+"):
|
||||
print(f"\033[92m{line}\033[0m")
|
||||
table.add_row(line, style="green")
|
||||
elif line.startswith("-"):
|
||||
print(f"\033[91m{line}\033[0m")
|
||||
table.add_row(line, style="red")
|
||||
|
||||
Console().print(table)
|
||||
|
||||
def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None:
|
||||
"""Substitutes text within the note.
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Any
|
||||
import questionary
|
||||
import typer
|
||||
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from obsidian_metadata.models.patterns import Patterns
|
||||
from obsidian_metadata.models.vault import Vault
|
||||
|
||||
@@ -72,198 +73,19 @@ class Questions:
|
||||
self.vault = vault
|
||||
self.key = key
|
||||
|
||||
def ask_confirm(self, question: str, default: bool = True) -> bool: # pragma: no cover
|
||||
"""Ask the user to confirm an action.
|
||||
|
||||
Args:
|
||||
question (str): The question to ask.
|
||||
default (bool, optional): The default value. Defaults to True.
|
||||
def _validate_existing_inline_tag(self, text: str) -> bool | str:
|
||||
"""Validates an existing inline tag.
|
||||
|
||||
Returns:
|
||||
bool: True if the user confirms, otherwise False.
|
||||
bool | str: True if the tag is valid, otherwise a string with the error message.
|
||||
"""
|
||||
return questionary.confirm(question, default=default, style=self.style).ask()
|
||||
if len(text) < 1:
|
||||
return "Tag cannot be empty"
|
||||
|
||||
def ask_main_application(self) -> str: # pragma: no cover
|
||||
"""Selectable list for the main application interface.
|
||||
if not self.vault.contains_inline_tag(text):
|
||||
return f"'{text}' does not exist as a tag in the vault"
|
||||
|
||||
Args:
|
||||
style (questionary.Style): The style to use for the question.
|
||||
|
||||
Returns:
|
||||
str: The selected application.
|
||||
"""
|
||||
return questionary.select(
|
||||
"What do you want to do?",
|
||||
choices=[
|
||||
questionary.Separator("\n-- VAULT ACTIONS -----------------"),
|
||||
{"name": "Backup vault", "value": "backup_vault"},
|
||||
{"name": "Delete vault backup", "value": "delete_backup"},
|
||||
{"name": "View all metadata", "value": "all_metadata"},
|
||||
{"name": "List notes in scope", "value": "list_notes"},
|
||||
{
|
||||
"name": "Filter the notes being processed by their path",
|
||||
"value": "filter_notes",
|
||||
},
|
||||
questionary.Separator("\n-- INLINE TAG ACTIONS ---------"),
|
||||
questionary.Separator("Tags in the note body"),
|
||||
{
|
||||
"name": "Rename an inline tag",
|
||||
"value": "rename_inline_tag",
|
||||
},
|
||||
{
|
||||
"name": "Delete an inline tag",
|
||||
"value": "delete_inline_tag",
|
||||
},
|
||||
questionary.Separator("\n-- METADATA ACTIONS -----------"),
|
||||
questionary.Separator("Frontmatter or inline metadata"),
|
||||
{"name": "Rename Key", "value": "rename_key"},
|
||||
{"name": "Delete Key", "value": "delete_key"},
|
||||
{"name": "Rename Value", "value": "rename_value"},
|
||||
{"name": "Delete Value", "value": "delete_value"},
|
||||
questionary.Separator("\n-- REVIEW/COMMIT CHANGES ------"),
|
||||
{"name": "Review changes", "value": "review_changes"},
|
||||
{"name": "Commit changes", "value": "commit_changes"},
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Quit", "value": "abort"},
|
||||
],
|
||||
use_shortcuts=False,
|
||||
style=self.style,
|
||||
).ask()
|
||||
|
||||
def ask_for_filter_path(self) -> str: # pragma: no cover
|
||||
"""Ask the user for the path to the filter file.
|
||||
|
||||
Returns:
|
||||
str: The regex to use for filtering.
|
||||
"""
|
||||
filter_path_regex = questionary.path(
|
||||
"Regex to filter the notes being processed by their path:",
|
||||
only_directories=False,
|
||||
validate=self._validate_valid_vault_regex,
|
||||
).ask()
|
||||
if filter_path_regex is None:
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
return filter_path_regex
|
||||
|
||||
def ask_for_selection(
|
||||
self, choices: list[Any], question: str = "Select an option"
|
||||
) -> Any: # pragma: no cover
|
||||
"""Ask the user to select an item from a list.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Select an option".
|
||||
choices (list[Any]): The list of choices.
|
||||
|
||||
Returns:
|
||||
any: The selected item value.
|
||||
"""
|
||||
return questionary.select(
|
||||
"Select an item:",
|
||||
choices=choices,
|
||||
use_shortcuts=False,
|
||||
style=self.style,
|
||||
).ask()
|
||||
|
||||
def ask_for_existing_inline_tag(self, question: str = "Enter a tag") -> str: # pragma: no cover
|
||||
"""Ask the user for an existing inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_existing_inline_tag,
|
||||
).ask()
|
||||
|
||||
def ask_for_new_tag(self, question: str = "New tag name") -> str: # pragma: no cover
|
||||
"""Ask the user for a new inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_tag,
|
||||
).ask()
|
||||
|
||||
def ask_for_existing_key(self, question: str = "Enter a key") -> str: # pragma: no cover
|
||||
"""Ask the user for a metadata key.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Enter a key".
|
||||
|
||||
Returns:
|
||||
str: A metadata key that exists in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_key_exists,
|
||||
).ask()
|
||||
|
||||
def ask_for_existing_keys_regex(
|
||||
self, question: str = "Regex for keys"
|
||||
) -> str: # pragma: no cover
|
||||
"""Ask the user for a regex for metadata keys.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Regex for keys".
|
||||
|
||||
Returns:
|
||||
str: A regex for metadata keys that exist in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_key_exists_regex,
|
||||
).ask()
|
||||
|
||||
def ask_for_existing_value_regex(
|
||||
self, question: str = "Regex for values"
|
||||
) -> str: # pragma: no cover
|
||||
"""Ask the user for a regex for metadata values.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Regex for values".
|
||||
|
||||
Returns:
|
||||
str: A regex for metadata values that exist in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_value_exists_regex,
|
||||
).ask()
|
||||
|
||||
def ask_for_existing_value(self, question: str = "Enter a value") -> str: # pragma: no cover
|
||||
"""Ask the user for a metadata value.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Enter a value".
|
||||
|
||||
Returns:
|
||||
str: A metadata value.
|
||||
"""
|
||||
return questionary.text(question, validate=self._validate_value).ask()
|
||||
|
||||
def ask_for_new_key(self, question: str = "New key name") -> str: # pragma: no cover
|
||||
"""Ask the user for a new metadata key.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "New key name".
|
||||
|
||||
Returns:
|
||||
str: A new metadata key.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_key,
|
||||
).ask()
|
||||
|
||||
def ask_for_new_value(self, question: str = "New value") -> str: # pragma: no cover
|
||||
"""Ask the user for a new metadata value.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "New value".
|
||||
|
||||
Returns:
|
||||
str: A new metadata value.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_value,
|
||||
).ask()
|
||||
return True
|
||||
|
||||
def _validate_key_exists(self, text: str) -> bool | str:
|
||||
"""Validates a valid key.
|
||||
@@ -298,42 +120,6 @@ class Questions:
|
||||
|
||||
return True
|
||||
|
||||
def _validate_existing_inline_tag(self, text: str) -> bool | str:
|
||||
"""Validates an existing inline tag.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the tag is valid, otherwise a string with the error message.
|
||||
"""
|
||||
if len(text) < 1:
|
||||
return "Tag cannot be empty"
|
||||
|
||||
if not self.vault.contains_inline_tag(text):
|
||||
return f"'{text}' does not exist as a tag in the vault"
|
||||
|
||||
return True
|
||||
|
||||
def _validate_valid_vault_regex(self, text: str) -> bool | str:
|
||||
"""Validates a valid regex.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the regex is valid, otherwise a string with the error message.
|
||||
"""
|
||||
if len(text) < 1:
|
||||
return "Regex cannot be empty"
|
||||
|
||||
try:
|
||||
re.compile(text)
|
||||
except re.error as error:
|
||||
return f"Invalid regex: {error}"
|
||||
|
||||
if self.vault is not None:
|
||||
for subdir in list(self.vault.vault_path.glob("**/*")):
|
||||
if re.search(text, str(subdir)):
|
||||
return True
|
||||
return "Regex does not match paths in the vault"
|
||||
|
||||
return True
|
||||
|
||||
def _validate_new_key(self, text: str) -> bool | str:
|
||||
"""Validate the tag name.
|
||||
|
||||
@@ -368,6 +154,42 @@ class Questions:
|
||||
|
||||
return True
|
||||
|
||||
def _validate_new_value(self, text: str) -> bool | str:
|
||||
"""Validate a new value.
|
||||
|
||||
Args:
|
||||
text (str): The value to validate.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the value is valid, otherwise a string with the error message.
|
||||
"""
|
||||
if len(text) < 1:
|
||||
return "Value cannot be empty"
|
||||
|
||||
if self.key is not None and self.vault.metadata.contains(self.key, text):
|
||||
return f"{self.key}:{text} already exists"
|
||||
|
||||
return True
|
||||
|
||||
def _validate_valid_vault_regex(self, text: str) -> bool | str:
|
||||
"""Validates a valid regex.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the regex is valid, otherwise a string with the error message.
|
||||
"""
|
||||
try:
|
||||
re.compile(text)
|
||||
except re.error as error:
|
||||
return f"Invalid regex: {error}"
|
||||
|
||||
if self.vault is not None:
|
||||
for subdir in list(self.vault.vault_path.glob("**/*")):
|
||||
if re.search(text, str(subdir)):
|
||||
return True
|
||||
return "Regex does not match paths in the vault"
|
||||
|
||||
return True
|
||||
|
||||
def _validate_value(self, text: str) -> bool | str:
|
||||
"""Validate the value.
|
||||
|
||||
@@ -407,19 +229,188 @@ class Questions:
|
||||
|
||||
return True
|
||||
|
||||
def _validate_new_value(self, text: str) -> bool | str:
|
||||
"""Validate a new value.
|
||||
|
||||
Args:
|
||||
text (str): The value to validate.
|
||||
def ask_area(self) -> MetadataType | str: # pragma: no cover
|
||||
"""Ask the user for the metadata area to work on.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the value is valid, otherwise a string with the error message.
|
||||
MetadataType: The metadata area to work on.
|
||||
"""
|
||||
if len(text) < 1:
|
||||
return "Value cannot be empty"
|
||||
choices = []
|
||||
for metadata_type in MetadataType:
|
||||
choices.append({"name": metadata_type.value, "value": metadata_type})
|
||||
|
||||
if self.key is not None and self.vault.metadata.contains(self.key, text):
|
||||
return f"{self.key}:{text} already exists"
|
||||
choices.append(questionary.Separator()) # type: ignore [arg-type]
|
||||
choices.append({"name": "Cancel", "value": "cancel"})
|
||||
return self.ask_selection(choices=choices, question="Select the type of metadata")
|
||||
|
||||
return True
|
||||
def ask_confirm(self, question: str, default: bool = True) -> bool: # pragma: no cover
|
||||
"""Ask the user to confirm an action.
|
||||
|
||||
Args:
|
||||
question (str): The question to ask.
|
||||
default (bool, optional): The default value. Defaults to True.
|
||||
|
||||
Returns:
|
||||
bool: True if the user confirms, otherwise False.
|
||||
"""
|
||||
return questionary.confirm(question, default=default, style=self.style).ask()
|
||||
|
||||
def ask_existing_inline_tag(self, question: str = "Enter a tag") -> str: # pragma: no cover
|
||||
"""Ask the user for an existing inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_existing_inline_tag,
|
||||
).ask()
|
||||
|
||||
def ask_existing_key(self, question: str = "Enter a key") -> str: # pragma: no cover
|
||||
"""Ask the user for a metadata key.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Enter a key".
|
||||
|
||||
Returns:
|
||||
str: A metadata key that exists in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_key_exists,
|
||||
).ask()
|
||||
|
||||
def ask_existing_keys_regex(self, question: str = "Regex for keys") -> str: # pragma: no cover
|
||||
"""Ask the user for a regex for metadata keys.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Regex for keys".
|
||||
|
||||
Returns:
|
||||
str: A regex for metadata keys that exist in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_key_exists_regex,
|
||||
).ask()
|
||||
|
||||
def ask_existing_value(self, question: str = "Enter a value") -> str: # pragma: no cover
|
||||
"""Ask the user for a metadata value.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Enter a value".
|
||||
|
||||
Returns:
|
||||
str: A metadata value.
|
||||
"""
|
||||
return questionary.text(question, validate=self._validate_value).ask()
|
||||
|
||||
def ask_filter_path(self) -> str: # pragma: no cover
|
||||
"""Ask the user for the path to the filter file.
|
||||
|
||||
Returns:
|
||||
str: The regex to use for filtering.
|
||||
"""
|
||||
filter_path_regex = questionary.path(
|
||||
"Regex to filter the notes being processed by their path:",
|
||||
only_directories=False,
|
||||
validate=self._validate_valid_vault_regex,
|
||||
).ask()
|
||||
if filter_path_regex is None:
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
return filter_path_regex
|
||||
|
||||
def ask_existing_value_regex(
|
||||
self, question: str = "Regex for values"
|
||||
) -> str: # pragma: no cover
|
||||
"""Ask the user for a regex for metadata values.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Regex for values".
|
||||
|
||||
Returns:
|
||||
str: A regex for metadata values that exist in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_value_exists_regex,
|
||||
).ask()
|
||||
|
||||
def ask_application_main(self) -> str: # pragma: no cover
|
||||
"""Selectable list for the main application interface.
|
||||
|
||||
Args:
|
||||
style (questionary.Style): The style to use for the question.
|
||||
|
||||
Returns:
|
||||
str: The selected application.
|
||||
"""
|
||||
return questionary.select(
|
||||
"What do you want to do?",
|
||||
choices=[
|
||||
{"name": "Vault Actions, ", "value": "vault_actions"},
|
||||
{"name": "Inspect Metadata", "value": "inspect_metadata"},
|
||||
{"name": "Filter Notes in Scope", "value": "filter_notes"},
|
||||
{"name": "Add Metadata", "value": "add_metadata"},
|
||||
{"name": "Rename Metadata", "value": "rename_metadata"},
|
||||
{"name": "Delete Metadata", "value": "delete_metadata"},
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Review Changes", "value": "review_changes"},
|
||||
{"name": "Commit Changes", "value": "commit_changes"},
|
||||
questionary.Separator("-------------------------------"),
|
||||
{"name": "Quit", "value": "abort"},
|
||||
],
|
||||
use_shortcuts=False,
|
||||
style=self.style,
|
||||
).ask()
|
||||
|
||||
def ask_new_key(self, question: str = "New key name") -> str: # pragma: no cover
|
||||
"""Ask the user for a new metadata key.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "New key name".
|
||||
|
||||
Returns:
|
||||
str: A new metadata key.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_key,
|
||||
).ask()
|
||||
|
||||
def ask_new_tag(self, question: str = "New tag name") -> str: # pragma: no cover
|
||||
"""Ask the user for a new inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_tag,
|
||||
).ask()
|
||||
|
||||
def ask_new_value(self, question: str = "New value") -> str: # pragma: no cover
|
||||
"""Ask the user for a new metadata value.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "New value".
|
||||
|
||||
Returns:
|
||||
str: A new metadata value.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_value,
|
||||
).ask()
|
||||
|
||||
def ask_selection(
|
||||
self, choices: list[Any], question: str = "Select an option"
|
||||
) -> Any: # pragma: no cover
|
||||
"""Ask the user to select an item from a list.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Select an option".
|
||||
choices (list[Any]): The list of choices.
|
||||
|
||||
Returns:
|
||||
any: The selected item value.
|
||||
"""
|
||||
return questionary.select(
|
||||
question,
|
||||
choices=choices,
|
||||
use_shortcuts=False,
|
||||
style=self.style,
|
||||
).ask()
|
||||
|
||||
@@ -13,7 +13,7 @@ from rich.table import Table
|
||||
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
|
||||
from obsidian_metadata.models import MetadataType, Note, VaultMetadata
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -48,10 +48,8 @@ class Vault:
|
||||
self.notes: list[Note] = [
|
||||
Note(note_path=p, dry_run=self.dry_run) for p in self.note_paths
|
||||
]
|
||||
for _note in self.notes:
|
||||
self.metadata.add_metadata(_note.frontmatter.dict)
|
||||
self.metadata.add_metadata(_note.inline_metadata.dict)
|
||||
self.metadata.add_metadata({_note.inline_tags.metadata_key: _note.inline_tags.list})
|
||||
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover
|
||||
"""Define rich representation of Vault."""
|
||||
@@ -85,6 +83,42 @@ class Vault:
|
||||
|
||||
return notes_list
|
||||
|
||||
def _rebuild_vault_metadata(self) -> None:
|
||||
"""Rebuild vault metadata."""
|
||||
self.metadata = VaultMetadata()
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
transient=True,
|
||||
) as progress:
|
||||
progress.add_task(description="Processing notes...", total=None)
|
||||
for _note in self.notes:
|
||||
self.metadata.add_metadata(_note.frontmatter.dict)
|
||||
self.metadata.add_metadata(_note.inline_metadata.dict)
|
||||
self.metadata.add_metadata({_note.inline_tags.metadata_key: _note.inline_tags.list})
|
||||
|
||||
def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> int:
|
||||
"""Add metadata to all notes in the vault.
|
||||
|
||||
Args:
|
||||
area (MetadataType): Area of metadata to add to.
|
||||
key (str): Key to add.
|
||||
value (str|list, optional): Value to add.
|
||||
|
||||
Returns:
|
||||
int: Number of notes updated.
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes:
|
||||
if _note.add_metadata(area, key, value):
|
||||
num_changed += 1
|
||||
|
||||
if num_changed > 0:
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
def backup(self) -> None:
|
||||
"""Backup the vault."""
|
||||
log.debug("Backing up vault")
|
||||
@@ -162,7 +196,7 @@ class Vault:
|
||||
num_changed += 1
|
||||
|
||||
if num_changed > 0:
|
||||
self.metadata.delete(self.notes[0].inline_tags.metadata_key, tag)
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
@@ -183,7 +217,7 @@ class Vault:
|
||||
num_changed += 1
|
||||
|
||||
if num_changed > 0:
|
||||
self.metadata.delete(key, value)
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
@@ -203,15 +237,14 @@ class Vault:
|
||||
|
||||
def info(self) -> None:
|
||||
"""Print information about the vault."""
|
||||
log.debug("Printing vault info")
|
||||
table = Table(title="Vault Info", show_header=False)
|
||||
table = Table(show_header=False)
|
||||
table.add_row("Vault", str(self.vault_path))
|
||||
table.add_row("Notes being edited", str(self.num_notes()))
|
||||
table.add_row("Notes excluded from editing", str(self.num_excluded_notes()))
|
||||
if self.backup_path.exists():
|
||||
table.add_row("Backup path", str(self.backup_path))
|
||||
else:
|
||||
table.add_row("Backup", "None")
|
||||
table.add_row("Notes in scope", str(self.num_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("Notes with updates", str(len(self.get_changed_notes())))
|
||||
|
||||
@@ -259,7 +292,7 @@ class Vault:
|
||||
num_changed += 1
|
||||
|
||||
if num_changed > 0:
|
||||
self.metadata.rename(key, value_1, value_2)
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
@@ -280,7 +313,7 @@ class Vault:
|
||||
num_changed += 1
|
||||
|
||||
if num_changed > 0:
|
||||
self.metadata.rename(self.notes[0].inline_tags.metadata_key, old_tag, new_tag)
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
return num_changed
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
|
||||
How mocking works in this test suite:
|
||||
|
||||
1. The main_app() method is mocked using a side effect iterable. This allows us to pass a value in the first run, and then a KeyError in the second run to exit the loop.
|
||||
1. The application_main() method is mocked using a side effect iterable. This allows us to pass a value in the first run, and then a KeyError in the second run to exit the loop.
|
||||
2. All questions are mocked using return_value. This allows us to pass in a value to the question and then the method will return that value. This is useful for testing questionary prompts without user input.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from tests.helpers import Regex
|
||||
|
||||
|
||||
@@ -31,273 +33,335 @@ def test_abort(test_application, mocker, capsys) -> None:
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
return_value="abort",
|
||||
)
|
||||
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "Vault Info" in captured.out
|
||||
assert "Done!" in captured.out
|
||||
|
||||
|
||||
def test_list_notes(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> None:
|
||||
"""Test adding new metadata to the vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["list_notes", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["add_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_area",
|
||||
return_value=MetadataType.FRONTMATTER,
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_key",
|
||||
return_value="new_key",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_value",
|
||||
return_value="new_key_value",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "04 no metadata/no_metadata_1.md" in captured.out
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "+inbox/Untitled.md" in captured.out
|
||||
assert "00 meta/templates/data sample.md" in captured.out
|
||||
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_all_metadata(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["all_metadata", KeyError],
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
expected = re.escape("┃ Keys ┃ Values")
|
||||
assert captured.out == Regex(expected)
|
||||
expected = re.escape("Inline Tags │ breakfast")
|
||||
assert captured.out == Regex(expected)
|
||||
|
||||
|
||||
def test_filter_notes(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["filter_notes", "list_notes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_filter_path",
|
||||
return_value="inline",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert "04 no metadata/no_metadata_1.md" not in captured.out
|
||||
assert "02 inline/inline 1.md" in captured.out
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "+inbox/Untitled.md" not in captured.out
|
||||
assert "00 meta/templates/data sample.md" not in captured.out
|
||||
|
||||
|
||||
def test_rename_key_success(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["rename_key", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
|
||||
return_value="tags",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_new_key",
|
||||
return_value="new_tags",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Renamed.*tags.*to.*new_tags.*in.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_key_fail(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["rename_key", KeyError],
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
|
||||
return_value="tag",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_new_key",
|
||||
return_value="new_tags",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert "WARNING | No notes were changed" in captured.out
|
||||
|
||||
|
||||
def test_rename_inline_tag_success(test_application, mocker, capsys) -> None:
|
||||
def test_delete_inline_tag(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["rename_inline_tag", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
|
||||
return_value="breakfast",
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_inline_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_new_tag",
|
||||
return_value="new_tag",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Renamed.*breakfast.*to.*new_tag.*in.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_inline_tag_fail(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["rename_inline_tag", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
|
||||
return_value="not_a_tag",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_new_tag",
|
||||
return_value="new_tag",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_inline_tag_success(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_inline_tag", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
|
||||
return_value="breakfast",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Deleted.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_inline_tag_fail(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_inline_tag", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_inline_tag",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
return_value="not_a_tag_in_vault",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_inline_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
return_value="breakfast",
|
||||
)
|
||||
|
||||
def test_delete_key_success(test_application, mocker, capsys) -> None:
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Deleted.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_key(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_key", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_keys_regex",
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_key", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_keys_regex",
|
||||
return_value=r"\d{7}",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes found with a.*key.*matching", re.DOTALL)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_key", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_keys_regex",
|
||||
return_value=r"d\w+",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(
|
||||
r"SUCCESS +\|.*Deleted.*keys.*matching:.*d\\w\+.*from.*10", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def test_delete_key_fail(test_application, mocker, capsys) -> None:
|
||||
def test_delete_value(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_key", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_keys_regex",
|
||||
return_value=r"\d{7}",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes found with a.*key.*matching", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_value_success(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["rename_value", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_value", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="area",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_value",
|
||||
return_value="frontmatter",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_new_value",
|
||||
return_value="new_key",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_value_regex",
|
||||
return_value=r"\d{7}",
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes found matching:", re.DOTALL)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_value", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="area",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_value_regex",
|
||||
return_value=r"^front\w+$",
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(
|
||||
r"SUCCESS | Renamed 'area:frontmatter' to 'area:new_key'", re.DOTALL
|
||||
r"SUCCESS +\| Deleted value.*\^front\\w\+\$.*from.*key.*area.*in.*\d+.*notes", re.DOTALL
|
||||
)
|
||||
assert captured.out == Regex(r".*in.*\d+.*notes.*", re.DOTALL)
|
||||
|
||||
|
||||
def test_filter_notes_filter(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["filter_notes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["apply_filter", "list_notes", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_filter_path",
|
||||
return_value="inline",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Loaded.*\d+.*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
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["filter_notes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["apply_filter", "list_notes", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_filter_path",
|
||||
return_value="",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Loaded all.*\d+.*total notes", re.DOTALL)
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "03 mixed/mixed 1.md" in captured.out
|
||||
|
||||
|
||||
def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
|
||||
"""Test backing up a vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["inspect_metadata", KeyError],
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["all_metadata", "back"],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"type +│ article", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_inline_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
return_value="not_a_tag",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_tag",
|
||||
return_value="new_tag",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_inline_tag", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_inline_tag",
|
||||
return_value="breakfast",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_tag",
|
||||
return_value="new_tag",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Renamed.*breakfast.*to.*new_tag.*in.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_key(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_key", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="tag",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_key",
|
||||
return_value="new_tags",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "WARNING | No notes were changed" in captured.out
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_key", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="tags",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_key",
|
||||
return_value="new_tags",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Renamed.*tags.*to.*new_tags.*in.*\d+.*notes", re.DOTALL)
|
||||
|
||||
|
||||
def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
@@ -305,71 +369,57 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["rename_value", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_value", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="area",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_value",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_value",
|
||||
return_value="not_exists",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_new_value",
|
||||
"obsidian_metadata.models.application.Questions.ask_new_value",
|
||||
return_value="new_key",
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL)
|
||||
|
||||
|
||||
def test_delete_value_success(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_value", KeyError],
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_value", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="area",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_value_regex",
|
||||
return_value=r"^front\w+$",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_value",
|
||||
return_value="frontmatter",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_value",
|
||||
return_value="new_key",
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(
|
||||
r"SUCCESS +\| Deleted value.*\^front\\w\+\$.*from.*key.*area.*in.*\d+.*notes", re.DOTALL
|
||||
r"SUCCESS +\| Renamed.*'area:frontmatter'.*to.*'area:new_key'", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def test_delete_value_fail(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_value", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_key",
|
||||
return_value="area",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_value_regex",
|
||||
return_value=r"\d{7}",
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"WARNING +\| No notes found matching:", re.DOTALL)
|
||||
assert captured.out == Regex(r".*in.*\d+.*notes.*", re.DOTALL)
|
||||
|
||||
|
||||
def test_review_no_changes(test_application, mocker, capsys) -> None:
|
||||
@@ -377,11 +427,11 @@ def test_review_no_changes(test_application, mocker, capsys) -> None:
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["review_changes", KeyError],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"INFO +\| No changes to review", re.DOTALL)
|
||||
|
||||
@@ -391,24 +441,68 @@ def test_review_changes(test_application, mocker, capsys) -> None:
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_main_application",
|
||||
side_effect=["delete_key", "review_changes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_existing_keys_regex",
|
||||
return_value=r"d\w+",
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", "review_changes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_confirm",
|
||||
return_value=True,
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_for_selection",
|
||||
side_effect=[1, "return"],
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="tags",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_new_key",
|
||||
return_value="new_tags",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["rename_key", 1, "return"],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.main_app()
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r".*Found.*\d+.*changed notes in the vault.*", re.DOTALL)
|
||||
assert "- date_created: 2022-12-22" in captured.out
|
||||
assert "+ - breakfast" in captured.out
|
||||
assert "- tags:" in captured.out
|
||||
assert "+ new_tags:" in captured.out
|
||||
|
||||
|
||||
def test_vault_backup(test_application, mocker, capsys) -> None:
|
||||
"""Test backing up a vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["vault_actions", KeyError],
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["backup_vault", "back"],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\|.*application\.bak", re.DOTALL)
|
||||
|
||||
|
||||
def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
|
||||
"""Test backing up a vault."""
|
||||
app = test_application
|
||||
backup_path = Path(tmp_path / "application.bak")
|
||||
backup_path.mkdir()
|
||||
app.load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["vault_actions", KeyError],
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["delete_backup", "back"],
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Backup deleted", re.DOTALL)
|
||||
|
||||
@@ -222,6 +222,71 @@ def test_frontmatter_contains() -> None:
|
||||
assert frontmatter.contains("key", r"\w\d_", is_regex=True) is True
|
||||
|
||||
|
||||
def test_frontmatter_add() -> None:
|
||||
"""Test frontmatter add."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
|
||||
assert frontmatter.add("frontmatter_Key1") is False
|
||||
assert frontmatter.add("added_key") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": [],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key", "added_value") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key", "added_value_2") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key", ["added_value_3", "added_value_4"]) is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key2", ["added_value_1", "added_value_2"]) is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"],
|
||||
"added_key2": ["added_value_1", "added_value_2"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key3", "added_value_1") is True
|
||||
assert frontmatter.dict == {
|
||||
"added_key": ["added_value", "added_value_2", "added_value_3", "added_value_4"],
|
||||
"added_key2": ["added_value_1", "added_value_2"],
|
||||
"added_key3": ["added_value_1"],
|
||||
"frontmatter_Key1": ["frontmatter_Key1_value"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"tags": ["tag_1", "tag_2", "📅/tag_3"],
|
||||
}
|
||||
|
||||
assert frontmatter.add("added_key3", "added_value_1") is False
|
||||
|
||||
|
||||
def test_frontmatter_rename() -> None:
|
||||
"""Test frontmatter rename."""
|
||||
frontmatter = Frontmatter(FRONTMATTER_CONTENT)
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from obsidian_metadata.models.notes import Note
|
||||
from tests.helpers import Regex
|
||||
|
||||
@@ -102,6 +103,65 @@ def test_append(sample_note) -> None:
|
||||
assert len(re.findall(re.escape(string2), note.file_content)) == 2
|
||||
|
||||
|
||||
def test_add_metadata(sample_note) -> None:
|
||||
"""Test adding metadata."""
|
||||
note = Note(note_path=sample_note)
|
||||
assert note.add_metadata(MetadataType.FRONTMATTER, "frontmatter_Key1") is False
|
||||
assert note.add_metadata(MetadataType.FRONTMATTER, "shared_key1", "shared_key1_value") is False
|
||||
assert note.add_metadata(MetadataType.FRONTMATTER, "new_key1") is True
|
||||
assert note.frontmatter.dict == {
|
||||
"date_created": ["2022-12-22"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"new_key1": [],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value1"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
"frontmatter_tag2",
|
||||
"shared_tag",
|
||||
"📅/frontmatter_tag3",
|
||||
],
|
||||
}
|
||||
assert note.add_metadata(MetadataType.FRONTMATTER, "new_key2", "new_key2_value") is True
|
||||
assert note.frontmatter.dict == {
|
||||
"date_created": ["2022-12-22"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"new_key1": [],
|
||||
"new_key2": ["new_key2_value"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value1"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
"frontmatter_tag2",
|
||||
"shared_tag",
|
||||
"📅/frontmatter_tag3",
|
||||
],
|
||||
}
|
||||
assert (
|
||||
note.add_metadata(
|
||||
MetadataType.FRONTMATTER, "new_key2", ["new_key2_value2", "new_key2_value3"]
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert note.frontmatter.dict == {
|
||||
"date_created": ["2022-12-22"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
"new_key1": [],
|
||||
"new_key2": ["new_key2_value", "new_key2_value2", "new_key2_value3"],
|
||||
"shared_key1": ["shared_key1_value"],
|
||||
"shared_key2": ["shared_key2_value1"],
|
||||
"tags": [
|
||||
"frontmatter_tag1",
|
||||
"frontmatter_tag2",
|
||||
"shared_tag",
|
||||
"📅/frontmatter_tag3",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_contains_inline_tag(sample_note) -> None:
|
||||
"""Test contains inline tag."""
|
||||
note = Note(note_path=sample_note)
|
||||
@@ -212,9 +272,6 @@ def test_print_note(sample_note, capsys) -> None:
|
||||
def test_print_diff(sample_note, capsys) -> None:
|
||||
"""Test printing diff."""
|
||||
note = Note(note_path=sample_note)
|
||||
note.print_diff()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == ""
|
||||
|
||||
note.append("This is a test string.")
|
||||
note.print_diff()
|
||||
|
||||
@@ -26,7 +26,6 @@ def test_validate_valid_regex() -> None:
|
||||
questions = Questions(vault=VAULT)
|
||||
assert questions._validate_valid_vault_regex(r".*\.md") is True
|
||||
assert "Invalid regex" in questions._validate_valid_vault_regex("[")
|
||||
assert "Regex cannot be empty" in questions._validate_valid_vault_regex("")
|
||||
assert "Regex does not match paths" in questions._validate_valid_vault_regex(r"\d\d\d\w\d")
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
|
||||
from obsidian_metadata._config import Config
|
||||
from obsidian_metadata.models import Vault
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from tests.helpers import Regex
|
||||
|
||||
|
||||
@@ -155,7 +156,7 @@ def test_info(test_vault, capsys):
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"Vault +\│ /[\d\w]+")
|
||||
assert captured.out == Regex(r"Notes being edited +\│ \d+")
|
||||
assert captured.out == Regex(r"Notes in scope +\│ \d+")
|
||||
assert captured.out == Regex(r"Backup +\│ None")
|
||||
|
||||
|
||||
@@ -170,6 +171,90 @@ def test_contains_inline_tag(test_vault) -> None:
|
||||
assert vault.contains_inline_tag("intext_tag2") is True
|
||||
|
||||
|
||||
def test_add_metadata(test_vault) -> None:
|
||||
"""Test adding metadata to the vault."""
|
||||
vault_path = test_vault
|
||||
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path)
|
||||
vault_config = config.vaults[0]
|
||||
vault = Vault(config=vault_config)
|
||||
|
||||
assert vault.add_metadata(MetadataType.FRONTMATTER, "new_key") == 3
|
||||
assert vault.metadata.dict == {
|
||||
"Inline Tags": [
|
||||
"ignored_file_tag2",
|
||||
"inline_tag_bottom1",
|
||||
"inline_tag_bottom2",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"intext_tag1",
|
||||
"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"],
|
||||
"new_key": [],
|
||||
"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"],
|
||||
}
|
||||
assert vault.add_metadata(MetadataType.FRONTMATTER, "new_key2", "new_key2_value") == 3
|
||||
assert vault.metadata.dict == {
|
||||
"Inline Tags": [
|
||||
"ignored_file_tag2",
|
||||
"inline_tag_bottom1",
|
||||
"inline_tag_bottom2",
|
||||
"inline_tag_top1",
|
||||
"inline_tag_top2",
|
||||
"intext_tag1",
|
||||
"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"],
|
||||
"new_key": [],
|
||||
"new_key2": ["new_key2_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"],
|
||||
}
|
||||
|
||||
|
||||
def test_contains_metadata(test_vault) -> None:
|
||||
"""Test if the vault contains a metadata key."""
|
||||
vault_path = test_vault
|
||||
|
||||
Reference in New Issue
Block a user