fix: repair search, notes, clean, move, import and write, speed up indexing, migrate packaging to pyproject and uv
This commit is contained in:
parent
85d93e3dae
commit
f04554a52c
27 changed files with 1147 additions and 1852 deletions
179
tests/test_orvani.py
Normal file
179
tests/test_orvani.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
import orvanitag.orvani as orvani
|
||||
from orvanitag.orvani import OrvaniTag, DEFAULT_PLUGINS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tree(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
for name in ['a.mp3', 'b.mp3', 'c.mp3']:
|
||||
(tmp_path / name).touch()
|
||||
(tmp_path / 'sub').mkdir()
|
||||
(tmp_path / 'sub' / 'd.txt').touch()
|
||||
return tmp_path
|
||||
|
||||
|
||||
def found(o, *query, either=False, path=''):
|
||||
return o.list(path, True, either, True, False, False, *query)['found']
|
||||
|
||||
|
||||
def items(path='.'):
|
||||
return yaml.safe_load((path / orvani.CONTEXT_FILE).read_text())['items']
|
||||
|
||||
|
||||
def test_and_search_within_one_category(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi', '#snare')
|
||||
o.add('b.mp3', '#lofi')
|
||||
assert found(o, '#lofi', '#snare') == ['./a.mp3']
|
||||
|
||||
|
||||
def test_and_search_across_categories(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi', '@drums')
|
||||
o.add('b.mp3', '#lofi')
|
||||
o.add('c.mp3', '@drums')
|
||||
assert found(o, '#lofi', '@drums') == ['./a.mp3']
|
||||
|
||||
|
||||
def test_or_search(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi')
|
||||
o.add('b.mp3', '#snare')
|
||||
o.add('c.mp3', '#other')
|
||||
assert found(o, '#lofi', '#snare', either=True) == ['./a.mp3', './b.mp3']
|
||||
|
||||
|
||||
def test_unprefixed_term_matches_any_category(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '+lofi')
|
||||
o.add('b.mp3', '#lofi')
|
||||
o.add('c.mp3', '#other')
|
||||
assert found(o, 'lofi') == ['./a.mp3', './b.mp3']
|
||||
|
||||
|
||||
def test_empty_query_lists_everything(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi')
|
||||
o.add('sub/d.txt', '#doc')
|
||||
assert found(o, None) == ['./a.mp3', './sub/d.txt']
|
||||
|
||||
|
||||
def test_multi_word_note(tree):
|
||||
OrvaniTag().add('a.mp3', '#lofi', 'my', 'long', 'note')
|
||||
assert items(tree)['a.mp3']['note'] == 'my long note'
|
||||
|
||||
|
||||
def test_trailing_commas_are_stripped(tree):
|
||||
OrvaniTag().add('a.mp3', '#lofi,', '#snare')
|
||||
assert items(tree)['a.mp3']['tags'] == ['lofi', 'snare']
|
||||
|
||||
|
||||
def test_clean_keeps_tagged_directories(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('sub', '#folder')
|
||||
o.add('a.mp3', '#lofi')
|
||||
(tree / 'a.mp3').unlink()
|
||||
assert o.clean('', True)[2] == 1
|
||||
assert list(items(tree)) == ['sub']
|
||||
|
||||
|
||||
def test_instances_do_not_share_state(tree):
|
||||
first = OrvaniTag()
|
||||
first.register_plugin('extra')
|
||||
assert 'extra' not in OrvaniTag().PLUGINS
|
||||
assert 'extra' not in DEFAULT_PLUGINS
|
||||
|
||||
|
||||
def test_index_is_rebuilt_per_list_call(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi')
|
||||
o.add('sub/d.txt', '#lofi')
|
||||
assert found(o, '#lofi') == ['./a.mp3', './sub/d.txt']
|
||||
assert found(o, '#lofi', path='sub') == ['sub/d.txt']
|
||||
|
||||
|
||||
def test_move_untagged_file(tree):
|
||||
assert OrvaniTag().move('a.mp3', 'sub/a.mp3', False, False)[0] == 2
|
||||
|
||||
|
||||
def test_move_tagged_file(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi')
|
||||
assert o.move('a.mp3', 'sub/', False, True)[0] == 0
|
||||
assert (tree / 'sub' / 'a.mp3').exists()
|
||||
assert items(tree / 'sub')['a.mp3']['tags'] == ['lofi']
|
||||
assert not (tree / orvani.CONTEXT_FILE).exists()
|
||||
|
||||
|
||||
def test_save_keeps_config(tree):
|
||||
(tree / orvani.CONTEXT_FILE).write_text('config:\n plugins: [pdf]\nitems: {}\n')
|
||||
OrvaniTag().add('a.mp3', '#lofi')
|
||||
data = yaml.safe_load((tree / orvani.CONTEXT_FILE).read_text())
|
||||
assert data['config'] == {'plugins': ['pdf']}
|
||||
assert data['items']['a.mp3']['tags'] == ['lofi']
|
||||
|
||||
|
||||
def test_failed_save_keeps_old_file(tree, monkeypatch):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi')
|
||||
before = (tree / orvani.CONTEXT_FILE).read_text()
|
||||
|
||||
def broken_dump(*args, **kwargs):
|
||||
raise RuntimeError('disk full')
|
||||
monkeypatch.setattr(orvani.yaml, 'dump', broken_dump)
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
o.add('a.mp3', '#snare')
|
||||
assert (tree / orvani.CONTEXT_FILE).read_text() == before
|
||||
assert sorted(os.listdir(tree)) == [orvani.CONTEXT_FILE, 'a.mp3', 'b.mp3', 'c.mp3', 'sub']
|
||||
|
||||
|
||||
def test_remove_saves_once(tree, monkeypatch):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi', '#snare', '#keep')
|
||||
saves = []
|
||||
save = o._OrvaniTag__save
|
||||
monkeypatch.setattr(o, '_OrvaniTag__save', lambda: saves.append(1) or save())
|
||||
assert o.remove('a.mp3', '#lofi', '#snare')[0] == 0
|
||||
assert len(saves) == 1
|
||||
assert items(tree)['a.mp3']['tags'] == ['keep']
|
||||
|
||||
|
||||
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 (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']
|
||||
|
||||
|
||||
def test_import_untagged_files(tree):
|
||||
o = OrvaniTag()
|
||||
o.add('a.mp3', '#lofi', 'a note')
|
||||
o.add('sub/d.txt', '@docs')
|
||||
assert o.export('context.txt')[0] == 0
|
||||
(tree / orvani.CONTEXT_FILE).unlink()
|
||||
(tree / 'sub' / orvani.CONTEXT_FILE).unlink()
|
||||
|
||||
assert OrvaniTag().load('context.txt') == [0, 'context.txt', 2]
|
||||
assert items(tree)['a.mp3']['tags'] == ['lofi']
|
||||
assert items(tree)['a.mp3']['note'] == 'a note'
|
||||
assert items(tree / 'sub')['d.txt']['contexts'] == ['docs']
|
||||
|
||||
|
||||
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') == [0, 'context.txt', 1]
|
||||
assert items(tree)['a.mp3']['tags'] == ['lofi', 'snare']
|
||||
assert 'b.mp3' not in items(tree)
|
||||
109
tests/test_plugins.py
Normal file
109
tests/test_plugins.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import sys
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
import orvanitag.orvani as orvani
|
||||
from orvanitag.cmd import main
|
||||
from orvanitag.orvani import OrvaniTag
|
||||
|
||||
DOCX_CORE = (
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" '
|
||||
'xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>T</dc:title><dc:description></dc:description>'
|
||||
'</cp:coreProperties>'
|
||||
)
|
||||
ODT_META = (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" '
|
||||
'xmlns:dc="http://purl.org/dc/elements/1.1/"><office:meta><dc:description>old</dc:description>'
|
||||
'</office:meta></office:document-meta>'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cwd(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def make_docx(path):
|
||||
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
z.writestr('[Content_Types].xml', '<Types/>')
|
||||
z.writestr('docProps/core.xml', DOCX_CORE)
|
||||
|
||||
|
||||
def make_odt(path):
|
||||
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
z.writestr(zipfile.ZipInfo('mimetype'), 'application/vnd.oasis.opendocument.text')
|
||||
z.writestr('content.xml', '<office:document-content/>')
|
||||
z.writestr('meta.xml', ODT_META)
|
||||
|
||||
|
||||
def roundtrip(cwd, name):
|
||||
o = OrvaniTag()
|
||||
o.add(name, '#lofi', '+proj', 'R&B', 'C:\\1')
|
||||
assert o.write_metadata(name, 'office')[0] == 0
|
||||
(cwd / orvani.CONTEXT_FILE).unlink()
|
||||
OrvaniTag().read_metadata(name)
|
||||
entry = yaml.safe_load((cwd / orvani.CONTEXT_FILE).read_text())['items'][name]
|
||||
assert entry['tags'] == ['lofi']
|
||||
assert entry['projects'] == ['proj']
|
||||
assert entry['note'] == 'R&B C:\\1'
|
||||
assert not list(cwd.glob('unzip__*'))
|
||||
|
||||
|
||||
def test_docx_roundtrip(cwd):
|
||||
make_docx(cwd / 'doc.docx')
|
||||
roundtrip(cwd, 'doc.docx')
|
||||
with zipfile.ZipFile(cwd / 'doc.docx') as z:
|
||||
assert z.testzip() is None
|
||||
assert '<dc:title>T</dc:title>' in z.read('docProps/core.xml').decode()
|
||||
|
||||
|
||||
def test_odt_roundtrip_keeps_mimetype_first(cwd):
|
||||
make_odt(cwd / 'doc.odt')
|
||||
roundtrip(cwd, 'doc.odt')
|
||||
with zipfile.ZipFile(cwd / 'doc.odt') as z:
|
||||
first = z.infolist()[0]
|
||||
assert first.filename == 'mimetype'
|
||||
assert first.compress_type == zipfile.ZIP_STORED
|
||||
|
||||
|
||||
def test_pdf_roundtrip(cwd):
|
||||
writer = PdfWriter()
|
||||
writer.add_blank_page(72, 72)
|
||||
writer.add_metadata({'/Title': 'T'})
|
||||
writer.write(cwd / 'doc.pdf')
|
||||
|
||||
o = OrvaniTag()
|
||||
o.add('doc.pdf', '#lofi', '@desk', 'a note')
|
||||
assert o.write_metadata('doc.pdf', 'pdf')[0] == 0
|
||||
meta = PdfReader(cwd / 'doc.pdf').metadata
|
||||
assert meta['/Title'] == 'T'
|
||||
|
||||
(cwd / orvani.CONTEXT_FILE).unlink()
|
||||
OrvaniTag().read_metadata('doc.pdf')
|
||||
entry = yaml.safe_load((cwd / orvani.CONTEXT_FILE).read_text())['items']['doc.pdf']
|
||||
assert entry == {'projects': [], 'contexts': ['desk'], 'tags': ['lofi'], 'note': 'a note'}
|
||||
|
||||
|
||||
def test_cmd_write(cwd, monkeypatch):
|
||||
make_docx(cwd / 'doc.docx')
|
||||
OrvaniTag().add('doc.docx', '#lofi')
|
||||
monkeypatch.setattr(sys, 'argv', ['otag', 'w', 'office', 'doc.docx'])
|
||||
main()
|
||||
with zipfile.ZipFile(cwd / 'doc.docx') as z:
|
||||
assert '<dc:description>#lofi</dc:description>' in z.read('docProps/core.xml').decode()
|
||||
|
||||
|
||||
def test_cmd_prints_errors(cwd, monkeypatch, capsys):
|
||||
(cwd / 'a.mp3').touch()
|
||||
(cwd / orvani.CONTEXT_FILE).write_text('items: [unclosed')
|
||||
monkeypatch.setattr(sys, 'argv', ['otag', 'a', 'a.mp3', '#lofi'])
|
||||
with pytest.raises(SystemExit) as exit:
|
||||
main()
|
||||
assert exit.value.code == 1
|
||||
assert 'expected' in capsys.readouterr().out
|
||||
Loading…
Add table
Add a link
Reference in a new issue