Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/fastjsonschema/ref_resolver.py: 23%

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

116 statements  

1# pylint: disable=import-outside-toplevel 

2 

3""" 

4JSON Schema URI resolution scopes and dereferencing 

5 

6https://tools.ietf.org/id/draft-zyp-json-schema-04.html#rfc.section.7 

7 

8Code adapted from https://github.com/Julian/jsonschema 

9""" 

10 

11import contextlib 

12import json 

13import re 

14import sys 

15from collections.abc import Callable, Iterator, Mapping 

16from typing import Any 

17from urllib import parse as urlparse 

18from urllib.parse import unquote 

19 

20from .exceptions import JsonSchemaDefinitionException 

21 

22MAX_SCHEMA_WALK_DEPTH = min(500, sys.getrecursionlimit() // 2) 

23 

24 

25def get_id(schema: dict[str, Any]) -> str: 

26 """ 

27 Originally ID was `id` and since v7 it's `$id`. 

28 """ 

29 return schema.get('$id', schema.get('id', '')) 

30 

31 

32def resolve_path(schema: Any, fragment: str) -> Any: 

33 """ 

34 Return definition from path. 

35 

36 Path is unescaped according https://tools.ietf.org/html/rfc6901 

37 """ 

38 fragment = fragment.lstrip('/') 

39 parts = unquote(fragment).split('/') if fragment else [] 

40 for part in parts: 

41 part = part.replace('~1', '/').replace('~0', '~') 

42 if isinstance(schema, list): 

43 schema = schema[int(part)] 

44 elif part in schema: 

45 schema = schema[part] 

46 else: 

47 raise JsonSchemaDefinitionException('Unresolvable ref: {}'.format(part)) 

48 return schema 

49 

50 

51def normalize(uri: str) -> str: 

52 return urlparse.urlsplit(uri).geturl() 

53 

54 

55def resolve_remote(uri: str, handlers: Mapping[str, Callable[[str], Any]]) -> Any: 

56 """ 

57 Resolve a remote ``uri``. 

58 

59 .. note:: 

60 

61 urllib library is used to fetch requests from the remote ``uri`` 

62 if handlers does notdefine otherwise. 

63 """ 

64 scheme = urlparse.urlsplit(uri).scheme 

65 if scheme in handlers: 

66 result = handlers[scheme](uri) 

67 else: 

68 from urllib.request import urlopen 

69 

70 with urlopen(uri) as response: 

71 encoding = response.info().get_content_charset() or 'utf-8' 

72 try: 

73 result = json.loads(response.read().decode(encoding),) 

74 except ValueError as exc: 

75 raise JsonSchemaDefinitionException('{} failed to decode'.format(uri)) from exc 

76 return result 

77 

78 

79class RefResolver: 

80 """ 

81 Resolve JSON References. 

82 """ 

83 

84 # pylint: disable=dangerous-default-value,too-many-arguments 

85 def __init__( 

86 self, 

87 base_uri: str, 

88 schema: dict[str, Any] | bool, 

89 store: dict[str, Any] = {}, 

90 cache: bool = True, 

91 handlers: Mapping[str, Callable[[str], Any]] = {}, 

92 ) -> None: 

93 """ 

94 `base_uri` is URI of the referring document from the `schema`. 

95 `store` is an dictionary that will be used to cache the fetched schemas 

96 (if `cache=True`). 

97 

98 Please notice that you can have caching problems when compiling schemas 

99 with colliding `$ref`. To force overwriting use `cache=False` or 

100 explicitly pass the `store` argument (with a brand new dictionary) 

101 """ 

102 self.base_uri = base_uri 

103 self.resolution_scope = base_uri 

104 self.schema = schema 

105 self.store = store 

106 self.cache = cache 

107 self.handlers = handlers 

108 self._walked_uris: set[str] = set() 

109 self.walk(schema) 

110 self._walked_uris.add(normalize(base_uri) if base_uri else '') 

111 

112 @classmethod 

113 def from_schema( 

114 cls, 

115 schema: dict[str, Any] | bool, 

116 handlers: Mapping[str, Callable[[str], Any]] = {}, 

117 **kwargs: Any, 

118 ) -> 'RefResolver': 

119 """ 

120 Construct a resolver from a JSON schema object. 

121 """ 

122 return cls( 

123 get_id(schema) if isinstance(schema, dict) else '', 

124 schema, 

125 handlers=handlers, 

126 **kwargs 

127 ) 

128 

129 @contextlib.contextmanager 

130 def in_scope(self, scope: str) -> Iterator[None]: 

131 """ 

132 Context manager to handle current scope. 

133 """ 

134 old_scope = self.resolution_scope 

135 self.resolution_scope = urlparse.urljoin(old_scope, scope) 

136 try: 

137 yield 

138 finally: 

139 self.resolution_scope = old_scope 

140 

141 @contextlib.contextmanager 

142 def resolving(self, ref: str) -> Iterator[Any]: 

143 """ 

144 Context manager which resolves a JSON ``ref`` and enters the 

145 resolution scope of this ref. 

146 """ 

147 new_uri = urlparse.urljoin(self.resolution_scope, ref) 

148 uri, fragment = urlparse.urldefrag(new_uri) 

149 

150 document_uri = uri or self.base_uri 

151 

152 if uri and normalize(uri) in self.store: 

153 schema = self.store[normalize(uri)] 

154 elif not uri or uri == self.base_uri: 

155 schema = self.schema 

156 else: 

157 schema = resolve_remote(uri, self.handlers) 

158 if self.cache: 

159 self.store[normalize(uri)] = schema 

160 

161 old_base_uri, old_schema = self.base_uri, self.schema 

162 self.base_uri, self.schema = document_uri, schema 

163 try: 

164 with self.in_scope(document_uri): 

165 self._ensure_walked(document_uri, schema) 

166 if fragment and not fragment.startswith('/'): 

167 plain_name = normalize(urlparse.urljoin(document_uri, '#' + fragment)) 

168 if plain_name in self.store: 

169 yield self.store[plain_name] 

170 return 

171 raise JsonSchemaDefinitionException('Unresolvable ref: {}'.format(fragment)) 

172 yield resolve_path(schema, fragment) 

173 finally: 

174 self.base_uri, self.schema = old_base_uri, old_schema 

175 

176 def _ensure_walked(self, uri: str, schema: Any) -> None: 

177 normalized = normalize(uri) if uri else '' 

178 if normalized in self._walked_uris: 

179 return 

180 self.walk(schema, rewrite_refs=False) 

181 self._walked_uris.add(normalized) 

182 

183 def get_uri(self) -> str: 

184 return normalize(self.resolution_scope) 

185 

186 def get_scope_name(self) -> str: 

187 """ 

188 Get current scope and return it as a valid function name. 

189 """ 

190 name = 'validate_' + unquote(self.resolution_scope).replace('~1', '_').replace('~0', '_').replace('"', '') 

191 name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '_', name) 

192 name = name.lower().rstrip('_') 

193 return name 

194 

195 def walk(self, node: dict[str, Any] | bool, depth: int = 0, rewrite_refs: bool = True) -> None: 

196 """ 

197 Walk thru schema and dereferencing ``id`` and ``$ref`` instances 

198 """ 

199 if depth >= MAX_SCHEMA_WALK_DEPTH: 

200 raise JsonSchemaDefinitionException( 

201 'Schema is too deeply nested (maximum depth is {})'.format(MAX_SCHEMA_WALK_DEPTH) 

202 ) 

203 

204 if isinstance(node, bool): 

205 pass 

206 elif '$ref' in node and isinstance(node['$ref'], str): 

207 if rewrite_refs: 

208 ref = node['$ref'] 

209 node['$ref'] = urlparse.urljoin(self.resolution_scope, ref) 

210 elif ('$id' in node or 'id' in node) and isinstance(get_id(node), str): 

211 with self.in_scope(get_id(node)): 

212 self.store[normalize(self.resolution_scope)] = node 

213 for _, item in node.items(): 

214 if isinstance(item, dict): 

215 self.walk(item, depth + 1, rewrite_refs) 

216 else: 

217 for _, item in node.items(): 

218 if isinstance(item, dict): 

219 self.walk(item, depth + 1, rewrite_refs)