From af89f9ed9bd409fbfe5bb272fc75408a5fd29238 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 24 Oct 2026 22:26:19 +0300 Subject: [PATCH] feat: add scan --replace to take over tags removed in other apps --- README.md | 2 +- orvanitag/cmd.py | 3 ++- orvanitag/orvani.py | 24 +++++++++++++++--------- tests/test_cmd.py | 7 +++++++ tests/test_frontmatter.py | 10 ++++++++++ 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 023704d..a4b8da9 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ $ otag purge old-project/ ### Embedded Metadata -`scan` (`s`) reads tags that are embedded in files and adds them to their entries, in the given folder (default: current) and its subdirectories. +`scan` (`s`) reads tags that are embedded in files and adds them to their entries, in the given folder (default: current) and its subdirectories. `--replace` makes the found tags replace the entry instead, so tags removed in another app also disappear from otag; files without embedded tags keep their entry. `write` (`w`) writes the entry of a file into the file itself, using the named plugin. It replaces what the plugin stored before, so tags removed with `otag remove` disappear from the file on the next `write`. `-c` (`--clear`) removes the plugin's tags from the file instead, e.g. after all tags were removed; it is never done implicitly, because a file without entry may also be one otag never tagged. diff --git a/orvanitag/cmd.py b/orvanitag/cmd.py index ec6bb4a..da57d89 100644 --- a/orvanitag/cmd.py +++ b/orvanitag/cmd.py @@ -174,7 +174,7 @@ def run_import(orvani, args, console): def run_scan(orvani, args, console): - found, errors = orvani.scan(args.path, args.recursive) + found, errors = orvani.scan(args.path, args.recursive, replace=args.replace) for path, entry in found.items(): console.print(f':label: Found {nice_context(entry)}in [strong]{escape(path)}[/strong].') for error in errors: @@ -238,6 +238,7 @@ def parser() -> ArgumentParser: sub = command('scan', ['s'], 'add tags embedded in files to their entries', run_scan) sub.add_argument('path', nargs='?', default='.') + sub.add_argument('--replace', action='store_true', help='replace the entries of files with tags found in them, dropping tags only otag had') no_recursive(sub) sub = command('write', ['w'], 'write the entry of a file into the file itself', run_write) diff --git a/orvanitag/orvani.py b/orvanitag/orvani.py index 12904eb..81d461a 100644 --- a/orvanitag/orvani.py +++ b/orvanitag/orvani.py @@ -141,11 +141,14 @@ class OrvaniTag: tmp.unlink(missing_ok=True) - def __merge(self, path, entry: Entry): - """Adds an entry to the tags of an existing file and saves its folder""" + def __merge(self, path, entry: Entry, replace: bool=False): + """Adds an entry to the tags of an existing file, or replaces them, and saves its folder""" target = Path(path) folder = self.__load(target.parent) - folder.entries.setdefault(target.name, Entry()).merge(entry) + if replace: + folder.entries[target.name] = entry + else: + folder.entries.setdefault(target.name, Entry()).merge(entry) self.__save(folder) @@ -437,9 +440,10 @@ class OrvaniTag: """The entry of a file in the last built index""" return Entry.from_dict(self.INDEX['items'].get(key(filepath), {})) - def scan(self, path: str, recursive: bool=False): + def scan(self, path: str, recursive: bool=False, replace: bool=False): """ - Adds the tags embedded in the files of a folder to their entries + Adds the tags embedded in the files of a folder to their entries. + With replace they replace the entries of files they were found in. Returns the found entries by path and the messages of failed plugin reads """ @@ -447,7 +451,7 @@ class OrvaniTag: for folder, files in walk(path, recursive): for file in files: try: - entry = self.read_metadata(folder / file) + entry = self.read_metadata(folder / file, replace) except PluginError as error: errors.append(str(error)) continue @@ -492,9 +496,11 @@ class OrvaniTag: yield plugin_name, plugin, [suffix for suffix in configured if suffix in plugin.TYPES] or plugin.TYPES - def read_metadata(self, filename: str) -> Entry: + def read_metadata(self, filename: str, replace: bool=False) -> Entry: """ - Reads the tags embedded in a file with every plugin that handles it and adds them to its entry + Reads the tags embedded in a file with every plugin that handles it and adds them to its entry. + With replace they replace its entry; finding no tags leaves the entry as it is, + since that may also mean the file has no place for them. Returns the found tags; raises PluginError naming the plugins that failed """ @@ -510,7 +516,7 @@ class OrvaniTag: except Exception as error: # a broken file must not stop a scan errors.append(f'{plugin_name} reading {path}: {error}') if found: - self.__merge(path, found) + self.__merge(path, found, replace) if errors: raise PluginError('; '.join(errors)) return found diff --git a/tests/test_cmd.py b/tests/test_cmd.py index b5d462a..bf23c79 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -57,3 +57,10 @@ def test_purge_keeps_config(otag, tmp_path): otag('p') assert not (tmp_path / orvani.CONTEXT_FILE).exists() assert (tmp_path / orvani.CONFIG_FILE).exists() + + +def test_scan_replace(otag, tmp_path): + (tmp_path / 'a.py').write_text('# otag: #one\n') + (tmp_path / orvani.CONTEXT_FILE).write_text('a.py --- #one #gone\n') + otag('s', '--replace') + assert (tmp_path / orvani.CONTEXT_FILE).read_text() == 'a.py --- #one\n' diff --git a/tests/test_frontmatter.py b/tests/test_frontmatter.py index 12d1935..c276d05 100644 --- a/tests/test_frontmatter.py +++ b/tests/test_frontmatter.py @@ -71,3 +71,13 @@ def test_rst_is_not_handled(cwd): with pytest.raises(UnsupportedType): o.write_metadata('doc.rst', 'frontmatter') assert (cwd / 'doc.rst').read_text() == 'Title\n=====\n' + + +def test_scan_replace_drops_tags_the_file_no_longer_has(cwd): + (cwd / 'a.md').write_text('---\ntags: [one]\n---\n') + (cwd / 'b.md').write_text('no front matter\n') + (cwd / orvani.CONTEXT_FILE).write_text('a.md --- #one #gone\nb.md --- #kept\n') + OrvaniTag().scan('.', False) + assert (cwd / orvani.CONTEXT_FILE).read_text() == 'a.md --- #one #gone\nb.md --- #kept\n' + OrvaniTag().scan('.', False, replace=True) + assert (cwd / orvani.CONTEXT_FILE).read_text() == 'a.md --- #one\nb.md --- #kept\n'