zeitwerk 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +650 -0
- data/lib/zeitwerk.rb +12 -0
- data/lib/zeitwerk/error.rb +10 -0
- data/lib/zeitwerk/explicit_namespace.rb +80 -0
- data/lib/zeitwerk/gem_inflector.rb +19 -0
- data/lib/zeitwerk/inflector.rb +49 -0
- data/lib/zeitwerk/kernel.rb +33 -0
- data/lib/zeitwerk/loader.rb +765 -0
- data/lib/zeitwerk/loader/callbacks.rb +71 -0
- data/lib/zeitwerk/real_mod_name.rb +21 -0
- data/lib/zeitwerk/registry.rb +147 -0
- data/lib/zeitwerk/version.rb +5 -0
- metadata +59 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module Zeitwerk::Loader::Callbacks
|
2
|
+
include Zeitwerk::RealModName
|
3
|
+
|
4
|
+
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
5
|
+
#
|
6
|
+
# @private
|
7
|
+
# @param file [String]
|
8
|
+
# @return [void]
|
9
|
+
def on_file_autoloaded(file)
|
10
|
+
cref = autoloads.delete(file)
|
11
|
+
to_unload[cpath(*cref)] = [file, cref] if reloading_enabled?
|
12
|
+
Zeitwerk::Registry.unregister_autoload(file)
|
13
|
+
|
14
|
+
if logger && cdef?(*cref)
|
15
|
+
log("constant #{cpath(*cref)} loaded from file #{file}")
|
16
|
+
elsif !cdef?(*cref)
|
17
|
+
raise Zeitwerk::NameError, "expected file #{file} to define constant #{cpath(*cref)}, but didn't"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Invoked from our decorated Kernel#require when a managed directory is
|
22
|
+
# autoloaded.
|
23
|
+
#
|
24
|
+
# @private
|
25
|
+
# @param dir [String]
|
26
|
+
# @return [void]
|
27
|
+
def on_dir_autoloaded(dir)
|
28
|
+
# Module#autoload does not serialize concurrent requires, and we handle
|
29
|
+
# directories ourselves, so the callback needs to account for concurrency.
|
30
|
+
#
|
31
|
+
# Multi-threading would introduce a race condition here in which thread t1
|
32
|
+
# autovivifies the module, and while autoloads for its children are being
|
33
|
+
# set, thread t2 autoloads the same namespace.
|
34
|
+
#
|
35
|
+
# Without the mutex and subsequent delete call, t2 would reset the module.
|
36
|
+
# That not only would reassign the constant (undesirable per se) but, worse,
|
37
|
+
# the module object created by t2 wouldn't have any of the autoloads for its
|
38
|
+
# children, since t1 would have correctly deleted its lazy_subdirs entry.
|
39
|
+
mutex2.synchronize do
|
40
|
+
if cref = autoloads.delete(dir)
|
41
|
+
autovivified_module = cref[0].const_set(cref[1], Module.new)
|
42
|
+
log("module #{autovivified_module.name} autovivified from directory #{dir}") if logger
|
43
|
+
|
44
|
+
to_unload[autovivified_module.name] = [dir, cref] if reloading_enabled?
|
45
|
+
|
46
|
+
# We don't unregister `dir` in the registry because concurrent threads
|
47
|
+
# wouldn't find a loader associated to it in Kernel#require and would
|
48
|
+
# try to require the directory. Instead, we are going to keep track of
|
49
|
+
# these to be able to unregister later if eager loading.
|
50
|
+
autoloaded_dirs << dir
|
51
|
+
|
52
|
+
on_namespace_loaded(autovivified_module)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Invoked when a class or module is created or reopened, either from the
|
58
|
+
# tracer or from module autovivification. If the namespace has matching
|
59
|
+
# subdirectories, we descend into them now.
|
60
|
+
#
|
61
|
+
# @private
|
62
|
+
# @param namespace [Module]
|
63
|
+
# @return [void]
|
64
|
+
def on_namespace_loaded(namespace)
|
65
|
+
if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
|
66
|
+
subdirs.each do |subdir|
|
67
|
+
set_autoloads_in_dir(subdir, namespace)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Zeitwerk::RealModName
|
2
|
+
UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
|
3
|
+
private_constant :UNBOUND_METHOD_MODULE_NAME
|
4
|
+
|
5
|
+
# Returns the real name of the class or module, as set after the first
|
6
|
+
# constant to which it was assigned (or nil).
|
7
|
+
#
|
8
|
+
# The name method can be overridden, hence the indirection in this method.
|
9
|
+
#
|
10
|
+
# @param mod [Class, Module]
|
11
|
+
# @return [String, nil]
|
12
|
+
if UnboundMethod.method_defined?(:bind_call)
|
13
|
+
def real_mod_name(mod)
|
14
|
+
UNBOUND_METHOD_MODULE_NAME.bind_call(mod)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
def real_mod_name(mod)
|
18
|
+
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zeitwerk
|
4
|
+
module Registry # :nodoc: all
|
5
|
+
class << self
|
6
|
+
# Keeps track of all loaders. Useful to broadcast messages and to prevent
|
7
|
+
# them from being garbage collected.
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
# @return [<Zeitwerk::Loader>]
|
11
|
+
attr_reader :loaders
|
12
|
+
|
13
|
+
# Registers loaders created with `for_gem` to make the method idempotent
|
14
|
+
# in case of reload.
|
15
|
+
#
|
16
|
+
# @private
|
17
|
+
# @return [{String => Zeitwerk::Loader}]
|
18
|
+
attr_reader :loaders_managing_gems
|
19
|
+
|
20
|
+
# Maps real paths to the loaders responsible for them.
|
21
|
+
#
|
22
|
+
# This information is used by our decorated `Kernel#require` to be able to
|
23
|
+
# invoke callbacks and autovivify modules.
|
24
|
+
#
|
25
|
+
# @private
|
26
|
+
# @return [{String => Zeitwerk::Loader}]
|
27
|
+
attr_reader :autoloads
|
28
|
+
|
29
|
+
# This hash table addresses an edge case in which an autoload is ignored.
|
30
|
+
#
|
31
|
+
# For example, let's suppose we want to autoload in a gem like this:
|
32
|
+
#
|
33
|
+
# # lib/my_gem.rb
|
34
|
+
# loader = Zeitwerk::Loader.new
|
35
|
+
# loader.push_dir(__dir__)
|
36
|
+
# loader.setup
|
37
|
+
#
|
38
|
+
# module MyGem
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# if you require "my_gem", as Bundler would do, this happens while setting
|
42
|
+
# up autoloads:
|
43
|
+
#
|
44
|
+
# 1. Object.autoload?(:MyGem) returns `nil` because the autoload for
|
45
|
+
# the constant is issued by Zeitwerk while the same file is being
|
46
|
+
# required.
|
47
|
+
# 2. The constant `MyGem` is undefined while setup runs.
|
48
|
+
#
|
49
|
+
# Therefore, a directory `lib/my_gem` would autovivify a module according to
|
50
|
+
# the existing information. But that would be wrong.
|
51
|
+
#
|
52
|
+
# To overcome this fundamental limitation, we keep track of the constant
|
53
|
+
# paths that are in this situation ---in the example above, "MyGem"--- and
|
54
|
+
# take this collection into account for the autovivification logic.
|
55
|
+
#
|
56
|
+
# Note that you cannot generally address this by moving the setup code
|
57
|
+
# below the constant definition, because we want libraries to be able to
|
58
|
+
# use managed constants in the module body:
|
59
|
+
#
|
60
|
+
# module MyGem
|
61
|
+
# include MyConcern
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @private
|
65
|
+
# @return [{String => (String, Zeitwerk::Loader)}]
|
66
|
+
attr_reader :inceptions
|
67
|
+
|
68
|
+
# Registers a loader.
|
69
|
+
#
|
70
|
+
# @private
|
71
|
+
# @param loader [Zeitwerk::Loader]
|
72
|
+
# @return [void]
|
73
|
+
def register_loader(loader)
|
74
|
+
loaders << loader
|
75
|
+
end
|
76
|
+
|
77
|
+
# This method returns always a loader, the same instance for the same root
|
78
|
+
# file. That is how Zeitwerk::Loader.for_gem is idempotent.
|
79
|
+
#
|
80
|
+
# @private
|
81
|
+
# @param root_file [String]
|
82
|
+
# @return [Zeitwerk::Loader]
|
83
|
+
def loader_for_gem(root_file)
|
84
|
+
loaders_managing_gems[root_file] ||= begin
|
85
|
+
Loader.new.tap do |loader|
|
86
|
+
loader.tag = File.basename(root_file, ".rb")
|
87
|
+
loader.inflector = GemInflector.new(root_file)
|
88
|
+
loader.push_dir(File.dirname(root_file))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @private
|
94
|
+
# @param loader [Zeitwerk::Loader]
|
95
|
+
# @param realpath [String]
|
96
|
+
# @return [void]
|
97
|
+
def register_autoload(loader, realpath)
|
98
|
+
autoloads[realpath] = loader
|
99
|
+
end
|
100
|
+
|
101
|
+
# @private
|
102
|
+
# @param realpath [String]
|
103
|
+
# @return [void]
|
104
|
+
def unregister_autoload(realpath)
|
105
|
+
autoloads.delete(realpath)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @private
|
109
|
+
# @param cpath [String]
|
110
|
+
# @param realpath [String]
|
111
|
+
# @param loader [Zeitwerk::Loader]
|
112
|
+
# @return [void]
|
113
|
+
def register_inception(cpath, realpath, loader)
|
114
|
+
inceptions[cpath] = [realpath, loader]
|
115
|
+
end
|
116
|
+
|
117
|
+
# @private
|
118
|
+
# @param cpath [String]
|
119
|
+
# @return [String, nil]
|
120
|
+
def inception?(cpath)
|
121
|
+
if pair = inceptions[cpath]
|
122
|
+
pair.first
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# @private
|
127
|
+
# @param path [String]
|
128
|
+
# @return [Zeitwerk::Loader, nil]
|
129
|
+
def loader_for(path)
|
130
|
+
autoloads[path]
|
131
|
+
end
|
132
|
+
|
133
|
+
# @private
|
134
|
+
# @param loader [Zeitwerk::Loader]
|
135
|
+
# @return [void]
|
136
|
+
def on_unload(loader)
|
137
|
+
autoloads.delete_if { |_path, object| object == loader }
|
138
|
+
inceptions.delete_if { |_cpath, (_path, object)| object == loader }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
@loaders = []
|
143
|
+
@loaders_managing_gems = {}
|
144
|
+
@autoloads = {}
|
145
|
+
@inceptions = {}
|
146
|
+
end
|
147
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zeitwerk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Xavier Noria
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-11-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |2
|
14
|
+
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
15
|
+
and application may have their own independent autoloader, with its own
|
16
|
+
configuration, inflector, and logger. Supports autoloading, preloading,
|
17
|
+
reloading, and eager loading.
|
18
|
+
email: fxn@hashref.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- MIT-LICENSE
|
24
|
+
- README.md
|
25
|
+
- lib/zeitwerk.rb
|
26
|
+
- lib/zeitwerk/error.rb
|
27
|
+
- lib/zeitwerk/explicit_namespace.rb
|
28
|
+
- lib/zeitwerk/gem_inflector.rb
|
29
|
+
- lib/zeitwerk/inflector.rb
|
30
|
+
- lib/zeitwerk/kernel.rb
|
31
|
+
- lib/zeitwerk/loader.rb
|
32
|
+
- lib/zeitwerk/loader/callbacks.rb
|
33
|
+
- lib/zeitwerk/real_mod_name.rb
|
34
|
+
- lib/zeitwerk/registry.rb
|
35
|
+
- lib/zeitwerk/version.rb
|
36
|
+
homepage: https://github.com/fxn/zeitwerk
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options: []
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 2.4.4
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
requirements: []
|
55
|
+
rubygems_version: 3.0.3
|
56
|
+
signing_key:
|
57
|
+
specification_version: 4
|
58
|
+
summary: Efficient and thread-safe constant autoloader
|
59
|
+
test_files: []
|