zeitwerk 2.1.7 → 2.2.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 +133 -30
- data/lib/zeitwerk.rb +1 -0
- data/lib/zeitwerk/error.rb +3 -0
- data/lib/zeitwerk/explicit_namespace.rb +19 -11
- data/lib/zeitwerk/gem_inflector.rb +2 -2
- data/lib/zeitwerk/inflector.rb +32 -2
- data/lib/zeitwerk/loader.rb +86 -41
- data/lib/zeitwerk/loader/callbacks.rb +4 -2
- data/lib/zeitwerk/real_mod_name.rb +21 -0
- data/lib/zeitwerk/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7d5a0f54405ad45dd6afc9bf54bde579f66a75fb142cdbbfb5476157dd75493
|
4
|
+
data.tar.gz: b81311bb19c3b699a6720a3bb3d6df503b858a439c5f1f2605e029117de36bcb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f6fd6e8f7bd479ca6935d1f57d065151aff3be6977ad068f6b4ecdce200bbd131f4db4f2cc1bc2a719cb332b538cf24be55eb936a51572ac5c06676d21763f2
|
7
|
+
data.tar.gz: d4266c65e380b356ff4df7394f2f1882f72ac32b5226dedd717f6b4064c8ac1b8fd3d70ec08b6f9838debfc3690d2f3eee2d6885e446f087a686a619c872c718
|
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
|
|
@@ -17,7 +17,6 @@
|
|
17
17
|
- [Setup](#setup)
|
18
18
|
- [Reloading](#reloading)
|
19
19
|
- [Eager loading](#eager-loading)
|
20
|
-
- [Preloading](#preloading)
|
21
20
|
- [Inflection](#inflection)
|
22
21
|
- [Zeitwerk::Inflector](#zeitwerkinflector)
|
23
22
|
- [Zeitwerk::GemInflector](#zeitwerkgeminflector)
|
@@ -30,8 +29,10 @@
|
|
30
29
|
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
31
30
|
- [Edge cases](#edge-cases)
|
32
31
|
- [Rules of thumb](#rules-of-thumb)
|
32
|
+
- [Autoloading, explicit namespaces, and debuggers](#autoloading-explicit-namespaces-and-debuggers)
|
33
33
|
- [Pronunciation](#pronunciation)
|
34
34
|
- [Supported Ruby versions](#supported-ruby-versions)
|
35
|
+
- [Testing](#testing)
|
35
36
|
- [Motivation](#motivation)
|
36
37
|
- [Thanks](#thanks)
|
37
38
|
- [License](#license)
|
@@ -61,12 +62,13 @@ Main interface for gems:
|
|
61
62
|
|
62
63
|
require "zeitwerk"
|
63
64
|
loader = Zeitwerk::Loader.for_gem
|
64
|
-
loader.setup
|
65
|
-
loader.eager_load # optionally
|
65
|
+
loader.setup # ready!
|
66
66
|
|
67
67
|
module MyGem
|
68
68
|
# ...
|
69
69
|
end
|
70
|
+
|
71
|
+
loader.eager_load # optionally
|
70
72
|
```
|
71
73
|
|
72
74
|
Main generic interface:
|
@@ -199,6 +201,22 @@ loader.setup
|
|
199
201
|
|
200
202
|
The loader returned by `Zeitwerk::Loader.for_gem` has the directory of the caller pushed, normally that is the absolute path of `lib`. In that sense, `for_gem` can be used also by projects with a gem structure, even if they are not technically gems. That is, you don't need a gemspec or anything.
|
201
203
|
|
204
|
+
If the main module of a library 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:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
# lib/my_gem.rb (main file)
|
208
|
+
|
209
|
+
require "zeitwerk"
|
210
|
+
loader = Zeitwerk::Loader.for_gem
|
211
|
+
loader.setup
|
212
|
+
|
213
|
+
module MyGem
|
214
|
+
# Since the setup has been performed, at this point we are already able
|
215
|
+
# to reference project constants, in this case MyGem::MyLogger.
|
216
|
+
include MyLogger
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
202
220
|
Zeitwerk works internally only with absolute paths to avoid costly file searches in `$LOAD_PATH`. Indeed, the root directories do not even need to belong to `$LOAD_PATH`, everything just works by design if they don't.
|
203
221
|
|
204
222
|
<a id="markdown-reloading" name="reloading"></a>
|
@@ -221,7 +239,7 @@ Enabling reloading after setup raises `Zeitwerk::Error`. Similarly, calling `rel
|
|
221
239
|
|
222
240
|
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.
|
223
241
|
|
224
|
-
Reloading removes the currently loaded classes and modules
|
242
|
+
Reloading removes the currently loaded classes and modules and resets the loader so that it will pick whatever is in the file system now.
|
225
243
|
|
226
244
|
It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
|
227
245
|
|
@@ -247,6 +265,8 @@ loader.setup
|
|
247
265
|
loader.eager_load # won't eager load the database adapters
|
248
266
|
```
|
249
267
|
|
268
|
+
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).
|
269
|
+
|
250
270
|
Eager loading is synchronized and idempotent.
|
251
271
|
|
252
272
|
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
@@ -259,22 +279,6 @@ This may be handy in top-level services, like web applications.
|
|
259
279
|
|
260
280
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
261
281
|
|
262
|
-
<a id="markdown-preloading" name="preloading"></a>
|
263
|
-
### Preloading
|
264
|
-
|
265
|
-
Zeitwerk instances are able to preload files and directories.
|
266
|
-
|
267
|
-
```ruby
|
268
|
-
loader.preload("app/models/videogame.rb")
|
269
|
-
loader.preload("app/models/book.rb")
|
270
|
-
```
|
271
|
-
|
272
|
-
The call can happen before `setup` (preloads during setup), or after `setup` (preloads on the spot). Each reload preloads too.
|
273
|
-
|
274
|
-
This is a feature specifically thought for STIs in Rails, preloading the leafs of a STI tree ensures all classes are known when doing a query.
|
275
|
-
|
276
|
-
The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
|
277
|
-
|
278
282
|
<a id="markdown-inflection" name="inflection"></a>
|
279
283
|
### Inflection
|
280
284
|
|
@@ -291,14 +295,34 @@ users_controller -> UsersController
|
|
291
295
|
html_parser -> HtmlParser
|
292
296
|
```
|
293
297
|
|
298
|
+
The camelize logic can be overridden easily for individual basenames:
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
loader.inflector.inflect(
|
302
|
+
"html_parser" => "HTMLParser",
|
303
|
+
"mysql_adapter" => "MySQLAdapter"
|
304
|
+
)
|
305
|
+
```
|
306
|
+
|
307
|
+
The `inflect` method can be invoked several times if you prefer this other style:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
loader.inflector.inflect "html_parser" => "HTMLParser"
|
311
|
+
loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
312
|
+
```
|
313
|
+
|
314
|
+
Overrides need to be configured before calling `setup`.
|
315
|
+
|
294
316
|
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
295
317
|
|
296
|
-
|
318
|
+
Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
|
297
319
|
|
298
320
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
299
321
|
#### Zeitwerk::GemInflector
|
300
322
|
|
301
|
-
|
323
|
+
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
324
|
+
|
325
|
+
Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
|
302
326
|
|
303
327
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
304
328
|
#### Custom inflector
|
@@ -309,12 +333,9 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
309
333
|
# frozen_string_literal: true
|
310
334
|
|
311
335
|
class MyInflector < Zeitwerk::Inflector
|
312
|
-
def camelize(basename,
|
313
|
-
|
314
|
-
|
315
|
-
"API"
|
316
|
-
when "mysql_adapter"
|
317
|
-
"MySQLAdapter"
|
336
|
+
def camelize(basename, abspath)
|
337
|
+
if basename =~ /\Ahtml_(.*)/
|
338
|
+
"HTML" + super($1, abspath)
|
318
339
|
else
|
319
340
|
super
|
320
341
|
end
|
@@ -334,6 +355,49 @@ loader.inflector = MyInflector.new
|
|
334
355
|
|
335
356
|
This needs to be done before calling `setup`.
|
336
357
|
|
358
|
+
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:
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
# lib/my_gem/inflector.rb
|
362
|
+
module MyGem
|
363
|
+
class Inflector < Zeitwerk::GemInflector
|
364
|
+
...
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# lib/my_gem.rb
|
369
|
+
require "zeitwerk"
|
370
|
+
require_relative "my_gem/inflector"
|
371
|
+
|
372
|
+
loader = Zeitwerk::Loader.for_gem
|
373
|
+
loader.inflector = MyGem::Inflector.new(__FILE__)
|
374
|
+
loader.setup
|
375
|
+
|
376
|
+
module MyGem
|
377
|
+
# ...
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
Since `MyGem` is referenced before the namespace is defined in the main file, it is important to use this style:
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
# Correct, effectively defines MyGem.
|
385
|
+
module MyGem
|
386
|
+
class Inflector < Zeitwerk::GemInflector
|
387
|
+
# ...
|
388
|
+
end
|
389
|
+
end
|
390
|
+
```
|
391
|
+
|
392
|
+
instead of:
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
# Raises uninitialized constant MyGem (NameError).
|
396
|
+
class MyGem::Inflector < Zeitwerk::GemInflector
|
397
|
+
# ...
|
398
|
+
end
|
399
|
+
```
|
400
|
+
|
337
401
|
<a id="markdown-logging" name="logging"></a>
|
338
402
|
### Logging
|
339
403
|
|
@@ -399,7 +463,7 @@ Zeitwerk ignores automatically any file or directory whose name starts with a do
|
|
399
463
|
|
400
464
|
However, sometimes it might still be convenient to tell Zeitwerk to completely ignore some particular Ruby file or directory. That is possible with `ignore`, which accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
|
401
465
|
|
402
|
-
You can ignore file names, directory names, and glob patterns. Glob patterns are expanded
|
466
|
+
You can ignore file names, directory names, and glob patterns. Glob patterns are expanded when they are added and again on each reload.
|
403
467
|
|
404
468
|
Let's see some use cases.
|
405
469
|
|
@@ -460,6 +524,8 @@ The chosen adapter, then, has to be loaded by hand somehow:
|
|
460
524
|
require "my_gem/db_adapters/#{config[:db_adapter]}"
|
461
525
|
```
|
462
526
|
|
527
|
+
Note that since the directory is ignored, the required adapter can instantiate another loader to manage its subtree, if desired. Such loader would coexist with the main one just fine.
|
528
|
+
|
463
529
|
<a id="markdown-use-case-test-files-mixed-with-implementation-files" name="use-case-test-files-mixed-with-implementation-files"></a>
|
464
530
|
#### Use case: Test files mixed with implementation files
|
465
531
|
|
@@ -515,6 +581,15 @@ This only affects explicit namespaces, those idioms work well for any other ordi
|
|
515
581
|
|
516
582
|
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.
|
517
583
|
|
584
|
+
<a id="markdown-autoloading-explicit-namespaces-and-debuggers" name="autoloading-explicit-namespaces-and-debuggers"></a>
|
585
|
+
### Autoloading, explicit namespaces, and debuggers
|
586
|
+
|
587
|
+
As of this writing, Zeitwerk is unable to autoload classes or modules that belong to [explicit namespaces](#explicit-namespaces) inside debugger sessions. You'll get a `NameError`.
|
588
|
+
|
589
|
+
The root cause is that debuggers set trace points, and Zeitwerk does too to support explicit namespaces. A debugger session happens inside a trace point handler, and Ruby does not invoke other handlers from within a running handler. Therefore, the code that manages explicit namespaces in Zeitwerk does not get called by the interpreter. See [this issue](https://github.com/deivid-rodriguez/byebug/issues/564#issuecomment-499413606) for further details.
|
590
|
+
|
591
|
+
As a workaround, you can eager load. Zeitwerk tries hard to succeed or fail consistently both autoloading and eager loading, so switching to eager loading should not introduce any interference in your debugging logic, generally speaking.
|
592
|
+
|
518
593
|
<a id="markdown-pronunciation" name="pronunciation"></a>
|
519
594
|
## Pronunciation
|
520
595
|
|
@@ -525,6 +600,32 @@ This only affects explicit namespaces, those idioms work well for any other ordi
|
|
525
600
|
|
526
601
|
Zeitwerk works with MRI 2.4.4 and above.
|
527
602
|
|
603
|
+
<a id="markdown-testing" name="testing"></a>
|
604
|
+
## Testing
|
605
|
+
|
606
|
+
In order to run the test suite of Zeitwerk, `cd` into the project root and execute
|
607
|
+
|
608
|
+
```
|
609
|
+
bin/test
|
610
|
+
```
|
611
|
+
|
612
|
+
To run one particular suite, pass its file name as an argument:
|
613
|
+
|
614
|
+
```
|
615
|
+
bin/test test/lib/zeitwerk/test_eager_load.rb
|
616
|
+
```
|
617
|
+
|
618
|
+
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`:
|
619
|
+
|
620
|
+
```ruby
|
621
|
+
focus
|
622
|
+
test "capitalizes the first letter" do
|
623
|
+
assert_equal "User", camelize("user")
|
624
|
+
end
|
625
|
+
```
|
626
|
+
|
627
|
+
and run `bin/test`.
|
628
|
+
|
528
629
|
<a id="markdown-motivation" name="motivation"></a>
|
529
630
|
## Motivation
|
530
631
|
|
@@ -539,6 +640,8 @@ I'd like to thank [@matthewd](https://github.com/matthewd) for the discussions w
|
|
539
640
|
|
540
641
|
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.
|
541
642
|
|
643
|
+
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 ❤️.
|
644
|
+
|
542
645
|
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. 💯
|
543
646
|
|
544
647
|
<a id="markdown-license" name="license"></a>
|
data/lib/zeitwerk.rb
CHANGED
data/lib/zeitwerk/error.rb
CHANGED
@@ -8,6 +8,8 @@ module Zeitwerk
|
|
8
8
|
# loading their constant before setup. This is documented.
|
9
9
|
module ExplicitNamespace # :nodoc: all
|
10
10
|
class << self
|
11
|
+
include RealModName
|
12
|
+
|
11
13
|
# Maps constant paths that correspond to explicit namespaces according to
|
12
14
|
# the file system, to the loader responsible for them.
|
13
15
|
#
|
@@ -52,21 +54,27 @@ module Zeitwerk
|
|
52
54
|
tracer.disable if cpaths.empty?
|
53
55
|
end
|
54
56
|
end
|
57
|
+
|
58
|
+
def tracepoint_class_callback(event)
|
59
|
+
# If the class is a singleton class, we won't do anything with it so we
|
60
|
+
# can bail out immediately. This is several orders of magnitude faster
|
61
|
+
# than accessing its name.
|
62
|
+
return if event.self.singleton_class?
|
63
|
+
|
64
|
+
# Note that it makes sense to compute the hash code unconditionally,
|
65
|
+
# because the trace point is disabled if cpaths is empty.
|
66
|
+
if loader = cpaths.delete(real_mod_name(event.self))
|
67
|
+
loader.on_namespace_loaded(event.self)
|
68
|
+
disable_tracer_if_unneeded
|
69
|
+
end
|
70
|
+
end
|
55
71
|
end
|
56
72
|
|
57
73
|
@cpaths = {}
|
58
74
|
@mutex = Mutex.new
|
59
|
-
@tracer = TracePoint.new(:class) do |event|
|
60
|
-
# If the class is a singleton class, we won't do anything with it so we can bail out immediately.
|
61
|
-
# This is several orders of magnitude faster than accessing `Module#name`.
|
62
|
-
next if event.self.singleton_class?
|
63
75
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
loader.on_namespace_loaded(event.self)
|
68
|
-
disable_tracer_if_unneeded
|
69
|
-
end
|
70
|
-
end
|
76
|
+
# We go through a method instead of defining a block mainly to have a better
|
77
|
+
# label when profiling.
|
78
|
+
@tracer = TracePoint.new(:class, &method(:tracepoint_class_callback))
|
71
79
|
end
|
72
80
|
end
|
@@ -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/loader.rb
CHANGED
@@ -7,6 +7,7 @@ module Zeitwerk
|
|
7
7
|
class Loader
|
8
8
|
require_relative "loader/callbacks"
|
9
9
|
include Callbacks
|
10
|
+
include RealModName
|
10
11
|
|
11
12
|
# @return [String]
|
12
13
|
attr_reader :tag
|
@@ -43,7 +44,7 @@ module Zeitwerk
|
|
43
44
|
#
|
44
45
|
# @private
|
45
46
|
# @return [Set<String>]
|
46
|
-
attr_reader :
|
47
|
+
attr_reader :ignored_glob_patterns
|
47
48
|
|
48
49
|
# The actual collection of absolute file and directory names at the time the
|
49
50
|
# ignored glob patterns were expanded. Computed on setup, and recomputed on
|
@@ -132,7 +133,7 @@ module Zeitwerk
|
|
132
133
|
|
133
134
|
@root_dirs = {}
|
134
135
|
@preloads = []
|
135
|
-
@
|
136
|
+
@ignored_glob_patterns = Set.new
|
136
137
|
@ignored_paths = Set.new
|
137
138
|
@autoloads = {}
|
138
139
|
@autoloaded_dirs = []
|
@@ -224,16 +225,12 @@ module Zeitwerk
|
|
224
225
|
#
|
225
226
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
226
227
|
# @return [void]
|
227
|
-
def ignore(*
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
def expand_ignored_glob_patterns
|
234
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
235
|
-
# glob patterns technically need no wildcards.
|
236
|
-
ignored_paths.replace(ignored.flat_map { |path| Dir.glob(path) })
|
228
|
+
def ignore(*glob_patterns)
|
229
|
+
glob_patterns = expand_paths(glob_patterns)
|
230
|
+
mutex.synchronize do
|
231
|
+
ignored_glob_patterns.merge(glob_patterns)
|
232
|
+
ignored_paths.merge(expand_glob_patterns(glob_patterns))
|
233
|
+
end
|
237
234
|
end
|
238
235
|
|
239
236
|
# Sets autoloads in the root namespace and preloads files, if any.
|
@@ -243,7 +240,6 @@ module Zeitwerk
|
|
243
240
|
mutex.synchronize do
|
244
241
|
break if @setup
|
245
242
|
|
246
|
-
expand_ignored_glob_patterns
|
247
243
|
actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
|
248
244
|
do_preload
|
249
245
|
|
@@ -325,6 +321,7 @@ module Zeitwerk
|
|
325
321
|
def reload
|
326
322
|
if reloading_enabled?
|
327
323
|
unload
|
324
|
+
recompute_ignored_paths
|
328
325
|
setup
|
329
326
|
else
|
330
327
|
raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
|
@@ -353,7 +350,7 @@ module Zeitwerk
|
|
353
350
|
if cref = autoloads[File.realpath(abspath)]
|
354
351
|
cref[0].const_get(cref[1], false)
|
355
352
|
end
|
356
|
-
elsif dir?(abspath)
|
353
|
+
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
357
354
|
cname = inflector.camelize(basename, abspath)
|
358
355
|
queue << [namespace.const_get(cname, false), abspath]
|
359
356
|
end
|
@@ -402,6 +399,22 @@ module Zeitwerk
|
|
402
399
|
@logger = ->(msg) { puts msg }
|
403
400
|
end
|
404
401
|
|
402
|
+
# @private
|
403
|
+
# @param dir [String]
|
404
|
+
# @return [Boolean]
|
405
|
+
def manages?(dir)
|
406
|
+
dir = dir + "/"
|
407
|
+
ignored_paths.each do |ignored_path|
|
408
|
+
return false if dir.start_with?(ignored_path + "/")
|
409
|
+
end
|
410
|
+
|
411
|
+
root_dirs.each_key do |root_dir|
|
412
|
+
return true if root_dir.start_with?(dir) || dir.start_with?(root_dir + "/")
|
413
|
+
end
|
414
|
+
|
415
|
+
false
|
416
|
+
end
|
417
|
+
|
405
418
|
# --- Class methods ---------------------------------------------------------------------------
|
406
419
|
|
407
420
|
class << self
|
@@ -461,21 +474,38 @@ module Zeitwerk
|
|
461
474
|
# @return [void]
|
462
475
|
def set_autoloads_in_dir(dir, parent)
|
463
476
|
ls(dir) do |basename, abspath|
|
464
|
-
|
465
|
-
basename
|
466
|
-
|
467
|
-
autoload_file(parent, cname, abspath)
|
468
|
-
elsif dir?(abspath)
|
469
|
-
# In a Rails application, `app/models/concerns` is a subdirectory of
|
470
|
-
# `app/models`, but both of them are root directories.
|
471
|
-
#
|
472
|
-
# To resolve the ambiguity file name -> constant path this introduces,
|
473
|
-
# the `app/models/concerns` directory is totally ignored as a namespace,
|
474
|
-
# it counts only as root. The guard checks that.
|
475
|
-
unless root_dirs.key?(abspath)
|
477
|
+
begin
|
478
|
+
if ruby?(basename)
|
479
|
+
basename.slice!(-3, 3)
|
476
480
|
cname = inflector.camelize(basename, abspath).to_sym
|
477
|
-
|
481
|
+
autoload_file(parent, cname, abspath)
|
482
|
+
elsif dir?(abspath)
|
483
|
+
# In a Rails application, `app/models/concerns` is a subdirectory of
|
484
|
+
# `app/models`, but both of them are root directories.
|
485
|
+
#
|
486
|
+
# To resolve the ambiguity file name -> constant path this introduces,
|
487
|
+
# the `app/models/concerns` directory is totally ignored as a namespace,
|
488
|
+
# it counts only as root. The guard checks that.
|
489
|
+
unless root_dirs.key?(abspath)
|
490
|
+
cname = inflector.camelize(basename, abspath).to_sym
|
491
|
+
autoload_subdir(parent, cname, abspath)
|
492
|
+
end
|
478
493
|
end
|
494
|
+
rescue ::NameError => error
|
495
|
+
path_type = ruby?(abspath) ? "file" : "directory"
|
496
|
+
|
497
|
+
raise NameError, <<~MESSAGE
|
498
|
+
#{error.message} inferred by #{inflector.class} from #{path_type}
|
499
|
+
|
500
|
+
#{abspath}
|
501
|
+
|
502
|
+
Possible ways to address this:
|
503
|
+
|
504
|
+
* Tell Zeitwerk to ignore this particular #{path_type}.
|
505
|
+
* Tell Zeitwerk to ignore one of its parent directories.
|
506
|
+
* Rename the #{path_type} to comply with the naming conventions.
|
507
|
+
* Modify the inflector to handle this case.
|
508
|
+
MESSAGE
|
479
509
|
end
|
480
510
|
end
|
481
511
|
end
|
@@ -593,8 +623,14 @@ module Zeitwerk
|
|
593
623
|
# @param parent [Module]
|
594
624
|
# @param cname [Symbol]
|
595
625
|
# @return [String, nil]
|
596
|
-
|
597
|
-
|
626
|
+
if method(:autoload?).arity == 1
|
627
|
+
def strict_autoload_path(parent, cname)
|
628
|
+
parent.autoload?(cname) if cdef?(parent, cname)
|
629
|
+
end
|
630
|
+
else
|
631
|
+
def strict_autoload_path(parent, cname)
|
632
|
+
parent.autoload?(cname, false)
|
633
|
+
end
|
598
634
|
end
|
599
635
|
|
600
636
|
# This method is called this way because I prefer `preload` to be the method
|
@@ -636,7 +672,7 @@ module Zeitwerk
|
|
636
672
|
# @param cname [Symbol]
|
637
673
|
# @return [String]
|
638
674
|
def cpath(parent, cname)
|
639
|
-
parent.equal?(Object) ? cname.to_s : "#{parent
|
675
|
+
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
640
676
|
end
|
641
677
|
|
642
678
|
# @param dir [String]
|
@@ -665,7 +701,20 @@ module Zeitwerk
|
|
665
701
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
666
702
|
# @return [<String>]
|
667
703
|
def expand_paths(paths)
|
668
|
-
|
704
|
+
paths.flatten.map! { |path| File.expand_path(path) }
|
705
|
+
end
|
706
|
+
|
707
|
+
# @param glob_patterns [<String>]
|
708
|
+
# @return [<String>]
|
709
|
+
def expand_glob_patterns(glob_patterns)
|
710
|
+
# Note that Dir.glob works with regular file names just fine. That is,
|
711
|
+
# glob patterns technically need no wildcards.
|
712
|
+
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
713
|
+
end
|
714
|
+
|
715
|
+
# @return [void]
|
716
|
+
def recompute_ignored_paths
|
717
|
+
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
669
718
|
end
|
670
719
|
|
671
720
|
# @param message [String]
|
@@ -686,16 +735,12 @@ module Zeitwerk
|
|
686
735
|
def raise_if_conflicting_directory(dir)
|
687
736
|
self.class.mutex.synchronize do
|
688
737
|
Registry.loaders.each do |loader|
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
696
|
-
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
697
|
-
EOS
|
698
|
-
end
|
738
|
+
if loader != self && loader.manages?(dir)
|
739
|
+
require "pp"
|
740
|
+
raise Error,
|
741
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
742
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
743
|
+
EOS
|
699
744
|
end
|
700
745
|
end
|
701
746
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
module Zeitwerk::Loader::Callbacks
|
2
|
+
include Zeitwerk::RealModName
|
3
|
+
|
2
4
|
# Invoked from our decorated Kernel#require when a managed file is autoloaded.
|
3
5
|
#
|
4
6
|
# @private
|
@@ -12,7 +14,7 @@ module Zeitwerk::Loader::Callbacks
|
|
12
14
|
if logger && cdef?(*cref)
|
13
15
|
log("constant #{cpath(*cref)} loaded from file #{file}")
|
14
16
|
elsif !cdef?(*cref)
|
15
|
-
raise NameError, "expected file #{file} to define constant #{cpath(*cref)}, but didn't"
|
17
|
+
raise Zeitwerk::NameError, "expected file #{file} to define constant #{cpath(*cref)}, but didn't"
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
@@ -60,7 +62,7 @@ module Zeitwerk::Loader::Callbacks
|
|
60
62
|
# @param namespace [Module]
|
61
63
|
# @return [void]
|
62
64
|
def on_namespace_loaded(namespace)
|
63
|
-
if subdirs = lazy_subdirs.delete(namespace
|
65
|
+
if subdirs = lazy_subdirs.delete(real_mod_name(namespace))
|
64
66
|
subdirs.each do |subdir|
|
65
67
|
set_autoloads_in_dir(subdir, namespace)
|
66
68
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Zeitwerk::RealModName
|
2
|
+
UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
|
3
|
+
private_constant :UNBOUND_METHOD_MODULE_NAME
|
4
|
+
|
5
|
+
# Returns the real name of the class or module, as set after the first
|
6
|
+
# constant to which it was assigned (or nil).
|
7
|
+
#
|
8
|
+
# The name method can be overridden, hence the indirection in this method.
|
9
|
+
#
|
10
|
+
# @param mod [Class, Module]
|
11
|
+
# @return [String, nil]
|
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
|
20
|
+
end
|
21
|
+
end
|
data/lib/zeitwerk/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: zeitwerk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1
|
4
|
+
version: 2.2.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: 2019-
|
11
|
+
date: 2019-11-01 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
Zeitwerk implements constant autoloading with Ruby semantics. Each gem
|
@@ -20,6 +20,7 @@ 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
|
@@ -29,6 +30,7 @@ files:
|
|
29
30
|
- lib/zeitwerk/kernel.rb
|
30
31
|
- lib/zeitwerk/loader.rb
|
31
32
|
- lib/zeitwerk/loader/callbacks.rb
|
33
|
+
- lib/zeitwerk/real_mod_name.rb
|
32
34
|
- lib/zeitwerk/registry.rb
|
33
35
|
- lib/zeitwerk/version.rb
|
34
36
|
homepage: https://github.com/fxn/zeitwerk
|