Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

Implementation of raw_input() #233

Merged
merged 15 commits into from
Feb 18, 2017
Merged

Implementation of raw_input() #233

merged 15 commits into from
Feb 18, 2017

Conversation

corona10
Copy link
Contributor

@corona10 corona10 commented Feb 3, 2017

I implemented a simplified version of raw_input().

Please review on focusing these.
First, It might be not 100% compatible with CPython and pypy's raw_input().
and also need more implementation to handle error cases.

Second, I need some guide for writing unit test codes to check it is well working for raw_input().
I don't know how to test user's input from keyboard.

Third, prompt printing is slower when running with go run *.go so if you have solutions then please give me a feedback.

Related PR= #247

Copy link
Contributor

@trotterdylan trotterdylan left a comment

Choose a reason for hiding this comment

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

As far as unit tests go, it should be pretty straightforward to stub out os.Stdin with a different File object and then read from that object. See the Print() tests to understand what I mean.

Re: performance: My guess is that go run is compiling a bunch of stuff before actually running the program. go run and go test don't do much caching of compiled artifacts between runs so that's probably what's causing the delay.

}

fin := os.Stdin
fout := os.Stdout
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than writing to Stdout directly, you could use the Print() function: Print(f, args, false)

prompt, _ := ToStr(f, args[0])
fmt.Fprint(fout, prompt.Value())
}
s, _, _ := in.ReadLine()
Copy link
Contributor

Choose a reason for hiding this comment

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

ReadLine() isn't ideal here because it can return with less than a full line. I think the right thing to do is probably use the grumpy.File object representing sys.stdin and call readLine() on it. But there's no clear way to get the sys module from here.

One possible solution that would be to create Stdin, Stdout and Stderr variables in the grumpy package that wrap os.Stdin/out/err with a grumpy.File object, and then have the sys module expose those instead of creating new files. The grumpy.Print() function could then use the new Stdout object. This isn't perfect but I think it is an improvement over what we have today.

s, _, _ := in.ReadLine()

// Todo: Should implement more cases of error.
pySsizeTmax := ((^uint(0)) - 1) >> 1
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that if len(s) were ever > than this value that Go would panic so don't bother checking this.

return nil, f.RaiseType(OverflowErrorType, msg)
}

return NewStr(string(s[0:size])).ToObject(), nil
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this different than NewStr(s)?

@corona10
Copy link
Contributor Author

corona10 commented Feb 5, 2017

@trotterdylan Thank you for great and awesome review. I will try to fix it.

@corona10 corona10 changed the title Implementation of raw_input(). [WIP]Implementation of raw_input(). Feb 5, 2017
@corona10 corona10 changed the title [WIP]Implementation of raw_input(). Implementation of raw_input() Feb 6, 2017
@corona10
Copy link
Contributor Author

corona10 commented Feb 6, 2017

@trotterdylan Updated! I fixed codes as you commented. Actually, about using grumpy.File object is not 100% same as you intended. I found the simpler way.
And I add unit test code, but it should be improvement later time. I wish other guys could improve this unit test to check user's input and function's output. Unfortunately, In now time, this is out of my ability.

PTAL

pyPrint(f, args, "", "", stdout)
}

line, _ := stdin.readLine(MaxInt)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I was wrong about implementing it this way :(

The trouble is that the C implementation uses fgets on the underlying C file pointer. fgets reads until it reaches \n but readLine will stop when it reaches \r and will convert that to a \n.

Probably the simplest thing to do here is to call stdin.reader.ReadString("\n") instead. This would mean you'd have to handle the EOF error case, however.

pyPrint(f, args, "", "", stdout)
}

line, _ := stdin.readLine(MaxInt)
Copy link
Contributor

Choose a reason for hiding this comment

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

You must handle the error case. It's pretty simple to do:

if err != nil {
  return nil, f.RaiseType(IOErrorType, err.Error())
}

}

stdout := os.Stdout
stdin := NewFileFromFD(os.Stdin.Fd())
Copy link
Contributor

Choose a reason for hiding this comment

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

The trouble with creating a new File wrapping os.Stdin is that it will have a different buffered reader than the one held by sys.stdin. This is especially problematic because this File object is cleaned up after builtinRawInput() returns, meaning that whatever buffered data it held just disappears.

This goes back to my suggestion of having grumpy.Stdin/out/err variables that get used by this function and having sys.stdin/out/err be aliases for those variables:

# sys.py
from __go__.grumpy import Stdin as stdin, Stdout as stdout, Stderr as stderr

This way any buffering done for one call will be used by subsequent calls.

@@ -422,3 +422,24 @@ func TestBuiltinSetAttr(t *testing.T) {
}
}
}

// TODO: Improvment unit tests codes into catch user's input and output.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd want to see some additional tests that exercise the whole builtinRawInput() function before I approve this PR. It's important that new code is fully tested because the probability that somebody retroactively adds tests is low.

@corona10
Copy link
Contributor Author

corona10 commented Feb 7, 2017

@trotterdylan A lot of stuff!! I will do it as your comment. Thanks

@corona10 corona10 changed the title Implementation of raw_input() [WIP]Implementation of raw_input() Feb 7, 2017
@corona10 corona10 changed the title [WIP]Implementation of raw_input() Implementation of raw_input() Feb 9, 2017
@corona10
Copy link
Contributor Author

corona10 commented Feb 9, 2017

@trotterdylan Updated!! Please take a look.

return nil, f.RaiseType(TypeErrorType, msg)
}

osStdout := os.Stdout
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the new grumpy.Stdout variable for output. This will require modifying pyPrint(), Print(), etc. to take a *grumpy.File instead of *os.File. Might be better to add Stdin/out/err and change the print behavior in a separate PR and once that's merged, continue with this one.

// Create a fake Stdin for input test.
stdinFile, _ := ioutil.TempFile(os.TempDir(), "prefix")
stdinFile.Write([]byte("hello grumpy\n"))
stdinFile.Close()
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need to reopen stdinFile. You should be able to read what you've written. You may need to Sync() (and possibly Seek() to the beginning, but I don't think so).

func TestRawInput(t *testing.T) {
fun := newBuiltinFunction("TestRawInput", func(f *Frame, args Args, _ KWArgs) (*Object, *BaseException) {
// Create a fake Stdin for input test.
stdinFile, _ := ioutil.TempFile(os.TempDir(), "prefix")
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to check errors for these io operations.

func TestRawInput(t *testing.T) {
fun := newBuiltinFunction("TestRawInput", func(f *Frame, args Args, _ KWArgs) (*Object, *BaseException) {
// Create a fake Stdin for input test.
stdinFile, _ := ioutil.TempFile(os.TempDir(), "prefix")
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if it'd be easier, but you could also use a Pipe() instead of a temp file for stdin, similar to what you've done for stdout. Something like this:

stdinFile, w := os.Pipe()
go func() {
  w.Write([]byte("hello grumpy\n"))
  w.Close()
}()

You need to do the write asynchronously this way in case the Write() call blocks waiting for a Read() on the other end of the pipe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool~! I will try it!!

os.Stdout = oldStdout
Stdin = oldStdin
stdinFile.Close()
os.Remove(stdinFile.Name())
Copy link
Contributor

Choose a reason for hiding this comment

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

Move defer os.Remove(stdinFile.Name()) up to where you create the file so that an early return won't leave the tempfile lying around.

if raised != nil {
return nil, raised
}
result := newTestTuple(input, output.String()).ToObject()
Copy link
Contributor

Choose a reason for hiding this comment

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

return newTestTuple(input, output.String()).ToObject(), nil

@corona10 corona10 changed the title Implementation of raw_input() [WIP] Implementation of raw_input() Feb 12, 2017
@corona10 corona10 changed the title [WIP] Implementation of raw_input() Implementation of raw_input() Feb 13, 2017
@corona10
Copy link
Contributor Author

@trotterdylan Updated. PTAL

}

if len(args) == 1 {
pyPrint(f, args, "", "", Stdout)
Copy link
Contributor

Choose a reason for hiding this comment

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

Check the resulting exception

}

go func() {
defer w.Close()
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't bother defering, just Close after the Write

go func() {
defer w.Close()
w.Write([]byte("hello grumpy\n"))
stdinFile.Sync()
Copy link
Contributor

Choose a reason for hiding this comment

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

It's w that needs to Sync, but closing it will do the same thing.

}()

// Create a fake Stdout for output test.
r, stdoutFile, err := os.Pipe()
Copy link
Contributor

Choose a reason for hiding this comment

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

Any way we can use captureStdout for this part?

Copy link
Contributor Author

@corona10 corona10 Feb 14, 2017

Choose a reason for hiding this comment

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

@trotterdylan I want to do by the same way you mentioned. But in my opinion, until we refactoring captureStdout, I think not possible right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we could change this part for general functions. It might be possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

What specifically needs to be generalized? Any inputs or outputs can made part of the closure for the passed function and read or written inside the function call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@trotterdylan In this case, builtinRawInput() will return *Object and *BaseException, but fn in captureStdout want to return only *BaseException. It will works for any function I will pass?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm probably oversimplifying, but would something like this work?

var input *Object
raised := captureStdout(func() *BaseException {
  input = builtinRawInput(...)
})
if raised != nil {
  ...
}
// validate input

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@trotterdylan Awesome! You are right!! Thank you for guiding me. 👍

var raised *BaseException

go func() {
defer close(done)
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to defer either of these, just call them normally below

}()
done := make(chan struct{})
var input *Object
var output bytes.Buffer
Copy link
Contributor

Choose a reason for hiding this comment

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

Declare this where it's used down below when passed to Copy

@corona10
Copy link
Contributor Author

@trotterdylan Updated.

p.s Can I get good materials to know when to use defer? I'm just using it as finally statement.

@trotterdylan
Copy link
Contributor

@corona10 re: defer: It can be used like finally. But, like finally, it's only really useful if there are multiple possible paths for exiting the function or an exception (panic in Go) is raised. In this case, a panic is a fatal error so it doesn't really matter if we close things. The only time it's really important to run some code in the case of panic is if the panic may be handled somewhere (for the most part in the Grumpy code, we assume panic is fatal) or if you need to release some resource even in the event of a panic. Sometimes mutex falls into this category because it can otherwise cause a deadlock if not released. It's also good to guarantee that temp files are cleaned up. But otherwise, the possibility of panic is not usually a good reason to defer.

@corona10
Copy link
Contributor Author

@trotterdylan Thank you for review and great guide! I updated it and please take a look. Thanks to you, I learned about how Go works.

Copy link
Contributor

@trotterdylan trotterdylan left a comment

Choose a reason for hiding this comment

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

This is looking great. The main thing now is to add a few more test cases.


go func() {
w.Write([]byte("hello grumpy\n"))
w.Sync()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you want to close w here since that's the end of stdin.

}).ToObject()
cases := []invokeTestCase{
{args: wrapArgs(), want: newTestTuple("hello grumpy", "").ToObject()},
{args: wrapArgs("abc"), want: newTestTuple("hello grumpy", "abc").ToObject()},
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's a few more things that need testing:

  • Multiline prompt parameter: make sure all string lines are printed
  • Multiple lines on stdin: only the first line should be returned by the raw_input call
  • An empty stdin should raise EOFError:
$ python -c 'raw_input()' < /dev/null
Traceback (most recent call last):
  File "<string>", line 1, in <module>
EOFError: EOF when reading a line

The last two test cases can be supported by having fun take a parameter that is the string to put on stdin.

1) Add more testcases.
2) Close w since that's the end of stdin.
3) Add EOFErrorType.
@corona10
Copy link
Contributor Author

@trotterdylan Updated!! PTAL

@@ -425,6 +425,69 @@ func TestBuiltinSetAttr(t *testing.T) {
}
}

func TestRawInput(t *testing.T) {
inputTest := func(s string, cases []invokeTestCase) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This organization seems like a reasonable way to support an additional parameter, but it's a little unorthodox. The way I've usually done it is something like this:

fun := wrapFuncForTest(func(f *Frame, s string, args ...*Object) (*Object, *BaseException) {
  ...
  go func() {
    w.Write([]byte(s))
    w.Close()
  }()
  ...
    in, raised := builtinRawInput(f, args, nil)
  ...
})
...
    {args: wrapArgs("hello\n", "show"), want: newTestTuple("hello", "show").ToObject()},

Copy link
Contributor

@trotterdylan trotterdylan left a comment

Choose a reason for hiding this comment

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

This is great! Thanks for all the iterations. I'm really happy with the end result. Merging.

@trotterdylan trotterdylan merged commit 498b88e into google:master Feb 18, 2017
@corona10
Copy link
Contributor Author

@trotterdylan Thank you for reviewing long days :-)

@corona10 corona10 deleted the raw_input branch February 19, 2017 02:45
@pombredanne
Copy link

@corona10 very nice :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants