refactor: rename list to search, load to import_file, split copy from move and use keyword flags
This commit is contained in:
parent
3a9f3a2fcd
commit
78528abd3a
3 changed files with 55 additions and 44 deletions
|
|
@ -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']:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue