zeitwerk 2.4.0 → 2.5.0.beta2

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: 682fa352a9d6d4bf68d07cf2cbfbb539de3cfec4e8af2f930894ab27c707ca93
4
- data.tar.gz: 1e7ce6157e6046cdb60f0f1b531f1f39fb97cad3537392114be5cf84dae672c9
3
+ metadata.gz: 64ea17faefd07265c96bd8c49c9a4e14170612f85f868c03df91aa4511e473e3
4
+ data.tar.gz: fb98b0f841e49016039d73310d50563e8e6c3bbcf4c16f2d4441b241cb266647
5
5
  SHA512:
6
- metadata.gz: e308baba1a8fbe4d7c64d7195c753d795a53caefa7264d2404117cb620f2081f6b0fc05c056dafb035b44430102e4472b7eb9fb3a4fd9d70aa124b7f9a3a8313
7
- data.tar.gz: 5139d5abaeed86e256493b592460b13b19fd7602f7c099f7017b55af9f16a91429e84074a4e31fe79bf28235e02cbba3055cc4bda579bbc7de2a391c5997a393
6
+ metadata.gz: 3dfdc023489a4b7306c2bbac6f56234ff4a80887820967ff691fd9b13609ea256ba5bca301870d5e5b35d26c09ece01158a984d3093fa63d0de0bd3d9cdff458
7
+ data.tar.gz: 4860a04e9e489251ce43e56f37c7213ac3dda7c301f9dff1826d7e5f2567b992bfac35f645dab67a356bb8a4024c29d0fd8431f91896f7a254bf747864dd8ef0
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,25 +136,57 @@ 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"))
140
175
  ```
141
176
 
142
- Alternatively, you can associate a custom namespace to a root directory by passing a class or module object in the optional `namespace` keyword argument.
177
+ these are the expected classes and modules being defined by these files:
143
178
 
144
- For example, Active Job queue adapters have to define a constant after their name in `ActiveJob::QueueAdapters`.
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.
145
188
 
146
- So, if you declare
189
+ For example, given:
147
190
 
148
191
  ```ruby
149
192
  require "active_job"
@@ -151,9 +194,26 @@ require "active_job/queue_adapters"
151
194
  loader.push_dir("#{__dir__}/adapters", namespace: ActiveJob::QueueAdapters)
152
195
  ```
153
196
 
154
- your adapter can be stored directly in that directory instead of the canonical `lib/active_job/queue_adapters`.
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
+ ```
155
202
 
156
- 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.
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`.
157
217
 
158
218
  <a id="markdown-implicit-namespaces" name="implicit-namespaces"></a>
159
219
  ### Implicit namespaces
@@ -202,7 +262,7 @@ booking/actions/create.rb -> Booking::Create
202
262
  To make it work that way, configure Zeitwerk to collapse said directory:
203
263
 
204
264
  ```ruby
205
- loader.collapse("booking/actions")
265
+ loader.collapse("#{__dir__}/booking/actions")
206
266
  ```
207
267
 
208
268
  This method accepts an arbitrary number of strings or `Pathname` objects, and also an array of them.
@@ -212,22 +272,9 @@ You can pass directories and glob patterns. Glob patterns are expanded when they
212
272
  To illustrate usage of glob patterns, if `actions` in the example above is part of a standardized structure, you could use a wildcard:
213
273
 
214
274
  ```ruby
215
- loader.collapse("*/actions")
275
+ loader.collapse("#{__dir__}/*/actions")
216
276
  ```
217
277
 
218
- <a id="markdown-nested-root-directories" name="nested-root-directories"></a>
219
- ### Nested root directories
220
-
221
- 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.
222
-
223
- 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:
224
-
225
- ```
226
- app/models/concerns/geolocatable.rb
227
- ```
228
-
229
- should define `Geolocatable`, not `Concerns::Geolocatable`.
230
-
231
278
  <a id="markdown-usage" name="usage"></a>
232
279
  ## Usage
233
280
 
@@ -386,11 +433,13 @@ On reloading, client code has to update anything that would otherwise be storing
386
433
  <a id="markdown-inflection" name="inflection"></a>
387
434
  ### Inflection
388
435
 
389
- Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors.
436
+ Each individual loader needs an inflector to figure out which constant path would a given file or directory map to. Zeitwerk ships with two basic inflectors, and you can define your own.
390
437
 
391
438
  <a id="markdown-zeitwerkinflector" name="zeitwerkinflector"></a>
392
439
  #### Zeitwerk::Inflector
393
440
 
441
+ Each loader instantiated with `Zeitwerk::Loader.new` has an inflector of this type by default.
442
+
394
443
  This is a very basic inflector that converts snake case to camel case:
395
444
 
396
445
  ```
@@ -417,16 +466,16 @@ loader.inflector.inflect "mysql_adapter" => "MySQLAdapter"
417
466
 
418
467
  Overrides need to be configured before calling `setup`.
419
468
 
420
- There are no inflection rules or global configuration that can affect this inflector. It is deterministic.
421
-
422
- Loaders instantiated with `Zeitwerk::Loader.new` have an inflector of this type, independent of each other.
469
+ The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
423
470
 
424
471
  <a id="markdown-zeitwerkgeminflector" name="zeitwerkgeminflector"></a>
425
472
  #### Zeitwerk::GemInflector
426
473
 
474
+ Each loader instantiated with `Zeitwerk::Loader.for_gem` has an inflector of this type by default.
475
+
427
476
  This inflector is like the basic one, except it expects `lib/my_gem/version.rb` to define `MyGem::VERSION`.
428
477
 
429
- Loaders instantiated with `Zeitwerk::Loader.for_gem` have an inflector of this type, independent of each other.
478
+ The inflectors of different loaders are independent of each other. There are no global inflection rules or global configuration that can affect this inflector. It is deterministic.
430
479
 
431
480
  <a id="markdown-custom-inflector" name="custom-inflector"></a>
432
481
  #### Custom inflector
@@ -502,6 +551,109 @@ class MyGem::Inflector < Zeitwerk::GemInflector
502
551
  end
503
552
  ```
504
553
 
554
+ <a id="markdown-the-on_load-callback" name="the-on_load-callback"></a>
555
+ ### The on_load callback
556
+
557
+ 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.
558
+
559
+ For example, let's imagine this class belongs to a Rails application:
560
+
561
+ ```ruby
562
+ class SomeApiClient
563
+ class << self
564
+ attr_accessor :endpoint
565
+ end
566
+ end
567
+ ```
568
+
569
+ With `on_load`, it is easy to schedule code at boot time that initializes `endpoint` according to the configuration:
570
+
571
+ ```ruby
572
+ # config/environments/development.rb
573
+ loader.on_load("SomeApiClient") do |klass, _abspath|
574
+ klass.endpoint = "https://api.dev"
575
+ end
576
+
577
+ # config/environments/production.rb
578
+ loader.on_load("SomeApiClient") do |klass, _abspath|
579
+ klass.endpoint = "https://api.prod"
580
+ end
581
+ ```
582
+
583
+ Some uses cases:
584
+
585
+ * Doing something with a reloadable class or module in a Rails application during initialization, in a way that plays well with reloading. As in the previous example.
586
+ * Delaying the execution of the block until the class is loaded for performance.
587
+ * 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.
588
+
589
+ `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.
590
+
591
+ Multiple callbacks on the same target are supported, and they run in order of definition.
592
+
593
+ 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`.
594
+
595
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
596
+
597
+ It is also possible to be called when any constant managed by the loader is loaded:
598
+
599
+ ```ruby
600
+ loader.on_load do |cpath, value, abspath|
601
+ # ...
602
+ end
603
+ ```
604
+
605
+ 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.
606
+
607
+ Multiple callbacks like these are supported, and they run in order of definition.
608
+
609
+ 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.
610
+
611
+ If both types of callbacks are defined, the specific ones run first.
612
+
613
+ <a id="markdown-the-on_unload-callback" name="the-on_unload-callback"></a>
614
+ ### The on_unload callback
615
+
616
+ 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.
617
+
618
+ 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:
619
+
620
+ ```ruby
621
+ loader.on_unload("Country") do |klass, _abspath|
622
+ klass.clear_cache
623
+ end
624
+ ```
625
+
626
+ `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.
627
+
628
+ `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.
629
+
630
+ Multiple callbacks on the same target are supported, and they run in order of definition.
631
+
632
+ Defining a callback for a target not managed by the receiver is not an error, the block simply won't ever be executed.
633
+
634
+ It is also possible to be called when any constant managed by the loader is unloaded:
635
+
636
+ ```ruby
637
+ loader.on_unload do |cpath, value, abspath|
638
+ # ...
639
+ end
640
+ ```
641
+
642
+ 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.
643
+
644
+ Multiple callbacks like these are supported, and they run in order of definition.
645
+
646
+ If both types of callbacks are defined, the specific ones run first.
647
+
648
+ <a id="markdown-technical-details" name="technical-details"></a>
649
+ #### Technical details
650
+
651
+ 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?
652
+
653
+ 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.
654
+
655
+ 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.
656
+
505
657
  <a id="markdown-logging" name="logging"></a>
506
658
  ### Logging
507
659
 
data/lib/zeitwerk.rb CHANGED
@@ -3,10 +3,23 @@
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"
14
+
15
+ # This is a dangerous method.
16
+ #
17
+ # @experimental
18
+ # @sig () -> void
19
+ def self.with_loader
20
+ loader = Zeitwerk::Loader.new
21
+ yield loader
22
+ ensure
23
+ loader.unregister
24
+ end
12
25
  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