iostreams 2.1.0 → 2.2.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.
- checksums.yaml +4 -4
- data/docs/config.md +13 -9
- data/docs/copy_files.md +20 -2
- data/docs/extensions.md +18 -4
- data/docs/formats.md +644 -16
- data/docs/path.md +18 -2
- data/docs/streams.md +54 -9
- data/docs/tutorial.md +14 -2
- data/docs/upgrading.md +470 -23
- data/lib/io_streams/builder/deprecations.rb +250 -0
- data/lib/io_streams/builder.rb +256 -39
- data/lib/io_streams/bzip2/reader.rb +18 -2
- data/lib/io_streams/bzip2/writer.rb +11 -2
- data/lib/io_streams/deprecation/methods.rb +51 -0
- data/lib/io_streams/deprecation/once.rb +27 -0
- data/lib/io_streams/deprecation/option_names.rb +26 -0
- data/lib/io_streams/deprecation/words.rb +42 -0
- data/lib/io_streams/deprecation.rb +259 -0
- data/lib/io_streams/encode/reader.rb +54 -3
- data/lib/io_streams/encode/writer.rb +19 -2
- data/lib/io_streams/encode.rb +99 -0
- data/lib/io_streams/errors.rb +51 -0
- data/lib/io_streams/gzip/reader.rb +11 -0
- data/lib/io_streams/io_streams.rb +365 -35
- data/lib/io_streams/line/reader.rb +2 -1
- data/lib/io_streams/line/utf8_reader.rb +23 -0
- data/lib/io_streams/path.rb +125 -35
- data/lib/io_streams/paths/file.rb +72 -9
- data/lib/io_streams/paths/http.rb +48 -2
- data/lib/io_streams/paths/matcher.rb +6 -3
- data/lib/io_streams/paths/s3.rb +180 -9
- data/lib/io_streams/paths/sftp.rb +47 -3
- data/lib/io_streams/paths.rb +23 -0
- data/lib/io_streams/pgp/reader.rb +18 -0
- data/lib/io_streams/pgp/writer.rb +16 -0
- data/lib/io_streams/pgp.rb +16 -2
- data/lib/io_streams/reader.rb +44 -0
- data/lib/io_streams/record/reader.rb +59 -68
- data/lib/io_streams/record/writer.rb +17 -10
- data/lib/io_streams/row/header_row.rb +21 -0
- data/lib/io_streams/row/reader.rb +52 -15
- data/lib/io_streams/row/writer.rb +64 -13
- data/lib/io_streams/schema/columns.rb +456 -0
- data/lib/io_streams/schema/constraints.rb +99 -0
- data/lib/io_streams/schema/field.rb +967 -0
- data/lib/io_streams/schema/json_number.rb +56 -0
- data/lib/io_streams/schema/keys.rb +78 -0
- data/lib/io_streams/schema/pattern.rb +257 -0
- data/lib/io_streams/schema/source.rb +31 -0
- data/lib/io_streams/schema.rb +616 -0
- data/lib/io_streams/stream.rb +836 -67
- data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
- data/lib/io_streams/tabular/header.rb +29 -0
- data/lib/io_streams/tabular/header_mapping.rb +332 -0
- data/lib/io_streams/tabular/mapping.rb +165 -0
- data/lib/io_streams/tabular/parser/array.rb +5 -0
- data/lib/io_streams/tabular/parser/base.rb +24 -0
- data/lib/io_streams/tabular/parser/fixed.rb +197 -0
- data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
- data/lib/io_streams/tabular/parser/hash.rb +10 -0
- data/lib/io_streams/tabular/parser/json.rb +17 -0
- data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
- data/lib/io_streams/tabular/schema_mapping.rb +361 -0
- data/lib/io_streams/tabular.rb +344 -30
- data/lib/io_streams/utils.rb +441 -6
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +27 -0
- data/lib/io_streams/xlsx/cells.rb +127 -0
- data/lib/io_streams/xlsx/reader.rb +56 -0
- data/lib/io_streams/xlsx/schema_reader.rb +90 -0
- data/lib/io_streams/xlsx/v3_writer.rb +16 -0
- data/lib/io_streams/zip/reader.rb +7 -0
- data/lib/io_streams/zip/writer.rb +24 -2
- data/lib/iostreams.rb +21 -17
- metadata +28 -2
|
@@ -8,9 +8,25 @@ module IOStreams
|
|
|
8
8
|
nil
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Returns [Array<Symbol>] the options that this class takes, which v3.0 declares as its `option_names`, see
|
|
12
|
+
# `IOStreams::Deprecation::OptionNames#known_option_names`, so that an option that neither it nor the writer takes
|
|
13
|
+
# is reported as deprecated when it is set, while one of the writer is not, since v3.0 accepts it, and ignores it,
|
|
14
|
+
# and so that a path opens it with these alone, and logs the others, see `IOStreams::Builder#taken_options`.
|
|
15
|
+
def self.known_option_names
|
|
16
|
+
OPTION_NAMES
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a reader block, in place of the
|
|
20
|
+
# `Bzip2::FFI::Reader` that this reader yields, answers: those of an IO that read, see
|
|
21
|
+
# `IOStreams::Reader.v3_stream_methods`.
|
|
22
|
+
def self.v3_stream_methods(_options)
|
|
23
|
+
V3_READ_METHODS
|
|
24
|
+
end
|
|
25
|
+
|
|
11
26
|
# Read from a Bzip2 stream, decompressing the contents as it is read
|
|
12
27
|
#
|
|
13
|
-
# Any other option is ignored and logs a warning.
|
|
28
|
+
# Any other option is ignored, and logs a warning. One that the writer does not take either also reports a
|
|
29
|
+
# deprecation warning when it is set, see .known_option_names, since it will raise `ArgumentError` in v3.0.
|
|
14
30
|
#
|
|
15
31
|
# Parameters are passed through to `Bzip2::FFI::Reader`:
|
|
16
32
|
# autoclose: [true|false]
|
|
@@ -25,7 +41,7 @@ module IOStreams
|
|
|
25
41
|
# Use an alternative decompression algorithm that uses less memory but is slower.
|
|
26
42
|
# Default: false
|
|
27
43
|
def self.stream(input_stream, autoclose: false, first_only: false, small: false, **unknown)
|
|
28
|
-
Utils.warn_unknown_options(unknown, :bz2,
|
|
44
|
+
Utils.warn_unknown_options(unknown, :bz2, :reader, OPTION_NAMES)
|
|
29
45
|
Utils.load_soft_dependency("bzip2-ffi", "Bzip2", "bzip2/ffi") unless defined?(::Bzip2::FFI)
|
|
30
46
|
|
|
31
47
|
begin
|
|
@@ -8,9 +8,18 @@ module IOStreams
|
|
|
8
8
|
nil
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Returns [Array<Symbol>] the options that this class takes, which v3.0 declares as its `option_names`, see
|
|
12
|
+
# `IOStreams::Deprecation::OptionNames#known_option_names`, so that an option that neither it nor the reader takes
|
|
13
|
+
# is reported as deprecated when it is set, while one of the reader is not, since v3.0 accepts it, and ignores it,
|
|
14
|
+
# and so that a path opens it with these alone, and logs the others, see `IOStreams::Builder#taken_options`.
|
|
15
|
+
def self.known_option_names
|
|
16
|
+
OPTION_NAMES
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
# Write to a stream, compressing with Bzip2
|
|
12
20
|
#
|
|
13
|
-
# Any other option is ignored and logs a warning.
|
|
21
|
+
# Any other option is ignored, and logs a warning. One that the reader does not take either also reports a
|
|
22
|
+
# deprecation warning when it is set, see .known_option_names, since it will raise `ArgumentError` in v3.0.
|
|
14
23
|
#
|
|
15
24
|
# Parameters are passed through to `Bzip2::FFI::Writer`:
|
|
16
25
|
# autoclose: [true|false]
|
|
@@ -26,7 +35,7 @@ module IOStreams
|
|
|
26
35
|
# to a slower algorithm, from 0 to 250. 0 uses the libbz2 default.
|
|
27
36
|
# Default: 0
|
|
28
37
|
def self.stream(input_stream, autoclose: false, block_size: nil, work_factor: nil, **unknown)
|
|
29
|
-
Utils.warn_unknown_options(unknown, :bz2,
|
|
38
|
+
Utils.warn_unknown_options(unknown, :bz2, :writer, OPTION_NAMES)
|
|
30
39
|
Utils.load_soft_dependency("bzip2-ffi", "Bzip2", "bzip2/ffi") unless defined?(::Bzip2::FFI)
|
|
31
40
|
|
|
32
41
|
begin
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module IOStreams
|
|
2
|
+
module Deprecation
|
|
3
|
+
# A module whose methods report a deprecation warning when the application calls them, and then call the method of
|
|
4
|
+
# the object that it extends, see .deprecate: for the methods of an object that IOStreams hands the application, such
|
|
5
|
+
# as the `Zlib::GzipReader` that a reader block receives for a `.gz` file, that the object that v3.0 hands it in its
|
|
6
|
+
# place does not answer, such as `#rewind`.
|
|
7
|
+
class Methods < ::Module
|
|
8
|
+
@modules = {}
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
# Extends the supplied object, which IOStreams hands to the application, with a module that reports a deprecation
|
|
12
|
+
# warning for each of its public methods that the object in its place in v3.0 does not answer: those other than
|
|
13
|
+
# the supplied methods, which that object answers, and the methods of every object. Each warning is the name of the
|
|
14
|
+
# method, such as `#rewind`, followed by the supplied message, such as "is deprecated on ...", or by the message of
|
|
15
|
+
# its name when the message is a Hash, such as one that names what replaces it, otherwise that of nil.
|
|
16
|
+
#
|
|
17
|
+
# The module is prepended to the singleton class of the object, rather than extending it, so that its methods
|
|
18
|
+
# come before those that the object defines on itself, such as the `#path` of a `Zlib::GzipReader`. The object
|
|
19
|
+
# keeps its class, and answers the same methods, which each still call its own method. Only the calls that the
|
|
20
|
+
# application makes itself report a warning, see `IOStreams::Deprecation.warn_call`, and none while deprecation
|
|
21
|
+
# warnings are off. The module is built once for each class of object, and kept, so that each object costs one
|
|
22
|
+
# `prepend`, and nothing else until a deprecated method is called.
|
|
23
|
+
#
|
|
24
|
+
# Returns the object.
|
|
25
|
+
def self.deprecate(object, kept_methods, message)
|
|
26
|
+
return object if object.frozen?
|
|
27
|
+
|
|
28
|
+
key = [object.class, object.singleton_methods.sort, kept_methods, message]
|
|
29
|
+
deprecated = @mutex.synchronize do
|
|
30
|
+
@modules[key] ||= new(object.public_methods - ::Object.public_instance_methods - kept_methods, message)
|
|
31
|
+
end
|
|
32
|
+
object.singleton_class.prepend(deprecated)
|
|
33
|
+
object
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Defines a method for each of the supplied names that reports the deprecation warning of its name, followed by the
|
|
37
|
+
# supplied message, or its own message when it is a Hash, see .deprecate, when the application calls it, and then
|
|
38
|
+
# calls the method of the object.
|
|
39
|
+
def initialize(names, message)
|
|
40
|
+
super()
|
|
41
|
+
names.each do |name|
|
|
42
|
+
warning = "##{name} #{message.is_a?(Hash) ? message.fetch(name) { message.fetch(nil) } : message}".freeze
|
|
43
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
44
|
+
::IOStreams::Deprecation.warn_call(warning) if ::Warning[:deprecated]
|
|
45
|
+
super(*args, **kwargs, &block)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IOStreams
|
|
4
|
+
module Deprecation
|
|
5
|
+
# Reports a deprecation warning the first time that it applies, for an object that would otherwise report one for each
|
|
6
|
+
# value that it converts, such as a field of a schema, which converts a value of each row of a file. The object holds
|
|
7
|
+
# one for each of its deprecations, which is not frozen with it, so that an object that is frozen, as a field is,
|
|
8
|
+
# still reports each one once.
|
|
9
|
+
class Once
|
|
10
|
+
# Reports the deprecation warning that the block returns, see `IOStreams::Deprecation.warn`, unless this has
|
|
11
|
+
# reported it already, or deprecation warnings are off, see `IOStreams::Deprecation.on?`, when the block is not
|
|
12
|
+
# called, so that a caller builds its message only when it is reported. The block returns nil when the
|
|
13
|
+
# deprecation does not apply to the value at hand, which leaves it to be reported for a later one.
|
|
14
|
+
#
|
|
15
|
+
# Returns nil.
|
|
16
|
+
def warn
|
|
17
|
+
return if @reported || !Deprecation.on?
|
|
18
|
+
|
|
19
|
+
message = yield
|
|
20
|
+
return if message.nil?
|
|
21
|
+
|
|
22
|
+
@reported = true
|
|
23
|
+
Deprecation.warn(message)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IOStreams
|
|
4
|
+
module Deprecation
|
|
5
|
+
# The names of the options that a reader or a writer takes in IOStreams 3.0, which `IOStreams::Reader` and
|
|
6
|
+
# `IOStreams::Writer` extend, so that each of their subclasses answers them. `#option`, `#stream` and
|
|
7
|
+
# `#option_or_stream` report a deprecation warning for an option that IOStreams 3.0 raises ArgumentError for when it
|
|
8
|
+
# is set: one that the reader or the writer of the stream replaces, see #replaced_option_names, and one that neither
|
|
9
|
+
# of them takes, see #known_option_names, such as a misspelled one, see `IOStreams::Builder#option`.
|
|
10
|
+
module OptionNames
|
|
11
|
+
# Returns [Array<Symbol>] the names of the options that this class takes in IOStreams 3.0 too, or [nil] when they
|
|
12
|
+
# are not known, since it does not declare its options: by default those that it declares, see `.option_names`,
|
|
13
|
+
# other than those that it replaces, see #replaced_option_names.
|
|
14
|
+
def known_option_names
|
|
15
|
+
names = option_names
|
|
16
|
+
names && (names - replaced_option_names.keys)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns [Hash<Symbol, Symbol>] the deprecated options that this class still takes, which IOStreams 3.0 does not,
|
|
20
|
+
# with the option that replaces each, which the deprecation warning names. None by default.
|
|
21
|
+
def replaced_option_names
|
|
22
|
+
{}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IOStreams
|
|
4
|
+
module Deprecation
|
|
5
|
+
# The words of the message of a deprecation warning that depend on how many things it is about, such as the options
|
|
6
|
+
# that are deprecated, so that each message says "is" and "it" of one, and "are" and "them" of more, see .warn.
|
|
7
|
+
class Words
|
|
8
|
+
# Parameters:
|
|
9
|
+
# items: [Array]
|
|
10
|
+
# The things that the message is about, such as the names of options.
|
|
11
|
+
def initialize(items)
|
|
12
|
+
@items = items
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns [String] the items as the keywords that a caller supplies them as, such as `compress:, level:`, and an
|
|
16
|
+
# item that is not a Symbol, which is not a keyword, as it is written, such as `"acl"`.
|
|
17
|
+
def keywords
|
|
18
|
+
@items.map { |item| item.is_a?(Symbol) ? "#{item}:" : item.inspect }.join(", ")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns [true|false] whether the message is about one item.
|
|
22
|
+
def one?
|
|
23
|
+
@items.size == 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns [String] "are", or "is" of one item.
|
|
27
|
+
def are
|
|
28
|
+
one? ? "is" : "are"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns [String] "them", or "it" of one item.
|
|
32
|
+
def them
|
|
33
|
+
one? ? "it" : "them"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns [String] "they are", or "it is" of one item.
|
|
37
|
+
def they_are
|
|
38
|
+
one? ? "it is" : "they are"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module IOStreams
|
|
6
|
+
# Reports the use of a deprecated method or option as a Ruby deprecation warning, see `Kernel#warn`, so that Ruby
|
|
7
|
+
# decides whether and where it is reported, as it does for its own deprecations, rather than a setting of IOStreams:
|
|
8
|
+
# - Only while deprecation warnings are on, see `Warning[:deprecated]`. They are off by default, so production stays
|
|
9
|
+
# quiet, and on in tests run by Minitest or test-unit, or with `ruby -W:deprecated` or `RUBYOPT=-W:deprecated`.
|
|
10
|
+
# - Written to `$stderr`, unless the application routes them elsewhere, such as to its log, or raises them in its
|
|
11
|
+
# tests, by overriding `Warning.warn`.
|
|
12
|
+
#
|
|
13
|
+
# Each warning names the line of the application that used what is deprecated, rather than a line of IOStreams, of
|
|
14
|
+
# Ruby itself, or of a gem that IOStreams called with a block, see .application_location.
|
|
15
|
+
module Deprecation
|
|
16
|
+
autoload :Methods, "io_streams/deprecation/methods"
|
|
17
|
+
autoload :Once, "io_streams/deprecation/once"
|
|
18
|
+
autoload :OptionNames, "io_streams/deprecation/option_names"
|
|
19
|
+
autoload :Words, "io_streams/deprecation/words"
|
|
20
|
+
|
|
21
|
+
# The directory of the files of this library, whose lines are skipped when finding the line that used it.
|
|
22
|
+
LIB_DIR = "#{__dir__}/".freeze
|
|
23
|
+
private_constant :LIB_DIR
|
|
24
|
+
|
|
25
|
+
# The file that loads this library, whose lines are skipped too. Only the files of this library are skipped, rather
|
|
26
|
+
# than every file of the directory that holds them, which can hold other libraries, such as `site_ruby`, or the
|
|
27
|
+
# `lib` directory of an application that it is copied into.
|
|
28
|
+
LIB_FILE = File.expand_path("../iostreams.rb", __dir__).freeze
|
|
29
|
+
private_constant :LIB_FILE
|
|
30
|
+
|
|
31
|
+
# The text that each warning starts with, after the location of the line that used what is deprecated.
|
|
32
|
+
PREFIX = "IOStreams: "
|
|
33
|
+
private_constant :PREFIX
|
|
34
|
+
|
|
35
|
+
# A warning reported by .warn, as `Kernel#warn` passes it to `Warning.warn`, see .warning?.
|
|
36
|
+
WARNING = /\A(?:.+:\d+: warning: )?#{Regexp.escape(PREFIX)}/
|
|
37
|
+
private_constant :WARNING
|
|
38
|
+
|
|
39
|
+
# The start of the paths of the core methods that Ruby writes in Ruby, such as `Kernel#tap`, see .core?.
|
|
40
|
+
CORE_PATH = "<internal:"
|
|
41
|
+
private_constant :CORE_PATH
|
|
42
|
+
|
|
43
|
+
# The start of the paths of the standard library of JRuby, when it is loaded from its jar.
|
|
44
|
+
JAR_PATH = "uri:classloader:"
|
|
45
|
+
private_constant :JAR_PATH
|
|
46
|
+
|
|
47
|
+
# The files of this module, whose methods find the line that called a deprecated method, see .calling_location.
|
|
48
|
+
OWN_FILES = ["#{__dir__}/deprecation.rb", "#{__dir__}/deprecation/methods.rb"].freeze
|
|
49
|
+
private_constant :OWN_FILES
|
|
50
|
+
|
|
51
|
+
# The end of the names of the files of the standard library of Ruby that define its delegators: `delegate.rb`, which
|
|
52
|
+
# defines `SimpleDelegator` and `DelegateClass`, and `forwardable.rb`, which defines `Forwardable`, see .delegator?.
|
|
53
|
+
DELEGATOR_FILES = %w[/delegate.rb /forwardable.rb].freeze
|
|
54
|
+
private_constant :DELEGATOR_FILES
|
|
55
|
+
|
|
56
|
+
# The directory of the standard library of Ruby.
|
|
57
|
+
RUBY_LIB_DIRS = [RbConfig::CONFIG["rubylibdir"]].compact.map { |dir| "#{dir}/" }.freeze
|
|
58
|
+
private_constant :RUBY_LIB_DIRS
|
|
59
|
+
|
|
60
|
+
# Reports that the application used something deprecated, unless deprecation warnings are off, with the message,
|
|
61
|
+
# which says what is deprecated and what to use instead, after "IOStreams: ", at the line of the application that
|
|
62
|
+
# used it, see .application_location, in the same form as `Kernel#warn` names the line of its caller, such as
|
|
63
|
+
# `app/jobs/export_job.rb:12: warning: IOStreams: stream(:none) is deprecated, ...`, or without a line when the
|
|
64
|
+
# call stack has none outside this library.
|
|
65
|
+
#
|
|
66
|
+
# Returns nil.
|
|
67
|
+
def self.warn(message)
|
|
68
|
+
return unless on?
|
|
69
|
+
|
|
70
|
+
report(message, application_location)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Reports, as .warn does, that the application uses something deprecated while this library is loaded, such as a
|
|
74
|
+
# version of Ruby that IOStreams 3.0 does not support, at the line of the application that loaded it, see
|
|
75
|
+
# .loading_location, rather than at a line of a gem that requires it for the application, such as Bundler.
|
|
76
|
+
#
|
|
77
|
+
# Returns nil.
|
|
78
|
+
def self.warn_on_load(message)
|
|
79
|
+
return unless on?
|
|
80
|
+
|
|
81
|
+
report(message, loading_location)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Reports, as .warn does, that the application called a deprecated method of an object that IOStreams handed it,
|
|
85
|
+
# such as the stream that a reader block receives, which calls this from that method, see
|
|
86
|
+
# `IOStreams::Deprecation::Methods`, at the line that called the method, see .calling_location, when it is the
|
|
87
|
+
# application's own: not a line of IOStreams, which calls the methods of the object for itself, of a gem, such as the
|
|
88
|
+
# gem whose object it is, which calls its own methods, or of a library that the application handed the object to,
|
|
89
|
+
# including the standard library of Ruby, see .standard_library?. So a method that the application hands IOStreams
|
|
90
|
+
# as the block itself, such as `reader(&:rewind)`, is not reported, since IOStreams calls it, while one that it hands
|
|
91
|
+
# a core method of Ruby, such as `io.then(&:rewind)`, or calls through a delegator of Ruby that wraps the object,
|
|
92
|
+
# such as `SimpleDelegator.new(io).rewind`, is, since they call it for the application.
|
|
93
|
+
#
|
|
94
|
+
# Returns nil.
|
|
95
|
+
def self.warn_call(message)
|
|
96
|
+
return unless Warning[:deprecated]
|
|
97
|
+
|
|
98
|
+
location = calling_location
|
|
99
|
+
return if location.nil? || library?(location) || gem?(location, installed_gem_dirs) || standard_library?(location)
|
|
100
|
+
|
|
101
|
+
report(message, location)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns [true|false] whether the message, as `Kernel#warn` passes it to `Warning.warn`, is a warning reported by
|
|
105
|
+
# .warn, so that an application, or its tests, can tell the deprecation warnings of IOStreams from the others.
|
|
106
|
+
def self.warning?(message)
|
|
107
|
+
WARNING.match?(message)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns [true|false] whether deprecation warnings are on, see `Warning[:deprecated]`, so that .warn reports them,
|
|
111
|
+
# for a caller to find out whether something is deprecated only while they are, where finding it out costs more
|
|
112
|
+
# than the call to .warn.
|
|
113
|
+
def self.on?
|
|
114
|
+
Warning[:deprecated]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns [Thread::Backtrace::Location|nil] the line of the application that used this library, or nil when the
|
|
118
|
+
# call stack has none outside this library. It is the first line of the call stack outside this library, other
|
|
119
|
+
# than:
|
|
120
|
+
# - A line of Ruby itself, see .ruby?, which runs on behalf of its caller, such as `Kernel#tap`, or `Open3.popen2e`
|
|
121
|
+
# that this library calls with a block, as Ruby names the caller of its own core methods.
|
|
122
|
+
# - A line of a gem that this library called with a block, such as `Net::SFTP.start`, which has a line of this
|
|
123
|
+
# library further out in the call stack. A line of a gem that called this library, such as one of Rocket Job, is
|
|
124
|
+
# the one returned, since it is the gem that used it.
|
|
125
|
+
#
|
|
126
|
+
# The call stack is walked outward from the caller, until the line of the application is found, rather than
|
|
127
|
+
# creating all of it.
|
|
128
|
+
def self.application_location
|
|
129
|
+
gem_dirs = installed_gem_dirs
|
|
130
|
+
candidate = nil
|
|
131
|
+
Thread.each_caller_location do |location|
|
|
132
|
+
if library?(location)
|
|
133
|
+
candidate = nil
|
|
134
|
+
elsif gem?(location, gem_dirs)
|
|
135
|
+
candidate ||= location
|
|
136
|
+
elsif !ruby?(location)
|
|
137
|
+
return candidate || location
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
candidate
|
|
141
|
+
end
|
|
142
|
+
private_class_method :application_location
|
|
143
|
+
|
|
144
|
+
# Returns [Thread::Backtrace::Location|nil] the line of the application that loaded this library, such as its
|
|
145
|
+
# `require "iostreams"`, or its `Bundler.require`: the first line of the call stack outside this library, Ruby
|
|
146
|
+
# itself, see .ruby?, and the installed gems, since a gem that loads this library, such as Bundler, Bootsnap, or a gem
|
|
147
|
+
# that requires it, loads it for the application. Otherwise the first line of an installed gem outside this library,
|
|
148
|
+
# for an application that is itself an installed gem, or nil when the call stack has neither.
|
|
149
|
+
def self.loading_location
|
|
150
|
+
gem_dirs = installed_gem_dirs
|
|
151
|
+
candidate = nil
|
|
152
|
+
Thread.each_caller_location do |location|
|
|
153
|
+
next if library?(location) || ruby?(location)
|
|
154
|
+
return location unless gem?(location, gem_dirs)
|
|
155
|
+
|
|
156
|
+
candidate ||= location
|
|
157
|
+
end
|
|
158
|
+
candidate
|
|
159
|
+
end
|
|
160
|
+
private_class_method :loading_location
|
|
161
|
+
|
|
162
|
+
# Returns [Thread::Backtrace::Location|nil] the line that called the deprecated method that called .warn_call: the
|
|
163
|
+
# first line of the call stack outside the files of this module, other than a line of a core method, see .core?, or
|
|
164
|
+
# of a delegator of Ruby, see .delegator?, which calls the method for its caller, such as `Kernel#then` in
|
|
165
|
+
# `io.then(&:rewind)`, or the `SimpleDelegator` in `SimpleDelegator.new(io).rewind`. Ruby names the line that
|
|
166
|
+
# called a core method written in C in its place, and CRuby 4.0 does the same for one written in Ruby, so skipping
|
|
167
|
+
# the lines that earlier versions name gives the same line on each version of Ruby.
|
|
168
|
+
def self.calling_location
|
|
169
|
+
Thread.each_caller_location do |location|
|
|
170
|
+
next if OWN_FILES.include?(location.absolute_path || location.path) || core?(location) || delegator?(location)
|
|
171
|
+
|
|
172
|
+
return location
|
|
173
|
+
end
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
private_class_method :calling_location
|
|
177
|
+
|
|
178
|
+
# Writes the message as a deprecation warning, see .warn, at the supplied line, or without a line when it is nil.
|
|
179
|
+
def self.report(message, location)
|
|
180
|
+
text = "#{PREFIX}#{message}"
|
|
181
|
+
text = "#{location.path}:#{location.lineno}: warning: #{text}" if location
|
|
182
|
+
Kernel.warn(text, category: :deprecated)
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
private_class_method :report
|
|
186
|
+
|
|
187
|
+
# Returns [true|false] whether the line of the call stack is in a file of this library. Code run with `eval` has
|
|
188
|
+
# only the file name that it was given.
|
|
189
|
+
def self.library?(location)
|
|
190
|
+
file = location.absolute_path || location.path
|
|
191
|
+
file.start_with?(LIB_DIR) || file == LIB_FILE
|
|
192
|
+
end
|
|
193
|
+
private_class_method :library?
|
|
194
|
+
|
|
195
|
+
# Returns [true|false] whether the line of the call stack is in the code of Ruby itself: a core method, see .core?,
|
|
196
|
+
# or a file of its standard library, see .standard_library?.
|
|
197
|
+
def self.ruby?(location)
|
|
198
|
+
core?(location) || standard_library?(location)
|
|
199
|
+
end
|
|
200
|
+
private_class_method :ruby?
|
|
201
|
+
|
|
202
|
+
# Returns [true|false] whether the line of the call stack is in a core method of Ruby that is not written in C: one
|
|
203
|
+
# written in Ruby, such as `Kernel#tap`, in a file whose name starts with `<internal:`, which CRuby before 4.0 and
|
|
204
|
+
# JRuby name in the call stack, or one of JRuby written in Java. Ruby names the line that called a core method
|
|
205
|
+
# written in C in its place, as CRuby 4.0 does for one written in Ruby.
|
|
206
|
+
def self.core?(location)
|
|
207
|
+
path = location.path
|
|
208
|
+
path.start_with?(CORE_PATH) || path.end_with?(".java")
|
|
209
|
+
end
|
|
210
|
+
private_class_method :core?
|
|
211
|
+
|
|
212
|
+
# Returns [true|false] whether the line of the call stack is in a file of the standard library of Ruby, see
|
|
213
|
+
# RUBY_LIB_DIRS, or of that of JRuby, when it is loaded from its jar.
|
|
214
|
+
def self.standard_library?(location)
|
|
215
|
+
location.path.start_with?(JAR_PATH) || in_dirs?(location, RUBY_LIB_DIRS)
|
|
216
|
+
end
|
|
217
|
+
private_class_method :standard_library?
|
|
218
|
+
|
|
219
|
+
# Returns [true|false] whether the line of the call stack is in a delegator of the standard library of Ruby, see
|
|
220
|
+
# DELEGATOR_FILES, which calls the method of the object that it wraps for its caller, as a core method does: the
|
|
221
|
+
# `#method_missing` of a `SimpleDelegator`, or a method that `DelegateClass` or `Forwardable` defines. The other
|
|
222
|
+
# files of the standard library call the methods of an object for themselves, such as a library that the
|
|
223
|
+
# application hands the object to, see .warn_call.
|
|
224
|
+
def self.delegator?(location)
|
|
225
|
+
location.path.end_with?(*DELEGATOR_FILES) && standard_library?(location)
|
|
226
|
+
end
|
|
227
|
+
private_class_method :delegator?
|
|
228
|
+
|
|
229
|
+
# Returns [true|false] whether the line of the call stack is in a file of a gem, in one of the directories of the
|
|
230
|
+
# installed gems.
|
|
231
|
+
def self.gem?(location, gem_dirs)
|
|
232
|
+
in_dirs?(location, gem_dirs)
|
|
233
|
+
end
|
|
234
|
+
private_class_method :gem?
|
|
235
|
+
|
|
236
|
+
# Returns [true|false] whether the file of the line of the call stack is in one of the directories, by the path
|
|
237
|
+
# that it was loaded from, or its absolute path, which differs when the path holds a link.
|
|
238
|
+
def self.in_dirs?(location, dirs)
|
|
239
|
+
location.path.start_with?(*dirs) || location.absolute_path&.start_with?(*dirs) || false
|
|
240
|
+
end
|
|
241
|
+
private_class_method :in_dirs?
|
|
242
|
+
|
|
243
|
+
# Returns [Array<String>] the directories of the installed gems, see `Gem.path`, each ending with "/". They are kept
|
|
244
|
+
# for as long as `Gem.path` returns the same Array, since Bundler replaces it when it changes them, so that a
|
|
245
|
+
# deprecated method that a gem calls for each line, see .warn_call, builds them once, rather than on each call.
|
|
246
|
+
def self.installed_gem_dirs
|
|
247
|
+
return [] unless defined?(::Gem)
|
|
248
|
+
|
|
249
|
+
path = ::Gem.path
|
|
250
|
+
cached = @installed_gem_dirs
|
|
251
|
+
return cached.last if cached&.first.equal?(path)
|
|
252
|
+
|
|
253
|
+
dirs = path.map { |dir| dir.end_with?("/") ? dir : "#{dir}/" }.freeze
|
|
254
|
+
@installed_gem_dirs = [path, dirs].freeze
|
|
255
|
+
dirs
|
|
256
|
+
end
|
|
257
|
+
private_class_method :installed_gem_dirs
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -5,6 +5,23 @@ module IOStreams
|
|
|
5
5
|
%i[encoding cleaner replace]
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
# The methods that the stream that v3.0 supplies to a reader block for the encoding answers: those of an IO that
|
|
9
|
+
# read, and #input_stream, see .v3_stream_methods.
|
|
10
|
+
V3_METHODS = [*V3_READ_METHODS, :input_stream].freeze
|
|
11
|
+
private_constant :V3_METHODS
|
|
12
|
+
|
|
13
|
+
# Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a reader block for the encoding answers,
|
|
14
|
+
# in place of this reader, see `IOStreams::Reader.v3_stream_methods`. It has no #encoding or #cleaner, which the
|
|
15
|
+
# settings of the path replace, see .v3_stream_advice.
|
|
16
|
+
def self.v3_stream_methods(_options)
|
|
17
|
+
V3_METHODS
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns [Hash<Symbol:String>] what replaces #encoding and #cleaner, see `IOStreams::Reader.v3_stream_advice`.
|
|
21
|
+
def self.v3_stream_advice
|
|
22
|
+
V3_STREAM_ADVICE
|
|
23
|
+
end
|
|
24
|
+
|
|
8
25
|
attr_reader :encoding, :cleaner
|
|
9
26
|
|
|
10
27
|
NOT_PRINTABLE = /[^[:print:]|\r\n]/
|
|
@@ -60,7 +77,11 @@ module IOStreams
|
|
|
60
77
|
|
|
61
78
|
# Returns [String] data returned from the input stream.
|
|
62
79
|
# Returns [nil] if end of file and no further data was read.
|
|
63
|
-
|
|
80
|
+
#
|
|
81
|
+
# As IO#read does, the data is read into the supplied buffer, which is returned, and which is empty once the end
|
|
82
|
+
# of the file was reached, so that `IO.copy_stream`, which supplies one, can read it, such as for a copy without a
|
|
83
|
+
# mode from a stream with an encoding.
|
|
84
|
+
def read(size = nil, outbuf = nil)
|
|
64
85
|
block =
|
|
65
86
|
if @read_cache_buffer
|
|
66
87
|
begin
|
|
@@ -75,11 +96,18 @@ module IOStreams
|
|
|
75
96
|
end
|
|
76
97
|
|
|
77
98
|
# EOF reached?
|
|
78
|
-
|
|
99
|
+
unless block
|
|
100
|
+
outbuf&.clear
|
|
101
|
+
return
|
|
102
|
+
end
|
|
79
103
|
|
|
104
|
+
block = text(block, size) if text_bytes?(block)
|
|
80
105
|
block = block.encode(@encoding, **@encoding_options) unless block.encoding == @encoding
|
|
81
106
|
block = @cleaner.call(block, @replace) if @cleaner
|
|
82
|
-
block
|
|
107
|
+
return outbuf.replace(block) if outbuf
|
|
108
|
+
|
|
109
|
+
# A copy of the buffer that it reads into, which the next read writes over, so that the caller keeps the data.
|
|
110
|
+
block.equal?(@read_cache_buffer) ? block.dup : block
|
|
83
111
|
end
|
|
84
112
|
|
|
85
113
|
def self.extract_cleaner(cleaner)
|
|
@@ -95,6 +123,29 @@ module IOStreams
|
|
|
95
123
|
cleaner
|
|
96
124
|
end
|
|
97
125
|
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# Returns [true|false] whether the binary data that a read returned can hold the bytes of text in the encoding,
|
|
130
|
+
# rather than text to convert into it, see #text. Without `replace:` it reads into a buffer of the encoding, whose
|
|
131
|
+
# bytes are then the text, as `IO#read` keeps the encoding of the buffer that it is supplied for a read of a length.
|
|
132
|
+
# A stream that returns the bytes as binary instead, since its `#read` takes no buffer, as `Zlib::GzipReader` on
|
|
133
|
+
# JRuby, or makes the buffer binary, as `Bzip2::FFI::Reader` does, holds the same text, as does a read of the whole
|
|
134
|
+
# of the data, which `IO#read` returns as binary, and converting it from binary would reject each character that is
|
|
135
|
+
# not ASCII. With `replace:` it is converted from binary, replacing each character that is not ASCII, as before.
|
|
136
|
+
def text_bytes?(block)
|
|
137
|
+
@replace.nil? && block.encoding == Encoding::BINARY
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Returns [String] the bytes of the binary data as text in the encoding, in a String of its own, rather than the
|
|
141
|
+
# String that the input stream returned, which it can still hold, such as a buffer that it reuses. Those of a read
|
|
142
|
+
# of a length are taken as they are, as `IO#read` reads them into a buffer of the encoding, since a character can
|
|
143
|
+
# be split between two reads. The whole of the data is taken only when it is valid in the encoding; otherwise the
|
|
144
|
+
# data itself is returned, which is converted from binary, raising for each character that is not ASCII, as before.
|
|
145
|
+
def text(block, size)
|
|
146
|
+
text = ::String.new(block, encoding: @encoding)
|
|
147
|
+
size.nil? && !text.valid_encoding? ? block : text
|
|
148
|
+
end
|
|
98
149
|
end
|
|
99
150
|
end
|
|
100
151
|
end
|
|
@@ -5,6 +5,23 @@ module IOStreams
|
|
|
5
5
|
%i[encoding cleaner replace]
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
# The methods that the stream that v3.0 supplies to a writer block for the encoding answers: those of an IO that
|
|
9
|
+
# write, #finish and #output_stream, see .v3_stream_methods.
|
|
10
|
+
V3_METHODS = [*V3_WRITE_METHODS, :finish, :output_stream].freeze
|
|
11
|
+
private_constant :V3_METHODS
|
|
12
|
+
|
|
13
|
+
# Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a writer block for the encoding answers,
|
|
14
|
+
# in place of this writer, see `IOStreams::Writer.v3_stream_methods`. It has no #encoding or #cleaner, which the
|
|
15
|
+
# settings of the path replace, see .v3_stream_advice.
|
|
16
|
+
def self.v3_stream_methods(_options)
|
|
17
|
+
V3_METHODS
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns [Hash<Symbol:String>] what replaces #encoding and #cleaner, see `IOStreams::Reader.v3_stream_advice`.
|
|
21
|
+
def self.v3_stream_advice
|
|
22
|
+
V3_STREAM_ADVICE
|
|
23
|
+
end
|
|
24
|
+
|
|
8
25
|
attr_reader :encoding, :cleaner
|
|
9
26
|
|
|
10
27
|
# Write a line at a time to a file or stream
|
|
@@ -50,7 +67,7 @@ module IOStreams
|
|
|
50
67
|
# Write a line to the output stream
|
|
51
68
|
#
|
|
52
69
|
# Example:
|
|
53
|
-
# IOStreams.path('a.txt').
|
|
70
|
+
# IOStreams.path('a.txt').encoding('UTF-8').writer do |stream|
|
|
54
71
|
# stream << 'first line' << 'second line'
|
|
55
72
|
# end
|
|
56
73
|
def <<(record)
|
|
@@ -62,7 +79,7 @@ module IOStreams
|
|
|
62
79
|
# Returns [Integer] the number of bytes written.
|
|
63
80
|
#
|
|
64
81
|
# Example:
|
|
65
|
-
# IOStreams.path('a.txt').
|
|
82
|
+
# IOStreams.path('a.txt').encoding('UTF-8').writer do |stream|
|
|
66
83
|
# count = stream.write('first line')
|
|
67
84
|
# puts "Wrote #{count} bytes to the output file"
|
|
68
85
|
# end
|