facets 4.0.0 → 4.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af1581fdee797dd22551c3257c73b76e2d0e5ee659ecb60690eee877672a5a00
4
- data.tar.gz: 8693eedfa8dc78cfa8c47964cac6f2834996337dc00bbf0e9672c2b664bc2cea
3
+ metadata.gz: db8b790b2c8e0551d30a4020f9e9a206306ef7b57dc3bb73cf75ab8bfe192b52
4
+ data.tar.gz: 38d282a641961639027ed77a9042de9f837d55f6d4c37935e502ed71c1b22431
5
5
  SHA512:
6
- metadata.gz: e4a41b846c6bf81d6103ec0fcac3f8b729f5ab324676ab7f2b25a4126bfe878ec36a7113d58b4a7c32c5d1748cd4a1a9ae4ebba80fa82e7226c83bc7d7220af1
7
- data.tar.gz: 5c32bd85bdeb4336d5774689023e73d5c7e74ee86eff5003e5ef4da1644f10c6de5c897b6a0f268f09c7976055b28acaba10cf420b546380ae21c4d73508da86
6
+ metadata.gz: 7e85bd3715fbf1a7c333aefbf9b9db67afe2bd7dfbfe1119a6c2af640e6f5b189518b5c59b47d36effca090d523eaf31bed497028d000691f3c5b26f15012265
7
+ data.tar.gz: d32670ff9724cdd492b10092970ef9cfb4716530c2aa3f6b4906846ca0e8dae45dc04336c30d78f66da41c99e00ce9cc9a23df833dd07cf66c515891e8527945
data/CONTRIBUTING.md CHANGED
@@ -104,8 +104,8 @@ explanation if needed, including *when* and *why* the method could be useful.
104
104
 
105
105
  The test suite uses four tools:
106
106
 
107
- * **RubyTest** provides the test runner interface; `rubytest-cli` supplies the
108
- `ruby-test` command used by the Rake tasks.
107
+ * **RubyTest** provides the test runner interface and the `rubytest` command
108
+ used by the Rake tasks.
109
109
  * **Lemon** defines the `test_case`, `method`, and `test` structure of the unit
110
110
  tests.
111
111
  * **AE** provides assertions such as `.assert` and `expect`, used in both the
@@ -130,6 +130,12 @@ TESTS=test/core/array/test_average.rb bundle exec rake test
130
130
  The Rakefile also provides `test:core`, `test:standard`, `test:rails` and the
131
131
  corresponding `qed:*` tasks for each library area.
132
132
 
133
+ Tests and demos of deprecated methods call them on purpose, so both suites hide
134
+ those deprecation warnings and print a count at the end instead
135
+ (`etc/quiet_deprecations.rb`). A deprecation warning raised from inside `lib/`
136
+ still shows, because it means Facets calls a deprecated method itself. To see
137
+ every warning, set `FACETS_DEPRECATIONS=show`.
138
+
133
139
  * A core method in `lib/core/facets/{class}/{method}.rb` normally has a Lemon
134
140
  test in `test/core/{class}/test_{method}.rb` and a QED demo in
135
141
  `demo/core/{class}/{method}.md`. Standard and rails libraries use
data/HISTORY.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Facets Release History
2
2
 
3
+ ## 4.0.1 / 2026-11-02
4
+
5
+ A patch release with a security fix: `require 'facets/shellwords'` replaced
6
+ Ruby's `Shellwords.escape` with a version that left shell metacharacters
7
+ unescaped. It also lets Rails apps that load Facets boot again, and fixes
8
+ several methods that broke on Ruby 3.
9
+
10
+ Changes:
11
+
12
+ * Bug Fixes
13
+
14
+ * Security: `require 'facets/shellwords'` replaced Ruby's
15
+ `Shellwords.escape`, and with it `String#shellescape`, with a fallback
16
+ that escaped almost nothing. `Shellwords.escape("x; rm -rf ~")` came back
17
+ unchanged, so code that relied on it to quote shell arguments was open to
18
+ command injection. The fallback was meant to apply only where Ruby had no
19
+ `escape`, but its check never matched Ruby's definition, so it always
20
+ applied. It is gone, and Ruby's own `escape` is always used. The bug dates
21
+ back to 2010. `require 'facets'` does not load this file (#67).
22
+ * `require 'facets'` no longer makes `attr`, `attr_reader`, `attr_writer`,
23
+ `attr_accessor`, `remove_method` and `undef_method` protected; Ruby has
24
+ made them public since 2.5. `Module#redefine_method` is now public, like
25
+ ActiveSupport's, so Rails apps that load Facets no longer fail with
26
+ "private method 'redefine_method' called" (#292). A redefined method now
27
+ keeps its visibility; before, a redefined private method became public.
28
+ * `Enumerable#graph` (also `Hash#graph` and `#mash`) and `Interval#step`
29
+ without a block, and every call to `Find.select`, no longer raise
30
+ `ArgumentError` on Ruby 3 (#246). `facets/find/select` now also requires
31
+ `find` itself.
32
+ * `Math.percentile` returns the minimum or maximum at the extremes, as the
33
+ NIST method it cites specifies. `percentile([1,2,3], 90)` raised
34
+ `NoMethodError`, `percentile([1,2,3], 100)` returned `nil` and
35
+ `percentile([1,2,3], 0)` returned 3 (#289).
36
+ * `Kernel#deep_clone` now copies array elements and hash values at any
37
+ depth; before, it only copied instance variables (#11). Equal but distinct
38
+ objects, such as two equal strings, no longer share one copy. Hash keys,
39
+ and Struct and Set members, are still shared with the original.
40
+ * `Enumerable#map_by` maps `false` and `nil` values instead of storing the
41
+ whole element (#75).
42
+ * `Module#alias_class_method` works. It raised `NameError` on every call.
43
+ * `Time#round_to` no longer prints a deprecation warning. It called the
44
+ deprecated `Time#trunc`.
45
+ * The `facets/cloneable` deprecation warning recommends `Kernel#deep_clone`
46
+ or `initialize_copy` instead of `deep_dup`, which Facets doesn't define.
47
+ The 3.2.0 notes gave the same wrong advice.
48
+
49
+ * Documentation
50
+
51
+ * The README names the current release (it still said 3.2.2) and shows how
52
+ to load chosen extensions from a Gemfile with `require: [...]` (#280).
53
+ * The `Shellwords.alt_escape` docs no longer suggest it for `system()`. It
54
+ leaves `$ ; * ? ~` and newlines unescaped, so it is not safe for untrusted
55
+ input.
56
+
57
+ * Internal
58
+
59
+ * The tests use rubytest 0.9's own `rubytest` command instead of the
60
+ `rubytest-cli` gem.
61
+ * The test and demo runs hide the deprecation warnings they trigger on
62
+ purpose and print a count instead. A deprecation warning raised from
63
+ inside `lib/` still shows. Set `FACETS_DEPRECATIONS=show` to see every
64
+ warning.
65
+ * First tests for `Interval#step`, `Find.select` and
66
+ `Module#alias_class_method`.
67
+
3
68
  ## 4.0.0 / 2026-09-27
4
69
 
5
70
  A major release, because it changes behavior: `Kernel#try!` is now the
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **More of Ruby, one method at a time.** Facets is a collection of extensions to Ruby's core classes and standard library, plus a few small, reusable classes and modules. Most methods live in their own files, so you can load one extension, a class's extensions, or the core collection.
7
7
 
8
- Facets took its name in 2004, growing out of a small methods library started in 2002, and is still maintained. The current release is **3.2.2**, which requires **Ruby 3.1 or newer**. See the [release history](HISTORY.md) for changes and migration notes from earlier versions. The `main` branch also contains changes awaiting the next release.
8
+ Facets took its name in 2004, growing out of a small methods library started in 2002, and is still maintained. The current release is **4.0.1**, which requires **Ruby 3.1 or newer**. See the [release history](HISTORY.md) for changes and migration notes from earlier versions. The `main` branch also contains changes awaiting the next release.
9
9
 
10
10
  ## Install
11
11
 
@@ -21,6 +21,12 @@ gem 'facets', require: false
21
21
 
22
22
  `require: false` lets you choose which extensions to load. Omit it if you want Bundler to load the core collection automatically.
23
23
 
24
+ To have Bundler load only certain extensions, for example in a Rails app, list their files instead:
25
+
26
+ ```ruby
27
+ gem 'facets', require: ['facets/array/to_ranges', 'facets/string/snakecase']
28
+ ```
29
+
24
30
  ## Choose how much to load
25
31
 
26
32
  ### One method
@@ -26,7 +26,7 @@ module Enumerable
26
26
  end
27
27
  h
28
28
  else
29
- Enumerator.new(self,:graph)
29
+ to_enum(:graph)
30
30
  end
31
31
  end
32
32
 
@@ -21,10 +21,14 @@ module Enumerable
21
21
  def map_by #:yield:
22
22
  res = {}
23
23
  each do |a|
24
- k,v = yield(*a)
25
- if v
24
+ r = yield(*a)
25
+ # Test whether a second value was returned, not whether it is truthy,
26
+ # so false and nil are mapped too.
27
+ if Array === r && r.size > 1
28
+ k, v = r
26
29
  (res[k] ||= []) << v
27
30
  else
31
+ k, = r
28
32
  (res[k] ||= []) << a
29
33
  end
30
34
  end
@@ -1,24 +1,52 @@
1
1
  module Kernel
2
2
 
3
3
  # Deep clone an object by deep cloning every instance
4
- # variable as well.
4
+ # variable as well, along with the elements of an Array and
5
+ # the values of a Hash.
6
+ #
7
+ # a = [1, [2, [3]]]
8
+ # b = a.deep_clone
9
+ # b[1][1] << 4
10
+ # a #=> [1, [2, [3]]]
11
+ #
12
+ # Hash keys are not copied: Ruby already copies and freezes String
13
+ # keys, and a hash compared by identity needs the same key objects.
14
+ # Other containers, such as Struct and Set, share their members with
15
+ # the original.
16
+ #
17
+ # An object that is referenced more than once, including an object
18
+ # that contains itself, is copied once.
5
19
  #
6
20
  # Returns [Object]
7
21
 
8
- def deep_clone(cache={})
22
+ def deep_clone(cache={}.compare_by_identity)
9
23
  return cache[self] if cache.key?(self)
10
24
 
11
- copy = clone()
25
+ collection = (Array === self || Hash === self)
26
+
27
+ # A frozen collection's copy is filled in first, then frozen.
28
+ copy = collection ? clone(freeze: false) : clone
12
29
  cache[self] = copy
13
30
 
14
- copy.instance_variables.each do |var|
15
- val = instance_variable_get(var)
31
+ deep = lambda do |val|
16
32
  begin
17
- val = val.deep_clone(cache)
33
+ val.deep_clone(cache)
18
34
  rescue TypeError
19
- next
35
+ val
36
+ end
37
+ end
38
+
39
+ copy.instance_variables.each do |var|
40
+ copy.instance_variable_set(var, deep[instance_variable_get(var)])
41
+ end
42
+
43
+ if collection
44
+ if Array === copy
45
+ copy.map!(&deep)
46
+ else
47
+ copy.transform_values!(&deep)
20
48
  end
21
- copy.instance_variable_set(var, val)
49
+ copy.freeze if frozen?
22
50
  end
23
51
 
24
52
  return copy
@@ -2,10 +2,15 @@ class Module
2
2
 
3
3
  # Alias a class method.
4
4
  #
5
+ # class Greeter
6
+ # def self.hello; "Hello"; end
7
+ # alias_class_method :hi, :hello
8
+ # end
9
+ #
10
+ # Greeter.hi #=> "Hello"
11
+ #
5
12
  def alias_class_method(name, target)
6
- class << self
7
- alias_method(name, target)
8
- end
13
+ singleton_class.alias_method(name, target)
9
14
  end
10
15
 
11
16
  end
@@ -16,16 +16,5 @@ class Module
16
16
  module_eval { define_method name, &blk }
17
17
  end
18
18
 
19
- #--
20
- # TODO: Why are we making these protected,
21
- # and where should these really go in Facets?
22
- #++
23
- protected :attr
24
- protected :attr_reader
25
- protected :attr_writer
26
- protected :attr_accessor
27
- protected :remove_method
28
- protected :undef_method
29
-
30
19
  end
31
20
 
@@ -1,7 +1,5 @@
1
1
  class Module
2
2
 
3
- private
4
-
5
3
  # Creates a new method for a pre-existing method.
6
4
  #
7
5
  # If _aka_ is given, then the method being redefined will
@@ -21,14 +19,25 @@ class Module
21
19
  #
22
20
  # Greeter.new.hello #=> "Hello, friend!"
23
21
  #
22
+ # The new method keeps the visibility of the one it replaces. Like
23
+ # ActiveSupport's Module#redefine_method, this is public.
24
+ #
24
25
  # CREDIT: Trans
25
26
 
26
27
  def redefine_method(sym, aka=nil, &blk)
28
+ visibility = if private_method_defined?(sym)
29
+ :private
30
+ elsif protected_method_defined?(sym)
31
+ :protected
32
+ else
33
+ :public
34
+ end
27
35
  alias_method(aka, sym) if aka
28
36
  ## prevent warning about method overwrite
29
37
  begin remove_method(sym) rescue NameError end
30
- ## define replacelemt method
38
+ ## define replacement method
31
39
  define_method(sym, &blk)
40
+ __send__(visibility, sym)
32
41
  end
33
42
 
34
43
  #
@@ -14,7 +14,7 @@ class Time
14
14
  # TODO: Fractional seconds should round the usec.
15
15
  #
16
16
  def round_to(seconds)
17
- (self + seconds / 2.0).trunc(seconds)
17
+ (self + seconds / 2.0).floor_to(seconds)
18
18
  end
19
19
 
20
20
  end
@@ -1,3 +1,3 @@
1
1
  module Facets
2
- VERSION = '4.0.0'
2
+ VERSION = '4.0.1'
3
3
  end
@@ -1,7 +1,8 @@
1
- # @deprecated Use deep_dup/deep_clone instead, or define your own
2
- # initialize_copy method.
1
+ # @deprecated Use Kernel#deep_clone (`require 'facets/kernel/deep_clone'`)
2
+ # instead, or define your own initialize_copy method.
3
3
  # Scheduled for removal after 2027-09-30.
4
- warn "facets/cloneable is deprecated. Use deep_dup/deep_clone instead. It will be removed after 2027-09-30.", uplevel: 1
4
+ warn "facets/cloneable is deprecated. Use Kernel#deep_clone (require 'facets/kernel/deep_clone') " \
5
+ "or define your own initialize_copy instead. It will be removed after 2027-09-30.", uplevel: 1
5
6
 
6
7
  module Cloneable
7
8
 
@@ -1,3 +1,5 @@
1
+ require 'find'
2
+
1
3
  module Find
2
4
  # Identical to find except select returns the matching files as an array.
3
5
  # (find returns nil, which is not very useful if you actually wanted an array.)
@@ -8,7 +10,7 @@ module Find
8
10
  #
9
11
  # CREDIT: Tyler Rick
10
12
  def self.select(*paths, &block)
11
- Enumerator.new(self, :find, *paths).select{|value| yield value}
13
+ find(*paths).select(&block)
12
14
  end
13
15
  end
14
16
 
@@ -257,7 +257,7 @@ class Interval
257
257
  end
258
258
  end
259
259
  else
260
- Enumerator.new(self, :step, n, d)
260
+ to_enum(:step, n, d)
261
261
  end
262
262
  end
263
263
 
@@ -16,19 +16,24 @@ module Math
16
16
  # require 'facets/math/percentile'
17
17
  #
18
18
  def self.percentile(array, pcnt)
19
- sorted_array = array.sort
20
-
21
19
  return nil if array.length == 0
22
20
 
21
+ sorted_array = array.sort
22
+
23
23
  rank = (pcnt.to_f / 100) * (array.length + 1)
24
24
  whole = rank.truncate
25
-
25
+
26
+ # A rank below 1 or past the last value is clamped to the minimum or
27
+ # maximum, as NIST specifies.
28
+ return sorted_array.first if whole < 1
29
+ return sorted_array.last if whole >= array.length
30
+
26
31
  # if has fractional part
27
32
  if whole != rank
28
33
  s0 = sorted_array[whole - 1]
29
34
  s1 = sorted_array[whole]
30
35
 
31
- f = (rank - rank.truncate).abs
36
+ f = rank - whole
32
37
 
33
38
  return (f * (s1 - s0)) + s0
34
39
  else
@@ -4,21 +4,17 @@ module Shellwords
4
4
 
5
5
  module_function
6
6
 
7
- # Escape special characters used in most unix shells
8
- # to use it, eg. with system().
7
+ # Escape backslash, tab, space, and the characters | & ` < > ( ) ' "
8
+ # with a backslash.
9
9
  #
10
- # This differs from Ruby's #escape in that it does not
11
- # escape shell variables, e.g. $0.
10
+ # Unlike Ruby's Shellwords.escape, this leaves other shell
11
+ # metacharacters alone, including $ (so shell variables still
12
+ # expand), ; * ? ~ and newlines. It is not safe for untrusted
13
+ # input: use Shellwords.escape for that.
12
14
  def alt_escape(cmdline)
13
15
  cmdline.gsub(/([\\\t\| &`<>)('"])/) { |s| '\\' << s }
14
16
  end
15
17
 
16
- unless method_defined?(:escape)
17
- def escape(cmdline)
18
- cmdline.gsub(/([\\\t\| &`<>)('"])\$/) { |s| '\\' << s }
19
- end
20
- end
21
-
22
18
  # Escape special character used in DOS-based shells.
23
19
  #
24
20
  # TODO: How to integrate with rest of system?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: facets
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Sawyer