zeitwerk 2.3.1 → 2.5.0.beta

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: 7b0087cb3e996cc4e5ccfabf179c0e793a811d24e4ee36debf9e07fcb06e1630
4
- data.tar.gz: 63c94cc509932eb9dfceb9e951f05092c30e9d3aff8bacda5a66d7da93bf4a64
3
+ metadata.gz: 7ac5e0ce2eba71a2099ead24de8762e3e737656ff4bcae57c83bb5797e922f72
4
+ data.tar.gz: 85e7c762a5164301ba5bcce82a31e6c49bc7c6caff2b4abff23c425f10374c69
5
5
  SHA512:
6
- metadata.gz: 6b978a994c59c4da2e50b32a825626a3e48c4e9e8f3455320f22799437c5fc81de4f3635ab8ea07626f42f03cbea71dc735c70b446942d8e914d5dd1104d15dc
7
- data.tar.gz: 2a0d8301b268d419be3dd36902ba44236e9de4a502c203bcbc2d32448f1c1d4e85fbae294b57d200a253e8e61e8ef07c119c9bf5f50e905d50064be6fe62babc
6
+ metadata.gz: f258acabd660f54702eb904550a109b08bdacff2cecc92d610376376f77ccf70ed421e09c64144930d2a145ebbedb7e9c3c26ff21ad2c1b40dd0537d9947b4d4
7
+ data.tar.gz: 1612dc2d505fe37b748d81a9055e36a91f9f73267ac273b39a4cbea3736cdd1baa4c557e3059d8f3a9a9807d450e29f3d75c7cb18c94c37ded70768444313336
data/README.md CHANGED
@@ -3,40 +3,48 @@
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/master?style=for-the-badge)](https://travis-ci.com/fxn/zeitwerk)
6
+ [![Build Status](https://img.shields.io/github/workflow/status/fxn/zeitwerk/CI?event=push&style=for-the-badge)](https://github.com/fxn/zeitwerk/actions?query=event%3Apush)
7
7
 
8
8
  <!-- TOC -->
9
9
 
10
10
  - [Introduction](#introduction)
11
11
  - [Synopsis](#synopsis)
12
12
  - [File structure](#file-structure)
13
- - [Implicit namespaces](#implicit-namespaces)
14
- - [Explicit namespaces](#explicit-namespaces)
15
- - [Collapsing directories](#collapsing-directories)
13
+ - [The idea: File paths match constant paths](#the-idea-file-paths-match-constant-paths)
14
+ - [Inner simple constants](#inner-simple-constants)
15
+ - [Root directories and root namespaces](#root-directories-and-root-namespaces)
16
+ - [The default root namespace is `Object`](#the-default-root-namespace-is-object)
17
+ - [Custom root namespaces](#custom-root-namespaces)
16
18
  - [Nested root directories](#nested-root-directories)
19
+ - [Implicit namespaces](#implicit-namespaces)
20
+ - [Explicit namespaces](#explicit-namespaces)
21
+ - [Collapsing directories](#collapsing-directories)
17
22
  - [Usage](#usage)
18
- - [Setup](#setup)
19
- - [Generic](#generic)
20
- - [for_gem](#for_gem)
21
- - [Autoloading](#autoloading)
22
- - [Eager loading](#eager-loading)
23
- - [Reloading](#reloading)
24
- - [Inflection](#inflection)
25
- - [Zeitwerk::Inflector](#zeitwerkinflector)
26
- - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
27
- - [Custom inflector](#custom-inflector)
28
- - [Logging](#logging)
29
- - [Loader tag](#loader-tag)
30
- - [Ignoring parts of the project](#ignoring-parts-of-the-project)
31
- - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
32
- - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
33
- - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
34
- - [Edge cases](#edge-cases)
35
- - [Reopening third-party namespaces](#reopening-third-party-namespaces)
36
- - [Rules of thumb](#rules-of-thumb)
37
- - [Debuggers](#debuggers)
38
- - [Break](#break)
39
- - [Byebug](#byebug)
23
+ - [Setup](#setup)
24
+ - [Generic](#generic)
25
+ - [for_gem](#for_gem)
26
+ - [Autoloading](#autoloading)
27
+ - [Eager loading](#eager-loading)
28
+ - [Reloading](#reloading)
29
+ - [Inflection](#inflection)
30
+ - [Zeitwerk::Inflector](#zeitwerkinflector)
31
+ - [Zeitwerk::GemInflector](#zeitwerkgeminflector)
32
+ - [Custom inflector](#custom-inflector)
33
+ - [The on_load callback](#the-on_load-callback)
34
+ - [The on_unload callback](#the-on_unload-callback)
35
+ - [Technical details](#technical-details)
36
+ - [Logging](#logging)
37
+ - [Loader tag](#loader-tag)
38
+ - [Ignoring parts of the project](#ignoring-parts-of-the-project)
39
+ - [Use case: Files that do not follow the conventions](#use-case-files-that-do-not-follow-the-conventions)
40
+ - [Use case: The adapter pattern](#use-case-the-adapter-pattern)
41
+ - [Use case: Test files mixed with implementation files](#use-case-test-files-mixed-with-implementation-files)
42
+ - [Edge cases](#edge-cases)
43
+ - [Reopening third-party namespaces](#reopening-third-party-namespaces)
44
+ - [Rules of thumb](#rules-of-thumb)
45
+ - [Debuggers](#debuggers)
46
+ - [Break](#break)
47
+ - [Byebug](#byebug)
40
48
  - [Pronunciation](#pronunciation)
41
49
  - [Supported Ruby versions](#supported-ruby-versions)
42
50
  - [Testing](#testing)
@@ -116,6 +124,9 @@ Zeitwerk::Loader.eager_load_all
116
124
  <a id="markdown-file-structure" name="file-structure"></a>
117
125
  ## File structure
118
126
 
127
+ <a id="markdown-the-idea-file-paths-match-constant-paths" name="the-idea-file-paths-match-constant-paths"></a>
128
+ ### The idea: File paths match constant paths
129
+
119
130
  To have a file structure Zeitwerk can work with, just name files and directories after the name of the classes and modules they define:
120
131
 
121
132
  ```
@@ -125,20 +136,85 @@ lib/my_gem/bar_baz.rb -> MyGem::BarBaz
125
136
  lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
126
137
  ```
127
138
 
128
- Every directory configured with `push_dir` acts as root namespace. There can be several of them. For example, given
139
+ You can tune that a bit by [collapsing directories](#collapsing-directories), or by [ignoring parts of the project](#ignoring-parts-of-the-project), but that is the main idea.
140
+
141
+ <a id="markdown-inner-simple-constants" name="inner-simple-constants"></a>
142
+ ### Inner simple constants
143
+
144
+ While a simple constant like `HttpCrawler::MAX_RETRIES` can be defined in its own file:
129
145
 
130
146
  ```ruby
131
- loader.push_dir(Rails.root.join("app/models"))
132
- loader.push_dir(Rails.root.join("app/controllers"))
147
+ # http_crawler/max_retries.rb
148
+ HttpCrawler::MAX_RETRIES = 10
133
149
  ```
134
150
 
135
- Zeitwerk understands that their respective files and subdirectories belong to the root namespace:
151
+ that is not required, you can also define it the regular way:
136
152
 
153
+ ```ruby
154
+ # http_crawler.rb
155
+ class HttpCrawler
156
+ MAX_RETRIES = 10
157
+ end
137
158
  ```
138
- app/models/user.rb -> User
139
- app/controllers/admin/users_controller.rb -> Admin::UsersController
159
+
160
+ <a id="markdown-root-directories-and-root-namespaces" name="root-directories-and-root-namespaces"></a>
161
+ ### Root directories and root namespaces
162
+
163
+ Every directory configured with `push_dir` is called a _root directory_, and they represent _root namespaces_.
164
+
165
+ <a id="markdown-the-default-root-namespace-is-object" name="the-default-root-namespace-is-object"></a>
166
+ #### The default root namespace is `Object`
167
+
168
+ By default, the namespace associated to a root directory is the top-level one: `Object`.
169
+
170
+ For example, given
171
+
172
+ ```ruby
173
+ loader.push_dir("#{__dir__}/models")
174
+ loader.push_dir("#{__dir__}/serializers"))
175
+ ```
176
+
177
+ these are the expected classes and modules being defined by these files:
178
+
179
+ ```
180
+ models/user.rb -> User
181
+ serializers/user_serializer.rb -> UserSerializer
182
+ ```
183
+
184
+ <a id="markdown-custom-root-namespaces" name="custom-root-namespaces"></a>
185
+ #### Custom root namespaces
186
+
187
+ While `Object` is by far the most common root namespace, you can associate a different one to a particular root directory. The method `push_dir` accepts a class or module object in the optional `namespace` keyword argument.
188
+
189
+ For example, given:
190
+
191
+ ```ruby
192
+ require "active_job"
193
+ require "active_job/queue_adapters"
194
+ loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
140
195
  ```
141
196
 
197
+ a file defining `ActiveJob::QueueAdapters::MyQueueAdapter` does not need the conventional parent directories, you can (and have to) store the file directly below `adapters`:
198
+
199
+ ```
200
+ adapters/my_queue_adapter.rb -> ActiveJob::QueueAdapters::MyQueueAdapter
201
+ ```
202
+
203
+ Please, note that the given root 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::Deliveries`, that one can be reloaded.
204
+
205
+ <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
206
+ #### Nested root directories
207
+
208
+ Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths.
209
+
210
+ Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file:
211
+
212
+ ```
213
+ app/models/concerns/geolocatable.rb
214
+ ```
215
+
216
+ should define `Geolocatable`, not `Concerns::Geolocatable`.
217
+
142
218
  <a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
143
219
  ### Implicit namespaces
144
220
 
@@ -186,7 +262,7 @@ booking/actions/create.rb -> Booking::Create
186
262
  To make it work that way, configure Zeitwerk to collapse said directory:
187
263
 
188
264
  ```ruby
189
- loader.collapse("booking/actions")
265
+ loader.collapse("#{__dir__}/booking/actions")
190
266
  ```
191
267
 
192
268
  This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
@@ -196,22 +272,9 @@ You can pass directories and glob patterns. Glob patterns are expanded when they
196
272
  To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
197
273
 
198
274
  ```ruby
199
- loader.collapse("*/actions")
275
+ loader.collapse("#{__dir__}/*/actions")
200
276
  ```
201
277
 
202
- <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
203
- ### Nested root directories
204
-
205
- Root directories should not be ideally nested, but Zeitwerk supports them because in Rails, for example, both `app/models` and `app/models/concerns` belong to the autoload paths.
206
-
207
- Zeitwerk detects nested root directories, and treats them as roots only. In the example above, `concerns` is not considered to be a namespace below `app/models`. For example, the file:
208
-
209
- ```
210
- app/models/concerns/geolocatable.rb
211
- ```
212
-
213
- should define `Geolocatable`, not `Concerns::Geolocatable`.
214
-
215
278
  <a id="markdown-usage" name="usage"></a>
216
279
  ## Usage
217
280
 
@@ -486,6 +549,109 @@ class MyGem::Inflector < Zeitwerk::GemInflector
486
549
  end
487
550
  ```
488
551
 
552
+ <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
553
+ ### The on_load callback
554
+
555
+ 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.
556
+
557
+ For example, let's imagine this class belongs to a Rails application:
558
+
559
+ ```ruby
560
+ class SomeApiClient
561
+ class << self
562
+ attr_accessor :endpoint
563
+ end
564
+ end
565
+ ```
566
+
567
+ With `on_load`, it is easy to schedule code at boot time that initializes `endpoint` according to the configuration:
568
+
569
+ ```ruby
570
+ # config/environments/development.rb
571
+ loader.on_load("SomeApiClient") do |klass, _abspath|
572
+ klass.endpoint = "https://api.dev"
573
+ end
574
+
575
+ # config/environments/production.rb
576
+ loader.on_load("SomeApiClient") do |klass, _abspath|
577
+ klass.endpoint = "https://api.prod"
578
+ end
579
+ ```
580
+
581
+ Some uses cases:
582
+
583
+ * 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.
584
+ * Delaying the execution of the block until the class is loaded for performance.
585
+ * 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.
586
+
587
+ `on_load` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is loaded. That includes reloads.
588
+
589
+ Multiple callbacks on the same target are supported, and they run in order of definition.
590
+
591
+ 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`.
592
+
593
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
594
+
595
+ It is also possible to be called when any constant managed by the loader is loaded:
596
+
597
+ ```ruby
598
+ loader.on_load do |cpath, value, abspath|
599
+ # ...
600
+ end
601
+ ```
602
+
603
+ The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
604
+
605
+ Multiple callbacks like these are supported, and they run in order of definition.
606
+
607
+ There are use cases for this last catch-all callback, but they are rare. If you just need to understand how things are being loaded for debugging purposes, please remember that `Zeitwerk::Loader#log!` logs plenty of information.
608
+
609
+ If both types of callbacks are defined, the specific ones run first.
610
+
611
+ <a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
612
+ ### The on_unload callback
613
+
614
+ When reloading is enabled, you may occasionally need to execute something before a certain autoloaded class or module is unloaded. The `on_unload` callback allows you to do that.
615
+
616
+ For example, let's imagine that a `Country` class fetches a list of countries and caches them when it is loaded. You might want to clear that cache if unloaded:
617
+
618
+ ```ruby
619
+ loader.on_unload("Country") do |klass, _abspath|
620
+ klass.clear_cache
621
+ end
622
+ ```
623
+
624
+ `on_unload` gets a target constant path as a string (e.g., "User", or "Service::NotificationsGateway"). When fired, its block receives the stored value, and the absolute path to the corresponding file or directory as a string. The callback is executed every time the target is unloaded.
625
+
626
+ `on_unload` blocks are executed before the class is unloaded, but in the middle of unloading, which happens in an unspecified order. Therefore, **that callback should not refer to any reloadable constant because there is no guarantee the constant works there**. Those blocks should rely on objects only, as in the example above, or regular constants not managed by the loader. This remark is transitive, applies to any methods invoked within the block.
627
+
628
+ Multiple callbacks on the same target are supported, and they run in order of definition.
629
+
630
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
631
+
632
+ It is also possible to be called when any constant managed by the loader is unloaded:
633
+
634
+ ```ruby
635
+ loader.on_unload do |cpath, value, abspath|
636
+ # ...
637
+ end
638
+ ```
639
+
640
+ The block gets the constant path as a string (e.g., "User", or "Foo::VERSION"), the value it stores (e.g., the class object stored in `User`, or "2.5.0"), and the absolute path to the corresponding file or directory as a string.
641
+
642
+ Multiple callbacks like these are supported, and they run in order of definition.
643
+
644
+ If both types of callbacks are defined, the specific ones run first.
645
+
646
+ <a id="markdown-technical-details" name="technical-details"></a>
647
+ #### Technical details
648
+
649
+ Zeitwerk uses the word "unload" to ease communication and for symmetry with `on_load`. However, in Ruby you cannot unload things for real. So, when does `on_unload` technically happen?
650
+
651
+ When unloading, Zeitwerk issues `Module#remove_const` calls. Classes and modules are no longer reachable through their constants, and `on_unload` callbacks are executed right before those calls.
652
+
653
+ Technically, though, the objects themselves are still alive, but if everything is used as expected and they are not stored in any non-reloadable place (don't do that), they are ready for garbage collection, which is when the real unloading happens.
654
+
489
655
  <a id="markdown-logging" name="logging"></a>
490
656
  ### Logging
491
657
 
data/lib/zeitwerk.rb CHANGED
@@ -3,10 +3,12 @@
3
3
  module Zeitwerk
4
4
  require_relative "zeitwerk/real_mod_name"
5
5
  require_relative "zeitwerk/loader"
6
+ require_relative "zeitwerk/autoloads"
6
7
  require_relative "zeitwerk/registry"
7
8
  require_relative "zeitwerk/explicit_namespace"
8
9
  require_relative "zeitwerk/inflector"
9
10
  require_relative "zeitwerk/gem_inflector"
10
11
  require_relative "zeitwerk/kernel"
11
12
  require_relative "zeitwerk/error"
13
+ require_relative "zeitwerk/version"
12
14
  end
@@ -0,0 +1,69 @@
1
+ module Zeitwerk
2
+ # @private
3
+ class Autoloads
4
+ # Maps crefs for which an autoload has been defined to the corresponding
5
+ # absolute path.
6
+ #
7
+ # [Object, :User] => "/Users/fxn/blog/app/models/user.rb"
8
+ # [Object, :Hotel] => "/Users/fxn/blog/app/models/hotel"
9
+ # ...
10
+ #
11
+ # This colection is transient, callbacks delete its entries as autoloads get
12
+ # executed.
13
+ #
14
+ # @sig Hash[[Module, Symbol], String]
15
+ attr_reader :c2a
16
+
17
+ # This is the inverse of c2a, for inverse lookups.
18
+ #
19
+ # @sig Hash[String, [Module, Symbol]]
20
+ attr_reader :a2c
21
+
22
+ # @sig () -> void
23
+ def initialize
24
+ @c2a = {}
25
+ @a2c = {}
26
+ end
27
+
28
+ # @sig (Module, Symbol, String) -> void
29
+ def define(parent, cname, abspath)
30
+ parent.autoload(cname, abspath)
31
+ cref = [parent, cname]
32
+ c2a[cref] = abspath
33
+ a2c[abspath] = cref
34
+ end
35
+
36
+ # @sig () { () -> [[Module, Symbol], String] } -> void
37
+ def each(&block)
38
+ c2a.each(&block)
39
+ end
40
+
41
+ # @sig (Module, Symbol) -> String?
42
+ def abspath_for(parent, cname)
43
+ c2a[[parent, cname]]
44
+ end
45
+
46
+ # @sig (String) -> [Module, Symbol]?
47
+ def cref_for(abspath)
48
+ a2c[abspath]
49
+ end
50
+
51
+ # @sig (String) -> [Module, Symbol]?
52
+ def delete(abspath)
53
+ cref = a2c.delete(abspath)
54
+ c2a.delete(cref)
55
+ cref
56
+ end
57
+
58
+ # @sig () -> void
59
+ def clear
60
+ c2a.clear
61
+ a2c.clear
62
+ end
63
+
64
+ # @sig () -> bool
65
+ def empty?
66
+ c2a.empty? && a2c.empty?
67
+ end
68
+ end
69
+ end
@@ -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,27 +40,36 @@ 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
61
62
  # than accessing its name.
62
63
  return if event.self.singleton_class?
63
64
 
64
- # Note that it makes sense to compute the hash code unconditionally,
65
- # because the trace point is disabled if cpaths is empty.
65
+ # It might be tempting to return if name.nil?, to avoid the computation
66
+ # of a hash code and delete call. But Ruby does not trigger the :class
67
+ # event on Class.new or Module.new, so that would incur in an extra call
68
+ # for nothing.
69
+ #
70
+ # On the other hand, if we were called, cpaths is not empty. Otherwise
71
+ # the tracer is disabled. So we do need to go ahead with the hash code
72
+ # computation and delete call.
66
73
  if loader = cpaths.delete(real_mod_name(event.self))
67
74
  loader.on_namespace_loaded(event.self)
68
75
  disable_tracer_if_unneeded