zeitwerk 2.1.10 → 2.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +20 -0
- data/README.md +272 -27
- data/lib/zeitwerk/gem_inflector.rb +2 -2
- data/lib/zeitwerk/inflector.rb +32 -2
- data/lib/zeitwerk/kernel.rb +31 -0
- data/lib/zeitwerk/loader.rb +79 -22
- data/lib/zeitwerk/loader/callbacks.rb +1 -1
- data/lib/zeitwerk/real_mod_name.rb +8 -2
- data/lib/zeitwerk/version.rb +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b0087cb3e996cc4e5ccfabf179c0e793a811d24e4ee36debf9e07fcb06e1630
|
4
|
+
data.tar.gz: 63c94cc509932eb9dfceb9e951f05092c30e9d3aff8bacda5a66d7da93bf4a64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b978a994c59c4da2e50b32a825626a3e48c4e9e8f3455320f22799437c5fc81de4f3635ab8ea07626f42f03cbea71dc735c70b446942d8e914d5dd1104d15dc
|
7
|
+
data.tar.gz: 2a0d8301b268d419be3dd36902ba44236e9de4a502c203bcbc2d32448f1c1d4e85fbae294b57d200a253e8e61e8ef07c119c9bf5f50e905d50064be6fe62babc
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2019–ω Xavier Noria
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
[![Gem Version](https://img.shields.io/gem/v/zeitwerk.svg?style=for-the-badge)](https://rubygems.org/gems/zeitwerk)
|
6
|
-
[![Build Status](https://img.shields.io/travis/com/fxn/zeitwerk
|
6
|
+
[![Build Status](https://img.shields.io/travis/com/fxn/zeitwerk/master?style=for-the-badge)](https://travis-ci.com/fxn/zeitwerk)
|
7
7
|
|
8
8
|
<!-- TOC -->
|
9
9
|
|
@@ -12,11 +12,15 @@
|
|
12
12
|
- [File structure](#file-structure)
|
13
13
|
- [Implicit namespaces](#implicit-namespaces)
|
14
14
|
- [Explicit namespaces](#explicit-namespaces)
|
15
|
+
- [Collapsing directories](#collapsing-directories)
|
15
16
|
- [Nested root directories](#nested-root-directories)
|
16
17
|
- [Usage](#usage)
|
17
18
|
- [Setup](#setup)
|
18
|
-
|
19
|
+
- [Generic](#generic)
|
20
|
+
- [for_gem](#for_gem)
|
21
|
+
- [Autoloading](#autoloading)
|
19
22
|
- [Eager loading](#eager-loading)
|
23
|
+
- [Reloading](#reloading)
|
20
24
|
- [Inflection](#inflection)
|
21
25
|
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
22
26
|
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
@@ -28,9 +32,14 @@
|
|
28
32
|
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
29
33
|
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
30
34
|
- [Edge cases](#edge-cases)
|
35
|
+
- [Reopening third-party namespaces](#reopening-third-party-namespaces)
|
31
36
|
- [Rules of thumb](#rules-of-thumb)
|
37
|
+
- [Debuggers](#debuggers)
|
38
|
+
- [Break](#break)
|
39
|
+
- [Byebug](#byebug)
|
32
40
|
- [Pronunciation](#pronunciation)
|
33
41
|
- [Supported Ruby versions](#supported-ruby-versions)
|
42
|
+
- [Testing](#testing)
|
34
43
|
- [Motivation](#motivation)
|
35
44
|
- [Thanks](#thanks)
|
36
45
|
- [License](#license)
|
@@ -48,7 +57,9 @@ Zeitwerk is also able to reload code, which may be handy while developing web ap
|
|
48
57
|
|
49
58
|
The gem is designed so that any project, gem dependency, application, etc. can have their own independent loader, coexisting in the same process, managing their own project trees, and independent of each other. Each loader has its own configuration, inflector, and optional logger.
|
50
59
|
|
51
|
-
Internally, Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`.
|
60
|
+
Internally, Zeitwerk issues `require` calls exclusively using absolute file names, so there are no costly file system lookups in `$LOAD_PATH`. Technically, the directories managed by Zeitwerk do not even need to be in `$LOAD_PATH`.
|
61
|
+
|
62
|
+
Furthermore, Zeitwerk does at most one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used.
|
52
63
|
|
53
64
|
<a id="markdown-synopsis" name="synopsis"></a>
|
54
65
|
## Synopsis
|
@@ -162,6 +173,32 @@ end
|
|
162
173
|
|
163
174
|
An explicit namespace must be managed by one single loader. Loaders that reopen namespaces owned by other projects are responsible for loading their constants before setup.
|
164
175
|
|
176
|
+
<a id="markdown-collapsing-directories" name="collapsing-directories"></a>
|
177
|
+
### Collapsing directories
|
178
|
+
|
179
|
+
Say some directories in a project exist for organizational purposes only, and you prefer not to have them as namespaces. For example, the `actions` subdirectory in the next example is not meant to represent a namespace, it is there only to group all actions related to bookings:
|
180
|
+
|
181
|
+
```
|
182
|
+
booking.rb -> Booking
|
183
|
+
booking/actions/create.rb -> Booking::Create
|
184
|
+
```
|
185
|
+
|
186
|
+
To make it work that way, configure Zeitwerk to collapse said directory:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
loader.collapse("booking/actions")
|
190
|
+
```
|
191
|
+
|
192
|
+
This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
|
193
|
+
|
194
|
+
You can pass directories and glob patterns. Glob patterns are expanded when they are added, and again on each reload.
|
195
|
+
|
196
|
+
To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
loader.collapse("*/actions")
|
200
|
+
```
|
201
|
+
|
165
202
|
<a id="markdown-nested-root-directories" name="nested-root-directories"></a>
|
166
203
|
### Nested root directories
|
167
204
|
|
@@ -181,6 +218,9 @@ should define `Geolocatable`, not `Concerns::Geolocatable`.
|
|
181
218
|
<a id="markdown-setup" name="setup"></a>
|
182
219
|
### Setup
|
183
220
|
|
221
|
+
<a id="markdown-generic" name="generic"></a>
|
222
|
+
#### Generic
|
223
|
+
|
184
224
|
Loaders are ready to load code right after calling `setup` on them:
|
185
225
|
|
186
226
|
```ruby
|
@@ -197,37 +237,73 @@ loader.push_dir(...)
|
|
197
237
|
loader.setup
|
198
238
|
```
|
199
239
|
|
200
|
-
|
240
|
+
<a id="markdown-for_gem" name="for_gem"></a>
|
241
|
+
#### for_gem
|
201
242
|
|
202
|
-
Zeitwerk
|
243
|
+
`Zeitwerk::Loader.for_gem` is a convenience shortcut for the common case in which a gem has its entry point directly under the `lib` directory:
|
203
244
|
|
204
|
-
|
205
|
-
|
245
|
+
```
|
246
|
+
lib/my_gem.rb # MyGem
|
247
|
+
lib/my_gem/version.rb # MyGem::VERSION
|
248
|
+
lib/my_gem/foo.rb # MyGem::Foo
|
249
|
+
```
|
206
250
|
|
207
|
-
|
251
|
+
Neither a gemspec nor a version file are technically required, this helper works as long as the code is organized using that standard structure.
|
252
|
+
|
253
|
+
If the entry point of your gem lives in a subdirectory of `lib` because it is reopening a namespace defined somewhere else, please use the generic API to setup the loader, and make sure you check the section [_Reopening third-party namespaces_](https://github.com/fxn/zeitwerk#reopening-third-party-namespaces) down below.
|
254
|
+
|
255
|
+
Conceptually, `for_gem` translates to:
|
208
256
|
|
209
257
|
```ruby
|
258
|
+
# lib/my_gem.rb
|
259
|
+
|
260
|
+
require "zeitwerk"
|
210
261
|
loader = Zeitwerk::Loader.new
|
211
|
-
loader.
|
212
|
-
loader.
|
262
|
+
loader.tag = File.basename(__FILE__, ".rb")
|
263
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
264
|
+
loader.push_dir(__dir__)
|
265
|
+
```
|
266
|
+
|
267
|
+
except that this method returns the same object in subsequent calls from the same file, in the unlikely case the gem wants to be able to reload.
|
268
|
+
|
269
|
+
If the main module references project constants at the top-level, Zeitwerk has to be ready to load them. Their definitions, in turn, may reference other project constants. And this is recursive. Therefore, it is important that the `setup` call happens above the main module definition:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
# lib/my_gem.rb (main file)
|
273
|
+
|
274
|
+
require "zeitwerk"
|
275
|
+
loader = Zeitwerk::Loader.for_gem
|
213
276
|
loader.setup
|
214
|
-
|
215
|
-
|
277
|
+
|
278
|
+
module MyGem
|
279
|
+
# Since the setup has been performed, at this point we are already able
|
280
|
+
# to reference project constants, in this case MyGem::MyLogger.
|
281
|
+
include MyLogger
|
282
|
+
end
|
216
283
|
```
|
217
284
|
|
218
|
-
|
285
|
+
<a id="markdown-autoloading" name="autoloading"></a>
|
286
|
+
### Autoloading
|
219
287
|
|
220
|
-
|
288
|
+
After `setup`, you are able to reference classes and modules from the project without issuing `require` calls for them. They are all available everywhere, autoloading loads them on demand. This works even if the reference to the class or module is first hit in client code, outside your project.
|
221
289
|
|
222
|
-
|
290
|
+
Let's revisit the example above:
|
223
291
|
|
224
|
-
|
292
|
+
```ruby
|
293
|
+
# lib/my_gem.rb (main file)
|
225
294
|
|
226
|
-
|
295
|
+
require "zeitwerk"
|
296
|
+
loader = Zeitwerk::Loader.for_gem
|
297
|
+
loader.setup
|
227
298
|
|
228
|
-
|
299
|
+
module MyGem
|
300
|
+
include MyLogger # (*)
|
301
|
+
end
|
302
|
+
```
|
229
303
|
|
230
|
-
|
304
|
+
That works, and there is no `require "my_gem/my_logger"`. When `(*)` is reached, Zeitwerk seamlessly autoloads `MyGem::MyLogger`.
|
305
|
+
|
306
|
+
If autoloading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
231
307
|
|
232
308
|
<a id="markdown-eager-loading" name="eager-loading"></a>
|
233
309
|
### Eager loading
|
@@ -247,8 +323,12 @@ loader.setup
|
|
247
323
|
loader.eager_load # won't eager load the database adapters
|
248
324
|
```
|
249
325
|
|
326
|
+
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).
|
327
|
+
|
250
328
|
Eager loading is synchronized and idempotent.
|
251
329
|
|
330
|
+
If eager loading a file does not define the expected class or module, Zeitwerk raises `Zeitwerk::NameError`, which is a subclass of `NameError`.
|
331
|
+
|
252
332
|
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
253
333
|
|
254
334
|
```ruby
|
@@ -259,6 +339,34 @@ This may be handy in top-level services, like web applications.
|
|
259
339
|
|
260
340
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
261
341
|
|
342
|
+
<a id="markdown-reloading" name="reloading"></a>
|
343
|
+
### Reloading
|
344
|
+
|
345
|
+
Zeitwerk is able to reload code, but you need to enable this feature:
|
346
|
+
|
347
|
+
```ruby
|
348
|
+
loader = Zeitwerk::Loader.new
|
349
|
+
loader.push_dir(...)
|
350
|
+
loader.enable_reloading # you need to opt-in before setup
|
351
|
+
loader.setup
|
352
|
+
...
|
353
|
+
loader.reload
|
354
|
+
```
|
355
|
+
|
356
|
+
There is no way to undo this, either you want to reload or you don't.
|
357
|
+
|
358
|
+
Enabling reloading after setup raises `Zeitwerk::Error`. Attempting to reload without having it enabled raises `Zeitwerk::ReloadingDisabledError`.
|
359
|
+
|
360
|
+
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.
|
361
|
+
|
362
|
+
Reloading removes the currently loaded classes and modules and resets the loader so that it will pick whatever is in the file system now.
|
363
|
+
|
364
|
+
It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
|
365
|
+
|
366
|
+
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.
|
367
|
+
|
368
|
+
On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
|
369
|
+
|
262
370
|
<a id="markdown-inflection" name="inflection"></a>
|
263
371
|
### Inflection
|
264
372
|
|
@@ -275,14 +383,34 @@ users_controller -> UsersController
|
|
275
383
|
html_parser -> HtmlParser
|
276
384
|
```
|
277
385
|
|
386
|
+
The camelize logic can be overridden easily for individual basenames:
|
387
|
+
|
388
|
+
```ruby
|
389
|
+
loader.inflector.inflect(
|
390
|
+
"html_parser" => "HTMLParser",
|
391
|
+
"mysql_adapter" => "MySQLAdapter"
|
392
|
+
)
|
393
|
+
```
|
394
|
+
|
395
|
+
The `inflect` method can be invoked several times if you prefer this other style:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
loader.inflector.inflect "html_parser" => "HTMLParser"
|
399
|
+
loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
400
|
+
```
|
401
|
+
|
402
|
+
Overrides need to be configured before calling `setup`.
|
403
|
+
|
278
404
|
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
279
405
|
|
280
|
-
|
406
|
+
Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
|
281
407
|
|
282
408
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
283
409
|
#### Zeitwerk::GemInflector
|
284
410
|
|
285
|
-
|
411
|
+
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
412
|
+
|
413
|
+
Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
|
286
414
|
|
287
415
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
288
416
|
#### Custom inflector
|
@@ -293,12 +421,9 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
293
421
|
# frozen_string_literal: true
|
294
422
|
|
295
423
|
class MyInflector < Zeitwerk::Inflector
|
296
|
-
def camelize(basename,
|
297
|
-
|
298
|
-
|
299
|
-
"API"
|
300
|
-
when "mysql_adapter"
|
301
|
-
"MySQLAdapter"
|
424
|
+
def camelize(basename, abspath)
|
425
|
+
if basename =~ /\Ahtml_(.*)/
|
426
|
+
"HTML" + super($1, abspath)
|
302
427
|
else
|
303
428
|
super
|
304
429
|
end
|
@@ -318,6 +443,49 @@ loader.inflector = MyInflector.new
|
|
318
443
|
|
319
444
|
This needs to be done before calling `setup`.
|
320
445
|
|
446
|
+
If a custom inflector definition in a gem takes too much space in the main file, you can extract it. For example, this is a simple pattern:
|
447
|
+
|
448
|
+
```ruby
|
449
|
+
# lib/my_gem/inflector.rb
|
450
|
+
module MyGem
|
451
|
+
class Inflector < Zeitwerk::GemInflector
|
452
|
+
...
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# lib/my_gem.rb
|
457
|
+
require "zeitwerk"
|
458
|
+
require_relative "my_gem/inflector"
|
459
|
+
|
460
|
+
loader = Zeitwerk::Loader.for_gem
|
461
|
+
loader.inflector = MyGem::Inflector.new(__FILE__)
|
462
|
+
loader.setup
|
463
|
+
|
464
|
+
module MyGem
|
465
|
+
# ...
|
466
|
+
end
|
467
|
+
```
|
468
|
+
|
469
|
+
Since `MyGem` is referenced before the namespace is defined in the main file, it is important to use this style:
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
# Correct, effectively defines MyGem.
|
473
|
+
module MyGem
|
474
|
+
class Inflector < Zeitwerk::GemInflector
|
475
|
+
# ...
|
476
|
+
end
|
477
|
+
end
|
478
|
+
```
|
479
|
+
|
480
|
+
instead of:
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
# Raises uninitialized constant MyGem (NameError).
|
484
|
+
class MyGem::Inflector < Zeitwerk::GemInflector
|
485
|
+
# ...
|
486
|
+
end
|
487
|
+
```
|
488
|
+
|
321
489
|
<a id="markdown-logging" name="logging"></a>
|
322
490
|
### Logging
|
323
491
|
|
@@ -486,6 +654,42 @@ Trip = Struct.new { ... } # NOT SUPPORTED
|
|
486
654
|
|
487
655
|
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
488
656
|
|
657
|
+
<a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
|
658
|
+
### Reopening third-party namespaces
|
659
|
+
|
660
|
+
Projects managed by Zeitwerk can work with namespaces defined by third-party libraries. However, they have to be loaded in memory before calling `setup`.
|
661
|
+
|
662
|
+
For example, let's imagine you're writing a gem that implements an adapter for [Active Job](https://guides.rubyonrails.org/active_job_basics.html) that uses AwesomeQueue as backend. By convention, your gem has to define a class called `ActiveJob::QueueAdapters::AwesomeQueue`, and it has to do so in a file with a matching path:
|
663
|
+
|
664
|
+
```ruby
|
665
|
+
# lib/active_job/queue_adapters/awesome_queue.rb
|
666
|
+
module ActiveJob
|
667
|
+
module QueueAdapters
|
668
|
+
class AwesomeQueue
|
669
|
+
# ...
|
670
|
+
end
|
671
|
+
end
|
672
|
+
end
|
673
|
+
```
|
674
|
+
|
675
|
+
It is very important that your gem _reopens_ the modules `ActiveJob` and `ActiveJob::QueueAdapters` instead of _defining_ them. Because their proper definition lives in Active Job. Furthermore, if the project reloads, you do not want any of `ActiveJob` or `ActiveJob::QueueAdapters` to be reloaded.
|
676
|
+
|
677
|
+
Bottom line, Zeitwerk should not be managing those namespaces. Active Job owns them and defines them. Your gem needs to _reopen_ them.
|
678
|
+
|
679
|
+
In order to do so, you need to make sure those modules are loaded before calling `setup`. For instance, in the entry file for the gem:
|
680
|
+
|
681
|
+
```ruby
|
682
|
+
# Ensure these namespaces are reopened, not defined.
|
683
|
+
require "active_job"
|
684
|
+
require "active_job/queue_adapters"
|
685
|
+
|
686
|
+
require "zeitwerk"
|
687
|
+
loader = Zeitwerk::Loader.for_gem
|
688
|
+
loader.setup
|
689
|
+
```
|
690
|
+
|
691
|
+
With that, when Zeitwerk scans the file system and reaches the gem directories `lib/active_job` and `lib/active_job/queue_adapters`, it detects the corresponding modules already exist and therefore understands it does not have to manage them. The loader just descends into those directories. Eventually will reach `lib/active_job/queue_adapters/awesome_queue.rb`, and since `ActiveJob::QueueAdapters::AwesomeQueue` is unknown, Zeitwerk will manage it. Which is what happens regularly with the files in your gem. On reload, the namespaces are safe, won't be reloaded. The loader only reloads what it manages, which in this case is the adapter itself.
|
692
|
+
|
489
693
|
<a id="markdown-rules-of-thumb" name="rules-of-thumb"></a>
|
490
694
|
### Rules of thumb
|
491
695
|
|
@@ -501,6 +705,19 @@ This only affects explicit namespaces, those idioms work well for any other ordi
|
|
501
705
|
|
502
706
|
6. In a given process, ideally, there should be at most one loader with reloading enabled. Technically, you can have more, but it may get tricky if one refers to constants managed by the other one. Do that only if you know what you are doing.
|
503
707
|
|
708
|
+
<a id="markdown-debuggers" name="debuggers"></a>
|
709
|
+
### Debuggers
|
710
|
+
|
711
|
+
<a id="markdown-break" name="break"></a>
|
712
|
+
#### Break
|
713
|
+
|
714
|
+
Zeitwerk works fine with [@gsamokovarov](https://github.com/gsamokovarov)'s [Break](https://github.com/gsamokovarov/break) debugger.
|
715
|
+
|
716
|
+
<a id="markdown-byebug" name="byebug"></a>
|
717
|
+
#### Byebug
|
718
|
+
|
719
|
+
Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatible, classes or modules that belong to [explicit namespaces](#explicit-namespaces) are not autoloaded inside a Byebug session. See [this issue](https://github.com/deivid-rodriguez/byebug/issues/564#issuecomment-499413606) for further details.
|
720
|
+
|
504
721
|
<a id="markdown-pronunciation" name="pronunciation"></a>
|
505
722
|
## Pronunciation
|
506
723
|
|
@@ -511,6 +728,32 @@ This only affects explicit namespaces, those idioms work well for any other ordi
|
|
511
728
|
|
512
729
|
Zeitwerk works with MRI 2.4.4 and above.
|
513
730
|
|
731
|
+
<a id="markdown-testing" name="testing"></a>
|
732
|
+
## Testing
|
733
|
+
|
734
|
+
In order to run the test suite of Zeitwerk, `cd` into the project root and execute
|
735
|
+
|
736
|
+
```
|
737
|
+
bin/test
|
738
|
+
```
|
739
|
+
|
740
|
+
To run one particular suite, pass its file name as an argument:
|
741
|
+
|
742
|
+
```
|
743
|
+
bin/test test/lib/zeitwerk/test_eager_load.rb
|
744
|
+
```
|
745
|
+
|
746
|
+
Furthermore, the project has a development dependency on [`minitest-focus`](https://github.com/seattlerb/minitest-focus). To run an individual test mark it with `focus`:
|
747
|
+
|
748
|
+
```ruby
|
749
|
+
focus
|
750
|
+
test "capitalizes the first letter" do
|
751
|
+
assert_equal "User", camelize("user")
|
752
|
+
end
|
753
|
+
```
|
754
|
+
|
755
|
+
and run `bin/test`.
|
756
|
+
|
514
757
|
<a id="markdown-motivation" name="motivation"></a>
|
515
758
|
## Motivation
|
516
759
|
|
@@ -525,6 +768,8 @@ I'd like to thank [@matthewd](https://github.com/matthewd) for the discussions w
|
|
525
768
|
|
526
769
|
Also, would like to thank [@Shopify](https://github.com/Shopify), [@rafaelfranca](https://github.com/rafaelfranca), and [@dylanahsmith](https://github.com/dylanahsmith), for sharing [this PoC](https://github.com/Shopify/autoload_reloader). The technique Zeitwerk uses to support explicit namespaces was copied from that project.
|
527
770
|
|
771
|
+
Jean Boussier ([@casperisfine](https://github.com/casperisfine), [@byroot](https://github.com/byroot)) deserves special mention. Jean migrated autoloading in Shopify when Zeitwerk integration in Rails was yet unreleased. His work and positive attitude have been outstanding, and thanks to his feedback the interface and performance of Zeitwerk are way, way better. Kudos man ❤️.
|
772
|
+
|
528
773
|
Finally, many thanks to [@schurig](https://github.com/schurig) for recording an [audio file](http://share.hashref.com/zeitwerk/zeitwerk_pronunciation.mp3) with the pronunciation of "Zeitwerk" in perfect German. 💯
|
529
774
|
|
530
775
|
<a id="markdown-license" name="license"></a>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Zeitwerk
|
4
|
-
class GemInflector < Inflector
|
4
|
+
class GemInflector < Inflector
|
5
5
|
# @param root_file [String]
|
6
6
|
def initialize(root_file)
|
7
7
|
namespace = File.basename(root_file, ".rb")
|
@@ -13,7 +13,7 @@ module Zeitwerk
|
|
13
13
|
# @param abspath [String]
|
14
14
|
# @return [String]
|
15
15
|
def camelize(basename, abspath)
|
16
|
-
|
16
|
+
abspath == @version_file ? "VERSION" : super
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
data/lib/zeitwerk/inflector.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Zeitwerk
|
4
|
-
class Inflector
|
4
|
+
class Inflector
|
5
5
|
# Very basic snake case -> camel case conversion.
|
6
6
|
#
|
7
7
|
# inflector = Zeitwerk::Inflector.new
|
@@ -9,11 +9,41 @@ module Zeitwerk
|
|
9
9
|
# inflector.camelize("users_controller", ...) # => "UsersController"
|
10
10
|
# inflector.camelize("api", ...) # => "Api"
|
11
11
|
#
|
12
|
+
# Takes into account hard-coded mappings configured with `inflect`.
|
13
|
+
#
|
12
14
|
# @param basename [String]
|
13
15
|
# @param _abspath [String]
|
14
16
|
# @return [String]
|
15
17
|
def camelize(basename, _abspath)
|
16
|
-
basename.split('_').map!(&:capitalize).join
|
18
|
+
overrides[basename] || basename.split('_').map!(&:capitalize).join
|
19
|
+
end
|
20
|
+
|
21
|
+
# Configures hard-coded inflections:
|
22
|
+
#
|
23
|
+
# inflector = Zeitwerk::Inflector.new
|
24
|
+
# inflector.inflect(
|
25
|
+
# "html_parser" => "HTMLParser",
|
26
|
+
# "mysql_adapter" => "MySQLAdapter"
|
27
|
+
# )
|
28
|
+
#
|
29
|
+
# inflector.camelize("html_parser", abspath) # => "HTMLParser"
|
30
|
+
# inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
|
31
|
+
# inflector.camelize("users_controller", abspath) # => "UsersController"
|
32
|
+
#
|
33
|
+
# @param inflections [{String => String}]
|
34
|
+
# @return [void]
|
35
|
+
def inflect(inflections)
|
36
|
+
overrides.merge!(inflections)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Hard-coded basename to constant name user maps that override the default
|
42
|
+
# inflection logic.
|
43
|
+
#
|
44
|
+
# @return [{String => String}]
|
45
|
+
def overrides
|
46
|
+
@overrides ||= {}
|
17
47
|
end
|
18
48
|
end
|
19
49
|
end
|
data/lib/zeitwerk/kernel.rb
CHANGED
@@ -3,6 +3,17 @@
|
|
3
3
|
module Kernel
|
4
4
|
module_function
|
5
5
|
|
6
|
+
# We are going to decorate Kerner#require with two goals.
|
7
|
+
#
|
8
|
+
# First, by intercepting Kernel#require calls, we are able to autovivify
|
9
|
+
# modules on required directories, and also do internal housekeeping when
|
10
|
+
# managed files are loaded.
|
11
|
+
#
|
12
|
+
# On the other hand, if you publish a new version of a gem that is now managed
|
13
|
+
# by Zeitwerk, client code can reference directly your classes and modules and
|
14
|
+
# should not require anything. But if someone has legacy require calls around,
|
15
|
+
# they will work as expected, and in a compatible way.
|
16
|
+
#
|
6
17
|
# We cannot decorate with prepend + super because Kernel has already been
|
7
18
|
# included in Object, and changes in ancestors don't get propagated into
|
8
19
|
# already existing ancestor chains.
|
@@ -30,4 +41,24 @@ module Kernel
|
|
30
41
|
end
|
31
42
|
end
|
32
43
|
end
|
44
|
+
|
45
|
+
# By now, I have seen no way so far to decorate require_relative.
|
46
|
+
#
|
47
|
+
# For starters, at least in CRuby, require_relative does not delegate to
|
48
|
+
# require. Both require and require_relative delegate the bulk of their work
|
49
|
+
# to an internal C function called rb_require_safe. So, our require wrapper is
|
50
|
+
# not executed.
|
51
|
+
#
|
52
|
+
# On the other hand, we cannot use the aliasing technique above because
|
53
|
+
# require_relative receives a path relative to the directory of the file in
|
54
|
+
# which the call is performed. If a wrapper here invoked the original method,
|
55
|
+
# Ruby would resolve the relative path taking lib/zeitwerk as base directory.
|
56
|
+
#
|
57
|
+
# A workaround could be to extract the base directory from caller_locations,
|
58
|
+
# but what if someone else decorated require_relative before us? You can't
|
59
|
+
# really know with certainty where's the original call site in the stack.
|
60
|
+
#
|
61
|
+
# However, the main use case for require_relative is to load files from your
|
62
|
+
# own project. Projects managed by Zeitwerk don't do this for files managed by
|
63
|
+
# Zeitwerk, precisely.
|
33
64
|
end
|
data/lib/zeitwerk/loader.rb
CHANGED
@@ -39,7 +39,7 @@ module Zeitwerk
|
|
39
39
|
# @return [<String>]
|
40
40
|
attr_reader :preloads
|
41
41
|
|
42
|
-
# Absolute paths of files, directories,
|
42
|
+
# Absolute paths of files, directories, or glob patterns to be totally
|
43
43
|
# ignored.
|
44
44
|
#
|
45
45
|
# @private
|
@@ -54,6 +54,19 @@ module Zeitwerk
|
|
54
54
|
# @return [Set<String>]
|
55
55
|
attr_reader :ignored_paths
|
56
56
|
|
57
|
+
# Absolute paths of directories or glob patterns to be collapsed.
|
58
|
+
#
|
59
|
+
# @private
|
60
|
+
# @return [Set<String>]
|
61
|
+
attr_reader :collapse_glob_patterns
|
62
|
+
|
63
|
+
# The actual collection of absolute directory names at the time the collapse
|
64
|
+
# glob patterns were expanded. Computed on setup, and recomputed on reload.
|
65
|
+
#
|
66
|
+
# @private
|
67
|
+
# @return [Set<String>]
|
68
|
+
attr_reader :collapse_dirs
|
69
|
+
|
57
70
|
# Maps real absolute paths for which an autoload has been set ---and not
|
58
71
|
# executed--- to their corresponding parent class or module and constant
|
59
72
|
# name.
|
@@ -131,15 +144,17 @@ module Zeitwerk
|
|
131
144
|
@inflector = Inflector.new
|
132
145
|
@logger = self.class.default_logger
|
133
146
|
|
134
|
-
@root_dirs
|
135
|
-
@preloads
|
136
|
-
@ignored_glob_patterns
|
137
|
-
@ignored_paths
|
138
|
-
@
|
139
|
-
@
|
140
|
-
@
|
141
|
-
@
|
142
|
-
@
|
147
|
+
@root_dirs = {}
|
148
|
+
@preloads = []
|
149
|
+
@ignored_glob_patterns = Set.new
|
150
|
+
@ignored_paths = Set.new
|
151
|
+
@collapse_glob_patterns = Set.new
|
152
|
+
@collapse_dirs = Set.new
|
153
|
+
@autoloads = {}
|
154
|
+
@autoloaded_dirs = []
|
155
|
+
@to_unload = {}
|
156
|
+
@lazy_subdirs = {}
|
157
|
+
@eager_load_exclusions = Set.new
|
143
158
|
|
144
159
|
# TODO: find a better name for these mutexes.
|
145
160
|
@mutex = Mutex.new
|
@@ -154,6 +169,7 @@ module Zeitwerk
|
|
154
169
|
|
155
170
|
# Sets a tag for the loader, useful for logging.
|
156
171
|
#
|
172
|
+
# @param tag [#to_s]
|
157
173
|
# @return [void]
|
158
174
|
def tag=(tag)
|
159
175
|
@tag = tag.to_s
|
@@ -233,6 +249,18 @@ module Zeitwerk
|
|
233
249
|
end
|
234
250
|
end
|
235
251
|
|
252
|
+
# Configure directories or glob patterns to be collapsed.
|
253
|
+
#
|
254
|
+
# @param paths [<String, Pathname, <String, Pathname>>]
|
255
|
+
# @return [void]
|
256
|
+
def collapse(*glob_patterns)
|
257
|
+
glob_patterns = expand_paths(glob_patterns)
|
258
|
+
mutex.synchronize do
|
259
|
+
collapse_glob_patterns.merge(glob_patterns)
|
260
|
+
collapse_dirs.merge(expand_glob_patterns(glob_patterns))
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
236
264
|
# Sets autoloads in the root namespace and preloads files, if any.
|
237
265
|
#
|
238
266
|
# @return [void]
|
@@ -306,7 +334,8 @@ module Zeitwerk
|
|
306
334
|
Registry.on_unload(self)
|
307
335
|
ExplicitNamespace.unregister(self)
|
308
336
|
|
309
|
-
@setup
|
337
|
+
@setup = false
|
338
|
+
@eager_loaded = false
|
310
339
|
end
|
311
340
|
end
|
312
341
|
|
@@ -322,6 +351,7 @@ module Zeitwerk
|
|
322
351
|
if reloading_enabled?
|
323
352
|
unload
|
324
353
|
recompute_ignored_paths
|
354
|
+
recompute_collapse_dirs
|
325
355
|
setup
|
326
356
|
else
|
327
357
|
raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
|
@@ -351,8 +381,12 @@ module Zeitwerk
|
|
351
381
|
cref[0].const_get(cref[1], false)
|
352
382
|
end
|
353
383
|
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
354
|
-
|
355
|
-
|
384
|
+
if collapse_dirs.member?(abspath)
|
385
|
+
queue << [namespace, abspath]
|
386
|
+
else
|
387
|
+
cname = inflector.camelize(basename, abspath)
|
388
|
+
queue << [namespace.const_get(cname, false), abspath]
|
389
|
+
end
|
356
390
|
end
|
357
391
|
end
|
358
392
|
end
|
@@ -430,7 +464,7 @@ module Zeitwerk
|
|
430
464
|
# require "zeitwerk"
|
431
465
|
# loader = Zeitwerk::Loader.new
|
432
466
|
# loader.tag = File.basename(__FILE__, ".rb")
|
433
|
-
# loader.inflector = Zeitwerk::GemInflector.new
|
467
|
+
# loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
434
468
|
# loader.push_dir(__dir__)
|
435
469
|
#
|
436
470
|
# except that this method returns the same object in subsequent calls from
|
@@ -476,7 +510,7 @@ module Zeitwerk
|
|
476
510
|
ls(dir) do |basename, abspath|
|
477
511
|
begin
|
478
512
|
if ruby?(basename)
|
479
|
-
basename
|
513
|
+
basename[-3..-1] = ''
|
480
514
|
cname = inflector.camelize(basename, abspath).to_sym
|
481
515
|
autoload_file(parent, cname, abspath)
|
482
516
|
elsif dir?(abspath)
|
@@ -488,13 +522,17 @@ module Zeitwerk
|
|
488
522
|
# it counts only as root. The guard checks that.
|
489
523
|
unless root_dirs.key?(abspath)
|
490
524
|
cname = inflector.camelize(basename, abspath).to_sym
|
491
|
-
|
525
|
+
if collapse_dirs.member?(abspath)
|
526
|
+
set_autoloads_in_dir(abspath, parent)
|
527
|
+
else
|
528
|
+
autoload_subdir(parent, cname, abspath)
|
529
|
+
end
|
492
530
|
end
|
493
531
|
end
|
494
532
|
rescue ::NameError => error
|
495
533
|
path_type = ruby?(abspath) ? "file" : "directory"
|
496
534
|
|
497
|
-
raise NameError,
|
535
|
+
raise NameError.new(<<~MESSAGE, error.name)
|
498
536
|
#{error.message} inferred by #{inflector.class} from #{path_type}
|
499
537
|
|
500
538
|
#{abspath}
|
@@ -558,7 +596,7 @@ module Zeitwerk
|
|
558
596
|
end
|
559
597
|
|
560
598
|
# @param dir [String] directory that would have autovivified a module
|
561
|
-
# @param file [String] the file where the namespace is
|
599
|
+
# @param file [String] the file where the namespace is explicitly defined
|
562
600
|
# @param parent [Module]
|
563
601
|
# @param cname [Symbol]
|
564
602
|
# @return [void]
|
@@ -578,7 +616,10 @@ module Zeitwerk
|
|
578
616
|
# $LOADED_FEATURES stores real paths since Ruby 2.4.4. We set and save the
|
579
617
|
# real path to be able to delete it from $LOADED_FEATURES on unload, and to
|
580
618
|
# be able to do a lookup later in Kernel#require for manual require calls.
|
581
|
-
|
619
|
+
#
|
620
|
+
# We freeze realpath because that saves allocations in Module#autoload.
|
621
|
+
# See #125.
|
622
|
+
realpath = File.realpath(abspath).freeze
|
582
623
|
parent.autoload(cname, realpath)
|
583
624
|
if logger
|
584
625
|
if ruby?(realpath)
|
@@ -623,8 +664,14 @@ module Zeitwerk
|
|
623
664
|
# @param parent [Module]
|
624
665
|
# @param cname [Symbol]
|
625
666
|
# @return [String, nil]
|
626
|
-
|
627
|
-
|
667
|
+
if method(:autoload?).arity == 1
|
668
|
+
def strict_autoload_path(parent, cname)
|
669
|
+
parent.autoload?(cname) if cdef?(parent, cname)
|
670
|
+
end
|
671
|
+
else
|
672
|
+
def strict_autoload_path(parent, cname)
|
673
|
+
parent.autoload?(cname, false)
|
674
|
+
end
|
628
675
|
end
|
629
676
|
|
630
677
|
# This method is called this way because I prefer `preload` to be the method
|
@@ -675,8 +722,13 @@ module Zeitwerk
|
|
675
722
|
def ls(dir)
|
676
723
|
Dir.foreach(dir) do |basename|
|
677
724
|
next if basename.start_with?(".")
|
725
|
+
|
678
726
|
abspath = File.join(dir, basename)
|
679
|
-
|
727
|
+
next if ignored_paths.member?(abspath)
|
728
|
+
|
729
|
+
# We freeze abspath because that saves allocations when passed later to
|
730
|
+
# File methods. See #125.
|
731
|
+
yield basename, abspath.freeze
|
680
732
|
end
|
681
733
|
end
|
682
734
|
|
@@ -711,6 +763,11 @@ module Zeitwerk
|
|
711
763
|
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
712
764
|
end
|
713
765
|
|
766
|
+
# @return [void]
|
767
|
+
def recompute_collapse_dirs
|
768
|
+
collapse_dirs.replace(expand_glob_patterns(collapse_glob_patterns))
|
769
|
+
end
|
770
|
+
|
714
771
|
# @param message [String]
|
715
772
|
# @return [void]
|
716
773
|
def log(message)
|
@@ -14,7 +14,7 @@ module Zeitwerk::Loader::Callbacks
|
|
14
14
|
if logger && cdef?(*cref)
|
15
15
|
log("constant #{cpath(*cref)} loaded from file #{file}")
|
16
16
|
elsif !cdef?(*cref)
|
17
|
-
raise NameError
|
17
|
+
raise Zeitwerk::NameError.new("expected file #{file} to define constant #{cpath(*cref)}, but didn't", cref.last)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -9,7 +9,13 @@ module Zeitwerk::RealModName
|
|
9
9
|
#
|
10
10
|
# @param mod [Class, Module]
|
11
11
|
# @return [String, nil]
|
12
|
-
|
13
|
-
|
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
|
14
20
|
end
|
15
21
|
end
|
data/lib/zeitwerk/version.rb
CHANGED
metadata
CHANGED
@@ -1,25 +1,26 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zeitwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1
|
4
|
+
version: 2.3.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:
|
11
|
+
date: 2020-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
15
15
|
and application may have their own independent autoloader, with its own
|
16
|
-
configuration, inflector, and logger. Supports autoloading,
|
16
|
+
configuration, inflector, and logger. Supports autoloading,
|
17
17
|
reloading, and eager loading.
|
18
18
|
email: fxn@hashref.com
|
19
19
|
executables: []
|
20
20
|
extensions: []
|
21
21
|
extra_rdoc_files: []
|
22
22
|
files:
|
23
|
+
- MIT-LICENSE
|
23
24
|
- README.md
|
24
25
|
- lib/zeitwerk.rb
|
25
26
|
- lib/zeitwerk/error.rb
|
@@ -35,7 +36,11 @@ files:
|
|
35
36
|
homepage: https://github.com/fxn/zeitwerk
|
36
37
|
licenses:
|
37
38
|
- MIT
|
38
|
-
metadata:
|
39
|
+
metadata:
|
40
|
+
homepage_uri: https://github.com/fxn/zeitwerk
|
41
|
+
changelog_uri: https://github.com/fxn/zeitwerk/blob/master/CHANGELOG.md
|
42
|
+
source_code_uri: https://github.com/fxn/zeitwerk
|
43
|
+
bug_tracker_uri: https://github.com/fxn/zeitwerk/issues
|
39
44
|
post_install_message:
|
40
45
|
rdoc_options: []
|
41
46
|
require_paths:
|
@@ -51,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
56
|
- !ruby/object:Gem::Version
|
52
57
|
version: '0'
|
53
58
|
requirements: []
|
54
|
-
rubygems_version: 3.
|
59
|
+
rubygems_version: 3.1.2
|
55
60
|
signing_key:
|
56
61
|
specification_version: 4
|
57
62
|
summary: Efficient and thread-safe constant autoloader
|