diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b76b8a2c7..f91cfae918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). #### Added - methods `Frame.to_tuples()` and `Frame.to_dict()` (#1400). - methods `Frame.head(n)` and `Frame.tail(n)` (#1307). +- `Frame` objects are now pickle-able (#1442). #### Fixed - crash when an int-column row selector is applied to a Frame which already had another row filter applied (#1437). +- Frame.copy() now retains the key (#1443). ### [v0.7.0](https://github.com/h2oai/datatable/compare/0.7.0...v0.6.0) — 2018-11-16 diff --git a/c/datatable.cc b/c/datatable.cc index de477c430b..511e5d4d1e 100644 --- a/c/datatable.cc +++ b/c/datatable.cc @@ -93,7 +93,9 @@ DataTable* DataTable::copy() const { // vector can be default-copied. newcols.push_back(col->shallowcopy()); } - return new DataTable(std::move(newcols), this); + DataTable* res = new DataTable(std::move(newcols), this); + res->nkeys = nkeys; + return res; } @@ -185,8 +187,7 @@ void DataTable::reify() { -size_t DataTable::memory_footprint() -{ +size_t DataTable::memory_footprint() const { size_t sz = 0; sz += sizeof(*this); sz += (ncols + 1) * sizeof(Column*); diff --git a/c/datatable.h b/c/datatable.h index f7c400d8bd..f94621db81 100644 --- a/c/datatable.h +++ b/c/datatable.h @@ -88,7 +88,7 @@ class DataTable { void rbind(std::vector, std::vector>); DataTable* cbind(std::vector); DataTable* copy() const; - size_t memory_footprint(); + size_t memory_footprint() const; /** * Sort the DataTable by specified columns, and return the corresponding @@ -118,6 +118,7 @@ class DataTable { size_t get_nkeys() const; void set_key(std::vector& col_indices); void clear_key(); + void set_nkeys_unsafe(size_t K); DataTable* min_datatable() const; DataTable* max_datatable() const; @@ -136,10 +137,8 @@ class DataTable { static DataTable* load(DataTable* schema, size_t nrows, const std::string& path, bool recode); - void save_jay(const std::string& path, - const std::vector& colnames, - WritableBuffer::Strategy wstrategy); - static DataTable* open_jay(const std::string& path); + MemoryRange save_jay(); + void save_jay(const std::string& path, WritableBuffer::Strategy); private: void _init_pynames() const; @@ -148,6 +147,7 @@ class DataTable { void _integrity_check_pynames() const; DataTable* _statdt(colmakerfn f) const; + void save_jay_impl(WritableBuffer*); #ifdef DTTEST friend void dttest::cover_names_integrity_checks(); @@ -155,6 +155,10 @@ class DataTable { }; +DataTable* open_jay_from_file(const std::string& path); +DataTable* open_jay_from_bytes(const char* ptr, size_t len); +DataTable* open_jay_from_mbuf(const MemoryRange&); + //============================================================================== diff --git a/c/frame/key.cc b/c/frame/key.cc index 36a49be068..9ff726860d 100644 --- a/c/frame/key.cc +++ b/c/frame/key.cc @@ -136,3 +136,8 @@ void DataTable::set_key(std::vector& col_indices) { nkeys = K; } + + +void DataTable::set_nkeys_unsafe(size_t K) { + nkeys = K; +} diff --git a/c/frame/py_frame.cc b/c/frame/py_frame.cc index 3f15b1e641..7f70e19d48 100644 --- a/c/frame/py_frame.cc +++ b/c/frame/py_frame.cc @@ -87,6 +87,7 @@ const char* Frame::Type::classdoc() { void Frame::Type::init_methods_and_getsets(Methods& mm, GetSetters& gs) { + _init_init(mm, gs); _init_names(mm, gs); gs.add<&Frame::get_ncols>("ncols", diff --git a/c/frame/py_frame.h b/c/frame/py_frame.h index e38acd8a54..6b67e93014 100644 --- a/c/frame/py_frame.h +++ b/c/frame/py_frame.h @@ -65,6 +65,7 @@ class Frame : public PyObject { static void init_methods_and_getsets(Methods&, GetSetters&); private: static void _init_names(Methods&, GetSetters&); + static void _init_init(Methods&, GetSetters&); }; // Internal "constructor" of Frame objects. We do not use real constructors @@ -78,6 +79,8 @@ class Frame : public PyObject { void m__release_buffer__(Py_buffer* buf) const; oobj m__getitem__(robj item); void m__setitem__(robj item, robj value); + oobj m__getstate__(const NoArgs&); // pickling support + void m__setstate__(const PKArgs&); oobj _repr_html_(const NoArgs&); oobj get_ncols() const; diff --git a/c/frame/py_frame_init.cc b/c/frame/py_frame_init.cc index 6072bfed9b..c005e3c60b 100644 --- a/c/frame/py_frame_init.cc +++ b/c/frame/py_frame_init.cc @@ -353,6 +353,9 @@ class FrameInitializationManager { } else { make_datatable(srcdt); } + if (srcdt->get_nkeys()) { + frame->dt->set_nkeys_unsafe(srcdt->get_nkeys()); + } } @@ -671,6 +674,52 @@ void Frame::m__init__(PKArgs& args) { } + +//------------------------------------------------------------------------------ +// pickling / unpickling +//------------------------------------------------------------------------------ + +static NoArgs fn___getstate__("__getstate__", nullptr); +static PKArgs fn___setstate__(1, 0, 0, false, false, {"state"}, + "__setstate__", nullptr); + + +// TODO: add py::obytes object +oobj Frame::m__getstate__(const NoArgs&) { + MemoryRange mr = dt->save_jay(); + auto data = static_cast(mr.xptr()); + auto size = static_cast(mr.size()); + return oobj::from_new_reference(PyBytes_FromStringAndSize(data, size)); +} + +void Frame::m__setstate__(const PKArgs& args) { + PyObject* _state = args[0].to_borrowed_ref(); + if (!PyBytes_Check(_state)) { + throw TypeError() << "`__setstate__()` expects a bytes object"; + } + // Clean up any previous state of the Frame (since pickle first creates an + // empty Frame object, and then calls __setstate__ on it). + m__dealloc__(); + core_dt = nullptr; + stypes = nullptr; + ltypes = nullptr; + + const char* data = PyBytes_AS_STRING(_state); + size_t length = static_cast(PyBytes_GET_SIZE(_state)); + dt = open_jay_from_bytes(data, length); + PyObject* _dt = pydatatable::wrap(dt); + if (!_dt) throw PyError(); + core_dt = reinterpret_cast(_dt); + core_dt->_frame = this; +} + + +void Frame::Type::_init_init(Methods& mm, GetSetters&) { + mm.add<&Frame::m__getstate__, fn___getstate__>(); + mm.add<&Frame::m__setstate__, fn___setstate__>(); +} + + } // namespace py diff --git a/c/jay/open_jay.cc b/c/jay/open_jay.cc index a092024abd..dc0f486598 100644 --- a/c/jay/open_jay.cc +++ b/c/jay/open_jay.cc @@ -12,7 +12,8 @@ // Helper functions -static Column* column_from_jay(const jay::Column* jaycol, MemoryRange& jaybuf); +static Column* column_from_jay(const jay::Column* jaycol, + const MemoryRange& jaybuf); @@ -20,10 +21,20 @@ static Column* column_from_jay(const jay::Column* jaycol, MemoryRange& jaybuf); // Open DataTable //------------------------------------------------------------------------------ -DataTable* DataTable::open_jay(const std::string& path) +DataTable* open_jay_from_file(const std::string& path) { + MemoryRange mbuf = MemoryRange::mmap(path); + return open_jay_from_mbuf(mbuf); +} + +DataTable* open_jay_from_bytes(const char* ptr, size_t len) { + MemoryRange mbuf = MemoryRange::external(ptr, len); + return open_jay_from_mbuf(mbuf); +} + + +DataTable* open_jay_from_mbuf(const MemoryRange& mbuf) { std::vector colnames; - MemoryRange mbuf = MemoryRange::mmap(path); const uint8_t* ptr = static_cast(mbuf.rptr()); const size_t len = mbuf.size(); @@ -69,7 +80,7 @@ DataTable* DataTable::open_jay(const std::string& path) } auto dt = new DataTable(std::move(columns), colnames); - dt->nkeys = static_cast(frame->nkeys()); + dt->set_nkeys_unsafe(static_cast(frame->nkeys())); return dt; } @@ -79,7 +90,9 @@ DataTable* DataTable::open_jay(const std::string& path) // Open an individual column //------------------------------------------------------------------------------ -static MemoryRange extract_buffer(MemoryRange& src, const jay::Buffer* jbuf) { +static MemoryRange extract_buffer( + const MemoryRange& src, const jay::Buffer* jbuf) +{ size_t offset = jbuf->offset(); size_t length = jbuf->length(); return MemoryRange::view(src, length, offset + 8); @@ -98,7 +111,9 @@ static void initStats(Stats* stats, const jay::Column* jcol) { } -static Column* column_from_jay(const jay::Column* jcol, MemoryRange& jaybuf) { +static Column* column_from_jay( + const jay::Column* jcol, const MemoryRange& jaybuf) +{ jay::Type jtype = jcol->type(); Column* col = nullptr; diff --git a/c/jay/save_jay.cc b/c/jay/save_jay.cc index 2023661f48..9965277070 100644 --- a/c/jay/save_jay.cc +++ b/c/jay/save_jay.cc @@ -16,8 +16,8 @@ using WritableBufferPtr = std::unique_ptr; static jay::Type stype_to_jaytype[DT_STYPES_COUNT]; static flatbuffers::Offset column_to_jay( Column* col, const std::string& name, flatbuffers::FlatBufferBuilder& fbb, - WritableBufferPtr& wb); -static jay::Buffer saveMemoryRange(const MemoryRange*, WritableBufferPtr&); + WritableBuffer* wb); +static jay::Buffer saveMemoryRange(const MemoryRange*, WritableBuffer*); template static flatbuffers::Offset saveStats( Stats* stats, flatbuffers::FlatBufferBuilder& fbb); @@ -27,16 +27,34 @@ static flatbuffers::Offset saveStats( // Save DataTable //------------------------------------------------------------------------------ +/** + * Save Frame in Jay format to the provided file. + */ void DataTable::save_jay(const std::string& path, - const std::vector& colnames, WritableBuffer::Strategy wstrategy) { - // Cannot store a view frame, so materialize first. - reify(); - size_t sizehint = (wstrategy == WritableBuffer::Strategy::Auto) ? memory_footprint() : 0; auto wb = WritableBuffer::create_target(path, sizehint, wstrategy); + save_jay_impl(wb.get()); +} + + +/** + * Save Frame in Jay format to memory, + */ +MemoryRange DataTable::save_jay() { + auto wb = std::unique_ptr( + new MemoryWritableBuffer(memory_footprint())); + save_jay_impl(wb.get()); + return wb->get_mbuf(); +} + + +void DataTable::save_jay_impl(WritableBuffer* wb) { + // Cannot store a view frame, so materialize first. + reify(); + wb->write(8, "JAY1\0\0\0\0"); flatbuffers::FlatBufferBuilder fbb(1024); @@ -45,10 +63,10 @@ void DataTable::save_jay(const std::string& path, for (size_t i = 0; i < ncols; ++i) { Column* col = columns[i]; if (col->stype() == SType::OBJ) { - DatatableWarning() << "Column `" << colnames[i] + DatatableWarning() << "Column `" << names[i] << "` of type obj64 was not saved"; } else { - auto saved_col = column_to_jay(col, colnames[i], fbb, wb); + auto saved_col = column_to_jay(col, names[i], fbb, wb); msg_columns.push_back(saved_col); } } @@ -82,7 +100,7 @@ void DataTable::save_jay(const std::string& path, static flatbuffers::Offset column_to_jay( Column* col, const std::string& name, flatbuffers::FlatBufferBuilder& fbb, - WritableBufferPtr& wb) + WritableBuffer* wb) { jay::Stats jsttype = jay::Stats_NONE; flatbuffers::Offset jsto; @@ -170,7 +188,7 @@ void init_jay() { static jay::Buffer saveMemoryRange( - const MemoryRange* mbuf, WritableBufferPtr& wb) + const MemoryRange* mbuf, WritableBuffer* wb) { if (!mbuf) return jay::Buffer(); size_t len = mbuf->size(); diff --git a/c/memrange.cc b/c/memrange.cc index f3f48a03af..ecd06ab05e 100644 --- a/c/memrange.cc +++ b/c/memrange.cc @@ -113,7 +113,7 @@ ViewedMRI* base; public: - ViewMRI(size_t n, MemoryRange& src, size_t offset); + ViewMRI(size_t n, const MemoryRange& src, size_t offset); virtual ~ViewMRI() override; void resize(size_t n) override; @@ -130,7 +130,7 @@ size_t refcount; public: - static ViewedMRI* acquire_viewed(MemoryRange& src); + static ViewedMRI* acquire_viewed(const MemoryRange& src); void release(); bool is_writable() const; @@ -138,7 +138,7 @@ const char* name() const override { return "viewed"; } private: - ViewedMRI(MemoryRange& src); + ViewedMRI(const MemoryRange& src); }; @@ -223,7 +223,7 @@ return MemoryRange(new ExternalMRI(n, ptr, pb)); } - MemoryRange MemoryRange::view(MemoryRange& src, size_t n, size_t offset) { + MemoryRange MemoryRange::view(const MemoryRange& src, size_t n, size_t offset) { return MemoryRange(new ViewMRI(n, src, offset)); } @@ -602,7 +602,7 @@ // ViewMRI //============================================================================== - ViewMRI::ViewMRI(size_t n, MemoryRange& src, size_t offs) { + ViewMRI::ViewMRI(size_t n, const MemoryRange& src, size_t offs) { xassert(offs + n <= src.size()); base = ViewedMRI::acquire_viewed(src); offset = offs; @@ -647,7 +647,7 @@ // ViewedMRI //============================================================================== - ViewedMRI::ViewedMRI(MemoryRange& src) { + ViewedMRI::ViewedMRI(const MemoryRange& src) { BaseMRI* implptr = src.o->impl.release(); src.o->impl.reset(this); parent = src.o; // copy std::shared_ptr @@ -660,7 +660,7 @@ resizable = false; } - ViewedMRI* ViewedMRI::acquire_viewed(MemoryRange& src) { + ViewedMRI* ViewedMRI::acquire_viewed(const MemoryRange& src) { BaseMRI* implptr = src.o->impl.get(); ViewedMRI* viewedptr = dynamic_cast(implptr); if (!viewedptr) { diff --git a/c/memrange.h b/c/memrange.h index 2496f542c0..f2a1b6b1d5 100644 --- a/c/memrange.h +++ b/c/memrange.h @@ -130,7 +130,7 @@ class MemoryRange static MemoryRange acquire(void* ptr, size_t n); static MemoryRange external(const void* ptr, size_t n); static MemoryRange external(const void* ptr, size_t n, Py_buffer* pybuf); - static MemoryRange view(MemoryRange& src, size_t n, size_t offset); + static MemoryRange view(const MemoryRange& src, size_t n, size_t offset); static MemoryRange mmap(const std::string& path); static MemoryRange mmap(const std::string& path, size_t n, int fd = -1); static MemoryRange overmap(const std::string& path, size_t nextra, diff --git a/c/py_datatable.cc b/c/py_datatable.cc index aa331a8ce4..1ca86cf9ae 100644 --- a/c/py_datatable.cc +++ b/c/py_datatable.cc @@ -95,9 +95,16 @@ PyObject* datatable_load(PyObject*, PyObject* args) { PyObject* open_jay(PyObject*, PyObject* args) { PyObject* arg1; if (!PyArg_ParseTuple(args, "O:open_jay", &arg1)) return nullptr; - std::string filename = py::robj(arg1).to_string(); - DataTable* dt = DataTable::open_jay(filename); + DataTable* dt = nullptr; + if (PyBytes_Check(arg1)) { + const char* data = PyBytes_AS_STRING(arg1); + size_t length = static_cast(PyBytes_GET_SIZE(arg1)); + dt = open_jay_from_bytes(data, length); + } else { + std::string filename = py::robj(arg1).to_string(); + dt = open_jay_from_file(filename); + } py::Frame* frame = py::Frame::from_datatable(dt); return frame; } @@ -576,26 +583,28 @@ PyObject* use_stype_for_buffers(obj* self, PyObject* args) { Py_RETURN_NONE; } + PyObject* save_jay(obj* self, PyObject* args) { DataTable* dt = self->ref; - PyObject* arg1, *arg2, *arg3; - if (!PyArg_ParseTuple(args, "OOO:save_jay", &arg1, &arg2, &arg3)) + PyObject* arg1; + PyObject* arg2; + if (!PyArg_ParseTuple(args, "OO:save_jay", &arg1, &arg2)) return nullptr; auto filename = py::robj(arg1).to_string(); - auto colnames = py::robj(arg2).to_stringlist(); - auto strategy = py::robj(arg3).to_string(); + auto strategy = py::robj(arg2).to_string(); auto sstrategy = (strategy == "mmap") ? WritableBuffer::Strategy::Mmap : (strategy == "write") ? WritableBuffer::Strategy::Write : WritableBuffer::Strategy::Auto; - - if (colnames.size() != dt->ncols) { - throw ValueError() - << "The list of column names has wrong length: " << colnames.size(); + if (!filename.empty()) { + dt->save_jay(filename, sstrategy); + Py_RETURN_NONE; + } else { + MemoryRange mr = dt->save_jay(); + auto data = static_cast(mr.xptr()); + auto size = static_cast(mr.size()); + return PyBytes_FromStringAndSize(data, size); } - - dt->save_jay(filename, colnames, sstrategy); - Py_RETURN_NONE; } @@ -604,8 +613,6 @@ PyObject* save_jay(obj* self, PyObject* args) { // Misc //------------------------------------------------------------------------------ - - static void dealloc(obj* self) { delete self->ref; Py_TYPE(self)->tp_free(self); diff --git a/datatable/frame.py b/datatable/frame.py index a284c1d9f7..08644acc32 100644 --- a/datatable/frame.py +++ b/datatable/frame.py @@ -531,7 +531,6 @@ def __sizeof__(self): - #------------------------------------------------------------------------------- # Global settings #------------------------------------------------------------------------------- diff --git a/datatable/nff.py b/datatable/nff.py index 6ac4ac965d..f8d0a71410 100644 --- a/datatable/nff.py +++ b/datatable/nff.py @@ -11,7 +11,7 @@ from datatable.lib import core # from datatable.fread import Frame # from datatable.fread import fread -from datatable.utils.typechecks import typed, TValueError, dtwarn +from datatable.utils.typechecks import typed, TTypeError, TValueError, dtwarn _builtin_open = open @@ -23,8 +23,7 @@ def _stringify(x): return str(x) -@typed(dest=str, format=str, _strategy=str) -def save(self, dest, format="jay", _strategy="auto"): +def save(self, dest=None, format="jay", _strategy="auto"): """ Save Frame in binary NFF/Jay format. @@ -32,6 +31,8 @@ def save(self, dest, format="jay", _strategy="auto"): :param format: either "nff" or "jay" :param _strategy: one of "mmap", "write" or "auto" """ + if dest is None: + return self.internal.save_jay(None, None) if _strategy not in ("auto", "write", "mmap"): raise TValueError("Invalid parameter _strategy: only 'write' / 'mmap' " "/ 'auto' are allowed") @@ -45,7 +46,7 @@ def save(self, dest, format="jay", _strategy="auto"): os.makedirs(dest) if format == "jay": - self.internal.save_jay(dest, self.names, _strategy) + self.internal.save_jay(dest, _strategy) return self.materialize() @@ -75,8 +76,11 @@ def save(self, dest, format="jay", _strategy="auto"): -@typed(path=str) def open(path): + if isinstance(path, bytes): + return core.open_jay(path) + if not isinstance(path, str): + raise TTypeError("Parameter `path` should be a string") path = os.path.expanduser(path) if not os.path.exists(path): msg = "Path %s does not exist" % path diff --git a/tests/test_dt.py b/tests/test_dt.py index e56109c935..452e922f14 100644 --- a/tests/test_dt.py +++ b/tests/test_dt.py @@ -1040,6 +1040,19 @@ def test_copy_frame(): assert d1.names != d0.names +def test_copy_keyed_frame(): + d0 = dt.Frame(A=range(5), B=["alpha", "beta", "gamma", "delta", "epsilon"]) + d0.key = "A" + d1 = d0.copy() + d2 = dt.Frame(d0) + d1.internal.check() + d2.internal.check() + assert d2.names == d1.names == d0.names + assert d2.stypes == d1.stypes == d0.stypes + assert d2.key == d1.key == d0.key + assert d2.to_list() == d1.to_list() == d0.to_list() + + #------------------------------------------------------------------------------- # head / tail diff --git a/tests/test_nff.py b/tests/test_nff.py index f9e15cca4b..058438bff4 100644 --- a/tests/test_nff.py +++ b/tests/test_nff.py @@ -1,15 +1,34 @@ #!/usr/bin/env python -# © H2O.ai 2018; -*- encoding: utf-8 -*- -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# -*- coding: utf-8 -*- #------------------------------------------------------------------------------- +# Copyright 2018 H2O.ai +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +#------------------------------------------------------------------------------- +import datatable as dt +import math import os -import shutil +import pickle import pytest import random +import shutil import tempfile -import datatable as dt from datatable import DatatableWarning from tests import assert_equals @@ -198,3 +217,56 @@ def test_jay_keys(tempfile): d1 = dt.open(tempfile) assert d1.key == ("x",) assert_equals(d0, d1) + + + +#------------------------------------------------------------------------------- +# pickling +#------------------------------------------------------------------------------- + +def test_pickle(tempfile): + DT = dt.Frame(A=range(10), B=list("ABCDEFGHIJ"), C=[5.5]*10) + with open(tempfile, 'wb') as out: + pickle.dump(DT, out) + with open(tempfile, 'rb') as inp: + DT2 = pickle.load(inp) + DT2.internal.check() + assert DT.to_list() == DT2.to_list() + assert DT.names == DT2.names + assert DT.stypes == DT2.stypes + + +def test_pickle2(tempfile): + DT = dt.Frame([[1, 2, 3, 4], + [5, 6, 7, None], + [10, 0, None, -5], + [1000, 10000, 10000000, 10**18], + [True, None, None, False], + [None, 3.14, 2.99, 1.6923e-18], + [134.23891, 901239.00001, 2.5e+300, math.inf], + ["Bespectable", None, "z e w", "1"], + ["We", "the", "people", "!"]], + stypes=[dt.int8, dt.int16, dt.int32, dt.int64, dt.bool8, + dt.float32, dt.float64, dt.str32, dt.str64]) + with open(tempfile, 'wb') as out: + pickle.dump(DT, out) + with open(tempfile, 'rb') as inp: + DT2 = pickle.load(inp) + DT2.internal.check() + assert DT.names == DT2.names + assert DT.stypes == DT2.stypes + assert DT.to_list() == DT2.to_list() + + +def test_pickle_keyed_frame(tempfile): + DT = dt.Frame(A=list("ABCD"), B=[12.1, 34.7, 90.1238, -23.239045]) + DT.key = "A" + with open(tempfile, 'wb') as out: + pickle.dump(DT, out) + with open(tempfile, 'rb') as inp: + DT2 = pickle.load(inp) + DT2.internal.check() + assert DT.names == DT2.names + assert DT.stypes == DT2.stypes + assert DT.to_list() == DT2.to_list() + assert DT.key == DT2.key