Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/libcst/_parser/types/config.py: 70%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

71 statements  

1# Copyright (c) Meta Platforms, Inc. and affiliates. 

2# 

3# This source code is licensed under the MIT license found in the 

4# LICENSE file in the root directory of this source tree. 

5 

6import codecs 

7import re 

8import sys 

9from dataclasses import dataclass, field, fields 

10from enum import Enum 

11from typing import Any, Callable, FrozenSet, List, Mapping, Optional, Pattern, Union 

12 

13from libcst._add_slots import add_slots 

14from libcst._nodes.whitespace import NEWLINE_RE 

15from libcst._parser.parso.utils import parse_version_string, PythonVersionInfo 

16 

17_INDENT_RE: Pattern[str] = re.compile(r"[ \t]+") 

18 

19try: 

20 from libcst_native import parser_config as config_mod 

21 

22 MockWhitespaceParserConfig = config_mod.BaseWhitespaceParserConfig 

23except ImportError: 

24 from libcst._parser.types import py_config as config_mod 

25 

26 MockWhitespaceParserConfig = config_mod.MockWhitespaceParserConfig 

27 

28BaseWhitespaceParserConfig = config_mod.BaseWhitespaceParserConfig 

29ParserConfig = config_mod.ParserConfig 

30parser_config_asdict: Callable[[ParserConfig], Mapping[str, Any]] = ( 

31 config_mod.parser_config_asdict 

32) 

33 

34 

35class AutoConfig(Enum): 

36 """ 

37 A sentinel value used in PartialParserConfig 

38 """ 

39 

40 token: int = 0 

41 

42 def __repr__(self) -> str: 

43 return str(self) 

44 

45 

46# This list should be kept in sorted order. 

47KNOWN_PYTHON_VERSION_STRINGS = [ 

48 "3.0", 

49 "3.1", 

50 "3.3", 

51 "3.5", 

52 "3.6", 

53 "3.7", 

54 "3.8", 

55 "3.9", 

56 "3.10", 

57 "3.11", 

58 "3.12", 

59 "3.13", 

60 "3.14", 

61 "3.15", 

62] 

63 

64 

65@add_slots 

66@dataclass(frozen=True) 

67class PartialParserConfig: 

68 r""" 

69 An optional object that can be supplied to the parser entrypoints (e.g. 

70 :func:`parse_module`) to configure the parser. 

71 

72 Unspecified fields will be inferred from the input source code or from the execution 

73 environment. 

74 

75 >>> import libcst as cst 

76 >>> tree = cst.parse_module("abc") 

77 >>> tree.bytes 

78 b'abc' 

79 >>> # override the default utf-8 encoding 

80 ... tree = cst.parse_module("abc", cst.PartialParserConfig(encoding="utf-32")) 

81 >>> tree.bytes 

82 b'\xff\xfe\x00\x00a\x00\x00\x00b\x00\x00\x00c\x00\x00\x00' 

83 """ 

84 

85 #: The version of Python that the input source code is expected to be syntactically 

86 #: compatible with. This may be different from the Python interpreter being used to 

87 #: run LibCST. For example, you can parse code as 3.7 with a CPython 3.6 

88 #: interpreter. 

89 #: 

90 #: If unspecified, it will default to the syntax of the running interpreter 

91 #: (rounding down from among the following list). 

92 #: 

93 #: Currently, only Python 3.0, 3.1, 3.3, and Python 3.5 through 3.15 

94 #: syntax is supported. 

95 #: The gaps did not have any syntax changes from the version prior. 

96 python_version: Union[str, AutoConfig] = AutoConfig.token 

97 

98 #: A named tuple with the ``major`` and ``minor`` Python version numbers. This is 

99 #: derived from :attr:`python_version` and should not be supplied to the 

100 #: :class:`PartialParserConfig` constructor. 

101 parsed_python_version: PythonVersionInfo = field(init=False) 

102 

103 #: The file's encoding format. When parsing a ``bytes`` object, this value may be 

104 #: inferred from the contents of the parsed source code. When parsing a ``str``, 

105 #: this value defaults to ``"utf-8"``. 

106 encoding: Union[str, AutoConfig] = AutoConfig.token 

107 

108 #: Detected ``__future__`` import names 

109 future_imports: Union[FrozenSet[str], AutoConfig] = AutoConfig.token 

110 

111 #: The indentation of the file, expressed as a series of tabs and/or spaces. This 

112 #: value is inferred from the contents of the parsed source code by default. 

113 default_indent: Union[str, AutoConfig] = AutoConfig.token 

114 

115 #: The newline of the file, expressed as ``\n``, ``\r\n``, or ``\r``. This value is 

116 #: inferred from the contents of the parsed source code by default. 

117 default_newline: Union[str, AutoConfig] = AutoConfig.token 

118 

119 def __post_init__(self) -> None: 

120 raw_python_version = self.python_version 

121 

122 if isinstance(raw_python_version, AutoConfig): 

123 # If unspecified, we'll try to pick the same as the running 

124 # interpreter. There will always be at least one entry. 

125 parsed_python_version = _pick_compatible_python_version() 

126 else: 

127 # If the caller specified a version, we require that to be a known 

128 # version (because we don't want to encourage doing duplicate work 

129 # when there weren't syntax changes). 

130 

131 # `parse_version_string` will raise a ValueError if the version is 

132 # invalid. 

133 parsed_python_version = parse_version_string(raw_python_version) 

134 

135 if not any( 

136 parsed_python_version == parse_version_string(v) 

137 for v in KNOWN_PYTHON_VERSION_STRINGS 

138 ): 

139 comma_versions = ", ".join(KNOWN_PYTHON_VERSION_STRINGS) 

140 raise ValueError( 

141 "LibCST can only parse code using one of the following versions of " 

142 + f"Python's grammar: {comma_versions}. More versions may be " 

143 + "supported by future releases." 

144 ) 

145 

146 # We use object.__setattr__ because the dataclass is frozen. See: 

147 # https://docs.python.org/3/library/dataclasses.html#frozen-instances 

148 # This should be safe behavior inside of `__post_init__`. 

149 object.__setattr__(self, "parsed_python_version", parsed_python_version) 

150 

151 encoding = self.encoding 

152 if not isinstance(encoding, AutoConfig): 

153 try: 

154 codecs.lookup(encoding) 

155 except LookupError: 

156 raise ValueError(f"{repr(encoding)} is not a supported encoding") 

157 

158 newline = self.default_newline 

159 if ( 

160 not isinstance(newline, AutoConfig) 

161 and NEWLINE_RE.fullmatch(newline) is None 

162 ): 

163 raise ValueError( 

164 f"Got an invalid value for default_newline: {repr(newline)}" 

165 ) 

166 

167 indent = self.default_indent 

168 if not isinstance(indent, AutoConfig) and _INDENT_RE.fullmatch(indent) is None: 

169 raise ValueError(f"Got an invalid value for default_indent: {repr(indent)}") 

170 

171 def __repr__(self) -> str: 

172 init_keys: List[str] = [] 

173 

174 for f in fields(self): 

175 # We don't display the parsed_python_version attribute because it contains 

176 # the same value as python_version, only parsed. 

177 if f.name == "parsed_python_version": 

178 continue 

179 value = getattr(self, f.name) 

180 if not isinstance(value, AutoConfig): 

181 init_keys.append(f"{f.name}={value!r}") 

182 

183 return f"{self.__class__.__name__}({', '.join(init_keys)})" 

184 

185 

186def _pick_compatible_python_version(version: Optional[str] = None) -> PythonVersionInfo: 

187 max_version = parse_version_string(version) 

188 for v in KNOWN_PYTHON_VERSION_STRINGS[::-1]: 

189 tmp = parse_version_string(v) 

190 if tmp <= max_version: 

191 return tmp 

192 

193 raise ValueError( 

194 f"No version found older than {version} ({max_version}) while " 

195 + f"running on {sys.version_info}" 

196 )