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,32 @@ 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 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 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
+ )
129
143
130
144
if version is None :
131
145
try :
@@ -141,17 +155,17 @@ def run(self) -> Sequence[nodes.Node]:
141
155
f"Version { version } not found in needs import file { correct_need_import_path } "
142
156
)
143
157
144
- needs_config = NeedsSphinxConfig (self .config )
145
158
data = needs_import_list ["versions" ][version ]
146
159
160
+ # TODO this is not exactly NeedsInfoType, because the export removes/adds some keys
161
+ needs_list : dict [str , NeedsInfoType ] = data ["needs" ]
162
+
147
163
if ids := self .options .get ("ids" ):
148
164
id_list = [i .strip () for i in ids .split ("," ) if i .strip ()]
149
- data [ "needs" ] = {
165
+ needs_list = {
150
166
key : data ["needs" ][key ] for key in id_list if key in data ["needs" ]
151
167
}
152
168
153
- # TODO this is not exactly NeedsInfoType, because the export removes/adds some keys
154
- needs_list : dict [str , NeedsInfoType ] = data ["needs" ]
155
169
if schema := data .get ("needs_schema" ):
156
170
# Set defaults from schema
157
171
defaults = {
@@ -160,7 +174,8 @@ def run(self) -> Sequence[nodes.Node]:
160
174
if "default" in value
161
175
}
162
176
needs_list = {
163
- key : {** defaults , ** value } for key , value in needs_list .items ()
177
+ key : {** defaults , ** value } # type: ignore[typeddict-item]
178
+ for key , value in needs_list .items ()
164
179
}
165
180
166
181
# Filter imported needs
@@ -169,7 +184,8 @@ def run(self) -> Sequence[nodes.Node]:
169
184
if filter_string is None :
170
185
needs_list_filtered [key ] = need
171
186
else :
172
- filter_context = need .copy ()
187
+ # we deepcopy here, to ensure that the original data is not modified
188
+ filter_context = deepcopy (need )
173
189
174
190
# Support both ways of addressing the description, as "description" is used in json file, but
175
191
# "content" is the sphinx internal name for this kind of information
@@ -185,7 +201,9 @@ def run(self) -> Sequence[nodes.Node]:
185
201
location = (self .env .docname , self .lineno ),
186
202
)
187
203
188
- needs_list = needs_list_filtered
204
+ # note we need to deepcopy here, as we are going to modify the data,
205
+ # but we want to ensure data referenced from the cache is not modified
206
+ needs_list = deepcopy (needs_list_filtered )
189
207
190
208
# If we need to set an id prefix, we also need to manipulate all used ids in the imported data.
191
209
extra_links = needs_config .extra_links
@@ -283,6 +301,41 @@ def docname(self) -> str:
283
301
return self .env .docname
284
302
285
303
304
+ class _ImportCache :
305
+ """A simple cache for imported needs,
306
+ mapping a (path, mtime) to a dictionary of needs.
307
+ that is thread safe,
308
+ and has a maximum size when adding new items.
309
+ """
310
+
311
+ def __init__ (self ) -> None :
312
+ self ._cache : OrderedDict [tuple [str , float ], dict [str , Any ]] = OrderedDict ()
313
+ self ._need_count = 0
314
+ self ._lock = threading .Lock ()
315
+
316
+ def set (
317
+ self , path : str , mtime : float , value : dict [str , Any ], max_size : int
318
+ ) -> None :
319
+ with self ._lock :
320
+ self ._cache [(path , mtime )] = value
321
+ self ._need_count += len (value )
322
+ max_size = max (max_size , 0 )
323
+ while self ._need_count > max_size :
324
+ _ , value = self ._cache .popitem (last = False )
325
+ self ._need_count -= len (value )
326
+
327
+ def get (self , path : str , mtime : float ) -> dict [str , Any ] | None :
328
+ with self ._lock :
329
+ return self ._cache .get ((path , mtime ), None )
330
+
331
+ def __repr__ (self ) -> str :
332
+ with self ._lock :
333
+ return f"{ self .__class__ .__name__ } ({ list (self ._cache )} )"
334
+
335
+
336
+ _FileCache = _ImportCache ()
337
+
338
+
286
339
class VersionNotFound (BaseException ):
287
340
pass
288
341
0 commit comments