feat: select insert location for new inline metadata

This commit is contained in:
Nathaniel Landau
2023-03-09 21:47:36 -05:00
parent b6a3d115fd
commit 1eb2d30d47
4 changed files with 166 additions and 134 deletions

View File

@@ -94,35 +94,6 @@ class Note:
_v = re.escape(_v)
self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True)
def _rename_inline_metadata(self, key: str, value_1: str, value_2: str = None) -> None:
"""Replace the inline metadata in the note with the current inline metadata object.
Args:
key (str): Key to rename.
value_1 (str): Value to replace OR new key name (if value_2 is None).
value_2 (str, optional): New value.
"""
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
for _k, _v in stripped_null_values:
if re.search(key, _k):
if value_2 is None:
if re.search(rf"{key}[^\w\d_-]+", _k):
key_text = re.split(r"[^\w\d_-]+$", _k)[0]
key_markdown = re.split(r"^[\w\d_-]+", _k)[1]
self.sub(
rf"{key_text}{key_markdown}::",
rf"{value_1}{key_markdown}::",
)
else:
self.sub(f"{_k}::", f"{value_1}::")
elif re.search(key, _k) and re.search(value_1, _v):
_k = re.escape(_k)
_v = re.escape(_v)
self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True)
def add_metadata( # noqa: C901
self,
area: MetadataType,
@@ -143,13 +114,13 @@ class Note:
"""
match area:
case MetadataType.FRONTMATTER if self.frontmatter.add(key, value):
self.update_frontmatter()
self.write_frontmatter()
return True
case MetadataType.INLINE:
if value is None and self.inline_metadata.add(key):
line = f"{key}::"
self.insert(new_string=line, location=location)
self.write_string(new_string=line, location=location)
return True
new_values = []
@@ -160,7 +131,7 @@ class Note:
if new_values:
for value in new_values:
self.insert(new_string=f"{key}:: {value}", location=location)
self.write_string(new_string=f"{key}:: {value}", location=location)
return True
case MetadataType.TAGS:
@@ -175,7 +146,7 @@ class Note:
_v = value
if _v.startswith("#"):
_v = _v[1:]
self.insert(new_string=f"#{_v}", location=location)
self.write_string(new_string=f"#{_v}", location=location)
return True
case _:
@@ -183,6 +154,28 @@ class Note:
return False
def commit(self, path: Path = None) -> None:
"""Write the note's content to disk. This is a destructive action.
Args:
path (Path): Path to write the note to. Defaults to the note's path.
Raises:
typer.Exit: If the note's path is not found.
"""
p = self.note_path if path is None else path
if self.dry_run:
log.trace(f"DRY RUN: Writing note {p} to disk")
return
try:
with open(p, "w") as f:
log.trace(f"Writing note {p} to disk")
f.write(self.file_content)
except FileNotFoundError as e:
alerts.error(f"Note {p} not found. Exiting")
raise typer.Exit(code=1) from e
def contains_inline_tag(self, tag: str, is_regex: bool = False) -> bool:
"""Check if a note contains the specified inline tag.
@@ -196,7 +189,7 @@ class Note:
return self.inline_tags.contains(tag, is_regex=is_regex)
def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool:
"""Check if a note has a key or a key-value pair in its metadata.
"""Check if a note has a key or a key-value pair in its Frontmatter or InlineMetadata.
Args:
key (str): Key to check for.
@@ -245,7 +238,7 @@ class Note:
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 Frontmatter or InlineMetadata. Regex is supported.
If no value is provided, will delete an entire key.
@@ -263,7 +256,7 @@ class Note:
if (
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
) and self.frontmatter.delete(key):
self.update_frontmatter()
self.write_frontmatter()
changed_value = True
if (
area == MetadataType.INLINE or area == MetadataType.ALL
@@ -274,7 +267,7 @@ class Note:
if (
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
) and self.frontmatter.delete(key, value):
self.update_frontmatter()
self.write_frontmatter()
changed_value = True
if (
area == MetadataType.INLINE or area == MetadataType.ALL
@@ -306,53 +299,6 @@ class Note:
return False
def insert(
self,
new_string: str,
location: InsertLocation,
allow_multiple: bool = False,
) -> None:
"""Insert a string at the top of a note.
Args:
new_string (str): String to insert at the top of the note.
allow_multiple (bool): Whether to allow inserting the string if it already exists in the note.
location (InsertLocation): Location to insert the string.
"""
if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0:
return
match location:
case InsertLocation.BOTTOM:
self.file_content += f"\n{new_string}"
case InsertLocation.TOP:
try:
top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter")
except AttributeError:
top = ""
if top == "":
self.file_content = f"{new_string}\n{self.file_content}"
else:
new_string = f"{top}\n{new_string}"
top = re.escape(top)
self.sub(top, new_string, is_regex=True)
case InsertLocation.AFTER_TITLE:
try:
top = PATTERNS.top_with_header.search(self.file_content).group("top")
except AttributeError:
top = ""
if top == "":
self.file_content = f"{new_string}\n{self.file_content}"
else:
new_string = f"{top}\n{new_string}"
top = re.escape(top)
self.sub(top, new_string, is_regex=True)
case _:
raise ValueError(f"Invalid location: {location}")
pass
def print_note(self) -> None:
"""Print the note to the console."""
console.print(self.file_content)
@@ -395,7 +341,7 @@ class Note:
return False
def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool:
"""Rename a key or key-value pair in the note's metadata.
"""Rename a key or key-value pair in the note's InlineMetadata and Frontmatter objects.
If no value is provided, will rename an entire key.
@@ -410,17 +356,17 @@ class Note:
changed_value: bool = False
if value_2 is None:
if self.frontmatter.rename(key, value_1):
self.update_frontmatter()
self.write_frontmatter()
changed_value = True
if self.inline_metadata.rename(key, value_1):
self._rename_inline_metadata(key, value_1)
self.write_metadata(key, value_1)
changed_value = True
else:
if self.frontmatter.rename(key, value_1, value_2):
self.update_frontmatter()
self.write_frontmatter()
changed_value = True
if self.inline_metadata.rename(key, value_1, value_2):
self._rename_inline_metadata(key, value_1, value_2)
self.write_metadata(key, value_1, value_2)
changed_value = True
if changed_value:
@@ -520,7 +466,7 @@ class Note:
return False
def update_frontmatter(self, sort_keys: bool = False) -> None:
def write_frontmatter(self, sort_keys: bool = False) -> None:
"""Replace the frontmatter in the note with the current frontmatter object."""
try:
current_frontmatter = PATTERNS.frontmatter_block.search(self.file_content).group(
@@ -542,24 +488,78 @@ class Note:
current_frontmatter = f"{re.escape(current_frontmatter)}\n?"
self.sub(current_frontmatter, new_frontmatter, is_regex=True)
def write(self, path: Path = None) -> None:
"""Write the note's content to disk.
def write_metadata(self, key: str, value_1: str, value_2: str = None) -> None:
"""Write changes to a specific inline metadata key or value.
Args:
path (Path): Path to write the note to. Defaults to the note's path.
key (str): Key to rename.
value_1 (str): Value to replace OR new key name (if value_2 is None).
value_2 (str, optional): New value.
Raises:
typer.Exit: If the note's path is not found.
"""
p = self.note_path if path is None else path
if self.dry_run:
log.trace(f"DRY RUN: Writing note {p} to disk")
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
for _k, _v in stripped_null_values:
if re.search(key, _k):
if value_2 is None:
if re.search(rf"{key}[^\w\d_-]+", _k):
key_text = re.split(r"[^\w\d_-]+$", _k)[0]
key_markdown = re.split(r"^[\w\d_-]+", _k)[1]
self.sub(
rf"{key_text}{key_markdown}::",
rf"{value_1}{key_markdown}::",
)
else:
self.sub(f"{_k}::", f"{value_1}::")
elif re.search(key, _k) and re.search(value_1, _v):
_k = re.escape(_k)
_v = re.escape(_v)
self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True)
def write_string(
self,
new_string: str,
location: InsertLocation,
allow_multiple: bool = False,
) -> None:
"""Insert a string into the note at a requested location.
Args:
new_string (str): String to insert at the top of the note.
allow_multiple (bool): Whether to allow inserting the string if it already exists in the note.
location (InsertLocation): Location to insert the string.
"""
if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0:
return
match location:
case InsertLocation.BOTTOM:
self.file_content += f"\n{new_string}"
case InsertLocation.TOP:
try:
with open(p, "w") as f:
log.trace(f"Writing note {p} to disk")
f.write(self.file_content)
except FileNotFoundError as e:
alerts.error(f"Note {p} not found. Exiting")
raise typer.Exit(code=1) from e
top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter")
except AttributeError:
top = ""
if top == "":
self.file_content = f"{new_string}\n{self.file_content}"
else:
new_string = f"{top}\n{new_string}"
top = re.escape(top)
self.sub(top, new_string, is_regex=True)
case InsertLocation.AFTER_TITLE:
try:
top = PATTERNS.top_with_header.search(self.file_content).group("top")
except AttributeError:
top = ""
if top == "":
self.file_content = f"{new_string}\n{self.file_content}"
else:
new_string = f"{top}\n{new_string}"
top = re.escape(top)
self.sub(top, new_string, is_regex=True)
case _:
raise ValueError(f"Invalid location: {location}")
pass

View File

@@ -114,7 +114,7 @@ class Vault:
return notes_list
def _find_insert_location(self) -> InsertLocation:
"""Find the insert location for a note.
"""Find the insert location for a note from the configuration file.
Returns:
InsertLocation: Insert location for the note.
@@ -130,6 +130,24 @@ class Vault:
return InsertLocation.BOTTOM
@property
def insert_location(self) -> InsertLocation:
"""Location to insert new or reorganized metadata.
Returns:
InsertLocation: The insert location.
"""
return self._insert_location
@insert_location.setter
def insert_location(self, value: InsertLocation) -> None:
"""Set the insert location for the vault.
Args:
value (InsertLocation): The insert location to set.
"""
self._insert_location = value
def _find_markdown_notes(self) -> list[Path]:
"""Build list of all markdown files in the vault.
@@ -234,7 +252,7 @@ class Vault:
for _note in self.notes_in_scope:
if _note.has_changes():
log.trace(f"writing to {_note.note_path}")
_note.write()
_note.commit()
def delete_backup(self) -> None:
"""Delete the vault backup."""

View File

@@ -297,7 +297,7 @@ def test_has_changes(sample_note) -> None:
note = Note(note_path=sample_note)
assert note.has_changes() is False
note.insert("This is a test string.", location=InsertLocation.BOTTOM)
note.write_string("This is a test string.", location=InsertLocation.BOTTOM)
assert note.has_changes() is True
note = Note(note_path=sample_note)
@@ -316,7 +316,7 @@ def test_has_changes(sample_note) -> None:
assert note.has_changes() is True
def test_insert_bottom(short_note) -> None:
def test_write_string_bottom(short_note) -> None:
"""Test inserting metadata to bottom of note."""
path1, path2 = short_note
note = Note(note_path=str(path1))
@@ -353,20 +353,20 @@ Lorem ipsum dolor sit amet.
This is a test string.
"""
note.insert(new_string=string1, location=InsertLocation.BOTTOM)
note.write_string(new_string=string1, location=InsertLocation.BOTTOM)
assert note.file_content == correct_content.strip()
note.insert(new_string=string2, location=InsertLocation.BOTTOM)
note.write_string(new_string=string2, location=InsertLocation.BOTTOM)
assert note.file_content == correct_content.strip()
note.insert(new_string=string2, allow_multiple=True, location=InsertLocation.BOTTOM)
note.write_string(new_string=string2, allow_multiple=True, location=InsertLocation.BOTTOM)
assert note.file_content == correct_content2.strip()
note2.insert(new_string=string1, location=InsertLocation.BOTTOM)
note2.write_string(new_string=string1, location=InsertLocation.BOTTOM)
assert note2.file_content == correct_content3.strip()
def test_insert_after_frontmatter(short_note) -> None:
def test_write_string_after_frontmatter(short_note) -> None:
"""Test inserting metadata to bottom of note."""
path1, path2 = short_note
note = Note(note_path=path1)
@@ -401,17 +401,17 @@ This is a test string.
Lorem ipsum dolor sit amet.
"""
note.insert(new_string=string1, location=InsertLocation.TOP)
note.write_string(new_string=string1, location=InsertLocation.TOP)
assert note.file_content.strip() == correct_content.strip()
note.insert(new_string=string2, allow_multiple=True, location=InsertLocation.TOP)
note.write_string(new_string=string2, allow_multiple=True, location=InsertLocation.TOP)
assert note.file_content.strip() == correct_content2.strip()
note2.insert(new_string=string1, location=InsertLocation.TOP)
note2.write_string(new_string=string1, location=InsertLocation.TOP)
assert note2.file_content.strip() == correct_content3.strip()
def test_insert_after_title(short_note) -> None:
def test_write_string_after_title(short_note) -> None:
"""Test inserting metadata to bottom of note."""
path1, path2 = short_note
note = Note(note_path=path1)
@@ -446,13 +446,13 @@ This is a test string.
Lorem ipsum dolor sit amet.
"""
note.insert(new_string=string1, location=InsertLocation.AFTER_TITLE)
note.write_string(new_string=string1, location=InsertLocation.AFTER_TITLE)
assert note.file_content.strip() == correct_content.strip()
note.insert(new_string=string2, allow_multiple=True, location=InsertLocation.AFTER_TITLE)
note.write_string(new_string=string2, allow_multiple=True, location=InsertLocation.AFTER_TITLE)
assert note.file_content.strip() == correct_content2.strip()
note2.insert(new_string=string1, location=InsertLocation.AFTER_TITLE)
note2.write_string(new_string=string1, location=InsertLocation.AFTER_TITLE)
assert note2.file_content.strip() == correct_content3.strip()
@@ -470,7 +470,7 @@ def test_print_diff(sample_note, capsys) -> None:
"""Test printing diff."""
note = Note(note_path=sample_note)
note.insert("This is a test string.", location=InsertLocation.BOTTOM)
note.write_string("This is a test string.", location=InsertLocation.BOTTOM)
note.print_diff()
captured = capsys.readouterr()
assert "+ This is a test string." in captured.out
@@ -513,20 +513,20 @@ def test_rename_inline_tag(sample_note) -> None:
assert note.file_content != Regex(r"#intext_tag1")
def test_rename_inline_metadata(sample_note) -> None:
def test_write_metadata(sample_note) -> None:
"""Test renaming inline metadata."""
note = Note(note_path=sample_note)
note._rename_inline_metadata("nonexistent_key", "new_key")
note.write_metadata("nonexistent_key", "new_key")
assert note.file_content == note.original_file_content
note._rename_inline_metadata("bottom_key1", "no_value", "new_value")
note.write_metadata("bottom_key1", "no_value", "new_value")
assert note.file_content == note.original_file_content
note._rename_inline_metadata("bottom_key1", "new_key")
note.write_metadata("bottom_key1", "new_key")
assert note.file_content != Regex(r"bottom_key1::")
assert note.file_content == Regex(r"new_key::")
note._rename_inline_metadata("key📅", "📅_key_value", "new_value")
note.write_metadata("key📅", "📅_key_value", "new_value")
assert note.file_content != Regex(r"key📅:: ?📅_key_value")
assert note.file_content == Regex(r"key📅:: ?new_value")
@@ -804,12 +804,12 @@ def test_transpose_frontmatter(sample_note) -> None:
}
def test_update_frontmatter(sample_note) -> None:
def test_write_frontmatter(sample_note) -> None:
"""Test replacing frontmatter."""
note = Note(note_path=sample_note)
note.rename_metadata("frontmatter_Key1", "author name", "some_new_key_here")
note.update_frontmatter()
note.write_frontmatter()
new_frontmatter = """---
date_created: '2022-12-22'
tags:
@@ -831,9 +831,9 @@ shared_key2: shared_key2_value1
assert "```python" in note.file_content
note2 = Note(note_path="tests/fixtures/test_vault/no_metadata.md")
note2.update_frontmatter()
note2.write_frontmatter()
note2.frontmatter.dict = {"key1": "value1", "key2": "value2"}
note2.update_frontmatter()
note2.write_frontmatter()
new_frontmatter = """---
key1: value1
key2: value2
@@ -842,18 +842,18 @@ key2: value2
assert "Lorem ipsum dolor sit amet" in note2.file_content
def test_write(sample_note, tmp_path) -> None:
def test_commit(sample_note, tmp_path) -> None:
"""Test writing note to file."""
note = Note(note_path=sample_note)
note.sub(pattern="Heading 1", replacement="Heading 2")
note.write()
note.commit()
note = Note(note_path=sample_note)
assert "Heading 2" in note.file_content
assert "Heading 1" not in note.file_content
new_path = Path(tmp_path / "new_note.md")
note.write(new_path)
note.commit(new_path)
note2 = Note(note_path=new_path)
assert "Heading 2" in note2.file_content
assert "Heading 1" not in note2.file_content

View File

@@ -95,6 +95,20 @@ def test_vault_creation(test_vault):
}
def set_insert_location(test_vault):
"""Test setting a new insert location."""
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.name == "vault"
assert vault.vault_path == vault_path
assert vault.insert_location == InsertLocation.BOTTOM
vault.insert_location = InsertLocation.TOP
assert vault.insert_location == InsertLocation.TOP
def test_add_metadata(test_vault) -> None:
"""Test adding metadata to the vault."""
vault_path = test_vault