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 

6import ast 

7import typing as typ 

8import logging 

9 

10from . import common 

11from . import checker_base as cb 

12 

13# TODO (mb 2020-05-28): 

14# instead of functools.singledispatch 

15# from singledispatch import singledispatch 

16# https://pypi.org/project/singledispatch/ 

17# 

18# instead of functools.lru_cache 

19# from backports import functools_lru_cache 

20# https://pypi.org/project/backports.functools-lru-cache/ 

21# 

22 

23logger = logging.getLogger(__name__) 

24 

25 

26class ModuleVersionInfo(typ.NamedTuple): 

27 

28 available_since : str 

29 backport_module : typ.Optional[str] 

30 backport_package: typ.Optional[str] 

31 

32 

33def parse_version(version: str) -> typ.Any: 

34 # pylint: disable=import-outside-toplevel; lazy import to speed up --help 

35 import pkg_resources 

36 

37 return pkg_resources.parse_version(version) 

38 

39 

40MAYBE_UNUSABLE_MODULES = { 

41 # case 1 (simple). Always error because no backport available 

42 'asyncio': ModuleVersionInfo("3.4", None, None), 

43 'zipapp' : ModuleVersionInfo("3.5", None, None), 

44 # case 2 (simple). Always error because modules have different names and only 

45 # backport should be used 

46 'csv' : ModuleVersionInfo("3.0", "backports.csv" , "backports.csv"), 

47 'selectors' : ModuleVersionInfo("3.4", "selectors2" , "selectors2"), 

48 'pathlib' : ModuleVersionInfo("3.4", "pathlib2" , "pathlib2"), 

49 "importlib.resources": ModuleVersionInfo("3.7", "importlib_resources", "importlib_resources"), 

50 'inspect' : ModuleVersionInfo("3.6", "inspect2" , "inspect2"), 

51 # case 3 (hard). Modules have the same name. 

52 # - By default, only logger.warning if these are modules are imported. 

53 # - If opt-in via '--install-requires' option or 

54 # 'install_requires' argument of 'lib3to6.fix', check that they 

55 # have been explicitly whitelisted. 

56 'lzma' : ModuleVersionInfo("3.3", "lzma" , "backports.lzma"), 

57 'ipaddress' : ModuleVersionInfo("3.4", "ipaddress" , "py2-ipaddress"), 

58 'enum' : ModuleVersionInfo("3.4", "enum" , "enum34"), 

59 'typing' : ModuleVersionInfo("3.5", 'typing' , 'typing'), 

60 'secrets' : ModuleVersionInfo("3.6", "secrets" , "python2-secrets"), 

61 'statistics' : ModuleVersionInfo("3.4", "statistics" , "statistics"), 

62 'dataclasses': ModuleVersionInfo("3.7", "dataclasses", "dataclasses"), 

63 'contextvars': ModuleVersionInfo("3.7", "contextvars", "contextvars"), 

64} 

65 

66 

67def _iter_module_names(node: ast.AST) -> typ.Iterable[str]: 

68 if isinstance(node, ast.Import): 

69 for alias in node.names: 

70 yield alias.name 

71 elif isinstance(node, ast.ImportFrom): 

72 mname = node.module 

73 if mname: 

74 yield mname 

75 

76 

77def _iter_maybe_unusable_modules(node: ast.AST) -> typ.Iterable[typ.Tuple[str, ModuleVersionInfo]]: 

78 for mname in _iter_module_names(node): 

79 vnfo = MAYBE_UNUSABLE_MODULES.get(mname) 

80 if vnfo: 

81 yield (mname, vnfo) 

82 

83 

84class NoUnusableImportsChecker(cb.CheckerBase): 

85 # NOTE (mb 2020-05-28): The (apparent double negation) naming of this does 

86 # make sense; the name "OnlyUsableImportsChecker" would not be better, 

87 # because this doesn't check all imports ever in existence, it only checks 

88 # that unusable imports are not used at the top level. 

89 

90 # NOTE (mb 2020-05-28): This checker only checks top level imports. 

91 # This allows for the common idom to work without errors. 

92 # 

93 # try: 

94 # import newmodule 

95 # except ImportError: 

96 # improt backport_module as newmodule 

97 

98 def __call__(self, ctx: common.BuildContext, tree: ast.Module) -> None: 

99 # NOTE (mb 2020-05-28): 

100 # - Strict mode fails hard 

101 # - Only raises an error if an unusable module is not used 

102 # - Only warns about backported modules after 

103 # opt-in to this check (by using the fix(install_requires) parameter). 

104 # - Existing systems will have to update 

105 # their config for this check to work and we don't want to 

106 # break them. 

107 

108 install_requires: typ.Optional[set[str]] = ctx.cfg.install_requires 

109 

110 target_version = ctx.cfg.target_version 

111 for node in ast.iter_child_nodes(tree): 

112 for mname, vnfo in _iter_maybe_unusable_modules(node): 

113 if parse_version(target_version) >= parse_version(vnfo.available_since): 

114 # target supports the newer name 

115 continue 

116 

117 bppkg = vnfo.backport_package 

118 

119 # if the backport has a different name, then there is no 

120 # excuse not to use it -> hard error 

121 is_backport_name_same = mname == vnfo.backport_module 

122 

123 is_whitelisted = ( 

124 is_backport_name_same 

125 and install_requires is not None 

126 and bppkg in install_requires 

127 ) 

128 if is_whitelisted: 

129 continue 

130 

131 # From here, we either error or at least show a warning. 

132 

133 # if there is no backport, then the import can obviously only 

134 # be using the stdlib module -> hard error 

135 is_backported = vnfo.backport_package is not None 

136 is_strict_mode = install_requires is not None 

137 is_hard_error = not is_backported or is_strict_mode or not is_backport_name_same 

138 

139 vnfo_msg = ( 

140 f"This module is available since Python {vnfo.available_since}, " 

141 f"but you configured target_version='{target_version}'." 

142 ) 

143 

144 if is_hard_error: 

145 errmsg = f"Prohibited import '{mname}'. {vnfo_msg}" 

146 if bppkg: 

147 errmsg += f" Use 'https://pypi.org/project/{bppkg}' instead." 

148 else: 

149 errmsg += " No backported for this package is known." 

150 

151 raise common.CheckError(errmsg, node) 

152 else: 

153 lineno = common.get_node_lineno(node) 

154 loc = f"{ctx.filepath}@{lineno}" 

155 logger.warning(f"{loc}: Use of import '{mname}'. {vnfo_msg}")