M3 namespace migration: complete Phase B, partial Phase D, docs and tests

- Native runtime under bearhub/ (api, commons, gems, view/util, view/core, view/qt)
- bauh/ compatibility re-export shims for legacy imports
- Phase D: canonical __version__/__app_name__ in bearhub/__init__.py; pyproject.toml
  and setup.py point at bearhub package
- Native gem loader at bearhub/view/core/gems.py
- Namespace compatibility tests (159 tests passing)
- Documentation: CHANGELOG [Unreleased], NAMESPACE_MIGRATION, ROADMAP, README,
  CONTRIBUTING, docs/qt6-migration.md, packaging/aur/README.md
This commit is contained in:
Sebastian Palencsar
2026-06-27 10:26:41 +02:00
parent da377aecdc
commit e95a23bc20
240 changed files with 1382 additions and 943 deletions

View File

@@ -1,5 +1,8 @@
__version__ = '0.10.7'
__app_name__ = 'bearhub'
"""
Legacy namespace compatibility shim.
import os
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
Deprecated: import from `bearhub` instead of `bauh`.
"""
from bearhub import ROOT_DIR, __app_name__, __version__
__all__ = ["__app_name__", "__version__", "ROOT_DIR"]

1
bauh/api/__init__.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.api import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.cache import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.context import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.controller import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.disk import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.download import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.handler import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.model import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.api.abstract.view import * # noqa: F401,F403

1
bauh/api/exception.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.api.exception import * # noqa: F401,F403

1
bauh/api/http.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.api.http import * # noqa: F401,F403

1
bauh/api/paths.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.api.paths import * # noqa: F401,F403

1
bauh/api/user.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.api.user import * # noqa: F401,F403

1
bauh/commons/__init__.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons import * # noqa: F401,F403

1
bauh/commons/boot.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.boot import * # noqa: F401,F403

1
bauh/commons/category.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.category import * # noqa: F401,F403

1
bauh/commons/config.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.config import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.commons.custom_types import * # noqa: F401,F403

1
bauh/commons/html.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.html import * # noqa: F401,F403

1
bauh/commons/internet.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.internet import * # noqa: F401,F403

1
bauh/commons/regex.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.regex import * # noqa: F401,F403

1
bauh/commons/resource.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.resource import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.commons.singleton import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.commons.suggestions import * # noqa: F401,F403

1
bauh/commons/system.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.system import * # noqa: F401,F403

1
bauh/commons/util.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.commons.util import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.commons.version_util import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.commons.view_utils import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage.config import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage.controller import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage.model import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage.query import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage.util import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.appimage.worker import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.__init__ import * # noqa: F401,F403

1
bauh/gems/arch/aur.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.aur import * # noqa: F401,F403

1
bauh/gems/arch/config.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.config import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.confirmation import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.controller import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.cpu_manager import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.database import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.dependencies import * # noqa: F401,F403

1
bauh/gems/arch/disk.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.disk import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.download import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.exceptions import * # noqa: F401,F403

1
bauh/gems/arch/git.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.git import * # noqa: F401,F403

1
bauh/gems/arch/gpg.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.gpg import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.makepkg import * # noqa: F401,F403

1
bauh/gems/arch/mapper.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.mapper import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.message import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.mirrors import * # noqa: F401,F403

1
bauh/gems/arch/model.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.model import * # noqa: F401,F403

1
bauh/gems/arch/output.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.output import * # noqa: F401,F403

1
bauh/gems/arch/pacman.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.pacman import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.pkgbuild import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.proc_util import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.rebuild_detector import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.sorting import * # noqa: F401,F403

1
bauh/gems/arch/sshell.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.sshell import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.suggestions import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.arch.updates import * # noqa: F401,F403

1
bauh/gems/arch/worker.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.arch.worker import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak.config import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak.constants import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak.controller import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak.flatpak import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak.model import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.flatpak.worker import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.web import * # noqa: F401,F403

1
bauh/gems/web/config.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.web.config import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.web.controller import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.web.environment import * # noqa: F401,F403

1
bauh/gems/web/model.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.web.model import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.web.nativefier import * # noqa: F401,F403

1
bauh/gems/web/search.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.web.search import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.gems.web.suggestions import * # noqa: F401,F403

1
bauh/gems/web/worker.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.gems.web.worker import * # noqa: F401,F403

View File

@@ -1,258 +1 @@
import glob
import os
import re
from typing import Optional, Dict, Tuple, Set
from bauh.api.paths import USER_THEMES_DIR
from bauh.view.util import resource
from bauh.view.util.translation import I18n
# RE_WIDTH_PERCENT = re.compile(r'[\d\\.]+%w') TODO percentage measures disabled for the moment (requires more testing)
# RE_HEIGHT_PERCENT = re.compile(r'[\d\\.]+%h') TODO percentage measures disabled for the moment (requires more testing)
RE_META_I18N_FIELDS = re.compile(r'((name|description)(\[\w+])?)')
RE_VAR_PATTERN = re.compile(r'^@[\w.\-_]+')
RE_QSS_EXT = re.compile(r'\.qss$')
class ThemeMetadata:
def __init__(self, file_path: str, default: bool, default_name: Optional[str] = None,
default_description: Optional[str] = None, version: Optional[str] = None,
root_theme: Optional[str] = None, abstract: bool = False):
self.names = {}
self.default_name = default_name
self.descriptions = {}
self.default_description = default_description
self.root_theme = root_theme
self.version = version
self.file_path = file_path
self.file_dir = '/'.join(file_path.split('/')[0:-1])
self.default = default
self.key = self.file_path.split('/')[-1].split('.')[0] if self.default else self.file_path
self.abstract = abstract
def __eq__(self, other) -> bool:
if isinstance(other, ThemeMetadata):
return self.file_path == other.file_path
return False
def __hash__(self):
return self.file_path.__hash__()
def __repr__(self):
return self.file_path if self.file_path else ''
def get_i18n_name(self, i18n: I18n) -> str:
if self.names:
name = self.names.get(i18n.current_key, self.names.get(i18n.default_key))
if name:
return name
if self.default_name:
return self.default_name
else:
return self.file_path.split('/')[-1]
def get_i18n_description(self, i18n: I18n) -> Optional[str]:
if self.descriptions:
des = self.descriptions.get(i18n.current_key, self.descriptions.get(i18n.default_key))
if des:
return des
return self.default_description
def read_theme_metada(key: str, file_path: str) -> ThemeMetadata:
meta_file = RE_QSS_EXT.sub('.meta', file_path)
meta_obj = ThemeMetadata(file_path=file_path, default_name=key, default=not key.startswith('/'))
if os.path.exists(meta_file):
meta_dict = {}
with open(meta_file) as f:
for line in f.readlines():
if line:
field_split = line.split('=')
if len(field_split) > 1:
meta_dict[field_split[0].strip()] = field_split[1].strip()
if meta_dict:
for field, val in meta_dict.items():
if field == 'version':
meta_obj.version = val
elif field == 'root_theme':
meta_obj.root_theme = val
elif field == 'name':
meta_obj.default_name = val
elif field == 'description':
meta_obj.default_description = val
elif field == 'abstract':
boolean = val.lower()
if boolean == 'true':
meta_obj.abstract = True
elif boolean == 'false':
meta_obj.abstract = False
else:
i18n_field = RE_META_I18N_FIELDS.findall(field)
if i18n_field:
if i18n_field[0][1] == 'name':
meta_obj.names[i18n_field[0][2][1:-1]] = val
else:
meta_obj.descriptions[i18n_field[0][2][1:-1]] = val
return meta_obj
def read_default_themes() -> Dict[str, str]:
return {f.split('/')[-1].split('.')[0].lower(): f for f in glob.glob(resource.get_path('style/**/*.qss'))}
def read_user_themes() -> Dict[str, str]:
return {f: f for f in glob.glob('{}/**/*.qss'.format(USER_THEMES_DIR), recursive=True)}
def read_all_themes_metadata() -> Set[ThemeMetadata]:
themes = set()
for key, file_path in read_default_themes().items():
themes.add(read_theme_metada(key=key, file_path=file_path))
for key, file_path in read_user_themes().items():
themes.add(read_theme_metada(key=key, file_path=file_path))
return themes
def process_theme(file_path: str, theme_str: str, metadata: ThemeMetadata,
available_themes: Optional[Dict[str, str]]) -> Optional[Tuple[str, ThemeMetadata]]:
if theme_str and metadata:
root_theme = None
if metadata.root_theme and metadata.root_theme in available_themes:
root_file = available_themes[metadata.root_theme]
if os.path.isfile(root_file):
with open(root_file) as f:
root_theme_str = f.read()
if root_theme_str:
root_metadata = read_theme_metada(key=metadata.root_theme, file_path=root_file)
root_theme = process_theme(file_path=root_file,
theme_str=root_theme_str,
metadata=root_metadata,
available_themes=available_themes)
var_map = _read_var_file(file_path)
var_map['images'] = resource.get_path('img')
var_map['style_dir'] = metadata.file_dir
if var_map:
var_list = [*var_map.keys()]
var_list.sort(key=_by_str_len, reverse=True)
for var in var_list:
theme_str = theme_str.replace('@' + var, var_map[var])
# TODO percentage measures disabled for the moment (requires more testing)
# screen_size = QApplication.primaryScreen().size()
# theme_str = process_width_percent_measures(theme_str, screen_size.width())
# theme_str = process_height_percent_measures(theme_str, screen_size.height())
return theme_str if not root_theme else '{}\n{}'.format(root_theme[0], theme_str), metadata
def _by_str_len(string: str) -> int:
return len(string)
def _read_var_file(theme_file: str) -> dict:
vars_file = theme_file.replace('.qss', '.vars')
var_map = {}
if os.path.isfile(vars_file):
with open(vars_file) as f:
for line in f.readlines():
if line:
line_strip = line.strip()
if line_strip:
var_value = line_strip.split('=')
if var_value and len(var_value) == 2:
var, value = var_value[0].strip(), var_value[1].strip()
if var and value:
var_map[var] = value
if var_map:
process_var_of_vars(var_map) # mapping keys that point to others
return var_map
def process_var_of_vars(var_map: dict):
while True:
pending_vars, invalid = {}, set()
for k, v in var_map.items():
var_match = RE_VAR_PATTERN.match(v)
if var_match:
var_name = var_match.group()[1:]
if var_name not in var_map or var_name == k:
invalid.add(k)
else:
pending_vars[k] = var_name
for key in invalid:
del var_map[key]
if not pending_vars:
break
resolved = 0
for key, val in pending_vars.items():
real_val = var_map[val]
if not RE_VAR_PATTERN.match(real_val):
var_map[key] = real_val
resolved += 1
if resolved == len(pending_vars):
break
# TODO percentage measures disabled for the moment (requires more testing)
# def process_width_percent_measures(theme: str, screen_width: int) -> str:
# width_measures = RE_WIDTH_PERCENT.findall(theme)
#
# final_theme = theme
# if width_measures:
# for m in width_measures:
# try:
# percent = float(m.split('%')[0])
# final_theme = final_theme.replace(m, '{}px'.format(round(screen_width * percent)))
# except ValueError:
# traceback.print_exc()
#
# return final_theme
# def process_height_percent_measures(theme: str, screen_height: int) -> str:
# width_measures = RE_HEIGHT_PERCENT.findall(theme)
#
# final_sheet = theme
# if width_measures:
# for m in width_measures:
# try:
# percent = float(m.split('%')[0])
# final_sheet = final_sheet.replace(m, '{}px'.format(round(screen_height * percent)))
# except ValueError:
# traceback.print_exc()
#
# return final_sheet
from bearhub.stylesheet import * # noqa: F401,F403

1
bauh/view/core/__init__.py Executable file → Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.core import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.core.controller import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.core.downloader import * # noqa: F401,F403

View File

@@ -1,81 +1 @@
import inspect
import importlib
import os
from logging import Logger
from typing import List, Generator
from bauh import __app_name__, ROOT_DIR
from bauh.api.abstract.controller import SoftwareManager, ApplicationContext
from bauh.view.util import translation
FORBIDDEN_GEMS_FILE = f'/etc/{__app_name__}/gems.forbidden'
def find_manager(member):
if not isinstance(member, str):
if inspect.isclass(member) and inspect.getmro(member)[1].__name__ == 'SoftwareManager':
return member
elif inspect.ismodule(member):
for name, mod in inspect.getmembers(member):
manager_found = find_manager(mod)
if manager_found:
return manager_found
def read_forbidden_gems() -> Generator[str, None, None]:
try:
with open(FORBIDDEN_GEMS_FILE) as f:
forbidden_lines = f.readlines()
for line in forbidden_lines:
clean_line = line.strip()
if clean_line and not clean_line.startswith('#'):
yield clean_line
except FileNotFoundError:
pass
def load_managers(locale: str, context: ApplicationContext, config: dict, default_locale: str, logger: Logger) -> List[SoftwareManager]:
managers = []
forbidden_gems = {gem for gem in read_forbidden_gems()}
for f in os.scandir(f'{ROOT_DIR}/gems'):
if f.is_dir() and f.name != '__pycache__':
if f.name in forbidden_gems:
logger.warning(f"gem '{f.name}' could not be loaded because it was marked as forbidden in '{FORBIDDEN_GEMS_FILE}'")
continue
module_name = f'bauh.gems.{f.name}.controller'
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = None
if module:
manager_class = find_manager(module)
if manager_class:
if locale:
locale_path = f'{f.path}/resources/locale'
if os.path.exists(locale_path):
context.i18n.current.update(translation.get_locale_keys(locale, locale_path)[1])
if default_locale and context.i18n.default:
context.i18n.default.update(translation.get_locale_keys(default_locale, locale_path)[1])
man = manager_class(context=context)
if config['gems'] is None:
man.set_enabled(man.is_default_enabled())
else:
man.set_enabled(f.name in config['gems'])
managers.append(man)
return managers
from bearhub.view.core.gems import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.core.settings import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.core.suggestions import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.core.timeshift import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.core.tray_client import * # noqa: F401,F403

1
bauh/view/core/update.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.core.update import * # noqa: F401,F403

1
bauh/view/qt/__init__.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt import * # noqa: F401,F403

1
bauh/view/qt/about.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt.about import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.qt.apps_table import * # noqa: F401,F403

1
bauh/view/qt/commons.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt.commons import * # noqa: F401,F403

View File

@@ -0,0 +1 @@
from bearhub.view.qt.components import * # noqa: F401,F403

1
bauh/view/qt/dialog.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt.dialog import * # noqa: F401,F403

1
bauh/view/qt/history.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt.history import * # noqa: F401,F403

1
bauh/view/qt/info.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt.info import * # noqa: F401,F403

1
bauh/view/qt/prepare.py Normal file
View File

@@ -0,0 +1 @@
from bearhub.view.qt.prepare import * # noqa: F401,F403

Some files were not shown because too many files have changed in this diff Show More