Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chomp option to readline(s) #19944

Closed
wants to merge 16 commits into from
Closed

Conversation

mpastell
Copy link
Contributor

@mpastell mpastell commented Jan 9, 2017

This pull request adds chomp::Bool=false option to readline, readlines and eachline

This is "fresh start" based on discussion in #19877 .

The implementation is completely based on Julia, but it could be made a bit faster by modifying C code.

An example with a text file with 1e6 lines, this matches current master:

julia> @time readlines("../readlines/test.txt", false)
  0.213501 seconds (1.00 M allocations: 39.531 MB, 12.46% gc time)
1000000-element Array{String,1}:
 "Line1\n"      
 "Line2\n"

Setting chomp to true has 2x the number allocations and is therefore a bit slower:

julia> @time readlines("../readlines/test.txt", true)
  0.302754 seconds (2.00 M allocations: 70.049 MB, 22.71% gc time)
1000000-element Array{String,1}:
 "Line1"      
 "Line2" 

line = readuntil(s, 0x0a)
i = length(line)
if i < 1 || line[i] != 0x0a
return String(line[1:i])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just do String(line)? This could also be merged with the simple case above, as getting the length of line should have a negligible cost.

if i < 1 || line[i] != 0x0a
return String(line[1:i])
elseif i < 2 || line[i-1] != 0x0d
return String(line[1:i-1])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unnecessarily creates a copy of line and explains the allocations you get. Use unsafe_string(pointer(line), i-1). Same below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsafe_string still allocates a copy. Maybe String(resize!(line, i-1))?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try!

readlines(filename::AbstractString)
line = readuntil(s, 0x0a)
i = length(line)
if i < 1 || line[i] != 0x0a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably clearer as i == 0? I wondered for one moment when it would be negative.

Also, it could be more efficient to start with the i >= 2 case, which is the common one, to avoid an unnecessary branch in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the order branches really make a difference? I've sometimes tried to benchmark with cases I thought were very obvious, but didn't find a clear effect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea, would need to check. You're right that it's probably going to be negligible except maybe for very short strings, so maybe it's OK to have the first branch precisely for them.

@@ -74,6 +74,12 @@ Base.compact(io)
@test_throws ArgumentError seek(io,0)
@test_throws ArgumentError truncate(io,0)
@test readline(io) == "whipped cream\n"
@test write(io,"pancakes\nwaffles\nblueberries\n") > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a few non-ASCII characters like ë and ? This would ensure we don't assume 1-byte characters.

readline(stream::IO=STDIN)
readline(filename::AbstractString)
readline(stream::IO=STDIN, chomp::Bool=false)
readline(filename::AbstractString, chomp::Bool=false)
Copy link
Member

@stevengj stevengj Jan 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if chomp=true should be the default (here and in eachline)? Almost all uses of readline that I can find in the packages either (a) are insensitive to whether it ends with a newline or (b) explicitly strip the newline.

That would be a breaking change, but I doubt it would break much actual code, since most code calling readline or eachline has to handle the case where the last line in the file does not end in a newline anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan was to merge this first, and then open a PR to deprecate the one-argument form of readline in 0.6. Then the default would be changed in 1.0.

We could make the breaking change if you say packages are fine, but the deprecation would allow people to notice they no longer need to remove trailing newlines, which will make code simpler/faster. Might be worth the inconvenience, not sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Would prefer to do the deprecation in the same PR, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Let's ensure we're happy with the implementation, and then have a commit to make the switch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for making chomp=true the default. Does adding a deprecation mean fixing all of deprecation warnings from base as well? I'm afraid I won't have time to do it before 0.6 feature freeze.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but really that should be much easier than what you've done until now. If you really can't, I'll do it for you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll look into it first. I guess the easiest would be to change all the calls to readline to `readline(..., false).

How do you add a deprecation warning?

readline(filename::AbstractString, chomp::Bool=false)

Read a single line of text from the given I/O stream or file (defaults to `STDIN`).
Lines in the input can end in `'\\n'` or `'\\r\\n'`. When reading from a file, the text is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, should be "\r\n", so do that and also "\n".

While you're at it, the description of chomp would better be in a separate paragraph.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it does look better with separate paragraphs

s = ccall(:jl_readuntil, Ref{String}, (Ptr{Void}, UInt8, UInt8), s.ios, '\n', 1)
i = endof(s)
if i < 1 || codeunit(s,i) != 0x0a
return s[1:i]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to find a strategy similar to above to avoid a copy. But I'm not sure how to do that with the new String design.

BTW, is this expected? I thought no copy was involved when creating the string:

julia> x=UInt8['a', 'b']
2-element Array{UInt8,1}:
 0x61
 0x62

julia> s=String(x)
"ab"

julia> x[1] = 'c'
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

julia> s # String hasn't changed
"ab"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, String(::Vector) is no longer copy-free unless the vector is allocated with the (undocumented) Base.StringVector:

julia> x = Base.StringVector(2); x[1] = 'a'; x[2] = 'b'
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)

julia> x = Base.StringVector(2); x[1] = 'a'; x[2] = 'b'; s = String(x)
"ab"

julia> x[1] = 'c'
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)

julia> s
"cb"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, in order to avoid an extra copy, it seems like the chomping will have to be done in C, in jl_readuntil.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also #19945.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this PR at least, we don't need these functions to be exported since we're in Base. But here we would need jl_readuntil to return a StringVector. Is that possible?

Copy link
Member

@stevengj stevengj Jan 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jl_readuntil can return a String already if str=1 is passed (see also #19946). It would be easy to add an additional chomp argument to it that chomped LF/CRLF, and this might be the only way to completely avoid copies with the current String implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too bad. We may as well reintroduce jl_readline from #19877 for now, since @mpastell has already written it. It will be easier to remove when possible than modifying jl_readuntil.

chomp || return ccall(:jl_readuntil, Ref{String}, (Ptr{Void}, UInt8, UInt8), s.ios, '\n', 1)

s = ccall(:jl_readuntil, Ref{String}, (Ptr{Void}, UInt8, UInt8), s.ios, '\n', 1)
i = endof(s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endof is costly since it needs to look for the start of the last Unicode char. Here we're working at the byte level, so use sizeof(s) instead. Then, you cannot index directly into s as you might be in the middle of a character: you need to access the underlying array of bytes directly (cf. point below).


s = ccall(:jl_readuntil, Ref{String}, (Ptr{Void}, UInt8, UInt8), s.ios, '\n', 1)
i = endof(s)
if i < 1 || codeunit(s,i) != 0x0a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add @inbounds here since you did the check manually. Same below.

@ararslan
Copy link
Member

ararslan commented Jan 9, 2017

IMO the chomp option might be nice as a keyword argument for clarity.

Another option would be, rather than adding chomp as an option, add a function argument that applies the given function to each element in the resulting iterator. That is, the pattern would be readline(chomp, file), but would also allow things like readline(line -> parse(Int, line), file). (Though this pattern doesn't give you the opportunity to chomp by default.)

Apologies for the noise if the ship has already sailed on the design discussion.

@stevengj
Copy link
Member

stevengj commented Jan 9, 2017

@ararslan, a function argument would not allow the chomp transformation to be copy-free.

@nalimilan
Copy link
Member

@ararslan I think the idea is that people won't use that argument that often, it's mostly useful as a deprecation path in order to chomp by default.

@ararslan
Copy link
Member

ararslan commented Jan 9, 2017

Okay, thanks for the explanation, both of you. Sorry for the noise! (I do still think a kwarg would be a nice choice, but I will now sink back into the shadows.)

@nalimilan
Copy link
Member

The positional vs. keyword choice is a hard one. In general I'd also prefer using keyword arguments everywhere for booleans, but they come at a price currently, so they are seldom used in Base. Maybe something to think about for 2.0.

@mpastell
Copy link
Contributor Author

mpastell commented Jan 9, 2017

I would also like to have a keyword argument, but @nalimilan pointed out that it comes with a performance penalty. As readline is called for each line in a file we can't afford the overhead (and I think the arguments to readlines and readline need to match.

@kshyatt kshyatt added the io Involving the I/O subsystem: libuv, read, write, etc. label Jan 9, 2017
@mpastell
Copy link
Contributor Author

mpastell commented Jan 9, 2017

After the last two commits there is no difference in performance using chomp=false or true for IO or IOStream.

@@ -292,6 +292,51 @@ JL_DLLEXPORT jl_value_t *jl_readuntil(ios_t *s, uint8_t delim, uint8_t str)
return (jl_value_t*)a;
}

JL_DLLEXPORT jl_value_t *jl_readline(ios_t *s, uint8_t chomp)
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of near-duplicate code. Could we add an argument to jl_readuntil instead? Similarly ios_copyline does not seem totally necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I've changed it now and its much cleaner.

@mpastell
Copy link
Contributor Author

Should I go ahead deprecate the old one argument version? What's the best practice here, should I rebase (and what's the correct git command) from master to avoid conflicts as readline is used in several places?

Or is there an easy way to check for conflicts for a file before modifying it?

@tkelman
Copy link
Contributor

tkelman commented Jan 10, 2017

Since there are no conflicts here at the moment, you should be able to git fetch origin; git rebase origin/master smoothly on your local copy of this branch. That would be a good idea if you're about to modify more code. Then when you're ready to push you'll just need to push with --force over this branch on your fork to replace it with the rebased copy.

"""
function readlines(filename::AbstractString, chomp::Bool=false)
open(filename) do f
readlines(f, chomp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indented a bit too far

eltype(::Type{EachLine}) = String

readlines(s=STDIN) = collect(eachline(s))
readlines(s::IO=STDIN, chomp::Bool=false) = collect(eachline(s, chomp))
Copy link
Contributor

@tkelman tkelman Jan 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this ever make sense to send in to eachline on a non-IO first argument? If so this might be restricting vs what worked before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can tell no (there is a separate definition for strings). This also matches documentation.

@mpastell
Copy link
Contributor Author

I have no added deprecation warnings to old methods and tried to fix all warnings from master. I have done clean build locally from scratch, but tests are still running.

@@ -165,6 +165,9 @@ Compiler/Runtime improvements
Deprecated or removed
---------------------

* One argument methods to `readline`, `readlines` and `eachline` have been depracated in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"deprecated"

eachline(stream::IO) = EachLine(stream)
function eachline(filename::AbstractString)
eachline(stream::IO, chomp::Bool=true) = EachLine(stream, chomp)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single new line is probably enough, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks, fixed

@@ -821,7 +821,8 @@ size_t ios_copyuntil(ios_t *to, ios_t *from, char delim)
}
else {
size_t ntowrite = pd - (from->buf+from->bpos) + 1;
written = ios_write(to, from->buf+from->bpos, ntowrite);
size_t nchomp = ios_nchomp(from, ntowrite, chomp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd check chomp here, and not pass it at all to ios_nchomp. That can avoid a function call when chomp is false. Then you can assume chomp is true inside ios_nchomp.

@test_throws ArgumentError seek(io,0)
@test_throws ArgumentError truncate(io,0)
@test readline(io) == "whipped cream\n"
@test readline(io, false) == "whipped cream\n"
@test write(io,"pancakes\nwaffles\nblueberries\n") > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-ASCII characters, please! :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@@ -33,6 +33,9 @@ Breaking changes

This section lists changes that do not have deprecation warnings.

* `readline`, `readlines` and `eachline` return lines without line ends by default.
You can use `readline(s, false`) to get the old behavior and include EOL character(s). ([#19944]).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misplaced closing backtick: should be after the )

@@ -33,6 +33,9 @@ Breaking changes

This section lists changes that do not have deprecation warnings.

* `readline`, `readlines` and `eachline` return lines without line ends by default.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"line endings" instead of "line ends" might be clearer

@nalimilan
Copy link
Member

@stevengj Great. If you post a list of affected packages here, I can help filing issues.

@mpastell
Copy link
Contributor Author

I have updated the news according to comments. I think this should be ready to merge.

readline(stdout_read)
readline(stdout_read)
readline(stdout_read, false)
readline(stdout_read, false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it also be a good idea to test the default behavior with readline(....) without line endings or is that already being tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's tested in iobuffer and read

@@ -31,7 +31,7 @@ function prompt(msg::AbstractString; default::AbstractString="", password::Bool=
Base.getpass(msg)
else
print(msg)
chomp(readline(STDIN))
readline(STDIN, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

@@ -7,7 +7,7 @@ end

function parserow(stream::IO)
withstream(stream) do
line = readline(stream) |> chomp
line = readline(stream, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

@@ -46,7 +46,7 @@ function linecontains(io::IO, chars; allow_whitespace = true,
eat = true,
allowempty = false)
start = position(io)
l = readline(io) |> chomp
l = readline(io, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

@@ -99,7 +99,7 @@ function startswith(stream::IO, r::Regex; eat = true, padding = false)
@assert Base.startswith(r.pattern, "^")
start = position(stream)
padding && skipwhitespace(stream)
line = chomp(readline(stream))
line = readline(stream, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

@@ -67,7 +67,7 @@ end

function getmetabranch()
try
chomp(readline(joinpath(path(),"META_BRANCH")))
readline(joinpath(path(),"META_BRANCH"), true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

@@ -65,8 +65,7 @@ end

function read(readable::Union{IO,Base.AbstractCmd})
lines = Line[]
for line in eachline(readable)
line = chomp(line)
for line in eachline(readable, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

unmark(sock)
@test !ismarked(sock)
@test_throws ArgumentError reset(sock)
@test !unmark(sock)
@test readline(sock) == "Goodbye, world...\n"
@test readline(sock, true) == "Goodbye, world..."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe leave off true since it's now default?

@StefanKarpinski
Copy link
Sponsor Member

One more bit of bikeshedding. How do people feel about chomp being a keyword parameter rather than a positional one? I guess there's a bit of a potential performance issue, but since we're doing I/O anyway, I feel like this might be an ok situation in which to use a keyword and it would be more self-documenting in calling code. Who knows what readline(io, false) means, but readline(io, chomp=false) is pretty self-explanatory.

@nalimilan
Copy link
Member

We touched this above. Overall I prefer keyword arguments in many cases, but they are rarely used in the API currently anyway. In the present case, the performance impact could be significant if one calls readline in a loop instead of using eachline?

@JeffBezanson
Copy link
Sponsor Member

Would be good to do that performance experiment.

@stevengj
Copy link
Member

stevengj commented Jan 14, 2017

As a worst case, I tried calling readline on an IO buffer with 10000 lines of "a" (i.e., one character each). This maximizes the overhead of keywords etcetera since the reads are so cheap.

buf = IOBuffer(join(["a" for i = 1:10000], "\n"))
function foo(io)
    len = 0
    while !eof(io)
        len += sizeof(readline(io))
    end
    return len
end
@benchmark foo($buf)
buf = IOBuffer(join(["a" for i = 1:10000], "\n"))
myreadline(io::IO; chomp=false) = chomp ? chomp(readline(io)) : readline(io)
function bar(io)
    len = 0
    while !eof(io)
        len += sizeof(myreadline(io))
    end
    return len
end
@benchmark bar($buf)

I get 6ns for foo (no keywords) and 7ns for bar (chomp keyword and an extra branch). A 16% overhead doesn't seem too bad, considering that in most realistic cases the reads will be much more expensive and hence the overhead will be much less.

@stevengj
Copy link
Member

stevengj commented Jan 14, 2017

No, wait, that benchmark isn't right. The 6ns time is for subsequent benchmark runs when the buffer is empty. (6ns is obviously too small a time to read 10^4 lines!)

@stevengj
Copy link
Member

Okay, here is a correct benchmark:

@benchmark foo(b) setup=(b=IOBuffer(join(["a" for i = 1:10000], "\n")))
@benchmark bar(b) setup=(b=IOBuffer(join(["a" for i = 1:10000], "\n")))

The minimum time for foo is 2.82µs, vs. 51.58µs for bar. So, the overhead of the keyword arg is pretty big (albeit in a pessimistic worst case).

@StefanKarpinski
Copy link
Sponsor Member

The call to chomp in myreadline should fail since chomp is a Boolean, not a function.

@stevengj
Copy link
Member

Oh right. It doesn't fail because I'm calling it with chomp=false, though

@mpastell
Copy link
Contributor Author

Given the overhead I also think its better to leave it as positional argument. I have now cleaned the code as suggested in the last review by removing extra true argument from calls to readline.

@stevengj
Copy link
Member

stevengj commented Jan 15, 2017

I realized that I forgot to give a type to the keyword argument (since type specialization does not occur on keyword args, I think?). I changed the definition to

myreadline(io::IO; c::Bool=false) = c ? chomp(readline(io)) : readline(io)

and got:

julia> @benchmark foo(b) setup=(b=IOBuffer(join(["a" for i = 1:10000], "\n")))

BenchmarkTools.Trial: 
  samples:          4415
  evals/sample:     186
  time tolerance:   5.00%
  memory tolerance: 1.00%
  memory estimate:  9.24 kb
  allocs estimate:  107
  minimum time:     2.92 μs (0.00% GC)
  median time:      3.12 μs (0.00% GC)
  mean time:        3.49 μs (9.95% GC)
  maximum time:     12.20 μs (59.94% GC)

julia> @benchmark bar(b) setup=(b=IOBuffer(join(["a" for i = 1:10000], "\n")))
BenchmarkTools.Trial: 
  samples:          4246
  evals/sample:     175
  time tolerance:   5.00%
  memory tolerance: 1.00%
  memory estimate:  9.82 kb
  allocs estimate:  114
  minimum time:     3.35 μs (0.00% GC)
  median time:      3.58 μs (0.00% GC)
  mean time:        4.03 μs (9.42% GC)
  maximum time:     15.24 μs (61.28% GC)

i.e. a 15% overhead. Since this case (10000 lines in a memory buffer, with 1 char per line) is so pessimistic, it seems like a keyword argument will be fine.

@nalimilan
Copy link
Member

@stevengj I think your benchmark is not completely correct, since foo should call a function similar to myreadline but with a positional chomp argument for comparability:

myreadline2(io::IO, c::Bool=false) = c ? chomp(readline(io)) : readline(io)
function foo(io)
    len = 0
    while !eof(io)
        len += sizeof(myreadline2(io))
    end
    return len
end

With this, I get an even smaller penalty due to the keyword argument: 5%.

So +1 for the keyword argument. But let's make sure that's the final decision before asking @mpastell to update the PR once again.

@nalimilan nalimilan added this to the 0.6.0 milestone Jan 19, 2017
@nalimilan
Copy link
Member

Adding a 0.6 milestone so that the decision is made before the feature freeze.

@mpastell Since everybody seems to be in favor of a keyword argument, maybe it's worth updating the PR to use it so that it can be merged immediately if the consensus is confirmed.

@StefanKarpinski
Copy link
Sponsor Member

I'm in favor of the keyword argument and then merging this as a breaking change.

@StefanKarpinski
Copy link
Sponsor Member

I'm going to go ahead and make a PR squashing these changes into a single commit and then changing chomp to a keyword argument.

@StefanKarpinski
Copy link
Sponsor Member

Superseded by #20203.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
io Involving the I/O subsystem: libuv, read, write, etc.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants