zeitwerk 2.5.0.beta3 → 2.5.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: 3c75f8cc8ed4e3df2f4bf5822402b5d1179e80d1bd2680efda63302f082dc366
4
- data.tar.gz: ee91b83fb9c7e15c9502a3200c3d0a5cb785f5f2e9f75cd419e57bc335691784
3
+ metadata.gz: c85bb1028371436b80651c897670fef7158dcca7a916b10f6cb8584684c44442
4
+ data.tar.gz: c07fec548ab787daa28b10260beb022d7375aff90a506daa9bfecdc2f5bd6e5c
5
5
  SHA512:
6
- metadata.gz: e2ccad2ec4ca741a01432875eb683307eeb9840b278a3fff45ec74d3b1c2892c6953e7ef18b42cca40bfb09ca21f952c7aadf8653fc67f5e7a29b40e07e8bb74
7
- data.tar.gz: b74645e665af729b8fd3a2fd6fcc6da86bcc953979f0f7db4b70fffa67e09e399615158acdfac996aaf083c28f538dde90f98b7682f9feb5331bdc498f2f24a5
6
+ metadata.gz: d01e5091fa868cdc80c4dd6eb82cc3ace4e21e0704aa2128e0ef5098a52140884daa62e9da862490104e31d393e06b33e29170e6d3f6fd3e44aa5111b25a06f1
7
+ data.tar.gz: 055d861fe2440f465277fbfce4c2bd9b7460d38f14dc5435449c6655d1be97f7e24fb7154b64102267c020e45d52f0bff21fbc7fc35eec0063b2dac2a92197bd
data/README.md CHANGED
@@ -19,19 +19,24 @@
19
19
  - [Implicit namespaces](#implicit-namespaces)
20
20
  - [Explicit namespaces](#explicit-namespaces)
21
21
  - [Collapsing directories](#collapsing-directories)
22
+ - [Testing compliance](#testing-compliance)
22
23
  - [Usage](#usage)
23
24
  - [Setup](#setup)
24
25
  - [Generic](#generic)
25
26
  - [for_gem](#for_gem)
26
27
  - [Autoloading](#autoloading)
27
28
  - [Eager loading](#eager-loading)
29
+ - [Eager load exclusions](#eager-load-exclusions)
30
+ - [Global eager load](#global-eager-load)
28
31
  - [Reloading](#reloading)
29
32
  - [Inflection](#inflection)
30
33
  - [Zeitwerk::Inflector](#zeitwerkinflector)
31
34
  - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
32
35
  - [Custom inflector](#custom-inflector)
33
- - [The on_load callback](#the-on_load-callback)
34
- - [The on_unload callback](#the-on_unload-callback)
36
+ - [Callbacks](#callbacks)
37
+ - [The on_setup callback](#the-on_setup-callback)
38
+ - [The on_load callback](#the-on_load-callback)
39
+ - [The on_unload callback](#the-on_unload-callback)
35
40
  - [Technical details](#technical-details)
36
41
  - [Logging](#logging)
37
42
  - [Loader tag](#loader-tag)
@@ -40,16 +45,18 @@
40
45
  - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
41
46
  - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
42
47
  - [Edge cases](#edge-cases)
48
+ - [Beware of circular dependencies](#beware-of-circular-dependencies)
43
49
  - [Reopening third-party namespaces](#reopening-third-party-namespaces)
44
50
  - [Rules of thumb](#rules-of-thumb)
45
51
  - [Debuggers](#debuggers)
52
+ - [debug.rb](#debugrb)
46
53
  - [Break](#break)
47
54
  - [Byebug](#byebug)
48
55
  - [Pronunciation](#pronunciation)
49
56
  - [Supported Ruby versions](#supported-ruby-versions)
50
57
  - [Testing](#testing)
51
58
  - [Motivation](#motivation)
52
- - [Kerner#require is brittle](#kernerrequire-is-brittle)
59
+ - [Kernel#require is brittle](#kernelrequire-is-brittle)
53
60
  - [Rails autoloading was brittle](#rails-autoloading-was-brittle)
54
61
  - [Thanks](#thanks)
55
62
  - [License](#license)
@@ -159,6 +166,16 @@ class HttpCrawler
159
166
  end
160
167
  ```
161
168
 
169
+ The first example needs a custom [inflection](https://github.com/fxn/zeitwerk#inflection) rule:
170
+
171
+ ```ruby
172
+ loader.inflector.inflect("max_retries" => "MAX_RETRIES")
173
+ ```
174
+
175
+ Otherwise, Zeitwerk would expect the file to define `MaxRetries`.
176
+
177
+ In the second example, no custom rule is needed.
178
+
162
179
  <a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
163
180
  ### Root directories and root namespaces
164
181
 
@@ -277,6 +294,23 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
277
294
  loader.collapse("#{__dir__}/*/actions")
278
295
  ```
279
296
 
297
+ <a id="markdown-testing-compliance" name="testing-compliance"></a>
298
+ ### Testing compliance
299
+
300
+ When a managed file is loaded, Zeitwerk verifies the expected constant is defined. If it is not, `Zeitwerk::NameError` is raised.
301
+
302
+ So, an easy way to ensure compliance in the test suite is to eager load the project:
303
+
304
+ ```ruby
305
+ begin
306
+ loader.eager_load(force: true)
307
+ rescue Zeitwerk::NameError => e
308
+ flunk e.message
309
+ else
310
+ assert true
311
+ end
312
+ ```
313
+
280
314
  <a id="markdown-usage" name="usage"></a>
281
315
  ## Usage
282
316
 
@@ -379,7 +413,16 @@ Zeitwerk instances are able to eager load their managed files:
379
413
  loader.eager_load
380
414
  ```
381
415
 
382
- That skips [ignored files and directories](#ignoring-parts-of-the-project), and you can also tell Zeitwerk that certain files or directories are autoloadable, but should not be eager loaded:
416
+ That skips [ignored files and directories](#ignoring-parts-of-the-project).
417
+
418
+ In gems, the method needs to be invoked after the main namespace has been defined, as shown in [Synopsis](https://github.com/fxn/zeitwerk#synopsis).
419
+
420
+ Eager loading is synchronized and idempotent.
421
+
422
+ <a id="markdown-eager-load-exclusions" name="eager-load-exclusions"></a>
423
+ #### Eager load exclusions
424
+
425
+ You can tell Zeitwerk that certain files or directories are autoloadable, but should not be eager loaded:
383
426
 
384
427
  ```ruby
385
428
  db_adapters = "#{__dir__}/my_gem/db_adapters"
@@ -388,13 +431,20 @@ loader.setup
388
431
  loader.eager_load # won't eager load the database adapters
389
432
  ```
390
433
 
391
- In gems, the method needs to be invoked after the main namespace has been defined, as shown in [Synopsis](https://github.com/fxn/zeitwerk#synopsis).
434
+ However, that can be overridden with `force`:
392
435
 
393
- Eager loading is synchronized and idempotent.
436
+ ```ruby
437
+ loader.eager_load(force: true) # database adapters are eager loaded
438
+ ```
439
+
440
+ Which may be handy if the project eager loads in the test suite to [ensure project layour compliance](#testing-compliance).
441
+
442
+ The `force` flag does not affect ignored files and directories, those are still ignored.
394
443
 
395
- If eager loading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
444
+ <a id="markdown-global-eager-load" name="global-eager-load"></a>
445
+ #### Global eager load
396
446
 
397
- If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
447
+ If you want to eager load yourself and all dependencies that use Zeitwerk, you can broadcast the `eager_load` call to all instances:
398
448
 
399
449
  ```ruby
400
450
  Zeitwerk::Loader.eager_load_all
@@ -404,6 +454,8 @@ This may be handy in top-level services, like web applications.
404
454
 
405
455
  Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
406
456
 
457
+ This method does not accept the `force` flag, since in general it wouldn't be a good idea to force eager loading in 3rd party code.
458
+
407
459
  <a id="markdown-reloading" name="reloading"></a>
408
460
  ### Reloading
409
461
 
@@ -553,8 +605,26 @@ class MyGem::Inflector < Zeitwerk::GemInflector
553
605
  end
554
606
  ```
555
607
 
608
+ <a id="markdown-callbacks" name="callbacks"></a>
609
+ ### Callbacks
610
+
611
+ <a id="markdown-the-on_setup-callback" name="the-on_setup-callback"></a>
612
+ #### The on_setup callback
613
+
614
+ The `on_setup` callback is fired on setup and on each reload:
615
+
616
+ ```ruby
617
+ loader.on_setup do
618
+ # Ready to autoload here.
619
+ end
620
+ ```
621
+
622
+ Multiple `on_setup` callbacks are supported, and they run in order of definition.
623
+
624
+ If `setup` was already executed, the callback is fired immediately.
625
+
556
626
  <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
557
- ### The on_load callback
627
+ #### The on_load callback
558
628
 
559
629
  The usual place to run something when a file is loaded is the file itself. However, sometimes you'd like to be called, and this is possible with the `on_load` callback.
560
630
 
@@ -612,8 +682,10 @@ There are use cases for this last catch-all callback, but they are rare. If you
612
682
 
613
683
  If both types of callbacks are defined, the specific ones run first.
614
684
 
685
+ Since `on_load` callbacks are executed right after files are loaded, even if the loading context seems to be far away, in practice **the block is subject to [circular dependencies](#beware-of-circular-dependencies)**. As a rule of thumb, as far as loading order and its interdependencies is concerned, you have to program as if the block was executed at the bottom of the file just loaded.
686
+
615
687
  <a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
616
- ### The on_unload callback
688
+ #### The on_unload callback
617
689
 
618
690
  When reloading is enabled, you may occasionally need to execute something before a certain autoloaded class or module is unloaded. The `on_unload` callback allows you to do that.
619
691
 
@@ -824,6 +896,26 @@ Trip = Struct.new { ... } # NOT SUPPORTED
824
896
 
825
897
  This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
826
898
 
899
+ <a id="markdown-beware-of-circular-dependencies" name="beware-of-circular-dependencies"></a>
900
+ ### Beware of circular dependencies
901
+
902
+ In Ruby, you can't have certain top-level circular dependencies. Take for example:
903
+
904
+ ```ruby
905
+ # c.rb
906
+ class C < D
907
+ end
908
+
909
+ # d.rb
910
+ class D
911
+ C
912
+ end
913
+ ```
914
+
915
+ In order to define `C`, you need to load `D`. However, the body of `D` refers to `C`.
916
+
917
+ Circular dependencies like those do not work in plain Ruby, and therefore do not work in projects managed by Zeitwerk either.
918
+
827
919
  <a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
828
920
  ### Reopening third-party namespaces
829
921
 
@@ -878,6 +970,11 @@ With that, when Zeitwerk scans the file system and reaches the gem directories `
878
970
  <a id="markdown-debuggers" name="debuggers"></a>
879
971
  ### Debuggers
880
972
 
973
+ <a id="markdown-debugrb" name="debugrb"></a>
974
+ #### debug.rb
975
+
976
+ The new [debug.rb](https://github.com/ruby/debug) gem and Zeitwerk seem to be compatible, as far as I can tell. This is the new debugger that is going to ship with Ruby 3.1.
977
+
881
978
  <a id="markdown-break" name="break"></a>
882
979
  #### Break
883
980
 
@@ -896,7 +993,11 @@ Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatib
896
993
  <a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
897
994
  ## Supported Ruby versions
898
995
 
899
- Zeitwerk works with MRI 2.4.4 and above.
996
+ Zeitwerk works with CRuby 2.5 and above.
997
+
998
+ On TruffleRuby all is good except for thread-safety. Right now, in TruffleRuby `Module#autoload` does not block threads accessing a constant that is being autoloaded. CRuby prevents such access to avoid concurrent threads from seeing partial evaluations of the corresponding file. Zeitwerk inherits autoloading thread-safety from this property. This is not an issue if your project gets eager loaded, or if you lazy load in single-threaded environments. (See https://github.com/oracle/truffleruby/issues/2431.)
999
+
1000
+ JRuby 9.3.0.0 is almost there. As of this writing, the test suite of Zeitwerk passes on JRuby except for three tests. (See https://github.com/jruby/jruby/issues/6781.)
900
1001
 
901
1002
  <a id="markdown-testing" name="testing"></a>
902
1003
  ## Testing
@@ -927,8 +1028,8 @@ and run `bin/test`.
927
1028
  <a id="markdown-motivation" name="motivation"></a>
928
1029
  ## Motivation
929
1030
 
930
- <a id="markdown-kernerrequire-is-brittle" name="kernerrequire-is-brittle"></a>
931
- ### Kerner#require is brittle
1031
+ <a id="markdown-kernelrequire-is-brittle" name="kernelrequire-is-brittle"></a>
1032
+ ### Kernel#require is brittle
932
1033
 
933
1034
  Since `require` has global side-effects, and there is no static way to verify that you have issued the `require` calls for code that your file depends on, in practice it is very easy to forget some. That introduces bugs that depend on the load order.
934
1035
 
@@ -939,7 +1040,7 @@ With Zeitwerk, you just name things following conventions and done. Things are a
939
1040
  <a id="markdown-rails-autoloading-was-brittle" name="rails-autoloading-was-brittle"></a>
940
1041
  ### Rails autoloading was brittle
941
1042
 
942
- Autoloading in Rails was based on `const_missing` up to Rails 5. That callback lacks fundamental information like the nesting or the resolution algorithm being used. Because of that, Rails autoloading was not able to match Ruby's semantics, and that introduced a series of issues. Zeitwerk is based on a different technique and fixed Rails autoloading starting with Rails 6.
1043
+ Autoloading in Rails was based on `const_missing` up to Rails 5. That callback lacks fundamental information like the nesting or the resolution algorithm being used. Because of that, Rails autoloading was not able to match Ruby's semantics, and that introduced a [series of issues](https://guides.rubyonrails.org/v5.2/autoloading_and_reloading_constants.html#common-gotchas). Zeitwerk is based on a different technique and fixed Rails autoloading starting with Rails 6.
943
1044
 
944
1045
  <a id="markdown-thanks" name="thanks"></a>
945
1046
  ## Thanks
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk
2
4
  # @private
3
5
  class Autoloads
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk
2
4
  class Error < StandardError
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk
2
4
  # Centralizes the logic for the trace point used to detect the creation of
3
5
  # explicit namespaces, needed to descend into matching subdirectories right
@@ -3,7 +3,7 @@
3
3
  module Kernel
4
4
  module_function
5
5
 
6
- # We are going to decorate Kerner#require with two goals.
6
+ # We are going to decorate Kernel#require with two goals.
7
7
  #
8
8
  # First, by intercepting Kernel#require calls, we are able to autovivify
9
9
  # modules on required directories, and also do internal housekeeping when
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk::Loader::Callbacks
2
4
  include Zeitwerk::RealModName
3
5
 
@@ -9,16 +11,19 @@ module Zeitwerk::Loader::Callbacks
9
11
  cref = autoloads.delete(file)
10
12
  cpath = cpath(*cref)
11
13
 
14
+ # If reloading is enabled, we need to put this constant for unloading
15
+ # regardless of what cdef? says. In Ruby < 3.1 the internal state is not
16
+ # fully cleared. Module#constants still includes it, and you need to
17
+ # remove_const. See https://github.com/ruby/ruby/pull/4715.
12
18
  to_unload[cpath] = [file, cref] if reloading_enabled?
13
19
  Zeitwerk::Registry.unregister_autoload(file)
14
20
 
15
- if logger && cdef?(*cref)
16
- log("constant #{cpath} loaded from file #{file}")
17
- elsif !cdef?(*cref)
21
+ if cdef?(*cref)
22
+ log("constant #{cpath} loaded from file #{file}") if logger
23
+ run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
24
+ else
18
25
  raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath}, but didn't", cref.last)
19
26
  end
20
-
21
- run_on_load_callbacks(cpath, cget(*cref), file) unless on_load_callbacks.empty?
22
27
  end
23
28
 
24
29
  # Invoked from our decorated Kernel#require when a managed directory is
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "set"
2
4
  require "securerandom"
3
5
 
@@ -54,6 +56,12 @@ module Zeitwerk::Loader::Config
54
56
  # @sig Set[String]
55
57
  attr_reader :eager_load_exclusions
56
58
 
59
+ # User-oriented callbacks to be fired on setup and on reload.
60
+ #
61
+ # @private
62
+ # @sig Array[{ () -> void }]
63
+ attr_reader :on_setup_callbacks
64
+
57
65
  # User-oriented callbacks to be fired when a constant is loaded.
58
66
  #
59
67
  # @private
@@ -81,6 +89,7 @@ module Zeitwerk::Loader::Config
81
89
  @collapse_dirs = Set.new
82
90
  @eager_load_exclusions = Set.new
83
91
  @reloading_enabled = false
92
+ @on_setup_callbacks = []
84
93
  @on_load_callbacks = {}
85
94
  @on_unload_callbacks = {}
86
95
  @logger = self.class.default_logger
@@ -188,6 +197,17 @@ module Zeitwerk::Loader::Config
188
197
  end
189
198
  end
190
199
 
200
+ # Configure a block to be called after setup and on each reload.
201
+ # If setup was already done, the block runs immediately.
202
+ #
203
+ # @sig () { () -> void } -> void
204
+ def on_setup(&block)
205
+ mutex.synchronize do
206
+ on_setup_callbacks << block
207
+ block.call if @setup
208
+ end
209
+ end
210
+
191
211
  # Configure a block to be invoked once a certain constant path is loaded.
192
212
  # Supports multiple callbacks, and if there are many, they are executed in
193
213
  # the order in which they were defined.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk::Loader::Helpers
2
4
  private
3
5
 
@@ -110,6 +110,8 @@ module Zeitwerk
110
110
  set_autoloads_in_dir(root_dir, namespace)
111
111
  end
112
112
 
113
+ on_setup_callbacks.each(&:call)
114
+
113
115
  @setup = true
114
116
  end
115
117
  end
@@ -143,7 +145,7 @@ module Zeitwerk
143
145
  # Could happen if loaded with require_relative. That is unsupported,
144
146
  # and the constant path would escape unloadable_cpath? This is just
145
147
  # defensive code to clean things up as much as we are able to.
146
- unload_cref(parent, cname) if cdef?(parent, cname)
148
+ unload_cref(parent, cname)
147
149
  unloaded_files.add(abspath) if ruby?(abspath)
148
150
  end
149
151
  end
@@ -154,7 +156,7 @@ module Zeitwerk
154
156
  run_on_unload_callbacks(cpath, value, abspath)
155
157
  end
156
158
 
157
- unload_cref(parent, cname) if cdef?(parent, cname)
159
+ unload_cref(parent, cname)
158
160
  unloaded_files.add(abspath) if ruby?(abspath)
159
161
  end
160
162
 
@@ -208,25 +210,28 @@ module Zeitwerk
208
210
  # Eager loads all files in the root directories, recursively. Files do not
209
211
  # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
210
212
  # are not eager loaded. You can opt-out specifically in specific files and
211
- # directories with `do_not_eager_load`.
213
+ # directories with `do_not_eager_load`, and that can be overridden passing
214
+ # `force: true`.
212
215
  #
213
- # @sig () -> void
214
- def eager_load
216
+ # @sig (true | false) -> void
217
+ def eager_load(force: false)
215
218
  mutex.synchronize do
216
219
  break if @eager_loaded
217
220
 
218
221
  log("eager load start") if logger
219
222
 
223
+ honour_exclusions = !force
224
+
220
225
  queue = []
221
226
  actual_root_dirs.each do |root_dir, namespace|
222
- queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
227
+ queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
223
228
  end
224
229
 
225
230
  while to_eager_load = queue.shift
226
231
  namespace, dir = to_eager_load
227
232
 
228
233
  ls(dir) do |basename, abspath|
229
- next if excluded_from_eager_load?(abspath)
234
+ next if honour_exclusions && excluded_from_eager_load?(abspath)
230
235
 
231
236
  if ruby?(abspath)
232
237
  if cref = autoloads.cref_for(abspath)
@@ -489,7 +494,13 @@ module Zeitwerk
489
494
 
490
495
  # @sig (Module, Symbol) -> void
491
496
  def unload_cref(parent, cname)
497
+ # Let's optimistically remove_const. The way we use it, this is going to
498
+ # succeed always if all is good.
492
499
  parent.__send__(:remove_const, cname)
500
+ rescue ::NameError
501
+ # There are a few edge scenarios in which this may happen. If the constant
502
+ # is gone, that is OK, anyway.
503
+ else
493
504
  log("#{cpath(parent, cname)} unloaded") if logger
494
505
  end
495
506
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Zeitwerk::RealModName
2
4
  UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
3
5
  private_constant :UNBOUND_METHOD_MODULE_NAME
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.5.0.beta3"
4
+ VERSION = "2.5.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: 2.5.0.beta3
4
+ version: 2.5.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: 2021-09-01 00:00:00.000000000 Z
11
+ date: 2021-10-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem
@@ -55,9 +55,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
55
  version: '2.5'
56
56
  required_rubygems_version: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - ">"
58
+ - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 1.3.1
60
+ version: '0'
61
61
  requirements: []
62
62
  rubygems_version: 3.2.22
63
63
  signing_key: