From 78528abd3aa7f0edd825913c11f16ce5f31eba2d Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 24 Oct 2026 21:57:01 +0300 Subject: [PATCH] refactor: rename list to search, load to import_file, split copy from move and use keyword flags --- orvanitag/cmd.py | 9 +++---- orvanitag/orvani.py | 56 ++++++++++++++++++++++++++------------------ tests/test_orvani.py | 34 +++++++++++++-------------- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/orvanitag/cmd.py b/orvanitag/cmd.py index cf976fd..62f786d 100644 --- a/orvanitag/cmd.py +++ b/orvanitag/cmd.py @@ -173,7 +173,7 @@ def main(): elif args.command in ['list', 'ls', 'l']: query = [args.file] + args.context if query: - r = orvani.list(args.path, args.exact, args.either, args.recursive, args.cache, args.delete_cache, *query) + r = orvani.search(*query, path=args.path, exact=args.exact, either=args.either, recursive=args.recursive, cache=args.cache, delete_cache=args.delete_cache) filter = None if args.files: filter = 'files' @@ -184,12 +184,13 @@ def main(): elif args.command in ['copy', 'cp', 'move', 'mv']: if args.file and args.context: copy = args.command in ['copy', 'cp'] - context = orvani.move(args.file, args.context[0], copy, args.file_operation) + transfer = orvani.copy if copy else orvani.move + context = transfer(args.file, args.context[0], with_file=args.file_operation) console.print(f':dancers: File [strong]{args.file}[/strong] with entry {nice_context(context)}successfully {"copied" if copy else "moved"} to [strong]{args.context[0]}[/strong]') elif args.command in ['del', 'rm']: if args.file: - context_file = orvani.delete(args.file, args.file_operation) + context_file = orvani.delete(args.file, with_file=args.file_operation) status = f':wastebasket: Entry for [strong]{args.file}[/strong] successfully removed.' if args.file_operation: status += f'\n:wastebasket: File [strong]{args.file}[/strong] successfully deleted.' @@ -234,7 +235,7 @@ def main(): console.print(f':floppy_disk: All entries successfully written to file [strong]{filename}[/strong].') elif args.command in ['import', 'i']: - count = orvani.load(args.file) + count = orvani.import_file(args.file) console.print(f':inbox_tray: [strong]{count}[/strong] entries successfully imported from file [strong]{args.file or "context.txt"}[/strong].') elif args.command in ['scan', 's']: diff --git a/orvanitag/orvani.py b/orvanitag/orvani.py index 9761a7b..abfa896 100644 --- a/orvanitag/orvani.py +++ b/orvanitag/orvani.py @@ -328,12 +328,12 @@ class OrvaniTag: return context - def delete(self, file: str, fop:bool=False): + def delete(self, file: str, with_file: bool=False): """ Deletes all entries of an existing file and optionally deletes the file file (str): path to existing file - fop (bool): if True removes the referenced file + with_file (bool): if True removes the referenced file Returns the folder context file if it was deleted because it became empty """ @@ -345,26 +345,38 @@ class OrvaniTag: raise NoEntry(f"No entry for {file} found.") del folder.entries[target.name] # Delete it self.__save(folder) # deletes the context file once it is empty - if fop: # delete the referenced file too? + if with_file: # delete the referenced file too? target.unlink(missing_ok=True) return None if folder.file.exists() else folder.file - def move(self, source: str, dest: str, retain: bool=False, fop: bool=True): + def move(self, source: str, dest: str, with_file: bool=False): """ - Copies or moves existing file with contexts to a new destination + Moves the tags of a file to a new destination, with with_file also the file itself. + Returns the moved entry. + """ + return self.__transfer(source, dest, False, with_file) + + def copy(self, source: str, dest: str, with_file: bool=False): + """ + Copies the tags of a file to a new destination, with with_file also the file itself. + Returns the copied entry. + """ + return self.__transfer(source, dest, True, with_file) + + + def __transfer(self, source: str, dest: str, keep_source: bool, with_file: bool): + """ source (str): existing source file dest (str): the new file destination - retain (bool): if True then copies, if False then moves - fop (bool): if True also does the file operation, if False only moves context data - - Returns the moved context + keep_source (bool): if True then copies, if False then moves + with_file (bool): if True also does the file operation, if False only moves context data """ src = Path(source) target = Path(dest) # An existing folder as destination of a file operation receives the file under its own name - if fop and target.is_dir(): + if with_file and target.is_dir(): target = target / src.name # Check if source file and destination folder exist @@ -376,17 +388,17 @@ class OrvaniTag: raise NotFound(f"Destination {dest} doesn't exist.") sourcedata = self.__load(src.parent).entries.get(src.name) # load its context data - if sourcedata is None and not fop: # without an entry there is nothing to move + if sourcedata is None and not with_file: # without an entry there is nothing to move raise NoEntry(f"No entry for {source} found.") if sourcedata is not None: destination = self.__load(target.parent) # load local context of destination folder destination.entries[target.name] = sourcedata # with the context data of our source file self.__save(destination) # and save it - if not retain: # do we copy or move the data? + if not keep_source: # do we copy or move the data? self.delete(source) # delete for move - if fop: # do we also physically move or copy the reference file? + if with_file: # do we also physically move or copy the reference file? shutil.copy2(src, target) - if not retain: + if not keep_source: src.unlink() return sourcedata if sourcedata is not None else Entry() @@ -420,21 +432,19 @@ class OrvaniTag: return count - def list(self, path: str='./', exact=True, either=False, recursive: bool=True, cache: bool=False, delete_cache: bool=False, *search: str): + def search(self, *terms: str, path: str='.', exact: bool=False, either: bool=False, recursive: bool=True, cache: bool=False, delete_cache: bool=False): """ Returns an index object with query results and optionally saves a cached index + terms (str): the search query; no terms find every tagged file path (str): the root context for the search and index - filter (str): limit the search to a context exact (bool): exact search match if True, otherwise partial match + either (bool): match any term instead of all recursive (bool): if True also searches all subfolder contexts cache (bool): if True loads index from cache file if exists, writes it if it doesn't exist - search (str): the search query - """ self.__makeindex(path, recursive, cache, delete_cache) # index the directories - result = self.__search(search, exact, either) # apply the search query - return result + return self.__search(terms, exact, either) # apply the search query def index(self, path): self.__makeindex(path) # index the directories @@ -608,7 +618,7 @@ class OrvaniTag: file = Path(filename) # Run `otag ls` to create the global context index - index = self.list(file.parent, False, False, True, False, False, '') + index = self.search(path=file.parent) # Write the index as lines to file items = index['index']['items'] @@ -622,7 +632,7 @@ class OrvaniTag: raise NoEntry("No entries found.") - def load(self, filename:str): + def import_file(self, filename: str): """ Imports lines from a text file created by `otag export` to the global context index, filtering out the already known entries @@ -640,7 +650,7 @@ class OrvaniTag: raise NotFound(f"File {filename} doesn't exist.") # Make an index of all context items that are already known - known = self.list(file.parent, False, False, True, False, False, '')['index']['items'] + known = self.search(path=file.parent)['index']['items'] count = 0 for import_line in file.read_text().split('\n'): if len(import_line) > 5: diff --git a/tests/test_orvani.py b/tests/test_orvani.py index 3b89843..de1123f 100644 --- a/tests/test_orvani.py +++ b/tests/test_orvani.py @@ -17,7 +17,7 @@ def tree(tmp_path, monkeypatch): def found(o, *query, either=False, path=''): - return o.list(path, True, either, True, False, False, *query)['found'] + return o.search(*query, path=path, exact=True, either=either)['found'] def lines(path): @@ -98,13 +98,13 @@ def test_index_is_rebuilt_per_list_call(tree): def test_move_untagged_file(tree): with pytest.raises(NoEntry): - OrvaniTag().move('a.mp3', 'sub/a.mp3', False, False) + OrvaniTag().move('a.mp3', 'sub/a.mp3') def test_move_tagged_file(tree): o = OrvaniTag() o.add('a.mp3', '#lofi') - o.move('a.mp3', 'sub/', False, True) + o.move('a.mp3', 'sub/', with_file=True) assert (tree / 'sub' / 'a.mp3').exists() assert lines(tree / 'sub') == ['a.mp3 --- #lofi'] assert not (tree / orvani.CONTEXT_FILE).exists() @@ -113,14 +113,14 @@ def test_move_tagged_file(tree): def test_copy_tags_within_current_directory(tree): o = OrvaniTag() o.add('a.mp3', '#lofi') - o.move('a.mp3', 'b.mp3', True, False) + o.copy('a.mp3', 'b.mp3') assert lines(tree) == ['a.mp3 --- #lofi', 'b.mp3 --- #lofi'] def test_rename_within_current_directory(tree): o = OrvaniTag() o.add('a.mp3', '#lofi') - o.move('a.mp3', 'renamed.mp3', False, True) + o.move('a.mp3', 'renamed.mp3', with_file=True) assert not (tree / 'a.mp3').exists() assert lines(tree) == ['renamed.mp3 --- #lofi'] @@ -128,7 +128,7 @@ def test_rename_within_current_directory(tree): def test_move_into_directory_without_slash(tree): o = OrvaniTag() o.add('a.mp3', '#lofi') - o.move('a.mp3', 'sub', False, True) + o.move('a.mp3', 'sub', with_file=True) assert (tree / 'sub' / 'a.mp3').exists() assert lines(tree / 'sub') == ['a.mp3 --- #lofi'] assert not (tree / orvani.CONTEXT_FILE).exists() @@ -138,7 +138,7 @@ def test_copy_tags_from_subfolder_to_folder(tree): (tree / 'other').mkdir() o = OrvaniTag() o.add('sub/d.txt', '#doc') - o.move('sub/d.txt', 'other/', True, False) + o.copy('sub/d.txt', 'other/') assert lines(tree) == ['other/ --- #doc'] assert lines(tree / 'sub') == ['d.txt --- #doc'] @@ -216,13 +216,13 @@ def test_remove_saves_once(tree, monkeypatch): def test_cache_is_rebuilt_when_context_changes(tree): o = OrvaniTag() o.add('a.mp3', '#lofi') - assert o.list('', True, False, True, True, False, '#lofi')['found'] == ['a.mp3'] + assert o.search('#lofi', exact=True, cache=True)['found'] == ['a.mp3'] assert (tree / orvani.CACHE_FILE).exists() o.add('b.mp3', '#lofi') stat = (tree / orvani.CONTEXT_FILE).stat() os.utime(tree / orvani.CONTEXT_FILE, ns=(stat.st_atime_ns, stat.st_mtime_ns + 10**9)) - assert OrvaniTag().list('', True, False, True, True, False, '#lofi')['found'] == ['a.mp3', 'b.mp3'] + assert OrvaniTag().search('#lofi', exact=True, cache=True)['found'] == ['a.mp3', 'b.mp3'] def test_import_untagged_files(tree): @@ -234,7 +234,7 @@ def test_import_untagged_files(tree): (tree / orvani.CONTEXT_FILE).unlink() (tree / 'sub' / orvani.CONTEXT_FILE).unlink() - assert OrvaniTag().load('context.txt') == 2 + assert OrvaniTag().import_file('context.txt') == 2 assert lines(tree) == ['a.mp3 --- #lofi a note'] assert lines(tree / 'sub') == ['d.txt --- @docs'] @@ -243,13 +243,13 @@ def test_import_skips_known_tags_and_empty_lines(tree): o = OrvaniTag() o.add('a.mp3', '#lofi') (tree / 'context.txt').write_text('./a.mp3 --- #lofi #snare\n./b.mp3 --- \n') - assert OrvaniTag().load('context.txt') == 1 + assert OrvaniTag().import_file('context.txt') == 1 assert lines(tree) == ['a.mp3 --- #lofi #snare'] def test_import_keeps_hyphenated_tags(tree): (tree / 'context.txt').write_text('./a.mp3 --- @home-office #lo-fi rock+roll take-2\n') - OrvaniTag().load('context.txt') + OrvaniTag().import_file('context.txt') assert lines(tree) == ['a.mp3 --- @home-office #lo-fi rock+roll take-2'] @@ -261,11 +261,11 @@ def test_duplicate_and_bare_prefixes(tree): def test_move_reports_what_is_missing(tree): o = OrvaniTag() with pytest.raises(NotFound, match=r"^Source file x\.mp3 doesn't exist\.$"): - o.move('x.mp3', 'sub/', False, True) + o.move('x.mp3', 'sub/', with_file=True) with pytest.raises(NotFound, match=r"^Destination nope/a\.mp3 doesn't exist\.$"): - o.move('a.mp3', 'nope/a.mp3', False, True) + o.move('a.mp3', 'nope/a.mp3', with_file=True) with pytest.raises(NotFound, match=r"^Source file x\.mp3 and destination nope/x don't exist\.$"): - o.move('x.mp3', 'nope/x', False, True) + o.move('x.mp3', 'nope/x', with_file=True) def test_errors_of_remove_delete_export_and_import(tree): @@ -278,10 +278,10 @@ def test_errors_of_remove_delete_export_and_import(tree): with pytest.raises(NotFound): o.add('missing.mp3', '#x') with pytest.raises(NotFound): - o.load('missing.txt') + o.import_file('missing.txt') (tree / 'bad.txt').write_text('no separator on this line\n') with pytest.raises(FormatError): - o.load('bad.txt') + o.import_file('bad.txt') o.delete('a.mp3') with pytest.raises(NoEntry): o.export('context.txt')