imports.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. """Import operations."""
  2. from __future__ import annotations
  3. from collections import defaultdict
  4. from typing import Dict, List, Optional
  5. from reflex.base import Base
  6. from reflex.constants.installer import PackageJson
  7. def merge_imports(*imports) -> ImportDict:
  8. """Merge multiple import dicts together.
  9. Args:
  10. *imports: The list of import dicts to merge.
  11. Returns:
  12. The merged import dicts.
  13. """
  14. all_imports = defaultdict(list)
  15. for import_dict in imports:
  16. for lib, fields in import_dict.items():
  17. all_imports[lib].extend(fields)
  18. return all_imports
  19. def collapse_imports(imports: ImportDict) -> ImportDict:
  20. """Remove all duplicate ImportVar within an ImportDict.
  21. Args:
  22. imports: The import dict to collapse.
  23. Returns:
  24. The collapsed import dict.
  25. """
  26. return {lib: list(set(import_vars)) for lib, import_vars in imports.items()}
  27. def split_library_name_version(library_fullname: str):
  28. """Split the name of a library from its version.
  29. Args:
  30. library_fullname: The fullname of the library.
  31. Returns:
  32. A tuple of the library name and version.
  33. """
  34. lib, at, version = library_fullname.rpartition("@")
  35. if not lib:
  36. lib = at + version
  37. version = None
  38. return lib, version
  39. class ImportVar(Base):
  40. """An import var."""
  41. # The package name associated with the tag
  42. library: Optional[str]
  43. # The name of the import tag.
  44. tag: Optional[str]
  45. # whether the import is default or named.
  46. is_default: Optional[bool] = False
  47. # The tag alias.
  48. alias: Optional[str] = None
  49. # The following fields provide extra information about the import,
  50. # but are not factored in when considering hash or equality
  51. # The version of the package
  52. version: Optional[str]
  53. # Whether this import need to install the associated lib
  54. install: Optional[bool] = True
  55. # whether this import should be rendered or not
  56. render: Optional[bool] = True
  57. # whether this import package should be added to transpilePackages in next.config.js
  58. # https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages
  59. transpile: Optional[bool] = False
  60. def __init__(
  61. self,
  62. *,
  63. package: Optional[str] = None,
  64. **kwargs,
  65. ):
  66. """Create a new ImportVar.
  67. Args:
  68. package: The package to install for this import.
  69. **kwargs: The import var fields.
  70. Raises:
  71. ValueError: If the package is provided with library or version.
  72. """
  73. if package is not None:
  74. if (
  75. kwargs.get("library", None) is not None
  76. or kwargs.get("version", None) is not None
  77. ):
  78. raise ValueError(
  79. "Cannot provide 'library' or 'version' as keyword arguments when "
  80. "specifying 'package' as an argument"
  81. )
  82. kwargs["library"], kwargs["version"] = split_library_name_version(package)
  83. install = (
  84. package is not None
  85. # TODO: handle version conflicts
  86. and package not in PackageJson.DEPENDENCIES
  87. and package not in PackageJson.DEV_DEPENDENCIES
  88. and not any(package.startswith(prefix) for prefix in ["/", ".", "next/"])
  89. and package != ""
  90. )
  91. kwargs.setdefault("install", install)
  92. super().__init__(**kwargs)
  93. @property
  94. def name(self) -> str:
  95. """The name of the import.
  96. Returns:
  97. The name(tag name with alias) of tag.
  98. """
  99. if self.alias:
  100. return (
  101. self.alias if self.is_default else " as ".join([self.tag, self.alias]) # type: ignore
  102. )
  103. else:
  104. return self.tag or ""
  105. @property
  106. def package(self) -> str | None:
  107. """The package to install for this import.
  108. Returns:
  109. The library name and (optional) version to be installed by npm/bun.
  110. """
  111. if self.version:
  112. return f"{self.library}@{self.version}"
  113. return self.library
  114. def __hash__(self) -> int:
  115. """Define a hash function for the import var.
  116. Returns:
  117. The hash of the var.
  118. """
  119. return hash(
  120. (
  121. self.library,
  122. self.tag,
  123. self.is_default,
  124. self.alias,
  125. )
  126. )
  127. def __eq__(self, other: ImportVar) -> bool:
  128. """Define equality for the import var.
  129. Args:
  130. other: The other import var to compare.
  131. Returns:
  132. Whether the two import vars are equal.
  133. """
  134. if type(self) != type(other):
  135. return NotImplemented
  136. return (self.library, self.tag, self.is_default, self.alias) == (
  137. other.library,
  138. other.tag,
  139. other.is_default,
  140. other.alias,
  141. )
  142. def collapse(self, other_import_var: ImportVar) -> ImportVar:
  143. """Collapse two import vars together.
  144. Args:
  145. other_import_var: The other import var to collapse with.
  146. Returns:
  147. The collapsed import var with sticky props perserved.
  148. Raises:
  149. ValueError: If the two import vars have conflicting properties.
  150. """
  151. if self != other_import_var:
  152. raise ValueError("Cannot collapse two import vars with different hashes")
  153. if (
  154. self.version is not None
  155. and other_import_var.version is not None
  156. and self.version != other_import_var.version
  157. ):
  158. raise ValueError(
  159. "Cannot collapse two import vars with conflicting version specifiers: "
  160. f"{self} {other_import_var}"
  161. )
  162. return type(self)(
  163. library=self.library,
  164. version=self.version or other_import_var.version,
  165. tag=self.tag,
  166. is_default=self.is_default,
  167. alias=self.alias,
  168. install=self.install or other_import_var.install,
  169. render=self.render or other_import_var.render,
  170. transpile=self.transpile or other_import_var.transpile,
  171. )
  172. class ImportList(List[ImportVar]):
  173. """A list of import vars."""
  174. def __init__(self, *args, **kwargs):
  175. """Create a new ImportList (wrapper over `list`).
  176. Any items that are not already `ImportVar` will be assumed as dicts to convert
  177. into an ImportVar.
  178. Args:
  179. *args: The args to pass to list.__init__
  180. **kwargs: The kwargs to pass to list.__init__
  181. """
  182. super().__init__(*args, **kwargs)
  183. for ix, value in enumerate(self):
  184. if not isinstance(value, ImportVar):
  185. # convert dicts to ImportVar
  186. self[ix] = ImportVar(**value)
  187. @classmethod
  188. def from_import_dict(
  189. cls, import_dict: ImportDict | Dict[str, set[ImportVar]]
  190. ) -> ImportList:
  191. """Create an import list from an import dict.
  192. Args:
  193. import_dict: The import dict to convert.
  194. Returns:
  195. The import list.
  196. """
  197. return cls(
  198. ImportVar(package=lib, **imp.dict())
  199. for lib, imps in import_dict.items()
  200. for imp in imps
  201. )
  202. def collapse(self) -> ImportDict:
  203. """When collapsing an import list, prefer packages with version specifiers.
  204. Returns:
  205. The collapsed import dict ({package_spec: [import_var1, ...]}).
  206. Raises:
  207. ValueError: If two imports have conflicting version specifiers.
  208. """
  209. collapsed: dict[str, dict[ImportVar, ImportVar]] = {}
  210. for imp in self:
  211. lib = imp.library or ""
  212. collapsed.setdefault(lib, {})
  213. if imp in collapsed[lib]:
  214. # Need to check if the current import has any special properties that need to
  215. # be preserved, like the version specifier, install, or transpile.
  216. existing_imp = collapsed[lib][imp]
  217. collapsed[lib][imp] = existing_imp.collapse(imp)
  218. else:
  219. collapsed[lib][imp] = imp
  220. # Check that all tags in the given library have the same version.
  221. deduped: ImportDict = {}
  222. for lib, imps in collapsed.items():
  223. packages = {imp.package for imp in imps if imp.version is not None}
  224. if len(packages) > 1:
  225. raise ValueError(
  226. f"Imports from {lib} have conflicting version specifiers: "
  227. f"{packages} {imps}"
  228. )
  229. deduped[list(packages)[0] or ""] = list(imps.values())
  230. return deduped
  231. ImportDict = Dict[str, List[ImportVar]]