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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cb6bd7dd37980b33d67ae3b1a8b5a8228911dd833698617991f6026b06a122c
4
- data.tar.gz: 8a9d2438c2008c5c04f21692d1dea9dd0cd7756eb3cd4460262866b1b9eda42f
3
+ metadata.gz: 5d964999444d0bc99d003d604cb2634d2753c579a4aebcce4fccb4c9ded78530
4
+ data.tar.gz: 95fc0030faf0472c8359c3665a09fa68dc3c79bff1d813e55d0f03f68c6f5282
5
5
  SHA512:
6
- metadata.gz: aa2bfe23d1b4c3dde4f6df132c0e6bfb908e540cdc76177cc676fdd7d6843bd6b9af4032978f9948c46c1b74e1979ee592028cafba02fa25f3bdd5eaec0a3d67
7
- data.tar.gz: ba5301619afb7d13dd1081330826b06c5bf6a8d520f56aa788423f14fcc2854c70cc9e8d5db7abec9733e05bc7ac3f6e8c7852f4563359abbcc46e5171f96371
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
- - [Zeitwerk](#zeitwerk)
11
- - [Introduction](#introduction)
12
- - [Synopsis](#synopsis)
13
- - [File structure](#file-structure)
14
- - [Implicit namespaces](#implicit-namespaces)
15
- - [Explicit namespaces](#explicit-namespaces)
16
- - [Nested root directories](#nested-root-directories)
17
- - [Usage](#usage)
18
- - [Setup](#setup)
19
- - [Reloading](#reloading)
20
- - [Eager loading](#eager-loading)
21
- - [Preloading](#preloading)
22
- - [Inflection](#inflection)
23
- - [Zeitwerk::Inflector](#zeitwerkinflector)
24
- - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
25
- - [Custom inflector](#custom-inflector)
26
- - [Logging](#logging)
27
- - [Loader tag](#loader-tag)
28
- - [Ignoring parts of the project](#ignoring-parts-of-the-project)
29
- - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
30
- - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
31
- - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
32
- - [Edge cases](#edge-cases)
33
- - [Pronunciation](#pronunciation)
34
- - [Supported Ruby versions](#supported-ruby-versions)
35
- - [Motivation](#motivation)
36
- - [Thanks](#thanks)
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
- Later, you can reload if you want to:
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 also eager load all the code:
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
- In order to reload code:
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
- Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, won't normally have a use case for reloading.
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 the project dependencies, their loaders are independent.
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
 
@@ -7,5 +7,5 @@ module Zeitwerk
7
7
  require_relative "zeitwerk/inflector"
8
8
  require_relative "zeitwerk/gem_inflector"
9
9
  require_relative "zeitwerk/kernel"
10
- require_relative "zeitwerk/conflicting_directory"
10
+ require_relative "zeitwerk/error"
11
11
  end
@@ -0,0 +1,2 @@
1
+ class Zeitwerk::Error < StandardError
2
+ end
@@ -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 ArgumentError, "the root directory #{abspath} does not exist"
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
- unload
278
- setup
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 ConflictingDirectory,
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 = autoloads[file]
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
- # That introduces a race condition here in which thread t1 autovivifies the
24
- # module, and while autoloads are being set on that module object, thread t2
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
- # In that situation, t2 resets the constant to store a new module. That not
28
- # only sets the constant twice (undesirable per se), but it gets worse,
29
- # because the module object created by t2 won't have any of the autoloads
30
- # for child constants set, since t1 correctly deleted the lazy_dirs entry,
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 = autoloads[dir]
34
- break if loaded_cpaths.include?(cpath(parent, cname))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "1.4.3"
4
+ VERSION = "2.0.0"
5
5
  end
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: 1.4.3
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-03-26 00:00:00.000000000 Z
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/conflicting_directory.rb
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
@@ -1,2 +0,0 @@
1
- class Zeitwerk::ConflictingDirectory < StandardError
2
- end