mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-14 07:53:47 -05:00
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:
16
README.md
16
README.md
@@ -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
12
poetry.lock
generated
@@ -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"},
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user