zeitwerk 2.2.2 → 2.4.2

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: b6e1f64abc085125dd45f5c8373d793417811074dcb1d5c655c915ad393d0cf5
4
- data.tar.gz: b4eba128f5267c2b3f40fdcd03085faae0a9063ef3a352f18b1bdbfd3031f068
3
+ metadata.gz: f45a7c3caf48f06e10dd513a7b074da7ec059fa3299751d361514aadc83d995e
4
+ data.tar.gz: c8c29793e95245b47b1283891791d2c20365b8c694b3dcdfb7daab0b74c16bdb
5
5
  SHA512:
6
- metadata.gz: 6928afbdf70077641fd07240ec8b22d4589e721fe0ab4a4008d3a880bf460b3daac3b7504151eae2320b98a55bd2e7cae3977c78d3dbfde156eca69270795362
7
- data.tar.gz: b64df779dcb21d094d87ee739291e7798942262b05ff32ebd841dd33b237addf7551ff641fb5e4fd8d10ccdf54f25845e77860d68931e0ebf307164c5ce076fb
6
+ metadata.gz: 6194d326b268c9333ed9d2ac1c62b65756fa9af844c5fb7ad8272ecec33aa439015506758bcd329e421508facb4153e04e45da0efb0a2a42001a6f2e0a0d3b6a
7
+ data.tar.gz: 656009f40e777f641ff1dc80f54b63559514439538c73965aa9226b6c3e33c6be71c07eb30adfe7f4cd4b3be72aa6e42918392ba27167fb9502b9aed037f36d3
data/README.md CHANGED
@@ -12,9 +12,12 @@
12
12
  - [File structure](#file-structure)
13
13
  - [Implicit namespaces](#implicit-namespaces)
14
14
  - [Explicit namespaces](#explicit-namespaces)
15
+ - [Collapsing directories](#collapsing-directories)
15
16
  - [Nested root directories](#nested-root-directories)
16
17
  - [Usage](#usage)
17
18
  - [Setup](#setup)
19
+ - [Generic](#generic)
20
+ - [for_gem](#for_gem)
18
21
  - [Autoloading](#autoloading)
19
22
  - [Eager loading](#eager-loading)
20
23
  - [Reloading](#reloading)
@@ -22,6 +25,7 @@
22
25
  - [Zeitwerk::Inflector](#zeitwerkinflector)
23
26
  - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
24
27
  - [Custom inflector](#custom-inflector)
28
+ - [The on_load callback](#the-on_load-callback)
25
29
  - [Logging](#logging)
26
30
  - [Loader tag](#loader-tag)
27
31
  - [Ignoring parts of the project](#ignoring-parts-of-the-project)
@@ -29,8 +33,11 @@
29
33
  - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
30
34
  - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
31
35
  - [Edge cases](#edge-cases)
36
+ - [Reopening third-party namespaces](#reopening-third-party-namespaces)
32
37
  - [Rules of thumb](#rules-of-thumb)
33
- - [Autoloading, explicit namespaces, and debuggers](#autoloading-explicit-namespaces-and-debuggers)
38
+ - [Debuggers](#debuggers)
39
+ - [Break](#break)
40
+ - [Byebug](#byebug)
34
41
  - [Pronunciation](#pronunciation)
35
42
  - [Supported Ruby versions](#supported-ruby-versions)
36
43
  - [Testing](#testing)
@@ -51,7 +58,9 @@ Zeitwerk is also able to reload code, which may be handy while developing web ap
51
58
 
52
59
  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.
53
60
 
54
- 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.
61
+ 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`.
62
+
63
+ Furthermore, Zeitwerk does at most one single scan of the project tree, and it descends into subdirectories lazily, only if their namespaces are used.
55
64
 
56
65
  <a id="markdown-synopsis" name="synopsis"></a>
57
66
  ## Synopsis
@@ -131,6 +140,22 @@ app/models/user.rb -> User
131
140
  app/controllers/admin/users_controller.rb -> Admin::UsersController
132
141
  ```
133
142
 
143
+ Alternatively, you can associate a custom namespace to a root directory by passing a class or module object in the optional `namespace` keyword argument.
144
+
145
+ For example, Active Job queue adapters have to define a constant after their name in `ActiveJob::QueueAdapters`.
146
+
147
+ So, if you declare
148
+
149
+ ```ruby
150
+ require "active_job"
151
+ require "active_job/queue_adapters"
152
+ loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
153
+ ```
154
+
155
+ your adapter can be stored directly in that directory instead of the canonical `#{__dir__}/active_job/queue_adapters`.
156
+
157
+ Please, note that the given namespace must be non-reloadable, though autoloaded constants in that namespace can be. That is, if you associate `app/api` with an existing `Api` module, that module should not be reloadable. However, if the project defines and autoloads the class `Api::V2::Deliveries`, that one can be reloaded.
158
+
134
159
  <a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
135
160
  ### Implicit namespaces
136
161
 
@@ -165,6 +190,32 @@ end
165
190
 
166
191
  An explicit namespace must be managed by one single loader. Loaders that reopen namespaces owned by other projects are responsible for loading their constants before setup.
167
192
 
193
+ <a id="markdown-collapsing-directories" name="collapsing-directories"></a>
194
+ ### Collapsing directories
195
+
196
+ Say some directories in a project exist for organizational purposes only, and you prefer not to have them as namespaces. For example, the `actions` subdirectory in the next example is not meant to represent a namespace, it is there only to group all actions related to bookings:
197
+
198
+ ```
199
+ booking.rb -> Booking
200
+ booking/actions/create.rb -> Booking::Create
201
+ ```
202
+
203
+ To make it work that way, configure Zeitwerk to collapse said directory:
204
+
205
+ ```ruby
206
+ loader.collapse("#{__dir__}/booking/actions")
207
+ ```
208
+
209
+ This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
210
+
211
+ You can pass directories and glob patterns. Glob patterns are expanded when they are added, and again on each reload.
212
+
213
+ To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
214
+
215
+ ```ruby
216
+ loader.collapse("#{__dir__}/*/actions")
217
+ ```
218
+
168
219
  <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
169
220
  ### Nested root directories
170
221
 
@@ -184,6 +235,9 @@ should define `Geolocatable`, not `Concerns::Geolocatable`.
184
235
  <a id="markdown-setup" name="setup"></a>
185
236
  ### Setup
186
237
 
238
+ <a id="markdown-generic" name="generic"></a>
239
+ #### Generic
240
+
187
241
  Loaders are ready to load code right after calling `setup` on them:
188
242
 
189
243
  ```ruby
@@ -200,9 +254,36 @@ loader.push_dir(...)
200
254
  loader.setup
201
255
  ```
202
256
 
203
- 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.
257
+ <a id="markdown-for_gem" name="for_gem"></a>
258
+ #### for_gem
204
259
 
205
- 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:
260
+ `Zeitwerk::Loader.for_gem` is a convenience shortcut for the common case in which a gem has its entry point directly under the `lib` directory:
261
+
262
+ ```
263
+ lib/my_gem.rb # MyGem
264
+ lib/my_gem/version.rb # MyGem::VERSION
265
+ lib/my_gem/foo.rb # MyGem::Foo
266
+ ```
267
+
268
+ Neither a gemspec nor a version file are technically required, this helper works as long as the code is organized using that standard structure.
269
+
270
+ If the entry point of your gem lives in a subdirectory of `lib` because it is reopening a namespace defined somewhere else, please use the generic API to setup the loader, and make sure you check the section [_Reopening third-party namespaces_](https://github.com/fxn/zeitwerk#reopening-third-party-namespaces) down below.
271
+
272
+ Conceptually, `for_gem` translates to:
273
+
274
+ ```ruby
275
+ # lib/my_gem.rb
276
+
277
+ require "zeitwerk"
278
+ loader = Zeitwerk::Loader.new
279
+ loader.tag = File.basename(__FILE__, ".rb")
280
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
281
+ loader.push_dir(__dir__)
282
+ ```
283
+
284
+ except that this method returns the same object in subsequent calls from the same file, in the unlikely case the gem wants to be able to reload.
285
+
286
+ If the main module references project constants at the top-level, Zeitwerk has to be ready to load them. Their definitions, in turn, may reference other project constants. And this is recursive. Therefore, it is important that the `setup` call happens above the main module definition:
206
287
 
207
288
  ```ruby
208
289
  # lib/my_gem.rb (main file)
@@ -218,8 +299,6 @@ module MyGem
218
299
  end
219
300
  ```
220
301
 
221
- 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.
222
-
223
302
  <a id="markdown-autoloading" name="autoloading"></a>
224
303
  ### Autoloading
225
304
 
@@ -424,6 +503,52 @@ class MyGem::Inflector < Zeitwerk::GemInflector
424
503
  end
425
504
  ```
426
505
 
506
+ <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
507
+ ### The on_load callback
508
+
509
+ The usual place to run something when a file is loaded is the file itself. However, sometimes you'd like to be called, and this is possible with the `on_load` callback.
510
+
511
+ For example, let's imagine this class belongs to a Rails application:
512
+
513
+ ```ruby
514
+ class SomeApiClient
515
+ class << self
516
+ attr_accessor :endpoint
517
+ end
518
+ end
519
+ ```
520
+
521
+ With `on_load`, it is easy to schedule code at boot time that initializes `endpoint` according to the configuration:
522
+
523
+ ```ruby
524
+ # config/environments/development.rb
525
+ loader.on_load("SomeApiClient") do
526
+ SomeApiClient.endpoint = "https://api.dev"
527
+ end
528
+
529
+ # config/environments/production.rb
530
+ loader.on_load("SomeApiClient") do
531
+ SomeApiClient.endpoint = "https://api.prod"
532
+ end
533
+ ```
534
+
535
+ Uses cases:
536
+
537
+ * Doing something with an autoloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
538
+ * Delaying the execution of the block until the class is loaded for performance.
539
+ * Delaying the execution of the block until the class is loaded because it follows the adapter pattern and better not to load the class if the user does not need it.
540
+ * Etc.
541
+
542
+ However, let me stress that the easiest way to accomplish that is to write whatever you have to do in the actual target file. `on_load` use cases are edgy, use it only if appropriate.
543
+
544
+ `on_load` receives the name of the target class or module as a string. The given block is executed every time its corresponding file is loaded. That includes reloads.
545
+
546
+ Multiple callbacks on the same target are supported, and they run in order of definition.
547
+
548
+ The block is executed once the loader has loaded the target. In particular, if the target was already loaded when the callback is defined, the block won't run. But if you reload and load the target again, then it will. Normally, you'll want to define `on_load` callbacks before `setup`.
549
+
550
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
551
+
427
552
  <a id="markdown-logging" name="logging"></a>
428
553
  ### Logging
429
554
 
@@ -592,6 +717,42 @@ Trip = Struct.new { ... } # NOT SUPPORTED
592
717
 
593
718
  This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
594
719
 
720
+ <a id="markdown-reopening-third-party-namespaces" name="reopening-third-party-namespaces"></a>
721
+ ### Reopening third-party namespaces
722
+
723
+ Projects managed by Zeitwerk can work with namespaces defined by third-party libraries. However, they have to be loaded in memory before calling `setup`.
724
+
725
+ For example, let's imagine you're writing a gem that implements an adapter for [Active Job](https://guides.rubyonrails.org/active_job_basics.html) that uses AwesomeQueue as backend. By convention, your gem has to define a class called `ActiveJob::QueueAdapters::AwesomeQueue`, and it has to do so in a file with a matching path:
726
+
727
+ ```ruby
728
+ # lib/active_job/queue_adapters/awesome_queue.rb
729
+ module ActiveJob
730
+ module QueueAdapters
731
+ class AwesomeQueue
732
+ # ...
733
+ end
734
+ end
735
+ end
736
+ ```
737
+
738
+ It is very important that your gem _reopens_ the modules `ActiveJob` and `ActiveJob::QueueAdapters` instead of _defining_ them. Because their proper definition lives in Active Job. Furthermore, if the project reloads, you do not want any of `ActiveJob` or `ActiveJob::QueueAdapters` to be reloaded.
739
+
740
+ Bottom line, Zeitwerk should not be managing those namespaces. Active Job owns them and defines them. Your gem needs to _reopen_ them.
741
+
742
+ In order to do so, you need to make sure those modules are loaded before calling `setup`. For instance, in the entry file for the gem:
743
+
744
+ ```ruby
745
+ # Ensure these namespaces are reopened, not defined.
746
+ require "active_job"
747
+ require "active_job/queue_adapters"
748
+
749
+ require "zeitwerk"
750
+ loader = Zeitwerk::Loader.for_gem
751
+ loader.setup
752
+ ```
753
+
754
+ With that, when Zeitwerk scans the file system and reaches the gem directories `lib/active_job` and `lib/active_job/queue_adapters`, it detects the corresponding modules already exist and therefore understands it does not have to manage them. The loader just descends into those directories. Eventually will reach `lib/active_job/queue_adapters/awesome_queue.rb`, and since `ActiveJob::QueueAdapters::AwesomeQueue` is unknown, Zeitwerk will manage it. Which is what happens regularly with the files in your gem. On reload, the namespaces are safe, won't be reloaded. The loader only reloads what it manages, which in this case is the adapter itself.
755
+
595
756
  <a id="markdown-rules-of-thumb" name="rules-of-thumb"></a>
596
757
  ### Rules of thumb
597
758
 
@@ -607,14 +768,18 @@ This only affects explicit namespaces, those idioms work well for any other ordi
607
768
 
608
769
  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.
609
770
 
610
- <a id="markdown-autoloading-explicit-namespaces-and-debuggers" name="autoloading-explicit-namespaces-and-debuggers"></a>
611
- ### Autoloading, explicit namespaces, and debuggers
771
+ <a id="markdown-debuggers" name="debuggers"></a>
772
+ ### Debuggers
773
+
774
+ <a id="markdown-break" name="break"></a>
775
+ #### Break
612
776
 
613
- 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`.
777
+ Zeitwerk works fine with [@gsamokovarov](https://github.com/gsamokovarov)'s [Break](https://github.com/gsamokovarov/break) debugger.
614
778
 
615
- 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.
779
+ <a id="markdown-byebug" name="byebug"></a>
780
+ #### Byebug
616
781
 
617
- 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.
782
+ Zeitwerk and [Byebug](https://github.com/deivid-rodriguez/byebug) are incompatible, classes or modules that belong to [explicit namespaces](#explicit-namespaces) are not autoloaded inside a Byebug session. See [this issue](https://github.com/deivid-rodriguez/byebug/issues/564#issuecomment-499413606) for further details.
618
783
 
619
784
  <a id="markdown-pronunciation" name="pronunciation"></a>
620
785
  ## Pronunciation
@@ -14,24 +14,22 @@ module Zeitwerk
14
14
  # the file system, to the loader responsible for them.
15
15
  #
16
16
  # @private
17
- # @return [{String => Zeitwerk::Loader}]
17
+ # @sig Hash[String, Zeitwerk::Loader]
18
18
  attr_reader :cpaths
19
19
 
20
20
  # @private
21
- # @return [Mutex]
21
+ # @sig Mutex
22
22
  attr_reader :mutex
23
23
 
24
24
  # @private
25
- # @return [TracePoint]
25
+ # @sig TracePoint
26
26
  attr_reader :tracer
27
27
 
28
28
  # Asserts `cpath` corresponds to an explicit namespace for which `loader`
29
29
  # is responsible.
30
30
  #
31
31
  # @private
32
- # @param cpath [String]
33
- # @param loader [Zeitwerk::Loader]
34
- # @return [void]
32
+ # @sig (String, Zeitwerk::Loader) -> void
35
33
  def register(cpath, loader)
36
34
  mutex.synchronize do
37
35
  cpaths[cpath] = loader
@@ -42,19 +40,22 @@ module Zeitwerk
42
40
  end
43
41
 
44
42
  # @private
45
- # @param loader [Zeitwerk::Loader]
46
- # @return [void]
43
+ # @sig (Zeitwerk::Loader) -> void
47
44
  def unregister(loader)
48
45
  cpaths.delete_if { |_cpath, l| l == loader }
49
46
  disable_tracer_if_unneeded
50
47
  end
51
48
 
49
+ private
50
+
51
+ # @sig () -> void
52
52
  def disable_tracer_if_unneeded
53
53
  mutex.synchronize do
54
54
  tracer.disable if cpaths.empty?
55
55
  end
56
56
  end
57
57
 
58
+ # @sig (TracePoint) -> void
58
59
  def tracepoint_class_callback(event)
59
60
  # If the class is a singleton class, we won't do anything with it so we
60
61
  # can bail out immediately. This is several orders of magnitude faster
@@ -2,16 +2,14 @@
2
2
 
3
3
  module Zeitwerk
4
4
  class GemInflector < Inflector
5
- # @param root_file [String]
5
+ # @sig (String) -> void
6
6
  def initialize(root_file)
7
7
  namespace = File.basename(root_file, ".rb")
8
8
  lib_dir = File.dirname(root_file)
9
9
  @version_file = File.join(lib_dir, namespace, "version.rb")
10
10
  end
11
11
 
12
- # @param basename [String]
13
- # @param abspath [String]
14
- # @return [String]
12
+ # @sig (String, String) -> String
15
13
  def camelize(basename, abspath)
16
14
  abspath == @version_file ? "VERSION" : super
17
15
  end
@@ -11,11 +11,9 @@ module Zeitwerk
11
11
  #
12
12
  # Takes into account hard-coded mappings configured with `inflect`.
13
13
  #
14
- # @param basename [String]
15
- # @param _abspath [String]
16
- # @return [String]
14
+ # @sig (String, String) -> String
17
15
  def camelize(basename, _abspath)
18
- overrides[basename] || basename.split('_').map!(&:capitalize).join
16
+ overrides[basename] || basename.split('_').each(&:capitalize!).join
19
17
  end
20
18
 
21
19
  # Configures hard-coded inflections:
@@ -30,8 +28,7 @@ module Zeitwerk
30
28
  # inflector.camelize("mysql_adapter", abspath) # => "MySQLAdapter"
31
29
  # inflector.camelize("users_controller", abspath) # => "UsersController"
32
30
  #
33
- # @param inflections [{String => String}]
34
- # @return [void]
31
+ # @sig (Hash[String, String]) -> void
35
32
  def inflect(inflections)
36
33
  overrides.merge!(inflections)
37
34
  end
@@ -41,7 +38,7 @@ module Zeitwerk
41
38
  # Hard-coded basename to constant name user maps that override the default
42
39
  # inflection logic.
43
40
  #
44
- # @return [{String => String}]
41
+ # @sig () -> Hash[String, String]
45
42
  def overrides
46
43
  @overrides ||= {}
47
44
  end
@@ -3,13 +3,23 @@
3
3
  module Kernel
4
4
  module_function
5
5
 
6
+ # We are going to decorate Kerner#require with two goals.
7
+ #
8
+ # First, by intercepting Kernel#require calls, we are able to autovivify
9
+ # modules on required directories, and also do internal housekeeping when
10
+ # managed files are loaded.
11
+ #
12
+ # On the other hand, if you publish a new version of a gem that is now managed
13
+ # by Zeitwerk, client code can reference directly your classes and modules and
14
+ # should not require anything. But if someone has legacy require calls around,
15
+ # they will work as expected, and in a compatible way.
16
+ #
6
17
  # We cannot decorate with prepend + super because Kernel has already been
7
18
  # included in Object, and changes in ancestors don't get propagated into
8
19
  # already existing ancestor chains.
9
20
  alias_method :zeitwerk_original_require, :require
10
21
 
11
- # @param path [String]
12
- # @return [Boolean]
22
+ # @sig (String) -> true | false
13
23
  def require(path)
14
24
  if loader = Zeitwerk::Registry.loader_for(path)
15
25
  if path.end_with?(".rb")
@@ -18,6 +28,7 @@ module Kernel
18
28
  end
19
29
  else
20
30
  loader.on_dir_autoloaded(path)
31
+ true
21
32
  end
22
33
  else
23
34
  zeitwerk_original_require(path).tap do |required|
@@ -30,4 +41,24 @@ module Kernel
30
41
  end
31
42
  end
32
43
  end
44
+
45
+ # By now, I have seen no way so far to decorate require_relative.
46
+ #
47
+ # For starters, at least in CRuby, require_relative does not delegate to
48
+ # require. Both require and require_relative delegate the bulk of their work
49
+ # to an internal C function called rb_require_safe. So, our require wrapper is
50
+ # not executed.
51
+ #
52
+ # On the other hand, we cannot use the aliasing technique above because
53
+ # require_relative receives a path relative to the directory of the file in
54
+ # which the call is performed. If a wrapper here invoked the original method,
55
+ # Ruby would resolve the relative path taking lib/zeitwerk as base directory.
56
+ #
57
+ # A workaround could be to extract the base directory from caller_locations,
58
+ # but what if someone else decorated require_relative before us? You can't
59
+ # really know with certainty where's the original call site in the stack.
60
+ #
61
+ # However, the main use case for require_relative is to load files from your
62
+ # own project. Projects managed by Zeitwerk don't do this for files managed by
63
+ # Zeitwerk, precisely.
33
64
  end