Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of the lib3to6 project 

2# https://github.com/mbarkhau/lib3to6 

3# 

4# Copyright (c) 2019-2021 Manuel Barkhau (mbarkhau@gmail.com) - MIT License 

5# SPDX-License-Identifier: MIT 

6 

7import os 

8import re 

9import sys 

10import shutil 

11import typing as typ 

12import hashlib 

13import tempfile 

14import warnings 

15 

16import pathlib2 as pl 

17import setuptools.dist 

18import setuptools.command.build_py as _build_py 

19 

20from . import common 

21from . import transpile 

22 

23ENV_PATH = str(pl.Path(sys.executable).parent.parent) 

24 

25 

26PYTHON_TAG_PREFIXES = { 

27 'py': "Generic Python", 

28 'cp': "CPython", 

29 'ip': "IronPython", 

30 'pp': "PyPy", 

31 'jy': "Jython", 

32} 

33 

34 

35CACHE_DIR = pl.Path(tempfile.gettempdir()) / ".lib3to6_cache" 

36 

37 

38def eval_build_config(**kwargs) -> common.BuildConfig: 

39 target_version = kwargs.get('target_version', transpile.DEFAULT_TARGET_VERSION) 

40 _install_requires = kwargs.get('install_requires', None) 

41 cache_enabled = kwargs.get('cache_enabled', True) 

42 default_mode = kwargs.get('default_mode', 'enabled') 

43 

44 install_requires: common.InstallRequires 

45 if _install_requires is None: 

46 install_requires = None 

47 elif isinstance(_install_requires, str): 

48 install_requires = set(_install_requires.split()) 

49 elif isinstance(_install_requires, list): 

50 install_requires = set(_install_requires) 

51 else: 

52 raise TypeError(f"Invalid argument for install_requires: {type(_install_requires)}") 

53 

54 if install_requires: 

55 # Remove version specs. We only handle the bare requirement 

56 # and assume the maintainer knows what they're doing wrt. 

57 # the appropriate versions. 

58 install_requires = {re.split(r"[\^\~<>=;]", req.strip())[0] for req in install_requires} 

59 

60 return common.BuildConfig( 

61 target_version=target_version, 

62 cache_enabled=cache_enabled, 

63 default_mode=default_mode, 

64 fixers="", 

65 checkers="", 

66 install_requires=install_requires, 

67 ) 

68 

69 

70def _ignore_tmp_files(src: str, names: typ.List[str]) -> typ.List[str]: 

71 if isinstance(src, str): 

72 src_str = src 

73 else: 

74 # https://bugs.python.org/issue39390 

75 if isinstance(src, os.DirEntry): 

76 src_str = src.path 

77 else: 

78 src_str = str(src) 

79 

80 if src_str.startswith("build") or src_str.startswith("./build"): 

81 return names 

82 if src_str.endswith(".egg-info"): 

83 return names 

84 if src_str.endswith("dist"): 

85 return names 

86 if src_str.endswith("__pycache__"): 

87 return names 

88 

89 return [name for name in names if name.endswith(".pyc")] 

90 

91 

92def init_build_package_dir(local_package_dir: common.PackageDir) -> common.PackageDir: 

93 output_dir = pl.Path("build") / "lib3to6_out" 

94 output_dir.mkdir(parents=True, exist_ok=True) 

95 

96 build_package_dir: common.PackageDir = {} 

97 

98 for package, src_package_dir in local_package_dir.items(): 

99 is_abs_path = pl.Path(src_package_dir) == pl.Path(src_package_dir).absolute() 

100 if is_abs_path: 

101 raise Exception(f"package_dir must use relative paths, got '{src_package_dir}'") 

102 

103 build_package_subdir = output_dir / src_package_dir 

104 

105 # TODO (mb 2018-08-25): As an optimization, we could 

106 # restrict deletion to files that we manipulate, in 

107 # other words, to *.py files. 

108 if build_package_subdir.exists(): 

109 shutil.rmtree(build_package_subdir) 

110 

111 shutil.copytree(src_package_dir, str(build_package_subdir), ignore=_ignore_tmp_files) 

112 build_package_dir[package] = str(build_package_subdir) 

113 

114 return build_package_dir 

115 

116 

117def transpile_path(cfg: common.BuildConfig, filepath: pl.Path) -> pl.Path: 

118 with open(filepath, mode="rb") as fobj: 

119 module_source_data = fobj.read() 

120 

121 filehash = hashlib.sha1() 

122 filehash.update(str(cfg).encode("utf-8")) 

123 filehash.update(module_source_data) 

124 

125 cache_path = CACHE_DIR / (filehash.hexdigest() + ".py") 

126 

127 if cfg.cache_enabled and cache_path.exists(): 

128 return cache_path 

129 

130 # NOTE (mb 2020-09-01): not cache_enabled -> always update cache 

131 ctx = common.BuildContext(cfg, str(filepath)) 

132 try: 

133 fixed_module_source_data = transpile.transpile_module_data(ctx, module_source_data) 

134 except common.CheckError as err: 

135 loc = str(filepath) 

136 if err.lineno >= 0: 

137 loc += "@" + str(err.lineno) 

138 

139 err.args = (loc + " - " + err.args[0],) + err.args[1:] 

140 raise 

141 

142 with open(cache_path, mode="wb") as fobj: 

143 fobj.write(fixed_module_source_data) 

144 

145 return cache_path 

146 

147 

148def build_package(cfg: common.BuildConfig, package: str, build_dir: str) -> None: 

149 # pylint:disable=unused-argument ; `package` is part of the public api now 

150 for root, _dirs, files in os.walk(build_dir): 

151 for filename in files: 

152 filepath = pl.Path(root) / filename 

153 if filepath.suffix == ".py": 

154 transpiled_path = transpile_path(cfg, filepath) 

155 # overwrite original with transpiled 

156 shutil.copy(transpiled_path, filepath) 

157 

158 

159def build_packages(cfg: common.BuildConfig, build_package_dir: common.PackageDir) -> None: 

160 CACHE_DIR.mkdir(exist_ok=True) 

161 

162 for package, build_dir in build_package_dir.items(): 

163 build_package(cfg, package, build_dir) 

164 

165 

166def fix( 

167 package_dir : common.PackageDir = None, 

168 target_version : str = transpile.DEFAULT_TARGET_VERSION, 

169 install_requires: typ.List[str] = None, 

170 default_mode : str = 'enabled', 

171) -> common.PackageDir: 

172 msg = "Depricated: lib3to6.fix(). See https://github.com/mbarkhau/lib3to6#Deprications" 

173 warnings.warn(msg, DeprecationWarning) 

174 

175 if package_dir is None: 

176 package_dir = {"": "."} 

177 

178 build_package_dir = init_build_package_dir(package_dir) 

179 build_cfg = eval_build_config( 

180 target_version=target_version, 

181 install_requires=install_requires, 

182 default_mode=default_mode, 

183 ) 

184 build_packages(build_cfg, build_package_dir) 

185 return build_package_dir 

186 

187 

188class build_py(_build_py.build_py): 

189 # pylint: disable=invalid-name ; following the convention of setuptools 

190 

191 def _get_outputs(self) -> typ.List[str]: 

192 outputs = _build_py.orig.build_py.get_outputs(self, include_bytecode=0) # type: ignore[attr-defined] 

193 return typ.cast(typ.List[str], outputs) 

194 

195 def run_3to6(self) -> None: 

196 outputs = self._get_outputs() 

197 dist = self.distribution 

198 pyreq = dist.python_requires 

199 

200 preq_match = isinstance(pyreq, str) and re.match(r">=([0-9]+\.[0-9]+)", pyreq) 

201 if preq_match: 

202 target_version = preq_match.group(1) 

203 else: 

204 raise ValueError('lib3to6: missing python_requires=">=X.Y" in setup.py') 

205 

206 # pylint: disable=protected-access 

207 install_requires = sorted(dist._lib3to6_install_requires) 

208 build_cfg = eval_build_config( 

209 target_version=target_version, 

210 install_requires=install_requires, 

211 default_mode=getattr(dist, 'lib3to6_default_mode', 'enabled'), 

212 ) 

213 

214 CACHE_DIR.mkdir(exist_ok=True) 

215 for output in outputs: 

216 if output.endswith(".py"): 

217 transpiled_path = transpile_path(build_cfg, pl.Path(output)) 

218 shutil.copy(transpiled_path, output) 

219 

220 def run(self) -> None: 

221 """Build modules, packages, and copy data files to build directory""" 

222 if not self.py_modules and not self.packages: 

223 return 

224 

225 if self.py_modules: 

226 self.build_modules() 

227 

228 if self.packages: 

229 self.build_packages() 

230 self.build_package_data() 

231 

232 if hasattr(self, 'run_2to3'): 

233 self.run_2to3(self.__updated_files, False) 

234 self.run_2to3(self.__updated_files, True) 

235 self.run_2to3(self.__doctests_2to3, True) 

236 

237 self.run_3to6() 

238 

239 # Only compile actual .py files, using our base class' idea of what our 

240 # output files are. 

241 self.byte_compile(self._get_outputs()) 

242 

243 

244class Distribution(setuptools.dist.Distribution): 

245 def __init__(self, attrs=None) -> None: 

246 # NOTE (mb 2021-08-20): Distutils removes all requirements 

247 # that are not needed for the current python version. We 

248 # need the original requirements for validation, so we 

249 # capture them here. 

250 self._lib3to6_install_requires = attrs.get('install_requires') 

251 super().__init__(attrs) 

252 

253 def get_command_class(self, command: str) -> typ.Any: 

254 if command in self.cmdclass: 

255 return self.cmdclass[command] 

256 elif command == 'build_py': 

257 self.cmdclass[command] = build_py 

258 return build_py 

259 else: 

260 return super().get_command_class(command) # type: ignore[no-untyped-call]