feat: transpose metadata (#18)

* feat: transpose between frontmatter and inline metadata

* ci: improve codecode patch thresholds

* test: remove ansi escape sequences from `capsys.errout`

* test: improve fixture for shared keys

* build(deps): update dependencies

* refactor: use deepcopy

* docs: add transpose metadata
This commit is contained in:
Nathaniel Landau
2023-02-06 17:31:42 -05:00
committed by GitHub
parent 446374b335
commit 0143967db8
20 changed files with 793 additions and 225 deletions

View File

@@ -77,6 +77,12 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
- **Delete a value from a key** - **Delete a value from a key**
- **Delete an inline tag** - **Delete an inline tag**
**Transpose Metadata**: Move metadata from inline to frontmatter or the reverse.
- **Transpose all metadata** - Moves all frontmatter to inline metadata, or the reverse
- **Transpose key** - Transposes a specific key and all it's values
- **Transpose value**- Transpose a specific key:value pair
**Review Changes**: Prior to committing changes, review all changes that will be made. **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
@@ -122,7 +128,7 @@ To bypass the configuration file and specify a vault to use at runtime use the `
There are two ways to contribute to this project. There are two ways to contribute to this project.
### 1. Containerized development (Recommended) ### 1. Containerized development
1. Clone this repository. `git clone https://github.com/natelandau/obsidian-metadata` 1. Clone this repository. `git clone https://github.com/natelandau/obsidian-metadata`
2. Open the repository in Visual Studio Code 2. Open the repository in Visual Studio Code

12
poetry.lock generated
View File

@@ -1357,14 +1357,14 @@ files = [
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.17.1" version = "20.18.0"
description = "Virtual Python Environment builder" description = "Virtual Python Environment builder"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.7"
files = [ files = [
{file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, {file = "virtualenv-20.18.0-py3-none-any.whl", hash = "sha256:9d61e4ec8d2c0345dab329fb825eb05579043766a4b26a2f66b28948de68c722"},
{file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, {file = "virtualenv-20.18.0.tar.gz", hash = "sha256:f262457a4d7298a6b733b920a196bf8b46c8af15bf1fd9da7142995eff15118e"},
] ]
[package.dependencies] [package.dependencies]
@@ -1373,8 +1373,8 @@ filelock = ">=3.4.1,<4"
platformdirs = ">=2.4,<3" platformdirs = ">=2.4,<3"
[package.extras] [package.extras]
docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
[[package]] [[package]]
name = "vulture" name = "vulture"

View File

@@ -122,7 +122,7 @@ def docstring_parameter(*sub: Any) -> Any:
def merge_dictionaries(dict1: dict, dict2: dict) -> dict: def merge_dictionaries(dict1: dict, dict2: dict) -> dict:
"""Merge two dictionaries. """Merge two dictionaries. When the values are lists, they are merged and sorted.
Args: Args:
dict1 (dict): First dictionary. dict1 (dict): First dictionary.

View File

@@ -135,6 +135,14 @@ def main(
• Delete a value from a key • Delete a value from a key
• Delete an inline tag • Delete an inline tag
[bold underline]Transpose Metadata[/]
Move metadata from inline to frontmatter or the reverse.
• Transpose all metadata - Moves all frontmatter to inline
metadata, or the reverse
• Transpose key - Transposes a specific key and all it's values
• Transpose value- Transpose a specific key:value pair
[bold underline]Review Changes[/] [bold underline]Review Changes[/]
Prior to committing changes, review all changes that will be made. 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

View File

@@ -65,6 +65,8 @@ class Application:
self.application_rename_metadata() self.application_rename_metadata()
case "delete_metadata": case "delete_metadata":
self.application_delete_metadata() self.application_delete_metadata()
case "transpose_metadata":
self.application_transpose_metadata()
case "review_changes": case "review_changes":
self.review_changes() self.review_changes()
case "commit_changes": case "commit_changes":
@@ -120,6 +122,51 @@ class Application:
case _: # pragma: no cover case _: # pragma: no cover
return return
def application_delete_metadata(self) -> None:
alerts.usage("Delete either a key and all associated values, or a specific value.")
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 _: # pragma: no cover
return
def application_rename_metadata(self) -> None:
"""Rename metadata."""
alerts.usage("Select the type of metadata to rename.")
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 _: # pragma: no cover
return
def application_filter(self) -> None: def application_filter(self) -> None:
"""Filter notes.""" """Filter notes."""
alerts.usage("Limit the scope of notes to be processed with one or more filters.") alerts.usage("Limit the scope of notes to be processed with one or more filters.")
@@ -286,6 +333,24 @@ class Application:
case _: case _:
return return
def application_transpose_metadata(self) -> None:
"""Transpose metadata."""
alerts.usage("Transpose metadata from frontmatter to inline or vice versa.")
choices = [
{"name": "Transpose frontmatter to inline", "value": "frontmatter_to_inline"},
{"name": "Transpose inline to frontmatter", "value": "inline_to_frontmatter"},
]
match self.questions.ask_selection(
choices=choices, question="Select metadata to transpose"
):
case "frontmatter_to_inline":
self.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE)
case "inline_to_frontmatter":
self.transpose_metadata(begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER)
case _: # pragma: no cover
return
def application_vault(self) -> None: def application_vault(self) -> None:
"""Vault actions.""" """Vault actions."""
alerts.usage("Create or delete a backup of your vault.") alerts.usage("Create or delete a backup of your vault.")
@@ -306,51 +371,6 @@ class Application:
case _: case _:
return return
def application_delete_metadata(self) -> None:
alerts.usage("Delete either a key and all associated values, or a specific value.")
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 _: # pragma: no cover
return
def application_rename_metadata(self) -> None:
"""Rename metadata."""
alerts.usage("Select the type of metadata to rename.")
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 _: # pragma: no cover
return
def commit_changes(self) -> bool: def commit_changes(self) -> bool:
"""Write all changes to disk. """Write all changes to disk.
@@ -537,3 +557,76 @@ class Application:
if note_to_review is None or note_to_review == "return": if note_to_review is None or note_to_review == "return":
break break
changed_notes[note_to_review].print_diff() changed_notes[note_to_review].print_diff()
def transpose_metadata(self, begin: MetadataType, end: MetadataType) -> None:
"""Transpose metadata from one format to another.
Args:
begin: The format to transpose from.
end: The format to transpose to.
"""
choices = [
{"name": f"Transpose all {begin.value} to {end.value}", "value": "transpose_all"},
{"name": "Transpose a key", "value": "transpose_key"},
{"name": "Transpose a value", "value": "transpose_value"},
{"name": "Back", "value": "back"},
]
match self.questions.ask_selection(choices=choices, question="Select an action to perform"):
case "transpose_all":
num_changed = self.vault.transpose_metadata(
begin=begin,
end=end,
location=self.vault.insert_location,
)
if num_changed == 0:
alerts.warning(f"No notes were changed")
return
alerts.success(f"Transposed {begin.value} to {end.value} in {num_changed} notes")
case "transpose_key":
key = self.questions.ask_existing_key(question="Which key to transpose?")
if key is None: # pragma: no cover
return
num_changed = self.vault.transpose_metadata(
begin=begin,
end=end,
key=key,
location=self.vault.insert_location,
)
if num_changed == 0:
alerts.warning(f"No notes were changed")
return
alerts.success(
f"Transposed key: `{key}` from {begin.value} to {end.value} in {num_changed} notes"
)
case "transpose_value":
key = self.questions.ask_existing_key(question="Which key contains the value?")
if key is None: # pragma: no cover
return
questions2 = Questions(vault=self.vault, key=key)
value = questions2.ask_existing_value(question="Which value to transpose?")
if value is None: # pragma: no cover
return
num_changed = self.vault.transpose_metadata(
begin=begin,
end=end,
key=key,
value=value,
location=self.vault.insert_location,
)
if num_changed == 0:
alerts.warning(f"No notes were changed")
return
alerts.success(
f"Transposed key: `{key}:{value}` from {begin.value} to {end.value} in {num_changed} notes"
)
case _:
return

View File

@@ -2,7 +2,7 @@
import re import re
from io import StringIO from io import StringIO
import copy
from rich import print from rich import print
from rich.columns import Columns from rich.columns import Columns
from rich.console import Console from rich.console import Console
@@ -49,15 +49,13 @@ class VaultMetadata:
""" """
if isinstance(metadata, dict): if isinstance(metadata, dict):
new_metadata = clean_dictionary(metadata) new_metadata = clean_dictionary(metadata)
self.dict = merge_dictionaries(self.dict.copy(), new_metadata.copy()) self.dict = merge_dictionaries(self.dict, new_metadata)
if area == MetadataType.FRONTMATTER: if area == MetadataType.FRONTMATTER:
self.frontmatter = merge_dictionaries(self.frontmatter.copy(), new_metadata.copy()) self.frontmatter = merge_dictionaries(self.frontmatter, new_metadata)
if area == MetadataType.INLINE: if area == MetadataType.INLINE:
self.inline_metadata = merge_dictionaries( self.inline_metadata = merge_dictionaries(self.inline_metadata, new_metadata)
self.inline_metadata.copy(), new_metadata.copy()
)
if area == MetadataType.TAGS and isinstance(metadata, list): if area == MetadataType.TAGS and isinstance(metadata, list):
self.tags.extend(metadata) self.tags.extend(metadata)
@@ -118,7 +116,7 @@ class VaultMetadata:
Returns: Returns:
bool: True if a value was deleted bool: True if a value was deleted
""" """
new_dict = self.dict.copy() new_dict = copy.deepcopy(self.dict)
if value_to_delete is None: if value_to_delete is None:
for _k in list(new_dict): for _k in list(new_dict):
@@ -216,7 +214,7 @@ class Frontmatter:
def __init__(self, file_content: str): def __init__(self, file_content: str):
self.dict: dict[str, list[str]] = self._grab_note_frontmatter(file_content) self.dict: dict[str, list[str]] = self._grab_note_frontmatter(file_content)
self.dict_original: dict[str, list[str]] = self.dict.copy() self.dict_original: dict[str, list[str]] = copy.deepcopy(self.dict)
def __repr__(self) -> str: # pragma: no cover def __repr__(self) -> str: # pragma: no cover
"""Representation of the frontmatter. """Representation of the frontmatter.
@@ -364,7 +362,7 @@ class Frontmatter:
str: Frontmatter as a YAML string. str: Frontmatter as a YAML string.
sort_keys (bool, optional): Sort the keys. Defaults to False. sort_keys (bool, optional): Sort the keys. Defaults to False.
""" """
dict_to_dump = self.dict.copy() dict_to_dump = copy.deepcopy(self.dict)
for k in dict_to_dump: for k in dict_to_dump:
if dict_to_dump[k] == []: if dict_to_dump[k] == []:
dict_to_dump[k] = None dict_to_dump[k] = None
@@ -391,7 +389,7 @@ class InlineMetadata:
def __init__(self, file_content: str): def __init__(self, file_content: str):
self.dict: dict[str, list[str]] = self.grab_inline_metadata(file_content) self.dict: dict[str, list[str]] = self.grab_inline_metadata(file_content)
self.dict_original: dict[str, list[str]] = self.dict.copy() self.dict_original: dict[str, list[str]] = copy.deepcopy(self.dict)
def __repr__(self) -> str: # pragma: no cover def __repr__(self) -> str: # pragma: no cover
"""Representation of inline metadata. """Representation of inline metadata.

View File

@@ -4,7 +4,7 @@
import difflib import difflib
import re import re
from pathlib import Path from pathlib import Path
import copy
import rich.repr import rich.repr
import typer import typer
from rich.console import Console from rich.console import Console
@@ -238,7 +238,9 @@ class Note:
return False return False
def delete_metadata(self, key: str, value: str = None) -> bool: def delete_metadata(
self, key: str, value: str = None, area: MetadataType = MetadataType.ALL
) -> bool:
"""Delete a key or key-value pair from the note's metadata. Regex is supported. """Delete a key or key-value pair from the note's metadata. Regex is supported.
If no value is provided, will delete an entire key. If no value is provided, will delete an entire key.
@@ -246,6 +248,7 @@ class Note:
Args: Args:
key (str): Key to delete. key (str): Key to delete.
value (str, optional): Value to delete. value (str, optional): Value to delete.
area (MetadataType, optional): Area to delete metadata from. Defaults to MetadataType.ALL.
Returns: Returns:
bool: Whether the key or key-value pair was deleted. bool: Whether the key or key-value pair was deleted.
@@ -253,17 +256,25 @@ class Note:
changed_value: bool = False changed_value: bool = False
if value is None: if value is None:
if self.frontmatter.delete(key): if (
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
) and self.frontmatter.delete(key):
self.update_frontmatter() self.update_frontmatter()
changed_value = True changed_value = True
if self.inline_metadata.delete(key): if (
area == MetadataType.INLINE or area == MetadataType.ALL
) and self.inline_metadata.delete(key):
self._delete_inline_metadata(key, value) self._delete_inline_metadata(key, value)
changed_value = True changed_value = True
else: else:
if self.frontmatter.delete(key, value): if (
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
) and self.frontmatter.delete(key, value):
self.update_frontmatter() self.update_frontmatter()
changed_value = True changed_value = True
if self.inline_metadata.delete(key, value): if (
area == MetadataType.INLINE or area == MetadataType.ALL
) and self.inline_metadata.delete(key, value):
self._delete_inline_metadata(key, value) self._delete_inline_metadata(key, value)
changed_value = True changed_value = True
@@ -426,6 +437,88 @@ class Note:
self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE) self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE)
def transpose_metadata(
self,
begin: MetadataType,
end: MetadataType,
key: str = None,
value: str | list[str] = None,
location: InsertLocation = InsertLocation.BOTTOM,
) -> bool:
"""Transpose metadata from one type to another.
Args:
begin (MetadataType): The type of metadata to transpose from.
end (MetadataType): The type of metadata to transpose to.
key (str, optional): The key to transpose. Defaults to None.
value (str | list[str], optional): The value to transpose. Defaults to None.
Returns:
bool: Whether the note was updated.
"""
if (begin == MetadataType.FRONTMATTER or begin == MetadataType.INLINE) and (
end == MetadataType.FRONTMATTER or end == MetadataType.INLINE
):
if begin == MetadataType.FRONTMATTER:
begin_dict = self.frontmatter.dict
else:
begin_dict = self.inline_metadata.dict
if begin_dict == {}:
return False
if key is None: # Transpose all metadata when no key is provided
for _key, _value in begin_dict.items():
self.add_metadata(key=_key, value=_value, area=end, location=location)
self.delete_metadata(key=_key, area=begin)
return True
has_changes = False
temp_dict = copy.deepcopy(begin_dict)
for k, v in begin_dict.items():
if key == k:
if value is None:
self.add_metadata(key=k, value=v, area=end, location=location)
self.delete_metadata(key=k, area=begin)
return True
if value == v:
self.add_metadata(key=k, value=v, area=end, location=location)
self.delete_metadata(key=k, area=begin)
return True
if isinstance(value, str):
if value in v:
self.add_metadata(key=k, value=value, area=end, location=location)
self.delete_metadata(key=k, value=value, area=begin)
return True
else:
return False
if isinstance(value, list):
for value_item in value:
if value_item in v:
self.add_metadata(
key=k, value=value_item, area=end, location=location
)
self.delete_metadata(key=k, value=value_item, area=begin)
temp_dict[k].remove(value_item)
has_changes = True
if temp_dict[k] == []:
self.delete_metadata(key=k, area=begin)
if has_changes:
return True
else:
return False
if begin == MetadataType.TAGS:
# TODO: Implement transposing to and from tags
pass
return False
def update_frontmatter(self, sort_keys: bool = False) -> None: def update_frontmatter(self, sort_keys: bool = False) -> None:
"""Replace the frontmatter in the note with the current frontmatter object.""" """Replace the frontmatter in the note with the current frontmatter object."""
try: try:
@@ -439,13 +532,16 @@ class Note:
return return
new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys) new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys)
if self.frontmatter.dict == {}:
new_frontmatter = ""
else:
new_frontmatter = f"---\n{new_frontmatter}---\n" new_frontmatter = f"---\n{new_frontmatter}---\n"
if current_frontmatter is None: if current_frontmatter is None:
self.file_content = new_frontmatter + self.file_content self.file_content = new_frontmatter + self.file_content
return return
current_frontmatter = re.escape(current_frontmatter) current_frontmatter = f"{re.escape(current_frontmatter)}\n?"
self.sub(current_frontmatter, new_frontmatter, is_regex=True) self.sub(current_frontmatter, new_frontmatter, is_regex=True)
def write(self, path: Path = None) -> None: def write(self, path: Path = None) -> None:

View File

@@ -280,6 +280,7 @@ class Questions:
{"name": "Add Metadata", "value": "add_metadata"}, {"name": "Add Metadata", "value": "add_metadata"},
{"name": "Rename Metadata", "value": "rename_metadata"}, {"name": "Rename Metadata", "value": "rename_metadata"},
{"name": "Delete Metadata", "value": "delete_metadata"}, {"name": "Delete Metadata", "value": "delete_metadata"},
{"name": "Transpose Metadata", "value": "transpose_metadata"},
questionary.Separator("-------------------------------"), questionary.Separator("-------------------------------"),
{"name": "Review Changes", "value": "review_changes"}, {"name": "Review Changes", "value": "review_changes"},
{"name": "Commit Changes", "value": "commit_changes"}, {"name": "Commit Changes", "value": "commit_changes"},

View File

@@ -411,3 +411,42 @@ class Vault:
self._rebuild_vault_metadata() self._rebuild_vault_metadata()
return num_changed return num_changed
def transpose_metadata(
self,
begin: MetadataType,
end: MetadataType,
key: str = None,
value: str | list[str] = None,
location: InsertLocation = None,
) -> int:
"""Transpose metadata from one type to another.
Args:
begin (MetadataType): Metadata type to transpose from.
end (MetadataType): Metadata type to transpose to.
key (str, optional): Key to transpose. Defaults to None.
value (str, optional): Value to transpose. Defaults to None.
location (InsertLocation, optional): Location to insert metadata. (Defaults to `vault.config.insert_location`)
Returns:
int: Number of notes that had metadata transposed.
"""
if location is None:
location = self.insert_location
num_changed = 0
for _note in self.notes_in_scope:
if _note.transpose_metadata(
begin=begin,
end=end,
key=key,
value=value,
location=location,
):
num_changed += 1
if num_changed > 0:
self._rebuild_vault_metadata()
return num_changed

View File

@@ -13,7 +13,7 @@ from pathlib import Path
import pytest import pytest
from obsidian_metadata.models.enums import MetadataType from obsidian_metadata.models.enums import MetadataType
from tests.helpers import Regex from tests.helpers import Regex, remove_ansi
def test_instantiate_application(test_application) -> None: def test_instantiate_application(test_application) -> None:
@@ -38,8 +38,8 @@ def test_abort(test_application, mocker, capsys) -> None:
) )
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert "Done!" in captured.out assert "Done!" in captured
def test_add_metadata_frontmatter(test_application, mocker, capsys) -> None: def test_add_metadata_frontmatter(test_application, mocker, capsys) -> None:
@@ -65,8 +65,8 @@ def test_add_metadata_frontmatter(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL) assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
def test_add_metadata_inline(test_application, mocker, capsys) -> None: def test_add_metadata_inline(test_application, mocker, capsys) -> None:
@@ -92,8 +92,8 @@ def test_add_metadata_inline(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL) assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
def test_add_metadata_tag(test_application, mocker, capsys) -> None: def test_add_metadata_tag(test_application, mocker, capsys) -> None:
@@ -115,8 +115,8 @@ def test_add_metadata_tag(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\| Added metadata to.*\d+.*notes", re.DOTALL) assert captured == Regex(r"SUCCESS +\| Added metadata to \d+ notes", re.DOTALL)
def test_delete_inline_tag(test_application, mocker, capsys) -> None: def test_delete_inline_tag(test_application, mocker, capsys) -> None:
@@ -138,8 +138,8 @@ def test_delete_inline_tag(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL) assert "WARNING | No notes were changed" in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -156,8 +156,8 @@ def test_delete_inline_tag(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\| Deleted.*\d+.*notes", re.DOTALL) assert captured == Regex(r"SUCCESS +\| Deleted inline tag: breakfast in \d+ notes", re.DOTALL)
def test_delete_key(test_application, mocker, capsys) -> None: def test_delete_key(test_application, mocker, capsys) -> None:
@@ -179,8 +179,8 @@ def test_delete_key(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"WARNING +\| No notes found with a.*key.*matching", re.DOTALL) assert r"WARNING | No notes found with a key matching: \d{7}" in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -197,10 +197,8 @@ def test_delete_key(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex( assert captured == Regex(r"SUCCESS \| Deleted keys matching: d\\w\+ from \d+ notes", re.DOTALL)
r"SUCCESS +\|.*Deleted.*keys.*matching:.*d\\w\+.*from.*10", re.DOTALL
)
def test_delete_value(test_application, mocker, capsys) -> None: def test_delete_value(test_application, mocker, capsys) -> None:
@@ -225,8 +223,8 @@ def test_delete_value(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"WARNING +\| No notes found matching:", re.DOTALL) assert r"WARNING | No notes found matching: area: \d{7}" in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -246,10 +244,8 @@ def test_delete_value(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex( assert r"SUCCESS | Deleted value ^front\w+$ from key area in 8 notes" in captured
r"SUCCESS +\| Deleted value.*\^front\\w\+\$.*from.*key.*area.*in.*\d+.*notes", re.DOTALL
)
def test_filter_notes(test_application, mocker, capsys) -> None: def test_filter_notes(test_application, mocker, capsys) -> None:
@@ -271,10 +267,10 @@ def test_filter_notes(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\| Loaded.*\d+.*notes from.*\d+.*total", re.DOTALL) assert captured == Regex(r"SUCCESS +\| Loaded \d+ notes from \d+ total", re.DOTALL)
assert "02 inline/inline 2.md" in captured.out assert "02 inline/inline 2.md" in captured
assert "03 mixed/mixed 1.md" not in captured.out assert "03 mixed/mixed 1.md" not in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -326,11 +322,11 @@ def test_filter_clear(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert "02 inline/inline 2.md" in captured.out assert "02 inline/inline 2.md" in captured
assert "03 mixed/mixed 1.md" in captured.out assert "03 mixed/mixed 1.md" in captured
assert "01 frontmatter/frontmatter 4.md" in captured.out assert "01 frontmatter/frontmatter 4.md" in captured
assert "04 no metadata/no_metadata_1.md " in captured.out assert "04 no metadata/no_metadata_1.md " in captured
def test_inspect_metadata_all(test_application, mocker, capsys) -> None: def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
@@ -348,8 +344,8 @@ def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"type +│ article", re.DOTALL) assert captured == Regex(r"type +│ article", re.DOTALL)
def test_rename_inline_tag(test_application, mocker, capsys) -> None: def test_rename_inline_tag(test_application, mocker, capsys) -> None:
@@ -375,8 +371,8 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL) assert "No notes were changed" in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -397,8 +393,8 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"Renamed.*breakfast.*to.*new_tag.*in.*\d+.*notes", re.DOTALL) assert captured == Regex(r"Renamed breakfast to new_tag in \d+ notes", re.DOTALL)
def test_rename_key(test_application, mocker, capsys) -> None: def test_rename_key(test_application, mocker, capsys) -> None:
@@ -424,8 +420,8 @@ def test_rename_key(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert "WARNING | No notes were changed" in captured.out assert "WARNING | No notes were changed" in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -446,8 +442,8 @@ def test_rename_key(test_application, mocker, capsys) -> None:
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"Renamed.*tags.*to.*new_tags.*in.*\d+.*notes", re.DOTALL) assert captured == Regex(r"Renamed tags to new_tags in \d+ notes", re.DOTALL)
def test_rename_value_fail(test_application, mocker, capsys) -> None: def test_rename_value_fail(test_application, mocker, capsys) -> None:
@@ -476,8 +472,8 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"WARNING +\| No notes were changed", re.DOTALL) assert "WARNING | No notes were changed" in captured
mocker.patch( mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main", "obsidian_metadata.models.application.Questions.ask_application_main",
@@ -501,11 +497,10 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex( assert captured == Regex(
r"SUCCESS +\| Renamed.*'area:frontmatter'.*to.*'area:new_key'", re.DOTALL r"SUCCESS +\| Renamed 'area:frontmatter' to 'area:new_key' in \d+ notes", re.DOTALL
) )
assert captured.out == Regex(r".*in.*\d+.*notes.*", re.DOTALL)
def test_review_no_changes(test_application, mocker, capsys) -> None: def test_review_no_changes(test_application, mocker, capsys) -> None:
@@ -518,8 +513,8 @@ def test_review_no_changes(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"INFO +\| No changes to review", re.DOTALL) assert "INFO | No changes to review" in captured
def test_review_changes(test_application, mocker, capsys) -> None: def test_review_changes(test_application, mocker, capsys) -> None:
@@ -544,10 +539,49 @@ def test_review_changes(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r".*Found.*\d+.*changed notes in the vault", re.DOTALL) assert captured == Regex(r".*Found \d+ changed notes in the vault", re.DOTALL)
assert "- tags:" in captured.out assert "- tags:" in captured
assert "+ new_tags:" in captured.out assert "+ new_tags:" in captured
def test_transpose_metadata(test_application, mocker, capsys) -> None:
"""Transpose metadata."""
app = test_application
app._load_vault()
assert app.vault.metadata.inline_metadata["inline_key"] == ["inline_key_value"]
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["transpose_metadata", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_selection",
side_effect=["inline_to_frontmatter", "transpose_all"],
)
with pytest.raises(KeyError):
app.application_main()
assert app.vault.metadata.inline_metadata == {}
assert app.vault.metadata.frontmatter["inline_key"] == ["inline_key_value"]
captured = remove_ansi(capsys.readouterr().out)
assert "SUCCESS | Transposed Inline Metadata to Frontmatter in 5 notes" in captured
app = test_application
app._load_vault()
assert app.vault.metadata.frontmatter["date_created"] == ["2022-12-21", "2022-12-22"]
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_application_main",
side_effect=["transpose_metadata", KeyError],
)
mocker.patch(
"obsidian_metadata.models.application.Questions.ask_selection",
side_effect=["frontmatter_to_inline", "transpose_all"],
)
with pytest.raises(KeyError):
app.application_main()
assert app.vault.metadata.inline_metadata["date_created"] == ["2022-12-21", "2022-12-22"]
assert app.vault.metadata.frontmatter == {}
def test_vault_backup(test_application, mocker, capsys) -> None: def test_vault_backup(test_application, mocker, capsys) -> None:
@@ -565,8 +599,10 @@ def test_vault_backup(test_application, mocker, capsys) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\|.*application\.bak", re.DOTALL) assert captured == Regex(
r"SUCCESS +\| Vault backed up to:[-\w\d\/\s]+application\.bak", re.DOTALL
)
def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None: def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
@@ -586,5 +622,5 @@ def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
app.application_main() app.application_main()
captured = capsys.readouterr() captured = remove_ansi(capsys.readouterr().out)
assert captured.out == Regex(r"SUCCESS +\| Backup deleted", re.DOTALL) assert captured == Regex(r"SUCCESS +\| Backup deleted", re.DOTALL)

View File

@@ -8,20 +8,11 @@ tags:
- dinner - dinner
- lunch - lunch
- breakfast - breakfast
thoughts:
rating: 8
reviewable: false
levels:
level1:
- level1a
- level1b
level2:
- level2a
- level2b
author: John Doe author: John Doe
status: new status: new
type: ["book", "article", "note", "one-off"] type: ["book", "article", "note", "one-off"]
--- ---
# Page Title H1 # Page Title H1
# Headings # Headings

View File

@@ -8,20 +8,11 @@ tags:
- dinner - dinner
- lunch - lunch
- breakfast - breakfast
thoughts:
rating: 8
reviewable: false
levels:
level1:
- level1a
- level1b
level2:
- level2a
- level2b
author: John Doe author: John Doe
status: new status: new
type: ["book", "article", "note"] type: ["book", "article", "note"]
--- ---
# Page Title H1 # Page Title H1
# Headings # Headings

View File

@@ -8,20 +8,11 @@ tags:
- dinner - dinner
- lunch - lunch
- breakfast - breakfast
thoughts:
rating: 8
reviewable: false
levels:
level1:
- level1a
- level1b
level2:
- level2a
- level2b
author: John Doe author: John Doe
status: new status: new
type: ["book", "article", "note"] type: ["book", "article", "note"]
--- ---
# Page Title H1 # Page Title H1
# Headings # Headings

View File

@@ -8,16 +8,6 @@ tags:
- dinner - dinner
- lunch - lunch
- breakfast - breakfast
thoughts:
rating: 8
reviewable: false
levels:
level1:
- level1a
- level1b
level2:
- level2a
- level2b
author: John Doe author: John Doe
status: new status: new
type: ["book", "article", "note"] type: ["book", "article", "note"]

View File

@@ -6,10 +6,6 @@ tags:
- breakfast - breakfast
- not_food - not_food
author: John Doe author: John Doe
nested_list:
nested_list_one:
- nested_list_one_a
- nested_list_one_b
type: type:
- article - article
- note - note

View File

@@ -8,7 +8,9 @@ tags:
- 📅/frontmatter_tag3 - 📅/frontmatter_tag3
frontmatter_Key1: author name frontmatter_Key1: author name
frontmatter_Key2: ["article", "note"] frontmatter_Key2: ["article", "note"]
shared_key1: shared_key1_value shared_key1:
- shared_key1_value
- shared_key1_value3
shared_key2: shared_key2_value1 shared_key2: shared_key2_value1
--- ---
@@ -18,10 +20,12 @@ top_key1:: top_key1_value
**top_key2:: top_key2_value** **top_key2:: top_key2_value**
top_key3:: [[top_key3_value_as_link]] top_key3:: [[top_key3_value_as_link]]
shared_key1:: shared_key1_value shared_key1:: shared_key1_value
shared_key1:: shared_key1_value2
shared_key2:: shared_key2_value2 shared_key2:: shared_key2_value2
emoji_📅_key:: emoji_📅_key_value key📅:: 📅_key_value
# Heading 1 # Heading 1
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. #intext_tag1 Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu [intext_key:: intext_value] fugiat nulla (#intext_tag2) pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est lab Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. #intext_tag1 Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu [intext_key:: intext_value] fugiat nulla (#intext_tag2) pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est lab
```python ```python

View File

@@ -22,6 +22,19 @@ class KeyInputs:
THREE = "3" THREE = "3"
def remove_ansi(text) -> str:
"""Remove ANSI escape sequences from a string.
Args:
text (str): String to remove ANSI escape sequences from.
Returns:
str: String without ANSI escape sequences.
"""
ansi_chars = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
return ansi_chars.sub("", text)
class Regex: class Regex:
"""Assert that a given string meets some expectations. """Assert that a given string meets some expectations.

View File

@@ -36,7 +36,7 @@ def test_note_create(sample_note) -> None:
"date_created": ["2022-12-22"], "date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -58,9 +58,9 @@ def test_note_create(sample_note) -> None:
assert note.inline_metadata.dict == { assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"], "bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"], "bottom_key2": ["bottom_key2_value"],
"emoji_📅_key": ["emoji_📅_key_value"],
"intext_key": ["intext_value"], "intext_key": ["intext_value"],
"shared_key1": ["shared_key1_value"], "key📅": ["📅_key_value"],
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
"shared_key2": ["shared_key2_value2"], "shared_key2": ["shared_key2_value2"],
"top_key1": ["top_key1_value"], "top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"], "top_key2": ["top_key2_value"],
@@ -127,7 +127,7 @@ def test_add_metadata_frontmatter(sample_note) -> None:
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"new_key1": [], "new_key1": [],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -143,7 +143,7 @@ def test_add_metadata_frontmatter(sample_note) -> None:
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"new_key1": [], "new_key1": [],
"new_key2": ["new_key2_value"], "new_key2": ["new_key2_value"],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -164,7 +164,7 @@ def test_add_metadata_frontmatter(sample_note) -> None:
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"new_key1": [], "new_key1": [],
"new_key2": ["new_key2_value", "new_key2_value2", "new_key2_value3"], "new_key2": ["new_key2_value", "new_key2_value2", "new_key2_value3"],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -271,6 +271,14 @@ def test_delete_metadata(sample_note) -> Note:
assert "bottom_key2" not in note.inline_metadata.dict assert "bottom_key2" not in note.inline_metadata.dict
assert note.file_content != Regex(r"bottom_key2") assert note.file_content != Regex(r"bottom_key2")
assert note.delete_metadata("shared_key1", area=MetadataType.INLINE) is True
assert note.frontmatter.dict["shared_key1"] == ["shared_key1_value", "shared_key1_value3"]
assert "shared_key1" not in note.inline_metadata.dict
assert note.delete_metadata("shared_key2", area=MetadataType.FRONTMATTER) is True
assert note.inline_metadata.dict["shared_key2"] == ["shared_key2_value2"]
assert "shared_key2" not in note.frontmatter.dict
def test_has_changes(sample_note) -> None: def test_has_changes(sample_note) -> None:
"""Test has changes.""" """Test has changes."""
@@ -506,9 +514,9 @@ def test_rename_inline_metadata(sample_note) -> None:
assert note.file_content != Regex(r"bottom_key1::") assert note.file_content != Regex(r"bottom_key1::")
assert note.file_content == Regex(r"new_key::") assert note.file_content == Regex(r"new_key::")
note._rename_inline_metadata("emoji_📅_key", "emoji_📅_key_value", "new_value") note._rename_inline_metadata("key📅", "📅_key_value", "new_value")
assert note.file_content != Regex(r"emoji_📅_key:: ?emoji_📅_key_value") assert note.file_content != Regex(r"key📅:: ?📅_key_value")
assert note.file_content == Regex(r"emoji_📅_key:: ?new_value") assert note.file_content == Regex(r"key📅:: ?new_value")
def test_rename_metadata(sample_note) -> None: def test_rename_metadata(sample_note) -> None:
@@ -539,6 +547,251 @@ def test_rename_metadata(sample_note) -> None:
assert note.file_content == Regex(r"new_key:: new_value") assert note.file_content == Regex(r"new_key:: new_value")
def test_transpose_frontmatter(sample_note) -> None:
"""Test transposing metadata."""
note = Note(note_path=sample_note)
note.frontmatter.dict = {}
assert note.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE) is False
note = Note(note_path=sample_note)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="not_a_key",
)
is False
)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="frontmatter_Key2",
value="not_a_value",
)
is False
)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="frontmatter_Key2",
value=["not_a_value", "not_a_value2"],
)
is False
)
# Transpose all frontmatter metadata to inline metadata
assert note.transpose_metadata(begin=MetadataType.FRONTMATTER, end=MetadataType.INLINE) is True
assert note.frontmatter.dict == {}
assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": [
"shared_key1_value",
"shared_key1_value2",
"shared_key1_value3",
],
"shared_key2": ["shared_key2_value2", "shared_key2_value1"],
"tags": [
"frontmatter_tag1",
"frontmatter_tag2",
"shared_tag",
"📅/frontmatter_tag3",
],
"top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"],
}
# Transpose when key exists in both frontmatter and inline metadata
note = Note(note_path=sample_note)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="shared_key1",
)
is True
)
assert note.frontmatter.dict == {
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"],
"shared_key2": ["shared_key2_value1"],
"tags": [
"frontmatter_tag1",
"frontmatter_tag2",
"shared_tag",
"📅/frontmatter_tag3",
],
}
assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": [
"shared_key1_value",
"shared_key1_value2",
"shared_key1_value3",
],
"shared_key2": ["shared_key2_value2"],
"top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"],
}
# Transpose a single key and it's respective values
note = Note(note_path=sample_note)
assert (
note.transpose_metadata(
begin=MetadataType.INLINE,
end=MetadataType.FRONTMATTER,
key="top_key1",
)
is True
)
assert note.frontmatter.dict == {
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"],
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"],
"tags": [
"frontmatter_tag1",
"frontmatter_tag2",
"shared_tag",
"📅/frontmatter_tag3",
],
"top_key1": ["top_key1_value"],
}
assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
"shared_key2": ["shared_key2_value2"],
"top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"],
}
# Transpose a key when it's value is a list
note = Note(note_path=sample_note)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="frontmatter_Key2",
value=["article", "note"],
)
is True
)
assert note.frontmatter.dict == {
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"],
"tags": [
"frontmatter_tag1",
"frontmatter_tag2",
"shared_tag",
"📅/frontmatter_tag3",
],
}
assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"frontmatter_Key2": ["article", "note"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
"shared_key2": ["shared_key2_value2"],
"top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"],
}
# Transpose a string value from a key
note = Note(note_path=sample_note)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="frontmatter_Key2",
value="note",
)
is True
)
assert note.frontmatter.dict == {
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article"],
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"],
"tags": [
"frontmatter_tag1",
"frontmatter_tag2",
"shared_tag",
"📅/frontmatter_tag3",
],
}
assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"frontmatter_Key2": ["note"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
"shared_key2": ["shared_key2_value2"],
"top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"],
}
# Transpose list values from a key
note = Note(note_path=sample_note)
assert (
note.transpose_metadata(
begin=MetadataType.FRONTMATTER,
end=MetadataType.INLINE,
key="frontmatter_Key2",
value=["note", "article"],
)
is True
)
assert note.frontmatter.dict == {
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"],
"tags": [
"frontmatter_tag1",
"frontmatter_tag2",
"shared_tag",
"📅/frontmatter_tag3",
],
}
assert note.inline_metadata.dict == {
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"frontmatter_Key2": ["note", "article"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
"shared_key2": ["shared_key2_value2"],
"top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"],
"top_key3": ["top_key3_value_as_link"],
}
def test_update_frontmatter(sample_note) -> None: def test_update_frontmatter(sample_note) -> None:
"""Test replacing frontmatter.""" """Test replacing frontmatter."""
note = Note(note_path=sample_note) note = Note(note_path=sample_note)
@@ -556,7 +809,9 @@ frontmatter_Key1: some_new_key_here
frontmatter_Key2: frontmatter_Key2:
- article - article
- note - note
shared_key1: shared_key1_value shared_key1:
- shared_key1_value
- shared_key1_value3
shared_key2: shared_key2_value1 shared_key2: shared_key2_value1
---""" ---"""
assert new_frontmatter in note.file_content assert new_frontmatter in note.file_content

View File

@@ -29,12 +29,16 @@ def test_vault_creation(test_vault):
"bottom_key1": ["bottom_key1_value"], "bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"], "bottom_key2": ["bottom_key2_value"],
"date_created": ["2022-12-22"], "date_created": ["2022-12-22"],
"emoji_📅_key": ["emoji_📅_key_value"],
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"], "ignored_frontmatter": ["ignore_me"],
"intext_key": ["intext_value"], "intext_key": ["intext_value"],
"shared_key1": ["shared_key1_value"], "key📅": ["📅_key_value"],
"shared_key1": [
"shared_key1_value",
"shared_key1_value2",
"shared_key1_value3",
],
"shared_key2": ["shared_key2_value1", "shared_key2_value2"], "shared_key2": ["shared_key2_value1", "shared_key2_value2"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -63,9 +67,9 @@ def test_vault_creation(test_vault):
assert vault.metadata.inline_metadata == { assert vault.metadata.inline_metadata == {
"bottom_key1": ["bottom_key1_value"], "bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"], "bottom_key2": ["bottom_key2_value"],
"emoji_📅_key": ["emoji_📅_key_value"],
"intext_key": ["intext_value"], "intext_key": ["intext_value"],
"shared_key1": ["shared_key1_value"], "key📅": ["📅_key_value"],
"shared_key1": ["shared_key1_value", "shared_key1_value2"],
"shared_key2": ["shared_key2_value2"], "shared_key2": ["shared_key2_value2"],
"top_key1": ["top_key1_value"], "top_key1": ["top_key1_value"],
"top_key2": ["top_key2_value"], "top_key2": ["top_key2_value"],
@@ -77,7 +81,7 @@ def test_vault_creation(test_vault):
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"], "ignored_frontmatter": ["ignore_me"],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -104,13 +108,17 @@ def test_add_metadata(test_vault) -> None:
"bottom_key1": ["bottom_key1_value"], "bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"], "bottom_key2": ["bottom_key2_value"],
"date_created": ["2022-12-22"], "date_created": ["2022-12-22"],
"emoji_📅_key": ["emoji_📅_key_value"],
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"], "ignored_frontmatter": ["ignore_me"],
"intext_key": ["intext_value"], "intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"new_key": [], "new_key": [],
"shared_key1": ["shared_key1_value"], "shared_key1": [
"shared_key1_value",
"shared_key1_value2",
"shared_key1_value3",
],
"shared_key2": ["shared_key2_value1", "shared_key2_value2"], "shared_key2": ["shared_key2_value1", "shared_key2_value2"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -132,7 +140,7 @@ def test_add_metadata(test_vault) -> None:
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"], "ignored_frontmatter": ["ignore_me"],
"new_key": [], "new_key": [],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -150,14 +158,18 @@ def test_add_metadata(test_vault) -> None:
"bottom_key1": ["bottom_key1_value"], "bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"], "bottom_key2": ["bottom_key2_value"],
"date_created": ["2022-12-22"], "date_created": ["2022-12-22"],
"emoji_📅_key": ["emoji_📅_key_value"],
"frontmatter_Key1": ["author name"], "frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"], "frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"], "ignored_frontmatter": ["ignore_me"],
"intext_key": ["intext_value"], "intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"new_key": [], "new_key": [],
"new_key2": ["new_key2_value"], "new_key2": ["new_key2_value"],
"shared_key1": ["shared_key1_value"], "shared_key1": [
"shared_key1_value",
"shared_key1_value2",
"shared_key1_value3",
],
"shared_key2": ["shared_key2_value1", "shared_key2_value2"], "shared_key2": ["shared_key2_value1", "shared_key2_value2"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -180,7 +192,7 @@ def test_add_metadata(test_vault) -> None:
"ignored_frontmatter": ["ignore_me"], "ignored_frontmatter": ["ignore_me"],
"new_key": [], "new_key": [],
"new_key2": ["new_key2_value"], "new_key2": ["new_key2_value"],
"shared_key1": ["shared_key1_value"], "shared_key1": ["shared_key1_value", "shared_key1_value3"],
"shared_key2": ["shared_key2_value1"], "shared_key2": ["shared_key2_value1"],
"tags": [ "tags": [
"frontmatter_tag1", "frontmatter_tag1",
@@ -470,3 +482,51 @@ def test_rename_metadata(test_vault) -> None:
"shared_tag", "shared_tag",
"📅/frontmatter_tag3", "📅/frontmatter_tag3",
] ]
def test_transpose_metadata(test_vault) -> None:
"""Test transposing metadata."""
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.transpose_metadata(begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER) == 2
assert vault.metadata.inline_metadata == {}
assert vault.metadata.frontmatter == {
"author": ["author name"],
"bottom_key1": ["bottom_key1_value"],
"bottom_key2": ["bottom_key2_value"],
"date_created": ["2022-12-22"],
"frontmatter_Key1": ["author name"],
"frontmatter_Key2": ["article", "note"],
"ignored_frontmatter": ["ignore_me"],
"intext_key": ["intext_value"],
"key📅": ["📅_key_value"],
"shared_key1": [
"shared_key1_value",
"shared_key1_value2",
"shared_key1_value3",
],
"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.transpose_metadata(
begin=MetadataType.INLINE, end=MetadataType.FRONTMATTER, location=InsertLocation.TOP
)
== 0
)