-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add StringIO newline tracking #8539
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4187,6 +4187,37 @@ mod _io { | |
| } | ||
| } | ||
|
|
||
| impl SeenNewline { | ||
| fn observe(&mut self, text: &Wtf8) { | ||
| let bytes = text.as_bytes(); | ||
| let mut matches = memchr::memchr2_iter(b'\r', b'\n', bytes); | ||
| while !self.is_all() { | ||
| let Some(i) = matches.next() else { break }; | ||
| match bytes[i] { | ||
| b'\n' => self.insert(Self::LF), | ||
| _ if bytes.get(i + 1) == Some(&b'\n') => { | ||
| matches.next(); | ||
| self.insert(Self::CRLF); | ||
| } | ||
| _ => self.insert(Self::CR), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn to_pyobject(self, vm: &VirtualMachine) -> PyObjectRef { | ||
| match self.bits() { | ||
| 1 => "\n".to_pyobject(vm), | ||
| 2 => "\r".to_pyobject(vm), | ||
| 3 => ("\r", "\n").to_pyobject(vm), | ||
| 4 => "\r\n".to_pyobject(vm), | ||
| 5 => ("\n", "\r\n").to_pyobject(vm), | ||
| 6 => ("\r", "\r\n").to_pyobject(vm), | ||
| 7 => ("\r", "\n", "\r\n").to_pyobject(vm), | ||
| _ => vm.ctx.none(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl DefaultConstructor for IncrementalNewlineDecoder {} | ||
|
|
||
| #[derive(FromArgs)] | ||
|
|
@@ -4278,16 +4309,7 @@ mod _io { | |
| #[pygetset] | ||
| fn newlines(&self, vm: &VirtualMachine) -> PyResult { | ||
| let data = self.lock(vm)?; | ||
| Ok(match data.seennl.bits() { | ||
| 1 => "\n".to_pyobject(vm), | ||
| 2 => "\r".to_pyobject(vm), | ||
| 3 => ("\r", "\n").to_pyobject(vm), | ||
| 4 => "\r\n".to_pyobject(vm), | ||
| 5 => ("\n", "\r\n").to_pyobject(vm), | ||
| 6 => ("\r", "\r\n").to_pyobject(vm), | ||
| 7 => ("\r", "\n", "\r\n").to_pyobject(vm), | ||
| _ => vm.ctx.none(), | ||
| }) | ||
| Ok(data.seennl.to_pyobject(vm)) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -4334,20 +4356,7 @@ mod _io { | |
| self.seennl.insert(SeenNewline::LF); | ||
| } | ||
| } else if !self.translate { | ||
| let output = output.as_bytes(); | ||
| let mut matches = memchr::memchr2_iter(b'\r', b'\n', output); | ||
| while !self.seennl.is_all() { | ||
| let Some(i) = matches.next() else { break }; | ||
| match output[i] { | ||
| b'\n' => self.seennl.insert(SeenNewline::LF), | ||
| // if c isn't \n, it can only be \r | ||
| _ if output.get(i + 1) == Some(&b'\n') => { | ||
| matches.next(); | ||
| self.seennl.insert(SeenNewline::CRLF); | ||
| } | ||
| _ => self.seennl.insert(SeenNewline::CR), | ||
| } | ||
| } | ||
| self.seennl.observe(&output); | ||
| } else { | ||
| let bytes = output.as_bytes(); | ||
| let mut matches = memchr::memchr2_iter(b'\r', b'\n', bytes); | ||
|
|
@@ -4390,6 +4399,7 @@ mod _io { | |
| _base: _TextIOBase, | ||
| buffer: PyRwLock<BufferedIO>, | ||
| newline: AtomicCell<Newlines>, | ||
| seennl: AtomicCell<SeenNewline>, | ||
| closed: AtomicCell<bool>, | ||
| } | ||
|
|
||
|
|
@@ -4410,6 +4420,7 @@ mod _io { | |
| _base: Default::default(), | ||
| buffer: PyRwLock::new(BufferedIO::new(Cursor::new(Vec::new()))), | ||
| newline: AtomicCell::new(Newlines::Lf), | ||
| seennl: AtomicCell::new(SeenNewline::empty()), | ||
| closed: AtomicCell::new(false), | ||
| }) | ||
| } | ||
|
|
@@ -4428,11 +4439,15 @@ mod _io { | |
| OptionalArg::Present(None) => Newlines::Universal, | ||
| OptionalArg::Present(Some(newline)) => newline, | ||
| }; | ||
| let raw_bytes = object.flatten().map_or_else(Vec::new, |v| { | ||
| let object = object.flatten(); | ||
| let raw_bytes = object.as_ref().map_or_else(Vec::new, |v| { | ||
| Self::translate_newlines(v.as_wtf8(), newline).into_bytes() | ||
| }); | ||
| *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); | ||
| zelf.newline.store(newline); | ||
| if let Some(object) = object { | ||
| zelf.observe_newlines(object.as_wtf8(), newline); | ||
| } | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
@@ -4457,6 +4472,14 @@ mod _io { | |
| } | ||
| } | ||
|
|
||
| fn observe_newlines(&self, data: &Wtf8, newline: Newlines) { | ||
| if matches!(newline, Newlines::Universal | Newlines::Passthrough) { | ||
| let mut seennl = self.seennl.load(); | ||
| seennl.observe(data); | ||
| self.seennl.store(seennl); | ||
| } | ||
| } | ||
|
|
||
| fn text(bytes: &[u8]) -> &Wtf8 { | ||
| // SAFETY: StringIO is populated only from PyStr values, which are valid WTF-8. | ||
| unsafe { Wtf8::from_bytes_unchecked(bytes) } | ||
|
|
@@ -4509,6 +4532,11 @@ mod _io { | |
| self.closed.load() | ||
| } | ||
|
|
||
| #[pygetset] | ||
| fn newlines(&self, vm: &VirtualMachine) -> PyObjectRef { | ||
| self.seennl.load().to_pyobject(vm) | ||
| } | ||
|
Comment on lines
+4535
to
+4538
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
PYTHON_BIN="${PYTHON_BIN:-python3}"
"${PYTHON_BIN}" - <<'PY'
import io
stream = io.StringIO("x\n", newline=None)
stream.close()
try:
stream.newlines
except ValueError:
pass
else:
raise AssertionError("newlines must fail on a closed StringIO")
PYRepository: RustPython/RustPython Length of output: 159 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- target implementation ---'
sed -n '4480,4575p' crates/vm/src/stdlib/_io.rs
printf '%s\n' '--- StringIO fields and close handling ---'
rg -n -A8 -B8 'struct StringIO|closed:|fn close|io_closed_error|fn newlines' crates/vm/src/stdlib/_io.rs
printf '%s\n' '--- related closed-state getters ---'
rg -n -A12 -B4 'closed\.load|io_closed_error\(vm\)' crates/vm/src/stdlib/_io.rs | head -240Repository: RustPython/RustPython Length of output: 33241 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import io
import sys
print("implementation:", sys.implementation.name)
stream = io.StringIO("x\n", newline=None)
assert stream.newlines == "\n"
stream.close()
try:
stream.newlines
except ValueError as exc:
print("closed access:", type(exc).__name__, str(exc))
else:
raise AssertionError("StringIO.newlines must raise ValueError after close")
PYRepository: RustPython/RustPython Length of output: 238 Raise
🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
| #[pymethod] | ||
| fn close(&self) { | ||
| self.closed.store(true); | ||
|
|
@@ -4517,8 +4545,11 @@ mod _io { | |
| // write string to underlying vector | ||
| #[pymethod] | ||
| fn write(&self, data: PyStrRef, vm: &VirtualMachine) -> PyResult<u64> { | ||
| let bytes = Self::translate_newlines(data.as_wtf8(), self.newline.load()).into_bytes(); | ||
| self.buffer(vm)? | ||
| let newline = self.newline.load(); | ||
| let bytes = Self::translate_newlines(data.as_wtf8(), newline).into_bytes(); | ||
| let mut buffer = self.buffer(vm)?; | ||
| self.observe_newlines(data.as_wtf8(), newline); | ||
| buffer | ||
| .write(&bytes) | ||
| .ok_or_else(|| vm.new_type_error("Error Writing String"))?; | ||
| Ok(data.char_len() as u64) | ||
|
|
@@ -4678,6 +4709,11 @@ mod _io { | |
| .map_err(|err| os_err(vm, err))?; | ||
| drop(buffer); | ||
| zelf.newline.store(newline); | ||
| let mut seennl = SeenNewline::empty(); | ||
| if matches!(newline, Newlines::Universal | Newlines::Passthrough) { | ||
| seennl.observe(content.as_wtf8()); | ||
| } | ||
| zelf.seennl.store(seennl); | ||
|
|
||
| // Set __dict__ if provided | ||
| if !vm.is_none(dict) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 159
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 32229
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 333
Reset
seennlwhen reinitializingStringIO.When
StringIO.__init__runs on an existing instance, it replaces the buffer but retains prior newline bits. Resetseennlbefore observing the new content.Proposed fix
*zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); zelf.newline.store(newline); + zelf.seennl.store(SeenNewline::empty()); if let Some(object) = object { zelf.observe_newlines(object.as_wtf8(), newline); }📝 Committable suggestion
🤖 Prompt for AI Agents
Source: MCP tools