zeitwerk 1.0.0.beta → 1.0.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: cb5151594ce18ccd7919a1ed6b7860107ce1bfc1a02a4f9b038d77cc91ce2450
4
- data.tar.gz: e403ef92cc0a14b09bcb54d15d53ba821ad3d72e7d6216c3b0e10e6e9c0734f9
3
+ metadata.gz: cfc51c080dd7eb9b63e869a04b6eeeb14c0098b64de32d0ef872bba22a6d85d2
4
+ data.tar.gz: 0d1f743c91aefa0dd073f8f9d05c97991849cd5aa2552633048c15e93269015c
5
5
  SHA512:
6
- metadata.gz: 10f8af59a66e1462c622cfb6f5316c98808113830041f094861b1fa07a2e9a6734d1ea64a01ebc664e9b5dcf20844b443e98c6bbf196fec99a0dcfee76010331
7
- data.tar.gz: 226489835ceb91d696138a966b34054755d9fb1e6bb3245cc028d03e156f21df6a949ee329f5575be1501e8bc143afe532037ff95af2a3631e82fe72963e9479
6
+ metadata.gz: 05fba3abfe897598b45650dd837bb8e7fe4ae19f8ddd43bac1e934e2da18d1374bec14c13c46e43ad37f5cc31f991ddd9c9e429455aa7014bc4e6760b690a03d
7
+ data.tar.gz: 9ae4f99de1c0e2ace6be146c80fb7384cc893f57f2ec4e6970b44b85ce80d9cf2aa95300b2aac18cb87bc56dfd07c3d155b6b7898c21216b303a1ebc24575414
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # Zeitwerk
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/zeitwerk.svg)](https://badge.fury.io/rb/zeitwerk)
3
4
  [![Build Status](https://travis-ci.com/fxn/zeitwerk.svg?branch=master)](https://travis-ci.com/fxn/zeitwerk)
4
5
 
5
6
  <!-- TOC -->
@@ -22,6 +23,8 @@
22
23
  - [Custom inflector](#custom-inflector)
23
24
  - [Logging](#logging)
24
25
  - [Ignoring parts of the project](#ignoring-parts-of-the-project)
26
+ - [Edge cases](#edge-cases)
27
+ - [Pronunciation](#pronunciation)
25
28
  - [Supported Ruby versions](#supported-ruby-versions)
26
29
  - [Motivation](#motivation)
27
30
  - [Thanks](#thanks)
@@ -64,7 +67,7 @@ loader.push_dir(...)
64
67
  loader.setup # ready!
65
68
  ```
66
69
 
67
- The `loader` variable can go out of scope. Zeitwerk keeps a registry with all of them, and so the object won't be garbage collected and remain active.
70
+ The `loader` variable can go out of scope. Zeitwerk keeps a registry with all of them, and so the object won't be garbage collected.
68
71
 
69
72
  Later, you can reload if you want to:
70
73
 
@@ -80,7 +83,7 @@ loader.eager_load
80
83
 
81
84
  It is also possible to broadcast `eager_load` to all instances:
82
85
 
83
- ```
86
+ ```ruby
84
87
  Zeitwerk::Loader.eager_load_all
85
88
  ```
86
89
 
@@ -128,7 +131,16 @@ app/models/hotel.rb -> Hotel
128
131
  app/models/hotel/pricing.rb -> Hotel::Pricing
129
132
  ```
130
133
 
131
- Zeitwerk does not autovivify a `Hotel` module in that case. The file `app/models/hotel.rb` explicitly defines `Hotel` and Zeitwerk loads it as needed before going for `Hotel::Pricing`.
134
+ There, `app/models/hotel.rb` defines `Hotel`, and thus Zeitwerk does not autovivify a module.
135
+
136
+ The classes and modules from the namespace are already available in the body of the class or module defining it:
137
+
138
+ ```ruby
139
+ class Hotel < ApplicationRecord
140
+ include Pricing # works
141
+ ...
142
+ end
143
+ ```
132
144
 
133
145
  ### Nested root directories
134
146
 
@@ -162,6 +174,8 @@ loader.setup
162
174
 
163
175
  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.
164
176
 
177
+ 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.
178
+
165
179
  ### Reloading
166
180
 
167
181
  In order to reload code:
@@ -170,12 +184,16 @@ In order to reload code:
170
184
  loader.reload
171
185
  ```
172
186
 
173
- Generally speaking, reloading is useful for services, servers, web applications, etc. Gems that implement regular libraries, so to speak, won't normally have a use case for reloading.
187
+ Generally speaking, reloading is useful while developing running services like web applications. Gems that implement regular libraries, so to speak, won't normally have a use case for reloading.
188
+
189
+ 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.
174
190
 
175
- It is important to highlight that this is and instance method. Therefore, reloading the code of a project managed by a particular loader does _not_ reload the code of other gems using Zeitwerk at all.
191
+ It is important to highlight that this is an instance method. Don't worry about the project dependencies, their loaders are independent.
176
192
 
177
193
  In order for reloading to be thread-safe, you need to implement some coordination. For example, a web framework that serves each request with its own thread may have a globally accessible RW lock. When a request comes in, the framework acquires the lock for reading at the beginning, and the code in the framework that calls `loader.reload` needs to acquire the lock for writing.
178
194
 
195
+ On reloading, client code has to update anything that would otherwise be storing a stale object. For example, if the routing layer of a web framework stores controller class objects or instances in internal structures, on reload it has to refresh them somehow, possibly reevaluating routes.
196
+
179
197
  ### Eager loading
180
198
 
181
199
  Zeitwerk instances are able to eager load their managed files:
@@ -184,23 +202,7 @@ Zeitwerk instances are able to eager load their managed files:
184
202
  loader.eager_load
185
203
  ```
186
204
 
187
- You can opt-out of eager loading individual files or directories:
188
-
189
- ```ruby
190
- db_adapters = File.expand_path("my_gem/db_adapters", __dir__)
191
- cache_adapters = File.expand_path("my_gem/cache_adapters", __dir__)
192
- loader.do_not_eager_load(db_adapters, cache_adapters)
193
- loader.setup
194
- loader.eager_load # won't go into the directories with db/cache adapters
195
- ```
196
-
197
- Files and directories excluded from eager loading can still be loaded on demand, so an idiom like this is possible:
198
-
199
- ```ruby
200
- db_adapter = Object.const_get("MyGem::DbAdapters::#{config[:db_adapter]}")
201
- ```
202
-
203
- Please check `Zeitwerk::Loader#ignore` if you want files or directories to not be even autoloadable.
205
+ That skips ignored files and directories.
204
206
 
205
207
  If you want to eager load yourself and all dependencies using Zeitwerk, you can broadcast the `eager_load` call to all instances:
206
208
 
@@ -208,8 +210,6 @@ If you want to eager load yourself and all dependencies using Zeitwerk, you can
208
210
  Zeitwerk::Loader.eager_load_all
209
211
  ```
210
212
 
211
- In that case, exclusions are per autoloader, and so will apply to each of them accordingly.
212
-
213
213
  This may be handy in top-level services, like web applications.
214
214
 
215
215
  ### Preloading
@@ -221,14 +221,12 @@ loader.preload("app/models/videogame.rb")
221
221
  loader.preload("app/models/book.rb")
222
222
  ```
223
223
 
224
- The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
225
-
226
- The call can happen after `setup` (preloads on the spot), or before `setup` (executes during setup).
227
-
228
- If you're using reloading, preloads run on each reload too.
224
+ The call can happen before `setup` (preloads during setup), or after `setup` (preloads on the spot). Each reload preloads too.
229
225
 
230
226
  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.
231
227
 
228
+ The example above depicts several calls are supported, but `preload` accepts multiple arguments and arrays of strings as well.
229
+
232
230
  ### Inflection
233
231
 
234
232
  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.
@@ -258,7 +256,7 @@ The inflectors that ship with Zeitwerk are deterministic and simple. But you can
258
256
  ```ruby
259
257
  # frozen_string_literal: true
260
258
 
261
- class MyInflector < Zeitwerk::Inflector # or Zeitwerk::GemInflector
259
+ class MyInflector < Zeitwerk::Inflector
262
260
  def camelize(basename, _abspath)
263
261
  case basename
264
262
  when "api"
@@ -276,13 +274,13 @@ The first argument, `basename`, is a string with the basename of the file or dir
276
274
 
277
275
  The second argument, `abspath`, is a string with the absolute path to the file or directory in case you need it to decide how to inflect the basename.
278
276
 
279
- Then, assign the inflector before calling `setup`:
277
+ Then, assign the inflector:
280
278
 
281
- ```
279
+ ```ruby
282
280
  loader.inflector = MyInflector.new
283
281
  ```
284
282
 
285
- This needs to be assigned before the call to `setup`.
283
+ This needs to be done before calling `setup`.
286
284
 
287
285
  ### Logging
288
286
 
@@ -292,7 +290,7 @@ Zeitwerk is silent by default, but you can configure a callable as logger:
292
290
  loader.logger = method(:puts)
293
291
  ```
294
292
 
295
- If there is a logger configured, the loader is going to print traces when autoloads are set, files loaded, and modules autovivified.
293
+ If there is a logger configured, the loader is going to print traces when autoloads are set, files loaded, and modules autovivified. While reloading, removed autoloads and unloaded objects are also traced.
296
294
 
297
295
  If your project has namespaces, you'll notice in the traces Zeitwerk sets autoloads for _directories_. That's a technique used to be able to descend into subdirectories on demand, avoiding that way unnecessary tree walks.
298
296
 
@@ -318,6 +316,52 @@ loader.setup
318
316
 
319
317
  You can pass several arguments to this method, also an array of strings. And you can call `ignore` multiple times too.
320
318
 
319
+ Another use case for ignoring files is the adapter pattern. Let's imagine your project talks to databases, supports several, and has adapters for each one of them. Those adapters may have top-level `require` calls that load their respective drivers, but you don't want your users to install them all, only the one they are going to use. On the other hand, if your code is eager loaded by you or a parent project (with `Zeitwerk::Loader.eager_load_all`), those `require` calls are going to be executed. Ignoring the adapters prevents that:
320
+
321
+ ```ruby
322
+ db_adapters = File.expand_path("my_gem/db_adapters", __dir__)
323
+ loader.ignore(db_adapters)
324
+ loader.setup
325
+ ```
326
+
327
+ The chosen adapter, then, has to be loaded by hand somehow:
328
+
329
+ ```ruby
330
+ require "my_gem/db_adapters/#{config[:db_adapter]}"
331
+ ```
332
+
333
+ ### Edge cases
334
+
335
+ A class or module that acts as a namespace:
336
+
337
+ ```ruby
338
+ # trip.rb
339
+ class Trip
340
+ include Geolocation
341
+ end
342
+
343
+ # trip/geolocation.rb
344
+ module Trip::Geolocation
345
+ ...
346
+ end
347
+ ```
348
+
349
+ has to be defined with the `class` or `module` keywords, as in the example above.
350
+
351
+ For technical reasons, raw constant assignment is not supported:
352
+
353
+ ```ruby
354
+ # trip.rb
355
+ Trip = Class.new { ... } # NOT SUPPORTED
356
+ Trip = Struct.new { ... } # NOT SUPPORTED
357
+ ```
358
+
359
+ This only affects explicit namespaces, those idioms work well for any other ordinary class or module.
360
+
361
+ ## Pronunciation
362
+
363
+ "Zeitwerk" is pronounced [this way](https://raw.githubusercontent.com/fxn/zeitwerk/master/extras/zeitwerk_pronunciation.mp3).
364
+
321
365
  ## Supported Ruby versions
322
366
 
323
367
  Zeitwerk works with MRI 2.4.4 and above.
@@ -12,7 +12,7 @@ module Zeitwerk
12
12
  # @param _abspath [String]
13
13
  # @return [String]
14
14
  def camelize(basename, _abspath)
15
- basename.split('_').map(&:capitalize).join
15
+ basename.split('_').map!(&:capitalize!).join
16
16
  end
17
17
  end
18
18
  end
@@ -33,7 +33,7 @@ module Zeitwerk
33
33
  # Absolute paths of files or directories to be totally ignored.
34
34
  #
35
35
  # @private
36
- # @return [String]
36
+ # @return [Set<String>]
37
37
  attr_reader :ignored
38
38
 
39
39
  # Maps real absolute paths for which an autoload has been set to their
@@ -65,10 +65,6 @@ module Zeitwerk
65
65
  # @return [{String => <String>}]
66
66
  attr_reader :lazy_subdirs
67
67
 
68
- # @private
69
- # @return [Set]
70
- attr_reader :eager_load_exclusions
71
-
72
68
  # @private
73
69
  # @return [Mutex]
74
70
  attr_reader :mutex
@@ -84,18 +80,17 @@ module Zeitwerk
84
80
  def initialize
85
81
  self.inflector = Inflector.new
86
82
 
87
- @dirs = {}
88
- @preloads = []
89
- @ignored = Set.new
90
- @autoloads = {}
91
- @lazy_subdirs = {}
92
- @eager_load_exclusions = Set.new
83
+ @dirs = {}
84
+ @preloads = []
85
+ @ignored = Set.new
86
+ @autoloads = {}
87
+ @lazy_subdirs = {}
93
88
 
94
89
  @mutex = Mutex.new
95
90
  @setup = false
96
91
  @eager_loaded = false
97
92
 
98
- @tracer = TracePoint.trace(:class) do |tp|
93
+ @tracer = TracePoint.new(:class) do |tp|
99
94
  unless lazy_subdirs.empty? # do not even compute the hash key if not needed
100
95
  if subdirs = lazy_subdirs.delete(tp.self.name)
101
96
  subdirs.each { |subdir| set_autoloads_in_dir(subdir, tp.self) }
@@ -155,7 +150,6 @@ module Zeitwerk
155
150
  mutex.synchronize do
156
151
  unless @setup
157
152
  actual_dirs.each { |dir| set_autoloads_in_dir(dir, Object) }
158
- tracer.enable
159
153
  do_preload
160
154
  @setup = true
161
155
  end
@@ -174,9 +168,13 @@ module Zeitwerk
174
168
  def unload
175
169
  mutex.synchronize do
176
170
  autoloads.each do |path, (parent, cname)|
177
- # If the constant was loaded, we unload it. Otherwise, this removes
178
- # the autoload in the parent, which is something we want to do anyway.
179
- parent.send(:remove_const, cname) rescue :user_removed_it_by_hand_that_is_fine
171
+ if parent.autoload?(cname)
172
+ parent.send(:remove_const, cname)
173
+ log("autoload for #{cpath(parent, cname)} removed") if logger
174
+ elsif parent.const_defined?(cname, false)
175
+ parent.send(:remove_const, cname)
176
+ log("#{cpath(parent, cname)} unloaded") if logger
177
+ end
180
178
 
181
179
  # Let Kernel#require load the same path later again by removing it
182
180
  # from $LOADED_FEATURES. We check the extension to avoid unnecessary
@@ -188,7 +186,7 @@ module Zeitwerk
188
186
 
189
187
  Registry.on_unload(self)
190
188
 
191
- tracer.disable
189
+ disable_tracer
192
190
  @setup = false
193
191
  end
194
192
  end
@@ -207,31 +205,19 @@ module Zeitwerk
207
205
 
208
206
  # Eager loads all files in the root directories, recursively. Files do not
209
207
  # need to be in `$LOAD_PATH`, absolute file names are used. Ignored files
210
- # are not eager loaded. You can opt-out specifically in specific files and
211
- # directories with `do_not_eager_load`.
208
+ # are not eager loaded.
212
209
  #
213
210
  # @return [void]
214
211
  def eager_load
215
212
  mutex.synchronize do
216
213
  unless @eager_loaded
217
- actual_dirs.each do |dir|
218
- eager_load_dir(dir) unless eager_load_exclusions.member?(dir)
219
- end
220
- tracer.disable if eager_load_exclusions.empty?
214
+ actual_dirs.each { |dir| eager_load_dir(dir) }
215
+ disable_tracer
221
216
  @eager_loaded = true
222
217
  end
223
218
  end
224
219
  end
225
220
 
226
- # Let eager load ignore the given files or directories. The constants
227
- # defined in those files are still autoloadable.
228
- #
229
- # @param paths [<String, Pathname, <String, Pathname>>]
230
- # @return [void]
231
- def do_not_eager_load(*paths)
232
- mutex.synchronize { eager_load_exclusions.merge(expand_paths(paths)) }
233
- end
234
-
235
221
  # --- Class methods ---------------------------------------------------------------------------
236
222
 
237
223
  # This is a shortcut for
@@ -270,7 +256,7 @@ module Zeitwerk
270
256
  def on_file_loaded(file)
271
257
  if logger
272
258
  parent, cname = autoloads[file]
273
- logger.call("constant #{cpath(parent, cname)} loaded from file #{file}")
259
+ log("constant #{cpath(parent, cname)} loaded from file #{file}")
274
260
  end
275
261
  end
276
262
 
@@ -282,7 +268,7 @@ module Zeitwerk
282
268
  def on_dir_loaded(dir)
283
269
  parent, cname = autoloads[dir]
284
270
  autovivified = parent.const_set(cname, Module.new)
285
- logger.call("module #{cpath(parent, cname)} autovivified from directory #{dir}") if logger
271
+ log("module #{cpath(parent, cname)} autovivified from directory #{dir}") if logger
286
272
 
287
273
  if subdirs = lazy_subdirs[cpath(parent, cname)]
288
274
  subdirs.each { |subdir| set_autoloads_in_dir(subdir, autovivified) }
@@ -293,7 +279,7 @@ module Zeitwerk
293
279
 
294
280
  # @return [<String>]
295
281
  def actual_dirs
296
- dirs.each_key.reject { |dir| ignored.member?(dir) }
282
+ dirs.keys.delete_if { |dir| ignored.member?(dir) }
297
283
  end
298
284
 
299
285
  # @param dir [String]
@@ -321,11 +307,11 @@ module Zeitwerk
321
307
  # @param subdir [String]
322
308
  # @return [void]
323
309
  def autoload_subdir(parent, cname, subdir)
324
- if autoload_for?(parent, cname)
325
- # If there is already an autoload for this cname, maybe there are
326
- # multiple directories defining the namespace, or the cname is going to
327
- # be defined in a file (explicit namespace). In either case, we do not
328
- # need to issue another autoload, the existing one is fine.
310
+ if autoload = autoload_for?(parent, cname)
311
+ enable_tracer if ruby?(autoload)
312
+ # We do not need to issue another autoload, the existing one is enough
313
+ # no matter if it is for a file or a directory. Just remember the
314
+ # subdirectory has to be visited if the namespace is used.
329
315
  (lazy_subdirs[cpath(parent, cname)] ||= []) << subdir
330
316
  elsif !parent.const_defined?(cname, false)
331
317
  # First time we find this namespace, set an autoload for it.
@@ -351,7 +337,9 @@ module Zeitwerk
351
337
  # class/module defined in this file.
352
338
  autoloads.delete(autoload_path)
353
339
  Registry.unregister_autoload(autoload_path)
340
+
354
341
  set_autoload(parent, cname, file)
342
+ enable_tracer
355
343
  elsif !parent.const_defined?(cname, false)
356
344
  set_autoload(parent, cname, file)
357
345
  end
@@ -369,9 +357,9 @@ module Zeitwerk
369
357
  parent.autoload(cname, realpath)
370
358
  if logger
371
359
  if ruby?(realpath)
372
- logger.call("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
360
+ log("autoload set for #{cpath(parent, cname)}, to be loaded from #{realpath}")
373
361
  else
374
- logger.call("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
362
+ log("autoload set for #{cpath(parent, cname)}, to be autovivified from #{realpath}")
375
363
  end
376
364
  end
377
365
 
@@ -395,8 +383,6 @@ module Zeitwerk
395
383
  # @return [void]
396
384
  def eager_load_dir(dir)
397
385
  each_abspath(dir) do |abspath|
398
- next if eager_load_exclusions.member?(abspath)
399
-
400
386
  if ruby?(abspath)
401
387
  require abspath
402
388
  elsif dir?(abspath)
@@ -434,7 +420,7 @@ module Zeitwerk
434
420
  # @param file [String]
435
421
  # @return [Boolean]
436
422
  def do_preload_file(file)
437
- logger.call("preloading #{file}") if logger
423
+ log("preloading #{file}") if logger
438
424
  require file
439
425
  end
440
426
 
@@ -473,5 +459,21 @@ module Zeitwerk
473
459
  def expand_paths(paths)
474
460
  Array(paths).flatten.map { |path| File.expand_path(path) }
475
461
  end
462
+
463
+ # @param message [String]
464
+ # @return [void]
465
+ def log(message)
466
+ logger.call("Zeitwerk: #{message}")
467
+ end
468
+
469
+ def enable_tracer
470
+ # We check enabled? because, looking at the C source code, enabling an
471
+ # enabled tracer does not seem to be a simple no-op.
472
+ tracer.enable if !tracer.enabled?
473
+ end
474
+
475
+ def disable_tracer
476
+ tracer.disable if tracer.enabled?
477
+ end
476
478
  end
477
479
  end
@@ -83,7 +83,7 @@ module Zeitwerk
83
83
  def loader_for_gem(root_file)
84
84
  loaders_managing_gems[root_file] ||= begin
85
85
  Loader.new.tap do |loader|
86
- loader.inflector = Zeitwerk::GemInflector.new(root_file)
86
+ loader.inflector = GemInflector.new(root_file)
87
87
  loader.push_dir(File.dirname(root_file))
88
88
  end
89
89
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zeitwerk
4
- VERSION = "1.0.0.beta"
4
+ VERSION = "1.0.0.beta2"
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: 1.0.0.beta
4
+ version: 1.0.0.beta2
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-01-18 00:00:00.000000000 Z
11
+ date: 2019-01-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Zeitwerk implements constant autoloading with Ruby semantics. Each gem