mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-17 09:23:40 -05:00
feat: transpose metadata between frontmatter and inline
This commit is contained in:
@@ -61,7 +61,7 @@ repos:
|
|||||||
entry: yamllint --strict --config-file .yamllint.yml
|
entry: yamllint --strict --config-file .yamllint.yml
|
||||||
|
|
||||||
- repo: "https://github.com/charliermarsh/ruff-pre-commit"
|
- repo: "https://github.com/charliermarsh/ruff-pre-commit"
|
||||||
rev: "v0.0.253"
|
rev: "v0.0.254"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--extend-ignore", "I001,D301,D401"]
|
args: ["--extend-ignore", "I001,D301,D401"]
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
|
|||||||
- **List notes in scope**: List notes that will be processed.
|
- **List notes in scope**: List notes that will be processed.
|
||||||
|
|
||||||
**Add Metadata**: Add new metadata to your vault.
|
**Add Metadata**: Add new metadata to your vault.
|
||||||
|
When adding a new key to inline metadata, the `insert location` value in the config file will specify where in the note it will be inserted.
|
||||||
|
|
||||||
- **Add new metadata to the frontmatter**
|
- **Add new metadata to the frontmatter**
|
||||||
- **Add new inline metadata** - Set `insert_location` in the config to control where the new metadata is inserted. (Default: Bottom)
|
- **Add new inline metadata** - Set `insert_location` in the config to control where the new metadata is inserted. (Default: Bottom)
|
||||||
@@ -78,6 +79,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive
|
|||||||
- **Delete an inline tag**
|
- **Delete an inline tag**
|
||||||
|
|
||||||
**Transpose Metadata**: Move metadata from inline to frontmatter or the reverse.
|
**Transpose Metadata**: Move metadata from inline to frontmatter or the reverse.
|
||||||
|
When transposing to inline metadata, the `insert location` value in the config file will specify where in the note it will be inserted.
|
||||||
|
|
||||||
- **Transpose all metadata** - Moves all frontmatter to inline metadata, 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 key** - Transposes a specific key and all it's values
|
||||||
@@ -110,7 +112,7 @@ Below is an example with two vaults.
|
|||||||
|
|
||||||
# Location to add metadata. One of:
|
# Location to add metadata. One of:
|
||||||
# TOP: Directly after frontmatter.
|
# TOP: Directly after frontmatter.
|
||||||
# AFTER_TITLE: After a header following frontmatter.
|
# AFTER_TITLE: After the first header following frontmatter.
|
||||||
# BOTTOM: The bottom of the note
|
# BOTTOM: The bottom of the note
|
||||||
insert_location = "BOTTOM"
|
insert_location = "BOTTOM"
|
||||||
|
|
||||||
|
|||||||
44
poetry.lock
generated
44
poetry.lock
generated
@@ -616,14 +616,14 @@ dev = ["black", "hypothesis", "mypy", "pygments (>=2.14.0)", "pytest", "pytest-c
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "3.1.0"
|
version = "3.1.1"
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"},
|
{file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"},
|
||||||
{file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"},
|
{file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -1102,29 +1102,29 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.0.253"
|
version = "0.0.254"
|
||||||
description = "An extremely fast Python linter, written in Rust."
|
description = "An extremely fast Python linter, written in Rust."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.0.253-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:69126b80d4da50a394cfe9da947377841cc6c83b0e05cfe9933672ce5c61bfcf"},
|
{file = "ruff-0.0.254-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0"},
|
||||||
{file = "ruff-0.0.253-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0f44caf5bbdaeacc3cba4ee3369638e4f6e4e71c9ca773d2f3fc3f65e4bfb434"},
|
{file = "ruff-0.0.254-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8144a2fd6533e7a0dbaaf9a3dde44b8414eebf5a86a1fe21e0471d052a3e9c14"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07603b362f0dad56e30e7ef2f37bf480732ff8bcf52fe4fd6c9445eb42259f42"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68f9a50f48510a443ec57bcf51656bbef47e5972290c450398108ac2a53dfd32"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed42010c379d42b81b537957b413cf8531a00d0a6270913e8527d9d73c7e0c"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba4b3921fa9c59855b66e1a5ef140d0d872f15a83282bff5b5e3e8db89a45aa2"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60bda6fd99f9d3919df4362b671a12c83ef83279fc7bc1dc0e1aa689dfd91a71"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa"},
|
||||||
{file = "ruff-0.0.253-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19061d9b5809a0505a233580b48b59b847823ab90e266f8ae40cb31d3708bacf"},
|
{file = "ruff-0.0.254-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6"},
|
||||||
{file = "ruff-0.0.253-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ee92a7688f327c664891567aa24e4a8cae8635934df95e0dbe65b0e991fcc6e"},
|
{file = "ruff-0.0.254-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21"},
|
||||||
{file = "ruff-0.0.253-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f0ff811ea61684c6e9284afa701b8388818ab5ef8ebd6144c15c9ba64f459f1e"},
|
{file = "ruff-0.0.254-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885"},
|
||||||
{file = "ruff-0.0.253-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4548734b2671b80ee4c20aa410d7d2a5b32f087f8759d4f5991c74b8cfa51d7b"},
|
{file = "ruff-0.0.254-py3-none-musllinux_1_2_i686.whl", hash = "sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c"},
|
||||||
{file = "ruff-0.0.253-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e2485f728f04bf3bd6142e55dd2869c769299b73a4bdbe1a795e98332df75561"},
|
{file = "ruff-0.0.254-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f"},
|
||||||
{file = "ruff-0.0.253-py3-none-win32.whl", hash = "sha256:a66109185382375246d7b0dae2f594801fd8ceb5f8206159c55791aaec9aa4bb"},
|
{file = "ruff-0.0.254-py3-none-win32.whl", hash = "sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15"},
|
||||||
{file = "ruff-0.0.253-py3-none-win_amd64.whl", hash = "sha256:a64e9f97a6b0bfce924e65fa845f669c969d42c30fb61e1e4d87b2c70d835cb9"},
|
{file = "ruff-0.0.254-py3-none-win_amd64.whl", hash = "sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec"},
|
||||||
{file = "ruff-0.0.253-py3-none-win_arm64.whl", hash = "sha256:506987ac3bc212cd74bf1ca032756e67ada93c4add3b7541e3549bbad5e0fc40"},
|
{file = "ruff-0.0.254-py3-none-win_arm64.whl", hash = "sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09"},
|
||||||
{file = "ruff-0.0.253.tar.gz", hash = "sha256:ab746c843a9673d2637bcbcb45da12ed4d44c0c90f0823484d6dcb660118b539"},
|
{file = "ruff-0.0.254.tar.gz", hash = "sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1349,4 +1349,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "30cbfdd2c49ef85a89a19ff225241872fcbbc3648195075118933f4e8e8d4eb4"
|
content-hash = "b77f1653a71eca187c8d47f8b27d2677191fe37d932fc9ae8e696c462d2ea999"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
poethepoet = "^0.18.1"
|
poethepoet = "^0.18.1"
|
||||||
pre-commit = "^3.1.1"
|
pre-commit = "^3.1.1"
|
||||||
pysnooper = "^1.1.1"
|
pysnooper = "^1.1.1"
|
||||||
ruff = "^0.0.253"
|
ruff = "^0.0.254"
|
||||||
typeguard = "^2.13.3"
|
typeguard = "^2.13.3"
|
||||||
types-python-dateutil = "^2.8.19.10"
|
types-python-dateutil = "^2.8.19.10"
|
||||||
vulture = "^2.7"
|
vulture = "^2.7"
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ class Config:
|
|||||||
# Folders within the vault to ignore when indexing metadata
|
# Folders within the vault to ignore when indexing metadata
|
||||||
exclude_paths = [".git", ".obsidian"]
|
exclude_paths = [".git", ".obsidian"]
|
||||||
|
|
||||||
# Location to add metadata. One of:
|
# Location to add new metadata. One of:
|
||||||
# TOP: Directly after frontmatter.
|
# TOP: Directly after frontmatter.
|
||||||
# AFTER_TITLE: After a header following frontmatter.
|
# AFTER_TITLE: After the first header following frontmatter.
|
||||||
# BOTTOM: The bottom of the note
|
# BOTTOM: The bottom of the note
|
||||||
insert_location = "BOTTOM"
|
insert_location = "BOTTOM"
|
||||||
"""
|
"""
|
||||||
@@ -164,6 +164,7 @@ class VaultConfig:
|
|||||||
yield "config", self.config
|
yield "config", self.config
|
||||||
yield "path", self.path
|
yield "path", self.path
|
||||||
yield "exclude_paths", self.exclude_paths
|
yield "exclude_paths", self.exclude_paths
|
||||||
|
yield "insert_location", self.insert_location
|
||||||
|
|
||||||
def _validate_vault_path(self, vault_path: Path | None) -> Path:
|
def _validate_vault_path(self, vault_path: Path | None) -> Path:
|
||||||
"""Validate the vault path."""
|
"""Validate the vault path."""
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def clear_screen() -> None: # pragma: no cover
|
|||||||
def dict_contains(
|
def dict_contains(
|
||||||
dictionary: dict[str, list[str]], key: str, value: str = None, is_regex: bool = False
|
dictionary: dict[str, list[str]], key: str, value: str = None, is_regex: bool = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if a dictionary contains a key.
|
"""Check if a dictionary contains a key or if a specified key contains a value.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dictionary (dict): Dictionary to check
|
dictionary (dict): Dictionary to check
|
||||||
|
|||||||
@@ -23,5 +23,5 @@ class InsertLocation(Enum):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
TOP = "Top"
|
TOP = "Top"
|
||||||
AFTER_TITLE = "Header"
|
AFTER_TITLE = "After title"
|
||||||
BOTTOM = "Bottom"
|
BOTTOM = "Bottom"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Representation of notes and in the vault."""
|
"""Representation of a not in the vault."""
|
||||||
|
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
@@ -71,29 +71,6 @@ class Note:
|
|||||||
yield "inline_tags", self.inline_tags
|
yield "inline_tags", self.inline_tags
|
||||||
yield "inline_metadata", self.inline_metadata
|
yield "inline_metadata", self.inline_metadata
|
||||||
|
|
||||||
def _delete_inline_metadata(self, key: str, value: str = None) -> None:
|
|
||||||
"""Delete an inline metadata key/value pair from the text of the note. This method does not remove the key/value from the metadata attribute of the note.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key (str): Key to delete.
|
|
||||||
value (str, optional): Value to delete.
|
|
||||||
"""
|
|
||||||
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 is None:
|
|
||||||
_k = re.escape(_k)
|
|
||||||
_v = re.escape(_v)
|
|
||||||
self.sub(rf"\[?{_k}:: ?{_v}]?", "", is_regex=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if re.search(value, _v):
|
|
||||||
_k = re.escape(_k)
|
|
||||||
_v = re.escape(_v)
|
|
||||||
self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True)
|
|
||||||
|
|
||||||
def add_metadata( # noqa: C901
|
def add_metadata( # noqa: C901
|
||||||
self,
|
self,
|
||||||
area: MetadataType,
|
area: MetadataType,
|
||||||
@@ -101,7 +78,7 @@ class Note:
|
|||||||
value: str | list[str] = None,
|
value: str | list[str] = None,
|
||||||
location: InsertLocation = None,
|
location: InsertLocation = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Add metadata to the note if it does not already exist.
|
"""Add metadata to the note if it does not already exist. This method adds specified metadata to the appropriate MetadataType object AND writes the new metadata to the note's file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
area (MetadataType): Area to add metadata to.
|
area (MetadataType): Area to add metadata to.
|
||||||
@@ -155,7 +132,7 @@ class Note:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def commit(self, path: Path = None) -> None:
|
def commit(self, path: Path = None) -> None:
|
||||||
"""Write the note's content to disk. This is a destructive action.
|
"""Write the note's new content to disk. This is a destructive action.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (Path): Path to write the note to. Defaults to the note's path.
|
path (Path): Path to write the note to. Defaults to the note's path.
|
||||||
@@ -238,9 +215,9 @@ class Note:
|
|||||||
def delete_metadata(
|
def delete_metadata(
|
||||||
self, key: str, value: str = None, area: MetadataType = MetadataType.ALL
|
self, key: str, value: str = None, area: MetadataType = MetadataType.ALL
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Delete a key or key-value pair from the note's Frontmatter or InlineMetadata. Regex is supported.
|
"""Delete a key or key-value pair from the note's Metadata object and the content of the note. Regex is supported.
|
||||||
|
|
||||||
If no value is provided, will delete an entire key.
|
If no value is provided, will delete an entire specified key.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Key to delete.
|
key (str): Key to delete.
|
||||||
@@ -252,27 +229,17 @@ class Note:
|
|||||||
"""
|
"""
|
||||||
changed_value: bool = False
|
changed_value: bool = False
|
||||||
|
|
||||||
if value is None:
|
|
||||||
if (
|
|
||||||
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
|
|
||||||
) and self.frontmatter.delete(key):
|
|
||||||
self.write_frontmatter()
|
|
||||||
changed_value = True
|
|
||||||
if (
|
|
||||||
area == MetadataType.INLINE or area == MetadataType.ALL
|
|
||||||
) and self.inline_metadata.delete(key):
|
|
||||||
self._delete_inline_metadata(key, value)
|
|
||||||
changed_value = True
|
|
||||||
else:
|
|
||||||
if (
|
if (
|
||||||
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
|
area == MetadataType.FRONTMATTER or area == MetadataType.ALL
|
||||||
) and self.frontmatter.delete(key, value):
|
) and self.frontmatter.delete(key, value):
|
||||||
self.write_frontmatter()
|
self.write_frontmatter()
|
||||||
changed_value = True
|
changed_value = True
|
||||||
|
|
||||||
if (
|
if (
|
||||||
area == MetadataType.INLINE or area == MetadataType.ALL
|
area == MetadataType.INLINE or area == MetadataType.ALL
|
||||||
) and self.inline_metadata.delete(key, value):
|
) and self.inline_metadata.contains(key, value):
|
||||||
self._delete_inline_metadata(key, value)
|
self.write_delete_inline_metadata(key, value)
|
||||||
|
self.inline_metadata.delete(key, value)
|
||||||
changed_value = True
|
changed_value = True
|
||||||
|
|
||||||
if changed_value:
|
if changed_value:
|
||||||
@@ -299,12 +266,8 @@ class Note:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def print_note(self) -> None:
|
|
||||||
"""Print the note to the console."""
|
|
||||||
console.print(self.file_content)
|
|
||||||
|
|
||||||
def print_diff(self) -> None:
|
def print_diff(self) -> None:
|
||||||
"""Print a diff of the note's original state and it's new state."""
|
"""Print a diff of the note's content. Compares original state to it's new state."""
|
||||||
a = self.original_file_content.splitlines()
|
a = self.original_file_content.splitlines()
|
||||||
b = self.file_content.splitlines()
|
b = self.file_content.splitlines()
|
||||||
|
|
||||||
@@ -320,8 +283,12 @@ class Note:
|
|||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
def print_note(self) -> None:
|
||||||
|
"""Print the note to the console."""
|
||||||
|
console.print(self.file_content)
|
||||||
|
|
||||||
def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool:
|
def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool:
|
||||||
"""Rename an inline tag from the note ONLY if it's not in the metadata as well.
|
"""Rename an inline tag. Updates the Metadata object and the text of the note.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tag_1 (str): Tag to rename.
|
tag_1 (str): Tag to rename.
|
||||||
@@ -341,9 +308,9 @@ class Note:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool:
|
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 InlineMetadata and Frontmatter objects.
|
"""Rename a key or key-value pair in the note's InlineMetadata and Frontmatter objects and the content of the note.
|
||||||
|
|
||||||
If no value is provided, will rename an entire key.
|
If no value is provided, will rename the entire specified key.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Key to rename.
|
key (str): Key to rename.
|
||||||
@@ -359,14 +326,14 @@ class Note:
|
|||||||
self.write_frontmatter()
|
self.write_frontmatter()
|
||||||
changed_value = True
|
changed_value = True
|
||||||
if self.inline_metadata.rename(key, value_1):
|
if self.inline_metadata.rename(key, value_1):
|
||||||
self.write_metadata(key, value_1)
|
self.write_inline_metadata_change(key, value_1)
|
||||||
changed_value = True
|
changed_value = True
|
||||||
else:
|
else:
|
||||||
if self.frontmatter.rename(key, value_1, value_2):
|
if self.frontmatter.rename(key, value_1, value_2):
|
||||||
self.write_frontmatter()
|
self.write_frontmatter()
|
||||||
changed_value = True
|
changed_value = True
|
||||||
if self.inline_metadata.rename(key, value_1, value_2):
|
if self.inline_metadata.rename(key, value_1, value_2):
|
||||||
self.write_metadata(key, value_1, value_2)
|
self.write_inline_metadata_change(key, value_1, value_2)
|
||||||
changed_value = True
|
changed_value = True
|
||||||
|
|
||||||
if changed_value:
|
if changed_value:
|
||||||
@@ -395,12 +362,15 @@ class Note:
|
|||||||
value: str | list[str] = None,
|
value: str | list[str] = None,
|
||||||
location: InsertLocation = InsertLocation.BOTTOM,
|
location: InsertLocation = InsertLocation.BOTTOM,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Transpose metadata from one type to another.
|
"""Move metadata from one metadata object to another. i.e. Frontmatter to InlineMetadata or vice versa.
|
||||||
|
|
||||||
|
If no key is specified, will transpose all metadata. If a key is specified, but no value, the entire key will be transposed. if a specific value is specified, just that value will be transposed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
begin (MetadataType): The type of metadata to transpose from.
|
begin (MetadataType): The type of metadata to transpose from.
|
||||||
end (MetadataType): The type of metadata to transpose to.
|
end (MetadataType): The type of metadata to transpose to.
|
||||||
key (str, optional): The key to transpose. Defaults to None.
|
key (str, optional): The key to transpose. Defaults to None.
|
||||||
|
location (InsertLocation, optional): Where to insert the metadata. Defaults to InsertLocation.BOTTOM.
|
||||||
value (str | list[str], optional): The value to transpose. Defaults to None.
|
value (str | list[str], optional): The value to transpose. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -466,8 +436,40 @@ class Note:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def write_frontmatter(self, sort_keys: bool = False) -> None:
|
def write_delete_inline_metadata(self, key: str = None, value: str = None) -> bool:
|
||||||
"""Replace the frontmatter in the note with the current frontmatter object."""
|
"""For a given inline metadata key and/or key-value pair, delete it from the text of the note. If no key is provided, will delete all inline metadata from the text of the note.
|
||||||
|
|
||||||
|
IMPORTANT: This method makes no changes to the InlineMetadata object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str, optional): Key to delete.
|
||||||
|
value (str, optional): Value to delete.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether the note was updated.
|
||||||
|
"""
|
||||||
|
for _k, _v in self.inline_metadata.dict.items():
|
||||||
|
if re.search(key, _k):
|
||||||
|
for _value in _v:
|
||||||
|
if value is None:
|
||||||
|
_k = re.escape(_k)
|
||||||
|
_value = re.escape(_value)
|
||||||
|
self.sub(rf"\[?{_k}:: ?{_value}]?", "", is_regex=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if re.search(value, _value):
|
||||||
|
_k = re.escape(_k)
|
||||||
|
_value = re.escape(_value)
|
||||||
|
self.sub(rf"({_k}::) ?{_value}", r"\1", is_regex=True)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write_frontmatter(self, sort_keys: bool = False) -> bool:
|
||||||
|
"""Replace the frontmatter in the note with the current Frontmatter object. If the Frontmatter object is empty, will delete the frontmatter from the note.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether the note was updated.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
current_frontmatter = PATTERNS.frontmatter_block.search(self.file_content).group(
|
current_frontmatter = PATTERNS.frontmatter_block.search(self.file_content).group(
|
||||||
"frontmatter"
|
"frontmatter"
|
||||||
@@ -476,19 +478,43 @@ class Note:
|
|||||||
current_frontmatter = None
|
current_frontmatter = None
|
||||||
|
|
||||||
if current_frontmatter is None and self.frontmatter.dict == {}:
|
if current_frontmatter is None and self.frontmatter.dict == {}:
|
||||||
return
|
return False
|
||||||
|
|
||||||
new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys)
|
new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys)
|
||||||
new_frontmatter = "" if self.frontmatter.dict == {} else f"---\n{new_frontmatter}---\n"
|
new_frontmatter = "" if self.frontmatter.dict == {} else 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 True
|
||||||
|
|
||||||
current_frontmatter = f"{re.escape(current_frontmatter)}\n?"
|
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)
|
||||||
|
return True
|
||||||
|
|
||||||
def write_metadata(self, key: str, value_1: str, value_2: str = None) -> None:
|
def write_all_inline_metadata(
|
||||||
|
self,
|
||||||
|
location: InsertLocation,
|
||||||
|
) -> bool:
|
||||||
|
"""Write all metadata found in the InlineMetadata object to the note at a specified insert location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
location (InsertLocation): Where to insert the metadata.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether the note was updated.
|
||||||
|
"""
|
||||||
|
if self.inline_metadata.dict != {}:
|
||||||
|
string = ""
|
||||||
|
for k, v in sorted(self.inline_metadata.dict.items()):
|
||||||
|
for value in v:
|
||||||
|
string += f"{k}:: {value}\n"
|
||||||
|
|
||||||
|
if self.write_string(new_string=string, location=location, allow_multiple=True):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def write_inline_metadata_change(self, key: str, value_1: str, value_2: str = None) -> None:
|
||||||
"""Write changes to a specific inline metadata key or value.
|
"""Write changes to a specific inline metadata key or value.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -503,9 +529,9 @@ class Note:
|
|||||||
for _k, _v in stripped_null_values:
|
for _k, _v in stripped_null_values:
|
||||||
if re.search(key, _k):
|
if re.search(key, _k):
|
||||||
if value_2 is None:
|
if value_2 is None:
|
||||||
if re.search(rf"{key}[^\w\d_-]+", _k):
|
if re.search(rf"{key}[^\\w\\d_-]+", _k):
|
||||||
key_text = re.split(r"[^\w\d_-]+$", _k)[0]
|
key_text = re.split(r"[^\\w\\d_-]+$", _k)[0]
|
||||||
key_markdown = re.split(r"^[\w\d_-]+", _k)[1]
|
key_markdown = re.split(r"^[\\w\\d_-]+", _k)[1]
|
||||||
self.sub(
|
self.sub(
|
||||||
rf"{key_text}{key_markdown}::",
|
rf"{key_text}{key_markdown}::",
|
||||||
rf"{value_1}{key_markdown}::",
|
rf"{value_1}{key_markdown}::",
|
||||||
@@ -522,20 +548,24 @@ class Note:
|
|||||||
new_string: str,
|
new_string: str,
|
||||||
location: InsertLocation,
|
location: InsertLocation,
|
||||||
allow_multiple: bool = False,
|
allow_multiple: bool = False,
|
||||||
) -> None:
|
) -> bool:
|
||||||
"""Insert a string into the note at a requested location.
|
"""Insert a string into the note at a requested location.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
new_string (str): String to insert at the top of the note.
|
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.
|
allow_multiple (bool): Whether to allow inserting the string if it already exists in the note.
|
||||||
location (InsertLocation): Location to insert the string.
|
location (InsertLocation): Location to insert the string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether the note was updated.
|
||||||
"""
|
"""
|
||||||
if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0:
|
if not allow_multiple and len(re.findall(re.escape(new_string), self.file_content)) > 0:
|
||||||
return
|
return False
|
||||||
|
|
||||||
match location:
|
match location:
|
||||||
case InsertLocation.BOTTOM:
|
case InsertLocation.BOTTOM:
|
||||||
self.file_content += f"\n{new_string}"
|
self.file_content += f"\n{new_string}"
|
||||||
|
return True
|
||||||
case InsertLocation.TOP:
|
case InsertLocation.TOP:
|
||||||
try:
|
try:
|
||||||
top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter")
|
top = PATTERNS.frontmatter_block.search(self.file_content).group("frontmatter")
|
||||||
@@ -544,10 +574,12 @@ class Note:
|
|||||||
|
|
||||||
if top == "":
|
if top == "":
|
||||||
self.file_content = f"{new_string}\n{self.file_content}"
|
self.file_content = f"{new_string}\n{self.file_content}"
|
||||||
else:
|
return True
|
||||||
|
|
||||||
new_string = f"{top}\n{new_string}"
|
new_string = f"{top}\n{new_string}"
|
||||||
top = re.escape(top)
|
top = re.escape(top)
|
||||||
self.sub(top, new_string, is_regex=True)
|
self.sub(top, new_string, is_regex=True)
|
||||||
|
return True
|
||||||
case InsertLocation.AFTER_TITLE:
|
case InsertLocation.AFTER_TITLE:
|
||||||
try:
|
try:
|
||||||
top = PATTERNS.top_with_header.search(self.file_content).group("top")
|
top = PATTERNS.top_with_header.search(self.file_content).group("top")
|
||||||
@@ -556,10 +588,11 @@ class Note:
|
|||||||
|
|
||||||
if top == "":
|
if top == "":
|
||||||
self.file_content = f"{new_string}\n{self.file_content}"
|
self.file_content = f"{new_string}\n{self.file_content}"
|
||||||
else:
|
return True
|
||||||
|
|
||||||
new_string = f"{top}\n{new_string}"
|
new_string = f"{top}\n{new_string}"
|
||||||
top = re.escape(top)
|
top = re.escape(top)
|
||||||
self.sub(top, new_string, is_regex=True)
|
self.sub(top, new_string, is_regex=True)
|
||||||
case _:
|
return True
|
||||||
|
case _: # pragma: no cover
|
||||||
raise ValueError(f"Invalid location: {location}")
|
raise ValueError(f"Invalid location: {location}")
|
||||||
pass
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class Vault:
|
|||||||
yield "num_notes", len(self.all_notes)
|
yield "num_notes", len(self.all_notes)
|
||||||
yield "num_notes_in_scope", len(self.notes_in_scope)
|
yield "num_notes_in_scope", len(self.notes_in_scope)
|
||||||
yield "exclude_paths", self.exclude_paths
|
yield "exclude_paths", self.exclude_paths
|
||||||
|
yield "insert_location", self.insert_location
|
||||||
|
|
||||||
def _filter_notes(self) -> list[Note]:
|
def _filter_notes(self) -> list[Note]:
|
||||||
"""Filter notes by path and metadata using the filters defined in self.filters.
|
"""Filter notes by path and metadata using the filters defined in self.filters.
|
||||||
@@ -122,7 +123,7 @@ class Vault:
|
|||||||
if self.config["insert_location"].upper() == "TOP":
|
if self.config["insert_location"].upper() == "TOP":
|
||||||
return InsertLocation.TOP
|
return InsertLocation.TOP
|
||||||
|
|
||||||
if self.config["insert_location"].upper() == "HEADER":
|
if self.config["insert_location"].upper() == "AFTER_TITLE":
|
||||||
return InsertLocation.AFTER_TITLE
|
return InsertLocation.AFTER_TITLE
|
||||||
|
|
||||||
if self.config["insert_location"].upper() == "BOTTOM":
|
if self.config["insert_location"].upper() == "BOTTOM":
|
||||||
@@ -374,6 +375,7 @@ class Vault:
|
|||||||
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 filters", str(len(self.filters)))
|
table.add_row("Active filters", str(len(self.filters)))
|
||||||
table.add_row("Notes with changes", str(len(self.get_changed_notes())))
|
table.add_row("Notes with changes", str(len(self.get_changed_notes())))
|
||||||
|
table.add_row("Insert Location", str(self.insert_location.value))
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ def test_delete_value(test_application, mocker, capsys) -> None:
|
|||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
app.application_main()
|
app.application_main()
|
||||||
captured = remove_ansi(capsys.readouterr().out)
|
captured = remove_ansi(capsys.readouterr().out)
|
||||||
assert r"SUCCESS | Deleted value ^front\w+$ from key area in 8 notes" in captured
|
assert r"SUCCESS | Deleted value ^front\w+$ from key area in 4 notes" in captured
|
||||||
|
|
||||||
|
|
||||||
def test_filter_notes(test_application, mocker, capsys) -> None:
|
def test_filter_notes(test_application, mocker, capsys) -> None:
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ def test_no_config_no_vault(tmp_path, mocker) -> None:
|
|||||||
# Folders within the vault to ignore when indexing metadata
|
# Folders within the vault to ignore when indexing metadata
|
||||||
exclude_paths = [".git", ".obsidian"]
|
exclude_paths = [".git", ".obsidian"]
|
||||||
|
|
||||||
# Location to add metadata. One of:
|
# Location to add new metadata. One of:
|
||||||
# TOP: Directly after frontmatter.
|
# TOP: Directly after frontmatter.
|
||||||
# AFTER_TITLE: After a header following frontmatter.
|
# AFTER_TITLE: After the first header following frontmatter.
|
||||||
# BOTTOM: The bottom of the note
|
# BOTTOM: The bottom of the note
|
||||||
insert_location = "BOTTOM\"
|
insert_location = "BOTTOM\"
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -38,8 +38,14 @@ def sample_note(tmp_path) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def short_note(tmp_path) -> Path:
|
def short_notes(tmp_path) -> Path:
|
||||||
"""Fixture which creates a temporary short note file."""
|
"""Fixture which creates two temporary note files.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Tuple[Path, Path]: Tuple of two temporary note files.
|
||||||
|
1. Very short note with frontmatter
|
||||||
|
2. Very short note without any frontmatter
|
||||||
|
"""
|
||||||
source_file1: Path = Path("tests/fixtures/short_textfile.md")
|
source_file1: Path = Path("tests/fixtures/short_textfile.md")
|
||||||
source_file2: Path = Path("tests/fixtures/no_metadata.md")
|
source_file2: Path = Path("tests/fixtures/no_metadata.md")
|
||||||
if not source_file1.exists():
|
if not source_file1.exists():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from obsidian_metadata.models.metadata import (
|
|||||||
InlineTags,
|
InlineTags,
|
||||||
VaultMetadata,
|
VaultMetadata,
|
||||||
)
|
)
|
||||||
from tests.helpers import Regex
|
from tests.helpers import Regex, remove_ansi
|
||||||
|
|
||||||
FILE_CONTENT: str = Path("tests/fixtures/test_vault/test1.md").read_text()
|
FILE_CONTENT: str = Path("tests/fixtures/test_vault/test1.md").read_text()
|
||||||
TAG_LIST: list[str] = ["tag 1", "tag 2", "tag 3"]
|
TAG_LIST: list[str] = ["tag 1", "tag 2", "tag 3"]
|
||||||
@@ -609,39 +609,39 @@ def test_vault_metadata_print(capsys) -> None:
|
|||||||
vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST)
|
vm.index_metadata(area=MetadataType.TAGS, metadata=TAG_LIST)
|
||||||
|
|
||||||
vm.print_metadata(area=MetadataType.ALL)
|
vm.print_metadata(area=MetadataType.ALL)
|
||||||
captured = capsys.readouterr()
|
captured = remove_ansi(capsys.readouterr().out)
|
||||||
assert "All metadata" in captured.out
|
assert "All metadata" in captured
|
||||||
assert "All inline tags" in captured.out
|
assert "All inline tags" in captured
|
||||||
assert "┃ Keys ┃ Values ┃" in captured.out
|
assert "┃ Keys ┃ Values ┃" in captured
|
||||||
assert "│ shared_key1 │ shared_key1_value │" in captured.out
|
assert "│ shared_key1 │ shared_key1_value │" in captured
|
||||||
assert captured.out == Regex("#tag 1 +#tag 2")
|
assert captured == Regex("#tag 1 +#tag 2")
|
||||||
|
|
||||||
vm.print_metadata(area=MetadataType.FRONTMATTER)
|
vm.print_metadata(area=MetadataType.FRONTMATTER)
|
||||||
captured = capsys.readouterr()
|
captured = remove_ansi(capsys.readouterr().out)
|
||||||
assert "All frontmatter" in captured.out
|
assert "All frontmatter" in captured
|
||||||
assert "┃ Keys ┃ Values ┃" in captured.out
|
assert "┃ Keys ┃ Values ┃" in captured
|
||||||
assert "│ shared_key1 │ shared_key1_value │" in captured.out
|
assert "│ shared_key1 │ shared_key1_value │" in captured
|
||||||
assert "value1" not in captured.out
|
assert "value1" not in captured
|
||||||
|
|
||||||
vm.print_metadata(area=MetadataType.INLINE)
|
vm.print_metadata(area=MetadataType.INLINE)
|
||||||
captured = capsys.readouterr()
|
captured = remove_ansi(capsys.readouterr().out)
|
||||||
assert "All inline" in captured.out
|
assert "All inline" in captured
|
||||||
assert "┃ Keys ┃ Values ┃" in captured.out
|
assert "┃ Keys ┃ Values ┃" in captured
|
||||||
assert "shared_key1" not in captured.out
|
assert "shared_key1" not in captured
|
||||||
assert "│ key1 │ value1 │" in captured.out
|
assert "│ key1 │ value1 │" in captured
|
||||||
|
|
||||||
vm.print_metadata(area=MetadataType.TAGS)
|
vm.print_metadata(area=MetadataType.TAGS)
|
||||||
captured = capsys.readouterr()
|
captured = remove_ansi(capsys.readouterr().out)
|
||||||
assert "All inline tags " in captured.out
|
assert "All inline tags " in captured
|
||||||
assert "┃ Keys ┃ Values ┃" not in captured.out
|
assert "┃ Keys ┃ Values ┃" not in captured
|
||||||
assert captured.out == Regex("#tag 1 +#tag 2")
|
assert captured == Regex("#tag 1 +#tag 2")
|
||||||
|
|
||||||
vm.print_metadata(area=MetadataType.KEYS)
|
vm.print_metadata(area=MetadataType.KEYS)
|
||||||
captured = capsys.readouterr()
|
captured = remove_ansi(capsys.readouterr().out)
|
||||||
assert "All Keys " in captured.out
|
assert "All Keys " in captured
|
||||||
assert "┃ Keys ┃ Values ┃" not in captured.out
|
assert "┃ Keys ┃ Values ┃" not in captured
|
||||||
assert captured.out != Regex("#tag 1 +#tag 2")
|
assert captured != Regex("#tag 1 +#tag 2")
|
||||||
assert captured.out == Regex("frontmatter_Key1 +frontmatter_Key2")
|
assert captured == Regex("frontmatter_Key1 +frontmatter_Key2")
|
||||||
|
|
||||||
|
|
||||||
def test_vault_metadata_contains() -> None:
|
def test_vault_metadata_contains() -> None:
|
||||||
|
|||||||
1464
tests/notes_test.py
1464
tests/notes_test.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user