zeitwerk 2.6.1 → 2.6.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +184 -36
- data/lib/zeitwerk/error.rb +6 -0
- data/lib/zeitwerk/explicit_namespace.rb +14 -10
- data/lib/zeitwerk/gem_loader.rb +1 -1
- data/lib/zeitwerk/internal.rb +12 -0
- data/lib/zeitwerk/kernel.rb +3 -0
- data/lib/zeitwerk/loader/callbacks.rb +10 -10
- data/lib/zeitwerk/loader/config.rb +82 -46
- data/lib/zeitwerk/loader/eager_load.rb +228 -0
- data/lib/zeitwerk/loader/helpers.rb +30 -16
- data/lib/zeitwerk/loader.rb +112 -113
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +1 -0
- metadata +5 -3
data/lib/zeitwerk/loader.rb
CHANGED
@@ -7,11 +7,15 @@ module Zeitwerk
|
|
7
7
|
require_relative "loader/helpers"
|
8
8
|
require_relative "loader/callbacks"
|
9
9
|
require_relative "loader/config"
|
10
|
+
require_relative "loader/eager_load"
|
11
|
+
|
12
|
+
extend Internal
|
10
13
|
|
11
14
|
include RealModName
|
12
15
|
include Callbacks
|
13
16
|
include Helpers
|
14
17
|
include Config
|
18
|
+
include EagerLoad
|
15
19
|
|
16
20
|
MUTEX = Mutex.new
|
17
21
|
private_constant :MUTEX
|
@@ -24,9 +28,9 @@ module Zeitwerk
|
|
24
28
|
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
|
25
29
|
# ...
|
26
30
|
#
|
27
|
-
# @private
|
28
31
|
# @sig Hash[String, [Module, Symbol]]
|
29
32
|
attr_reader :autoloads
|
33
|
+
internal :autoloads
|
30
34
|
|
31
35
|
# We keep track of autoloaded directories to remove them from the registry
|
32
36
|
# at the end of eager loading.
|
@@ -34,9 +38,9 @@ module Zeitwerk
|
|
34
38
|
# Files are removed as they are autoloaded, but directories need to wait due
|
35
39
|
# to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
|
36
40
|
#
|
37
|
-
# @private
|
38
41
|
# @sig Array[String]
|
39
42
|
attr_reader :autoloaded_dirs
|
43
|
+
internal :autoloaded_dirs
|
40
44
|
|
41
45
|
# Stores metadata needed for unloading. Its entries look like this:
|
42
46
|
#
|
@@ -50,11 +54,11 @@ module Zeitwerk
|
|
50
54
|
# If reloading is enabled, this hash is filled as constants are autoloaded
|
51
55
|
# or eager loaded. Otherwise, the collection remains empty.
|
52
56
|
#
|
53
|
-
# @private
|
54
57
|
# @sig Hash[String, [String, [Module, Symbol]]]
|
55
58
|
attr_reader :to_unload
|
59
|
+
internal :to_unload
|
56
60
|
|
57
|
-
# Maps constant paths
|
61
|
+
# Maps namespace constant paths to their respective directories.
|
58
62
|
#
|
59
63
|
# For example, given this mapping:
|
60
64
|
#
|
@@ -64,21 +68,32 @@ module Zeitwerk
|
|
64
68
|
# ...
|
65
69
|
# ]
|
66
70
|
#
|
67
|
-
# when `Admin` gets defined we know that it plays the role of a namespace
|
68
|
-
# that its children are spread over those directories. We'll visit them
|
69
|
-
# up the corresponding autoloads.
|
71
|
+
# when `Admin` gets defined we know that it plays the role of a namespace
|
72
|
+
# and that its children are spread over those directories. We'll visit them
|
73
|
+
# to set up the corresponding autoloads.
|
70
74
|
#
|
71
|
-
# @private
|
72
75
|
# @sig Hash[String, Array[String]]
|
73
|
-
attr_reader :
|
76
|
+
attr_reader :namespace_dirs
|
77
|
+
internal :namespace_dirs
|
78
|
+
|
79
|
+
# A shadowed file is a file managed by this loader that is ignored when
|
80
|
+
# setting autoloads because its matching constant is already taken.
|
81
|
+
#
|
82
|
+
# This private set is populated as we descend. For example, if the loader
|
83
|
+
# has only scanned the top-level, `shadowed_files` does not have shadowed
|
84
|
+
# files that may exist deep in the project tree yet.
|
85
|
+
#
|
86
|
+
# @sig Set[String]
|
87
|
+
attr_reader :shadowed_files
|
88
|
+
internal :shadowed_files
|
74
89
|
|
75
|
-
# @private
|
76
90
|
# @sig Mutex
|
77
91
|
attr_reader :mutex
|
92
|
+
private :mutex
|
78
93
|
|
79
|
-
# @private
|
80
94
|
# @sig Mutex
|
81
95
|
attr_reader :mutex2
|
96
|
+
private :mutex2
|
82
97
|
|
83
98
|
def initialize
|
84
99
|
super
|
@@ -86,7 +101,8 @@ module Zeitwerk
|
|
86
101
|
@autoloads = {}
|
87
102
|
@autoloaded_dirs = []
|
88
103
|
@to_unload = {}
|
89
|
-
@
|
104
|
+
@namespace_dirs = Hash.new { |h, cpath| h[cpath] = [] }
|
105
|
+
@shadowed_files = Set.new
|
90
106
|
@mutex = Mutex.new
|
91
107
|
@mutex2 = Mutex.new
|
92
108
|
@setup = false
|
@@ -95,15 +111,15 @@ module Zeitwerk
|
|
95
111
|
Registry.register_loader(self)
|
96
112
|
end
|
97
113
|
|
98
|
-
# Sets autoloads in the root
|
114
|
+
# Sets autoloads in the root namespaces.
|
99
115
|
#
|
100
116
|
# @sig () -> void
|
101
117
|
def setup
|
102
118
|
mutex.synchronize do
|
103
119
|
break if @setup
|
104
120
|
|
105
|
-
|
106
|
-
set_autoloads_in_dir(root_dir,
|
121
|
+
actual_roots.each do |root_dir, root_namespace|
|
122
|
+
set_autoloads_in_dir(root_dir, root_namespace)
|
107
123
|
end
|
108
124
|
|
109
125
|
on_setup_callbacks.each(&:call)
|
@@ -120,12 +136,14 @@ module Zeitwerk
|
|
120
136
|
# unload them.
|
121
137
|
#
|
122
138
|
# This method is public but undocumented. Main interface is `reload`, which
|
123
|
-
# means `unload` + `setup`. This one is
|
139
|
+
# means `unload` + `setup`. This one is available to be used together with
|
124
140
|
# `unregister`, which is undocumented too.
|
125
141
|
#
|
126
142
|
# @sig () -> void
|
127
143
|
def unload
|
128
144
|
mutex.synchronize do
|
145
|
+
raise SetupRequired unless @setup
|
146
|
+
|
129
147
|
# We are going to keep track of the files that were required by our
|
130
148
|
# autoloads to later remove them from $LOADED_FEATURES, thus making them
|
131
149
|
# loadable by Kernel#require again.
|
@@ -181,10 +199,11 @@ module Zeitwerk
|
|
181
199
|
autoloads.clear
|
182
200
|
autoloaded_dirs.clear
|
183
201
|
to_unload.clear
|
184
|
-
|
202
|
+
namespace_dirs.clear
|
203
|
+
shadowed_files.clear
|
185
204
|
|
186
205
|
Registry.on_unload(self)
|
187
|
-
ExplicitNamespace.
|
206
|
+
ExplicitNamespace.__unregister_loader(self)
|
188
207
|
|
189
208
|
@setup = false
|
190
209
|
@eager_loaded = false
|
@@ -201,6 +220,7 @@ module Zeitwerk
|
|
201
220
|
# @sig () -> void
|
202
221
|
def reload
|
203
222
|
raise ReloadingDisabledError unless reloading_enabled?
|
223
|
+
raise SetupRequired unless @setup
|
204
224
|
|
205
225
|
unload
|
206
226
|
recompute_ignored_paths
|
@@ -208,58 +228,6 @@ module Zeitwerk
|
|
208
228
|
setup
|
209
229
|
end
|
210
230
|
|
211
|
-
# Eager loads all files in the root directories, recursively. Files do not
|
212
|
-
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
213
|
-
# are not eager loaded. You can opt-out specifically in specific files and
|
214
|
-
# directories with `do_not_eager_load`, and that can be overridden passing
|
215
|
-
# `force: true`.
|
216
|
-
#
|
217
|
-
# @sig (true | false) -> void
|
218
|
-
def eager_load(force: false)
|
219
|
-
mutex.synchronize do
|
220
|
-
break if @eager_loaded
|
221
|
-
|
222
|
-
log("eager load start") if logger
|
223
|
-
|
224
|
-
honour_exclusions = !force
|
225
|
-
|
226
|
-
queue = []
|
227
|
-
actual_root_dirs.each do |root_dir, namespace|
|
228
|
-
queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
|
229
|
-
end
|
230
|
-
|
231
|
-
while to_eager_load = queue.shift
|
232
|
-
namespace, dir = to_eager_load
|
233
|
-
|
234
|
-
ls(dir) do |basename, abspath|
|
235
|
-
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
236
|
-
|
237
|
-
if ruby?(abspath)
|
238
|
-
if cref = autoloads[abspath]
|
239
|
-
cget(*cref)
|
240
|
-
end
|
241
|
-
elsif !root_dirs.key?(abspath)
|
242
|
-
if collapse?(abspath)
|
243
|
-
queue << [namespace, abspath]
|
244
|
-
else
|
245
|
-
cname = inflector.camelize(basename, abspath)
|
246
|
-
queue << [cget(namespace, cname), abspath]
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
autoloaded_dirs.each do |autoloaded_dir|
|
253
|
-
Registry.unregister_autoload(autoloaded_dir)
|
254
|
-
end
|
255
|
-
autoloaded_dirs.clear
|
256
|
-
|
257
|
-
@eager_loaded = true
|
258
|
-
|
259
|
-
log("eager load end") if logger
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
231
|
# Says if the given constant path would be unloaded on reload. This
|
264
232
|
# predicate returns `false` if reloading is disabled.
|
265
233
|
#
|
@@ -282,7 +250,15 @@ module Zeitwerk
|
|
282
250
|
# @sig () -> void
|
283
251
|
def unregister
|
284
252
|
Registry.unregister_loader(self)
|
285
|
-
ExplicitNamespace.
|
253
|
+
ExplicitNamespace.__unregister_loader(self)
|
254
|
+
end
|
255
|
+
|
256
|
+
# The return value of this predicate is only meaningful if the loader has
|
257
|
+
# scanned the file. This is the case in the spots where we use it.
|
258
|
+
#
|
259
|
+
# @sig (String) -> Boolean
|
260
|
+
internal def shadowed_file?(file)
|
261
|
+
shadowed_files.member?(file)
|
286
262
|
end
|
287
263
|
|
288
264
|
# --- Class methods ---------------------------------------------------------------------------
|
@@ -311,11 +287,32 @@ module Zeitwerk
|
|
311
287
|
Registry.loader_for_gem(called_from, warn_on_extra_files: warn_on_extra_files)
|
312
288
|
end
|
313
289
|
|
314
|
-
# Broadcasts `eager_load` to all loaders.
|
290
|
+
# Broadcasts `eager_load` to all loaders. Those that have not been setup
|
291
|
+
# are skipped.
|
315
292
|
#
|
316
293
|
# @sig () -> void
|
317
294
|
def eager_load_all
|
318
|
-
Registry.loaders.each
|
295
|
+
Registry.loaders.each do |loader|
|
296
|
+
begin
|
297
|
+
loader.eager_load
|
298
|
+
rescue SetupRequired
|
299
|
+
# This is fine, we eager load what can be eager loaded.
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Broadcasts `eager_load_namespace` to all loaders. Those that have not
|
305
|
+
# been setup are skipped.
|
306
|
+
#
|
307
|
+
# @sig (Module) -> void
|
308
|
+
def eager_load_namespace(mod)
|
309
|
+
Registry.loaders.each do |loader|
|
310
|
+
begin
|
311
|
+
loader.eager_load_namespace(mod)
|
312
|
+
rescue SetupRequired
|
313
|
+
# This is fine, we eager load what can be eager loaded.
|
314
|
+
end
|
315
|
+
end
|
319
316
|
end
|
320
317
|
|
321
318
|
# Returns an array with the absolute paths of the root directories of all
|
@@ -327,10 +324,8 @@ module Zeitwerk
|
|
327
324
|
end
|
328
325
|
end
|
329
326
|
|
330
|
-
private # -------------------------------------------------------------------------------------
|
331
|
-
|
332
327
|
# @sig (String, Module) -> void
|
333
|
-
def set_autoloads_in_dir(dir, parent)
|
328
|
+
private def set_autoloads_in_dir(dir, parent)
|
334
329
|
ls(dir) do |basename, abspath|
|
335
330
|
begin
|
336
331
|
if ruby?(basename)
|
@@ -338,19 +333,11 @@ module Zeitwerk
|
|
338
333
|
cname = inflector.camelize(basename, abspath).to_sym
|
339
334
|
autoload_file(parent, cname, abspath)
|
340
335
|
else
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
# To resolve the ambiguity file name -> constant path this introduces,
|
345
|
-
# the `app/models/concerns` directory is totally ignored as a namespace,
|
346
|
-
# it counts only as root. The guard checks that.
|
347
|
-
unless root_dir?(abspath)
|
336
|
+
if collapse?(abspath)
|
337
|
+
set_autoloads_in_dir(abspath, parent)
|
338
|
+
else
|
348
339
|
cname = inflector.camelize(basename, abspath).to_sym
|
349
|
-
|
350
|
-
set_autoloads_in_dir(abspath, parent)
|
351
|
-
else
|
352
|
-
autoload_subdir(parent, cname, abspath)
|
353
|
-
end
|
340
|
+
autoload_subdir(parent, cname, abspath)
|
354
341
|
end
|
355
342
|
end
|
356
343
|
rescue ::NameError => error
|
@@ -373,17 +360,26 @@ module Zeitwerk
|
|
373
360
|
end
|
374
361
|
|
375
362
|
# @sig (Module, Symbol, String) -> void
|
376
|
-
def autoload_subdir(parent, cname, subdir)
|
363
|
+
private def autoload_subdir(parent, cname, subdir)
|
377
364
|
if autoload_path = autoload_path_set_by_me_for?(parent, cname)
|
378
365
|
cpath = cpath(parent, cname)
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
366
|
+
if ruby?(autoload_path)
|
367
|
+
# Scanning visited a Ruby file first, and now a directory for the same
|
368
|
+
# constant has been found. This means we are dealing with an explicit
|
369
|
+
# namespace whose definition was seen first.
|
370
|
+
#
|
371
|
+
# Registering is idempotent, and we have to keep the autoload pointing
|
372
|
+
# to the file. This may run again if more directories are found later
|
373
|
+
# on, no big deal.
|
374
|
+
register_explicit_namespace(cpath)
|
375
|
+
end
|
376
|
+
# If the existing autoload points to a file, it has to be preserved, if
|
377
|
+
# not, it is fine as it is. In either case, we do not need to override.
|
378
|
+
# Just remember the subdirectory conforms this namespace.
|
379
|
+
namespace_dirs[cpath] << subdir
|
384
380
|
elsif !cdef?(parent, cname)
|
385
381
|
# First time we find this namespace, set an autoload for it.
|
386
|
-
|
382
|
+
namespace_dirs[cpath(parent, cname)] << subdir
|
387
383
|
set_autoload(parent, cname, subdir)
|
388
384
|
else
|
389
385
|
# For whatever reason the constant that corresponds to this namespace has
|
@@ -394,10 +390,11 @@ module Zeitwerk
|
|
394
390
|
end
|
395
391
|
|
396
392
|
# @sig (Module, Symbol, String) -> void
|
397
|
-
def autoload_file(parent, cname, file)
|
393
|
+
private def autoload_file(parent, cname, file)
|
398
394
|
if autoload_path = strict_autoload_path(parent, cname) || Registry.inception?(cpath(parent, cname))
|
399
395
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
400
396
|
if ruby?(autoload_path)
|
397
|
+
shadowed_files << file
|
401
398
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
402
399
|
else
|
403
400
|
promote_namespace_from_implicit_to_explicit(
|
@@ -408,6 +405,7 @@ module Zeitwerk
|
|
408
405
|
)
|
409
406
|
end
|
410
407
|
elsif cdef?(parent, cname)
|
408
|
+
shadowed_files << file
|
411
409
|
log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
|
412
410
|
else
|
413
411
|
set_autoload(parent, cname, file)
|
@@ -418,7 +416,7 @@ module Zeitwerk
|
|
418
416
|
# the file where we've found the namespace is explicitly defined.
|
419
417
|
#
|
420
418
|
# @sig (dir: String, file: String, parent: Module, cname: Symbol) -> void
|
421
|
-
def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
419
|
+
private def promote_namespace_from_implicit_to_explicit(dir:, file:, parent:, cname:)
|
422
420
|
autoloads.delete(dir)
|
423
421
|
Registry.unregister_autoload(dir)
|
424
422
|
|
@@ -429,7 +427,7 @@ module Zeitwerk
|
|
429
427
|
end
|
430
428
|
|
431
429
|
# @sig (Module, Symbol, String) -> void
|
432
|
-
def set_autoload(parent, cname, abspath)
|
430
|
+
private def set_autoload(parent, cname, abspath)
|
433
431
|
parent.autoload(cname, abspath)
|
434
432
|
|
435
433
|
if logger
|
@@ -450,7 +448,7 @@ module Zeitwerk
|
|
450
448
|
end
|
451
449
|
|
452
450
|
# @sig (Module, Symbol) -> String?
|
453
|
-
def autoload_path_set_by_me_for?(parent, cname)
|
451
|
+
private def autoload_path_set_by_me_for?(parent, cname)
|
454
452
|
if autoload_path = strict_autoload_path(parent, cname)
|
455
453
|
autoload_path if autoloads.key?(autoload_path)
|
456
454
|
else
|
@@ -459,26 +457,27 @@ module Zeitwerk
|
|
459
457
|
end
|
460
458
|
|
461
459
|
# @sig (String) -> void
|
462
|
-
def register_explicit_namespace(cpath)
|
463
|
-
ExplicitNamespace.
|
460
|
+
private def register_explicit_namespace(cpath)
|
461
|
+
ExplicitNamespace.__register(cpath, self)
|
464
462
|
end
|
465
463
|
|
466
464
|
# @sig (String) -> void
|
467
|
-
def raise_if_conflicting_directory(dir)
|
465
|
+
private def raise_if_conflicting_directory(dir)
|
468
466
|
MUTEX.synchronize do
|
467
|
+
dir_slash = dir + "/"
|
468
|
+
|
469
469
|
Registry.loaders.each do |loader|
|
470
470
|
next if loader == self
|
471
|
-
next if loader.
|
471
|
+
next if loader.__ignores?(dir)
|
472
472
|
|
473
|
-
|
474
|
-
loader.root_dirs.each do |root_dir, _namespace|
|
473
|
+
loader.__roots.each_key do |root_dir|
|
475
474
|
next if ignores?(root_dir)
|
476
475
|
|
477
|
-
|
478
|
-
if
|
476
|
+
root_dir_slash = root_dir + "/"
|
477
|
+
if dir_slash.start_with?(root_dir_slash) || root_dir_slash.start_with?(dir_slash)
|
479
478
|
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
480
479
|
raise Error,
|
481
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir
|
480
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
482
481
|
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
483
482
|
EOS
|
484
483
|
end
|
@@ -488,23 +487,23 @@ module Zeitwerk
|
|
488
487
|
end
|
489
488
|
|
490
489
|
# @sig (String, Object, String) -> void
|
491
|
-
def run_on_unload_callbacks(cpath, value, abspath)
|
490
|
+
private def run_on_unload_callbacks(cpath, value, abspath)
|
492
491
|
# Order matters. If present, run the most specific one.
|
493
492
|
on_unload_callbacks[cpath]&.each { |c| c.call(value, abspath) }
|
494
493
|
on_unload_callbacks[:ANY]&.each { |c| c.call(cpath, value, abspath) }
|
495
494
|
end
|
496
495
|
|
497
496
|
# @sig (Module, Symbol) -> void
|
498
|
-
def unload_autoload(parent, cname)
|
499
|
-
parent
|
497
|
+
private def unload_autoload(parent, cname)
|
498
|
+
crem(parent, cname)
|
500
499
|
log("autoload for #{cpath(parent, cname)} removed") if logger
|
501
500
|
end
|
502
501
|
|
503
502
|
# @sig (Module, Symbol) -> void
|
504
|
-
def unload_cref(parent, cname)
|
503
|
+
private def unload_cref(parent, cname)
|
505
504
|
# Let's optimistically remove_const. The way we use it, this is going to
|
506
505
|
# succeed always if all is good.
|
507
|
-
parent
|
506
|
+
crem(parent, cname)
|
508
507
|
rescue ::NameError
|
509
508
|
# There are a few edge scenarios in which this may happen. If the constant
|
510
509
|
# is gone, that is OK, anyway.
|
data/lib/zeitwerk/version.rb
CHANGED
data/lib/zeitwerk.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zeitwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.6.
|
4
|
+
version: 2.6.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Noria
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
@@ -28,10 +28,12 @@ files:
|
|
28
28
|
- lib/zeitwerk/gem_inflector.rb
|
29
29
|
- lib/zeitwerk/gem_loader.rb
|
30
30
|
- lib/zeitwerk/inflector.rb
|
31
|
+
- lib/zeitwerk/internal.rb
|
31
32
|
- lib/zeitwerk/kernel.rb
|
32
33
|
- lib/zeitwerk/loader.rb
|
33
34
|
- lib/zeitwerk/loader/callbacks.rb
|
34
35
|
- lib/zeitwerk/loader/config.rb
|
36
|
+
- lib/zeitwerk/loader/eager_load.rb
|
35
37
|
- lib/zeitwerk/loader/helpers.rb
|
36
38
|
- lib/zeitwerk/real_mod_name.rb
|
37
39
|
- lib/zeitwerk/registry.rb
|
@@ -59,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
61
|
- !ruby/object:Gem::Version
|
60
62
|
version: '0'
|
61
63
|
requirements: []
|
62
|
-
rubygems_version: 3.
|
64
|
+
rubygems_version: 3.4.3
|
63
65
|
signing_key:
|
64
66
|
specification_version: 4
|
65
67
|
summary: Efficient and thread-safe constant autoloader
|