zeitwerk 1.4.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +46 -33
- data/lib/zeitwerk.rb +1 -1
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/loader.rb +59 -4
- data/lib/zeitwerk/loader/callbacks.rb +22 -12
- data/lib/zeitwerk/version.rb +1 -1
- metadata +3 -3
- data/lib/zeitwerk/conflicting_directory.rb +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d964999444d0bc99d003d604cb2634d2753c579a4aebcce4fccb4c9ded78530
|
4
|
+
data.tar.gz: 95fc0030faf0472c8359c3665a09fa68dc3c79bff1d813e55d0f03f68c6f5282
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81f2ac76566c1cda996eeac723fa7579dba24011f32fab94458ced0faf5eb7fe920ee2e016ce45ec83fc0d7b17244a1bf98764d71e667cbe067fa8ee87f01ca3
|
7
|
+
data.tar.gz: a90efd80c7caba53d6ec12bc20ee1a76f8d49a6a2e59a82b60a646486eac758348ff1250eee42585dbb2294718a41d59d97c9254ee51b802cddd1ce7add46ead
|
data/README.md
CHANGED
@@ -7,34 +7,33 @@
|
|
7
7
|
|
8
8
|
<!-- TOC -->
|
9
9
|
|
10
|
-
- [
|
11
|
-
|
12
|
-
|
13
|
-
- [
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
- [
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
- [
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
- [
|
27
|
-
|
28
|
-
- [
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
- [License](#license)
|
10
|
+
- [Introduction](#introduction)
|
11
|
+
- [Synopsis](#synopsis)
|
12
|
+
- [File structure](#file-structure)
|
13
|
+
- [Implicit namespaces](#implicit-namespaces)
|
14
|
+
- [Explicit namespaces](#explicit-namespaces)
|
15
|
+
- [Nested root directories](#nested-root-directories)
|
16
|
+
- [Usage](#usage)
|
17
|
+
- [Setup](#setup)
|
18
|
+
- [Reloading](#reloading)
|
19
|
+
- [Eager loading](#eager-loading)
|
20
|
+
- [Preloading](#preloading)
|
21
|
+
- [Inflection](#inflection)
|
22
|
+
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
23
|
+
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
24
|
+
- [Custom inflector](#custom-inflector)
|
25
|
+
- [Logging](#logging)
|
26
|
+
- [Loader tag](#loader-tag)
|
27
|
+
- [Ignoring parts of the project](#ignoring-parts-of-the-project)
|
28
|
+
- [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
|
29
|
+
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
30
|
+
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
31
|
+
- [Edge cases](#edge-cases)
|
32
|
+
- [Pronunciation](#pronunciation)
|
33
|
+
- [Supported Ruby versions](#supported-ruby-versions)
|
34
|
+
- [Motivation](#motivation)
|
35
|
+
- [Thanks](#thanks)
|
36
|
+
- [License](#license)
|
38
37
|
|
39
38
|
<!-- /TOC -->
|
40
39
|
|
@@ -79,13 +78,18 @@ loader.setup # ready!
|
|
79
78
|
|
80
79
|
The `loader` variable can go out of scope. Zeitwerk keeps a registry with all of them, and so the object won't be garbage collected.
|
81
80
|
|
82
|
-
|
81
|
+
You can reload if you want to:
|
83
82
|
|
84
83
|
```ruby
|
84
|
+
loader = Zeitwerk::Loader.new
|
85
|
+
loader.push_dir(...)
|
86
|
+
loader.enable_reloading # you need to opt-in before setup
|
87
|
+
loader.setup
|
88
|
+
...
|
85
89
|
loader.reload
|
86
90
|
```
|
87
91
|
|
88
|
-
and you can
|
92
|
+
and you can eager load all the code:
|
89
93
|
|
90
94
|
```ruby
|
91
95
|
loader.eager_load
|
@@ -192,17 +196,26 @@ Zeitwerk works internally only with absolute paths to avoid costly file searches
|
|
192
196
|
|
193
197
|
### Reloading
|
194
198
|
|
195
|
-
|
199
|
+
Zeitwer is able to reload code, but you need to enable this feature:
|
196
200
|
|
197
201
|
```ruby
|
202
|
+
loader = Zeitwerk::Loader.new
|
203
|
+
loader.push_dir(...)
|
204
|
+
loader.enable_reloading # you need to opt-in before setup
|
205
|
+
loader.setup
|
206
|
+
...
|
198
207
|
loader.reload
|
199
208
|
```
|
200
209
|
|
201
|
-
|
210
|
+
There is no way to undo this, either you want to reload or you don't.
|
211
|
+
|
212
|
+
Enabling reloading after setup raises `Zeitwerk::Error`. Similarly, calling `reload` without having enabled reloading also raises `Zeitwerk::Error`.
|
213
|
+
|
214
|
+
Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, or services running in testing or production environments, won't normally have a use case for reloading. If reloading is not enabled, Zeitwerk is able to use less memory.
|
202
215
|
|
203
216
|
Reloading removes the currently loaded classes and modules, resets the loader so that it will pick whatever is in the file system now, and runs preloads if there are any.
|
204
217
|
|
205
|
-
It is important to highlight that this is an instance method. Don't worry about
|
218
|
+
It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
|
206
219
|
|
207
220
|
In order for reloading to be thread-safe, you need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible RW lock. When a request comes in, the framework acquires the lock for reading at the beginning, and the code in the framework that calls `loader.reload` needs to acquire the lock for writing.
|
208
221
|
|
data/lib/zeitwerk.rb
CHANGED
data/lib/zeitwerk/loader.rb
CHANGED
@@ -79,6 +79,13 @@ module Zeitwerk
|
|
79
79
|
# @return [{String => (Module, String)}]
|
80
80
|
attr_reader :autoloads
|
81
81
|
|
82
|
+
# We keep track of autoloaded directories to remove them from the registry
|
83
|
+
# at the end of eager loading.
|
84
|
+
#
|
85
|
+
# Files are removed as they are autoloaded, but directories need to wait due
|
86
|
+
# to concurrency (see why in Zeitwerk::Loader::Callbacks#on_dir_autoloaded).
|
87
|
+
attr_reader :autoloaded_dirs
|
88
|
+
|
82
89
|
# Constant paths loaded so far.
|
83
90
|
#
|
84
91
|
# @private
|
@@ -129,6 +136,7 @@ module Zeitwerk
|
|
129
136
|
@ignored = Set.new
|
130
137
|
@ignored_paths = Set.new
|
131
138
|
@autoloads = {}
|
139
|
+
@autoloaded_dirs = []
|
132
140
|
@loaded_cpaths = Set.new
|
133
141
|
@lazy_subdirs = {}
|
134
142
|
@shadowed_files = {}
|
@@ -140,6 +148,8 @@ module Zeitwerk
|
|
140
148
|
@setup = false
|
141
149
|
@eager_loaded = false
|
142
150
|
|
151
|
+
@reloading_enabled = false
|
152
|
+
|
143
153
|
Registry.register_loader(self)
|
144
154
|
end
|
145
155
|
|
@@ -161,6 +171,7 @@ module Zeitwerk
|
|
161
171
|
# Pushes `paths` to the list of root directories.
|
162
172
|
#
|
163
173
|
# @param path [<String, Pathname>]
|
174
|
+
# @raise [Zeitwerk::Error]
|
164
175
|
# @return [void]
|
165
176
|
def push_dir(path)
|
166
177
|
abspath = File.expand_path(path)
|
@@ -168,10 +179,32 @@ module Zeitwerk
|
|
168
179
|
raise_if_conflicting_directory(abspath)
|
169
180
|
root_dirs[abspath] = true
|
170
181
|
else
|
171
|
-
raise
|
182
|
+
raise Error, "the root directory #{abspath} does not exist"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# You need to call this method before setup in order to be able to reload.
|
187
|
+
# There is no way to undo this, either you want to reload or you don't.
|
188
|
+
#
|
189
|
+
# @raise [Zeitwerk::Error]
|
190
|
+
# @return [void]
|
191
|
+
def enable_reloading
|
192
|
+
mutex.synchronize do
|
193
|
+
break if @reloading_enabled
|
194
|
+
|
195
|
+
if @setup
|
196
|
+
raise Error, "cannot enable reloading after setup"
|
197
|
+
else
|
198
|
+
@reloading_enabled = true
|
199
|
+
end
|
172
200
|
end
|
173
201
|
end
|
174
202
|
|
203
|
+
# @return [Boolean]
|
204
|
+
def reloading_enabled?
|
205
|
+
@reloading_enabled
|
206
|
+
end
|
207
|
+
|
175
208
|
# Files or directories to be preloaded instead of lazy loaded.
|
176
209
|
#
|
177
210
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
@@ -251,10 +284,22 @@ module Zeitwerk
|
|
251
284
|
end
|
252
285
|
|
253
286
|
unless unloaded_files.empty?
|
287
|
+
# Bootsnap decorates Kernel#require to speed it up using a cache and
|
288
|
+
# this optimization does not check if $LOADED_FEATURES has the file.
|
289
|
+
#
|
290
|
+
# To make it aware of changes, the gem defines singleton methods in
|
291
|
+
# $LOADED_FEATURES:
|
292
|
+
#
|
293
|
+
# https://github.com/Shopify/bootsnap/blob/master/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb
|
294
|
+
#
|
295
|
+
# Rails applications may depend on bootsnap, so for unloading to work
|
296
|
+
# in that setting it is preferable that we restrict our API choice to
|
297
|
+
# one of those methods.
|
254
298
|
$LOADED_FEATURES.reject! { |file| unloaded_files.member?(file) }
|
255
299
|
end
|
256
300
|
|
257
301
|
autoloads.clear
|
302
|
+
autoloaded_dirs.clear
|
258
303
|
loaded_cpaths.clear
|
259
304
|
lazy_subdirs.clear
|
260
305
|
shadowed_files.clear
|
@@ -272,10 +317,15 @@ module Zeitwerk
|
|
272
317
|
# This method is not thread-safe, please see how this can be achieved by
|
273
318
|
# client code in the README of the project.
|
274
319
|
#
|
320
|
+
# @raise [Zeitwerk::Error]
|
275
321
|
# @return [void]
|
276
322
|
def reload
|
277
|
-
|
278
|
-
|
323
|
+
if reloading_enabled?
|
324
|
+
unload
|
325
|
+
setup
|
326
|
+
else
|
327
|
+
raise Error, "can't reload, please call loader.enable_reloading before setup"
|
328
|
+
end
|
279
329
|
end
|
280
330
|
|
281
331
|
# Eager loads all files in the root directories, recursively. Files do not
|
@@ -301,6 +351,11 @@ module Zeitwerk
|
|
301
351
|
end
|
302
352
|
end
|
303
353
|
|
354
|
+
autoloaded_dirs.each do |dir|
|
355
|
+
Registry.unregister_autoload(dir)
|
356
|
+
end
|
357
|
+
autoloaded_dirs.clear
|
358
|
+
|
304
359
|
@eager_loaded = true
|
305
360
|
end
|
306
361
|
end
|
@@ -598,7 +653,7 @@ module Zeitwerk
|
|
598
653
|
loader.dirs.each do |already_managed_dir|
|
599
654
|
if dir.start_with?(already_managed_dir) || already_managed_dir.start_with?(dir)
|
600
655
|
require "pp"
|
601
|
-
raise
|
656
|
+
raise Error,
|
602
657
|
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
603
658
|
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
604
659
|
EOS
|
@@ -5,8 +5,9 @@ module Zeitwerk::Loader::Callbacks
|
|
5
5
|
# @param file [String]
|
6
6
|
# @return [void]
|
7
7
|
def on_file_autoloaded(file)
|
8
|
-
parent, cname =
|
8
|
+
parent, cname = cref_autoloaded_from(file)
|
9
9
|
loaded_cpaths.add(cpath(parent, cname))
|
10
|
+
Zeitwerk::Registry.unregister_autoload(file)
|
10
11
|
log("constant #{cpath(parent, cname)} loaded from file #{file}") if logger
|
11
12
|
end
|
12
13
|
|
@@ -18,25 +19,28 @@ module Zeitwerk::Loader::Callbacks
|
|
18
19
|
# @return [void]
|
19
20
|
def on_dir_autoloaded(dir)
|
20
21
|
# Module#autoload does not serialize concurrent requires, and we handle
|
21
|
-
# directories ourselves.
|
22
|
+
# directories ourselves, so the callback needs to account for concurrency.
|
22
23
|
#
|
23
|
-
#
|
24
|
-
# module, and while autoloads
|
25
|
-
# autoloads the same namespace.
|
24
|
+
# Multi-threading would introduce a race condition here in which thread t1
|
25
|
+
# autovivifies the module, and while autoloads for its children are being
|
26
|
+
# set, thread t2 autoloads the same namespace.
|
26
27
|
#
|
27
|
-
#
|
28
|
-
# only
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# thus resulting in NameErrors when client code tries to reach them.
|
28
|
+
# Without the mutex and short-circuiting break, t2 would reset the module.
|
29
|
+
# That not only would reassign the constant (undesirable per se) but, worse,
|
30
|
+
# the module object created by t2 wouldn't have any of the autoloads for its
|
31
|
+
# children, since t1 would have correctly deleted its lazy_subdirs entry.
|
32
32
|
mutex2.synchronize do
|
33
|
-
parent, cname =
|
34
|
-
|
33
|
+
parent, cname = cref_autoloaded_from(dir)
|
34
|
+
# If reloading is disabled and there are several threads autoloading the
|
35
|
+
# same namespace at the same time, the parent is going to bbe nil for all
|
36
|
+
# except the first one.
|
37
|
+
break if parent.nil? || loaded_cpaths.include?(cpath(parent, cname))
|
35
38
|
|
36
39
|
autovivified_module = parent.const_set(cname, Module.new)
|
37
40
|
log("module #{autovivified_module.name} autovivified from directory #{dir}") if logger
|
38
41
|
|
39
42
|
loaded_cpaths.add(autovivified_module.name)
|
43
|
+
autoloaded_dirs << dir
|
40
44
|
on_namespace_loaded(autovivified_module)
|
41
45
|
end
|
42
46
|
end
|
@@ -55,4 +59,10 @@ module Zeitwerk::Loader::Callbacks
|
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
62
|
+
|
63
|
+
# @private
|
64
|
+
# @return [(Module, String)]
|
65
|
+
def cref_autoloaded_from(path)
|
66
|
+
reloading_enabled? ? autoloads[path] : autoloads.delete(path)
|
67
|
+
end
|
58
68
|
end
|
data/lib/zeitwerk/version.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:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Noria
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-04-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
@@ -22,7 +22,7 @@ extra_rdoc_files: []
|
|
22
22
|
files:
|
23
23
|
- README.md
|
24
24
|
- lib/zeitwerk.rb
|
25
|
-
- lib/zeitwerk/
|
25
|
+
- lib/zeitwerk/error.rb
|
26
26
|
- lib/zeitwerk/explicit_namespace.rb
|
27
27
|
- lib/zeitwerk/gem_inflector.rb
|
28
28
|
- lib/zeitwerk/inflector.rb
|