mirror of
https://github.com/lloeki/package-ruby.git
synced 2025-12-06 01:54:41 +01:00
preview
This commit is contained in:
commit
32b99b62bf
20 changed files with 644 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/Gemfile.lock
|
||||
/.yardoc
|
||||
/doc
|
||||
/coverage
|
||||
*.gem
|
||||
2
.rspec
Normal file
2
.rspec
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
--color
|
||||
--require spec_helper
|
||||
3
.rubocop.yml
Normal file
3
.rubocop.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
TrivialAccessors:
|
||||
ExactNameMatch: true
|
||||
AllowPredicates: true
|
||||
7
.travis.yml
Normal file
7
.travis.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
language: ruby
|
||||
ruby:
|
||||
- 1.9.3
|
||||
- 2.0.0
|
||||
- 2.1.3
|
||||
script:
|
||||
rake spec rubocop
|
||||
3
Gemfile
Normal file
3
Gemfile
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2014 Loic Nageleisen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
211
README.md
Normal file
211
README.md
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
# Pak - A namespaced package system for Ruby
|
||||
|
||||
## Why, oh why?
|
||||
|
||||
If you are:
|
||||
|
||||
- sick of side effects when requiring?
|
||||
- think writing namespaces when you're already nested deep in directories is
|
||||
not quite DRY?
|
||||
- want easier reloading/dependency tracking/source mapping?
|
||||
|
||||
You've come to the right place.
|
||||
|
||||
## A quick note about require, monkeys, and use cases
|
||||
|
||||
I have no issue *per se* with monkey-patching, require being side-effectful WRT
|
||||
namespacing by design, and classes (modules, really) being open. The trouble
|
||||
is, most of the time it's not what we need, and more often than not it gets in
|
||||
the way in terrible ways. Hence, this, taking inspiration from Python and Go.
|
||||
|
||||
## An example, for good measure
|
||||
|
||||
Take a module named `foo.rb`:
|
||||
|
||||
````ruby
|
||||
HELLO = 'FOO'
|
||||
|
||||
def hello
|
||||
'i am foo'
|
||||
end
|
||||
```
|
||||
|
||||
Importing `foo` will make it accessible to `self`:
|
||||
|
||||
```ruby
|
||||
import('foo', as: :method)
|
||||
p foo #=> #<Package foo>
|
||||
p foo.name #=> "foo"
|
||||
p foo.hello #=> "i am foo"
|
||||
p foo::HELLO #=> "FOO"
|
||||
```
|
||||
|
||||
To avoid pollution (especially with `main` being a special case of `Object`),
|
||||
import defines a `.foo` method on the caller's `class << self`, so that `foo`
|
||||
may not become accessible from too much unexpected places.
|
||||
|
||||
```ruby
|
||||
class ABC; end
|
||||
p ABC.new.foo # NoMethodError
|
||||
```
|
||||
|
||||
You can import under another name to prevent conflict:
|
||||
|
||||
```ruby
|
||||
import('foo', as: :fee)
|
||||
p fee #=> #<Package foo>
|
||||
p fee.name #=> "foo"
|
||||
p fee.hello #=> "i am foo"
|
||||
p fee::HELLO #=> "FOO"
|
||||
```
|
||||
|
||||
Alternatively, you can import as a const:
|
||||
|
||||
```ruby
|
||||
import('foo', to: :const)
|
||||
p Foo #=> #<Package foo>
|
||||
p Foo.name #=> "foo"
|
||||
p Foo.hello #=> "i am foo"
|
||||
p Foo::HELLO #=> "FOO"
|
||||
```
|
||||
|
||||
And if that doesn't suit you, you can import as a local:
|
||||
|
||||
```ruby
|
||||
qux = import('foo', to: nil)
|
||||
p qux
|
||||
puts qux.name
|
||||
puts qux.hello
|
||||
puts qux::HELLO
|
||||
```
|
||||
|
||||
From a package `bar.rb`, you can import `foo`...:
|
||||
|
||||
```ruby
|
||||
import 'foo'
|
||||
|
||||
HELLO = 'BAR'
|
||||
|
||||
def hello
|
||||
"i am bar and I can access #{foo.name}"
|
||||
end
|
||||
```
|
||||
|
||||
...all without polluting anyone:
|
||||
|
||||
```ruby
|
||||
import('bar')
|
||||
p bar
|
||||
p bar.name
|
||||
p bar.hello
|
||||
p foo # NameError
|
||||
```
|
||||
|
||||
Note that `foo` as used by `bar` is visible to the world:
|
||||
|
||||
```ruby
|
||||
p bar.foo
|
||||
p bar.foo.name
|
||||
```
|
||||
|
||||
Packages can be nested. Here's a surprising `foo/baz.rb` file:
|
||||
|
||||
```ruby
|
||||
HELLO = 'BAZ'
|
||||
|
||||
def hello
|
||||
'i am baz'
|
||||
end
|
||||
```
|
||||
|
||||
You can guess how to use it:
|
||||
|
||||
```ruby
|
||||
import('foo/baz')
|
||||
p baz # #<Package foo/baz>
|
||||
p baz.name
|
||||
p baz.hello
|
||||
p foo # NameError
|
||||
```
|
||||
|
||||
Importing a package will load the package only once, as future import calls
|
||||
will reuse the cached version. Loaded packages can be listed and manipulated,
|
||||
allowing a reload for future instances.
|
||||
|
||||
```ruby
|
||||
foo.object_id #=> 70151878063900
|
||||
p Package.loaded #=> {"foo.rb"=>#<Package foo>, ...}
|
||||
# old_foo = Package.delete("foo")
|
||||
old_foo = Package.loaded.delete("foo.rb")
|
||||
import 'foo'
|
||||
foo.object_id #=> 70151879713940
|
||||
```
|
||||
|
||||
`foo` in `bar` will be reloaded once bar itself is reloaded. The logic is that
|
||||
while you *may* want new code to be reloaded by old code sometimes, you'd
|
||||
rather not have old code call new code in an incompatible manner. So, to
|
||||
minimize surprise, global (i.e unscoped const) reload is declared a bad thing
|
||||
and module scoped reload is favored.
|
||||
|
||||
```ruby
|
||||
bar.foo.object_id == old_foo.object_id #=> true
|
||||
```
|
||||
|
||||
Dependency tracking becomes easy, and reloading a whole graph just as well:
|
||||
|
||||
```ruby
|
||||
bar.dependencies #=> 'foo'
|
||||
bar = bar.reload! # evicts dependencies recursively and reimports bar
|
||||
|
||||
## Wishlist: setting locals directly
|
||||
|
||||
I hoped to be able to have an implicit syntax similar to Python or Go allowing
|
||||
for real local variable setting, but this seems unlikely given how local
|
||||
variables are set up by the Ruby VM: although you can get the
|
||||
`binding.of_caller`, modifying the binding doesn't *create* the variable as a
|
||||
caller's local. As such, you can guess how being forced to do `foo = nil;
|
||||
import 'foo'` is not really useful (and entirely arcane) when compared to `foo
|
||||
= import 'foo'`.
|
||||
|
||||
See how [`bind_local_variable_set`][1] works on a binding, defining new vars
|
||||
dynamically inside the binding but outside the local table, resulting in the
|
||||
following behavior (excerpted form Ruby's own inline doc):
|
||||
|
||||
```ruby
|
||||
def foo
|
||||
a = 1
|
||||
b = binding
|
||||
b.local_variable_set(:a, 2) # set existing local variable `a'
|
||||
b.local_variable_set(:b, 3) # create new local variable `b'
|
||||
# `b' exists only in binding.
|
||||
b.local_variable_get(:a) #=> 2
|
||||
b.local_variable_get(:b) #=> 3
|
||||
p a #=> 2
|
||||
p b #=> NameError
|
||||
end
|
||||
```
|
||||
|
||||
A good way to look at the local table is to use RubyVM ISeq features:
|
||||
|
||||
> puts RubyVM::InstructionSequence.disasm(-> { foo=42 })
|
||||
== disasm: <RubyVM::InstructionSequence:block in __pry__@(pry)>=========
|
||||
== catch table
|
||||
| catch type: redo st: 0002 ed: 0009 sp: 0000 cont: 0002
|
||||
| catch type: next st: 0002 ed: 0009 sp: 0000 cont: 0009
|
||||
|------------------------------------------------------------------------
|
||||
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
|
||||
[ 2] foo
|
||||
0000 trace 256 ( 13)
|
||||
0002 trace 1
|
||||
0004 putobject 42
|
||||
0006 dup
|
||||
0007 setlocal_OP__WC__0 2
|
||||
0009 trace 512
|
||||
0011 leave
|
||||
=> nil
|
||||
|
||||
That's because, IIUC, the local variables table is basically fixed and cannot
|
||||
be changed, so the binding works around that with dynavars, but it doesn't
|
||||
bubble up to the function local table.
|
||||
|
||||
[1]: https://github.com/ruby/ruby/blob/6b6ba319ea4a5afe445bad918a214b7d5691fd7c/proc.c#L473
|
||||
21
Rakefile
Normal file
21
Rakefile
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
require 'yard'
|
||||
YARD::Rake::YardocTask.new do |t|
|
||||
t.files = ['lib/**/*.rb']
|
||||
t.options = %w(- README.md LICENSE CONTRIBUTING)
|
||||
end
|
||||
|
||||
require 'rspec/core'
|
||||
require 'rspec/core/rake_task'
|
||||
desc 'Run all specs in spec directory (excluding plugin specs)'
|
||||
RSpec::Core::RakeTask.new
|
||||
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
|
||||
desc 'Run RSpec with code coverage'
|
||||
task :coverage do
|
||||
ENV['COVERAGE'] = 'yes'
|
||||
Rake::Task['spec'].execute
|
||||
end
|
||||
|
||||
task default: :spec
|
||||
151
lib/package.rb
Normal file
151
lib/package.rb
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
require 'binding_of_caller'
|
||||
|
||||
#
|
||||
class Package < Module
|
||||
#
|
||||
module KernelMethods
|
||||
NS_SEP = '/'
|
||||
|
||||
module_function
|
||||
|
||||
# Return the package as a value
|
||||
def import(caller_binding, namespace, as: nil, to: :method)
|
||||
to ||= :value
|
||||
|
||||
send("import_to_#{to}", caller_binding, namespace, as: as)
|
||||
end
|
||||
|
||||
# Return the package as a value
|
||||
def import_to_value(_binding, namespace, as: nil)
|
||||
Package.new(namespace)
|
||||
end
|
||||
|
||||
# Assign the package to a local variable in the caller's binding
|
||||
# Return the package as a value
|
||||
# /!\ Experimental
|
||||
def import_to_local(caller_binding, namespace, as: nil)
|
||||
sym = (as || ns_last(namespace)).to_sym
|
||||
setter = caller_binding.eval(<<-RUBY)
|
||||
#{sym} = nil
|
||||
-> (v) { #{sym} = v }
|
||||
RUBY
|
||||
|
||||
setter.call(Package.new(namespace))
|
||||
end
|
||||
|
||||
# Define a method in the caller's context that hands out the package
|
||||
# Return the package as a value
|
||||
def import_to_method(caller_binding, namespace, as: nil)
|
||||
sym = (as || ns_last(namespace)).to_sym
|
||||
clr = caller_binding.eval('self')
|
||||
setter = clr.instance_eval(<<-RUBY)
|
||||
-> (v) { define_singleton_method(:#{sym}) { v }; v }
|
||||
RUBY
|
||||
|
||||
setter.call(Package.new(namespace))
|
||||
end
|
||||
|
||||
# Set a const to the package in the caller's context
|
||||
# Return the package as a value
|
||||
def import_to_const(caller_binding, namespace, as: nil)
|
||||
sym = (as || ns_classify(ns_last(namespace)).to_sym)
|
||||
clr = caller_binding.eval('self')
|
||||
target = clr.respond_to?(:const_set) ? clr : clr.class
|
||||
setter = target.instance_eval(<<-RUBY)
|
||||
-> (v) { const_set(:#{sym}, v) }
|
||||
RUBY
|
||||
|
||||
setter.call(Package.new(namespace))
|
||||
end
|
||||
|
||||
def ns_from_filename(ns)
|
||||
ns.gsub('/', NS_SEP).gsub(/\.rb$/, '')
|
||||
end
|
||||
|
||||
def ns_to_filename(ns)
|
||||
ns.gsub(NS_SEP, '/') + '.rb'
|
||||
end
|
||||
|
||||
def ns_last(ns)
|
||||
ns.split(NS_SEP).last
|
||||
end
|
||||
|
||||
def ns_classify(namespace)
|
||||
namespace.split(NS_SEP).map! do |v|
|
||||
v.split('_').map!(&:capitalize).join('')
|
||||
end.join('::')
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def new(file)
|
||||
file = KernelMethods.ns_to_filename(file)
|
||||
self[file] ||= super(file)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
loaded[key]
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
loaded[key] = value
|
||||
end
|
||||
|
||||
def loaded
|
||||
@store ||= {}
|
||||
end
|
||||
|
||||
def delete(ns)
|
||||
@store.delete(ns_to_filename(ns))
|
||||
end
|
||||
|
||||
def reload!
|
||||
@store = {}
|
||||
end
|
||||
|
||||
def path
|
||||
$LOAD_PATH
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(file)
|
||||
@source_file = file
|
||||
@name = KernelMethods.ns_from_filename(file)
|
||||
load_in_module(file)
|
||||
end
|
||||
|
||||
attr_reader :name
|
||||
alias_method :to_s, :name
|
||||
|
||||
def load(file, wrap = false)
|
||||
wrap ? super : load_in_module(File.join(dir, file))
|
||||
rescue Errno::ENOENT
|
||||
super
|
||||
end
|
||||
|
||||
def inspect
|
||||
format("#<#{self.class.name}(#{name}):0x%014x>", object_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_in_module(file)
|
||||
module_eval(IO.read(file),
|
||||
File.expand_path(file))
|
||||
rescue Errno::ENOENT
|
||||
raise
|
||||
end
|
||||
|
||||
def method_added(name)
|
||||
module_function(name)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
module Kernel
|
||||
def import(*args)
|
||||
caller_binding = args.last.is_a?(Binding) ? args.pop : binding.of_caller(1)
|
||||
args.unshift(caller_binding)
|
||||
Package::KernelMethods.import(*args)
|
||||
end
|
||||
end
|
||||
1
lib/pak.rb
Normal file
1
lib/pak.rb
Normal file
|
|
@ -0,0 +1 @@
|
|||
require_relative 'package'
|
||||
20
pak.gemspec
Normal file
20
pak.gemspec
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
Gem::Specification.new do |s|
|
||||
s.name = 'pak'
|
||||
s.version = '1.0.0'
|
||||
s.licenses = ['MIT']
|
||||
s.summary = 'Packaged namespacing for Ruby'
|
||||
s.description = 'Implicit namespacing and package definition, '\
|
||||
'inspired by Python, Go, CommonJS.'
|
||||
s.authors = ['Loic Nageleisen']
|
||||
s.email = 'loic.nageleisen@gmail.com'
|
||||
s.files = Dir['lib/*']
|
||||
|
||||
s.add_development_dependency 'rake', '~> 10.3'
|
||||
s.add_development_dependency 'rspec', '~> 3.0'
|
||||
s.add_development_dependency 'simplecov'
|
||||
s.add_development_dependency 'rubocop'
|
||||
s.add_development_dependency 'yard', '~> 0.8.7'
|
||||
s.add_development_dependency 'binding_of_caller'
|
||||
|
||||
s.add_development_dependency 'pry'
|
||||
end
|
||||
0
spec/fixtures/empty_package.rb
vendored
Normal file
0
spec/fixtures/empty_package.rb
vendored
Normal file
68
spec/package_spec.rb
Normal file
68
spec/package_spec.rb
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
require 'package'
|
||||
|
||||
RSpec.describe Package do
|
||||
context 'instance' do
|
||||
let(:package) { Package.new('spec/fixtures/empty_package') }
|
||||
|
||||
it { expect(package).to be_a Module }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'Kernel#import' do
|
||||
before(:each) { Package.reload! }
|
||||
|
||||
[Module, Object].each do |ctx|
|
||||
let(:package_name) { 'spec/fixtures/empty_package' }
|
||||
let(:target) { nil }
|
||||
let(:pak) do
|
||||
t = target
|
||||
n = package_name
|
||||
context.instance_eval { import(n, to: t) }
|
||||
end
|
||||
|
||||
context "called inside #{ctx.name}.new" do
|
||||
let(:context) { ctx.new }
|
||||
|
||||
it 'should import a package as a value' do
|
||||
expect(pak).to be_a Package
|
||||
expect(context).not_to respond_to :empty_package
|
||||
expect do
|
||||
context.instance_eval { EmptyPackage }
|
||||
end.to raise_error NameError
|
||||
expect do
|
||||
context.instance_eval { empty_package }
|
||||
end.to raise_error NameError
|
||||
end
|
||||
|
||||
it 'should import a package as a method' do
|
||||
pak = nil
|
||||
context = Module.new do
|
||||
pak = import('spec/fixtures/empty_package', to: :method)
|
||||
end
|
||||
|
||||
expect(context).to respond_to :empty_package
|
||||
expect(context.empty_package).to eq pak
|
||||
end
|
||||
|
||||
it 'should import a package as a const' do
|
||||
pak = nil
|
||||
context = Module.new do
|
||||
pak = import('spec/fixtures/empty_package', to: :const)
|
||||
end
|
||||
|
||||
expect(context).to have_constant :EmptyPackage
|
||||
expect(context::EmptyPackage).to eq pak
|
||||
end
|
||||
|
||||
it 'should import a package as a local' do
|
||||
pending 'not ready yet'
|
||||
|
||||
context = Module.new do
|
||||
import('spec/fixtures/empty_package', to: :local)
|
||||
end
|
||||
|
||||
expect(context.instance_eval { empty_package }).to be_a Package
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
spec/spec_helper.rb
Normal file
26
spec/spec_helper.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
if ENV['COVERAGE'] == 'yes'
|
||||
require 'simplecov'
|
||||
SimpleCov.start
|
||||
end
|
||||
|
||||
require 'support/matchers'
|
||||
require 'pry'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.disable_monkey_patching!
|
||||
config.warnings = true
|
||||
|
||||
config.default_formatter = 'doc' if config.files_to_run.one?
|
||||
|
||||
config.order = :random
|
||||
|
||||
Kernel.srand config.seed
|
||||
end
|
||||
5
spec/support/matchers.rb
Normal file
5
spec/support/matchers.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
RSpec::Matchers.define :have_constant do |const|
|
||||
match do |owner|
|
||||
owner.const_defined?(const)
|
||||
end
|
||||
end
|
||||
47
test.rb
Normal file
47
test.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
$LOAD_PATH.push File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
|
||||
|
||||
require 'pry'
|
||||
require 'binding_of_caller'
|
||||
require 'package'
|
||||
|
||||
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
Dir.chdir('test')
|
||||
|
||||
import('foo')
|
||||
p foo
|
||||
puts foo.name
|
||||
puts foo.hello
|
||||
puts foo::HELLO
|
||||
|
||||
import('foo', as: :fee)
|
||||
p fee
|
||||
puts fee.name
|
||||
puts fee.hello
|
||||
puts fee::HELLO
|
||||
|
||||
import('foo', to: :const)
|
||||
p Foo
|
||||
puts Foo.name
|
||||
puts Foo.hello
|
||||
puts Foo::HELLO
|
||||
|
||||
import('bar')
|
||||
p bar
|
||||
p bar.name
|
||||
p bar.hello
|
||||
p bar.foo
|
||||
p bar.foo.name
|
||||
|
||||
import('foo/baz', as: 'baz2')
|
||||
p baz2.name
|
||||
|
||||
import('foo/baz')
|
||||
p baz.name
|
||||
|
||||
p Package.loaded
|
||||
end
|
||||
|
||||
class ABC; end
|
||||
|
||||
p ABC.new.foo
|
||||
7
test/bar.rb
Normal file
7
test/bar.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import 'foo'
|
||||
|
||||
HELLO = 'BAR'
|
||||
|
||||
def hello
|
||||
"i am bar and I can access #{foo.name}"
|
||||
end
|
||||
5
test/foo.rb
Normal file
5
test/foo.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
HELLO = 'FOO'
|
||||
|
||||
def hello
|
||||
'i am foo'
|
||||
end
|
||||
5
test/foo/baz.rb
Normal file
5
test/foo/baz.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
HELLO = 'BAZ'
|
||||
|
||||
def hello
|
||||
'i am baz'
|
||||
end
|
||||
38
test2.rb
Normal file
38
test2.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
require 'pp'
|
||||
#require 'pry'
|
||||
require 'binding_of_caller'
|
||||
|
||||
def cards
|
||||
ace = 'of hearts'
|
||||
queen = 'of diamonds'
|
||||
binding
|
||||
end
|
||||
|
||||
c = cards
|
||||
|
||||
pp c.eval('ace')
|
||||
pp c.eval('ace = "of spades"')
|
||||
pp c.eval('ace')
|
||||
pp c.eval('foo = 42')
|
||||
pp c.eval('foo')
|
||||
|
||||
def set_baz(b)
|
||||
b.eval('baz = 44')
|
||||
pp b.eval('baz')
|
||||
pp binding.callers
|
||||
end
|
||||
|
||||
def meh
|
||||
foo = 42
|
||||
bar = 43
|
||||
#baz = nil
|
||||
|
||||
set_baz binding
|
||||
binding
|
||||
end
|
||||
|
||||
m = meh
|
||||
|
||||
pp m.eval('foo')
|
||||
pp m.eval('bar')
|
||||
pp m.eval('baz')
|
||||
Loading…
Add table
Add a link
Reference in a new issue