Skip to content

Commit 4fef1b9

Browse files
authored
Merge pull request #2833 from zas/revisit_plugin_oo
Revisit plugin/registry, refactor for more object-oriented approach
2 parents 223ce27 + 03f674b commit 4fef1b9

23 files changed

+861
-403
lines changed

picard/git/ops.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
GitStatusFlag,
3131
)
3232
from picard.git.factory import git_backend
33+
from picard.git.ref_utils import get_ref_type
3334

3435

3536
def clean_python_cache(directory):
@@ -118,12 +119,12 @@ def validate_ref(url, ref, uuid=None, registry=None):
118119
# For registry plugins with versioning_scheme, validate against version tags
119120
if registry and uuid:
120121
registry_plugin = registry.find_plugin(uuid=uuid)
121-
if registry_plugin and registry_plugin.get('versioning_scheme'):
122+
if registry_plugin and registry_plugin.versioning_scheme:
122123
# Import here to avoid circular dependency
123124
from picard.plugin3.refs_cache import RefsCache
124125

125126
refs_cache = RefsCache(registry)
126-
pattern = refs_cache.parse_versioning_scheme(registry_plugin['versioning_scheme'])
127+
pattern = refs_cache.parse_versioning_scheme(registry_plugin.versioning_scheme)
127128
if pattern and pattern.match(ref):
128129
# It's a version tag - fetch and check
129130
version_tags = []
@@ -136,8 +137,8 @@ def validate_ref(url, ref, uuid=None, registry=None):
136137
return True
137138

138139
# For registry plugins with explicit refs list
139-
if registry_plugin and registry_plugin.get('refs'):
140-
ref_names = [r['name'] for r in registry_plugin['refs']]
140+
if registry_plugin and registry_plugin.refs:
141+
ref_names = [r['name'] for r in registry_plugin.refs]
141142
if ref not in ref_names:
142143
raise PluginRefNotFoundError(uuid, ref)
143144
return True
@@ -178,29 +179,17 @@ def check_ref_type(repo_path, ref=None):
178179
try:
179180
backend = git_backend()
180181
repo = backend.create_repository(repo_path)
181-
references = repo.get_references()
182182

183183
if ref:
184-
# Check if ref exists in standard locations first
185-
if f'refs/tags/{ref}' in references:
184+
# Use robust reference type detection
185+
ref_type, resolved_ref = get_ref_type(repo, ref)
186+
if ref_type == 'tag':
186187
return 'tag', ref
187-
if f'refs/heads/{ref}' in references:
188-
return 'branch', ref
189-
if f'refs/remotes/origin/{ref}' in references:
188+
elif ref_type in ('local_branch', 'remote_branch'):
190189
return 'branch', ref
191-
192-
# Not found in standard refs, try to resolve it
193-
try:
194-
obj = repo.revparse_single(ref)
195-
from picard.git.backend import GitObjectType
196-
197-
if obj.type == GitObjectType.COMMIT:
198-
return 'commit', ref
199-
elif obj.type == GitObjectType.TAG:
200-
return 'tag', ref
201-
else:
202-
return None, ref
203-
except KeyError:
190+
elif ref_type == 'commit':
191+
return 'commit', ref
192+
else:
204193
return None, ref
205194
else:
206195
# Check current HEAD state

picard/git/ref_utils.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Picard, the next-generation MusicBrainz tagger
4+
#
5+
# Copyright (C) 2025 Laurent Monin
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License
9+
# as published by the Free Software Foundation; either version 2
10+
# of the License, or (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with this program; if not, write to the Free Software
19+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20+
21+
"""Git reference utilities for robust reference type detection."""
22+
23+
24+
def get_ref_type(repo, ref):
25+
"""Determine the type of a git reference.
26+
27+
Args:
28+
repo: Git repository object
29+
ref: Reference name to check
30+
31+
Returns:
32+
tuple: (ref_type, resolved_ref) where ref_type is one of:
33+
'tag', 'local_branch', 'remote_branch', 'commit', 'unknown'
34+
"""
35+
if not ref:
36+
return 'unknown', ref
37+
38+
try:
39+
# Get all references from the repository
40+
references = repo.get_references()
41+
42+
# Check exact matches first
43+
if f'refs/tags/{ref}' in references:
44+
return 'tag', f'refs/tags/{ref}'
45+
if f'refs/heads/{ref}' in references:
46+
return 'local_branch', f'refs/heads/{ref}'
47+
if f'refs/remotes/{ref}' in references:
48+
return 'remote_branch', f'refs/remotes/{ref}'
49+
if f'refs/remotes/origin/{ref}' in references:
50+
return 'remote_branch', f'refs/remotes/origin/{ref}'
51+
52+
# Check if ref is already a full reference
53+
if ref in references:
54+
if ref.startswith('refs/tags/'):
55+
return 'tag', ref
56+
elif ref.startswith('refs/heads/'):
57+
return 'local_branch', ref
58+
elif ref.startswith('refs/remotes/'):
59+
return 'remote_branch', ref
60+
61+
# Try to resolve as commit hash
62+
try:
63+
repo.revparse_single(ref)
64+
return 'commit', ref
65+
except KeyError:
66+
pass
67+
68+
except Exception:
69+
# If we can't get references, fall back to string analysis
70+
if ref.startswith('refs/tags/'):
71+
return 'tag', ref
72+
elif ref.startswith('refs/heads/'):
73+
return 'local_branch', ref
74+
elif ref.startswith('refs/remotes/') or ref.startswith('origin/'):
75+
return 'remote_branch', ref
76+
77+
return 'unknown', ref

picard/plugin3/api.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ def _install_qt_translator(self) -> None:
273273
self._qt_translator._current_locale = self.get_locale()
274274
self._tagger._qt_translators.add_translator(self._qt_translator)
275275

276+
# Only emit signal if application is already running (not during startup)
277+
# This ensures UI retranslation when plugins are installed dynamically
278+
if hasattr(self._tagger, 'window') and self._tagger.window:
279+
self._tagger._qt_translators_updated.emit()
280+
276281
def _remove_qt_translator(self) -> None:
277282
"""Remove Qt translator for .ui file translations."""
278283
if not self._qt_translator:
@@ -281,6 +286,27 @@ def _remove_qt_translator(self) -> None:
281286
self._tagger._qt_translators.remove_translator(self._qt_translator)
282287
self._qt_translator = None
283288

289+
def reload_translations(self) -> None:
290+
"""Reload translations and reinstall Qt translator.
291+
292+
Used when plugin is updated to refresh translations without recreating API instance.
293+
"""
294+
# Clear existing translations
295+
self._translations.clear()
296+
297+
# Remove old Qt translator
298+
self._remove_qt_translator()
299+
300+
# Reload translations from disk
301+
self._load_translations()
302+
303+
# Reinstall Qt translator with new translations
304+
self._install_qt_translator()
305+
306+
# Emit signal to trigger Qt translator reinstall and UI retranslation
307+
# This is only called during plugin updates, not normal installation
308+
self._tagger._qt_translators_updated.emit()
309+
284310
def _is_valid_locale(self, locale: str) -> bool:
285311
"""Check if locale string is valid (basic sanity check).
286312
@@ -402,6 +428,9 @@ def _load_translation_file_for_locale(self, locale: str) -> bool:
402428
data = self._load_translation_file(file_path, format, locale)
403429
if data:
404430
self._translations[locale] = data
431+
self._logger.debug(
432+
"Loaded %d translations for locale '%s': %s", len(data), locale, list(data.keys())[:10]
433+
)
405434
return True
406435
return False
407436

@@ -531,20 +560,31 @@ def tr(self, key: str, text: str | None = None, **kwargs) -> str:
531560
# Try exact locale match (e.g., de_DE)
532561
if locale in self._translations and key in self._translations[locale]:
533562
result = self._translations[locale][key]
563+
self._logger.debug("tr() found exact locale match: '%s' -> '%s'", key, result)
534564
else:
535565
# Try language without region (e.g., de from de_DE)
536566
lang = locale.split('_')[0]
537567
if lang in self._translations and key in self._translations[lang]:
538568
result = self._translations[lang][key]
569+
self._logger.debug("tr() found language match: '%s' -> '%s'", key, result)
570+
else:
571+
# Try source locale as fallback
572+
if self._source_locale in self._translations and key in self._translations[self._source_locale]:
573+
result = self._translations[self._source_locale][key]
574+
self._logger.debug("tr() found source locale match: '%s' -> '%s'", key, result)
575+
else:
576+
self._logger.debug("tr() no translation found for key '%s' in any locale", key)
539577

540578
# Fall back to text parameter or key
541579
if result is None:
542580
result = text if text is not None else key
581+
self._logger.debug("tr() using fallback: '%s' -> '%s'", key, result)
543582

544583
# Apply placeholder substitution
545584
if kwargs:
546585
result = result.format(**kwargs)
547586

587+
self._logger.debug("tr() final result: '%s' -> '%s'", key, result)
548588
return result
549589

550590
def trn(self, key: str, singular: str | None = None, plural: str | None = None, n: int = 0, **kwargs) -> str:
@@ -587,6 +627,15 @@ def trn(self, key: str, singular: str | None = None, plural: str | None = None,
587627
result = trans[plural_form]
588628
elif isinstance(trans, dict) and 'other' in trans:
589629
result = trans['other']
630+
else:
631+
# Try source locale as fallback
632+
if self._source_locale in self._translations and key in self._translations[self._source_locale]:
633+
trans = self._translations[self._source_locale][key]
634+
source_plural_form = get_plural_form(self._source_locale, n)
635+
if isinstance(trans, dict) and source_plural_form in trans:
636+
result = trans[source_plural_form]
637+
elif isinstance(trans, dict) and 'other' in trans:
638+
result = trans['other']
590639

591640
# Fall back to singular/plural parameters
592641
if result is None:

picard/plugin3/asyncops/registry.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,18 @@ def fetch_registry(self, callback, use_cache=True):
4444
use_cache: Whether to use cached registry
4545
"""
4646
# Check cache first
47-
if use_cache and self._registry._registry_data:
48-
callback(OperationResult(success=True, result=self._registry._registry_data))
47+
if use_cache and self._registry.is_registry_loaded():
48+
callback(OperationResult(success=True, result=self._registry.get_raw_registry_data()))
4949
return
5050

5151
def _on_response(data, reply, error):
5252
if error:
5353
callback(OperationResult(success=False, error=error, error_message=str(error)))
5454
else:
5555
try:
56-
self._registry._registry_data = json.loads(data)
57-
callback(OperationResult(success=True, result=self._registry._registry_data))
56+
registry_data = json.loads(data)
57+
self._registry.set_raw_registry_data(registry_data)
58+
callback(OperationResult(success=True, result=registry_data))
5859
except json.JSONDecodeError as e:
5960
callback(OperationResult(success=False, error=e, error_message=f'Invalid JSON: {e}'))
6061

0 commit comments

Comments
 (0)