Skip to content

Commit b739a78

Browse files
authored
Merge pull request #6 from stitchfix/py3
Add Python 3 Support
2 parents 08d7b38 + 89af4ee commit b739a78

12 files changed

Lines changed: 102 additions & 56 deletions

File tree

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ Nodebook is a plugin for Jupyter Notebook designed to enforce an ordered flow of
77

88
## Installation
99

10-
Nodebook is available on pypi and can be installed with pip:
10+
Nodebook is available on pypi and can be installed with pip. Additionally, the jupyter extension must be registered:
1111
```
1212
pip install nodebook
13+
jupyter nbextension install --py nodebook
1314
```
1415

1516
## Usage
1617

1718
To use Nodebook, add the following lines to a cell in your Jupyter notebook:
1819
```
1920
#pragma nodebook off
20-
%load_ext nodebookext
21+
%load_ext nodebook.ipython
2122
%nodebook {mode} {name}
2223
```
2324
Where `{mode}` is one of `memory` or `disk`, and `{name}` is a unique identifier for your notebook.
@@ -30,9 +31,9 @@ For additional example usage, see [nodebook_demo.ipynb](./nodebook_demo.ipynb).
3031

3132
## FAQ
3233

33-
#### Q: Does Nodebook support Python 3?
34+
#### Q: Should I use Python 2 or Python 3 with Nodebook?
3435

35-
Unfortunately, not yet. Please try it in Python 2.7.
36+
There has been an [increasing consensus](http://www.python3statement.org/) toward sunsetting support for Python 2, including in Project Jupyter. Nodebook currently supports both Python 2 and Python 3, but Python 3 is preferred.
3637

3738
#### Q: Why am I seeing "ERROR:root:Cell magic `%%execute_cell` not found."?
3839

nodebook/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
def _jupyter_nbextension_paths():
2+
"""
3+
Jupyter nbextension location
4+
"""
5+
return [dict(
6+
section="notebook",
7+
src="ipython/nbextensions",
8+
# directory in the `nbextension/` namespace
9+
dest="nodebook",
10+
# _also_ in the `nbextension/` namespace
11+
require="nodebook/nodebookext")]

nodebook/ipython/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .nodebookext import load_ipython_extension
File renamed without changes.
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import cPickle as pickle
1+
from __future__ import absolute_import
2+
import six.moves.cPickle as pickle
23
import os
34
import sys
45
import errno
@@ -92,7 +93,7 @@ def execute_cell(line, cell):
9293
def load_ipython_extension(ipython):
9394
ipython.register_magic_function(nodebook, magic_kind='line')
9495
ipython.register_magic_function(execute_cell, magic_kind='cell')
95-
ipython.run_cell_magic('javascript', '', "Jupyter.utils.load_extensions('nodebookext')")
96+
ipython.run_cell_magic('javascript', '', "Jupyter.utils.load_extensions('nodebook/nodebookext')")
9697

9798

9899
def unload_ipython_extension(ipython):

nodebook/nodebookcore.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from __future__ import absolute_import
2+
from __future__ import print_function
13
from . import pickledict
24
import ast
3-
import __builtin__
5+
import six.moves.builtins
6+
import six
47

58
INDENT = ' ' # an indent is canonically 4 spaces ;)
69

@@ -116,7 +119,7 @@ def run_node(self, node_id):
116119
res, output_objs, output_hashes = node.run(input_objs, input_hashes)
117120

118121
# update node outputs
119-
for var, val in output_objs.iteritems():
122+
for var, val in six.iteritems(output_objs):
120123
self.variables[output_hashes[var]] = val
121124
self._update_output_hashes(node, output_hashes)
122125

@@ -129,7 +132,7 @@ def _find_latest_output(self, node, var):
129132
"""
130133
# base case
131134
if node is None:
132-
if var in __builtin__.__dict__:
135+
if var in six.moves.builtins.__dict__:
133136
return None
134137
else:
135138
raise KeyError("name '%s' is not defined" % var)
@@ -141,7 +144,7 @@ def _find_latest_output(self, node, var):
141144
else:
142145
# re-run the parent if it wasn't valid
143146
# TODO: synchronize output with frontend javascript
144-
print "auto-running invalidated node N_%s (%s)" % (node.get_index() + 1, node.name)
147+
print("auto-running invalidated node N_%s (%s)" % (node.get_index() + 1, node.name))
145148
self.run_node(node.name)
146149
return self._find_latest_output(node, var)
147150
else:
@@ -153,23 +156,23 @@ def _update_output_hashes(self, node, outputs):
153156
Update node's output hashes and invalid downstream nodes that depended on their previous values
154157
"""
155158
# invalidate any any children relying on specific hash-versions of old outputs that aren't in the new outputs
156-
invalidated_outputs = set(node.outputs.iteritems()) - set(outputs.iteritems())
159+
invalidated_outputs = set(six.iteritems(node.outputs)) - set(six.iteritems(outputs))
157160
invalidated_outputs = {k: v for k, v in invalidated_outputs}
158161

159162
# also invalidate any children that rely on any version of a brand-new output, regardless of hash
160163
# TODO this is potentially overly restrictive, if, eg, a value is blindly over-written again later
161164
# TODO(con't) we should try to account for this to avoid invalidating excessively many cells
162-
new_outputs = set(outputs.iteritems()) - set(node.outputs.iteritems())
165+
new_outputs = set(six.iteritems(outputs)) - set(six.iteritems(node.outputs))
163166
new_outputs = {k: v for k, v in new_outputs}
164167

165168
# update reference counts
166-
for val_hash in new_outputs.itervalues():
169+
for val_hash in six.itervalues(new_outputs):
167170
self.add_ref(val_hash)
168-
for val_hash in invalidated_outputs.itervalues():
171+
for val_hash in six.itervalues(invalidated_outputs):
169172
self.remove_ref(val_hash)
170173

171174
# invalidate changed outputs
172-
invalidated_outputs.update({k: None for k, _ in new_outputs.iteritems()})
175+
invalidated_outputs.update({k: None for k, _ in six.iteritems(new_outputs)})
173176
node.outputs = outputs
174177
node.invalidate_children(invalidated_outputs)
175178

@@ -274,10 +277,10 @@ def run(self, input_objs, input_hashes):
274277
block = ast.parse(self.code)
275278
if len(block.body) > 0 and type(block.body[-1]) is ast.Expr:
276279
last = ast.Expression(block.body.pop().value)
277-
exec compile(block, '<string>', mode='exec') in env
280+
exec(compile(block, '<string>', mode='exec'), env)
278281
res = eval(compile(last, '<string>', mode='eval'), env)
279282
else:
280-
exec compile(block, '<string>', mode='exec') in env
283+
exec(compile(block, '<string>', mode='exec'), env)
281284
res = None
282285

283286
# find outputs which have changed from input hashes

nodebook/pickledict.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from __future__ import absolute_import
12
import io
23
import os
34
from functools import partial
45
import hashlib
56
import pandas as pd
67
import msgpack
78
import inspect
9+
import six
810

911
# using dill instead of pickle for more complete serialization
1012
import dill
@@ -13,9 +15,18 @@
1315
try:
1416
from cStringIO import StringIO
1517
except ImportError:
16-
from StringIO import StringIO
18+
try:
19+
from StringIO import StringIO
20+
except ImportError:
21+
# Python3. We are using StringIO as a target for pickle, so we
22+
# actually want BytesIO.
23+
from io import BytesIO as StringIO
1724

18-
import UserDict
25+
try:
26+
from UserDict import DictMixin
27+
except ImportError:
28+
# see https://github.com/flask-restful/flask-restful/pull/231/files
29+
from collections import MutableMapping as DictMixin
1930

2031
PANDAS_CODE = 1
2132
DILL_CODE = 2
@@ -45,7 +56,7 @@ def msgpack_deserialize(code, data):
4556
return msgpack.ExtType(code, data)
4657

4758

48-
class PickleDict(object, UserDict.DictMixin):
59+
class PickleDict(DictMixin):
4960
"""
5061
Dictionary with immutable elements using pickle(dill), optionally supporting persisting to disk
5162
"""
@@ -55,13 +66,13 @@ def __init__(self, persist_path=None):
5566
persist_path: if provided, perform serialization to/from disk to this path
5667
"""
5768
self.persist_path = persist_path
69+
self.encodings = {}
5870
self.dump = partial(msgpack.dump, default=msgpack_serialize)
5971
self.load = partial(msgpack.load, ext_hook=msgpack_deserialize)
60-
6172
self.dict = {}
6273

6374
def keys(self):
64-
return self.dict.keys()
75+
return list(self.dict.keys())
6576

6677
def __len__(self):
6778
return len(self.dict)
@@ -77,25 +88,33 @@ def get(self, key, default=None):
7788
return self[key]
7889
return default
7990

91+
def __iter__(self):
92+
for key in self.dict:
93+
yield key
94+
8095
def __getitem__(self, key):
8196
if self.persist_path is not None:
8297
path = self.dict[key]
8398
with open(path, 'rb') as f:
84-
value = self.load(f)
99+
value = self.load(f, encoding=self.encodings[key])
85100
else:
86101
f = StringIO(self.dict[key])
87-
value = self.load(f)
102+
value = self.load(f, encoding=self.encodings[key])
88103
return value
89104

90105
def __setitem__(self, key, value):
106+
encoding = None
107+
if isinstance(value, six.string_types):
108+
encoding = 'utf-8'
109+
self.encodings[key] = encoding
91110
if self.persist_path is not None:
92111
path = os.path.join(self.persist_path, '%s.pak' % key)
93112
with open(path, 'wb') as f:
94-
self.dump(value, f)
113+
self.dump(value, f, encoding=encoding)
95114
self.dict[key] = path
96115
else:
97116
f = StringIO()
98-
self.dump(value, f)
117+
self.dump(value, f, encoding=encoding)
99118
serialized = f.getvalue()
100119
self.dict[key] = serialized
101120

nodebook/utils.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from __future__ import absolute_import
12
import json
2-
from nodebookcore import INDENT
3+
from .nodebookcore import INDENT
4+
import six
35

46

57
def output_to_function(output_node, main_closing_statement, args):
68
def add_dependencies(node_inputs, dep_set):
7-
dep = {(k, v) for k, v in node_inputs.iteritems() if k not in args and v is not None}
9+
dep = {(k, v) for k, v in six.iteritems(node_inputs) if k not in args and v is not None}
810
return dep_set.union(dep)
911

1012
depends = add_dependencies(output_node.inputs, set())
@@ -14,8 +16,8 @@ def add_dependencies(node_inputs, dep_set):
1416
n = output_node
1517
while not depends.issubset(avail) and n.parent is not None:
1618
n = n.parent
17-
if len(depends.intersection(n.outputs.iteritems())) != 0:
18-
avail.update(n.outputs.iteritems())
19+
if len(depends.intersection(six.iteritems(n.outputs))) != 0:
20+
avail.update(six.iteritems(n.outputs))
1921
depends = add_dependencies(n.inputs, depends)
2022
funcs.append(n.extract_function())
2123

@@ -37,16 +39,16 @@ def create_module(node, export_statement, input_dict):
3739
"""
3840
create a python module to execute a given node
3941
"""
40-
body = output_to_function(node, export_statement, input_dict.keys())
42+
body = output_to_function(node, export_statement, list(input_dict.keys()))
4143

4244
imports = '\n'.join([
4345
'import json',
4446
])
4547

4648
deser = ''
47-
for k, v in input_dict.iteritems():
49+
for k, v in six.iteritems(input_dict):
4850
deser += "\n{} = json.loads('{}')".format(k, json.dumps(v))
49-
deser += '\nmain({})'.format(','.join(input_dict.iterkeys()))
51+
deser += '\nmain({})'.format(','.join(six.iterkeys(input_dict)))
5052

5153
code = '{}\n\n{}\n\n{}\n'.format(imports, body, deser)
5254
return code

nodebook_demo.ipynb

Lines changed: 16 additions & 16 deletions
Large diffs are not rendered by default.

setup.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import absolute_import
12
from setuptools import setup, find_packages
23
import os
34
import sys
@@ -8,7 +9,7 @@
89

910
setup(
1011
name='nodebook',
11-
version='0.1.0',
12+
version='0.2.0',
1213
author='Kevin Zielnicki',
1314
author_email='kzielnicki@stitchfix.com',
1415
license='Stitch Fix 2017',
@@ -17,7 +18,7 @@
1718
long_description='Nodebook Jupyter Extension',
1819
url='https://github.com/stitchfix/nodebook',
1920
install_requires=[
20-
'ipython<6', # newer versions of ipython do not support 2.7
21+
'ipython',
2122
'jupyter',
2223
'click',
2324
'dill',
@@ -26,8 +27,7 @@
2627
'pytest-runner',
2728
],
2829
tests_require=['pytest'],
29-
data_files=[
30-
(os.path.expanduser('~/.ipython/nbextensions'), ['ipython/nbextensions/nodebookext.js']),
31-
(os.path.expanduser('~/.ipython/extensions'), ['ipython/extensions/nodebookext.py']),
32-
],
30+
package_data={
31+
'nodebook': ['ipython/nbextensions/*.js']
32+
},
3333
)

0 commit comments

Comments
 (0)