hook-sqlalchemy.py 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. #-----------------------------------------------------------------------------
  2. # Copyright (c) 2005-2023, PyInstaller Development Team.
  3. #
  4. # Distributed under the terms of the GNU General Public License (version 2
  5. # or later) with exception for distributing the bootloader.
  6. #
  7. # The full license is in the file COPYING.txt, distributed with this software.
  8. #
  9. # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
  10. #-----------------------------------------------------------------------------
  11. import re
  12. import importlib.util
  13. from PyInstaller import isolated
  14. from PyInstaller.lib.modulegraph.modulegraph import SourceModule
  15. from PyInstaller.utils.hooks import check_requirement, collect_entry_point, logger
  16. datas = []
  17. # 'sqlalchemy.testing' causes bundling a lot of unnecessary modules.
  18. excludedimports = ['sqlalchemy.testing']
  19. # Include most common database bindings some database bindings are detected and include some are not. We should
  20. # explicitly include database backends.
  21. hiddenimports = ['pysqlite2', 'MySQLdb', 'psycopg2', 'sqlalchemy.ext.baked']
  22. if check_requirement('sqlalchemy >= 1.4'):
  23. hiddenimports.append("sqlalchemy.sql.default_comparator")
  24. @isolated.decorate
  25. def _get_dialect_modules(module_name):
  26. import importlib
  27. module = importlib.import_module(module_name)
  28. return [f"{module_name}.{submodule_name}" for submodule_name in module.__all__]
  29. # In SQLAlchemy >= 0.6, the "sqlalchemy.dialects" package provides dialects.
  30. # In SQLAlchemy <= 0.5, the "sqlalchemy.databases" package provides dialects.
  31. if check_requirement('sqlalchemy >= 0.6'):
  32. hiddenimports += _get_dialect_modules("sqlalchemy.dialects")
  33. else:
  34. hiddenimports += _get_dialect_modules("sqlalchemy.databases")
  35. # Collect additional dialects and plugins that are registered via entry-points, under assumption that they are available
  36. # in the build environment for a reason (i.e., they are used).
  37. for entry_point_name in ('sqlalchemy.dialects', 'sqlalchemy.plugins'):
  38. ep_datas, ep_hiddenimports = collect_entry_point(entry_point_name)
  39. datas += ep_datas
  40. hiddenimports += ep_hiddenimports
  41. def hook(hook_api):
  42. """
  43. SQLAlchemy 0.9 introduced the decorator 'util.dependencies'. This decorator does imports. E.g.:
  44. @util.dependencies("sqlalchemy.sql.schema")
  45. This hook scans for included SQLAlchemy modules and then scans those modules for any util.dependencies and marks
  46. those modules as hidden imports.
  47. """
  48. if not check_requirement('sqlalchemy >= 0.9'):
  49. return
  50. # this parser is very simplistic but seems to catch all cases as of V1.1
  51. depend_regex = re.compile(r'@util.dependencies\([\'"](.*?)[\'"]\)')
  52. hidden_imports_set = set()
  53. known_imports = set()
  54. for node in hook_api.module_graph.iter_graph(start=hook_api.module):
  55. if isinstance(node, SourceModule) and node.identifier.startswith('sqlalchemy.'):
  56. known_imports.add(node.identifier)
  57. # Read the source...
  58. with open(node.filename, 'rb') as f:
  59. source_code = f.read()
  60. source_code = importlib.util.decode_source(source_code)
  61. # ... and scan it
  62. for match in depend_regex.findall(source_code):
  63. hidden_imports_set.add(match)
  64. hidden_imports_set -= known_imports
  65. if len(hidden_imports_set):
  66. logger.info(" Found %d sqlalchemy hidden imports", len(hidden_imports_set))
  67. hook_api.add_imports(*list(hidden_imports_set))