zeitwerk 2.5.0.beta → 2.5.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 +4 -4
- data/README.md +133 -18
- data/lib/zeitwerk/error.rb +2 -0
- data/lib/zeitwerk/explicit_namespace.rb +3 -1
- data/lib/zeitwerk/kernel.rb +1 -1
- data/lib/zeitwerk/loader/callbacks.rb +10 -5
- data/lib/zeitwerk/loader/config.rb +23 -10
- data/lib/zeitwerk/loader/helpers.rb +2 -0
- data/lib/zeitwerk/loader.rb +69 -33
- data/lib/zeitwerk/real_mod_name.rb +2 -0
- data/lib/zeitwerk/registry.rb +9 -0
- data/lib/zeitwerk/version.rb +1 -1
- data/lib/zeitwerk.rb +11 -1
- metadata +5 -6
- data/lib/zeitwerk/autoloads.rb +0 -69
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f424f7f7538a469affa93fbe0c24533d146eb8dfe7289f3c5167b36abd0e362
|
|
4
|
+
data.tar.gz: 8aa11ee61f57a965e3fbb705713f6a4dcf0e3586dc8a3e90f35801b8e8ff9e12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70022bbaa975835042e6550a3e1f8ca8e09e7e1f764d29c659adc9d6c727765fea276da4425ae43e7f4baaaaae6f167d9f4c567e63c0d322ac553603f2d6063b
|
|
7
|
+
data.tar.gz: 7144f057043a326be20b866b2a23dc3c2b5cd3274a084a73770e6b643f65e308ba2eacfbf87701092ee20d440e256d0afe37066933d396cb332d28e5108775a7
|
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
|
-
- [
|
|
34
|
-
|
|
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,15 +45,19 @@
|
|
|
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)
|
|
59
|
+
- [Kernel#require is brittle](#kernelrequire-is-brittle)
|
|
60
|
+
- [Rails autoloading was brittle](#rails-autoloading-was-brittle)
|
|
52
61
|
- [Thanks](#thanks)
|
|
53
62
|
- [License](#license)
|
|
54
63
|
|
|
@@ -157,6 +166,16 @@ class HttpCrawler
|
|
|
157
166
|
end
|
|
158
167
|
```
|
|
159
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
|
+
|
|
160
179
|
<a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
|
|
161
180
|
### Root directories and root namespaces
|
|
162
181
|
|
|
@@ -275,6 +294,23 @@ To illustrate usage of glob patterns, if `actions` in the example above is part
|
|
|
275
294
|
loader.collapse("#{__dir__}/*/actions")
|
|
276
295
|
```
|
|
277
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
|
+
|
|
278
314
|
<a id="markdown-usage" name="usage"></a>
|
|
279
315
|
## Usage
|
|
280
316
|
|
|
@@ -377,7 +413,16 @@ Zeitwerk instances are able to eager load their managed files:
|
|
|
377
413
|
loader.eager_load
|
|
378
414
|
```
|
|
379
415
|
|
|
380
|
-
That skips [ignored files and directories](#ignoring-parts-of-the-project)
|
|
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:
|
|
381
426
|
|
|
382
427
|
```ruby
|
|
383
428
|
db_adapters = "#{__dir__}/my_gem/db_adapters"
|
|
@@ -386,13 +431,20 @@ loader.setup
|
|
|
386
431
|
loader.eager_load # won't eager load the database adapters
|
|
387
432
|
```
|
|
388
433
|
|
|
389
|
-
|
|
434
|
+
However, that can be overridden with `force`:
|
|
390
435
|
|
|
391
|
-
|
|
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).
|
|
392
441
|
|
|
393
|
-
|
|
442
|
+
The `force` flag does not affect ignored files and directories, those are still ignored.
|
|
394
443
|
|
|
395
|
-
|
|
444
|
+
<a id="markdown-global-eager-load" name="global-eager-load"></a>
|
|
445
|
+
#### Global eager load
|
|
446
|
+
|
|
447
|
+
If you want to eager load yourself and all dependencies that use Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
|
396
448
|
|
|
397
449
|
```ruby
|
|
398
450
|
Zeitwerk::Loader.eager_load_all
|
|
@@ -402,6 +454,8 @@ This may be handy in top-level services, like web applications.
|
|
|
402
454
|
|
|
403
455
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
|
404
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
|
+
|
|
405
459
|
<a id="markdown-reloading" name="reloading"></a>
|
|
406
460
|
### Reloading
|
|
407
461
|
|
|
@@ -433,11 +487,13 @@ On reloading, client code has to update anything that would otherwise be storing
|
|
|
433
487
|
<a id="markdown-inflection" name="inflection"></a>
|
|
434
488
|
### Inflection
|
|
435
489
|
|
|
436
|
-
Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors.
|
|
490
|
+
Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors, and you can define your own.
|
|
437
491
|
|
|
438
492
|
<a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
|
|
439
493
|
#### Zeitwerk::Inflector
|
|
440
494
|
|
|
495
|
+
Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
|
|
496
|
+
|
|
441
497
|
This is a very basic inflector that converts snake case to camel case:
|
|
442
498
|
|
|
443
499
|
```
|
|
@@ -464,16 +520,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
|
|
464
520
|
|
|
465
521
|
Overrides need to be configured before calling `setup`.
|
|
466
522
|
|
|
467
|
-
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
|
468
|
-
|
|
469
|
-
Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
|
|
523
|
+
The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
|
|
470
524
|
|
|
471
525
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
|
472
526
|
#### Zeitwerk::GemInflector
|
|
473
527
|
|
|
528
|
+
Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
|
|
529
|
+
|
|
474
530
|
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
|
475
531
|
|
|
476
|
-
|
|
532
|
+
The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
|
|
477
533
|
|
|
478
534
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
|
479
535
|
#### Custom inflector
|
|
@@ -549,8 +605,26 @@ class MyGem::Inflector < Zeitwerk::GemInflector
|
|
|
549
605
|
end
|
|
550
606
|
```
|
|
551
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
|
+
|
|
552
626
|
<a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
|
|
553
|
-
|
|
627
|
+
#### The on_load callback
|
|
554
628
|
|
|
555
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.
|
|
556
630
|
|
|
@@ -580,7 +654,7 @@ end
|
|
|
580
654
|
|
|
581
655
|
Some uses cases:
|
|
582
656
|
|
|
583
|
-
* Doing something with
|
|
657
|
+
* Doing something with a reloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
|
|
584
658
|
* Delaying the execution of the block until the class is loaded for performance.
|
|
585
659
|
* Delaying the execution of the block until the class is loaded because it follows the adapter pattern and better not to load the class if the user does not need it.
|
|
586
660
|
|
|
@@ -608,8 +682,10 @@ There are use cases for this last catch-all callback, but they are rare. If you
|
|
|
608
682
|
|
|
609
683
|
If both types of callbacks are defined, the specific ones run first.
|
|
610
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
|
+
|
|
611
687
|
<a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
|
|
612
|
-
|
|
688
|
+
#### The on_unload callback
|
|
613
689
|
|
|
614
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.
|
|
615
691
|
|
|
@@ -820,6 +896,26 @@ Trip = Struct.new { ... } # NOT SUPPORTED
|
|
|
820
896
|
|
|
821
897
|
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
|
822
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
|
+
|
|
823
919
|
<a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
|
|
824
920
|
### Reopening third-party namespaces
|
|
825
921
|
|
|
@@ -874,6 +970,11 @@ With that, when Zeitwerk scans the file system and reaches the gem directories `
|
|
|
874
970
|
<a id="markdown-debuggers" name="debuggers"></a>
|
|
875
971
|
### Debuggers
|
|
876
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
|
+
|
|
877
978
|
<a id="markdown-break" name="break"></a>
|
|
878
979
|
#### Break
|
|
879
980
|
|
|
@@ -892,7 +993,11 @@ Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatib
|
|
|
892
993
|
<a id="markdown-supported-ruby-versions" name="supported-ruby-versions"></a>
|
|
893
994
|
## Supported Ruby versions
|
|
894
995
|
|
|
895
|
-
Zeitwerk works with
|
|
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.)
|
|
896
1001
|
|
|
897
1002
|
<a id="markdown-testing" name="testing"></a>
|
|
898
1003
|
## Testing
|
|
@@ -923,9 +1028,19 @@ and run `bin/test`.
|
|
|
923
1028
|
<a id="markdown-motivation" name="motivation"></a>
|
|
924
1029
|
## Motivation
|
|
925
1030
|
|
|
926
|
-
|
|
1031
|
+
<a id="markdown-kernelrequire-is-brittle" name="kernelrequire-is-brittle"></a>
|
|
1032
|
+
### Kernel#require is brittle
|
|
1033
|
+
|
|
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.
|
|
1035
|
+
|
|
1036
|
+
Also, if the project has namespaces, setting things up and getting client code to load things in a consistent way needs discipline. For example, `require "foo/bar"` may define `Foo`, instead of reopen it. That may be a broken window, giving place to superclass mismatches or partially-defined namespaces.
|
|
1037
|
+
|
|
1038
|
+
With Zeitwerk, you just name things following conventions and done. Things are available everywhere, and descend is always orderly. Without effort and without broken windows.
|
|
1039
|
+
|
|
1040
|
+
<a id="markdown-rails-autoloading-was-brittle" name="rails-autoloading-was-brittle"></a>
|
|
1041
|
+
### Rails autoloading was brittle
|
|
927
1042
|
|
|
928
|
-
|
|
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.
|
|
929
1044
|
|
|
930
1045
|
<a id="markdown-thanks" name="thanks"></a>
|
|
931
1046
|
## Thanks
|
data/lib/zeitwerk/error.rb
CHANGED
|
@@ -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
|
|
@@ -41,7 +43,7 @@ module Zeitwerk
|
|
|
41
43
|
|
|
42
44
|
# @private
|
|
43
45
|
# @sig (Zeitwerk::Loader) -> void
|
|
44
|
-
def
|
|
46
|
+
def unregister_loader(loader)
|
|
45
47
|
cpaths.delete_if { |_cpath, l| l == loader }
|
|
46
48
|
disable_tracer_if_unneeded
|
|
47
49
|
end
|
data/lib/zeitwerk/kernel.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Kernel
|
|
4
4
|
module_function
|
|
5
5
|
|
|
6
|
-
# We are going to decorate
|
|
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
|
|
16
|
-
log("constant #{cpath} loaded from file #{file}")
|
|
17
|
-
|
|
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.
|
|
@@ -247,17 +267,10 @@ module Zeitwerk::Loader::Config
|
|
|
247
267
|
|
|
248
268
|
# @private
|
|
249
269
|
# @sig (String) -> bool
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return false if dir.start_with?(ignored_path + "/")
|
|
270
|
+
def ignores?(abspath)
|
|
271
|
+
ignored_paths.any? do |ignored_path|
|
|
272
|
+
ignored_path == abspath || (dir?(ignored_path) && abspath.start_with?(ignored_path + "/"))
|
|
254
273
|
end
|
|
255
|
-
|
|
256
|
-
root_dirs.each_key do |root_dir|
|
|
257
|
-
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
false
|
|
261
274
|
end
|
|
262
275
|
|
|
263
276
|
private
|
data/lib/zeitwerk/loader.rb
CHANGED
|
@@ -14,22 +14,16 @@ module Zeitwerk
|
|
|
14
14
|
include Helpers
|
|
15
15
|
include Config
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
# executed
|
|
17
|
+
# Maps absolute paths for which an autoload has been set ---and not
|
|
18
|
+
# executed--- to their corresponding parent class or module and constant
|
|
19
|
+
# name.
|
|
19
20
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# and invoke user callbacks. If reloading is enabled, remember cref and
|
|
24
|
-
# abspath for later unloading logic.
|
|
25
|
-
#
|
|
26
|
-
# 2. When unloading, remove autoloads that have not been executed.
|
|
27
|
-
#
|
|
28
|
-
# 3. Eager load with a recursive const_get, rather than a recursive require,
|
|
29
|
-
# for consistency with lazy loading.
|
|
21
|
+
# "/Users/fxn/blog/app/models/user.rb" => [Object, :User],
|
|
22
|
+
# "/Users/fxn/blog/app/models/hotel/pricing.rb" => [Hotel, :Pricing]
|
|
23
|
+
# ...
|
|
30
24
|
#
|
|
31
25
|
# @private
|
|
32
|
-
# @sig
|
|
26
|
+
# @sig Hash[String, [Module, Symbol]]
|
|
33
27
|
attr_reader :autoloads
|
|
34
28
|
|
|
35
29
|
# We keep track of autoloaded directories to remove them from the registry
|
|
@@ -87,7 +81,7 @@ module Zeitwerk
|
|
|
87
81
|
def initialize
|
|
88
82
|
super
|
|
89
83
|
|
|
90
|
-
@autoloads =
|
|
84
|
+
@autoloads = {}
|
|
91
85
|
@autoloaded_dirs = []
|
|
92
86
|
@to_unload = {}
|
|
93
87
|
@lazy_subdirs = Hash.new { |h, cpath| h[cpath] = [] }
|
|
@@ -110,6 +104,8 @@ module Zeitwerk
|
|
|
110
104
|
set_autoloads_in_dir(root_dir, namespace)
|
|
111
105
|
end
|
|
112
106
|
|
|
107
|
+
on_setup_callbacks.each(&:call)
|
|
108
|
+
|
|
113
109
|
@setup = true
|
|
114
110
|
end
|
|
115
111
|
end
|
|
@@ -121,7 +117,10 @@ module Zeitwerk
|
|
|
121
117
|
# else, they are eligible for garbage collection, which would effectively
|
|
122
118
|
# unload them.
|
|
123
119
|
#
|
|
124
|
-
#
|
|
120
|
+
# This method is public but undocumented. Main interface is `reload`, which
|
|
121
|
+
# means `unload` + `setup`. This one is avaiable to be used together with
|
|
122
|
+
# `unregister`, which is undocumented too.
|
|
123
|
+
#
|
|
125
124
|
# @sig () -> void
|
|
126
125
|
def unload
|
|
127
126
|
mutex.synchronize do
|
|
@@ -133,14 +132,14 @@ module Zeitwerk
|
|
|
133
132
|
# is enough.
|
|
134
133
|
unloaded_files = Set.new
|
|
135
134
|
|
|
136
|
-
autoloads.each do |(parent, cname)
|
|
135
|
+
autoloads.each do |abspath, (parent, cname)|
|
|
137
136
|
if parent.autoload?(cname)
|
|
138
137
|
unload_autoload(parent, cname)
|
|
139
138
|
else
|
|
140
139
|
# Could happen if loaded with require_relative. That is unsupported,
|
|
141
140
|
# and the constant path would escape unloadable_cpath? This is just
|
|
142
141
|
# defensive code to clean things up as much as we are able to.
|
|
143
|
-
unload_cref(parent, cname)
|
|
142
|
+
unload_cref(parent, cname)
|
|
144
143
|
unloaded_files.add(abspath) if ruby?(abspath)
|
|
145
144
|
end
|
|
146
145
|
end
|
|
@@ -151,7 +150,7 @@ module Zeitwerk
|
|
|
151
150
|
run_on_unload_callbacks(cpath, value, abspath)
|
|
152
151
|
end
|
|
153
152
|
|
|
154
|
-
unload_cref(parent, cname)
|
|
153
|
+
unload_cref(parent, cname)
|
|
155
154
|
unloaded_files.add(abspath) if ruby?(abspath)
|
|
156
155
|
end
|
|
157
156
|
|
|
@@ -176,7 +175,7 @@ module Zeitwerk
|
|
|
176
175
|
lazy_subdirs.clear
|
|
177
176
|
|
|
178
177
|
Registry.on_unload(self)
|
|
179
|
-
ExplicitNamespace.
|
|
178
|
+
ExplicitNamespace.unregister_loader(self)
|
|
180
179
|
|
|
181
180
|
@setup = false
|
|
182
181
|
@eager_loaded = false
|
|
@@ -205,28 +204,31 @@ module Zeitwerk
|
|
|
205
204
|
# Eager loads all files in the root directories, recursively. Files do not
|
|
206
205
|
# need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
|
|
207
206
|
# are not eager loaded. You can opt-out specifically in specific files and
|
|
208
|
-
# directories with `do_not_eager_load
|
|
207
|
+
# directories with `do_not_eager_load`, and that can be overridden passing
|
|
208
|
+
# `force: true`.
|
|
209
209
|
#
|
|
210
|
-
# @sig () -> void
|
|
211
|
-
def eager_load
|
|
210
|
+
# @sig (true | false) -> void
|
|
211
|
+
def eager_load(force: false)
|
|
212
212
|
mutex.synchronize do
|
|
213
213
|
break if @eager_loaded
|
|
214
214
|
|
|
215
215
|
log("eager load start") if logger
|
|
216
216
|
|
|
217
|
+
honour_exclusions = !force
|
|
218
|
+
|
|
217
219
|
queue = []
|
|
218
220
|
actual_root_dirs.each do |root_dir, namespace|
|
|
219
|
-
queue << [namespace, root_dir] unless excluded_from_eager_load?(root_dir)
|
|
221
|
+
queue << [namespace, root_dir] unless honour_exclusions && excluded_from_eager_load?(root_dir)
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
while to_eager_load = queue.shift
|
|
223
225
|
namespace, dir = to_eager_load
|
|
224
226
|
|
|
225
227
|
ls(dir) do |basename, abspath|
|
|
226
|
-
next if excluded_from_eager_load?(abspath)
|
|
228
|
+
next if honour_exclusions && excluded_from_eager_load?(abspath)
|
|
227
229
|
|
|
228
230
|
if ruby?(abspath)
|
|
229
|
-
if cref = autoloads
|
|
231
|
+
if cref = autoloads[abspath]
|
|
230
232
|
cget(*cref)
|
|
231
233
|
end
|
|
232
234
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
|
@@ -267,6 +269,15 @@ module Zeitwerk
|
|
|
267
269
|
to_unload.keys.freeze
|
|
268
270
|
end
|
|
269
271
|
|
|
272
|
+
# This is a dangerous method.
|
|
273
|
+
#
|
|
274
|
+
# @experimental
|
|
275
|
+
# @sig () -> void
|
|
276
|
+
def unregister
|
|
277
|
+
Registry.unregister_loader(self)
|
|
278
|
+
ExplicitNamespace.unregister_loader(self)
|
|
279
|
+
end
|
|
280
|
+
|
|
270
281
|
# --- Class methods ---------------------------------------------------------------------------
|
|
271
282
|
|
|
272
283
|
class << self
|
|
@@ -359,7 +370,7 @@ module Zeitwerk
|
|
|
359
370
|
|
|
360
371
|
# @sig (Module, Symbol, String) -> void
|
|
361
372
|
def autoload_subdir(parent, cname, subdir)
|
|
362
|
-
if autoload_path =
|
|
373
|
+
if autoload_path = autoload_path_set_by_me_for?(parent, cname)
|
|
363
374
|
cpath = cpath(parent, cname)
|
|
364
375
|
register_explicit_namespace(cpath) if ruby?(autoload_path)
|
|
365
376
|
# We do not need to issue another autoload, the existing one is enough
|
|
@@ -415,7 +426,7 @@ module Zeitwerk
|
|
|
415
426
|
|
|
416
427
|
# @sig (Module, Symbol, String) -> void
|
|
417
428
|
def set_autoload(parent, cname, abspath)
|
|
418
|
-
|
|
429
|
+
parent.autoload(cname, abspath)
|
|
419
430
|
|
|
420
431
|
if logger
|
|
421
432
|
if ruby?(abspath)
|
|
@@ -425,6 +436,7 @@ module Zeitwerk
|
|
|
425
436
|
end
|
|
426
437
|
end
|
|
427
438
|
|
|
439
|
+
autoloads[abspath] = [parent, cname]
|
|
428
440
|
Registry.register_autoload(self, abspath)
|
|
429
441
|
|
|
430
442
|
# See why in the documentation of Zeitwerk::Registry.inceptions.
|
|
@@ -433,6 +445,15 @@ module Zeitwerk
|
|
|
433
445
|
end
|
|
434
446
|
end
|
|
435
447
|
|
|
448
|
+
# @sig (Module, Symbol) -> String?
|
|
449
|
+
def autoload_path_set_by_me_for?(parent, cname)
|
|
450
|
+
if autoload_path = strict_autoload_path(parent, cname)
|
|
451
|
+
autoload_path if autoloads.key?(autoload_path)
|
|
452
|
+
else
|
|
453
|
+
Registry.inception?(cpath(parent, cname))
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
436
457
|
# @sig (String) -> void
|
|
437
458
|
def register_explicit_namespace(cpath)
|
|
438
459
|
ExplicitNamespace.register(cpath, self)
|
|
@@ -442,12 +463,21 @@ module Zeitwerk
|
|
|
442
463
|
def raise_if_conflicting_directory(dir)
|
|
443
464
|
self.class.mutex.synchronize do
|
|
444
465
|
Registry.loaders.each do |loader|
|
|
445
|
-
if loader
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
466
|
+
next if loader == self
|
|
467
|
+
next if loader.ignores?(dir)
|
|
468
|
+
|
|
469
|
+
dir = dir + "/"
|
|
470
|
+
loader.root_dirs.each do |root_dir, _namespace|
|
|
471
|
+
next if ignores?(root_dir)
|
|
472
|
+
|
|
473
|
+
root_dir = root_dir + "/"
|
|
474
|
+
if dir.start_with?(root_dir) || root_dir.start_with?(dir)
|
|
475
|
+
require "pp" # Needed for pretty_inspect, even in Ruby 2.5.
|
|
476
|
+
raise Error,
|
|
477
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir.chop}," \
|
|
478
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
|
479
|
+
EOS
|
|
480
|
+
end
|
|
451
481
|
end
|
|
452
482
|
end
|
|
453
483
|
end
|
|
@@ -468,7 +498,13 @@ module Zeitwerk
|
|
|
468
498
|
|
|
469
499
|
# @sig (Module, Symbol) -> void
|
|
470
500
|
def unload_cref(parent, cname)
|
|
501
|
+
# Let's optimistically remove_const. The way we use it, this is going to
|
|
502
|
+
# succeed always if all is good.
|
|
471
503
|
parent.__send__(:remove_const, cname)
|
|
504
|
+
rescue ::NameError
|
|
505
|
+
# There are a few edge scenarios in which this may happen. If the constant
|
|
506
|
+
# is gone, that is OK, anyway.
|
|
507
|
+
else
|
|
472
508
|
log("#{cpath(parent, cname)} unloaded") if logger
|
|
473
509
|
end
|
|
474
510
|
end
|
data/lib/zeitwerk/registry.rb
CHANGED
|
@@ -73,6 +73,15 @@ module Zeitwerk
|
|
|
73
73
|
loaders << loader
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# @private
|
|
77
|
+
# @sig (Zeitwerk::Loader) -> void
|
|
78
|
+
def unregister_loader(loader)
|
|
79
|
+
loaders.delete(loader)
|
|
80
|
+
loaders_managing_gems.delete_if { |_, l| l == loader }
|
|
81
|
+
autoloads.delete_if { |_, l| l == loader }
|
|
82
|
+
inceptions.delete_if { |_, (_, l)| l == loader }
|
|
83
|
+
end
|
|
84
|
+
|
|
76
85
|
# This method returns always a loader, the same instance for the same root
|
|
77
86
|
# file. That is how Zeitwerk::Loader.for_gem is idempotent.
|
|
78
87
|
#
|
data/lib/zeitwerk/version.rb
CHANGED
data/lib/zeitwerk.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module Zeitwerk
|
|
4
4
|
require_relative "zeitwerk/real_mod_name"
|
|
5
5
|
require_relative "zeitwerk/loader"
|
|
6
|
-
require_relative "zeitwerk/autoloads"
|
|
7
6
|
require_relative "zeitwerk/registry"
|
|
8
7
|
require_relative "zeitwerk/explicit_namespace"
|
|
9
8
|
require_relative "zeitwerk/inflector"
|
|
@@ -11,4 +10,15 @@ module Zeitwerk
|
|
|
11
10
|
require_relative "zeitwerk/kernel"
|
|
12
11
|
require_relative "zeitwerk/error"
|
|
13
12
|
require_relative "zeitwerk/version"
|
|
13
|
+
|
|
14
|
+
# This is a dangerous method.
|
|
15
|
+
#
|
|
16
|
+
# @experimental
|
|
17
|
+
# @sig () -> void
|
|
18
|
+
def self.with_loader
|
|
19
|
+
loader = Zeitwerk::Loader.new
|
|
20
|
+
yield loader
|
|
21
|
+
ensure
|
|
22
|
+
loader.unregister
|
|
23
|
+
end
|
|
14
24
|
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.
|
|
4
|
+
version: 2.5.1
|
|
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-
|
|
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
|
|
@@ -23,7 +23,6 @@ files:
|
|
|
23
23
|
- MIT-LICENSE
|
|
24
24
|
- README.md
|
|
25
25
|
- lib/zeitwerk.rb
|
|
26
|
-
- lib/zeitwerk/autoloads.rb
|
|
27
26
|
- lib/zeitwerk/error.rb
|
|
28
27
|
- lib/zeitwerk/explicit_namespace.rb
|
|
29
28
|
- lib/zeitwerk/gem_inflector.rb
|
|
@@ -55,11 +54,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
55
54
|
version: '2.5'
|
|
56
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
56
|
requirements:
|
|
58
|
-
- - "
|
|
57
|
+
- - ">="
|
|
59
58
|
- !ruby/object:Gem::Version
|
|
60
|
-
version:
|
|
59
|
+
version: '0'
|
|
61
60
|
requirements: []
|
|
62
|
-
rubygems_version: 3.
|
|
61
|
+
rubygems_version: 3.2.22
|
|
63
62
|
signing_key:
|
|
64
63
|
specification_version: 4
|
|
65
64
|
summary: Efficient and thread-safe constant autoloader
|
data/lib/zeitwerk/autoloads.rb
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
module Zeitwerk
|
|
2
|
-
# @private
|
|
3
|
-
class Autoloads
|
|
4
|
-
# Maps crefs for which an autoload has been defined to the corresponding
|
|
5
|
-
# absolute path.
|
|
6
|
-
#
|
|
7
|
-
# [Object, :User] => "/Users/fxn/blog/app/models/user.rb"
|
|
8
|
-
# [Object, :Hotel] => "/Users/fxn/blog/app/models/hotel"
|
|
9
|
-
# ...
|
|
10
|
-
#
|
|
11
|
-
# This colection is transient, callbacks delete its entries as autoloads get
|
|
12
|
-
# executed.
|
|
13
|
-
#
|
|
14
|
-
# @sig Hash[[Module, Symbol], String]
|
|
15
|
-
attr_reader :c2a
|
|
16
|
-
|
|
17
|
-
# This is the inverse of c2a, for inverse lookups.
|
|
18
|
-
#
|
|
19
|
-
# @sig Hash[String, [Module, Symbol]]
|
|
20
|
-
attr_reader :a2c
|
|
21
|
-
|
|
22
|
-
# @sig () -> void
|
|
23
|
-
def initialize
|
|
24
|
-
@c2a = {}
|
|
25
|
-
@a2c = {}
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# @sig (Module, Symbol, String) -> void
|
|
29
|
-
def define(parent, cname, abspath)
|
|
30
|
-
parent.autoload(cname, abspath)
|
|
31
|
-
cref = [parent, cname]
|
|
32
|
-
c2a[cref] = abspath
|
|
33
|
-
a2c[abspath] = cref
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# @sig () { () -> [[Module, Symbol], String] } -> void
|
|
37
|
-
def each(&block)
|
|
38
|
-
c2a.each(&block)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @sig (Module, Symbol) -> String?
|
|
42
|
-
def abspath_for(parent, cname)
|
|
43
|
-
c2a[[parent, cname]]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# @sig (String) -> [Module, Symbol]?
|
|
47
|
-
def cref_for(abspath)
|
|
48
|
-
a2c[abspath]
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# @sig (String) -> [Module, Symbol]?
|
|
52
|
-
def delete(abspath)
|
|
53
|
-
cref = a2c.delete(abspath)
|
|
54
|
-
c2a.delete(cref)
|
|
55
|
-
cref
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# @sig () -> void
|
|
59
|
-
def clear
|
|
60
|
-
c2a.clear
|
|
61
|
-
a2c.clear
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# @sig () -> bool
|
|
65
|
-
def empty?
|
|
66
|
-
c2a.empty? && a2c.empty?
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|