path_ops.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. """Path operations."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import re
  6. import shutil
  7. import stat
  8. from pathlib import Path
  9. from reflex.config import get_config
  10. from reflex.environment import environment
  11. # Shorthand for join.
  12. join = os.linesep.join
  13. def chmod_rm(path: Path):
  14. """Remove a file or directory with chmod.
  15. Args:
  16. path: The path to the file or directory.
  17. """
  18. path.chmod(stat.S_IWRITE)
  19. if path.is_dir():
  20. shutil.rmtree(path)
  21. elif path.is_file():
  22. path.unlink()
  23. def rm(path: str | Path):
  24. """Remove a file or directory.
  25. Args:
  26. path: The path to the file or directory.
  27. """
  28. path = Path(path)
  29. if path.is_dir():
  30. # In Python 3.12, onerror is deprecated in favor of onexc
  31. shutil.rmtree(path, onerror=lambda _func, _path, _info: chmod_rm(path))
  32. elif path.is_file():
  33. path.unlink()
  34. def copy_tree(
  35. src: str | Path,
  36. dest: str | Path,
  37. ignore: tuple[str, ...] | None = None,
  38. ):
  39. """Copy a directory tree.
  40. Args:
  41. src: The path to the source directory.
  42. dest: The path to the destination directory.
  43. ignore: Ignoring files and directories that match one of the glob-style patterns provided
  44. """
  45. src = Path(src)
  46. dest = Path(dest)
  47. if dest.exists():
  48. for item in dest.iterdir():
  49. rm(item)
  50. shutil.copytree(
  51. src,
  52. dest,
  53. ignore=shutil.ignore_patterns(*ignore) if ignore is not None else ignore,
  54. dirs_exist_ok=True,
  55. )
  56. def cp(
  57. src: str | Path,
  58. dest: str | Path,
  59. overwrite: bool = True,
  60. ignore: tuple[str, ...] | None = None,
  61. ) -> bool:
  62. """Copy a file or directory.
  63. Args:
  64. src: The path to the file or directory.
  65. dest: The path to the destination.
  66. overwrite: Whether to overwrite the destination.
  67. ignore: Ignoring files and directories that match one of the glob-style patterns provided
  68. Returns:
  69. Whether the copy was successful.
  70. """
  71. src, dest = Path(src), Path(dest)
  72. if src == dest:
  73. return False
  74. if not overwrite and dest.exists():
  75. return False
  76. if src.is_dir():
  77. copy_tree(src, dest, ignore)
  78. else:
  79. shutil.copyfile(src, dest)
  80. return True
  81. def mv(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
  82. """Move a file or directory.
  83. Args:
  84. src: The path to the file or directory.
  85. dest: The path to the destination.
  86. overwrite: Whether to overwrite the destination.
  87. Returns:
  88. Whether the move was successful.
  89. """
  90. src, dest = Path(src), Path(dest)
  91. if src == dest:
  92. return False
  93. if not overwrite and dest.exists():
  94. return False
  95. rm(dest)
  96. shutil.move(src, dest)
  97. return True
  98. def mkdir(path: str | Path):
  99. """Create a directory.
  100. Args:
  101. path: The path to the directory.
  102. """
  103. Path(path).mkdir(parents=True, exist_ok=True)
  104. def ls(path: str | Path) -> list[Path]:
  105. """List the contents of a directory.
  106. Args:
  107. path: The path to the directory.
  108. Returns:
  109. A list of paths to the contents of the directory.
  110. """
  111. return list(Path(path).iterdir())
  112. def ln(src: str | Path, dest: str | Path, overwrite: bool = False) -> bool:
  113. """Create a symbolic link.
  114. Args:
  115. src: The path to the file or directory.
  116. dest: The path to the destination.
  117. overwrite: Whether to overwrite the destination.
  118. Returns:
  119. Whether the link was successful.
  120. """
  121. src, dest = Path(src), Path(dest)
  122. if src == dest:
  123. return False
  124. if not overwrite and (dest.exists() or dest.is_symlink()):
  125. return False
  126. if src.is_dir():
  127. rm(dest)
  128. src.symlink_to(dest, target_is_directory=True)
  129. else:
  130. src.symlink_to(dest)
  131. return True
  132. def which(program: str | Path) -> Path | None:
  133. """Find the path to an executable.
  134. Args:
  135. program: The name of the executable.
  136. Returns:
  137. The path to the executable.
  138. """
  139. which_result = shutil.which(program)
  140. return Path(which_result) if which_result else None
  141. def use_system_bun() -> bool:
  142. """Check if the system bun should be used.
  143. Returns:
  144. Whether the system bun should be used.
  145. """
  146. return environment.REFLEX_USE_SYSTEM_BUN.get()
  147. def get_node_bin_path() -> Path | None:
  148. """Get the node binary dir path.
  149. Returns:
  150. The path to the node bin folder.
  151. """
  152. return bin_path.parent.absolute() if (bin_path := get_node_path()) else None
  153. def get_node_path() -> Path | None:
  154. """Get the node binary path.
  155. Returns:
  156. The path to the node binary file.
  157. """
  158. return which("node")
  159. def get_npm_path() -> Path | None:
  160. """Get npm binary path.
  161. Returns:
  162. The path to the npm binary file.
  163. """
  164. return npm_path.absolute() if (npm_path := which("npm")) else None
  165. def get_bun_path() -> Path | None:
  166. """Get bun binary path.
  167. Returns:
  168. The path to the bun binary file.
  169. """
  170. bun_path = get_config().bun_path
  171. if use_system_bun() or not bun_path.exists():
  172. bun_path = which("bun")
  173. return bun_path.absolute() if bun_path else None
  174. def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]):
  175. """Update the contents of a json file.
  176. Args:
  177. file_path: the path to the JSON file.
  178. update_dict: object to update json.
  179. """
  180. fp = Path(file_path)
  181. # Create the parent directory if it doesn't exist.
  182. fp.parent.mkdir(parents=True, exist_ok=True)
  183. # Create the file if it doesn't exist.
  184. fp.touch(exist_ok=True)
  185. # Create an empty json object if file is empty
  186. fp.write_text("{}") if fp.stat().st_size == 0 else None
  187. # Read the existing json object from the file.
  188. json_object = {}
  189. if fp.stat().st_size:
  190. with fp.open() as f:
  191. json_object = json.load(f)
  192. # Update the json object with the new data.
  193. json_object.update(update_dict)
  194. # Write the updated json object to the file
  195. with fp.open("w") as f:
  196. json.dump(json_object, f, ensure_ascii=False)
  197. def find_replace(directory: str | Path, find: str, replace: str):
  198. """Recursively find and replace text in files in a directory.
  199. Args:
  200. directory: The directory to search.
  201. find: The text to find.
  202. replace: The text to replace.
  203. """
  204. directory = Path(directory)
  205. for root, _dirs, files in os.walk(directory):
  206. for file in files:
  207. filepath = Path(root, file)
  208. text = filepath.read_text(encoding="utf-8")
  209. text = re.sub(find, replace, text)
  210. filepath.write_text(text, encoding="utf-8")
  211. def samefile(file1: Path, file2: Path) -> bool:
  212. """Check if two files are the same.
  213. Args:
  214. file1: The first file.
  215. file2: The second file.
  216. Returns:
  217. Whether the files are the same. If either file does not exist, returns False.
  218. """
  219. if file1.exists() and file2.exists():
  220. return file1.samefile(file2)
  221. return False
  222. def update_directory_tree(src: Path, dest: Path):
  223. """Recursively copies a directory tree from src to dest.
  224. Only copies files if the destination file is missing or modified earlier than the source file.
  225. Args:
  226. src: Source directory
  227. dest: Destination directory
  228. Raises:
  229. ValueError: If the source is not a directory
  230. """
  231. if not src.is_dir():
  232. msg = f"Source {src} is not a directory"
  233. raise ValueError(msg)
  234. # Ensure the destination directory exists
  235. dest.mkdir(parents=True, exist_ok=True)
  236. for item in src.iterdir():
  237. dest_item = dest / item.name
  238. if item.is_dir():
  239. # Recursively copy subdirectories
  240. update_directory_tree(item, dest_item)
  241. elif item.is_file() and (
  242. not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime
  243. ):
  244. # Copy file if it doesn't exist in the destination or is older than the source
  245. shutil.copy2(item, dest_item)