Skip to content

Commit f19f334

Browse files
committed
Further refactoring:
- Move `TagVar` and `TagVars` to `picard/tags/tagvar.py` - Move `picard/util/preservedtags.py` to `picard/tags/preservedtags.py` - Move common tags functions to `picard/tags/__init__.py` - Rename `test_util_tags.py` to `test_tags.py` - Update `import` statments in affected files including tests - Update affected `mock` statements in tests - Move and combine `test_display_tag_name()` test from `test_utile.py` into `test_tags.py`
1 parent 7c54e26 commit f19f334

19 files changed

+392
-374
lines changed

picard/const/tags.py

Lines changed: 5 additions & 319 deletions
Original file line numberDiff line numberDiff line change
@@ -19,329 +19,15 @@
1919
# along with this program; if not, write to the Free Software
2020
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
2121

22-
from collections import (
23-
OrderedDict,
24-
namedtuple,
25-
)
26-
from collections.abc import MutableSequence
27-
from enum import IntEnum
28-
import html
29-
30-
31-
try:
32-
from markdown import markdown
33-
except ImportError:
34-
markdown = None
35-
3622
from picard.const import PICARD_URLS
37-
from picard.i18n import (
38-
N_,
39-
gettext as _,
40-
)
41-
from picard.options import get_option_title
42-
43-
44-
DocumentLink = namedtuple('DocumentLink', ('title', 'link'))
45-
46-
47-
def _markdown(text: str):
48-
text = html.escape(text)
49-
if markdown is None:
50-
return '<p>' + text.replace('\n', '<br />') + '</p>'
51-
return markdown(text)
52-
53-
54-
class Section(IntEnum):
55-
notes = 1
56-
options = 2
57-
links = 3
58-
see_also = 4
59-
60-
61-
SectionInfo = namedtuple('Section', ('title', 'tagvar_func'))
62-
SECTIONS = {
63-
Section.notes: SectionInfo(N_('Notes'), 'notes'),
64-
Section.options: SectionInfo(N_('Option Settings'), 'related_options_titles'),
65-
Section.links: SectionInfo(N_('Links'), 'links'),
66-
Section.see_also: SectionInfo(N_('See Also'), 'see_alsos'),
67-
}
68-
69-
TEXT_NO_DESCRIPTION = N_('No description available.')
70-
71-
ATTRIB2NOTE = OrderedDict(
72-
is_multi_value=N_('multi-value variable'),
73-
is_preserved=N_('preserved read-only'),
74-
not_script_variable=N_('not for use in scripts'),
75-
is_calculated=N_('calculated'),
76-
is_file_info=N_('info from audio file'),
77-
not_from_mb=N_('not provided from MusicBrainz data'),
78-
not_populated_by_picard=N_('not populated by stock Picard'),
23+
from picard.i18n import N_
24+
from picard.tags.tagvar import (
25+
DocumentLink,
26+
TagVar,
27+
TagVars,
7928
)
8029

8130

82-
class TagVar:
83-
def __init__(
84-
self, name, shortdesc=None, longdesc=None, additionaldesc=None,
85-
is_preserved=False, is_hidden=False, is_script_variable=True, is_tag=True, is_calculated=False,
86-
is_file_info=False, is_from_mb=True, is_populated_by_picard=True, is_multi_value=False,
87-
see_also=None, related_options=None, doc_links=None
88-
):
89-
"""
90-
shortdesc: Short description (typically one or two words) in title case that is suitable
91-
for a column header.
92-
longdesc: Brief description in sentence case describing the tag/variable. This should
93-
be similar (within reasonable length constraints) to the description in the Picard User
94-
Guide documentation, and will be used as a tooltip when reviewing a script. May
95-
contain markdown.
96-
additionaldesc: Additional description which might include more details or examples. May
97-
contain markdown.
98-
is_preserved: the tag is preserved (boolean, default: False)
99-
is_hidden: the tag is "hidden", name will be prefixed with "~" (boolean, default: False)
100-
is_script_variable: the tag can be used as script variable (boolean, default: True)
101-
is_tag: the tag is an actual tag (not a calculated or derived one) (boolean, default: True)
102-
is_calculated: the tag is obtained by external calculation (boolean, default: False)
103-
is_file_info: the tag is a file information, displayed in file info box (boolean, default: False)
104-
is_from_mb: the tag information is provided from the MusicBrainz database (boolean, default: True)
105-
is_populated_by_picard: the tag information is populated by stock Picard (boolean, default: True)
106-
is_multi_value: the tag is a multi-value variable (boolean, default: False)
107-
see_also: an iterable containing ids of related tags
108-
related_options: an iterable containing the related option settings (see picard/options.py)
109-
doc_links: an iterable containing links to external documentation (DocumentLink tuples)
110-
"""
111-
self._name = name
112-
self._shortdesc = shortdesc
113-
self._longdesc = longdesc
114-
self._additionaldesc = additionaldesc
115-
self.is_preserved = is_preserved
116-
self.is_hidden = is_hidden
117-
self.is_script_variable = is_script_variable
118-
self.is_tag = is_tag
119-
self.is_calculated = is_calculated
120-
self.is_file_info = is_file_info
121-
self.is_from_mb = is_from_mb
122-
self.is_populated_by_picard = is_populated_by_picard
123-
self.is_multi_value = is_multi_value
124-
self.see_also = see_also
125-
self.related_options = related_options
126-
self.doc_links = doc_links
127-
128-
@property
129-
def shortdesc(self):
130-
"""default to name"""
131-
if self._shortdesc:
132-
return self._shortdesc.strip()
133-
return str(self)
134-
135-
@property
136-
def longdesc(self):
137-
"""default to shortdesc"""
138-
if self._longdesc:
139-
return self._longdesc.strip()
140-
return self.shortdesc
141-
142-
@property
143-
def additionaldesc(self):
144-
if not self._additionaldesc:
145-
return ''
146-
return self._additionaldesc.strip()
147-
148-
@property
149-
def not_from_mb(self):
150-
return not self.is_from_mb
151-
152-
@property
153-
def not_script_variable(self):
154-
return not self.is_script_variable
155-
156-
@property
157-
def not_populated_by_picard(self):
158-
return not self.is_populated_by_picard
159-
160-
def __str__(self):
161-
"""hidden marked with a prefix"""
162-
if self.is_hidden:
163-
return '~' + self._name
164-
else:
165-
return self._name
166-
167-
def script_name(self):
168-
"""In scripts, ~ prefix is replaced with _ for hidden variables"""
169-
if self.is_hidden:
170-
return '_' + self._name
171-
else:
172-
return self._name
173-
174-
175-
class TagVars(MutableSequence):
176-
"""Mutable sequence for TagVar items
177-
It maintains an internal dict object for display names.
178-
Also it doesn't allow to add a TagVar of the same name more than once.
179-
"""
180-
def __init__(self, *tagvars):
181-
self._items = []
182-
self._name2item = dict()
183-
self.extend(tagvars)
184-
185-
def __len__(self):
186-
return len(self._items)
187-
188-
def __getitem__(self, index):
189-
return self._items[index]
190-
191-
def _get_name(self, tagvar):
192-
if not isinstance(tagvar, TagVar):
193-
raise TypeError(f"Value isn't a TagVar instance: {tagvar}")
194-
name = str(tagvar)
195-
if name in self._name2item:
196-
raise ValueError(f"Already an item with same name: {name}")
197-
return name
198-
199-
def __setitem__(self, index, tagvar):
200-
name = self._get_name(tagvar)
201-
self._name2item[name] = self._items[index] = tagvar
202-
203-
def __delitem__(self, index):
204-
name = str(self._items[index])
205-
del self._items[index]
206-
del self._name2item[name]
207-
208-
def insert(self, index, tagvar):
209-
name = self._get_name(tagvar)
210-
self._items.insert(index, tagvar)
211-
self._name2item[name] = self._items[index]
212-
213-
def __repr__(self):
214-
return f"TagVars({self._items!r})"
215-
216-
def item_from_name(self, name):
217-
if ':' in name:
218-
name, tagdesc = name.split(':', 1)
219-
else:
220-
tagdesc = None
221-
222-
if name and name.startswith('_'):
223-
search_name = name.replace('_', '~', 1)
224-
elif name and name.startswith('~'):
225-
search_name = name
226-
name = name.replace('~', '_')
227-
else:
228-
search_name = name
229-
230-
item: TagVar = self._name2item.get(search_name, None)
231-
232-
return name, tagdesc, search_name, item
233-
234-
def script_name_from_name(self, name):
235-
tagname, tagdesc, search_name, item = self.item_from_name(name)
236-
if item:
237-
return str(item)
238-
return None
239-
240-
def display_name(self, name):
241-
name, tagdesc, search_name, item = self.item_from_name(name)
242-
243-
if item and item.shortdesc:
244-
title = _(item.shortdesc)
245-
else:
246-
title = search_name
247-
if tagdesc:
248-
return '%s [%s]' % (title, tagdesc)
249-
else:
250-
return title
251-
252-
@staticmethod
253-
def _format_display(name, content, tagdesc):
254-
fmt_tagdesc = _("<p><em>%{name}%</em> [{tagdesc}]</p>{content}")
255-
fmt_normal = _("<p><em>%{name}%</em></p>{content}")
256-
fmt = fmt_tagdesc if tagdesc else fmt_normal
257-
return fmt.format(name=name, content=content, tagdesc=tagdesc)
258-
259-
def notes(self, item: TagVar):
260-
for attrib, note in ATTRIB2NOTE.items():
261-
if getattr(item, attrib):
262-
yield html.escape(_(note))
263-
264-
def related_options_titles(self, item: TagVar):
265-
if not item.related_options:
266-
return
267-
for setting in item.related_options:
268-
title = get_option_title(setting)
269-
if title:
270-
yield html.escape(_(title))
271-
272-
def links(self, item: TagVar):
273-
if not item.doc_links:
274-
return
275-
for doclink in item.doc_links:
276-
translated_title = html.escape(_(doclink.title))
277-
yield f"<a href='{doclink.link}'>{translated_title}</a>"
278-
279-
def see_alsos(self, item: TagVar):
280-
if not item.see_also:
281-
return
282-
for tag in item.see_also:
283-
if self.script_name_from_name(tag):
284-
yield f'<a href="#{tag}">%{tag}%</a>'
285-
286-
def _base_description(self, item: TagVar):
287-
return _markdown(_(item.longdesc) if item.longdesc else _(TEXT_NO_DESCRIPTION))
288-
289-
def _add_sections(self, item, include_sections):
290-
# Note: format has to be translatable, for languages not using left-to-right for example
291-
fmt = _("<p><strong>{title}:</strong> {values}.</p>")
292-
return ''.join(self._gen_sections(fmt, item, include_sections))
293-
294-
def _gen_sections(self, fmt, item, include_sections):
295-
for section_id in include_sections:
296-
section = SECTIONS[section_id]
297-
func_for_values = getattr(self, section.tagvar_func)
298-
values = tuple(func_for_values(item))
299-
if not values:
300-
continue
301-
yield fmt.format(
302-
title=_(section.title),
303-
values='; '.join(values),
304-
)
305-
306-
def tooltip_content(self, item: TagVar):
307-
content = self._base_description(item)
308-
content += self._add_sections(item, (Section.notes, ))
309-
return content
310-
311-
def full_description_content(self, item: TagVar):
312-
content = self._base_description(item)
313-
314-
# Append additional description
315-
if item.additionaldesc:
316-
content += _markdown(_(item.additionaldesc))
317-
318-
# Append additional sections as required
319-
include_sections = (
320-
Section.notes,
321-
Section.options,
322-
Section.links,
323-
Section.see_also,
324-
)
325-
content += self._add_sections(item, include_sections)
326-
327-
return content
328-
329-
def display_tooltip(self, tagname):
330-
name, tagdesc, _search_name, item = self.item_from_name(tagname)
331-
content = self.tooltip_content(item) if item else _markdown(_(TEXT_NO_DESCRIPTION))
332-
return self._format_display(name, content, tagdesc)
333-
334-
def display_full_description(self, tagname):
335-
name, tagdesc, _search_name, item = self.item_from_name(tagname)
336-
content = self.full_description_content(item) if item else _markdown(_(TEXT_NO_DESCRIPTION))
337-
return self._format_display(name, content, tagdesc)
338-
339-
def names(self, selector=None):
340-
for item in self._items:
341-
if selector is None or selector(item):
342-
yield str(item)
343-
344-
34531
ALL_TAGS = TagVars(
34632
TagVar(
34733
'absolutetracknumber',

picard/file.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@
8181
)
8282
from picard.plugin import PluginFunctions
8383
from picard.script import get_file_naming_script
84+
from picard.tags import (
85+
calculated_tag_names,
86+
file_info_tag_names,
87+
preserved_tag_names,
88+
)
89+
from picard.tags.preservedtags import PreservedTags
8490
from picard.util import (
8591
any_exception_isinstance,
8692
bytes2human,
@@ -100,13 +106,7 @@
100106
make_short_filename,
101107
move_ensure_casing,
102108
)
103-
from picard.util.preservedtags import PreservedTags
104109
from picard.util.scripttofilename import script_to_filename_with_metadata
105-
from picard.util.tags import (
106-
calculated_tag_names,
107-
file_info_tag_names,
108-
preserved_tag_names,
109-
)
110110

111111

112112
FILE_COMPARISON_WEIGHTS = {

picard/formats/id3.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@
6060
delall_ci,
6161
)
6262
from picard.metadata import Metadata
63+
from picard.tags import (
64+
parse_comment_tag,
65+
parse_subtag,
66+
)
6367
from picard.util import (
6468
encode_filename,
6569
sanitize_date,
6670
)
67-
from picard.util.tags import (
68-
parse_comment_tag,
69-
parse_subtag,
70-
)
7171

7272

7373
try:

picard/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@
5353
)
5454
from picard.plugin import PluginFunctions
5555
from picard.similarity import similarity2
56+
from picard.tags import preserved_tag_names
5657
from picard.util import (
5758
ReadWriteLockContext,
5859
extract_year_from_date,
5960
linear_combination_of_weights,
6061
)
6162
from picard.util.imagelist import ImageList
62-
from picard.util.tags import preserved_tag_names
6363

6464

6565
MULTI_VALUED_JOINER = '; '

0 commit comments

Comments
 (0)