import_from 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/docs/ruby-box.md ADDED
@@ -0,0 +1,361 @@
1
+ # Ruby Box - Ruby's in-process separation of Classes and Modules
2
+
3
+ Ruby Box is designed to provide separated spaces in a Ruby process, to isolate application codes, libraries and monkey patches.
4
+
5
+ ## Known issues
6
+
7
+ * Experimental warning is shown when ruby starts with `RUBY_BOX=1` (specify `-W:no-experimental` option to hide it)
8
+ * Installing native extensions may fail under `RUBY_BOX=1` because of stack level too deep in extconf.rb
9
+ * `require 'active_support/core_ext'` may fail under `RUBY_BOX=1`
10
+ * Defined methods in a box may not be referred by built-in methods written in Ruby
11
+
12
+ ## TODOs
13
+
14
+ * Add the loaded box on iseq to check if another box tries running the iseq (add a field only when VM_CHECK_MODE?)
15
+ * Assign its own TOPLEVEL_BINDING in boxes
16
+ * Fix calling `warn` in boxes to refer `$VERBOSE` and `Warning.warn` in the box
17
+ * Make an internal data container class `Ruby::Box::Entry` invisible
18
+ * More test cases about `$LOAD_PATH` and `$LOADED_FEATURES`
19
+
20
+ ## How to use
21
+
22
+ ### Enabling Ruby Box
23
+
24
+ First, an environment variable should be set at the ruby process bootup: `RUBY_BOX=1`.
25
+ The only valid value is `1` to enable Ruby Box. Other values (or unset `RUBY_BOX`) means disabling Ruby Box. And setting the value after Ruby program starts doesn't work.
26
+
27
+ ### Using Ruby Box
28
+
29
+ `Ruby::Box` class is the entrypoint of Ruby Box.
30
+
31
+ ```ruby
32
+ box = Ruby::Box.new
33
+ box.require('something') # or require_relative, load
34
+ ```
35
+
36
+ The required file (either .rb or .so/.dll/.bundle) is loaded in the box (`box` here). The required/loaded files from `something` will be loaded in the box recursively.
37
+
38
+ ```ruby
39
+ # something.rb
40
+
41
+ X = 1
42
+
43
+ class Something
44
+ def self.x = X
45
+ def x = ::X
46
+ end
47
+ ```
48
+
49
+ Classes/modules, those methods and constants defined in the box can be accessed via `box` object.
50
+
51
+ ```ruby
52
+ X = 2
53
+ p X # 2
54
+ p ::X # 2
55
+ p box::Something.x # 1
56
+ p box::X # 1
57
+ ```
58
+
59
+ Instance methods defined in the box also run with definitions in the box.
60
+
61
+ ```ruby
62
+ s = box::Something.new
63
+
64
+ p s.x # 1
65
+ ```
66
+
67
+ ## Specifications
68
+
69
+ ### Ruby Box types
70
+
71
+ There are two box types:
72
+
73
+ * Root box
74
+ * User boxes
75
+
76
+ There is the root box, just a single box in a Ruby process. Ruby bootstrap runs in the root box, and all builtin classes/modules are defined in the root box. (See "Builtin classes and modules".)
77
+
78
+ User boxes are to run user-written programs and libraries loaded from user programs. The user's main program (specified by the `ruby` command line argument) is executed in the "main" box, which is a user box automatically created at the end of Ruby's bootstrap, copied from the root box.
79
+
80
+ When `Ruby::Box.new` is called, an "optional" box (a user, non-main box) is created, copied from the root box. All user boxes are flat, copied from the root box.
81
+
82
+ ### Ruby Box class and instances
83
+
84
+ `Ruby::Box` is a class, as a subclass of `Module`. `Ruby::Box` instances are a kind of `Module`.
85
+
86
+ ### Classes and modules defined in boxes
87
+
88
+ The classes and modules, newly defined in a box `box`, are accessible via `box`. For example, if a class `A` is defined in `box`, it is accessible as `box::A` from outside of the box.
89
+
90
+ In the box `box`, `A` can be referred to as `A` (and `::A`).
91
+
92
+ ### Built-in classes and modules reopened in boxes
93
+
94
+ In boxes, builtin classes/modules are visible and can be reopened. Those classes/modules can be reopened using `class` or `module` clauses, and class/module definitions can be changed.
95
+
96
+ The changed definitions are visible only in the box. In other boxes, builtin classes/modules and those instances work without changed definitions.
97
+
98
+ ```ruby
99
+ # in foo.rb
100
+ class String
101
+ BLANK_PATTERN = /\A\s*\z/
102
+ def blank?
103
+ self =~ BLANK_PATTERN
104
+ end
105
+ end
106
+
107
+ module Foo
108
+ def self.foo = "foo"
109
+
110
+ def self.foo_is_blank?
111
+ foo.blank?
112
+ end
113
+ end
114
+
115
+ Foo.foo.blank? #=> false
116
+ "foo".blank? #=> false
117
+
118
+ # in main.rb
119
+ box = Ruby::Box.new
120
+ box.require('foo')
121
+
122
+ box::Foo.foo_is_blank? #=> false (#blank? called in box)
123
+
124
+ "foo".blank? # NoMethodError
125
+ String::BLANK_PATTERN # NameError
126
+ ```
127
+
128
+ The main box and `box` are different boxes, so monkey patches in main are also invisible in `box`.
129
+
130
+ ### Builtin classes and modules
131
+
132
+ In the box context, "builtin" classes and modules are classes and modules:
133
+
134
+ * Accessible without any `require` calls in user scripts
135
+ * Defined before any user program start running
136
+ * Including classes/modules loaded by `prelude.rb` (including RubyGems `Gem`, for example)
137
+
138
+ Hereafter, "builtin classes and modules" will be referred to as just "builtin classes".
139
+
140
+ ### Builtin classes referred via box objects
141
+
142
+ Builtin classes in a box `box` can be referred from other boxes. For example, `box::String` is a valid reference, and `String` and `box::String` are identical (`String == box::String`, `String.object_id == box::String.object_id`).
143
+
144
+ `box::String`-like reference returns just a `String` in the current box, so its definition is `String` in the box, not in `box`.
145
+
146
+ ```ruby
147
+ # foo.rb
148
+ class String
149
+ def self.foo = "foo"
150
+ end
151
+
152
+ # main.rb
153
+ box = Ruby::Box.new
154
+ box.require('foo')
155
+
156
+ box::String.foo # NoMethodError
157
+ ```
158
+
159
+ ### Class instance variables, class variables, constants
160
+
161
+ Builtin classes can have different sets of class instance variables, class variables and constants between boxes.
162
+
163
+ ```ruby
164
+ # foo.rb
165
+ class Array
166
+ @v = "foo"
167
+ @@v = "_foo_"
168
+ V = "FOO"
169
+ end
170
+
171
+ Array.instance_variable_get(:@v) #=> "foo"
172
+ Array.class_variable_get(:@@v) #=> "_foo_"
173
+ Array.const_get(:V) #=> "FOO"
174
+
175
+ # main.rb
176
+ box = Ruby::Box.new
177
+ box.require('foo')
178
+
179
+ Array.instance_variable_get(:@v) #=> nil
180
+ Array.class_variable_get(:@@v) # NameError
181
+ Array.const_get(:V) # NameError
182
+ ```
183
+
184
+ ### Global variables
185
+
186
+ In boxes, changes on global variables are also isolated in the boxes. Changes on global variables in a box are visible/applied only in the box.
187
+
188
+ ```ruby
189
+ # foo.rb
190
+ $foo = "foo"
191
+ $VERBOSE = nil
192
+
193
+ puts "This appears: '#{$foo}'"
194
+
195
+ # main.rb
196
+ p $foo #=> nil
197
+ p $VERBOSE #=> false
198
+
199
+ box = Ruby::Box.new
200
+ box.require('foo') # "This appears: 'foo'"
201
+
202
+ p $foo #=> nil
203
+ p $VERBOSE #=> false
204
+ ```
205
+
206
+ ### Top level constants
207
+
208
+ Usually, top level constants are defined as constants of `Object`. In boxes, top level constants are constants of `Object` in the box. And the box object `box`'s constants are strictly equal to constants of `Object`.
209
+
210
+ ```ruby
211
+ # foo.rb
212
+ FOO = 100
213
+
214
+ FOO #=> 100
215
+ Object::FOO #=> 100
216
+
217
+ # main.rb
218
+ box = Ruby::Box.new
219
+ box.require('foo')
220
+
221
+ box::FOO #=> 100
222
+
223
+ FOO # NameError
224
+ Object::FOO # NameError
225
+ ```
226
+
227
+ ### Top level methods
228
+
229
+ Top level methods are private instance methods of `Object`, in each box.
230
+
231
+ ```ruby
232
+ # foo.rb
233
+ def yay = "foo"
234
+
235
+ class Foo
236
+ def self.say = yay
237
+ end
238
+
239
+ Foo.say #=> "foo"
240
+ yay #=> "foo"
241
+
242
+ # main.rb
243
+ box = Ruby::Box.new
244
+ box.require('foo')
245
+
246
+ box::Foo.say #=> "foo"
247
+
248
+ yay # NoMethodError
249
+ ```
250
+
251
+ There is no way to expose top level methods in boxes to others.
252
+ (See "Expose top level methods as a method of the box object" in "Discussions" section below)
253
+
254
+ ### Ruby Box scopes
255
+
256
+ Ruby Box works in file scope. One `.rb` file runs in a single box.
257
+
258
+ Once a file is loaded in a box `box`, all methods/procs defined/created in the file run in `box`.
259
+
260
+ ### Utility methods
261
+
262
+ Several methods are available for trying/testing Ruby Box.
263
+
264
+ * `Ruby::Box.current` returns the current box
265
+ * `Ruby::Box.enabled?` returns true/false to represent `RUBY_BOX=1` is specified or not
266
+ * `Ruby::Box.root` returns the root box
267
+ * `Ruby::Box.main` returns the main box
268
+ * `Ruby::Box#eval` evaluates a Ruby code (String) in the receiver box, just like calling `#load` with a file
269
+
270
+ ## Implementation details
271
+
272
+ #### ISeq inline method/constant cache
273
+
274
+ As described above in "Ruby Box scopes", an ".rb" file runs in a box. So method/constant resolution will be done in a box consistently.
275
+
276
+ That means ISeq inline caches work well even with boxes. Otherwise, it's a bug.
277
+
278
+ #### Method call global cache (gccct)
279
+
280
+ `rb_funcall()` C function refers to the global cc cache table (gccct), and the cache key is calculated with the current box.
281
+
282
+ So, `rb_funcall()` calls have a performance penalty when Ruby Box is enabled.
283
+
284
+ #### Current box and loading box
285
+
286
+ The current box is the box that the executing code is in. `Ruby::Box.current` returns the current box object.
287
+
288
+ The loading box is an internally managed box to determine the box to load newly required/loaded files. For example, `box` is the loading box when `box.require("foo")` is called.
289
+
290
+ ## Discussions
291
+
292
+ #### More builtin methods written in Ruby
293
+
294
+ If Ruby Box is enabled by default, builtin methods can be written in Ruby because it can't be overridden by users' monkey patches. Builtin Ruby methods can be JIT-ed, and it could bring performance reward.
295
+
296
+ #### Monkey patching methods called by builtin methods
297
+
298
+ Builtin methods sometimes call other builtin methods. For example, `Hash#map` calls `Hash#each` to retrieve entries to be mapped. Without Ruby Box, Ruby users can overwrite `Hash#each` and expect the behavior change of `Hash#map` as a result.
299
+
300
+ But with boxes, `Hash#map` runs in the root box. Ruby users can define `Hash#each` only in user boxes, so users cannot change `Hash#map`'s behavior in this case. To achieve it, users should override both`Hash#map` and `Hash#each` (or only `Hash#map`).
301
+
302
+ It is a breaking change.
303
+
304
+ Users can define methods using `Ruby::Box.root.eval(...)`, but it's clearly not ideal API.
305
+
306
+ #### Assigning values to global variables used by builtin methods
307
+
308
+ Similar to monkey patching methods, global variables assigned in a box is separated from the root box. Methods defined in the root box referring a global variable can't find the re-assigned one.
309
+
310
+ #### Context of `$LOAD_PATH` and `$LOADED_FEATURES`
311
+
312
+ Global variables `$LOAD_PATH` and `$LOADED_FEATURES` control `require` method behaviors. So those variables are determined by the loading box instead of the current box.
313
+
314
+ This could potentially conflict with the user's expectations. We should find the solution.
315
+
316
+ #### Expose top level methods as a method of the box object
317
+
318
+ Currently, top level methods in boxes are not accessible from outside of the box. But there might be a use case to call other box's top level methods.
319
+
320
+ #### Split root and builtin box
321
+
322
+ Currently, the single "root" box is the source of classext CoW. And also, the "root" box can load additional files after starting main script evaluation by calling methods which contain lines like `require "openssl"`.
323
+
324
+ That means, user boxes can have different sets of definitions according to when it is created.
325
+
326
+ ```
327
+ [root]
328
+ |
329
+ |----[main]
330
+ |
331
+ |(require "openssl" called in root)
332
+ |
333
+ |----[box1] having OpenSSL
334
+ |
335
+ |(remove_const called for OpenSSL in root)
336
+ |
337
+ |----[box2] without OpenSSL
338
+ ```
339
+
340
+ This could cause unexpected behavior differences between user boxes. It should NOT be a problem because user scripts which refer to `OpenSSL` should call `require "openssl"` by themselves.
341
+ But in the worst case, a script (without `require "openssl"`) runs well in `box1`, but doesn't run in `box2`. This situation looks like a "random failure" to users.
342
+
343
+ An option possible to prevent this situation is to have "root" and "builtin" boxes.
344
+
345
+ * root
346
+ * The box for the Ruby process bootstrap, then the source of CoW
347
+ * After starting the main box, no code runs in this box
348
+ * builtin
349
+ * The box copied from the root box at the same time with "main"
350
+ * Methods and procs defined in the "root" box run in this box
351
+ * Classes and modules required will be loaded in this box
352
+
353
+ This design realizes a consistent source of box CoW.
354
+
355
+ #### Separate `cc_tbl` and `callable_m_tbl`, `cvc_tbl` for less classext CoW
356
+
357
+ The fields of `rb_classext_t` contains several cache(-like) data, `cc_tbl`(callcache table), `callable_m_tbl`(table of resolved complemented methods) and `cvc_tbl`(class variable cache table).
358
+
359
+ The classext CoW is triggered when the contents of `rb_classext_t` are changed, including `cc_tbl`, `callable_m_tbl`, and `cvc_tbl`. But those three tables are changed by just calling methods or referring class variables. So, currently, classext CoW is triggered much more times than the original expectation.
360
+
361
+ If we can move those three tables outside of `rb_classext_t`, the number of copied `rb_classext_t` will be much less than the current implementation.
data/docs/todo.md ADDED
@@ -0,0 +1,42 @@
1
+ ```ruby
2
+
3
+ # Import multiple modules
4
+ import 'http', 'money', 'rails'
5
+
6
+ # Import multiple with individual aliases
7
+ import 'temp', as: 'Temporary'
8
+ import 'sys', as: 'System'
9
+
10
+ # Import from package
11
+ from 'money', import: 'Currency'
12
+
13
+ # Import from package with alias
14
+ from 'money', import: 'Currency', as: 'C'
15
+
16
+ # Import multiple items from module
17
+ from 'money', import: %w[Counter Currency]
18
+
19
+ # Import multiple items with aliases
20
+ from 'money', import: { Currency: 'Cur', Bank: 'Banco' }
21
+
22
+ # Import all (wildcard)
23
+ from 'money', import: '*'
24
+
25
+ # Import nested module
26
+ import 'money/currency/loader' # most ruby-like syntax
27
+
28
+ # Import nested module with alias
29
+ import 'money/currency/loader', as: 'CurrencyLoader'
30
+
31
+ # From nested module import item
32
+ from 'money/currency', import: 'Loader'
33
+
34
+ # From nested module import with alias
35
+ from 'money/currency', import: 'Loader', as: 'L'
36
+
37
+ # Multiple from nested
38
+ from 'money/currency', import: %w[Heuristics Loader]
39
+
40
+ # Deep nesting with aliases
41
+ from 'money/currency', import: { Heuristics: 'H', Loader: 'L' }
42
+ ```
data/examples/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'money', require: false
@@ -0,0 +1,26 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ bigdecimal (4.0.1)
5
+ concurrent-ruby (1.3.6)
6
+ i18n (1.14.8)
7
+ concurrent-ruby (~> 1.0)
8
+ money (7.0.2)
9
+ bigdecimal
10
+ i18n (~> 1.9)
11
+
12
+ PLATFORMS
13
+ arm64-darwin-25
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ money
18
+
19
+ CHECKSUMS
20
+ bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
21
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
22
+ i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
23
+ money (7.0.2) sha256=615c27c1fd1d190dceda5c5f4dd5af5f4d20f790ff3b62ea2fe2d96eb22e2c60
24
+
25
+ BUNDLED WITH
26
+ 4.0.3
@@ -0,0 +1,15 @@
1
+ # Examples
2
+
3
+ | Python | Ruby | Implemented | Notes |
4
+ |--------------------------------|----------------------------------------------------------------------------|-------------|-----------------------------------------|
5
+ | `import x` | [`import 'x'`](./import_x.rb) | ✅ | Same as `require 'x'` |
6
+ | `import x as y` | [`import 'x', as: 'Y'`](./import_x_as_y.rb) | ⚠️ | Requires only part of the box's context |
7
+ | `import x, y, z` | [`import 'x, y, z'`](./import_x_y_z.rb) | ⚠️ | Same as `%w[x y z].each { require it }` |
8
+ | `from x import y` | [`from 'x', import: 'Y'`](./from_x_import_y.rb) | ⚠️ | |
9
+ | `from x import y as z` | [`from 'x', import: 'Y', as: 'Z'`](./from_x_import_y_as_z.rb) | ⚠️ | |
10
+ | `from x import a, b` | [`from 'x', import: %w[A, B]`](./from_x_import_a_b.rb) | ⚠️ | |
11
+ | `from x import a as p, b as q` | [`from 'x', import: { A: 'P', B: 'Q' }`](./from_x_import_a_as_p_b_as_q.rb) | ⚠️ | |
12
+ | `from x import *` | [`from 'x', import: '*'`](./from_x_import_star.rb) | ⚠️ | |
13
+ | `from x.y.z import a` | [`from 'x/y/z', import: 'A'`](./from_x_y_z_import_a.rb) | ⚠️ | |
14
+ | `from x.y.z import a as b` | [`from 'x/y/z', import: 'A', as: 'B'`](./from_x_y_z_import_a_as_b.rb) | ⚠️ | |
15
+ | `N/A` | [`import 'x', as: 'Y' do .. end`](./import_x_as_y_scoped.rb) | ⚠️ | |
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ from 'money', import: '*'
7
+
8
+ raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+
10
+ puts "from 'money', import: '*'"
11
+ puts "Money::VERSION => #{Money::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import with alias
6
+ from 'money', import: '*', as: 'Plata'
7
+
8
+ raise 'Global namespace pollution. Expected: Money not defined. Plata defined. Got: Money defined.' if defined?(Money)
9
+
10
+ puts "from 'money', import: '*', as: 'Plata'"
11
+ puts "Plata::VERSION => #{Plata::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ from 'money', import: 'Currency' # TODO
7
+
8
+ # raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+ #
10
+ # puts "from 'money', import: '*'"
11
+ # puts "Money::VERSION => #{Money::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ from 'money', import: 'Currency', as: 'C' # TODO
7
+
8
+ # raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+ #
10
+ # puts "from 'money', import: '*'"
11
+ # puts "Money::VERSION => #{Money::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ from 'money/currency', import: 'Loader' # TODO
7
+
8
+ # raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+ #
10
+ # puts "from 'money', import: '*'"
11
+ # puts "Money::VERSION => #{Money::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ from 'money/currency', import: 'Loader', as: 'L' # TODO
7
+
8
+ # raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+ #
10
+ # puts "from 'money', import: '*'"
11
+ # puts "Money::VERSION => #{Money::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ import 'money'
7
+
8
+ raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+
10
+ puts "import 'money'"
11
+ puts "Money::VERSION => #{Money::VERSION}"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Simple import
6
+ import 'money', as: 'Plata'
7
+
8
+ raise 'Global namespace pollution. Expected: Money not defined. Plata defined. Got: Money defined.' if defined?(Money)
9
+
10
+ puts "import 'money', as: 'Plata'"
11
+ puts "Plata::VERSION => #{Plata::VERSION}"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Scoped import (block-based)
6
+ import 'money', as: 'Mon' do
7
+ # Mon only available in this block
8
+
9
+ # 10.00 USD
10
+ money = Mon.from_cents(1000, 'USD')
11
+ money.cents #=> 1000
12
+ money.currency #=> Currency.new("USD")
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require '../lib/import_from'
4
+
5
+ # Nested import
6
+ import 'money/currency' # TODO
7
+
8
+ # raise 'Expected: Money defined. Got: Money undefined.' unless defined?(Money)
9
+ #
10
+ # puts "from 'money', import: '*'"
11
+ # puts "Money::VERSION => #{Money::VERSION}"