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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cf7265c8e1a167cdf0107bdef2643d219317855b0f5a06b77048672e3a192a3
4
- data.tar.gz: d1429158609c03b57f85b261290e61d88c7762f02693232336a0f4eb94bc7417
3
+ metadata.gz: b7d5a0f54405ad45dd6afc9bf54bde579f66a75fb142cdbbfb5476157dd75493
4
+ data.tar.gz: b81311bb19c3b699a6720a3bb3d6df503b858a439c5f1f2605e029117de36bcb
5
5
  SHA512:
6
- metadata.gz: 3457b0e4afeb1e134db7815e538c8251aab11bda9b9feb8eec07257506786693f9f9ec0c0eb7a11c0663502f48778b37939cb47483d84b369ed82830518ff78a
7
- data.tar.gz: 4996a053629f48004cb4c86f24a649295baaf1afbde0a2a5b5be0c3163205376659382ac3a9322586e2b86fee200c8bf97c5bb8faa2ae17c19aa0b169457af09
6
+ metadata.gz: 4f6fd6e8f7bd479ca6935d1f57d065151aff3be6977ad068f6b4ecdce200bbd131f4db4f2cc1bc2a719cb332b538cf24be55eb936a51572ac5c06676d21763f2
7
+ data.tar.gz: d4266c65e380b356ff4df7394f2f1882f72ac32b5226dedd717f6b4064c8ac1b8fd3d70ec08b6f9838debfc3690d2f3eee2d6885e446f087a686a619c872c718
@@ -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.svg?style=for-the-badge&branch=master)](https://travis-ci.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 # ready!
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, resets the loader so that it will pick whatever is in the file system now, and runs preloads if there are any.
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
- This is the default inflector.
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
- The loader instantiated behind the scenes by `Zeitwerk::Loader.for_gem` gets assigned by default an inflector that is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
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, _abspath)
313
- case basename
314
- when "api"
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 on setup and on reload.
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>
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
+ require_relative "zeitwerk/real_mod_name"
4
5
  require_relative "zeitwerk/loader"
5
6
  require_relative "zeitwerk/registry"
6
7
  require_relative "zeitwerk/explicit_namespace"
@@ -4,4 +4,7 @@ module Zeitwerk
4
4
 
5
5
  class ReloadingDisabledError < Error
6
6
  end
7
+
8
+ class NameError < ::NameError
9
+ end
7
10
  end
@@ -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
- # 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(event.self.name)
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 # :nodoc:
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
- (basename == "version" && abspath == @version_file) ? "VERSION" : super
16
+ abspath == @version_file ? "VERSION" : super
17
17
  end
18
18
  end
19
19
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- class Inflector # :nodoc:
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
@@ -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 :ignored
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
- @ignored = Set.new
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(*paths)
228
- mutex.synchronize { ignored.merge(expand_paths(paths)) }
229
- end
230
-
231
- # @private
232
- # @return [void]
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
- if ruby?(basename)
465
- basename.slice!(-3, 3)
466
- cname = inflector.camelize(basename, abspath).to_sym
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
- autoload_subdir(parent, cname, abspath)
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
- def strict_autoload_path(parent, cname)
597
- parent.autoload?(cname) if cdef?(parent, cname)
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.name}::#{cname}"
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
- Array(paths).flatten.map! { |path| File.expand_path(path) }
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
- next if loader == self
690
-
691
- loader.dirs.each do |already_managed_dir|
692
- if dir.start_with?(already_managed_dir) || already_managed_dir.start_with?(dir)
693
- require "pp"
694
- raise Error,
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.name)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "2.1.7"
4
+ VERSION = "2.2.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeitwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.7
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-06-28 00:00:00.000000000 Z
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