zeitwerk 2.1.6 → 2.2.0
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 +125 -37
- 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 +1 -1
- data/lib/zeitwerk/inflector.rb +32 -2
- data/lib/zeitwerk/loader.rb +88 -66
- data/lib/zeitwerk/loader/callbacks.rb +3 -1
- data/lib/zeitwerk/real_mod_name.rb +15 -0
- data/lib/zeitwerk/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2af50b4d74832ccd3c08e3445c09a941ed56758c57acd20e8d88436f79e16ea2
|
4
|
+
data.tar.gz: 23f693ca2b9fc44870c26991cd91c587031825e9d29c90dd5285f2c7a3731f6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d47e1a735c5314ec56f9c16d66695222209109dddb7da3a663969b852295eed82d8eee56fc48a48cb6b0604e508940f474533e98c7d824175c344420a64baec
|
7
|
+
data.tar.gz: be275743889771c745fc90d300a4cbd01911b5b4547c41ea1df6429af1357b9328fb69293a03a282677295f27528220f180f1ca943f43b5567677b856baa3e06
|
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)
|
@@ -29,6 +28,8 @@
|
|
29
28
|
- [Use case: The adapter pattern](#use-case-the-adapter-pattern)
|
30
29
|
- [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
|
31
30
|
- [Edge cases](#edge-cases)
|
31
|
+
- [Rules of thumb](#rules-of-thumb)
|
32
|
+
- [Autoloading, explicit namespaces, and debuggers](#autoloading-explicit-namespaces-and-debuggers)
|
32
33
|
- [Pronunciation](#pronunciation)
|
33
34
|
- [Supported Ruby versions](#supported-ruby-versions)
|
34
35
|
- [Motivation](#motivation)
|
@@ -42,15 +43,13 @@
|
|
42
43
|
|
43
44
|
Zeitwerk is an efficient and thread-safe code loader for Ruby.
|
44
45
|
|
45
|
-
Given a conventional file structure, Zeitwerk
|
46
|
+
Given a [conventional file structure](#file-structure), Zeitwerk is able to load your project's classes and modules on demand (autoloading), or upfront (eager loading). You don't need to write `require` calls for your own files, rather, you can streamline your programming knowing that your classes and modules are available everywhere. This feature is efficient, thread-safe, and matches Ruby's semantics for constants.
|
46
47
|
|
47
|
-
Zeitwerk
|
48
|
+
Zeitwerk is also able to reload code, which may be handy while developing web applications. Coordination is needed to reload in a thread-safe manner. The documentation below explains how to do this.
|
48
49
|
|
49
|
-
The
|
50
|
+
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
51
|
|
51
|
-
Zeitwerk
|
52
|
-
|
53
|
-
Finally, in some production setups it may be optimal to eager load all code upfront. Zeitwerk is able to do that too.
|
52
|
+
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`. Furthermore, Zeitwerk does only one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used.
|
54
53
|
|
55
54
|
<a id="markdown-synopsis" name="synopsis"></a>
|
56
55
|
## Synopsis
|
@@ -63,11 +62,12 @@ Main interface for gems:
|
|
63
62
|
require "zeitwerk"
|
64
63
|
loader = Zeitwerk::Loader.for_gem
|
65
64
|
loader.setup # ready!
|
66
|
-
# loader.eager_load, optionally
|
67
65
|
|
68
66
|
module MyGem
|
69
67
|
# ...
|
70
68
|
end
|
69
|
+
|
70
|
+
loader.eager_load # optionally
|
71
71
|
```
|
72
72
|
|
73
73
|
Main generic interface:
|
@@ -200,12 +200,28 @@ loader.setup
|
|
200
200
|
|
201
201
|
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.
|
202
202
|
|
203
|
+
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:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# lib/my_gem.rb (main file)
|
207
|
+
|
208
|
+
require "zeitwerk"
|
209
|
+
loader = Zeitwerk::Loader.for_gem
|
210
|
+
loader.setup
|
211
|
+
|
212
|
+
module MyGem
|
213
|
+
# Since the setup has been performed, at this point we are already able
|
214
|
+
# to reference project constants, in this case MyGem::MyLogger.
|
215
|
+
include MyLogger
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
203
219
|
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.
|
204
220
|
|
205
221
|
<a id="markdown-reloading" name="reloading"></a>
|
206
222
|
### Reloading
|
207
223
|
|
208
|
-
|
224
|
+
Zeitwerk is able to reload code, but you need to enable this feature:
|
209
225
|
|
210
226
|
```ruby
|
211
227
|
loader = Zeitwerk::Loader.new
|
@@ -222,7 +238,7 @@ Enabling reloading after setup raises `Zeitwerk::Error`. Similarly, calling `rel
|
|
222
238
|
|
223
239
|
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.
|
224
240
|
|
225
|
-
Reloading removes the currently loaded classes and modules
|
241
|
+
Reloading removes the currently loaded classes and modules and resets the loader so that it will pick whatever is in the file system now.
|
226
242
|
|
227
243
|
It is important to highlight that this is an instance method. Don't worry about project dependencies managed by Zeitwerk, their loaders are independent.
|
228
244
|
|
@@ -248,6 +264,8 @@ loader.setup
|
|
248
264
|
loader.eager_load # won't eager load the database adapters
|
249
265
|
```
|
250
266
|
|
267
|
+
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).
|
268
|
+
|
251
269
|
Eager loading is synchronized and idempotent.
|
252
270
|
|
253
271
|
If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
|
@@ -260,22 +278,6 @@ This may be handy in top-level services, like web applications.
|
|
260
278
|
|
261
279
|
Note that thanks to idempotence `Zeitwerk::Loader.eager_load_all` won't eager load twice if any of the instances already eager loaded.
|
262
280
|
|
263
|
-
<a id="markdown-preloading" name="preloading"></a>
|
264
|
-
### Preloading
|
265
|
-
|
266
|
-
Zeitwerk instances are able to preload files and directories.
|
267
|
-
|
268
|
-
```ruby
|
269
|
-
loader.preload("app/models/videogame.rb")
|
270
|
-
loader.preload("app/models/book.rb")
|
271
|
-
```
|
272
|
-
|
273
|
-
The call can happen before `setup` (preloads during setup), or after `setup` (preloads on the spot). Each reload preloads too.
|
274
|
-
|
275
|
-
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.
|
276
|
-
|
277
|
-
The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
|
278
|
-
|
279
281
|
<a id="markdown-inflection" name="inflection"></a>
|
280
282
|
### Inflection
|
281
283
|
|
@@ -292,14 +294,34 @@ users_controller -> UsersController
|
|
292
294
|
html_parser -> HtmlParser
|
293
295
|
```
|
294
296
|
|
297
|
+
The camelize logic can be overridden easily for individual basenames:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
loader.inflector.inflect(
|
301
|
+
"html_parser" => "HTMLParser",
|
302
|
+
"mysql_adapter" => "MySQLAdapter"
|
303
|
+
)
|
304
|
+
```
|
305
|
+
|
306
|
+
The `inflect` method can be invoked several times if you prefer this other style:
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
loader.inflector.inflect "html_parser" => "HTMLParser"
|
310
|
+
loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
|
311
|
+
```
|
312
|
+
|
313
|
+
Overrides need to be configured before calling `setup`.
|
314
|
+
|
295
315
|
There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
|
296
316
|
|
297
|
-
|
317
|
+
Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
|
298
318
|
|
299
319
|
<a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
|
300
320
|
#### Zeitwerk::GemInflector
|
301
321
|
|
302
|
-
|
322
|
+
This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
|
323
|
+
|
324
|
+
Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
|
303
325
|
|
304
326
|
<a id="markdown-custom-inflector" name="custom-inflector"></a>
|
305
327
|
#### Custom inflector
|
@@ -310,12 +332,9 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
|
|
310
332
|
# frozen_string_literal: true
|
311
333
|
|
312
334
|
class MyInflector < Zeitwerk::Inflector
|
313
|
-
def camelize(basename,
|
314
|
-
|
315
|
-
|
316
|
-
"API"
|
317
|
-
when "mysql_adapter"
|
318
|
-
"MySQLAdapter"
|
335
|
+
def camelize(basename, abspath)
|
336
|
+
if basename =~ /\Ahtml_(.*)/
|
337
|
+
"HTML" + super($1, abspath)
|
319
338
|
else
|
320
339
|
super
|
321
340
|
end
|
@@ -335,12 +354,55 @@ loader.inflector = MyInflector.new
|
|
335
354
|
|
336
355
|
This needs to be done before calling `setup`.
|
337
356
|
|
357
|
+
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:
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
# lib/my_gem/inflector.rb
|
361
|
+
module MyGem
|
362
|
+
class Inflector < Zeitwerk::GemInflector
|
363
|
+
...
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
# lib/my_gem.rb
|
368
|
+
require "zeitwerk"
|
369
|
+
require_relative "my_gem/inflector"
|
370
|
+
|
371
|
+
loader = Zeitwerk::Loader.for_gem
|
372
|
+
loader.inflector = MyGem::Inflector.new(__FILE__)
|
373
|
+
loader.setup
|
374
|
+
|
375
|
+
module MyGem
|
376
|
+
# ...
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
Since `MyGem` is referenced before the namespace is defined in the main file, it is important to use this style:
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
# Correct, effectively defines MyGem.
|
384
|
+
module MyGem
|
385
|
+
class Inflector < Zeitwerk::GemInflector
|
386
|
+
# ...
|
387
|
+
end
|
388
|
+
end
|
389
|
+
```
|
390
|
+
|
391
|
+
instead of:
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
# Raises uninitialized constant MyGem (NameError).
|
395
|
+
class MyGem::Inflector < Zeitwerk::GemInflector
|
396
|
+
# ...
|
397
|
+
end
|
398
|
+
```
|
399
|
+
|
338
400
|
<a id="markdown-logging" name="logging"></a>
|
339
401
|
### Logging
|
340
402
|
|
341
403
|
Zeitwerk is silent by default, but you can ask loaders to trace their activity. Logging is meant just for troubleshooting, shouldn't normally be enabled.
|
342
404
|
|
343
|
-
The `log!`
|
405
|
+
The `log!` method is a quick shortcut to let the loader log to `$stdout`:
|
344
406
|
|
345
407
|
```
|
346
408
|
loader.log!
|
@@ -400,7 +462,7 @@ Zeitwerk ignores automatically any file or directory whose name starts with a do
|
|
400
462
|
|
401
463
|
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.
|
402
464
|
|
403
|
-
You can ignore file names, directory names, and glob patterns. Glob patterns are expanded
|
465
|
+
You can ignore file names, directory names, and glob patterns. Glob patterns are expanded when they are added and again on each reload.
|
404
466
|
|
405
467
|
Let's see some use cases.
|
406
468
|
|
@@ -461,6 +523,8 @@ The chosen adapter, then, has to be loaded by hand somehow:
|
|
461
523
|
require "my_gem/db_adapters/#{config[:db_adapter]}"
|
462
524
|
```
|
463
525
|
|
526
|
+
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.
|
527
|
+
|
464
528
|
<a id="markdown-use-case-test-files-mixed-with-implementation-files" name="use-case-test-files-mixed-with-implementation-files"></a>
|
465
529
|
#### Use case: Test files mixed with implementation files
|
466
530
|
|
@@ -501,6 +565,30 @@ Trip = Struct.new { ... } # NOT SUPPORTED
|
|
501
565
|
|
502
566
|
This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
|
503
567
|
|
568
|
+
<a id="markdown-rules-of-thumb" name="rules-of-thumb"></a>
|
569
|
+
### Rules of thumb
|
570
|
+
|
571
|
+
1. Different loaders should manage different directory trees. It is an error condition to configure overlapping root directories in different loaders.
|
572
|
+
|
573
|
+
2. Think the mere existence of a file is effectively like writing a `require` call for them, which is executed on demand (autoload) or upfront (eager load).
|
574
|
+
|
575
|
+
3. In that line, if two loaders manage files that translate to the same constant in the same namespace, the first one wins, the rest are ignored. Similar to what happens with `require` and `$LOAD_PATH`, only the first occurrence matters.
|
576
|
+
|
577
|
+
4. Projects that reopen a namespace defined by some dependency have to ensure said namespace is loaded before setup. That is, the project has to make sure it reopens, rather than define. This is often accomplished just loading the dependency.
|
578
|
+
|
579
|
+
5. Objects stored in reloadable constants should not be cached in places that are not reloaded. For example, non-reloadable classes should not subclass a reloadable class, or mixin a reloadable module. Otherwise, after reloading, those classes or module objects would become stale. Referring to constants in dynamic places like method calls or lambdas is fine.
|
580
|
+
|
581
|
+
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.
|
582
|
+
|
583
|
+
<a id="markdown-autoloading-explicit-namespaces-and-debuggers" name="autoloading-explicit-namespaces-and-debuggers"></a>
|
584
|
+
### Autoloading, explicit namespaces, and debuggers
|
585
|
+
|
586
|
+
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`.
|
587
|
+
|
588
|
+
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.
|
589
|
+
|
590
|
+
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.
|
591
|
+
|
504
592
|
<a id="markdown-pronunciation" name="pronunciation"></a>
|
505
593
|
## Pronunciation
|
506
594
|
|
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
|
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
|
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) # => "PostsController"
|
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
|
@@ -53,23 +54,6 @@ module Zeitwerk
|
|
53
54
|
# @return [Set<String>]
|
54
55
|
attr_reader :ignored_paths
|
55
56
|
|
56
|
-
# A _shadowed file_ is a file managed by this loader that is ignored when
|
57
|
-
# setting autoloads because its matching constant is taken. Either the
|
58
|
-
# constant is already defined, or there exists an autoload for it.
|
59
|
-
#
|
60
|
-
# Think $LOAD_PATH and require, only the first occurrence of a given
|
61
|
-
# relative name is loaded.
|
62
|
-
#
|
63
|
-
# This set keeps track of the absolute path of shadowed files, to be able to
|
64
|
-
# skip them while eager loading.
|
65
|
-
#
|
66
|
-
# Note this cannot be implemented with do_not_eager_load because these
|
67
|
-
# files are not autoloadable.
|
68
|
-
#
|
69
|
-
# @private
|
70
|
-
# @return [Set<String>]
|
71
|
-
attr_reader :shadowed_files
|
72
|
-
|
73
57
|
# Maps real absolute paths for which an autoload has been set ---and not
|
74
58
|
# executed--- to their corresponding parent class or module and constant
|
75
59
|
# name.
|
@@ -149,13 +133,12 @@ module Zeitwerk
|
|
149
133
|
|
150
134
|
@root_dirs = {}
|
151
135
|
@preloads = []
|
152
|
-
@
|
136
|
+
@ignored_glob_patterns = Set.new
|
153
137
|
@ignored_paths = Set.new
|
154
138
|
@autoloads = {}
|
155
139
|
@autoloaded_dirs = []
|
156
140
|
@to_unload = {}
|
157
141
|
@lazy_subdirs = {}
|
158
|
-
@shadowed_files = Set.new
|
159
142
|
@eager_load_exclusions = Set.new
|
160
143
|
|
161
144
|
# TODO: find a better name for these mutexes.
|
@@ -242,16 +225,12 @@ module Zeitwerk
|
|
242
225
|
#
|
243
226
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
244
227
|
# @return [void]
|
245
|
-
def ignore(*
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
def expand_ignored_glob_patterns
|
252
|
-
# Note that Dir.glob works with regular file names just fine. That is,
|
253
|
-
# glob patterns technically need no wildcards.
|
254
|
-
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
|
255
234
|
end
|
256
235
|
|
257
236
|
# Sets autoloads in the root namespace and preloads files, if any.
|
@@ -261,7 +240,6 @@ module Zeitwerk
|
|
261
240
|
mutex.synchronize do
|
262
241
|
break if @setup
|
263
242
|
|
264
|
-
expand_ignored_glob_patterns
|
265
243
|
actual_root_dirs.each { |root_dir| set_autoloads_in_dir(root_dir, Object) }
|
266
244
|
do_preload
|
267
245
|
|
@@ -324,7 +302,6 @@ module Zeitwerk
|
|
324
302
|
autoloaded_dirs.clear
|
325
303
|
to_unload.clear
|
326
304
|
lazy_subdirs.clear
|
327
|
-
shadowed_files.clear
|
328
305
|
|
329
306
|
Registry.on_unload(self)
|
330
307
|
ExplicitNamespace.unregister(self)
|
@@ -344,6 +321,7 @@ module Zeitwerk
|
|
344
321
|
def reload
|
345
322
|
if reloading_enabled?
|
346
323
|
unload
|
324
|
+
recompute_ignored_paths
|
347
325
|
setup
|
348
326
|
else
|
349
327
|
raise ReloadingDisabledError, "can't reload, please call loader.enable_reloading before setup"
|
@@ -361,20 +339,24 @@ module Zeitwerk
|
|
361
339
|
break if @eager_loaded
|
362
340
|
|
363
341
|
queue = actual_root_dirs.reject { |dir| eager_load_exclusions.member?(dir) }
|
364
|
-
|
365
|
-
|
342
|
+
queue.map! { |dir| [Object, dir] }
|
343
|
+
while to_eager_load = queue.shift
|
344
|
+
namespace, dir = to_eager_load
|
345
|
+
|
346
|
+
ls(dir) do |basename, abspath|
|
366
347
|
next if eager_load_exclusions.member?(abspath)
|
367
348
|
|
368
349
|
if ruby?(abspath)
|
369
|
-
|
370
|
-
|
371
|
-
|
350
|
+
if cref = autoloads[File.realpath(abspath)]
|
351
|
+
cref[0].const_get(cref[1], false)
|
352
|
+
end
|
353
|
+
elsif dir?(abspath) && !root_dirs.key?(abspath)
|
354
|
+
cname = inflector.camelize(basename, abspath)
|
355
|
+
queue << [namespace.const_get(cname, false), abspath]
|
372
356
|
end
|
373
357
|
end
|
374
358
|
end
|
375
359
|
|
376
|
-
shadowed_files.clear
|
377
|
-
|
378
360
|
autoloaded_dirs.each do |autoloaded_dir|
|
379
361
|
Registry.unregister_autoload(autoloaded_dir)
|
380
362
|
end
|
@@ -417,6 +399,22 @@ module Zeitwerk
|
|
417
399
|
@logger = ->(msg) { puts msg }
|
418
400
|
end
|
419
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
|
+
|
420
418
|
# --- Class methods ---------------------------------------------------------------------------
|
421
419
|
|
422
420
|
class << self
|
@@ -476,21 +474,38 @@ module Zeitwerk
|
|
476
474
|
# @return [void]
|
477
475
|
def set_autoloads_in_dir(dir, parent)
|
478
476
|
ls(dir) do |basename, abspath|
|
479
|
-
|
480
|
-
basename
|
481
|
-
|
482
|
-
autoload_file(parent, cname, abspath)
|
483
|
-
elsif dir?(abspath)
|
484
|
-
# In a Rails application, `app/models/concerns` is a subdirectory of
|
485
|
-
# `app/models`, but both of them are root directories.
|
486
|
-
#
|
487
|
-
# To resolve the ambiguity file name -> constant path this introduces,
|
488
|
-
# the `app/models/concerns` directory is totally ignored as a namespace,
|
489
|
-
# it counts only as root. The guard checks that.
|
490
|
-
unless root_dirs.key?(abspath)
|
477
|
+
begin
|
478
|
+
if ruby?(basename)
|
479
|
+
basename.slice!(-3, 3)
|
491
480
|
cname = inflector.camelize(basename, abspath).to_sym
|
492
|
-
|
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
|
493
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
|
494
509
|
end
|
495
510
|
end
|
496
511
|
end
|
@@ -526,7 +541,6 @@ module Zeitwerk
|
|
526
541
|
if autoload_path = autoload_for?(parent, cname)
|
527
542
|
# First autoload for a Ruby file wins, just ignore subsequent ones.
|
528
543
|
if ruby?(autoload_path)
|
529
|
-
shadowed_files.add(file)
|
530
544
|
log("file #{file} is ignored because #{autoload_path} has precedence") if logger
|
531
545
|
else
|
532
546
|
promote_namespace_from_implicit_to_explicit(
|
@@ -537,7 +551,6 @@ module Zeitwerk
|
|
537
551
|
)
|
538
552
|
end
|
539
553
|
elsif cdef?(parent, cname)
|
540
|
-
shadowed_files.add(file)
|
541
554
|
log("file #{file} is ignored because #{cpath(parent, cname)} is already defined") if logger
|
542
555
|
else
|
543
556
|
set_autoload(parent, cname, file)
|
@@ -653,7 +666,7 @@ module Zeitwerk
|
|
653
666
|
# @param cname [Symbol]
|
654
667
|
# @return [String]
|
655
668
|
def cpath(parent, cname)
|
656
|
-
parent.equal?(Object) ? cname.to_s : "#{parent
|
669
|
+
parent.equal?(Object) ? cname.to_s : "#{real_mod_name(parent)}::#{cname}"
|
657
670
|
end
|
658
671
|
|
659
672
|
# @param dir [String]
|
@@ -682,7 +695,20 @@ module Zeitwerk
|
|
682
695
|
# @param paths [<String, Pathname, <String, Pathname>>]
|
683
696
|
# @return [<String>]
|
684
697
|
def expand_paths(paths)
|
685
|
-
|
698
|
+
paths.flatten.map! { |path| File.expand_path(path) }
|
699
|
+
end
|
700
|
+
|
701
|
+
# @param glob_patterns [<String>]
|
702
|
+
# @return [<String>]
|
703
|
+
def expand_glob_patterns(glob_patterns)
|
704
|
+
# Note that Dir.glob works with regular file names just fine. That is,
|
705
|
+
# glob patterns technically need no wildcards.
|
706
|
+
glob_patterns.flat_map { |glob_pattern| Dir.glob(glob_pattern) }
|
707
|
+
end
|
708
|
+
|
709
|
+
# @return [void]
|
710
|
+
def recompute_ignored_paths
|
711
|
+
ignored_paths.replace(expand_glob_patterns(ignored_glob_patterns))
|
686
712
|
end
|
687
713
|
|
688
714
|
# @param message [String]
|
@@ -703,16 +729,12 @@ module Zeitwerk
|
|
703
729
|
def raise_if_conflicting_directory(dir)
|
704
730
|
self.class.mutex.synchronize do
|
705
731
|
Registry.loaders.each do |loader|
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
713
|
-
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
714
|
-
EOS
|
715
|
-
end
|
732
|
+
if loader != self && loader.manages?(dir)
|
733
|
+
require "pp"
|
734
|
+
raise Error,
|
735
|
+
"loader\n\n#{pretty_inspect}\n\nwants to manage directory #{dir}," \
|
736
|
+
" which is already managed by\n\n#{loader.pretty_inspect}\n"
|
737
|
+
EOS
|
716
738
|
end
|
717
739
|
end
|
718
740
|
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
|
@@ -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,15 @@
|
|
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
|
+
def real_mod_name(mod)
|
13
|
+
UNBOUND_METHOD_MODULE_NAME.bind(mod).call
|
14
|
+
end
|
15
|
+
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.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Xavier Noria
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-09 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
|
@@ -50,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
52
|
- !ruby/object:Gem::Version
|
51
53
|
version: '0'
|
52
54
|
requirements: []
|
53
|
-
rubygems_version: 3.0.
|
55
|
+
rubygems_version: 3.0.3
|
54
56
|
signing_key:
|
55
57
|
specification_version: 4
|
56
58
|
summary: Efficient and thread-safe constant autoloader
|