Skip to content

Commit c5a7060

Browse files
transclaude
andcommitted
v0.3.0 — fold in import/import_relative from rubyworks/finder
Add Kernel#import and Kernel#import_relative as scope-aware alternatives to require / require_relative. They evaluate the loaded script into the current scope rather than the toplevel, providing a partial workaround for Ruby's toplevel pollution of Object (Problem 1 in the README). The two libraries are conceptually about the same thing — making Ruby's toplevel/module loading semantics behave more sensibly — so consolidating them makes sense. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 15679f9 commit c5a7060

7 files changed

Lines changed: 171 additions & 8 deletions

File tree

HISTORY.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# RELEASE HISTORY
22

3+
## 0.3.0 / 2026-04-08
4+
5+
Fold in the `import`/`import_relative` feature from rubyworks/finder.
6+
7+
Changes:
8+
9+
* Add `Kernel#import` and `Kernel#import_relative` (require
10+
`'main_like_module/import'`) as scope-aware alternatives to `require`
11+
and `require_relative`. They evaluate the loaded script into the
12+
current scope rather than into the toplevel, providing a partial
13+
workaround for Problem 1 (toplevel pollution of Object).
14+
* Update README to document the new feature and explain how the two
15+
pieces of the library address Ruby's two toplevel design quirks.
16+
17+
318
## 0.2.0 / 2026-04-07
419

520
Maintenance release modernizing the project.

README.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,14 @@ not a hand-picked subset.
8787

8888
## What This Library Does
8989

90-
The first problem — toplevel polluting `Object` — cannot be fixed from
91-
userland. It is baked into the parser and the interpreter. Only a change
92-
to Ruby itself could address it.
90+
This library addresses both problems, though only one of them fully.
91+
92+
### Completing the Toplevel Proxy (Problem 2)
93+
94+
```ruby
95+
require 'main_like_module'
96+
```
9397

94-
The second problem *can* be addressed, and that is what this library does.
9598
On `require`, it walks `Module`'s instance methods and, for each one not
9699
already available at the toplevel, defines a singleton method on `main`
97100
that forwards the call to `Object.class_eval`. The result is that anything
@@ -108,9 +111,45 @@ you can do inside a `module` body, you can now do at the toplevel as well.
108111
Public, private, and protected `Module` methods are all proxied with their
109112
correct visibility.
110113

111-
Note carefully: this does *nothing* about Problem 1. The methods you define
112-
at the toplevel still end up on `Object`. This library only completes the
113-
proxy; it does not redesign the toplevel.
114+
### A Partial Workaround for Toplevel Pollution (Problem 1)
115+
116+
```ruby
117+
require 'main_like_module/import'
118+
```
119+
120+
The parser-level pollution of `Object` cannot be fixed from userland —
121+
that ship sailed when the Ruby grammar was written. But for code you
122+
*write yourself*, this library provides `Kernel#import` and
123+
`Kernel#import_relative` as scope-aware alternatives to `require` and
124+
`require_relative`. They evaluate the loaded script directly into the
125+
*current scope* rather than into the toplevel:
126+
127+
```ruby
128+
require 'main_like_module/import'
129+
130+
module MyApp
131+
import 'helpers' # helpers.rb is evaluated into MyApp,
132+
# not into Object
133+
end
134+
135+
MyApp.some_helper_method #=> works
136+
Object.private_instance_methods.include?(:some_helper_method)
137+
#=> false -- Object stays clean
138+
```
139+
140+
`import` searches `$LOAD_PATH` the way `require` does. `import_relative`
141+
resolves its argument relative to the calling file the way
142+
`require_relative` does. Both raise `LoadError` on miss.
143+
144+
This is a *partial* workaround. It only helps for code that uses `import`
145+
explicitly — toplevel code in scripts you don't control still pollutes
146+
`Object`. But for organizing your own libraries into clean module
147+
namespaces without ceremony, it is genuinely useful.
148+
149+
`import` was originally part of the `rubyworks/finder` gem, which has
150+
been folded into `main_like_module` because the two libraries are really
151+
about the same thing: making Ruby's toplevel/module loading semantics
152+
behave more sensibly.
114153

115154

116155
## Should You Use This?

Rakefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ end
1010
desc "Run tests"
1111
task :test do
1212
sh 'ruby -Ilib -Itest test/test_main_like_module.rb'
13+
sh 'ruby -Ilib -Itest test/test_import.rb'
1314
end
1415

1516
task :default => :test

lib/main_like_module/import.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Kernel#import and Kernel#import_relative are variants of #require and
2+
# #require_relative that evaluate the loaded script *into the current scope*
3+
# instead of into the toplevel. This provides a partial workaround for the
4+
# fact that toplevel definitions in Ruby pollute Object globally.
5+
#
6+
# module MyApp
7+
# import 'somefile' # somefile's contents are loaded into MyApp,
8+
# # not into Object
9+
# end
10+
#
11+
# Originally extracted from rubyworks/finder.
12+
13+
module Kernel
14+
15+
private
16+
17+
# Find +feature+ in $LOAD_PATH and evaluate it into the current scope.
18+
#
19+
# Unlike #require, definitions in the loaded file are added to whatever
20+
# scope #import was called from rather than to Object.
21+
def import(feature)
22+
file = nil
23+
if File.file?(feature) && File.absolute_path(feature) == feature
24+
file = feature
25+
else
26+
candidates = feature.end_with?('.rb') ? [feature] : ["#{feature}.rb", feature]
27+
$LOAD_PATH.each do |dir|
28+
candidates.each do |c|
29+
path = File.expand_path(c, dir)
30+
if File.file?(path)
31+
file = path
32+
break
33+
end
34+
end
35+
break if file
36+
end
37+
end
38+
raise LoadError, "no such file to import -- #{feature}" unless file
39+
instance_eval(::File.read(file), file)
40+
end
41+
42+
# Like #import, but resolves +fname+ relative to the file that called
43+
# #import_relative (mirroring how #require_relative works).
44+
def import_relative(fname)
45+
call = caller.first
46+
fail "Can't parse #{call}" unless call.rindex(/:\d+(:in [`'].*[`'])?$/)
47+
path = $`
48+
if /\A\((.*)\)/ =~ path # eval, etc.
49+
raise LoadError, "import_relative is called in #{$1}"
50+
end
51+
base = File.expand_path(fname, File.dirname(path))
52+
file = if File.file?(base)
53+
base
54+
elsif File.file?("#{base}.rb")
55+
"#{base}.rb"
56+
end
57+
raise LoadError, "no such file to import -- #{base}" unless file
58+
instance_eval(::File.read(file), file)
59+
end
60+
61+
end

main_like_module.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |s|
22
s.name = 'main_like_module'
3-
s.version = '0.2.0'
3+
s.version = '0.3.0'
44
s.summary = 'Completes the toplevel proxy of the Object class.'
55
s.description = 'A small demonstration library that fills in the missing ' \
66
'parts of Ruby\'s toplevel object so that it behaves like a ' \

test/fixtures/greeter.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def hello
2+
'hello from greeter'
3+
end

test/test_import.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'minitest/autorun'
2+
require 'main_like_module/import'
3+
4+
class ImportTest < Minitest::Test
5+
6+
def setup
7+
@fixtures = File.expand_path('fixtures', __dir__)
8+
$LOAD_PATH.unshift(@fixtures) unless $LOAD_PATH.include?(@fixtures)
9+
end
10+
11+
def teardown
12+
$LOAD_PATH.delete(@fixtures)
13+
end
14+
15+
def test_import_into_module_scope
16+
target = Module.new
17+
target.module_eval { import 'greeter' }
18+
assert_equal 'hello from greeter', target.send(:hello)
19+
end
20+
21+
def test_import_does_not_pollute_object
22+
target = Module.new
23+
target.module_eval { import 'greeter' }
24+
refute Object.private_instance_methods.include?(:hello),
25+
'import should not add methods to Object'
26+
end
27+
28+
def test_import_relative
29+
target = Module.new
30+
fixture = File.expand_path('fixtures/greeter.rb', __dir__)
31+
target.module_eval(<<~RUBY, fixture, 1)
32+
import_relative 'greeter'
33+
RUBY
34+
assert_equal 'hello from greeter', target.send(:hello)
35+
end
36+
37+
def test_import_raises_for_missing_file
38+
target = Module.new
39+
assert_raises(LoadError) do
40+
target.module_eval { import 'nonexistent_xyz' }
41+
end
42+
end
43+
44+
end

0 commit comments

Comments
 (0)