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] 

62 

63 

64@add_slots 

65@dataclass(frozen=True) 

66class PartialParserConfig: 

67 r""" 

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

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

70 

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

72 environment. 

73 

74 >>> import libcst as cst 

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

76 >>> tree.bytes 

77 b'abc' 

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

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

80 >>> tree.bytes 

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

82 """ 

83 

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

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

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

87 #: interpreter. 

88 #: 

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

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

91 #: 

92 #: Currently, only Python 3.0, 3.1, 3.3, and Python 3.5 through 3.14 

93 #: syntax is supported. 

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

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

96 

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

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

99 #: :class:`PartialParserConfig` constructor. 

100 parsed_python_version: PythonVersionInfo = field(init=False) 

101 

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

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

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

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

106 

107 #: Detected ``__future__`` import names 

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

109 

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

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

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

113 

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

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

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

117 

118 def __post_init__(self) -> None: 

119 raw_python_version = self.python_version 

120 

121 if isinstance(raw_python_version, AutoConfig): 

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

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

124 parsed_python_version = _pick_compatible_python_version() 

125 else: 

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

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

128 # when there weren't syntax changes). 

129 

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

131 # invalid. 

132 parsed_python_version = parse_version_string(raw_python_version) 

133 

134 if not any( 

135 parsed_python_version == parse_version_string(v) 

136 for v in KNOWN_PYTHON_VERSION_STRINGS 

137 ): 

138 comma_versions = ", ".join(KNOWN_PYTHON_VERSION_STRINGS) 

139 raise ValueError( 

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

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

142 + "supported by future releases." 

143 ) 

144 

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

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

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

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

149 

150 encoding = self.encoding 

151 if not isinstance(encoding, AutoConfig): 

152 try: 

153 codecs.lookup(encoding) 

154 except LookupError: 

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

156 

157 newline = self.default_newline 

158 if ( 

159 not isinstance(newline, AutoConfig) 

160 and NEWLINE_RE.fullmatch(newline) is None 

161 ): 

162 raise ValueError( 

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

164 ) 

165 

166 indent = self.default_indent 

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

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

169 

170 def __repr__(self) -> str: 

171 init_keys: List[str] = [] 

172 

173 for f in fields(self): 

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

175 # the same value as python_version, only parsed. 

176 if f.name == "parsed_python_version": 

177 continue 

178 value = getattr(self, f.name) 

179 if not isinstance(value, AutoConfig): 

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

181 

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

183 

184 

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

186 max_version = parse_version_string(version) 

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

188 tmp = parse_version_string(v) 

189 if tmp <= max_version: 

190 return tmp 

191 

192 raise ValueError( 

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

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

195 )