Skip to content

display images, HTML... and MIME rendering #105

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

Merged
merged 14 commits into from
Jun 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
gophernotes
.ipynb_checkpoints
Untitled.ipynb
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Fransesc Campoy <campoy@golang.org>
Harry Moreno <harry@capsulerx.com>
Josh Cheek <josh.cheek@gmail.com>
Kevin Burke <kev@inburke.com>
Massimiliano Ghilardi <massimiliano.ghilardi@gmail.com>
Matthew Steffen <matt@pachyderm.io>
Sebastien Binet <binet@cern.ch>
Spencer Park <spinnr95@gmail.com>
Expand Down
38 changes: 16 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

`gophernotes` is a Go kernel for [Jupyter](http://jupyter.org/) notebooks and [nteract](https://nteract.io/). It lets you use Go interactively in a browser-based notebook or desktop app. Use `gophernotes` to create and share documents that contain live Go code, equations, visualizations and explanatory text. These notebooks, with the live Go code, can then be shared with others via email, Dropbox, GitHub and the [Jupyter Notebook Viewer](http://nbviewer.jupyter.org/). Go forth and do data science, or anything else interesting, with Go notebooks!

**Acknowledgements** - This project utilizes a Go interpreter called [gomacro](https://github.com/cosmos72/gomacro) under the hood to evaluate Go code interactively. The gophernotes logo was designed by the brilliant [Marcus Olsson](https://github.com/marcusolsson) and was inspired by Renee French's original Go Gopher design.
**Acknowledgements** - This project utilizes a Go interpreter called [gomacro](https://github.com/cosmos72/gomacro) under the hood to evaluate Go code interactively. The gophernotes logo was designed by the brilliant [Marcus Olsson](https://github.com/marcusolsson) and was inspired by Renee French's original Go Gopher design.

- [Examples](#examples)
- Install gophernotes:
Expand All @@ -19,9 +19,9 @@

## Examples

### Jupyter Notebook:
### Jupyter Notebook:

![](files/jupyter.gif)
![](files/jupyter.gif)

### nteract:

Expand All @@ -46,7 +46,7 @@
```sh
$ go get -u github.com/gopherdata/gophernotes
$ mkdir -p ~/.local/share/jupyter/kernels/gophernotes
$ cp $GOPATH/src/github.com/gopherdata/gophernotes/kernel/* ~/.local/share/jupyter/kernels/gophernotes
$ cp $GOPATH/src/github.com/gopherdata/gophernotes/kernel/* ~/.local/share/jupyter/kernels/gophernotes
```

To confirm that the `gophernotes` binary is installed and in your PATH, you should see the following when running `gophernotes` directly:
Expand All @@ -57,7 +57,7 @@ $ gophernotes
```

**Note** - if you have the `JUPYTER_PATH` environmental variable set or if you are using an older version of Jupyter, you may need to copy this kernel config to another directory. You can check which directories will be searched by executing:

```sh
$ jupyter --data-dir
```
Expand All @@ -80,7 +80,7 @@ $ gophernotes
```

**Note** - if you have the `JUPYTER_PATH` environmental variable set or if you are using an older version of Jupyter, you may need to copy this kernel config to another directory. You can check which directories will be searched by executing:

```sh
$ jupyter --data-dir
```
Expand All @@ -102,12 +102,12 @@ Then:
REM Download w/o building.
go get -d github.com/gopherdata/gophernotes
cd %GOPATH%\src\github.com\gopherdata\gophernotes\zmq-win

REM Build x64 version.
build.bat amd64
move gophernotes.exe %GOPATH%\bin
copy lib-amd64\libzmq.dll %GOPATH%\bin

REM Build x86 version.
build.bat 386
move gophernotes.exe %GOPATH%\bin
Expand All @@ -120,9 +120,9 @@ Then:
mkdir %APPDATA%\jupyter\kernels\gophernotes
xcopy %GOPATH%\src\github.com\gopherdata\gophernotes\kernel %APPDATA%\jupyter\kernels\gophernotes /s
```

Note, if you have the `JUPYTER_PATH` environmental variable set or if you are using an older version of Jupyter, you may need to copy this kernel config to another directory. You can check which directories will be searched by executing:

```
jupyter --data-dir
```
Expand All @@ -143,7 +143,7 @@ Then:

### Docker

You can try out or run Jupyter + gophernotes without installing anything using Docker. To run a Go notebook that only needs things from the standard library, run:
You can try out or run Jupyter + gophernotes without installing anything using Docker. To run a Go notebook that only needs things from the standard library, run:

```
$ docker run -it -p 8888:8888 gopherdata/gophernotes
Expand All @@ -159,7 +159,7 @@ In either case, running this command should output a link that you can follow to

```
$ docker run -it -p 8888:8888 -v /path/to/local/notebooks:/path/to/notebooks/in/docker gopherdata/gophernotes
```
```

## Getting Started

Expand Down Expand Up @@ -188,16 +188,10 @@ $ docker run -it -p 8888:8888 -v /path/to/local/notebooks:/path/to/notebooks/in/
gophernotes uses [gomacro](https://github.com/cosmos72/gomacro) under the hood to evaluate Go code interactively. You can evaluate most any Go code with gomacro, but there are some limitation, which are discussed in further detail [here](https://github.com/cosmos72/gomacro#current-status). Most noteably, gophernotes does NOT support:

- third party packages when running natively on Mac and Windows - This is a current limitation of the Go `plugin` package.
- unexported struct fields
- interfaces - They can be declared, but nothing more: there is no way to implement them or call their methods
- extracting methods from types - For example time.Duration.String should return a func(time.Duration) string but currently gives an error. Instead extracting methods from objects is supported: time.Duration(1s).String correctly returns a func() string
- goto
- named return values
- named imports like:

```
import tf "github.com/tensorflow/tensorflow/tensorflow/go"
```
- some corner cases on interpreted interfaces, as interface -&gt; interface type switch and type assertion, are not implemented yet.
- conversion from typed constant to interpreted interface is not implemented. Workaround: assign the constant to a variable, then convert the variable to the interpreted interface type.
- goto is only partially implemented.
- out-of-order code in the same cell is supported, but not heavily tested. It has some known limitations for composite literals.

## Troubleshooting

Expand Down
152 changes: 152 additions & 0 deletions display.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package main

import (
"errors"
"fmt"
r "reflect"
"strings"

"github.com/cosmos72/gomacro/imports"
)

// Support an interface similar - but not identical - to the IPython (canonical Jupyter kernel).
// See http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display
// for a good overview of the support types. Note: This is missing _repr_markdown_ and _repr_javascript_.

const (
MIMETypeHTML = "text/html"
MIMETypeJavaScript = "application/javascript"
MIMETypeJPEG = "image/jpeg"
MIMETypeJSON = "application/json"
MIMETypeLatex = "text/latex"
MIMETypeMarkdown = "text/markdown"
MIMETypePNG = "image/png"
MIMETypePDF = "application/pdf"
MIMETypeSVG = "image/svg+xml"
)

// injected as placeholder in the interpreter, it's then replaced at runtime
// by a closure that knows how to talk with Jupyter
func stubDisplay(Data) error {
return errors.New("cannot display: connection with Jupyter not available")
}

// TODO handle the metadata

func MakeData(mimeType string, data interface{}) Data {
return Data{
Data: BundledMIMEData{
"text/plain": fmt.Sprint(data),
mimeType: data,
},
}
}

func MakeData3(mimeType string, plaintext string, data interface{}) Data {
return Data{
Data: BundledMIMEData{
"text/plain": plaintext,
mimeType: data,
},
}
}

func Bytes(mimeType string, bytes []byte) Data {
return MakeData3(mimeType, mimeType, bytes)
}

func HTML(html string) Data {
return MakeData(MIMETypeHTML, html)
}

func JSON(json map[string]interface{}) Data {
return MakeData(MIMETypeJSON, json)
}

func JavaScript(javascript string) Data {
return MakeData(MIMETypeJavaScript, javascript)
}

func JPEG(jpeg []byte) Data {
return MakeData3(MIMETypeJPEG, "jpeg image", jpeg) // []byte are encoded as base64 by the marshaller
}

func Latex(latex string) Data {
return MakeData3(MIMETypeLatex, latex, "$"+strings.Trim(latex, "$")+"$")
}

func Markdown(markdown string) Data {
return MakeData(MIMETypeMarkdown, markdown)
}

func Math(latex string) Data {
return MakeData3(MIMETypeLatex, latex, "$$"+strings.Trim(latex, "$")+"$$")
}

func PDF(pdf []byte) Data {
return MakeData3(MIMETypePDF, "pdf document", pdf) // []byte are encoded as base64 by the marshaller
}

func PNG(png []byte) Data {
return MakeData3(MIMETypePNG, "png image", png) // []byte are encoded as base64 by the marshaller
}

func String(mimeType string, s string) Data {
return MakeData(mimeType, s)
}

func SVG(svg string) Data {
return MakeData(MIMETypeSVG, svg)
}

// MIME encapsulates the data and metadata into a Data.
// The 'data' map is expected to contain at least one {key,value} pair,
// with value being a string, []byte or some other JSON serializable representation,
// and key equal to the MIME type of such value.
// The exact structure of value is determined by what the frontend expects.
// Some easier-to-use functions for common formats supported by the Jupyter frontend
// are provided by the various functions above.
func MIME(data, metadata map[string]interface{}) Data {
return Data{data, metadata, nil}
}

// prepare imports.Package for interpreted code
var display = imports.Package{
Binds: map[string]r.Value{
"Bytes": r.ValueOf(Bytes),
"HTML": r.ValueOf(HTML),
"Image": r.ValueOf(Image),
"JPEG": r.ValueOf(JPEG),
"JSON": r.ValueOf(JSON),
"JavaScript": r.ValueOf(JavaScript),
"Latex": r.ValueOf(Latex),
"MakeData": r.ValueOf(MakeData),
"MakeData3": r.ValueOf(MakeData3),
"Markdown": r.ValueOf(Markdown),
"Math": r.ValueOf(Math),
"MIME": r.ValueOf(MIME),
"MIMETypeHTML": r.ValueOf(MIMETypeHTML),
"MIMETypeJavaScript": r.ValueOf(MIMETypeJavaScript),
"MIMETypeJPEG": r.ValueOf(MIMETypeJPEG),
"MIMETypeJSON": r.ValueOf(MIMETypeJSON),
"MIMETypeLatex": r.ValueOf(MIMETypeLatex),
"MIMETypeMarkdown": r.ValueOf(MIMETypeMarkdown),
"MIMETypePDF": r.ValueOf(MIMETypePDF),
"MIMETypePNG": r.ValueOf(MIMETypePNG),
"MIMETypeSVG": r.ValueOf(MIMETypeSVG),
"PDF": r.ValueOf(PDF),
"PNG": r.ValueOf(PNG),
"String": r.ValueOf(String),
"SVG": r.ValueOf(SVG),
},
Types: map[string]r.Type{
"BundledMIMEData": r.TypeOf((*BundledMIMEData)(nil)).Elem(),
"Data": r.TypeOf((*Data)(nil)).Elem(),
},
}

// allow importing "display" and "github.com/gopherdata/gophernotes" packages
func init() {
imports.Packages["display"] = display
imports.Packages["github.com/gopherdata/gophernotes"] = display
}
103 changes: 103 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"bytes"
"fmt"
"image"
"image/png"
)

// Image converts an image.Image to DisplayData containing PNG []byte,
// or to DisplayData containing error if the conversion fails
func Image(img image.Image) Data {
data, err := image0(img)
if err != nil {
return Data{
Data: BundledMIMEData{
"ename": "ERROR",
"evalue": err.Error(),
"traceback": nil,
"status": "error",
},
}
}
return data
}

// Image converts an image.Image to Data containing PNG []byte,
// or error if the conversion fails
func image0(img image.Image) (Data, error) {
bytes, mime, err := encodePng(img)
if err != nil {
return Data{}, err
}
return Data{
Data: BundledMIMEData{
mime: bytes,
},
Metadata: BundledMIMEData{
mime: imageMetadata(img),
},
}, nil
}

// encodePng converts an image.Image to PNG []byte
func encodePng(img image.Image) (data []byte, mime string, err error) {
var buf bytes.Buffer
err = png.Encode(&buf, img)
if err != nil {
return nil, "", err
}
return buf.Bytes(), "image/png", nil
}

// imageMetadata returns image size, represented as BundledMIMEData{"width": width, "height": height}
func imageMetadata(img image.Image) BundledMIMEData {
rect := img.Bounds()
return BundledMIMEData{
"width": rect.Dx(),
"height": rect.Dy(),
}
}

// PublishImage sends a "display_data" broadcast message for given image.Image.
func (receipt *msgReceipt) PublishImage(img image.Image) error {
data, err := image0(img)
if err != nil {
return err
}
return receipt.PublishDisplayData(data)
}

// if vals[] contain a single non-nil value which is an image.Image,
// convert it to Data and return it.
// if instead the single non-nil value is a Data, return it.
// otherwise return MakeData("text/plain", fmt.Sprint(vals...))
func renderResults(vals []interface{}) Data {
var nilcount int
var obj interface{}
for _, val := range vals {
switch val.(type) {
case image.Image, Data:
obj = val
case nil:
nilcount++
}
}
if obj != nil && nilcount == len(vals)-1 {
switch val := obj.(type) {
case image.Image:
data, err := image0(val)
if err == nil {
return data
}
case Data:
return val
}
}
if nilcount == len(vals) {
// if all values are nil, return empty Data
return Data{}
}
return MakeData("text/plain", fmt.Sprint(vals...))
}
Loading