3
3
import json
4
4
import os
5
5
import re
6
- from typing import Sequence
6
+ import threading
7
+ from copy import deepcopy
8
+ from typing import Any , OrderedDict , Sequence
7
9
from urllib .parse import urlparse
8
10
9
11
import requests
@@ -52,7 +54,8 @@ class NeedimportDirective(SphinxDirective):
52
54
53
55
@measure_time ("needimport" )
54
56
def run (self ) -> Sequence [nodes .Node ]:
55
- # needs_list = {}
57
+ needs_config = NeedsSphinxConfig (self .config )
58
+
56
59
version = self .options .get ("version" )
57
60
filter_string = self .options .get ("filter" )
58
61
id_prefix = self .options .get ("id_prefix" , "" )
@@ -111,21 +114,34 @@ def run(self) -> Sequence[nodes.Node]:
111
114
raise ReferenceError (
112
115
f"Could not load needs import file { correct_need_import_path } "
113
116
)
117
+ mtime = os .path .getmtime (correct_need_import_path )
114
118
115
- try :
116
- with open (correct_need_import_path ) as needs_file :
117
- needs_import_list = json .load (needs_file )
118
- except (OSError , json .JSONDecodeError ) as e :
119
- # TODO: Add exception handling
120
- raise SphinxNeedsFileException (correct_need_import_path ) from e
121
-
122
- errors = check_needs_data (needs_import_list )
123
- if errors .schema :
124
- logger .info (
125
- f"Schema validation errors detected in file { correct_need_import_path } :"
126
- )
127
- for error in errors .schema :
128
- logger .info (f' { error .message } -> { "." .join (error .path )} ' )
119
+ if (
120
+ needs_import_list := _FileCache .get (correct_need_import_path , mtime )
121
+ ) is None :
122
+ try :
123
+ with open (correct_need_import_path ) as needs_file :
124
+ needs_import_list = json .load (needs_file )
125
+ except (OSError , json .JSONDecodeError ) as e :
126
+ # TODO: Add exception handling
127
+ raise SphinxNeedsFileException (correct_need_import_path ) from e
128
+
129
+ errors = check_needs_data (needs_import_list )
130
+ if errors .schema :
131
+ logger .info (
132
+ f"Schema validation errors detected in file { correct_need_import_path } :"
133
+ )
134
+ for error in errors .schema :
135
+ logger .info (f' { error .message } -> { "." .join (error .path )} ' )
136
+ else :
137
+ _FileCache .set (
138
+ correct_need_import_path ,
139
+ mtime ,
140
+ needs_import_list ,
141
+ needs_config .import_cache_size ,
142
+ )
143
+
144
+ self .env .note_dependency (correct_need_import_path )
129
145
130
146
if version is None :
131
147
try :
@@ -141,17 +157,17 @@ def run(self) -> Sequence[nodes.Node]:
141
157
f"Version { version } not found in needs import file { correct_need_import_path } "
142
158
)
143
159
144
- needs_config = NeedsSphinxConfig (self .config )
145
160
data = needs_import_list ["versions" ][version ]
146
161
162
+ # TODO this is not exactly NeedsInfoType, because the export removes/adds some keys
163
+ needs_list : dict [str , NeedsInfoType ] = data ["needs" ]
164
+
147
165
if ids := self .options .get ("ids" ):
148
166
id_list = [i .strip () for i in ids .split ("," ) if i .strip ()]
149
- data [ "needs" ] = {
167
+ needs_list = {
150
168
key : data ["needs" ][key ] for key in id_list if key in data ["needs" ]
151
169
}
152
170
153
- # TODO this is not exactly NeedsInfoType, because the export removes/adds some keys
154
- needs_list : dict [str , NeedsInfoType ] = data ["needs" ]
155
171
if schema := data .get ("needs_schema" ):
156
172
# Set defaults from schema
157
173
defaults = {
@@ -160,7 +176,8 @@ def run(self) -> Sequence[nodes.Node]:
160
176
if "default" in value
161
177
}
162
178
needs_list = {
163
- key : {** defaults , ** value } for key , value in needs_list .items ()
179
+ key : {** defaults , ** value } # type: ignore[typeddict-item]
180
+ for key , value in needs_list .items ()
164
181
}
165
182
166
183
# Filter imported needs
@@ -169,7 +186,8 @@ def run(self) -> Sequence[nodes.Node]:
169
186
if filter_string is None :
170
187
needs_list_filtered [key ] = need
171
188
else :
172
- filter_context = need .copy ()
189
+ # we deepcopy here, to ensure that the original data is not modified
190
+ filter_context = deepcopy (need )
173
191
174
192
# Support both ways of addressing the description, as "description" is used in json file, but
175
193
# "content" is the sphinx internal name for this kind of information
@@ -185,7 +203,9 @@ def run(self) -> Sequence[nodes.Node]:
185
203
location = (self .env .docname , self .lineno ),
186
204
)
187
205
188
- needs_list = needs_list_filtered
206
+ # note we need to deepcopy here, as we are going to modify the data,
207
+ # but we want to ensure data referenced from the cache is not modified
208
+ needs_list = deepcopy (needs_list_filtered )
189
209
190
210
# tags update
191
211
if tags := [
@@ -265,6 +285,41 @@ def docname(self) -> str:
265
285
return self .env .docname
266
286
267
287
288
+ class _ImportCache :
289
+ """A simple cache for imported needs,
290
+ mapping a (path, mtime) to a dictionary of needs.
291
+ that is thread safe,
292
+ and has a maximum size when adding new items.
293
+ """
294
+
295
+ def __init__ (self ) -> None :
296
+ self ._cache : OrderedDict [tuple [str , float ], dict [str , Any ]] = OrderedDict ()
297
+ self ._need_count = 0
298
+ self ._lock = threading .Lock ()
299
+
300
+ def set (
301
+ self , path : str , mtime : float , value : dict [str , Any ], max_size : int
302
+ ) -> None :
303
+ with self ._lock :
304
+ self ._cache [(path , mtime )] = value
305
+ self ._need_count += len (value )
306
+ max_size = max (max_size , 0 )
307
+ while self ._need_count > max_size :
308
+ _ , value = self ._cache .popitem (last = False )
309
+ self ._need_count -= len (value )
310
+
311
+ def get (self , path : str , mtime : float ) -> dict [str , Any ] | None :
312
+ with self ._lock :
313
+ return self ._cache .get ((path , mtime ), None )
314
+
315
+ def __repr__ (self ) -> str :
316
+ with self ._lock :
317
+ return f"{ self .__class__ .__name__ } ({ list (self ._cache )} )"
318
+
319
+
320
+ _FileCache = _ImportCache ()
321
+
322
+
268
323
class VersionNotFound (BaseException ):
269
324
pass
270
325
0 commit comments