之前使用PyQt5写的程序,但是目标系统没有,但是有PySide2/PySide6,因其授权更宽松。因为程序比较简单,那么,能不能一劳永逸?兼容各种Qt库版本呢?
写一个自己的PyQt5包,处理兼容问题。这也就引出了当前的问题,在一个包里面,导入同名的系统包问题,或者叫“包重载”。
这个问题耗了我2天时间
最终的解决方案是,使用修改包导入函数。参考官方importlib的导入包示例代码,但导入包时需要加前缀,并处理导入列表。
import os
import sys
import importlib.util
__this_package_path__ = os.path.abspath(__package__ + '/..')
__sys_path_without_this__ = [p for p in sys.path if not p.startswith(__this_package_path__)]
def import_from_path(
name,
package=None,
path=None,
globals=None,
locals=None,
fromlist=(),
rename=None
):
"""An approximate implementation of import."""
absolute_name = importlib.util.resolve_name(name, package)
if rename:
sys_module_name = '{}{}'.format(rename, absolute_name)
else:
sys_module_name = absolute_name
try:
return sys.modules[sys_module_name]
except KeyError:
pass
has_parent = False
if '.' in absolute_name:
has_parent = True
parent_name, _, child_name = absolute_name.rpartition('.')
parent_module = import_from_path(
parent_name,
package=package,
path=path,
globals=globals,
locals=locals,
fromlist=(),
rename=rename,
)
path = parent_module.__spec__.submodule_search_locations
for finder in sys.meta_path:
spec = finder.find_spec(absolute_name, path)
if spec is not None:
break
else:
msg = f'No module named {absolute_name!r}'
raise ModuleNotFoundError(msg, name=absolute_name)
module = importlib.util.module_from_spec(spec)
sys.modules[sys_module_name] = module
spec.loader.exec_module(module)
if has_parent:
setattr(parent_module, child_name, module)
if len(fromlist) > 0:
if '*' in fromlist:
# is there an __all__? if so respect it
if "__all__" in module.__dict__:
names = module.__dict__["__all__"]
else:
# otherwise we import all names that don't begin with _
names = [x for x in module.__dict__ if not x.startswith("_")]
else:
names = fromlist
# now drag them in
if locals:
locals.update({k: getattr(module, k) for k in names})
if globals:
globals.update({k: getattr(module, k) for k in names})
return module
def import_sys_module(name, package=None, globals=None, locals=None, fromlist=()):
return import_from_path(
name,
package,
__sys_path_without_this__,
globals=globals,
locals=locals,
fromlist=fromlist,
rename='Sys'
)
之后对于同名包,导入时使用import_sys_module函数导入
# PyQt5/__init__.py
__qt_module__ = None
try:
from PySide2 import *
__qt_module__ = "PySide2"
except ImportError:
try:
from PySide6 import *
__qt_module__ = "PySide6"
except ImportError:
try:
import_sys_module(
'PyQt5',
globals=globals(),
locals=locals(),
fromlist=('*',)
)
__qt_module__ = "PyQt5"
except ImportError:
try:
from PyQt6 import *
__qt_module__ = "PyQt6"
except ImportError:
raise
其他模块
# PyQt5/QtCore/__init__.py
from .. import __qt_module__, import_sys_module
if __qt_module__ == 'PySide2':
from PySide2.QtCore import *
elif __qt_module__ == 'PySide6':
from PySide6.QtCore import *
from PySide6.QtCore import Signal as pyqtSignal
elif __qt_module__ == 'PyQt5':
import_sys_module('PyQt5.QtCore', globals=globals(), locals=locals(), fromlist=('*',))
elif __qt_module__ == 'PyQt6':
from PyQt6.QtCore import *
else:
raise ValueError('Can not handle this qt module: ', __qt_module__)
模块好多啊, 那么能不能写个代码生成模块呢?
import os
import sys
compat_qt_module_name = 'PyQt5'
submodules = ['QtCore', 'QtGui', 'QtWidgets', 'sip']
compat_qt_source_modules = ['PySide2', 'PySide6', 'PyQt5', 'PyQt6']
# generate main module __init__.py codes
main_module_init_string = '''
import os
import sys
import importlib.util
__this_package_path__ = os.path.abspath(__package__ + '/..')
__sys_path_without_this__ = [p for p in sys.path if not p.startswith(__this_package_path__)]
def import_from_path(
name,
package=None,
path=None,
globals=None,
locals=None,
fromlist=(),
rename=None
):
"""An approximate implementation of import."""
absolute_name = importlib.util.resolve_name(name, package)
if rename:
sys_module_name = '{}{}'.format(rename, absolute_name)
else:
sys_module_name = absolute_name
try:
return sys.modules[sys_module_name]
except KeyError:
pass
has_parent = False
if '.' in absolute_name:
has_parent = True
parent_name, _, child_name = absolute_name.rpartition('.')
parent_module = import_from_path(
parent_name,
package=package,
path=path,
globals=globals,
locals=locals,
fromlist=(),
rename=rename,
)
path = parent_module.__spec__.submodule_search_locations
for finder in sys.meta_path:
spec = finder.find_spec(absolute_name, path)
if spec is not None:
break
else:
msg = f'No module named {absolute_name!r}'
raise ModuleNotFoundError(msg, name=absolute_name)
module = importlib.util.module_from_spec(spec)
sys.modules[sys_module_name] = module
spec.loader.exec_module(module)
if has_parent:
setattr(parent_module, child_name, module)
if len(fromlist) > 0:
if '*' in fromlist:
# is there an __all__? if so respect it
if "__all__" in module.__dict__:
names = module.__dict__["__all__"]
else:
# otherwise we import all names that don't begin with _
names = [x for x in module.__dict__ if not x.startswith("_")]
else:
names = fromlist
# now drag them in
if locals:
locals.update({k: getattr(module, k) for k in names})
if globals:
globals.update({k: getattr(module, k) for k in names})
return module
def import_sys_module(name, package=None, globals=None, locals=None, fromlist=()):
return import_from_path(
name,
package,
__sys_path_without_this__,
globals=globals,
locals=locals,
fromlist=fromlist,
rename='Sys'
)
'''
main_module_init_string += '__qt_module__ = None\n'
for i, sm in enumerate(compat_qt_source_modules):
main_module_init_string += ' ' * i + 'try:\n'
if not sm.startswith('PyQt5'):
main_module_init_string += ' ' * i + ' from {} import *\n'.format(sm)
else:
main_module_init_string += ' ' * i + ' import_sys_module(\n'
main_module_init_string += ' ' * i + ' \'PyQt5\',\n'
main_module_init_string += ' ' * i + ' globals=globals(),\n'
main_module_init_string += ' ' * i + ' locals=locals(),\n'
main_module_init_string += ' ' * i + ' fromlist=(\'*\',)\n'
main_module_init_string += ' ' * i + ' )\n'
main_module_init_string += ' ' * i + ' __qt_module__ = "{}"\n'.format(sm)
main_module_init_string += ' ' * i + 'except ImportError:\n'
if i != len(compat_qt_source_modules) - 1:
continue
else:
main_module_init_string += ' ' * i + ' raise\n'
print(main_module_init_string)
main_compat_qt_module_file = '{}/__init__.py'.format(compat_qt_module_name)
os.makedirs(os.path.dirname(main_compat_qt_module_file), exist_ok=True)
with open(main_compat_qt_module_file, 'w') as fp:
fp.write(main_module_init_string)
# generate submodule __init__.py codes
for i, sm in enumerate(submodules):
submodule_init_string = '\n'
submodule_init_string += 'from {} import __qt_module__, import_sys_module\n'.format('.' * (len(sm.split('/')) + 1))
for j, srm in enumerate(compat_qt_source_modules):
submodule_init_string += '{} __qt_module__ == \'{}\':\n'.format('if' if j == 0 else 'elif', srm)
if srm.startswith('PyQt5'):
submodule_init_string += ' import_sys_module(\'{}.{}\',' \
' globals=globals(), locals=locals(), fromlist=(' \
'\'*\',))\n'.format(srm, sm)
else:
submodule_init_string += ' from {}.{} import *\n'.format(srm, sm)
submodule_init_string += 'else:\n'
submodule_init_string += ' raise ValueError(\'Can not handle this qt module: \', __qt_module__)\n'
print(submodule_init_string)
compat_qt_submodule_file = '{}/{}/__init__.py'.format(compat_qt_module_name, sm)
os.makedirs(os.path.dirname(compat_qt_submodule_file), exist_ok=True)
with open(compat_qt_submodule_file, 'w') as fp:
fp.write(submodule_init_string)
if __name__ == '__main__':
pass
本文并没有处理所有的兼容问题,如pyqtSignal 在 PySide6中没有等,程序中用的其他涉及兼容的问题还需要进一步处理。
对Qt和Python了解都不够深入,有不足之处,还请多多指正!