Python 包(package) 重名, 包重载

来源

之前使用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了解都不够深入,有不足之处,还请多多指正!

你可能感兴趣的:(软件开发经验分享,python,开发语言)