attic 0.6.pre.RC1 → 1.0.0.pre.RC2

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: cab1b37cf99fda2d7f47b898c998af908e2c8f4dcdde31ae66cd34d7c337a993
4
- data.tar.gz: 7d579f1320f3493b6639705d316aaf1197f77aa20894ecff84abd0a34a162124
3
+ metadata.gz: 65eaa51d7ee74a1e97bef40a3cdb0c6cb8c78ca29e0cd8f8bb55e0cba641f664
4
+ data.tar.gz: fbc75d2a1063522b54edfc6d608a2f4599ac2b34c39a92aa400b138138f19cd8
5
5
  SHA512:
6
- metadata.gz: 920f145e1b3eb27151653a69be076fe3224c82e2781695209e2017ac4634708855dc0331ad254a9765bd185f25e566b1c6a712bc6118668c201ec0fa240239fc
7
- data.tar.gz: e0e4204bd76ae3f76c7c3a51166c278f69b2152db651a2ccd28ce8d559f281bfbf18e253713608fad2d9d488b96f52d3397070de7cc606842c3f47971db1276a
6
+ metadata.gz: 2a54f4d47b2aa70d9a8d6ee30578d5c06360e9e5c2ffd3c5dfa196086b40122dfee3d84b079cbabd9a24076262b8d8b4521d50a649fe111c3f8f3b80d20945da
7
+ data.tar.gz: b06ab0a4a824c1536046f4d36ee0a42ce0e5315e214ef398955cf45f7046c56937d93333102044a1a4607fbe4780dd176d0377569126320e81f623c220527c16
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ .bundle
3
+ .byebug*
4
+ .history
5
+ .devcontainer
6
+ .vscode
7
+ *.env
8
+ *.log
9
+ *.md
10
+ *.txt
11
+ !LICENSE.txt
12
+ .ruby-version
13
+ appendonlydir
14
+ Gemfile.lock
15
+ etc/config
16
+ log
17
+ tmp
18
+ vendor
data/.rubocop.yml ADDED
@@ -0,0 +1,28 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.1, 3.2
4
+ EnabledByDefault: false
5
+
6
+ Style/StringLiterals:
7
+ Enabled: true
8
+ EnforcedStyle: double_quotes
9
+
10
+ Style/StringLiteralsInInterpolation:
11
+ Enabled: true
12
+ EnforcedStyle: double_quotes
13
+
14
+ Layout/LineLength:
15
+ Enabled: true
16
+ Max: 120
17
+
18
+ Metrics/CyclomaticComplexity:
19
+ Enabled: false
20
+
21
+ Layout/HashAlignment:
22
+ Enabled: false
23
+
24
+ Lint/Void:
25
+ Enabled: false
26
+
27
+ Metrics/MethodLength:
28
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ ruby '>= 3.1'
6
+
7
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
8
+
9
+ # bundle install --without production
10
+ # bundle install --with development
11
+ # bundle install --without development
12
+ group "development" do
13
+ gem "byebug"
14
+ gem "pry"
15
+ gem "pry-doc"
16
+ gem "rubocop"
17
+ gem "tryouts", '2.2.0.pre.RC1'
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,62 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ast (2.4.2)
5
+ byebug (11.1.3)
6
+ coderay (1.1.3)
7
+ drydock (0.6.9)
8
+ json (2.7.2)
9
+ language_server-protocol (3.17.0.3)
10
+ method_source (1.0.0)
11
+ parallel (1.24.0)
12
+ parser (3.3.0.5)
13
+ ast (~> 2.4.1)
14
+ racc
15
+ pry (0.14.2)
16
+ coderay (~> 1.1)
17
+ method_source (~> 1.0)
18
+ pry-doc (1.5.0)
19
+ pry (~> 0.11)
20
+ yard (~> 0.9.11)
21
+ racc (1.7.3)
22
+ rainbow (3.1.1)
23
+ regexp_parser (2.9.0)
24
+ rexml (3.2.6)
25
+ rubocop (1.62.1)
26
+ json (~> 2.3)
27
+ language_server-protocol (>= 3.17.0)
28
+ parallel (~> 1.10)
29
+ parser (>= 3.3.0.2)
30
+ rainbow (>= 2.2.2, < 4.0)
31
+ regexp_parser (>= 1.8, < 3.0)
32
+ rexml (>= 3.2.5, < 4.0)
33
+ rubocop-ast (>= 1.31.1, < 2.0)
34
+ ruby-progressbar (~> 1.7)
35
+ unicode-display_width (>= 2.4.0, < 3.0)
36
+ rubocop-ast (1.31.2)
37
+ parser (>= 3.3.0.4)
38
+ ruby-progressbar (1.13.0)
39
+ storable (0.10.pre.RC1)
40
+ sysinfo (0.9.0.pre.RC1)
41
+ drydock (< 1.0)
42
+ storable (= 0.10.pre.RC1)
43
+ tryouts (2.2.0.pre.RC1)
44
+ sysinfo (= 0.9.0.pre.RC1)
45
+ unicode-display_width (2.5.0)
46
+ yard (0.9.36)
47
+
48
+ PLATFORMS
49
+ arm64-darwin-22
50
+
51
+ DEPENDENCIES
52
+ byebug
53
+ pry
54
+ pry-doc
55
+ rubocop
56
+ tryouts (= 2.2.0.pre.RC1)
57
+
58
+ RUBY VERSION
59
+ ruby 3.2.0p0
60
+
61
+ BUNDLED WITH
62
+ 2.4.12
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2009-2024 delano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,54 +1,146 @@
1
- # Attic - v0.6-RC1 (2021-07-01)
1
+ # Attic - v1.0-RC1 (2024-04-01)
2
2
 
3
- A place to hide private instance variables in your Ruby objects.
3
+ A place to hide private instance variables in your Ruby objects: in the attic.
4
+
5
+ ## Description
6
+
7
+ Attic is a Ruby library that provides a place to hide private instance variables in your Ruby objects: in the attic.
8
+
9
+ Like, _why though_? Well sometimes you want to hide thing from the public interface of your object. You could use the `@@` prefix to place them at the class level but then you can't use them on a per-instance basis without creating a new data structure to store them. Attic does this for you in a transparent way using the Ruby singleton classes that are already there**.
10
+
11
+
12
+ ### Example
4
13
 
5
- ## Example
6
14
 
7
15
  ```ruby
8
16
  require 'attic'
9
17
 
18
+ # Extend a class with Attic
10
19
  class String
11
20
  extend Attic
21
+
22
+ # Add an instance variable to the attic. All instances
23
+ # of this class will have this variable available.
12
24
  attic :timestamp
13
25
  end
14
26
 
15
- a = "anything"
16
- a.timestamp = "1980-11-18"
27
+ # Instantiate a new String object
28
+ a = "A lovely example of a string"
29
+
30
+ # Set and get the timestamp
31
+ a.timestamp = "1990-11-18"
32
+ a.timestamp # => "1990-11-18"
33
+
34
+ # The timestamp is not visible in the public interface
17
35
  a.instance_variables # => []
18
- a.timestamp # 1980-11-18
19
36
 
37
+ # But it is available in the attic
20
38
  a.attic_variables # => [:timestamp]
21
39
 
40
+ # If you prefer getting your hands dirty, you can also
41
+ # interact with the attic at a lower level.
22
42
  a.attic_variable_set :tags, [:a, :b, :c]
23
- a.attic_variable_get :tags # [:a, :b, :c]
43
+ a.attic_variable_get :tags # => [:a, :b, :c]
24
44
 
45
+ # Looking at the attic again shows that the timestamp
46
+ # is still there too.
25
47
  a.attic_variables # => [:timestamp, :tags]
26
48
  ```
27
49
 
28
- ## Some objects have no metaclasses
29
50
 
30
- Symbol objects do not have metaclasses so instance variables are hidden in the object itself.
51
+ ### **Objects without singleton classes
31
52
 
53
+ Symbol, Integer, Float, TrueClass, FalseClass, NilClass, and Integer are all objects that do not have singleton classes. TrueClass, FalseClass, and NilClass are all singletons themselves. Integer is a singleton of Integer.
32
54
 
33
- ## Installation
55
+ These objects do not have metaclasses so the attic is hidden in the object itself.
56
+
57
+ ### Explore in irb
58
+
59
+ ```shell
60
+ $ irb -r attic
61
+ ```
62
+
63
+ ---
34
64
 
35
- Via Rubygems, one of:
65
+ ## Installation
36
66
 
37
67
  ```shell
38
68
  $ gem install attic
39
69
  ```
40
70
 
71
+ ```shell
72
+ $ bundle install attic
73
+ ```
74
+
41
75
  or via download:
42
- * attic-latest.tar.gz[http://github.com/delano/attic/tarball/latest]
43
- * attic-latest.zip[http://github.com/delano/attic/zipball/latest]
76
+ * [attic-latest.tar.gz](https://github.com/delano/attic/tarball/latest)
77
+ * [attic-latest.zip](https://github.com/delano/attic/zipball/latest)
78
+
79
+
80
+ ## Proofs
44
81
 
82
+ Tested the following code in IRB for Ruby 2.6.8 and 3.0.2:
45
83
 
46
- ## Credits
84
+ ```ruby
85
+ rquire 'pp'
86
+
87
+ test_values = [:sym, 1, 1.01, Symbol, Integer, Float, String, TrueClass, FalseClass, NilClass, '', true, false, nil]
47
88
 
48
- * `gems@solutious.com` (@delano)
89
+ results = test_values.map do |value|
90
+ { value: value,
91
+ class: value.class,
92
+ attic: [value.attic?, value.attic? && value.attic.object_id] }
93
+ end
94
+ ```
95
+
96
+ which produced the same results for both.
97
+
98
+ ```ruby
99
+ attic> RUBY_VERSION
100
+ => "3.2.0"
101
+
102
+ pp results
103
+ [
104
+ {:value=>:sym, :class=>Symbol, :attic=>[false, false]},
105
+ {:value=>1, :class=>Integer, :attic=>[false, false]},
106
+ {:value=>1.01, :class=>Float, :attic=>[false, false]},
107
+ {:value=>Symbol, :class=>Class, :attic=>[true, 564680]},
108
+ {:value=>Integer, :class=>Class, :attic=>[true, 564700]},
109
+ {:value=>Float, :class=>Class, :attic=>[true, 564720]},
110
+ {:value=>String, :class=>Class, :attic=>[true, 564740]},
111
+ {:value=>TrueClass, :class=>Class, :attic=>[true, 564760]},
112
+ {:value=>FalseClass, :class=>Class, :attic=>[true, 564780]},
113
+ {:value=>NilClass, :class=>Class, :attic=>[true, 564800]},
114
+ {:value=>"", :class=>String, :attic=>[true, 602880]}
115
+ {:value=>true, :class=>TrueClass, :attic=>[true, 13840]},
116
+ {:value=>false, :class=>FalseClass, :attic=>[true, 13860]},
117
+ {:value=>nil, :class=>NilClass, :attic=>[true, 40]}
118
+ ]
119
+ ```
120
+ ```ruby
121
+ attic> RUBY_VERSION
122
+ => "2.6.8"
123
+
124
+ pp results
125
+ [
126
+ {:value=>:sym, :class=>Symbol, :attic=>[false, false]},
127
+ {:value=>1, :class=>Integer, :attic=>[false, false]},
128
+ {:value=>1.01, :class=>Float, :attic=>[false, false]},
129
+ {:value=>Symbol, :class=>Class, :attic=>[true, 2844115920]},
130
+ {:value=>Integer, :class=>Class, :attic=>[true, 2844089400]},
131
+ {:value=>Float, :class=>Class, :attic=>[true, 2844087700]},
132
+ {:value=>String, :class=>Class, :attic=>[true, 2844122580]},
133
+ {:value=>TrueClass, :class=>Class, :attic=>[true, 2844136260]},
134
+ {:value=>FalseClass, :class=>Class, :attic=>[true, 2844136000]},
135
+ {:value=>NilClass, :class=>Class, :attic=>[true, 2844139060]},
136
+ {:value=>"", :class=>String, :attic=>[true, 2845261220]},
137
+ {:value=>true, :class=>TrueClass, :attic=>[true, 2844136280]},
138
+ {:value=>false, :class=>FalseClass, :attic=>[true, 2844136020]},
139
+ {:value=>nil, :class=>NilClass, :attic=>[true, 2844139080]}
140
+ ]
141
+ ```
49
142
 
50
143
 
51
144
  ## License
52
145
 
53
146
  MIT
54
-
data/attic.gemspec CHANGED
@@ -1,20 +1,23 @@
1
- @spec = Gem::Specification.new do |s|
2
- s.name = "attic"
3
- s.version = "0.6-RC1"
4
- s.summary = "When in doubt, store it in the attic"
5
- s.description = "Attic: a place to hide metadata about the class or variable itself (e.g. SHA hash summaries)."
6
- s.author = "Delano Mandelbaum"
7
- s.email = "gems@solutious.com"
8
- s.homepage = "http://github.com/delano/attic"
9
- s.executables = %w[]
10
- s.require_paths = %w[lib]
11
- s.extra_rdoc_files = %w[README.md]
12
- s.licenses = ["MIT"] # https://spdx.org/licenses/MIT-Modern-Variant.html
13
- s.rdoc_options = ["--line-numbers", "--title", s.summary, "--main", "README.md"]
14
- s.files = %w(
15
- README.md
16
- Rakefile
17
- attic.gemspec
18
- lib/attic.rb
19
- )
1
+ Gem::Specification.new do |s|
2
+ s.name = "attic"
3
+ s.version = "1.0.0-RC2"
4
+ s.summary = "When in doubt, store it in the attic"
5
+ s.description = "Attic: a place to hide metadata about the class or variable itself (e.g. SHA hash summaries)."
6
+ s.authors = ["Delano Mandelbaum"]
7
+ s.email = "gems@solutious.com"
8
+ s.homepage = "https://github.com/delano/attic"
9
+ s.license = "MIT"
10
+
11
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
12
+ s.bindir = "exe"
13
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
14
+ s.require_paths = ["lib"]
15
+
16
+ s.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
17
+
18
+ s.add_dependency "rake", ">= 13.0.6"
19
+
20
+ # Add development dependencies
21
+ s.add_development_dependency "rspec", "~> 3.0"
22
+ s.add_development_dependency "rubocop", "~> 1.0"
20
23
  end
@@ -0,0 +1,66 @@
1
+ #
2
+ # = Attic::ClassMethods
3
+ #
4
+ module Attic
5
+ # Adds a few methods for accessing the metaclass of an
6
+ # object. We do this with great caution since the Object
7
+ # class is as global as it gets in Ruby.
8
+ module ClassMethods
9
+ # A list of all the attic variables defined for this class.
10
+ attr_reader :attic_variables
11
+
12
+ # A quick way to check if the current object already has a
13
+ # dedicated singleton class. We want to know this because
14
+ # this is where our attic variables will be stored.
15
+ def attic?
16
+ return false if NoSingletonError.member? self
17
+
18
+ # NOTE: Calling this on an object for the first time lazily
19
+ # creates a singleton class for itself. Another way of doing
20
+ # the same thing is to attempt defining a singleton method
21
+ # for the object. In either case, objects that cannot have
22
+ # cannot have a dedicated singleton class (e.g. nil, true,
23
+ # false) will raise a TypeError. We rescue this and add the
24
+ # object to the NoSingletonError list so we don't keep
25
+ # trying to access its singleton class over and over.
26
+ !singleton_class.nil?
27
+
28
+ rescue TypeError
29
+ # Remember for next time.
30
+ NoSingletonError.add_member self
31
+ false
32
+ end
33
+
34
+ def attic(name=nil)
35
+ return singleton_class if name.nil?
36
+
37
+ name = name.normalize
38
+
39
+ self.attic_variables << name unless attic_variable? name
40
+
41
+ safe_name = "@_attic_#{name}"
42
+ instance_variable_set(safe_name, name)
43
+ end
44
+
45
+ def attic_variable?(name)
46
+ attic_variables.include? name.normalize
47
+ end
48
+
49
+ def attic_variable_set(name, val)
50
+ unless attic_variable? name
51
+ attic_variables << name.normalize
52
+ end
53
+ attic.instance_variable_set("@_attic_#{name}", val)
54
+ end
55
+
56
+ def attic_variable_get(name)
57
+ instance_variable_get("@_attic_#{name}")
58
+ end
59
+
60
+ protected
61
+
62
+ def normalize(name)
63
+ name.to_s.gsub(/\@[\?\!\=]$/, '_').to_sym
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,70 @@
1
+
2
+ class NoMetaClass < RuntimeError
3
+ end
4
+
5
+ # = Object
6
+ #
7
+ # These methods are copied directly from _why's metaid.rb.
8
+ # See: http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
9
+ class Object
10
+
11
+ unless defined?(::Object::NOMETACLASS)
12
+ # An Array of classes which do not have metaclasses.
13
+ NOMETACLASS = [Symbol, Integer].freeze
14
+ end
15
+
16
+ def nometaclass?
17
+ NOMETACLASS.member?(self)
18
+ end
19
+
20
+ def metaclass?
21
+ !NOMETACLASS.member?(self.class)
22
+ end
23
+
24
+ # A convenient method for getting the metaclass of the current object.
25
+ # i.e.
26
+ #
27
+ # class << self; self; end;
28
+ #
29
+ # NOTE: Some Ruby class do not have meta classes (see: NOMETACLASS).
30
+ # For these classes, this method returns the class itself. That means
31
+ # the instance variables will stored in the class itself.
32
+ def metaclass
33
+ if !self.metaclass?
34
+ raise NoMetaClass, self
35
+ else
36
+ class << self; self; end;
37
+ end
38
+ end
39
+
40
+ # Execute a block +&blk+ within the metaclass of the current object.
41
+ def meta_eval &blk; metaclass.instance_eval &blk; end
42
+
43
+ # Add an instance method called +name+ to metaclass for the current object.
44
+ # This is useful because it will be available as a singleton method
45
+ # to all subclasses too.
46
+ def meta_def name, &blk
47
+ meta_eval { define_method name, &blk }
48
+ end
49
+
50
+ # Add a class method called +name+ for the current object's class. This
51
+ # isn't so special but it maintains consistency with meta_def.
52
+ def class_def name, &blk
53
+ class_eval { define_method name, &blk }
54
+ end
55
+
56
+
57
+ # A convenient method for getting the metaclass of the metaclass
58
+ # i.e.
59
+ #
60
+ # self.metaclass.metaclass
61
+ #
62
+ def metametaclass; self.metaclass.metaclass; end
63
+
64
+ def metameta_eval &blk; metametaclass.instance_eval &blk; end
65
+
66
+ def metameta_def name, &blk
67
+ metameta_eval { define_method name, &blk }
68
+ end
69
+
70
+ end
@@ -0,0 +1,52 @@
1
+
2
+ # Attic: A special place to store instance variables.
3
+
4
+ class NoMetaClass < RuntimeError
5
+ end
6
+
7
+ # = NoSingletonError
8
+ #
9
+ # This error is raised when an attempt is made to access the
10
+ # attic of an object which does not have a singleton class.
11
+ #
12
+ # This is a TypeError because it is not an exceptional
13
+ # condition. It is simply a condition that is not supported
14
+ # by the Attic module.
15
+ #
16
+ # == Usage
17
+ #
18
+ # require 'attic'
19
+ # class MyClass
20
+ # include Attic
21
+ # attic :name, :age
22
+ # end
23
+ #
24
+ class NoSingletonError < TypeError
25
+ unless defined?(MEMBERS)
26
+ # A Set of classes which do not have singleton classes
27
+ # (i.e. meta classes). This is used to prevent an exception
28
+ # the first time an attic is accessed. It's populated
29
+ # dynamically at start time by simply checking whether
30
+ # the object has a singleton. This only needs to be
31
+ # done once per class.
32
+ #
33
+ # We use a set here to avoid having to deal with duplicate
34
+ # values. Realistically there are only a few classes that
35
+ # do not have singleton classes. We could hard code them
36
+ # here which is not lost on us.
37
+ #
38
+ MEMBERS = Set.new
39
+ end
40
+
41
+ # Check if the given object is a member of the NoSingleton
42
+ # members list. This checks for the object itself and all
43
+ # of its ancestors. See the docs for `Enumerable#===` for
44
+ # more details.
45
+ def self.member?(obj)
46
+ MEMBERS === obj
47
+ end
48
+
49
+ def self.add_member(obj)
50
+ MEMBERS.merge [self]
51
+ end
52
+ end
@@ -0,0 +1,36 @@
1
+ #
2
+
3
+ # = Attic::InstanceMethods
4
+ #
5
+ module Attic
6
+ # Adds a few methods for object instances to access the
7
+ # attic variables of their class.
8
+ module InstanceMethods
9
+
10
+ def attic
11
+ raise NoSingleton, self, caller unless attic?
12
+
13
+ singleton_class
14
+
15
+ rescue TypeError
16
+ NoSingleton.add_member self
17
+ end
18
+
19
+ def attic_variables
20
+ self.class.attic_variables
21
+ end
22
+
23
+ def attic_variable?(name)
24
+ self.class.attic_variable? name
25
+ end
26
+
27
+ def attic_variable_set(name, val)
28
+ attic_variables << name unless attic_variable? name
29
+ attic.instance_variable_set("@___attic_#{name}", val)
30
+ end
31
+
32
+ def attic_variable_get(name)
33
+ attic.instance_variable_get("@___attic_#{name}")
34
+ end
35
+ end
36
+ end
data/lib/attic.rb CHANGED
@@ -1,157 +1,174 @@
1
1
 
2
- class NoMetaClass < RuntimeError
3
- end
4
-
5
- # = Object
6
- #
7
- # These methods are copied directly from _why's metaid.rb.
8
- # See: http://whytheluckystiff.net/articles/seeingMetaclassesClearly.html
9
- class Object
2
+ # Attic: A special place to store instance variables.
10
3
 
11
- unless defined?(::Object::NOMETACLASS)
12
- # An Array of classes which do not have metaclasses.
13
- NOMETACLASS = [Symbol, Integer]
14
- end
15
-
16
- def nometaclass?
17
- NOMETACLASS.member?(self)
18
- end
4
+ require_relative "attic/class_methods"
5
+ require_relative "attic/errors"
6
+ require_relative "attic/instance_methods"
19
7
 
20
- def metaclass?
21
- !NOMETACLASS.member?(self.class)
22
- end
8
+ # = Attic
9
+ #
10
+ # == Usage:
11
+ #
12
+ # require 'attic'
13
+ # class MyClass
14
+ # include Attic
15
+ # attic :name, :age
16
+ # end
17
+ #
18
+ # obj = MyClass.new
19
+ # obj.attic.nickname = 'My Classic Automobile'
20
+ # obj.attic.secret_age = 27
21
+ #
22
+ # obj.nickname #=> 'My Classic Automobile'
23
+ # obj.secret_age #=> 27
24
+ # obj.to_h #=> {}
25
+ #
26
+ # OR
27
+ #
28
+ # require 'attic'
29
+ # Attic.construct MyClass, :nickname, :secret_age
30
+ #
31
+ #
32
+ # == Description:
33
+ #
34
+ # Attic is a module that allows you to store instance variables
35
+ # in a dedicated singleton class. This is useful for storing
36
+ # instance variables that you don't want to be available to
37
+ # the public interface of your class. e.g. you want to store
38
+ # a value for the running instance but want to prevent it
39
+ # from being serialized.
40
+ #
41
+ # == Why?
42
+ #
43
+ # == Important Notes:
44
+ #
45
+ # Some objects just straight up are not capable of contructing
46
+ # an attic. `Symbols`, `Integers`, and `Floats` specifically do not
47
+ # have a dedicated singleton classes. These are what ruby
48
+ # internals refer to as "immediate values". They're special
49
+ # in that they are not objects in the traditional sense.
50
+ # They're just values (they're not even instances of a
51
+ # class 😮‍💨).
52
+ #
53
+ # When you call attic on an immediate value you get an error.
54
+ #
55
+ # :sym.attic #=> raises NoSingleton error
56
+ # 1.attic #=> Ditto
57
+ # 1.0.1.attic #=> Ditto again
58
+ #
59
+ #
60
+ # The other objects that do not have singleton classes are
61
+ # `true`, `false`, and `nil`. Calling attic on these don't
62
+ # raise an error but they simply return their class. This
63
+ # is because they are all instances of their same singleton
64
+ # class.
65
+ #
66
+ # true.attic #=> TrueClass
67
+ # false.attic #=> FalseClass
68
+ # nil.attic #=> NilClass
69
+ #
70
+ # Note: this means that every instance of nil
71
+ # returns the exact same singleton class. This is different
72
+ # from the behaviour of all other objects.
73
+ #
74
+ # nil.attic.object_id #=> 800
75
+ # nil.attic.object_id #=> 800
76
+ #
77
+ #
78
+ # NilClass, TrueClass, and FalseClass on the otherhand each
79
+ # have their own singleton class. Calling attic on these
80
+ # returns the singleton class for each of them. But again
81
+ # a singleton class for each of them but again they all
82
+ #
83
+ # TrueClass.attic #=> #<Class:TrueClass>
84
+ # FalseClass.attic #=> #<Class:FalseClass>
85
+ # NilClass.attic #=> #<Class:NilClass>
86
+ #
87
+ #
88
+ # For comparison, here's what happens with a String (each
89
+ # time attic is called on a new string you get a new
90
+ # singleton)
91
+ #
92
+ # "".attic #=> #<Class:#<String:0x0001234>>
93
+ # "".attic #=> #<Class:#<String:0x0005678>>
94
+ # "".attic.object_id #=> 1234
95
+ # "".attic.object_id #=> 5678
96
+ #
97
+ # nil.attic #=> NilClass
98
+ # nil.attic #=> NilClass
99
+ # nil.attic.object_id #=> 800
100
+ # nil.attic.object_id #=> 800
101
+ #
102
+ module Attic
103
+ VERSION = '0.9.0-RC1'.freeze unless defined?(VERSION)
23
104
 
24
- # A convenient method for getting the metaclass of the current object.
25
- # i.e.
105
+ # A convenince method at the class level for including
106
+ # ConstructMethods in the given object specifically.
26
107
  #
27
- # class << self; self; end;
108
+ # e.g.
28
109
  #
29
- # NOTE: Some Ruby class do not have meta classes (see: NOMETACLASS).
30
- # For these classes, this method returns the class itself. That means
31
- # the instance variables will be stored in the class itself.
32
- def metaclass
33
- if self.metaclass?
34
- class << self
35
- self
36
- end
37
- else
38
- self
39
- end
40
- end
41
-
42
- def metaclassfly
43
- location = self.class
44
- attr_name = "@@_attic_#{self.object_id}"
45
- unless location.class_variable_defined? attr_name
46
- location.class_variable_set attr_name, Class.new
47
- end
48
- location.class_variable_get attr_name
49
- end
50
-
51
- # Execute a block +&blk+ within the metaclass of the current object.
52
- def meta_eval &blk
53
- metaclass.instance_eval blk
54
- end
55
-
56
- # Add an instance method called +name+ to metaclass for the current object.
57
- # This is useful because it will be available as a singleton method
58
- # to all subclasses too.
59
- def meta_def name, &blk
60
- meta_eval { define_method name, &blk }
61
- end
62
-
63
- # Add a class method called +name+ for the current object's class. This
64
- # isn't so special but it maintains consistency with meta_def.
65
- def class_def name, &blk
66
- class_eval { define_method name, &blk }
67
- end
68
-
69
-
70
- # A convenient method for getting the metaclass of the metaclass
71
- # i.e.
110
+ # Add Attic support to all objects available now and
111
+ # in the future:
112
+ #
113
+ # Attic.construct(Object)
72
114
  #
73
- # self.metaclass.metaclass
115
+ # which is equivalent to:
74
116
  #
75
- def metametaclass
76
- self.metaclass.metaclass
117
+ # class Object; include Attic::ClassMethods; end
118
+ #
119
+ def self.construct(obj)
120
+ obj.include Attic::ClassMethods
77
121
  end
78
122
 
79
- def metameta_eval &blk
80
- metametaclass.instance_eval blk
123
+ # Friendly exception to say we're not to be included
124
+ #
125
+ def self.included(obj)
126
+ raise RuntimeError, "Did you mean to `extend Attic`` in #{obj}"
81
127
  end
82
128
 
83
- def metameta_def name, &blk
84
- metameta_eval { define_method name, &blk }
85
- end
86
- end
129
+ def self.extended(obj)
130
+ # If the class has already been extended, we don't need
131
+ # to add the class methods again.
132
+ return if obj.ancestors.member? self
87
133
 
134
+ # Add the instance methods for accessing attic variables
135
+ obj.send :include, Attic::InstanceMethods
88
136
 
137
+ # If the object doesn't have a dedicated singleton class
138
+ # an exception will be raised so it can be caught and
139
+ # handled appropriately.
140
+ obj.attic.instance_variable_defined?("@attic_variables")
89
141
 
90
- # = Attic
91
- #
92
- # A place to store instance variables.
93
- #
94
- module Attic
95
- VERSION = '0.6-RC1' unless defined?(VERSION)
142
+ obj.attic.instance_variable_set("@attic_variables", [])
96
143
 
97
- module InstanceMethods
98
- def attic_variables
99
- self.class.attic_variables
100
- end
101
- alias_method :attic_vars, :attic_variables
102
- def attic_variable? n
103
- self.class.attic_variable? n
144
+ def obj.inherited(klass)
145
+ super
146
+ attic_vars = self.attic_variables.clone
147
+ klass.attic.instance_variable_set("@attic_variables", attic_vars)
104
148
  end
105
- def attic_variable_set(n,v)
106
- attic_variables << n unless attic_variable? n
107
- # binding.pry
108
- metaclassfly.instance_variable_set("@___attic_#{n}", v)
109
- end
110
- def attic_variable_get(n)
111
- metaclassfly.instance_variable_get("@___attic_#{n}")
112
- end
113
- def get_binding
114
- binding
115
- end
116
- end
117
-
118
- def self.included(o)
119
- raise "You probably meant to 'extend Attic' in #{o}"
120
- end
121
-
122
- def self.extended(o)
123
- # This class has already been extended.
124
- return if o.ancestors.member? Attic::InstanceMethods
125
-
126
- # Add the instance methods for accessing attic variables
127
- o.send :include, Attic::InstanceMethods
128
-
129
- o.metaclass.instance_variable_set("@attic_variables", [])
130
- o.class_eval do
131
- def self.inherited(o2)
132
- attic_vars = self.attic_variables.clone
133
- o2.metaclass.instance_variable_set("@attic_variables", attic_vars)
149
+ if method_defined? :instance_variables
150
+ instance_variables_orig = instance_method(:instance_variables)
151
+ define_method :instance_variables do
152
+ ret = _instance_variables_orig.bind(self).call.clone
153
+ ret.reject! { |v| v.to_s =~ /^@___?attic/ } # match 2 or 3 underscores
154
+ ret
134
155
  end
135
- if method_defined? :instance_variables
136
- old_instance_variables = instance_method(:instance_variables)
137
- define_method :instance_variables do
138
- ret = old_instance_variables.bind(self).call.clone
139
- ret.reject! { |v| v.to_s =~ /^@___?attic/ } # match 2 or 3 underscores
140
- ret
141
- end
142
- define_method :all_instance_variables do
143
- old_instance_variables.bind(self).call
144
- end
156
+ define_method :all_instance_variables do
157
+ _instance_variables_orig.bind(self).call
145
158
  end
146
159
  end
160
+
161
+ rescue TypeError => e
162
+ raise NoSingleton, obj, caller
147
163
  end
148
164
 
149
165
  # A class method for defining variables to store in the attic.
150
- # * +junk+ is a list of variables names. Accessor methods are
166
+ # * +names+ is a list of variables names. Accessor methods are
151
167
  # created for each variable name in the list.
152
168
  #
153
- # Returns the list of attic variable names or if not junk was
154
- # given, returns the metaclass.
169
+ # Returns an Array of all attic variables for the current
170
+ # class unless no arguments are given in which case it
171
+ # returns its singleton.
155
172
  #
156
173
  # e.g.
157
174
  #
@@ -162,24 +179,28 @@ module Attic
162
179
  # * <tt>String#timestamp</tt> for getting the value
163
180
  # * <tt>String#timestamp</tt> for setting the value
164
181
  #
165
- def attic *junk
166
- return metaclass if junk.empty?
167
- junk.each do |name|
182
+ def attic(*names)
183
+ return singleton_class if names.empty?
184
+
185
+ names.each do |name|
168
186
  next if attic_variable? name
187
+
169
188
  self.attic_variables << name
170
189
 
171
- unless method_defined? name
190
+ unless method_defined?(name)
172
191
  define_method(name) do
173
192
  attic_variable_get name
174
193
  end
175
194
  end
176
- unless method_defined? "#{name}="
195
+
196
+ unless method_defined?("#{name}=")
177
197
  define_method("#{name}=") do |val|
178
198
  attic_variable_set name, val
179
199
  end
180
200
  end
181
201
  end
182
- attic_vars
202
+
203
+ attic_variables # only after defining new attic vars
183
204
  end
184
205
 
185
206
  # Returns an Array of attic variables for the current class.
@@ -190,17 +211,27 @@ module Attic
190
211
  # String.attic_variables # => [:timestamp]
191
212
  #
192
213
  def attic_variables
193
- a = self.metaclass.instance_variable_get("@attic_variables")
194
- a ||= self.metaclass.instance_variable_set("@attic_variables", [])
214
+ a = attic.instance_variable_get('@attic_variables')
215
+ a ||= attic.instance_variable_set('@attic_variables', [])
195
216
  a
196
217
  end
197
- alias_method :attic_vars, :attic_variables
198
218
 
199
- def attic_variable?(n)
200
- attic_variables.member? n
219
+ def attic_variable?(name)
220
+ attic_variables.member? name
201
221
  end
202
-
203
222
  end
204
223
 
205
224
  # - Module#instance_method returns an UnboundMethod
206
225
  # - http://ruby-doc.org/core/classes/Module.html#M001659
226
+
227
+ # Add some candy when we're in irb
228
+ if defined?(IRB)
229
+ require 'irb/completion'
230
+ IRB.conf[:PROMPT][:ATTIC] = {
231
+ PROMPT_I: "attic> ",
232
+ PROMPT_S: "attic%l> ",
233
+ PROMPT_C: "attic* ",
234
+ RETURN: "=> %s\n\n"
235
+ }
236
+ IRB.conf[:PROMPT_MODE] = :ATTIC
237
+ end
@@ -0,0 +1,65 @@
1
+ require_relative '../lib/attic'
2
+
3
+ #
4
+ # Tests for the Object mixins that Attic relies on.
5
+ #
6
+
7
+ ## Has a valid NoMetaClass exception class
8
+ NoMetaClass < RuntimeError
9
+ #=> true
10
+
11
+ ## Has a pre-populated array of built-ins without a metaclass
12
+ begin
13
+ Object::NOMETACLASS
14
+ rescue NameError => e
15
+ e.class
16
+ end
17
+ #=> NameError
18
+
19
+ ## Has Object#metaclass method
20
+ Object.new.respond_to? :metaclass
21
+ #=> false
22
+
23
+ ## Has Object#singleton_class method
24
+ Object.new.respond_to? :singleton_class
25
+ #=> true
26
+
27
+ ## Object#singleton_class is a class
28
+ Object.new.singleton_class.class
29
+ #=> Class
30
+
31
+ ## Object#singleton_class is a class
32
+ Object.new.singleton_class.object_id.class
33
+ #=> Integer
34
+
35
+ ## Object#singleton_class is a class
36
+ a = Object.new
37
+ b = Object.new
38
+ a.singleton_class.object_id == b.singleton_class.object_id
39
+ #=> false
40
+
41
+ ## Object#singleton_class is an Object class
42
+ Object.new.singleton_class.superclass
43
+ #=> Object
44
+
45
+ ## Object#singleton_class is equivalent to `class << self; self; end;`
46
+ a = Object.new
47
+ a.singleton_class == (class << a; self; end)
48
+ #=> true
49
+
50
+ ## Integer doesn't have a singleton_class
51
+ Integer.singleton_class?
52
+ #=> false
53
+
54
+ ## Symbol doesn't have a singleton_class
55
+ Symbol.singleton_class?
56
+ #=> false
57
+
58
+ ## Object has a singleton_class
59
+ Object.singleton_class?
60
+ #=> false
61
+
62
+ ## Object#singleton_class is equivalent to Object#singleton_class
63
+ a = Object.new
64
+ a.singleton_class == a.singleton_class
65
+ #=> true
@@ -0,0 +1,51 @@
1
+ require_relative '../lib/attic'
2
+
3
+ #
4
+ # Tests for the Attic module
5
+ #
6
+
7
+ ## A class we define can extend Attic
8
+ class ::ExampleClass
9
+ extend Attic
10
+ def kind() :unlikely_value end
11
+ end
12
+ ExampleClass.methods.member?(:attic)
13
+ #=> true
14
+
15
+ ## Trying to include Attic raises an exception
16
+ begin
17
+ class ::ExampleClass
18
+ include Attic
19
+ end
20
+ rescue => e
21
+ e.class
22
+ end
23
+ #=> RuntimeError
24
+
25
+ ## Can define attic variables at class level
26
+ ExampleClass.attic :size
27
+ w = ExampleClass.new
28
+ w.respond_to? :size
29
+ #=> true
30
+
31
+ ## Accessing attic vars at the instance level fails
32
+ begin
33
+ w = ExampleClass.new
34
+ w.attic :size, 2
35
+ rescue => e
36
+ e.class
37
+ end
38
+ #=> NoMethodError
39
+
40
+ ## Can access attic vars the long way though
41
+ w = ExampleClass.new
42
+ w.attic_variable_set :size, 2
43
+ w.attic_variable_get :size
44
+ #=> 2
45
+
46
+ ## Won't clobber an existing method with the same name
47
+ ## NOTE: But also won't tell you it didn't define the method
48
+ ExampleClass.attic :kind
49
+ a = ExampleClass.new
50
+ a.kind
51
+ #=> :unlikely_value
@@ -0,0 +1,29 @@
1
+ require 'attic'
2
+ class ::Worker
3
+ extend Attic
4
+ attic :size
5
+ end
6
+
7
+
8
+ ## save an instance variable the long way
9
+ w = Worker.new
10
+ w.metametaclass.instance_variable_set '@mattress', 'S&F'
11
+ w.metametaclass.instance_variable_get '@mattress'
12
+ #=> 'S&F'
13
+
14
+ ## save an instance variable the short way
15
+ w = Worker.new
16
+ w.size = :california_king
17
+ w.size
18
+ #=> :california_king
19
+
20
+ ## new instances don't cross streams
21
+ w = Worker.new
22
+ w.size
23
+ #=> nil
24
+
25
+ ## instance variables are hidden
26
+ w = Worker.new
27
+ w.metametaclass.instance_variable_set '@mattress', 'S&F'
28
+ w.instance_variables
29
+ ## []
@@ -0,0 +1,32 @@
1
+ require 'attic'
2
+
3
+ ## String can extend Attic
4
+ String.extend Attic
5
+ String.respond_to? :attic
6
+ #=> true
7
+
8
+ ## save an instance variable the long way
9
+ s = ""
10
+ s.metametaclass.instance_variable_set '@mattress', 'S&F'
11
+ s.metametaclass.instance_variable_get '@mattress'
12
+ #=> 'S&F'
13
+
14
+ ## can create attributes
15
+ String.attic :goodies
16
+ #=> [:goodies]
17
+
18
+ ## save an instance variable the short way
19
+ s = ""
20
+ s.goodies = :california_king
21
+ p s.instance_variables
22
+ p s.attic_vars
23
+ s.goodies
24
+ #=> :california_king
25
+
26
+ ## String instances don't cross streams
27
+ String.extend Attic
28
+ String.attic :name
29
+ a = "any"
30
+ a.name = :roger
31
+ a.name == "".name
32
+ #=> false
@@ -0,0 +1,68 @@
1
+ require_relative "../lib/attic"
2
+
3
+ Attic.construct Symbol #, :name
4
+
5
+ ## has list of no metaclass classes
6
+ NoSingletonError::MEMBERS
7
+ #=> [Symbol, Integer]
8
+
9
+ # ## Symbol metaclass does not raise an exception
10
+ # begin
11
+ # :any.attic.class
12
+ # rescue NoSingletonError
13
+ # :failed
14
+ # end
15
+ # #=> :failed
16
+
17
+ ## Accessing Symbol metaclass raises an exception
18
+ begin
19
+ :any.attic.class
20
+ rescue NoSingletonError
21
+ :failed
22
+ end
23
+ #=> :failed
24
+
25
+ ## Symbol instances don't cross streams
26
+ Symbol.extend Attic
27
+ Symbol.attic :name
28
+ a, b = :symbol1, :symbol2
29
+ a.name = :roger
30
+ [a.name, b.name]
31
+ #=> [:roger, nil]
32
+
33
+ ## attic? method exists
34
+ Symbol.extend Attic
35
+ :any.respond_to? :attic?
36
+ #=> true
37
+
38
+ ## attic? method is false for a Symbol", false do
39
+ :any.attic?
40
+ #=> false
41
+
42
+ ## A Symbol's attic vars appear in `all_instance_variables` do
43
+ Symbol.extend Attic
44
+ Symbol.attic :_name
45
+ a, b = :symbol1, :symbol2
46
+ a._name = :roger
47
+ a.all_instance_variables
48
+ #=> [:@___attic_name]
49
+
50
+ ## An Integer's attic vars appear in `all_instance_variables` do
51
+ Integer.extend Attic
52
+ Integer.attic :_name
53
+ a, b = 1, 2
54
+ a._name = :roger
55
+ a.all_instance_variables
56
+ #=> [:@___attic_name]
57
+
58
+ ## A Symbol's attic vars do not appear in `instance_variables` do
59
+ Symbol.extend Attic
60
+ Symbol.attic :name
61
+ a, b = :symbol1, :symbol2
62
+ a.name = :roger
63
+ a.instance_variables
64
+ #=> []
65
+
66
+ ## knows attic variables, [:name] do
67
+ Symbol.attic_variables
68
+ #=> [:name]
@@ -0,0 +1,17 @@
1
+ require 'attic'
2
+
3
+ class ::Worker
4
+ extend Attic
5
+ end
6
+
7
+ ## can set value", 100 do
8
+ a = Worker.new
9
+ a.attic_variable_set :space, 100
10
+ a.attic_variable_get :space
11
+ #=> 100
12
+
13
+ ## doesn't create accessor methods", false do
14
+ a = Worker.new
15
+ a.attic_variable_set :space, 100
16
+ a.respond_to? :space
17
+ #=> false
metadata CHANGED
@@ -1,53 +1,104 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.pre.RC1
4
+ version: 1.0.0.pre.RC2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Delano Mandelbaum
8
- autorequire:
9
- bindir: bin
8
+ autorequire:
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-22 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 13.0.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 13.0.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
13
55
  description: 'Attic: a place to hide metadata about the class or variable itself (e.g.
14
56
  SHA hash summaries).'
15
57
  email: gems@solutious.com
16
58
  executables: []
17
59
  extensions: []
18
- extra_rdoc_files:
19
- - README.md
60
+ extra_rdoc_files: []
20
61
  files:
62
+ - ".gitignore"
63
+ - ".rubocop.yml"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - LICENSE.txt
21
67
  - README.md
22
68
  - Rakefile
23
69
  - attic.gemspec
24
70
  - lib/attic.rb
25
- homepage: http://github.com/delano/attic
71
+ - lib/attic/class_methods.rb
72
+ - lib/attic/core_ext.rb
73
+ - lib/attic/errors.rb
74
+ - lib/attic/instance_methods.rb
75
+ - try/01_mixins_tryouts.rb
76
+ - try/10_attic_tryouts.rb
77
+ - try/20_accessing_tryouts.rb
78
+ - try/25_string_tryouts.rb
79
+ - try/30_nometaclass_tryouts.rb
80
+ - try/40_explicit_accessor_tryouts.rb
81
+ homepage: https://github.com/delano/attic
26
82
  licenses:
27
83
  - MIT
28
84
  metadata: {}
29
- post_install_message:
30
- rdoc_options:
31
- - "--line-numbers"
32
- - "--title"
33
- - When in doubt, store it in the attic
34
- - "--main"
35
- - README.md
85
+ post_install_message:
86
+ rdoc_options: []
36
87
  require_paths:
37
88
  - lib
38
89
  required_ruby_version: !ruby/object:Gem::Requirement
39
90
  requirements:
40
91
  - - ">="
41
92
  - !ruby/object:Gem::Version
42
- version: '0'
93
+ version: 2.6.0
43
94
  required_rubygems_version: !ruby/object:Gem::Requirement
44
95
  requirements:
45
96
  - - ">"
46
97
  - !ruby/object:Gem::Version
47
98
  version: 1.3.1
48
99
  requirements: []
49
- rubygems_version: 3.2.21
50
- signing_key:
100
+ rubygems_version: 3.4.12
101
+ signing_key:
51
102
  specification_version: 4
52
103
  summary: When in doubt, store it in the attic
53
104
  test_files: []