From 58a24c5ba243f89302f90ef62550cee252968c9d Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 18 Jan 2018 10:56:15 -0500 Subject: [PATCH 01/46] ARROW-2004: [C++] Add shrink_to_fit parameter to BufferBuilder::Resize, add Reserve method I also relaxed the requirement to pass `const uint8_t*` so that one can pass `const void*` when writing to a `BufferBuilder`. This will not affect any downstream users Author: Wes McKinney Closes #1486 from wesm/ARROW-2004 and squashes the following commits: 2d6660a8 [Wes McKinney] Add shrink_to_fit parameter to BufferBuilder::Resize, add Reserve method, relax pointer type in Append --- cpp/src/arrow/buffer-test.cc | 25 ++++++++++++++++++++++ cpp/src/arrow/buffer.h | 41 ++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/cpp/src/arrow/buffer-test.cc b/cpp/src/arrow/buffer-test.cc index 5fd2706f0466b..398cc06363a6f 100644 --- a/cpp/src/arrow/buffer-test.cc +++ b/cpp/src/arrow/buffer-test.cc @@ -194,4 +194,29 @@ TEST(TestBuffer, SliceMutableBuffer) { ASSERT_TRUE(slice->Equals(expected)); } +TEST(TestBufferBuilder, ResizeReserve) { + const std::string data = "some data"; + auto data_ptr = data.c_str(); + + BufferBuilder builder; + + ASSERT_OK(builder.Append(data_ptr, 9)); + ASSERT_EQ(9, builder.length()); + + ASSERT_OK(builder.Resize(128)); + ASSERT_EQ(128, builder.capacity()); + + // Do not shrink to fit + ASSERT_OK(builder.Resize(64, false)); + ASSERT_EQ(128, builder.capacity()); + + // Shrink to fit + ASSERT_OK(builder.Resize(64)); + ASSERT_EQ(64, builder.capacity()); + + // Reserve elements + ASSERT_OK(builder.Reserve(60)); + ASSERT_EQ(128, builder.capacity()); +} + } // namespace arrow diff --git a/cpp/src/arrow/buffer.h b/cpp/src/arrow/buffer.h index 450a4c78b5bbb..b50b1a1aa041d 100644 --- a/cpp/src/arrow/buffer.h +++ b/cpp/src/arrow/buffer.h @@ -25,6 +25,7 @@ #include #include +#include "arrow/memory_pool.h" #include "arrow/status.h" #include "arrow/util/bit-util.h" #include "arrow/util/macros.h" @@ -32,13 +33,12 @@ namespace arrow { -class MemoryPool; - // ---------------------------------------------------------------------- // Buffer classes -/// Immutable API for a chunk of bytes which may or may not be owned by the -/// class instance. +/// \class Buffer +/// \brief Object containing a pointer to a piece of contiguous memory with a +/// particular size. Base class does not own its memory /// /// Buffers have two related notions of length: size and capacity. Size is /// the number of bytes that might have valid data. Capacity is the number @@ -133,7 +133,8 @@ ARROW_EXPORT std::shared_ptr SliceMutableBuffer(const std::shared_ptr& buffer, const int64_t offset, const int64_t length); -/// A Buffer whose contents can be mutated. May or may not own its data. +/// \class MutableBuffer +/// \brief A Buffer whose contents can be mutated. May or may not own its data. class ARROW_EXPORT MutableBuffer : public Buffer { public: MutableBuffer(uint8_t* data, const int64_t size) : Buffer(data, size) { @@ -148,6 +149,8 @@ class ARROW_EXPORT MutableBuffer : public Buffer { MutableBuffer() : Buffer(NULLPTR, 0) {} }; +/// \class ResizableBuffer +/// \brief A mutable buffer that can be resized class ARROW_EXPORT ResizableBuffer : public MutableBuffer { public: /// Change buffer reported size to indicated size, allocating memory if @@ -190,13 +193,22 @@ class ARROW_EXPORT PoolBuffer : public ResizableBuffer { MemoryPool* pool_; }; +/// \class BufferBuilder +/// \brief A class for incrementally building a contiguous chunk of in-memory data class ARROW_EXPORT BufferBuilder { public: - explicit BufferBuilder(MemoryPool* pool) + explicit BufferBuilder(MemoryPool* pool ARROW_MEMORY_POOL_DEFAULT) : pool_(pool), data_(NULLPTR), capacity_(0), size_(0) {} - /// Resizes the buffer to the nearest multiple of 64 bytes per Layout.md - Status Resize(const int64_t elements) { + /// \brief Resizes the buffer to the nearest multiple of 64 bytes + /// + /// \param elements the new capacity of the of the builder. Will be rounded + /// up to a multiple of 64 bytes for padding + /// \param shrink_to_fit if new capacity smaller than existing size, + /// reallocate internal buffer. Set to false to avoid reallocations when + /// shrinking the builder + /// \return Status + Status Resize(const int64_t elements, bool shrink_to_fit = true) { // Resize(0) is a no-op if (elements == 0) { return Status::OK(); @@ -205,7 +217,7 @@ class ARROW_EXPORT BufferBuilder { buffer_ = std::make_shared(pool_); } int64_t old_capacity = capacity_; - RETURN_NOT_OK(buffer_->Resize(elements)); + RETURN_NOT_OK(buffer_->Resize(elements, shrink_to_fit)); capacity_ = buffer_->capacity(); data_ = buffer_->mutable_data(); if (capacity_ > old_capacity) { @@ -214,7 +226,14 @@ class ARROW_EXPORT BufferBuilder { return Status::OK(); } - Status Append(const uint8_t* data, int64_t length) { + /// \brief Ensure that builder can accommodate the additional number of bytes + /// without the need to perform allocations + /// + /// \param size number of additional bytes to make space for + /// \return Status + Status Reserve(const int64_t size) { return Resize(size_ + size, false); } + + Status Append(const void* data, int64_t length) { if (capacity_ < length + size_) { int64_t new_capacity = BitUtil::NextPower2(length + size_); RETURN_NOT_OK(Resize(new_capacity)); @@ -248,7 +267,7 @@ class ARROW_EXPORT BufferBuilder { } // Unsafe methods don't check existing size - void UnsafeAppend(const uint8_t* data, int64_t length) { + void UnsafeAppend(const void* data, int64_t length) { memcpy(data_ + size_, data, static_cast(length)); size_ += length; } From bc9f9e532ea2a16810d5ce14e1dfc3272628cb95 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 18 Jan 2018 11:01:23 -0500 Subject: [PATCH 02/46] ARROW-1966: [C++] Accommodate JAVA_HOME on Linux that includes the jre/ directory, or is the full path to directory with libjvm Some users ran into a rough edge where they had a non-standard JRE directory (possibly related to some recent changes by Oracle in their JDK installer) Author: Wes McKinney Closes #1487 from wesm/ARROW-1966 and squashes the following commits: 7e14923d [Wes McKinney] Add note to API documentation about JAVA_HOME f77b31e6 [Wes McKinney] Accommodate a JAVA_HOME containing the jre/ directory, or an absolute path to directory containing libjvm --- cpp/apidoc/HDFS.md | 4 ++++ cpp/src/arrow/io/hdfs-internal.cc | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cpp/apidoc/HDFS.md b/cpp/apidoc/HDFS.md index d54ad270c05f4..d3671fb7691ba 100644 --- a/cpp/apidoc/HDFS.md +++ b/cpp/apidoc/HDFS.md @@ -50,6 +50,10 @@ export CLASSPATH=`$HADOOP_HOME/bin/hadoop classpath --glob` * `ARROW_LIBHDFS_DIR` (optional): explicit location of `libhdfs.so` if it is installed somewhere other than `$HADOOP_HOME/lib/native`. +To accommodate distribution-specific nuances, the `JAVA_HOME` variable may be +set to the root path for the Java SDK, the JRE path itself, or to the directory +containing the `libjvm` library. + ### Mac Specifics The installed location of Java on OS X can vary, however the following snippet diff --git a/cpp/src/arrow/io/hdfs-internal.cc b/cpp/src/arrow/io/hdfs-internal.cc index 9cd1c5052fe8d..545b2d17d2e78 100644 --- a/cpp/src/arrow/io/hdfs-internal.cc +++ b/cpp/src/arrow/io/hdfs-internal.cc @@ -147,7 +147,7 @@ static std::vector get_potential_libjvm_paths() { file_name = "jvm.dll"; #elif __APPLE__ search_prefixes = {""}; - search_suffixes = {"", "/jre/lib/server"}; + search_suffixes = {"", "/jre/lib/server", "/lib/server"}; file_name = "libjvm.dylib"; // SFrame uses /usr/libexec/java_home to find JAVA_HOME; for now we are @@ -175,7 +175,7 @@ static std::vector get_potential_libjvm_paths() { "/usr/lib/jvm/default", // alt centos "/usr/java/latest", // alt centos }; - search_suffixes = {"/jre/lib/amd64/server"}; + search_suffixes = {"", "/jre/lib/amd64/server", "/lib/amd64/server"}; file_name = "libjvm.so"; #endif // From direct environment variable From a11da7f914e117675d9e662fa7b5f78dd02c61ed Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Fri, 19 Jan 2018 12:46:40 -0500 Subject: [PATCH 03/46] ARROW-2005: [Python] Fix incorrect flake8 config path to Cython lint config This was silently passing even though the config file was not found. This first build should fail, then I will fix the flakes Author: Wes McKinney Closes #1488 from wesm/ARROW-2005 and squashes the following commits: 4305e1d1 [Wes McKinney] Fix Cython flakes 894d4ab3 [Wes McKinney] Fix incorrect flake8 config path to Cython lint config --- ci/travis_lint.sh | 6 +++--- python/pyarrow/_orc.pxd | 8 +++++--- python/pyarrow/_orc.pyx | 12 +++++++----- python/pyarrow/plasma.pyx | 8 +++++--- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ci/travis_lint.sh b/ci/travis_lint.sh index e234b7b015b8d..6a2a0be18cf9f 100755 --- a/ci/travis_lint.sh +++ b/ci/travis_lint.sh @@ -35,10 +35,10 @@ popd # Fail fast on style checks sudo pip install flake8 -PYARROW_DIR=$TRAVIS_BUILD_DIR/python/pyarrow +PYTHON_DIR=$TRAVIS_BUILD_DIR/python -flake8 --count $PYARROW_DIR +flake8 --count $PYTHON_DIR/pyarrow # Check Cython files with some checks turned off flake8 --count --config=$PYTHON_DIR/.flake8.cython \ - $PYARROW_DIR + $PYTHON_DIR/pyarrow diff --git a/python/pyarrow/_orc.pxd b/python/pyarrow/_orc.pxd index 411691510423c..c07a19442b577 100644 --- a/python/pyarrow/_orc.pxd +++ b/python/pyarrow/_orc.pxd @@ -29,9 +29,10 @@ from pyarrow.includes.libarrow cimport (CArray, CSchema, CStatus, TimeUnit) -cdef extern from "arrow/adapters/orc/adapter.h" namespace "arrow::adapters::orc" nogil: - cdef cppclass ORCFileReader: +cdef extern from "arrow/adapters/orc/adapter.h" \ + namespace "arrow::adapters::orc" nogil: + cdef cppclass ORCFileReader: @staticmethod CStatus Open(const shared_ptr[RandomAccessFile]& file, CMemoryPool* pool, @@ -40,7 +41,8 @@ cdef extern from "arrow/adapters/orc/adapter.h" namespace "arrow::adapters::orc" CStatus ReadSchema(shared_ptr[CSchema]* out) CStatus ReadStripe(int64_t stripe, shared_ptr[CRecordBatch]* out) - CStatus ReadStripe(int64_t stripe, std_vector[int], shared_ptr[CRecordBatch]* out) + CStatus ReadStripe(int64_t stripe, std_vector[int], + shared_ptr[CRecordBatch]* out) CStatus Read(shared_ptr[CTable]* out) CStatus Read(std_vector[int], shared_ptr[CTable]* out) diff --git a/python/pyarrow/_orc.pyx b/python/pyarrow/_orc.pyx index 7ff4bac6dc95f..cf04f48a32319 100644 --- a/python/pyarrow/_orc.pyx +++ b/python/pyarrow/_orc.pyx @@ -50,7 +50,7 @@ cdef class ORCReader: get_reader(source, &rd_handle) with nogil: check_status(ORCFileReader.Open(rd_handle, self.allocator, - &self.reader)) + &self.reader)) def schema(self): """ @@ -69,10 +69,10 @@ cdef class ORCReader: return pyarrow_wrap_schema(sp_arrow_schema) def nrows(self): - return deref(self.reader).NumberOfRows(); + return deref(self.reader).NumberOfRows() def nstripes(self): - return deref(self.reader).NumberOfStripes(); + return deref(self.reader).NumberOfStripes() def read_stripe(self, n, include_indices=None): cdef: @@ -85,11 +85,13 @@ cdef class ORCReader: if include_indices is None: with nogil: - check_status(deref(self.reader).ReadStripe(stripe, &sp_record_batch)) + (check_status(deref(self.reader) + .ReadStripe(stripe, &sp_record_batch))) else: indices = include_indices with nogil: - check_status(deref(self.reader).ReadStripe(stripe, indices, &sp_record_batch)) + (check_status(deref(self.reader) + .ReadStripe(stripe, indices, &sp_record_batch))) batch = RecordBatch() batch.init(sp_record_batch) diff --git a/python/pyarrow/plasma.pyx b/python/pyarrow/plasma.pyx index 29e233b6e4e67..32f6d189da08c 100644 --- a/python/pyarrow/plasma.pyx +++ b/python/pyarrow/plasma.pyx @@ -248,8 +248,8 @@ cdef class PlasmaClient: check_status(self.client.get().Get(ids.data(), ids.size(), timeout_ms, result[0].data())) - cdef _make_plasma_buffer(self, ObjectID object_id, shared_ptr[CBuffer] buffer, - int64_t size): + cdef _make_plasma_buffer(self, ObjectID object_id, + shared_ptr[CBuffer] buffer, int64_t size): result = PlasmaBuffer(object_id, self) result.init(buffer) return result @@ -302,7 +302,9 @@ cdef class PlasmaClient: check_status(self.client.get().Create(object_id.data, data_size, (metadata.data()), metadata.size(), &data)) - return self._make_mutable_plasma_buffer(object_id, data.get().mutable_data(), data_size) + return self._make_mutable_plasma_buffer(object_id, + data.get().mutable_data(), + data_size) def get_buffers(self, object_ids, timeout_ms=-1): """ From 305b54ce0ca092fb243e3c69e5fb186cdd90266f Mon Sep 17 00:00:00 2001 From: Justin Dunham Date: Fri, 19 Jan 2018 12:49:13 -0500 Subject: [PATCH 04/46] ARROW-1872: [Website] Minor edits and addition of YAML for versions Hi, in this version I've: * Added a YAML file for tracking versions and outputting related links etc. * Done some minor cleanup on HTML and images * Added a small paragraph about what Arrow is to the front page Author: Justin Dunham Closes #1483 from riboflavin/master and squashes the following commits: 9ffb5263 [Justin Dunham] fix responsive layout fece5578 [Justin Dunham] update front page description 6174a5fb [Justin Dunham] small image cleanups 173ead2c [Justin Dunham] slight improvements to mobile and html layout fe9007d5 [Justin Dunham] add version vars, small amount of copy, and clean up formatting slightly --- site/_data/versions.yml | 29 ++++++++++ site/img/copy.png | Bin 23204 -> 64271 bytes site/img/copy2.png | Bin 37973 -> 0 bytes site/img/shared.png | Bin 37973 -> 20925 bytes site/img/shared2.png | Bin 23204 -> 0 bytes site/index.html | 118 +++++++++++++++++++++------------------- site/install.md | 20 +++---- 7 files changed, 100 insertions(+), 67 deletions(-) create mode 100644 site/_data/versions.yml delete mode 100644 site/img/copy2.png delete mode 100644 site/img/shared2.png diff --git a/site/_data/versions.yml b/site/_data/versions.yml new file mode 100644 index 0000000000000..0d04183868dcf --- /dev/null +++ b/site/_data/versions.yml @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to you under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Database of contributors to Apache Arrow (WIP) +# Blogs and other pages use this data +# +current: + number: '0.8.0' + date: '18 December 2017' + git-tag: '1d689e5' + github-tag-link: 'https://github.com/apache/arrow/releases/tag/apache-arrow-0.8.0' + release-notes: 'http://arrow.apache.org/release/0.8.0.html' + mirrors: 'https://www.apache.org/dyn/closer.cgi/arrow/arrow-0.8.0/' + mirrors-tar: 'https://www.apache.org/dyn/closer.cgi/arrow/arrow-0.8.0/apache-arrow-0.8.0.tar.gz' + java-artifacts: 'http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.arrow%22%20AND%20v%3A%220.8.0%22' + asc: 'https://www.apache.org/dist/arrow/arrow-0.8.0/apache-arrow-0.8.0.tar.gz.asc' + sha512: 'https://www.apache.org/dist/arrow/arrow-0.8.0/apache-arrow-0.8.0.tar.gz.sha512' \ No newline at end of file diff --git a/site/img/copy.png b/site/img/copy.png index a1e04999eb3fd3bd7c350a0850659740d4a8cd03..55ff71ece1e597f51a5038757b6604e7dc82aeba 100644 GIT binary patch literal 64271 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

qSDYW` z+S-=O$oPKH5oNWt4ZSv>29=~|YT&ap>p-CQr@e1nap3R`#g2A7cqbSHe1o|bL@{l9 z-aw*FS%&9_gOkBTa5eZPh|=1+@@2;|cI8BZC1#Z(27{wB!yZlj!r{MeyyGWN2d=36CV<8{qCXmeG=IUm?*>;Dca& zOEQ$DX{^t=Tzw)q4!HdH0(cbIXP*X99IG5h+A|)-vf6r_L`FU24*~Vj5p9uFJ_96F zj#o*#SOd{8ySUm^T{9Z+xkySL18)OQ07*-G#epQ2vmNJ>pQFM3;KyJQh+-Ss)5-Js zM^M%~fP`xa_%4_Olzh6nke2-|&o`cIZv<}ulY#Mn1^Hz*E;A|58^c?2)Fpoh@NG?k z6ZOeyois;$X3UroX3m@$Hf-1s#*7&g_Sg^ock;T zabB0-OFk^7xA02o%ER+{8yfWLNp}*TkI;;xz<&VAPkZ_g*&CHH#QXqU3M$!mqZwNf zq47BeJq+FjocL=&OC%a@`Tg%mX8Vm3{!;KHu-tn={sp2Z5FVP8Z6enjWm|m_T?g7L z>PFJ{NpNI)3rqRirh$%dVYaR2l&d)n=;d)TcOt(Dv~S6>}|`qQ5VzGXP9Sg|5pcG+cN z$&w{u`|Y<60|ySwQakyV6M$GuQ*kj(`Ek>xP2rJ89tnT>%U`lO9(?e@AfefDN9Uo} z%Iejt!=;yA8h-Pe--KtLc_yq~yEgpupZ^SeJ9OA-r=1!IT2t*PkE?Vq`LLKi9~N5` z?QBr4W_ zE|>w7nAcg<^LTAsB^gcN$v~30vQmPk!fw9#=5X3+r-du7xFYPb%Pyh1x;i62Yu2m@ z{Lp4oS=$vS;kxa%+rqSI)4~~NoDrUS>Z$O;3opE`ObODHPd*ub@Pi)&$V1qB@4d4& z@^cVbJ0mfBUB-3>M2}A;;*&4>s;jQbJXQPb<0I{HqNisR_`fb_nFM4paA~(Q#Xj|8 z@G#gFJP2w*UShv1?>m!7WIsO`ECaseSdPdO)5h|XtlMNTHGjOmc9$+~zwIo)$ND6# zI#~AM;M1TZ%4=2SDRG?!67lJ|mSCrN3ET|yko+$w_gE0rX0iM@=*;K*SHOYI!Rjn< zIru%W&U{+WTPrb-^}Paq4J2FMmv#IGTmkaw;yJ-Bl9)c;lcer}dQ9WE2he-=L!e%* z_fB9sp)^N;90acivp~$lY4gIbiib-sxg=b4(M92~!ww5IHFaHY)F|i5I?=mw`|Y;} zJu;FIJuF8ad1M$eWJu<9dG5LAvgI+oMSJeKXP7WyLgtm(dFP!oB4sJFX3YvuJn=+B zKEqtn+Gd+=f&^y3fC1UEmCIe_!tY;&^Ups&oN&Sk;n-u3%}9@F@@=$T5;%S@D~lgL zetdZATi+V^b@Xuk_16bU+=2xQvbtO%JLQy9GEb9BWiIJSa-M(w`5=k;#3w!x?!EWk zaQEGJXHAhLG6kGSJPtNJ_XD~aVC3bd0v`Ky#Rg>|9^d=N=`t_B<& z{s(*zbjBzj2c9?x-9tU!0)GUDgWrJzL5w%O=WUgk-{Y;|8{h`;G4Nebx7JfP7`C+@ z%N9y3!^!Ak@FO6Bl(>8WoDX!r>&{ltFvm%co~Wl3_Z@P85mPg%@6! zwNY#ekIrz-HP?i5&pkI=n!D+yo5I;=pB>)xrZpml8Laly6|#{^7J+eHP?`O}mxV*XuE4%w|B6IF z20tE1ihOzbSMU`0Ik2z&5%@y20a#Y-SFw*0)MLc9*~d=g$Q{wWDz4W@Kc_lqs1vieE<$C!KUsM%E-iBS((Rh>+f+FMs*V z*>aoY?Zz8#%m|boqxtjaXZQQz4}UoGAo1(#;qb!`Zy-5(cqUGqn2{gvwTH+=+ToI| z9u4idD`XRuECyXce7qmMK7Ry~n~#F^U;x+$*l(u-UoI{L>R|@RzZ}@#mDnHau6o|z zqiE8nfP~F4$ac57ual=SalSnUe5tVnDZFbm;Dbog^EL2Gn)pF*CFl&|6E)z#6iLZU z&fTx@05}!=8k`H9{MP~{PYmipTJ|>%fY#;P>vcf4xb-+FTm^m$9AGG+flbf0icNuA~z4i*f``z!d zeMXW9_x(iCGo1 z0VK^1il%R?9huj96p7DIfP}}lB0WDtfobyTxNNiJ%d%b>g@d_uy2R&zcQm*dd<1L* z`hu^5KY+QwGFmNT$BxasBH#JWce1@O5(Pa#dSmpWII&BN^nkdJMGuf4JwN#1gB$kP z=#4RtWxE@P>Uwdb{*6={Zuo50}~&En1YdBZkMiy6HHXdiMp| zkQ0Ab$~r0&eLApG`-FYjqeh|fJs%GC2EK^cNA3Uik?(`Ez%^hFP)skBHuaUdtX=xo zvDFu@yTC6ysb=*m!@mw}E_&%CRdj!`$>Y=g&!8nO&@19w)v4fV;G0%wi=8tLfWD#p z5jen~2;7fz3oz_u5C=$3TivPigMb71C13$K1N;em4Lk-+=ip&o?Uh((9H2K)pmodu z{{*`5j{x55qrl1S3n1#|-Abk|w<=E=~DqEWZQDkH5RzWVw1z-1nnr#ZAu=8N(F`#p$P?9)9$r zA7vgE%h5CBJ{w7wA6-BBo8Js)opn}t=%I(Qa6L|XhHQsq#qjTb z_q$meZU5K5{`Kr$^g`tci5scowH~kIk3T;19360g4mN%~j?K=W`OIgswz&t%dJJ>J zwC&ClIMcg{+C5B?BA3{_-*}(KcZ0l@I=2EBbwk~Hee8QA*`|XAR zUqamXV*G{R6F`!4KCr(=v5Z#riI}mQj4Nl@bRD>(mQ#FnA|$aQrj487v1%t4Tp!w(M`dp2>u~9~=sP z4J3XJw3hGS)X|c!+p@O@yMSK-y*@tz7XWpyCrqh$uMiKH2Yo|+o#-i9xpHL&Cw7U5 z6SyS7~bal^5m5WO&ZJ|r%aCr{2k5vJ!x^5U|R1j&z}OO{;9a_OsF^pvG?6OM9T7G+e{bB)WalEs~}zV1%^|pTQ7mcST9Id%Q!0h9ry;grwx^B z@6qPtB(FXDu(W+ioeb3%Ej=OL=PYm}_z!S0sC(eAk|lyR3yJqFvHMS8KmRaT0%9De z9V#wMJ^{7`@dc_wD@cVU4Q#0zxSbn21@!BN8+1U(%Ooy73M6B?hxY^$geVT8ZIOIE z!zkciz_s8^ur*LjlTVigcF>=Exz>FhI1xyEZU)}~4wQpHlxj{I@-ewr{H-p$A4IiY zAQ>gTTc?DVsA6XJ_mo&dDzOYyF}{}tYKNUEK4)wMz;|f^UJFz&F6oU@H*C0j>bis7QR;NHlBJhGF((|fj zI-B_TtX&G#7EcG=CB6il1^%@K4bXn=hxxwfxuoqN@IOGC{w??*P>1m)gVP4F9K}3( zfF^+$#_47wwpow0FEU5fx4JX$qlP;4&PlYA^Z*TX=ELp*x3|SLL89>#&<*XRTms!C z+D@#080QX#rvjJ4ToSq!EC7mxpa^1m!&)nnjMIUG^7nynYKjB6Wp|Xs`}#i$l+=6{ ztjZPcgy0*Z&tNZb3-}v&7OVv>=RCk+Z_kr-xdu9OsY~0?mcI!`fd2+dL3fBRKc55I zrhD!xiG9O1yLTsweRVwWh2;pK=jd*52bc}!gY}>>iT&d*B$x?)2r|2GEJF_r_r860 z-ox2l(7t}u;ka$RbGo3uR7EoykclSQd^E6urrrw<26Mq0&|M;liG$!W&h_{_4(!~% zO&tsV1Rew{fKmn8N>U_xE;qTi;LE@P`$Dh@w5MFMzec<-Cj~t{E+gbg50}}#hFdv4 z33dZ-2R{KfgDb!ch>vZGQe=;cWp<|MWy+pKXV+CIeb#2*)f$f0g zzPm^dRcCf@zk7AVl%aKfQd>~s^YMAvW^n{K5IhTR29k8i&SKCKDb*+X2FFXmHQ;^_ z!#VAdlJyT-wmbMFxD}iYI$NxF0`R3Zk)F=hb_>f-#HWRd>OskTOZh0c2y`d$Q6+J( zi-XE@gq;f0%<&AT zXZ$=dhcjnAbIv(uQ88!Ck`xdT5tJOi=joloW|-ZbU1n!ycjx{6p6OIwRbAa(_3yW; zdkia~Qf4-58%@aD;1@Uw6hAkE;>60mI&rewTr&=z=r4t9;0Gwr#<1ZP_yY9&>v5pm6-zQ1^5)!hHZE& zYZh-DHSjGh-pcwCu`r(Xt?`yLt^qH}Y~K26i`Z(#e{xMA-5>%vh-^yeE^MP8N&CJa?_(a99`=KMKAeh?uQ0^W3Zh18OXVQp8m* zfs*j4a*-8$2XEMq!JBC~*ccU~*#LX9e+%ou&afdo0`J3*pjj~}II1FmtYBXNEB=$< zY`7SPLb4HHF>5pZCE*x202C`#F`u5Fh7b9Fs^|H>%^?Vqj_upGf1_*HuCMN~#~z=i zM2{7)6|Tpe3!A}51UKcQh8ywm!l(EjtB;5@m-lr8j-vshYW$!V)_5;V=od$aB@*#RiCN7VSm>%Rp$x0k zfL@eG!OFNcn3ar`vQJzJpX;G7B=fIesgAA0PPfB-&}J^5$!hxwS`^?W#R74sazrSD(p<+xCD|Km0L z5FC*Es4I=p^N@ZmcVV4k_bkx+sTe&OHh{%pL4`X}RLK-zAH!OZp4Tcxl*7Q`zP3Ji z0*=mM+}TEDS!@Y!XS+iwT&V;~B~Wn*_;jcHwnfFkigK&MZC&^;SSi_E6$A^j@l$Q6 zxLnT0Iuhs%cp0`z?U1dA>P~pOFnn^(mCemRf9lzUFD-Xt{LQ|6Y54)Tg~88zF#gA` zwb)WZW9~j-eKu3#+L<`7Mpy-pS2nw43=;&88xd10@c05UaGS7!k z>X*Q^pcgM6X1ZRx$?MmLgSBCAuzTrI(91aj^g^|TLYf|@n_-bcW+Ih#xQ~`*`A)FB znx(eobD{t}3w*#US*Zj{C6I#>*q=CVgM3xSImq&SQRrRS8g$P;2!}(m@%D)+RAS7Q zYaxsS7mtMxVB1vaWaE)qHFx4R)6i9K<>Y*>E#%F3ZA5#3PoIy#RiH>~1;HxHZwQk< z{W~kJfu89bs<<4|Zp?l=z};{=C`!${mOP%aHFf)UI}Yyw@X5WVEnJVubZorMgAWIG znd*5u9uz@(TE2kf^}}M`&1=GL>vv%{sEPZCXscxA=Tf*GIzb`KhtlW3I<91;5-62G z4oYBg;`kD}p zSsVaXlr?D~J=5^%y*Hc%55w88F$6PgGJIHH-n1VZ^hnqRQXNaCWx1aYdgA^KbG=f` zC&RH=r_Ez+2X{h$@SN3znNCAUmTha6H-v-W5>V`120KF!aJ%L;`!=lLLJ`{cF{4TRw(-NL#_Y|!OC3w$!x9tZbLmXYDD6pu>r8`GM!kUx)k zDtrb*U=7$64o%I!!SqAWDmvb>5M{r*)Qxvaa%GnDp?$=*;j!32dt0shpfqR$i;D6-r_YGmn&VCEAt726&WX`aOe)6*MW33-p?Pl4&zT+ ze`ne2T5G_#tET*;{dl%7GIQq4vbBZ#^;qtQZQx)y7bZYH%{*w^!Y1G~?8Bk!^L_1_ z=AzIH#z4u6s02#Fr-)8Q6|>@v^&aRs_!NA#w6U{#_BjQohKlvT4ZIHK?6S))i+}gscWX?ZJb78Z#k4ekWZRzcR6`hF z_2?2yEb-+s%PjNjb=O@tk!|sy^M@XKs1bw9<7;T!ZMU8C`RAWcVEou(UwrXJuJc#b zvJcnw+3a{1Yy{6jz7(0)sn@b&2OS4x+ES3~mvlWJ%fmNNvLY)1FN%`Y-V*3X);ECD z;1{UvmaL>&b1*$&J5UUK3m?G?Fd4LD__8(g{wfIeh5f*;f~5FS^mK=Qpa3a^CG&4Z zxfE;xn}9<0HFz0*11%Z8Y)#qkiA)qP|AZN3t1A2r@i?`7cLdAoL3kZZyI|&^LpLt|%y ztQh$ihJuZR9-zHo75D(22cPgX!-sw26{(-;Onv%n1XjX^v)gMA*cAQ&g%Uj^JvYzh z%VYTw%vz{+4V;Rsb*6`I=UtNZ``4(Mw-F3azy#^N4Y!ec9(!pRQtob5QISO zKBBVRhifxb`t4b5BfSlD0-uZr!fOyLRDxkSOl`Ohgk9l!I09~jhoQWE*pzom=A|X6 z_X3}smxAx$br=9zGHidAmxM#0JPYA%P!&~TLunbj0ikgU;aF8TL--6Qb)TB^ZL{fC zYK^F9N|)0l#?R>Ou}oxvO>Y))H)qUHYsL^6=M27z{X0U#?pcM8=O;TjE3UmB>60K zhJE2Q*Z?w_jf)Mbo}w?opZ@EEg-XQAZx)P#N5JmBjbIb#nwo!&={vAF^n>%@GqB9Mfp6{{48K9B zOqnezOF!Ok$elvh21OVDVc-~`dqE4~J2CH=raoYUa1(q0lR+!i<1Ncw>9;PFfY;dj z@Hlv#4u?Tdq3N~3!gZV}Y}mFt8>b?AyswKz^qAinurPcHA+OBV9?Rk!)SiRMkW(+r zlGXkaup!?cY~*X@jaJQ5;PorvDN-Td0rdWC1iG57YDTCx& z_UY3{(HHL942M#5tL&0|4&iBiQfI9;osc?v5^`{0x{z;&CtxbnH}g7{Ab^@O-wy2B z(yOB|)Fbj5C=P>lXXbhs2xo_twP5P8hDv@-=6#y>cX-&w#1V;z8UA6cYIZQDvk7XT|~(M zg`8KonLn~LhqnTK_$r#bWl=6ZJRU>dZ_OfXtqtNqmaO)dz*ihX7ibRkQTQkV#0o~N zRP}JI05<*(hF%x;hraL{=m{ALr-7c55l~xh_He9W`X_n!US0wg1(*H2fp5Ic1g$nz zzGXQs=a;V>M?d}aQ}o9lfB1J2QRmK`qxS9F&l8%)Cn~QktZ3Ujk)$n*9z8l5Hf&hL zK-#EBj~)?UbxsTM*P?kFI&^5nYwHo;>WTB%rcE0MG|$afa{jM9%jPQDmLAsSio#d4 zEmK@Iu+D4GhmV`#;MBZb(-oP8+q%2}2#QEBJrq(j=5^R85|(Sj)Cb2pZFA2~ zoj%ICe{c@a?F9;-`fLgTX$WURCpZ+w!Yi;TYyf+M0_Y8R3HpQKN5G3v%w{7Jj?;Yh zhRt9YJO*EY*Fqn-0G@@{z3@kGQ&2jjTu!iYB5V1roHgfLG1&n(5!OE0|?eel5t z5nVA6Pyf-1E3O!AwbfP;A1j<2yrO(mv~6w#iJ6MRC%0L4>#x6lV$ZzHdhL1L`4D0^ ztX;D|!f+_hrm-Phv*}^t_z-j)T09mg#Gf9n7?z7UZ{?Lw@DP``z1FWU37=Yj3gZL# zhTm-4;!nG%4!aw{O&`Q>SR! zv}w`c!Gj~lRg`-W*?K%G3a8NG4?&~-_uoI_zqO*@e)}!Xhu)l|{rvOK(Pfui77L;S z4?Hm9tIDxwYW(=|@&3uMxtureT2ka>ivPqDPmJDu_uWjJYI>a+oCO!dO|Tc(oi!0M zSwmKadoX=_ygqfT&X*T1eZN917jxdqE05Zk=kksvbOX{YWDv7??)G3d~xhaS#G)I zVvmmv{_3l*9(Q}_?fLfGZ=(?-M#P>MMaLRztPyufef8B>u_#c0*bQYj)!+X1x46rU ze?53>ZQFXe{AGHdKKkgRXyU|)(ZB!w z@2Fe1ZgHB5GCe`Ob{z{e{<0!Y!@S#VX4=MWSK1Fh{1EXK=E!{V=cDm98+eb4KTeI~ zGo3+$21R@)CR$;I6{7dvdoNDY?iD>s{IO~D<(Cfe;$lajDGpa%b=Byz&pwO8zVgZ| z5r2XjAOByu&&!1XR@}tNkTlWnwKn@8f6fKd!1C!7A)< zD%=lGq;|Gp7hM8Yyk7G@dHxQ*4RQw99i-V!v;+JXIzkWzK^(Z8PfMon{EBc2DAW{i z=2O9A{w71nUzmD~_kvH^BR~((>97K5g*vXX)T{XW?6c2CyY04HwDi(T&mBXdC}`TW zX)JvB7xn1stFMlJ`Q?|$aX{Bzdu{aMi!a7v!|9b*UK#0;Q8?Uqiu(*0Fd$CL?ls5nD4K$~&l_*N5pBKo)=~HF-D8pBHU*D) zRG1j%?z``fMTj1xmtTH4dhWUB;&}92op8blk=<$&CQOL#Vb`KkTr>;xFT7 z$fuQW!-pPvsKFk4?4bbqgD3B2_t|Hk0d!}q!ISo{G&D!hNX^D$@B&Z%ulMWMZw&DT zFG+dv#z~o%3~5$%m5qb(gt!)CUeU5PIQHQoco7sktw2xTbO_KnvW2Kd1;p7+| zJx-cJM$w@+MN7}4UZbzS{yHA?eBOEI#onI97F#Sj{q)mgG3P!CCiAaIQn0-C+H27^ z+iVkUw%KOUQ%^k=Ex-Ko(HCEQ5eubNR#_$9=0NM_&6~$U%x*S4S&AD6fO9WIreS;= zZ@h6_4x4Ve>D)3i%{*JUHtptG2K3IJ4?jVbuR+T=oPYMui&Ztvs)of0}nh99dyt^aaWu9IQr3MQY zlE+^JW*w!+Qg7sba5Z=jW@QRY-DsmNn3fgd8SpLDnI+v5{T~2OuIasFi=Q_P{ zLxv2A#jWDkv@@Au^*kD0L2S7zYCZ2E&17D}x)4|THkUJn3G33shj^SOwr_LJBIrK61dA{};vZCOdP|n4AzGQI>K~|JU%%syJ6xA3ep}-iR4AFYv8h5! z3XUd(Tn%=BZZH_`gwG)KQ5arxTHXLrw}-rh_!Ui;!4_bveG)tmPl4jd@F6U-#bY-5 zlzl9I^j_Hgq(D#{^z7L)T5`!HBRw~INNi~AI&!@tNJGfR<6RU%3IoT?C7M;>`3K34ZlHhe$+_+uQe>lJeaH0!5fa1Dfqmb;h_UDC|XZ~r%!}j793xg=tt5Qv&Y5>mLR%VfpQH|l zCBRCg&Ki|bL&iibMML*@p0w|#67%| z-5p!7y)j*1Z!tdNGa6I7h|;Y&D=vpo=^H9o4kAOrVd7n37g!m50(QKT-BJoV&G4Gx zJyw?|KxGyLjp5Auv!EwyZwTq#!~FLU!a6M)(<9;-mJ2VuFn*eL0J06C9+@-FI3sR^ zF2DTpxKY&mL3d_67AAOr9C&P48!`(ES?kq3I_)lD}sVI#sgP0*>&Wr?DySw zUo5U1e`8mb0!qX zx)13ZZ+)^*iPooMuB*p-9tUPac@a0j*G!kIs|T8PT;Up?$#FNPQdr=|ns|sN?*3AU z=wUI9vf8sO@$%9z+$!v=pa}V@C~R_DJNDTY)(0O*-i61&LCnF7Gr3PEmj4aI;A+U$ zlJR(qTY=sm=M_XU1+-+?vNd`5N}3)UUttdom%>HwOK5NvWXU(Y z46mT^%`Y1q$KUw0@0(~Y>vd6hnXZCKuasUN-$rvBlj6z7$_CUlJqFF=)iY(cPPcB| z;&nE7dXP+0Gd{bx5bCiISaZ!aWU7#q!fmFX!P`XK74D=veEUFvC#v8|D$t;%)b_jXAIm7xov#R9rsWYcCeu@vB_xi`JOzMkS z>nVCX-UUAHR6L3REqRZaj`5e91{G4+F9;Nuoe#@l->~c;fj*$<(FZ;V6g=NSF!xEu z5!Ne~+%_(($Z~32As;^4Ds(=Ff52{_x9N5m4u#`89adiERxq!3DOa}cqf8@o#M(4?DNgc^-}C6EgZLk@AlhokHw+q$G7GbO51I>T`V^Ij?qOIT@;s> zo-akDBG!TKz6EG`Dk!eM{`&YFhBWk|C>HH%-D#(t;&EZduZU2%1c9UY&@1WNhhbfa zBjnNUP2Vt7&=}^Pd+v$Hj{WnW|BPOJ_0@RnnQ!rVo%)vFfB*a6SSZ_lrYBMFpXHzk zv`h7_yY7kwn8)HVDTMSQ`WB>JTYAS7IHoUlzbc4AenS50(==JB*QevIi^pnLNQ=~g z>7^*E3G3pK&VNiclr?d@$#_G9Twyvq5n6E@)^RyLh6lkA_7BU7)aAi$rta`F_>}bq z1alv)yfhU`ijrRqR7DGMnuhZu;3C)#^!}Uy55NFOcC&^3Se-L74`SIEe}LKYt-Ojm z(|Q{|8?{UozE9S6OX#@?-2|CZ%@l8@bz$F%wi%|p?dj{&mqYx?X*OX(_^N~r`_%B% zYj`XueEP$xczotlpn6>tsh&qIUF<13KK}UQc)&4lX~y0>MX26BMSyMCp4Svx+eP|(+8y#(#RDlT z^uOCH`wZ!X_M`1={CE((`sv;HQDsM=m1E#Sk_|o*y)3IF=AK2#dDL||Dn0F&M_ckQ z!$>L%lo;G$O-F`#gZsm;Nt|LCBWodyGUGM-RqX@$nw^DkNRObPQXYhD1GHS>uOM~L zhF#F+)$5vjF&w4b=M+?zExvr=Kb^6|W@Gj{(MXgC%cop~`U zgQhLF3$YfKPD=ZLOBXnrl%VW0Dj3gLf^3pz(HrVPP=Vza3i)zTQ}MltX?W6K;V#y% z(^5E!#PlS>7Wk=)%iG2l=sasc?vd>jXiP>~I}s9jNuoJ@o{al;vdOl^WxG73*{n22 zbGZ(Sm=*)hQ1QK##*TVsbjiFrGCcE;;XOeBfh>m3#hj9DbE0{)T*F-FXM);=i6hna zrrtuxM*Z;b8XyfI32p4qeu|2Cq-Pqc`v4AI`A~;5^3CbC^VOg})2Q4Hlz>EIh))no z)9Fhc;@<4=Gdk=o&N5>LiDEW{39+9x#b_%HdpiQ#k8Idin*>Xr7pmyc?|2$B`#Lap z+R%fBVEx#C>I>93GN6XEjkg{&xQ;d{E;{edPk;aFnC^!2Pz2*-hbDPEWBS^#=^mV{ z;64GIds#g2C9&i#BF@PoUgZ30Ts^)0vPdxRiscm95M8%0U-$Yp5FozMWXQwit4s}Q^IB%uQ$2nxp(+}qN8`v)A7kCc3Hh$ zaI|1>$Me5Y4&OBK!vAhaBqgm|E1W)>Ee$mv$U>DJloy? zVFaBBz>)?0oy&EXJI?v6Lc{mX&q1Z;y*p0$s`3p+*ewB3gsidAe1jBC%K?;|LinQT7 z!OWql7TayX&Fp+0Z4z^Js~o4zbNJ7*%)ihY4?I^yD74!QkB7hMo5Zx8RaJl+bxV2K z7V^9uhxuW@(bKYe!;1R2nBe44v^|=d?n}mi(b#1TE99W+jAMDA=tf7D)sWiRMida& z%isi!0Mk5~$I{XQqL-8sx88VD6fC`ldVs0nJipvH+snWzK*s(!wEsrC&21BBJqsI_ zG_@WjL(Sc3qc=t*4f(hL$?~e&={5~uHTb}f2C$WHG+W(;8=H6h^n<97Ha&OJ5WlXZ z2^=1-mZTdb$kUecCxJ*b1CfbNrjs1}(>p;U+K%ZsL1!eLEh%ee>z=%B1o7_EaoM+y zd5;AHZxrJX+UWh?*4GSfP?$rO%zJu=vZ_zQ;m^ruFnb23S$=NghP(k z49Rg#`OW6D>nvu~bq`owOyyK`c3J%|!XB~H!{txV$kj~VGTS}D?bXs}i5!Q8sQX-{ zi6GgE+>x`Z%$dEn-BEjQcnkJwPq`#Ig@LG%A8h({3y&#>_W({?cn^iWv=2C#(^PkI`!hJ*sY3QMUqp@J_Cn8DA*RjCcyx&i^X6AKS9v?XT%&4F zL55kFKU00NSxXPJ%CYO=SnGo~%^^~9a(vz! z-;R?Z?YN92Jp(laixWo|a1{8%UFn94P$@>6qRswL#>mXy3!vUT#P2%1Z7&$F&GZCH zsWL98gAp@GMn+up{$`=A|G6+LUW^9qo}(%}IroqGAjIFt0GuwQNK~OPGW)2SSg#45T=%-6 za3fH6(l)oG?3SpgD++DRLuXM6t2;CucO~1de@tza@4sx;sey3$o$%kEhB^jrPcpSb zO3&TCc{mC0l!;ZJJVlo+MXz}&22CyKuZ=bOFxvfzsY>c3|8XaU7QC`03g*VKZo&qh zWebnj&Z^7oV(o+#tEfX%?3_tYX4?j!TH312(*m1sb$b|7*))n~Bkz!nkiO#V1tw=y zxP1!I)*>-9jsIa=NHzFy%h|TQf_KHi89B zl=tW&aZbhTUTjQiMD^UCphj@h+wVY?FAgNribbY<3UR1k0MV3s0(mh~w3lD==ma)7 z$$5X``txv%l_E-Y6@FO6VTM!Xjvv>{Un9;61qW}x>AB{Ep=Y5(9kJ%A3v8s+3>q2d z*PL2jT#)}En~oDDp#7$VS1f=l;-rUijja?WYLNfnJlf4%RMV9hF8>6xzPoab0m=RoSL++}IJv#+yRND9=@k zItg{Y4b0vrD+&5&_+ljX4o4n3uhgfb)?ZQ^cdN^>m~C*c6yZvj|<+@rhlmYa^ za$br2cPF0+VMm4!K_7a(9x{lk__x~?+G*s zmS>P$Y*`yOR(<;J=`h6oy?;e)-pO70O52J0_ceFGL%72ECKVBUNnd~tSkx7CKQQ?8 zTTi=xrQ74AIKYS?U=faOaGdts4sKgs$I_KS5^O&&ar3FitQ{lRofYL2l%R3Ot3F>)?dX=ByupcY(w`$|LeL=#2En z{P{Iii%#hY)t+P9uY>H`l%9vg+7gYB?*$f{zVmmU#t>_s{{f0gSE&CRsZST8gFFA z!f3PgxCnReX@4>^oFMwpxZ#1LqJ_9U!sCc|EN98AYw10cBwGG#mj?!Ggkd;IPuXU{ zOMU$zQDbvg^#S6u9fg#2sMs8dR>=8}Q?SWi`g!BxK4?mRYAdl@DW}49r%VFsQ1&g zKew6l*Ow+U_59~|CPN!~mDU_>d8Vn1gPOL`_pt)b&6>Oaz227g2_NWH)&8j|vIGup zTL}&6a@*@BT-e&2RXg1FgvAAICHhU?Z(P5E5zA{QIkA3v7<80+mA$0@?~eIE>g>|3?5eWtT>B_9KQpz&V~J@w2?givF`-8)VPJiG8^9!^^2$8Fv`+_d^%bB*ts zmb;-H!+YeFs{Q>5C__J7McA!j8IyHIA6b&k9TTj_yp8DNuTL|5UqFyltVG|;x;v%k z@eM{|4Xol>JqK$PSFgU%sWAt+hX&wy3gid~Xq|s+9ugU@imH=VxBr`Ef-m%DKiS9=Hd_;gzDQ%hgdRqPSN8mCeg3Vh4 z0Sdgm$_OT>n8}e*O0)vgMNL zUmW^Qf8LPxMf9E@pczSkyxH4C?&zteqG2u4iZh zKnpmx>7d86C;KUH7DznY<^hg3DI|A39!rn3I~LL%v6t>_)k5n(Oa0gZH|7*L5<@Jb zK+dxwv@f=E+>hj&ug@N&5S5ZKCf5{xRaHF2-@B5Zqv8!Z;%2u&45Z_luKbbX_xWfQ ztiS4+fz_{I?~3)$>2~;EIc;RrGVn=2-#%=#y>;&tA_c(Z^ou@6Be-7yA}lT&?uR^O zv<`L8RziiOMAql%y5s-)zg!8^`AMFUPu?Dxi*dRNACoXt9ghp^5d^>5$q#gU&>jS) zuF~i>h`uldC7$~Rv?f7V&cCXj9@ZN{0+^oq+Se83wv5zBJbd-`NOWJeN;*yy`Y*dx z>S>8FkqW~*hW!P*zyFP%pnm?`{Hu9^lV#(Qi+=-%I(ag%;VC-y^N8>In^pZfcAEBA zS(SRN(;`gASCV*8n=1?i{Kat(FFEEA#v}%lF~8d^pthd++`0FR7l47#`}WP+fgfE! zo1V77#oX{F7xNwgnZR=kvv^6i&6ioaeq7*zJ@;|O;aon~qz^zaHc!753MY9B z-!>Tr)dUWw{GCsm=2s3FIQTc?c#k*@HnOMw=P`)Su8LU;2- zt8V23ODI5K0Iz8a5N~l3RsA~XRI7(yw=;t4d_F(??;%9uiv7CxJIy@L7QX>%Gk*}> zQPhXU2RZ>RJ>n6^invZXD4)wJ32~JkIkE;io>1IpG`>`NQ-kwUqzJ5#m@I}YquE$( z2)DOfggE*NnFl{!o|03nd{Psc*5{?@zK=R$pUvv({Xpe<cYZz!0rJkFsq)ZRz7^(r&I>IQ(o!d}8{v%eo{5Y_@v2yF}HY_T@~hz7*NDToBndKn&%J z;=T$ttk=p7Ls&<6oelwuBF}U;H-0lI4Dl>bAe*7?kfryR#JfKkgnL!B{rxWHZ@C@e zbB|`dCQCYF5XY%56bll`b+;>*oI;%-!mJYl$0DJ>q5b7MJ!CIkqx;~L8)sg`?=at zBDwo1R*ST~uaITVR*Uyy6z(w`73v_eE4($c8F|sN*V;PsGj$e{s;*h}@>PiD8!Nrz9y8JePa7(>@#Uh($Z;RzRR>mAH_P%2 zqZGdApz7$tvCGGi(#auXNBiQSFmt3h9dO4xx-g&A^nxSeivRF6c-&b{UEz4FE(gnYF%CC6Otr zt$#ZvNe3toaawpxPjJ;LMEr2W8y4vsa_JNvmRl33$5OZQJS!pFM)4=>Absz;b_Y$4 z;limVRQkA7G+jin-FePMZ4uL_?qmo-P2d3O0^e*h)zIJ8_o!XvEU>DLY4E7DedIl> z;jUf!#bT^=PQWl~ojnrXbDj=brd-IlI10%IOGDkLhb{Z7<~&U5-fRcP?w{t<+L@q6+dnrizWN5`iY`|rO86}?D!Gw$FUyxbD5p! zR#Np$pV2D@9l%zvALs&fUr6RgKd^;BLhL<|HTTQnmm}Ag#U~V~%c)Epz5hGbNU|A> zOo>NyqcSsHoo`>;k7ofw^&k;jF5^zLztkn-E(3-!Q75>W0Y7{0~!35q4ed8lZX><7YT}oP7$ZV6{J2VSqs7zAk{>PaI^#f4Isnnq@mZra}^v1Ju zeb8BIGj6^klh&V(x-I0|=KnGVLUq=WI|-LGuc+`cazaF4(j#h0aUA5!aMP68iMMAM zf3#uy_P~9_m8ec)@YIGdNPzGq*X{m?v5vz(@gjq8lulfsB}?I*%9?gnhqvTd(wAGm zz({EvCtjSa-wiEogcUFFU`#E0HfGTE^GCRppl#lk!{?F2uIzsI42L*VQQ>>u1`HV|l z!>UUKWFPQuniU8=T3g2oT@gRtoO~)M>XumAGmQ0zx#zjQYJq>DVitG(chTW zj|h%yTN0f^^v>v6L&ST;Py8GKRnO>Mk+p2{W}JraqAtY^z$>VXa;G-pBB7q|e4{Tz zf1L|pcNY^$R{qQ@0JhBE=IAJ^=nfk|VzGdRu{k@~o)f*n($`(GpAD0FF z)f37yW_$Xc7D6UB^v)f7QF%rNlPYO5^?gyIqZiRKRaB1;0VS{fxwW}!f2_TpF%~U` z6iIA2OyBpFLfmrA+B_EbbT_(&#M^VY{(<)kDp>zmwk5)z>HEYUT3$~}v|LTe9QOWq z0U=N<(J;rnW23c6^$1bn><~;S8YD+}fflcs?8=zy5&`xh3*FgQK9OXP!bjcEj3GX=5 zbo>6WqiAdWdlnbjnd)en)aZ|Acm7_p4Y$$U^|yle)-XIMo%BJo5S+q1bB@jDenTQ%J6<*t^kxoaKxv3uMFBjX(=pXKd4%&Qz*aM+fVX~*#0%Z0udhY6?mtoBOJmGTm z*6<9}>Ym3UiWWG*E;c0^u;`mkewJa27KyKPUIB_xIO@26!nEWoCGt2X^|TPXX`&8h zpi}NPF<{eA#u(M+A%^td-g)0Fj}GL4eqwj7qKNm zNVH8)$2+UQdEKMnw*+6pmZQ^BNZw#aD1VQs)BmML%Ct&63^_r)0q1Mi*I{XE#8xA$ zD05LQLhUy)%ULcyLa6yi1uZU+{*GWEG7ft3-^tB& zIC7T%IGwy2FcnBL;&0!>;32+>u#W-$r2&;jSqEesWChOc~&&C%!(qF9DlJvI=bVP!9< z3@dOJpwt5_oHM~5sU}%pD)d9x)J|gA!-h6M30)8d^Ie(f5I{B*wq~3fa7nIQhRnI4 zO^|}W)nVdd0M0T96hF#;1>^#O3+F#J^r`bOa1e|iIT=aSm9dSVfOOSG@|m-8baQx# z=;OhBS+_XXQ|cpuJZw`ig4$(W+(_he{ni-1hZ(hqm+@sVAOn$7oS}ze_EOk!TYGbr z>%e~j!gB%Ic(7U6)7U=qw*t5}u+9(-2*~{lo{o1QDY+&EVp@kX}eZR`-v6OWp zh+sseWjsW`J(z>7xhb3RW8_O znc;Mxq4+H&B%Do0&)(Ea*ZA8ZwnUF?3sBz9xn*9528SeJ&yjl}uaR<2XbNpP^0TF= zMzp2y*V4C34GNMZnhH<2#AoEaJVPV8mb7*NW9qI5mNlP~2izB2@jbj+*mgOz)mFm- z1xJUdlm?Ix22;Akan48^-{o_=)}Q-@FCXx^Ur1r(gl~ycM}W?0of!Y$Fh`0avPP&&A$D?7Y0XsfC?13Nmc z{88|~GC6hOVaiRTmK2n~3M5Lt{%StCypp|@&2gUnLWJPdJdGvNnU+PHKe(oBOd1x+YbCbe#1Fr&UHDZtK)eQ@(5s_s2r2n;oUxc5cS{g_ z=V{WqAA0ufid@J0FKsZY7RJ&RFz@2gm_%gV5bCS_2&%9o>jR@Vhu9h9aDDp2PK~T7 zs2F(gMm54_Uqqe+oe;!FG+SC2)1{*fvn(yjeC(>^<_rv~M1%I7*_D1XNAU||Xl%+^ ze!JhI(zCCNGwU=z74~TAo)qSWL#pX?Y?c<1rZ)6W1$&oY3Ku5dSy7eR?EJ3vg)q;0(izdjcQ3fZFaB& z8B?}}FL84}W1y{y*W5Fu3fbDlIQz2yC`VR0v~-r?-wH{zM#9a57};?R9;mnq+&q{h zF=xZlC0V$swCuxXJIDv3d~tLx7O{aZ0lyu|^M6#;cyMQql6UVr*9Uaoe&pp#!d&xN zA`=dkk^`B!ibOVm_{)0wJ-GeNogiLA`>YU?e!6Nax|i2sIOQ$wZ2owkLRcDY`z=uO zSvXpf)^y0RmNi=WIVL7_+<(GkYC>BnmqRQKzMWwiwGHhIJH_AVv4^K<0gkZLJV8}V|Vhxn<*78y1Ta)WmHt~CeDb=fWDVwA^*9piBAZ8c9^Vt;E_Z#or5&#Us~+X`m8}uEEBJ&a7edT4loI!zDTgoG9Dswb$5-fNDA2hFa0tlv~`> zv87C_0iZXNF}38?Ut=TRvyP;%V`EBn+UjSXv~vX1$-h$VOdu4@v(fy1%{9x&Mfk#Q zja=to?nebECv5AIBOXv~Hw-7ojT|Caw-WBMSAeAKFBw+Yi*W;KEi`kS0%Xk4ml=~3 z%n78Zm0>jgM`dAQ4%E58$~E{26l)fv-avTxFvt-wg9f}zA*uO?J%>aV*i9{a;Dq+> zSZ3n4*61Wt%tCAEII*P@WPm+&oojeyCa+bAchP|PX+6rvHlmoya#Qk@IRC9iNQySr zVF&PWQN?WaDA!SWzvu)-1>qc_11yjh_%<#Ihtis?8L2_Ff{!E_2m!!P>pWUEs_d7_ z^>bgoh8fSYmU2DkS&|x8&F3T6A`hg)hbR~rNb=n8YlFmK$MVFyN|$**+^n?pmuco) zi8NK>rbliWlRs%7?iFzMdmCI_2_JlSCyFNByB|w2?KuZ^ zr^Wa~X)HD8NJ?eR`Nykg(`L6cSAB|WX5zk<4LGxyIMf+42`mS$z7Q=^3rt7d=|FxV zEIUfs=UAQeLE=x8(x@D}fZ7R1<76(>;Aq#L8&EW1A;nk&NBK!5ZpvD`mqJb00k-fA z4+4!Bkdp37ITSRtlfW3D;RXsg8hePXq2|Qj9*tTjBmXGRBgc&6$(+Qc*Wxl0r0`P7 zP=$B-ql|q$|FH3zF#6OmZ#fo_oR#``a?=WDj)qwltuJOlUQFPvT^tny`Y4|i^rD4g z(u>e~^#F+`>?+x;YkLLT7dyxoS2q3kZV$=gJS51}o0ApQ5Xn24ZVV&@yU$e2N9;|P zCCRfs*6bF?;loe+$uRGC+K-6iJF*Sm%p-lX^P5PR#3Zxh@oLF$MTo)6#KLHA&}b+$ zAh82J9`*bqNr!Co3}<$qafIXJ_>0b-9%8VC7pKvmj{FL=PEu_^r2Y_49~DP;56n7z z$!;>91PK$d3^vwTOYLyF>^s0TU&Z$)2+ML)O8dIdpCe`rvKL0PsBR2>CZP?_`1j%% zfP+n9&Vm*029o2;#D<|6V2Ri&;lc^eMVh><(pHXqe>7ZoJlsc0CU&F1 z(mxguwE8kTHwRQj)>GzTG-ksS$D3QeV3LbIs1R@s%K4jH`Ge48z-ARjjyeSW)L$`L z2Lp{o?9iL|)f(5qkgJo{P;4`L2Ol?jfhC#Zg+Iaxm}XcbTNJ)iw12wg5_{p?9xhJkZB3CbYX@HV%hWGB zY<*d52Vqru-X_X4h2MJzA1`tfLjJQ&-$}2Lc%U0$gr4YAkG7d*&Gr}Ybe#0QL%IP( zgOPCE+j3+9X2ZmRG0kmTy)-nTrMHL?lKMwEfwFP(=5)iTpOJ`gd;$q9g8n7*G1p#G zYG!0Fk*sA~H5CeDjvJIG$vpYfoVDx%IVx6tR$eVod%|XIDQ4dPM{5{<_+!6DdVZGr zfZkFH#>NVl4*45f@LX3-Td-4T3t$hmZvBAceWO%xkiB^VnWJ{Os8}q2H$>F{k~qdR zU!&!98{Pi_tdbPKZ;s}L>IwromDNKT6s+uFHuT0VUA^#sMv-g0TeYKO@klM66DN%% zErFUTIz0EVFqSM3sSVDpCq0G3{7m+vdDd>F;CSS9dFF4Fh-?36OvAp${u!3f!la2# z{y4q2@w!W9e_HcDl-lB$qGAf{y>r zP+Q&7pMubW^rm663TZnvv=NSGpu}2XTI-gqs#oV^3O0BygMHj;vw zN=1w?ZL8Nfr;$EGH@$5zt{tJQ|@B0@Wfc&J3{T^lgH+eV$ zNB)Zhu287>-szy#v{96uk_oA2#A>5wc7i|l&OQVFiR*+y@+iEqR+9afSBqgOf$FIr zZZ4Mjnkm*op1n;f(o01RP;sbNVAD8K|1t2{w%-VKSy3eH?0~aVs^px-3o&!|2T7PGDx{AwB@VnRCwqgQui)jk?TX zcwaDJY^vGFPArjS>*rU_F<`R;g^IzMp^H^LVa~G5LN8?v&2N=Sgw>$EiT|i*ClL1H zO^bJAea}6=$xfYd6-841!t9#a4IsYRHh}Y=U&_YHp+J9nYfr5#u<&#x>n=jaK%qf! zr!*J->iW0ZVm;QUZ4q8yFMal64`P-Lyo6AZqPfVDITH{mhPzvPGHtkp9L~LH!RTlX zt#rxO6OEPTgx&gUwYrCi7H%n1Q}%K_JzR7xB{X}1F! zhYJnnNswul_&L68sUWed(_Gx>)xY@n*(^H<{(1_o0Ig{RWwdr`v5`vBZ+)HJcmQVZ zfBvO@#}Q{gnS*?J*w2RoSair?$#!EiOp7wB*sKysCP&60mNcs8%sS+H-!k04@kv7o zQ)X3fB|bH6)}h;P{CTIZ6_F1*uFTP!X3V^Mp}UYETpH|F)yUKBLLhZ9QSlX_mREd= z7?0f^IU*Hu_Mx~uz%ltVd-VJeJ*=1-3}^b=GWC73Xd$WmF!Jrz!?jGRbHx<*9=l4; zA%1HsL7iqLm^+6^G*OnSXKWsQj9{B{#wXlpRqN*rSX{U!v23#ZjD~#Bh@gVMClxMb z63Sdjh2tD*UB)rDBtQX+^k;>k15u|z_PIps!0DP!Va*JNdLY^g0mVnVfqOzND}S6< z4446r9dsGHBp*5mEwU>ppeVilfcn@ao?Ne1k;A62G2NO z5y1-B$8C$qKQ)uNLyX;~jsqp9_d>@Lmx%2+-zT530}~^qdIw=drKwf_pF%mr{03(C zLXMH^jC0rAlns@T%UMzp=z+tZETxxGD*so6mO6in@B`lN`#7q8Yz-1|U-N)z4sT_J zDHx@}>Z{jo)s^SI6b)$YgmsN1SfVshNKq6@Z$LgzsG$e1J}Yz%Qn${Da=g=IsQ?4c z2|;q`1b@P_xB@3eokbjH?n37ESfUJYr0obK3mS8m3avixrNW&xP927G}Kk$@wbm-iHb^~RGN0sq!PTCK~!<+SH;TToXB>+ zMU+;GBvCTaUEmnJiq!NgN>u->N9np)Xtj}0o2osT{{$`7vkYdZ`%$Lt+q|JN~WlL zJFJ}hdlXpXV5hF~w95H<`|8DpfO5q1_g~*hG+QMa4}J^0n<;%v!OLkj#dEiBT}z?L z7I7vVb&}$EH}Q3^=#(aP(KvdCO86a-P_F2sCVnj(hmJedQ=m*EQ_R8e_07bA$U5`M z*z4;meJIDtW=|p>)o4DSotB`DTH2NSL^%ilpnQm0Ky%d3dEcNuy|xc9tDqLOQl!{^g|^`IMCTUi9<_lJo@a<$g!a zA^l)u3VakL^E0SbEp$6ryYu_rLc#$B3{Pia^zQ{e z;|b5b{5$t1A$$HNCvG>#z3T%8h?Y8QjzV|*X zF8pbNdC%Q zX4{`4@R>-2C9i*Cx$Y@346b(|iNq-;kvt4ZC9=Fq`22+g2_6PczEPneL4tp7r)Lky ztBq`Wg($`5WvKJ-?*`?LBbj`YNWD7wX|VB|3~ux7xLh7dm47Y-B)H9^^w4d@>5Igt z^Bt3J>-!4<1S|-QOA=rHBO^VPcnT^;GjN`8%H+91J!|Gpky7|wws)r( z9O1^%DXwL1xXJ1Z!Jv9iB~`Yk%?+^-jxbEuQa3a-%+VtN7W{GvK-EBzV(Yh!&9{*S z_0XRPZ7N<6XnUWJil`t_I(tE63?!!8rZCZk;FM>R@VSBZbg&rX8{z$Cs&=sj{FX_4 zH3Y3#*!4d}qz&6N*B)xESEiK!L6+`!6GWC{v_#h;jBQW+NGFj*Go_g8fBGuMYX|>A zn#0woYc^iP&U3XQGxvr(o7gAfk|?h^w0A!V%?C$v@+?}=|9vonCjY)gt7^nFCX#Nv z`u#^^U}q$YMw8rR3TibbyqBs z<&AbkqFMcXtnD}#Cnm;}hP=Gu-!gGTq`79=}lDJ=M)qLu=7|)>GE9smCjZ-tj1^MH;$|YQVY2)iXJ_A9*k9()E6TUu!y(!OvYX6YDsitUjTF>-1nD>d782rShSDfd!EJkYl5B%iZ26O!D zioiJnHUt(Rc_^7H$DpU%omy6YU8My)kwt>j!Ed!Y!`v;%Q!_iQUx}mM%^tSi95Tp5 zGl}F=Y5r0EwODsZvxg0z<87bRtS9%*eeDbG=o~8?ojwSNu}I(m*!At2SrSOrTV!dx zAEkjB{l}}ljzj`wmP-0J@fhZ!;s585P@^sAAoV+C*?zINjpGMT&wT>9l%$ffSL+mQ zRrIJc>kjU4)t?rgyb)vt3z&cmwIU1nT{(Y<@5gqos@{ln0EBI~whA;2L?S#q2L}QFYLM-1TNSoy(@Sc7%Uek)yIfchUTd(O37Cu1VyK zM)l9;4c*D}D@LC4#Y~}lUv_-+*m(b$3wDvCJ!$YN7ujK>c4x>_+Bc9%N^2Y8sj^qh zz$x*xgJfGhk57f5pfN-y(0)a#2$)IP@gofVrLN{|YNL0M9RrfIkZK(N+=;#q z#5SJZ$QA?x^4ff5Ez`s$BCppr_$UUET|r1)+xANBIY?vTIO|k7?>NKNMluMVut7a| z-)gR=*+woeV%OjL+CLJPa936k3_eS{)IZcO_08+)I1h20Rx!Vmz<*9bYw{i&#F)Qk|Np`woJ%P1P zAXbdE4WSwdY8-q=IEg+(Y)fvoxi(b);H+*9-?~>3ESJix8T((OGa1r6VC^;iN6)4? zvG_GloVj84{eMCw#=0)Mns=+=pmn#;$R3c6Zp|51G?Irm`93oyELRo(JRFn%8exFm zOC!m3T>jbrnp~;&{!CJpFrDw#-JZDePS1c}BipXnE+%aAHMlK_L(0h1af-*>*p*V2 z97}7D+V+*9X<%La%$d&{Hw;RNp6mp9cLO>0i*81{Y#-}tF0t$BTySGSnv-Yhz>ILN zzIedC4@WFx`>ztl8~XyJWf)wwU^O!AK@J?)my5oOn4g=^J=FTr|6WErD-;p4mqYc?gw(2|7A}Fb8wRqi6%Ipgyq#LeCQ9*5 zOTG&&Ic~_mO589;cH^|+`>Mhc!-OQ#nqKRLd~h!bpdK&@6|!0&nVa`ZZi$ddW@noB}HVq=)!~tQw!E$#$OH_?MNuJsVS7F+9$o62wi@?Bt21WPcGk zp0cXv9nd66g&}v+kw#XT5BGOsxyKi5IW{;3rPI8}3X_5-l$zaI zu5ZCtM2cPc$dR2-FQvp$ntn;{)WvqEhk7nT{e(UZL|1ZvD8RGw#<#cAs^>To&)(T{ zg7r%yrkfqaKgM2-gB_gAeUYcDtu~GB99p-DmZ2)4QxpUQCSJaTlif*&0k-iN84|m@o=nX3Ovtk3cCO1&E`)#3xlUekQxm1_aFn~CW#yS1` zoi*9;VG+|z@swmPvvHJCNh(LgdWtt2635M6Uu9hWwplK(^MPHQm&3UrqVh&y9$1n4 zLi%xgy2Gw_tC8_Mai^8~Jv%g}S{{c%p6}z{@DNn!!LhI;XPJ0#`jrJ2$m)XN@ZT0F zAh6dL5K)p7#^fZY$|@N~uI484EazQPZ3KSJi(oqcuBvveoR!bOid9roJ*{CyVxWt> z!{&*kW5-H8EcNJ&aGD!;W;1@joDNq5_G`4wZ+#&EuK%rw{GY@N!<(%^W_rgiDw%v7C%YFfL_=OH%IQn}U2c2*oAneKw|3l&k;j#zlFqjc1pkf7YwE{Ef7H!Hlfe_CGgK zt_U2f-Y&bh!|?h9me%p+X>!3KKF$csVGkar@bvAtcJzT{DUPdd(;orn?Z7x<`hs2- zv!-puNiU!o9!M548U;E1=qBgUA|2Gea1k*#`o&Vs4RlI_G(em-O*DR@4SrA89QoJ_ zvs+VW`8cj|N-=OruMsdz&s}C3!?DSBP(W+MUvW;Kd){O0uy#jkbe0=E+M3O*ZTWur zM&VCpu1NgEUB*3~FBIP4uFs2-aY+hzQ^I@JAENmqAab%oMj6-itNFN3=4eu*(-f;| zrQY;z;NJri3fig+)32TlkOlk6X35OJZz4*YF4MJ2zZ0*%@?EyOP8Cc$C>1>MN!^`| zXT1USmd)M{$!n5+Rmoi3ey_AJ-AvI{^)`=08X_vARY1Jdq<-u;%Vn6rter&*diCJN zULBs8Mc(c%&e%RuT3028laN&~ z2J?_R6t^Z>bu7 zeYl7}dw<|~5W(gND>X=0uwgghVn^u|Gh8IWF0hz8B9De;W*E18>dnk|Hu2MDKi#~7 zZL-*e=V^%Qg}eEsFXwC6-)Jm)rchyXr#s7AWtiS1YHB5r~SqT1j z8!J*w>q^2_U+fxpu$>2uJ)f#b-htVuQ23!XuNbax{2A=ezm z;o{!L`FOM&$%BOyH^IdWiw9|@zpWniq9BV0aVAruj51W#IV1nP4&00$#xDW%T8eAI z66umR1wJhzMlLIo**!{sHF+;~h9>QzU8p;6|9su;HR_h*8;xyicA>^;5)GTuc4ZGe zwVh!x;Mnzc?u+eT!^zC;s>MiGFn{;E%gzgR5z8+;eG$^c?AN`OcsgWGB$n_Eg*LrK zl7JfE%=Qy)+|={VWUJ>S9rb>0v_!0O=$w*8ceG%Wk7*}iOp=9*lS=E?B`Mc$IGai= z%XMXKfS%KG?x~&YTP4jL!9;;{Ndj%<^7PTqO|GT`41KPeSPl!`&*c`%3d->bN?y@Q z3yM^iN0fPf9%Z`?|EImT?2035*L4XHB)Gdb?oK0(TWB1D26uONcMWbq(u4$ecXxMp zceiZbcb$FKk2t5k%uzK)`K(8-`P4m$^rGNUdz8TjWSQ`a85r_CSiLjsiQJGf%dd#n1))b6U5ZNfr3y%7tRkVO zA(5&Dw8HQ7UiUYNQ%Mm?vCXgeB<(wdQ<;ozkv#fMs<*Ei!p_{>anE;FWX#~aV@-*n zH7XjZHM5D`ARBB~t98KfTC?RXCbZcKwT|=}x`=*!9wEvzs|4_~trGVs2Wr6&G8Qve`0muI+}%DZMqkxc+w_GaLtLoNX54J^?)~haYz3?DZ^_q{nk=7d{_IoWtiLk+ z;V!PL%8;9Ies*d)F#SkRXM3MGTgczk0zql>5z)JRWH;5klH{>Eo-iQx06ufv&ssRZ^2%fA@*VintViupEA)mz%3~Y2e@$o8|ml#rQ zhQ3|6N=?h$yt_&T57gV({EgkMI~B?VnqK(6w@obl)3QFRYw^D7?ibf&XGE+irlw3K z%%`pr)pUJ#VH%ec_3?n>myyCfU5xklzjH3q0&8;966otEhKK7fi^)$BoAifom5u#+ zd>>g!AR~w%^JgYI7?YJ|LmXZYmQRdVFJqK{hAV z=Of2_x)Q6d5g!3Q2`6eZ{uIRU0Itm6Tk*Kc1JP24n^OW(j}1RA+9f4soM~r_!TzS0 zSF0Dx9M^d-QlJs#k*d5dEG#||twSm`xth%1HhaEa;LzX_R`X=ZX~bnIT1)mPpQEvf zpJ9eEp4eYEil@$1jidU|Lz`Z1&|CWblF`RGl70j+JUd1E4vE|N#u@lzrDuCn(ThO;PZ4TGDPyl4X0enu8fhZ{?_l(+v^E%l--~bl3>c!pgS|78 zi7Js~@97+761Q;9}Ny&7blN=wemh+#{@Q@XMda#CGr`5whP2-dO2 z8_&uKQy;ljQkw~r=Dm~G&j;-d4Rs@T-jpdUrcW1@2#>Hy{gkYOQTpv0^U0b zkaFPadbb)d3U5c&s@xR#Z}^sLCOwf9v^zwReNZ_Xd7WMN_A%>XTdth6?!+U%zgbM4 z1~69W2f%_!KPe|WDR1y+Ovk1Qj=y0{E_d%mKH7Tpf z*`RVdk(w0DC#-hQZ-gxJ#TRu0&f1+5iWqq(RFbuzk(;Chsgs~uC1iPh#);u>IkNLE zIM6@u!#HJWyzmR=Q4ys8oC+szT3?L{9d;OZ-6klKvK)`oo%DEEM&;NL21jG?0SWXS?d|B?b!VCvhkerCpw598U}D(45`$-g z_}q3B!H|cGZOPRSw$p;WJCEDEmdQTpx{yZ!VQyG`Eq)Y9oY=?xnk>W&M9#yJv%~1SGSE>|=ky&eTtTC-L=A5|g;rOdpS{#ok z-u}R4o>u8GWyYM^UsrMCD>cM#=W_^`b?51#a&W)E5LYnAjWw4ZtW#7xtsV6P&D?RPF+G{sv2G8n;fUZ0(Xm-&X zD7=&2KK>}VtyaMyg@CZofOmo8U--Z5HE=TP#93t!&G@4LM!}t zOQm~G2DxZN%7dImKU>~f4DXM%4h82(HdhXJohxU#IwebnUUacLu^8nVh^7Vn%;-{m z)hBJnz|H13JL$sd*F8S1EvTv;%pJ-df%=mqWxs$COZ@7lg1T^KAd3E~AlC2{`S~|Av?W=bYtt>BQQ{oX1M3_@(1hCz-QD=%R{PZ*JnkFV9H3}n{Y^VDd z+Vrd|S{=)S&1z$nVQKl!heR;%fB5;7?g&$==of1RwQ^#)GbQY_T91gC*9+6zL!OD9 zJ#8A~ORU-g=*tWJjW7nQOX?p{I=f5%QgCV4HN z*t0c{*Ru)b=8F0B=5?8C6)E#_g!PwC07>_8h0eJVbMge{j+}R(7(f3J5+Gzi2rT1% z_%l(Rc-*80K;|ZuA*QoD8ENSULfCHSaU1=X`x1%_(mNI84I&uuz&>G19dz`nF> zUKHf04?oY1=xnYmAR&zztAx8LnA;Rv)rf+%0a691Y+Ra5#aZW7yDtn_c%KlrX?mrT zvK~kG+^241;I0x`nA()}xa|!wJ935Dg(d*o>le@5aCuk`m)vb1m}u;_?1_YmZDZi~ z(1tNom;Mz$V1N2Il15g;XDIdQRxwy172C7&neFeF! zmaF(L%|@QjkCm?Dg^|aa?D^$maf*MJV;$({uB%%hn7kE(eVEU?r5o*J@IbfK1_rdW zX;_+>wP-YM=hEXXZ+4OrU>K|e_-Si~Z0A}FXK+NJw}~ri3cGx6G=oJnAg2ep&P&j;6k4@-+26h4@oW(3Xg*D% z>WHLjx?D%5Tq;-^Fy~iOQ5Y?r&HC;t35PoqsVNhNu<_&3w{vp#)*;y@rFCV|@c1yq zXYLeR@MhGmN8NQO2%ib^M`ax2$oK$9@%1yol*et;_x_i+U(wqMgd|4j96-pSLCuzu z@6jrk&%qUQ_TcD&MG=AA#8yFAZI=@?Wd`sDt6ow|Un15uq>npiZLxq13;k2Z(jE=y z{FrqYj{*e0N+fky;X-_r;MB3v2iPxb$*0`Sd`P}P4%e0z$yU#b=R zgZwZF6Y0j&y>8Q}KHKq%Z;b5=g}P|2p!#AiYQv6l0XHDThVl zDfG>BXrg)xXh@(U^n%AnosuEnyyom#_#&c9w0-tFh!0N>!xUxcjJks-nRDM>H5ROT z7;i6{HcLx9;_8>)>)kL(k-E1FD#Mukjk_p%N-WTPBgaj3!_EK2$^jN!jK<7yENE2C z)zxTdps&65mVAJ(ZxpIRGcr-PZdWKxDMw-u?~K)=aD8}zIv-V_7A((oryArW9hI$Z z1Ym0^sP*X1C1E5iZ@5h~>nM4QK71<&fzLSH&F7d$ka;Do;(2643NG{lRf3t#&5PzPtU`N-q267^-;`D z5|NJ!weEJbxvAM)!8_=xuEra$SGbktxJ86n|HfO}HR`&h_xKZCBPT0*W3?8a`(U1s zgoll1IGy_SK9$4ee$n7+Rw0(#!B}m*h#EAV7ql3_ZFBjSa(7r@#h%bJ`V(^?K%+qC z9Fk$RQ|~JiiQLUGzWnE)wA?l(>FKx`+R79^;Oq9Y(qGkYqFCu3t!VdZhNtQ-AiqSO zC8hqS5109+`LOriQJH!DsTSE?m!T#pkS24E)Z1C$T(1s&gm16y*IJnW>F-YG3%@XW zM8p0!^6}aD*7jfsvq0*^S5XTCj$-EfotkSY;@@`cv$=`0YHwF(6AlBCPntaLUx-~C z)@xg)Ct;gTOt62GVc`U7e$y>YQ>XIy)4kgnT2cD6f@ zG5_>fc}(0mw<|N?n^0I@d`oWfQ-uNOoJ0F>%?KT)EQ%tOmb=iKi3wsA%wqIgr6)wM2^sD5 z42!%UH;i^6Ix4u{6C_()^cF)BEo%XJ1RXCXs*)m4tEfro=A4Ftd^j*U|$`%9kRn`1-@+)54g_-!%YA z(*Y5(@9v`mZ1(hewO-SkI)t`|cBf{1-?0jsl)s5>C~jl)$1Q$U-_dic*Y#ZdYkfofCiO$IUFgGX~0O28lkRv$A zXK;u4vq19jAf?jC>HDAyS~B|R2HTKaTTvpmXZA04ZZ?wmTUe)!VDPqcQBgQ~OxmE) z;svLX6V1@j6d_rODi-3{0kM8H+yM5~=kF^vWG-C}bJ})B1kpxt15i!*ebeOU1oks? zT@GgZ^{|=9ZnbyhVWwA%sO%eM0JVPO(B(XrTvpD=4{`S!EXw*EV`aRzV2SQV)}(>K zbQeg(DmeLD46X1G3~lj<&E-xM6KdwRd*j@iVyQ!%^eI*UGF^trR9Kl?gsvFhNR*sA z0f`*HB7HII`%NLG8_m_Gqiwu7ydwT9i|25h1F4PAf#MTmj#Kj!TN6iaF6murTyDSD z*Iem~9h#KL&;T~QLV_m^ZLRfvVS}VVTfXgcr>)W5jr4uaw>V4YkEcXM?r~w_6?e1( z)?jbz-nE1GBf&s4Vp#&~#3yIn<`ZR5W=PcVPVaHbr6fy=^#3@b#pm3Wbm!OaUo(!f zaE4rYC?>*jV+!w9=$MR^9o337HViE$4m+F1B->tBgF6mcCGJPuFxokZ&IyME_4p$q zPHzPJ7x{*S1vfLYH5gEyO)0~XC1X~LhxOgzNf=n^#iiI1{W#v_Z*QJL47;fW>2BGd zZiwCA%Q?U2B&AmGL=pRgK?6eC?jAts?Fx!DDZQ}*_!BP+hn39<)%P9JQ$WWB=N|i; zJ>(9|P#7Grni}-t4Hg53_+Yc+u*DWNMotuqn=kT#q1Y#m_ ze)D;U9usl|4E7Vw9X6xMHxsvxy{6_@66zgjM4zgP>y=XI6~%ERH~A0{t`g1j67p)& zdrdfq2h5J_uEBDq<(b-DsR;=F{ae=u*ltRS{1eooZb!c&ga{ahhoaU^0}%l9Uy{ z{#(!tgyQ~$Mp6XhQbZ&&9v+|SPJK2zpUc054w_aZAU9OM^)_eh;v>7W$c;m&O6=08 zh2AjuYVH+b$(ig;*6}BX1Nv_CA3e=gS+i{=Q%QqUxFoI z*qDbQs&iLTMEXG4LWdx5+(mtxu9JVM8e3Y8-8phFtJT7XaBqM_9j}f|I<+ zT;P9>{|P#%;qj%lw>IR6gT|8>S))k)mv88;$3E>|$ja-ShlF5$EF`_y|8y zgQTenI&cE3L`3#oGRt^i2l~2ANCTEGTwc9(d;1Bwf)l6xMzPU4&>gE&EU&+M^%s!D zGl|QJkq^+Er0F5Yp&mw$_ zHIj0FA!K79c)(vJIt*@~{TgCqW6Ll(^Gul%-vl*Xn6NjpENV||D}C$v1df>45bxFoU&%l}|zv87uvwrkpS3z)G zIWE@#M0CLBFcL0kbh+NzACnUr-S~S0rxH%~CHm0!qo(G%h*#7JV?YLYD^NOrhb zPZ-SH(?TKrJIDtttrJAG<chZxiLlq z$63F~!a=dJF2}PChp#p?DJU|x9#wr(Z^f$`|57byibDd6(hv^aw~k{ zx`GdsmwQ`_m>6rOBPW11Y~}A2u{J~|EUx%6sK7fIGQ>_KU8 zT~|=iMqws+vWe3hE}D_+db=dd4FvR84L4F69ADlYoow)wyYl_Hj5*1RxT(7LUoe4$ znP5iLY#dqJ(@j)|(Z0&r%Z+_?7Lke6_b^JpLcJN%%Z&`EO|#FL*_cVeN@919HkT#*5JpYdB6qC8U{mX73#|i{fKF?Og5eeR)85Csrlw@y9C{?nLa+xHnBLW z#BKM~qE{Iu#i8OIGrN`UMN2x?m}06oer%H$Y*KdNWsK|dnx3*UD&q%$pAT_fEqP*l zOC7h%#DVsiRSB^7w$mITP3ynQ_m`AFT zh%L)!v^eL&qAog~dvg2qSX=G-^h~^un6S)9F}p&J@i4sAy8loL=;%6r*d~;h!FJF= zqUl>!+t;{SvHtVQv0Tr$Oz`$anXeph8FL;jt@r9H{Z4$LuRGYkOI0Nwi_<^Q!qdaJ z(Kz;fs^Mv#3ghrUly-WVbO-sFG5yVSz81L%#`t%~!RK@zCMlBE!{EQj?AjfXcZ6vN zhgC9-e<^T97xYe!zKsov@<+|k*R&u%8D*_voma043_P(8eNY8mSljHsPK_RETJv$; zbKwm~4Oij@d7AzMimic`+ zG`}9DI0l1X<6KqQEKbRdDt*jAC&nNybMA=JU=?>~Ub;2(5glsbX3NEJ_J#%s%`vF> zhow8(v8`UN2jyZp%>BN^l~|*$t&)0hupcu#`J=q{kKw}Dn!X?ssa4j&yW{hDXj@Nc zFWb-oUsp+AoFH^qo5ya=95K$P57Vavi~XPG5(N9>(L+DWBKE(l|J`cv16^sG^J}M7 zeN+JDPXs#Xk7m;sf$r+6oo9-W{y(e#-P%b<@w3X+zq~Ebju}Vij-O3sq6Mcy-t?sZJe84cd2swLb_kP|zHQ z>A;mGvCNAo{6lT5g)S0Qdt_dxq=nNs55C9(qMY3n?+wH0j?q(nm>9{G0S5uQAWGkO z!>p|l-)eHSLggH{J!(Zu=kLy2wG^{hIGAf&X_!M=)&>w6`s#RH7c$v7XUOs+G%Yet6k8G^sB z<(zc*2bCJGgF#>rxMUGU??FSh>UsOlx<*TY=6JqI zQ>aOWwzE?8)S{Rs==EE>k&Bx6{T&y3p|)p!=Hc^GqF2++RcT?BJSgnP735J z!%vI4+S2Ri&fU&&CW*mNm94JZ6@ki0z8R!lVu z1&_?4H!JIY3#y|9$NHF@`RT%$hv(Xr)3Gjk_Bk(g?4*wHT0-iX8TVq;@DTqjjt(!+$0)WegJ zu*Dn2Hi($@VAypKit#QXaM8DX_WN*BYtu>kC(UsUAUEu=t#6~dB^>U=*MaemDbH`) zC{4mOr$klO`qeQ>n*{MVAvD`_@_imgE23bOT8Aa(dl(V75JlL<8Z4CvuoXzq(y*$df7u&Vapp4kP_UV%fkY;-w_b(pbgPN z7n=#aL?a0CB6*{;+;1{sDIB7tSPT-$91T(9`>o7iafHDGD8x5vWCdqO(Y?RcH2g(_ z;c`b{ye=-G3|;<&Ygib~;;N1#_qIcFNA+t4ivz=D3h@@Ngm+ z&HD>ugCmd$DZQ7y9Yw)Vn3m6ku|L2IR==~b`&JHVR|XJMhW@f>{yQvx%nb; zVAMVj6#rEbL+|P;&DkyfU4!SgPhr0q%2mRQP-A3eRJmo));4ALcu<7EzTN4HD?5mr zn)5ZU(3(?>LN3qVHq{u8EoA^fl%Ce|*icV{J^m7kGm4RaYQQPfVM={vlHW8L?0TK; z+X|7}fx+l?ezJ^6m=|BIVfT>Gx3uGxB z{aqAjzKzO?n?wi`uAF(8ulSWRmP#?o_G{D>I!?X1>=sE;B!GZ>+_oN1J#CuaKMCAw zSpA;vnilnEqUM`V_478mgFoehNo;7xQtf3&ss@6QnFUP!iZ)3cFG5aXeo|p5>|CIk>>3 z5GOu`DSLb_pdXZr?9_O##6T1^OR{G?*UO0f^+X@GR9#yh`^J(`)4w>$L`U6=_IErl zu3ZoQ+|!sgNjAGkKRYii!aCJtYkQ+YJc+OIBQuKXWdCrVKsd}bHUe~XEHyhb;vgXT z>!y860`a+Z`Y#i@1njO^R!EVkZG3WOymji{&&15kq$nC#IQY%$U$1M11I}c)I}jj! z<5qXZqlx?H4=r0kUZ_uj?_vK&V>y!?b|h;m|JK5(!xL>GUxv?ZkbT0{KdoQ7nglMf zy}abNHiw$@21#Z>yaAJQla*ByV8`jJeQFmXlxql_TK9a*kCkz5vA!J(2Tj$>!YS}UE z??HY-N=W3#at39h`(hc*^GmCc$Wmvik_e$wwkX&{I4C7J*?FM-A0PTOd4#c2xW^v- zS#7)clNOFJ{OIx#xj~c&oDga%uAhN4bI{02atG4P@@`VZxcdO8Giwa-vYoOK$ky@R z(_&l4C;2<(S@aAxcWpo64^t;mlbbN@XW>@?T;k%z&>XVL+8@aUN$GTitb9nFd|5BW7Sb zi&?Jdrm;OlqHbblhW&Q@1-xg#CqRS$rl!oCn;crsLi9cnows?mZDu0%y!E;Y`}lbp z#6fo*WM8MPv@jd!@1qi?cvykWF&V!%M4{PHg5I&-+uHJSbwXsmd5mOkOg*$}ou^KJ zONGWI`TV~q7>?f6#9{IHWWV&hxjG)4o#a9_(_!w3_@o3Z0p z{BJC|VdE?Ue($q1Hh+(LpR_OrBjQCka&9C;CM%=SFo`r+&d-eW)IUOpz#Xcey$6qr zhq;I`0i!0zn_$|H2tyvRo#racVay__qd$h=9JAN}5n*9fJh@Bf#o=X?E?bD3u;7!` z@u?`-14`_Wxb<%->fP8`<5SX7dP;K0_M2?8WTZ%Cb*z)YhqQ&F;zWAdK*Ms0OWsy;&Z5NiFfyl5XJo;$9J`787eJ@sQdD>~w zc=D2-wnU`=enZa`zQ(Nf5y>G;1|+|INVYK_eEBofPXGPK8-)-JLWxW2dylnFRaq&4 zC16S(xude8d5JCbtCUkNoj>vn-iX^CmDY7ji|}9|!$;ihfcz1i#RW?6f?Z(wawql8 zp!!l!9Hw_~l7D4*T&Cn?58Lg<&}O=Nh%myJFeo6x`|1MX6a`5nFJ6{)M`hy0w0vo; zw3}l;Nx)%dbiZ$pTkXEvB2_!9U|`|&f1>-m1OO^_)DTtR&XQk@DJCYTMO9WBBozYM zp9Z#${Li}we02N`=DCsdaksQbdEN5c zOFSGce`Edlw;NCG(Aa%5mh9b$t$rPil2LHw69A+a$9~F?POnoU20z_azM?)C{?{T$ zVoXYJL|#p`VPSuY)K`^Z9?OM6Y22w^YCOrtu1#{l=6NWnEWtY?Hf^sVC*_?0v%%$$ z;DWpMW5yx+I;r8u&);lxJMhT-bcg9Dz5_@( zh@!i@?^u)%< zi}9toL-$eB6N}>FhFcag3Wp`ERS118ZK{cYzgWW@bjWL# zu^+Er#yW7!-GVNrUv;71f{s@lnqRXXXNO75>D!Q05Ui}T-m{s^(~}6K5loQKAk0;r zZh4tBJn5UJpZX0l5EsoMshZWMkq$?;@-PW&vBuT<(!)K=b(kF4fx?$3veNu zB0?Z~dkF~(@`Cg~zY`I6p#blqQ!z;==5>T2MrLMA?OtxUHm+lb)-^;5lU<{KqiL#J zWAJsyeD5JVe3|@~sfUs<@(MXFOmPvYznYj3>g@#PKGN)KI~#mixquY=$86=ksOKrx z0ZRM3;EGG5l$uF)?N?Xml3$)#%aE*ZF4GzS0<<+Ac=_0zhzKNUF5kG`Y+8lMr1&|$ z0lYS`Opmu}?C?>_=r2fOP$U7`C(9UPfqY>LxTL4?{7Lj!@YWw*j~xFQQ_fOaKM9R? zg(^^SKJPyTU6VK?W}CwHdNE4R=4T~mB8)Hceo{>DFE2M!(^IiUKk!Y8rJSRhqc6`( z?4&=9{YqmQ7CRX`JC%0p?Uklv73v?r z8Y)<@jStI8fDf@V`+O#cERXDMSEJA7NDvH0bD^L2v*q*Rm$J3$;$ifGb&}sOGf6Y~ zvTL=u8(i}XgKym6WFwLRzjHc#OTW8}lZse=r~OvJ+=+9rXEb*$$mPWFc0a9sx~?Ht zlY!>rK?(y~7<-^umQCHeQ9%D?OQtz(MC+944;SQa!)?H6c1HJ`=r+LhIOSR^b@&q9 zVOn=i5=9@K_}NwG zVTy~WXDOn|=Ed&xm!*xhAvNZ2p``bp1s~i*{KFrbTj;u+07OJ`-!g~+epd?g^B30L zU&mu2f8F1tJW9MBa@^dhmcr&HKM5`-UIbm zYY2H?Q~rGD-?d$lbvD&%c(aUKB5W+&7`NOnrkn0EB`FdD;8u?Y^Pv%8+DIY5XPBUB zSKFmD5`FN4IPe3{sYHIe3_{)HP!pad`+FGYCB?J$pwlPhb(oMSVab5R2K2| zYPKkCPgL_z$?zwNC{et=j3&+Nd|~*Izr8aZ(jhR`3=>EI0!Ax0<_z`})9B;`{HotE z2Cw*5ZiJ2TPl*|hlJfe-G*eJ@-)4>qceT}K2G2%>(^)?;EIl09%fDO)51dDo>X5Mp z4AG)-=vc)_vazJ@$>7@~o^sACmW%mF^!lhzfM%F{qZbbAEjd**foh@>gX=rV0 zFE+zxFG6&q4XUIPjljpmfWn;}_xT4FHBZ7-{KlS-BU{Rqd}HU-O9*)AsapRU$B6nV zO0MPy zDR6dm3z?rJmj@MQAlZzvxJ7q@QsWJ5WLX33?SfLv3v=bu4cTE$bH2zm1kTFaocjsZ zfyEI4nav6qY(hRpl;pwDSxm$iUqS{Od*9LkU2n^y@+@MG9(e*}anb{8v@q$TY&!zL zz00O^AJY!7_&xScwx|)+%3ednU%&7h2U}O)VfOHoi>3j|bK>sGFm(5I@zZH}+Q%7A5JL(%auL$`T)loy^qa1=fx+YCWO7i&F=RmT@ewOeDYb!t?nR}0GY!~q&h4l> zN?O8?%c0SZ&I!D$$094KAW<%65cGclM8kc9 literal 23204 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 diff --git a/site/img/shared.png b/site/img/shared.png index 7869daddefe9f5476f678395e3aae91442ffddb6..b079ad0c6b4e5209b343cc7b4829da9c97d329cd 100644 GIT binary patch literal 20925 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 literal 37973 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 diff --git a/site/img/shared2.png b/site/img/shared2.png deleted file mode 100644 index a1e04999eb3fd3bd7c350a0850659740d4a8cd03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23204 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 -

+
+

Apache Arrow

-

Powering Columnar In-Memory Analytics

+

A cross-language development platform for in-memory data

- Join Mailing List - Install (0.8.0 Release - December 18, 2017) + Join Mailing List + Install ({{site.data.versions['current'].number}} Release - {{site.data.versions['current'].date}})

-
-

See Latest News

-
+
+
+
+

+ See Latest News +

+
+
+
+
+

Apache Arrow is a cross-language development platform for in-memory data. It specifies a standardized language-independent columnar memory format for flat and hierarchical data, organized for efficient analytic operations on modern hardware. It also provides computational libraries and zero-copy streaming messaging and interprocess communication. Languages currently supported include C, C++, Java, JavaScript, Python, and Ruby.

+
+
+
-

Fast

-

Apache Arrow™ enables execution engines to take advantage of - the latest SIMD (Single input multiple data) operations included in modern - processors, for native vectorized optimization of analytical data - processing. Columnar layout is optimized for data locality for better - performance on modern hardware like CPUs and GPUs.

- -

The Arrow memory format supports zero-copy reads - for lightning-fast data access without serialization overhead.

- +

Fast

+

Apache Arrow™ enables execution engines to take advantage of the latest SIMD (Single input multiple data) operations included in modern processors, for native vectorized optimization of analytical data processing. Columnar layout is optimized for data locality for better performance on modern hardware like CPUs and GPUs.

+

The Arrow memory format supports zero-copy reads for lightning-fast data access without serialization overhead.

-

Flexible

-

Arrow acts as a new high-performance interface between various - systems. It is also focused on supporting a wide variety of - industry-standard programming languages. Java, C, C++, Python, Ruby, - and JavaScript implementations are in progress and more languages are - welcome.

+

Flexible

+

Arrow acts as a new high-performance interface between various systems. It is also focused on supporting a wide variety of industry-standard programming languages. Java, C, C++, Python, Ruby, and JavaScript implementations are in progress and more languages are welcome. +

-

Standard

-

Apache Arrow is backed by key developers of 13 major open source - projects, including Calcite, Cassandra, Drill, Hadoop, HBase, Ibis, - Impala, Kudu, Pandas, Parquet, Phoenix, Spark, and Storm making it - the de-facto standard for columnar in-memory analytics.

- -

Learn more about projects that are Powered By Apache Arrow

+

Standard

+

Apache Arrow is backed by key developers of 13 major open source projects, including Calcite, Cassandra, Drill, Hadoop, HBase, Ibis, Impala, Kudu, Pandas, Parquet, Phoenix, Spark, and Storm making it the de-facto standard for columnar in-memory analytics.

+

Learn more about projects that are Powered By Apache Arrow

+
+
+ +
+
+

Performance Advantage of Columnar In-Memory

+
+
+ SIMD
-
+
-

Performance Advantage of Columnar In-Memory

-
- SIMD +
+
+

Advantages of a Common Data Layer

+
+
+ common data layer +
    +
  • Each system has its own internal memory format
  • +
  • 70-80% computation wasted on serialization and deserialization
  • +
  • Similar functionality implemented in multiple projects
  • +
+
+
+ common data layer +
    +
  • All systems utilize the same memory format
  • +
  • No overhead for cross-system communication
  • +
  • Projects can share functionality (eg, Parquet-to-Arrow reader)
  • +
+
+
-

Advantages of a Common Data Layer

+ -
-
-common data layer -
    -
  • Each system has its own internal memory format
  • -
  • 70-80% computation wasted on serialization and deserialization
  • -
  • Similar functionality implemented in multiple projects
  • -
-
-
-common data layer -
    -
  • All systems utilize the same memory format
  • -
  • No overhead for cross-system communication
  • -
  • Projects can share functionality (eg, Parquet-to-Arrow reader)
  • -
-
-
-
- + diff --git a/site/install.md b/site/install.md index ec30e0469cdc1..f795299676eb5 100644 --- a/site/install.md +++ b/site/install.md @@ -20,9 +20,9 @@ limitations under the License. {% endcomment %} --> -## Current Version: 0.8.0 +## Current Version: {{site.data.versions['current'].number}} -### Released: 18 December 2017 +### Released: {{site.data.versions['current'].date}} See the [release notes][10] for more about what's new. @@ -30,7 +30,7 @@ See the [release notes][10] for more about what's new. * **Source Release**: [apache-arrow-0.8.0.tar.gz][6] * **Verification**: [sha512][3], [asc][7] ([verification instructions][12]) -* [Git tag 1d689e5][2] +* [Git tag {{site.data.versions['current'].git-tag}}][2] * [PGP keys for release signatures][11] ### Java Packages @@ -145,15 +145,15 @@ These repositories are managed at [red-data-tools/arrow-packages][9]. If you have any feedback, please send it to the project instead of Apache Arrow project. -[1]: https://www.apache.org/dyn/closer.cgi/arrow/arrow-0.8.0/ -[2]: https://github.com/apache/arrow/releases/tag/apache-arrow-0.8.0 -[3]: https://www.apache.org/dist/arrow/arrow-0.8.0/apache-arrow-0.8.0.tar.gz.sha512 -[4]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.arrow%22%20AND%20v%3A%220.8.0%22 +[1]: {{site.data.versions['current'].mirrors}} +[2]: {{site.data.versions['current'].github-tag-link}} +[3]: {{site.data.versions['current'].sha512}} +[4]: {{site.data.versions['current'].java-artifacts}} [5]: http://conda-forge.github.io -[6]: https://www.apache.org/dyn/closer.cgi/arrow/arrow-0.8.0/apache-arrow-0.8.0.tar.gz -[7]: https://www.apache.org/dist/arrow/arrow-0.8.0/apache-arrow-0.8.0.tar.gz.asc +[6]: {{site.data.versions['current'].mirrors-tar}} +[7]: {{site.data.versions['current'].asc}} [8]: https://github.com/red-data-tools/parquet-glib [9]: https://github.com/red-data-tools/arrow-packages -[10]: http://arrow.apache.org/release/0.8.0.html +[10]: {{site.data.versions['current'].release-notes}} [11]: http://www.apache.org/dist/arrow/KEYS [12]: https://www.apache.org/dyn/closer.cgi#verify \ No newline at end of file From 9e4a6e4baa3dc18380a8173b07bf33f8764bf7ac Mon Sep 17 00:00:00 2001 From: Adam Seibert Date: Fri, 19 Jan 2018 15:41:43 -0500 Subject: [PATCH 05/46] ARROW-1930: [C++] Adds Slice operation to ChunkedArray and Column Replicates `Slice` api from Array to ChunkedArray and Column. Author: Adam Seibert Author: Wes McKinney Closes #1491 from seibs/ARROW-1930 and squashes the following commits: 1f03793b [Wes McKinney] Tweak doxygen comments d920d80c [Adam Seibert] ARROW-1930: [C++] Adds Slice operation to ChunkedArray and Column --- cpp/src/arrow/table-test.cc | 31 +++++++++++++++++++++++++++++++ cpp/src/arrow/table.cc | 24 ++++++++++++++++++++++++ cpp/src/arrow/table.h | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/table-test.cc b/cpp/src/arrow/table-test.cc index 3f1c6be3a87f6..99e4dd5db5146 100644 --- a/cpp/src/arrow/table-test.cc +++ b/cpp/src/arrow/table-test.cc @@ -108,6 +108,21 @@ TEST_F(TestChunkedArray, EqualsDifferingLengths) { ASSERT_TRUE(one_->Equals(*another_.get())); } +TEST_F(TestChunkedArray, SliceEquals) { + arrays_one_.push_back(MakeRandomArray(100)); + arrays_one_.push_back(MakeRandomArray(50)); + arrays_one_.push_back(MakeRandomArray(50)); + Construct(); + + std::shared_ptr slice = one_->Slice(125, 50); + ASSERT_EQ(slice->length(), 50); + ASSERT_TRUE(slice->Equals(one_->Slice(125, 50))); + + std::shared_ptr slice2 = one_->Slice(75)->Slice(25)->Slice(25, 50); + ASSERT_EQ(slice2->length(), 50); + ASSERT_TRUE(slice2->Equals(slice)); +} + class TestColumn : public TestChunkedArray { protected: void Construct() override { @@ -158,6 +173,22 @@ TEST_F(TestColumn, ChunksInhomogeneous) { ASSERT_RAISES(Invalid, column_->ValidateData()); } +TEST_F(TestColumn, SliceEquals) { + arrays_one_.push_back(MakeRandomArray(100)); + arrays_one_.push_back(MakeRandomArray(50)); + arrays_one_.push_back(MakeRandomArray(50)); + one_field_ = field("column", int32()); + Construct(); + + std::shared_ptr slice = one_col_->Slice(125, 50); + ASSERT_EQ(slice->length(), 50); + ASSERT_TRUE(slice->Equals(one_col_->Slice(125, 50))); + + std::shared_ptr slice2 = one_col_->Slice(75)->Slice(25)->Slice(25, 50); + ASSERT_EQ(slice2->length(), 50); + ASSERT_TRUE(slice2->Equals(slice)); +} + TEST_F(TestColumn, Equals) { std::vector null_bitmap(100, true); std::vector data(100, 1); diff --git a/cpp/src/arrow/table.cc b/cpp/src/arrow/table.cc index 2cf6c26523965..14877ccb537c2 100644 --- a/cpp/src/arrow/table.cc +++ b/cpp/src/arrow/table.cc @@ -102,6 +102,30 @@ bool ChunkedArray::Equals(const std::shared_ptr& other) const { return Equals(*other.get()); } +std::shared_ptr ChunkedArray::Slice(int64_t offset, int64_t length) const { + DCHECK_LE(offset, length_); + + int curr_chunk = 0; + while (offset >= chunk(curr_chunk)->length()) { + offset -= chunk(curr_chunk)->length(); + curr_chunk++; + } + + ArrayVector new_chunks; + while (length > 0 && curr_chunk < num_chunks()) { + new_chunks.push_back(chunk(curr_chunk)->Slice(offset, length)); + length -= chunk(curr_chunk)->length() - offset; + offset = 0; + curr_chunk++; + } + + return std::make_shared(new_chunks); +} + +std::shared_ptr ChunkedArray::Slice(int64_t offset) const { + return Slice(offset, length_); +} + Column::Column(const std::shared_ptr& field, const ArrayVector& chunks) : field_(field) { data_ = std::make_shared(chunks); diff --git a/cpp/src/arrow/table.h b/cpp/src/arrow/table.h index c813b32ad36dc..570a650e7fa4a 100644 --- a/cpp/src/arrow/table.h +++ b/cpp/src/arrow/table.h @@ -44,6 +44,7 @@ class ARROW_EXPORT ChunkedArray { /// \return the total length of the chunked array; computed on construction int64_t length() const { return length_; } + /// \return the total number of nulls among all chunks int64_t null_count() const { return null_count_; } int num_chunks() const { return static_cast(chunks_.size()); } @@ -53,6 +54,20 @@ class ARROW_EXPORT ChunkedArray { const ArrayVector& chunks() const { return chunks_; } + /// \brief Construct a zero-copy slice of the chunked array with the + /// indicated offset and length + /// + /// \param[in] offset the position of the first element in the constructed + /// slice + /// \param[in] length the length of the slice. If there are not enough + /// elements in the chunked array, the length will be adjusted accordingly + /// + /// \return a new object wrapped in std::shared_ptr + std::shared_ptr Slice(int64_t offset, int64_t length) const; + + /// \brief Slice from offset until end of the chunked array + std::shared_ptr Slice(int64_t offset) const; + std::shared_ptr type() const; bool Equals(const ChunkedArray& other) const; @@ -67,8 +82,9 @@ class ARROW_EXPORT ChunkedArray { ARROW_DISALLOW_COPY_AND_ASSIGN(ChunkedArray); }; +/// \class Column /// \brief An immutable column data structure consisting of a field (type -/// metadata) and a logical chunked data array +/// metadata) and a chunked data array class ARROW_EXPORT Column { public: Column(const std::shared_ptr& field, const ArrayVector& chunks); @@ -97,6 +113,24 @@ class ARROW_EXPORT Column { /// \return the column's data as a chunked logical array std::shared_ptr data() const { return data_; } + /// \brief Construct a zero-copy slice of the column with the indicated + /// offset and length + /// + /// \param[in] offset the position of the first element in the constructed + /// slice + /// \param[in] length the length of the slice. If there are not enough + /// elements in the column, the length will be adjusted accordingly + /// + /// \return a new object wrapped in std::shared_ptr + std::shared_ptr Slice(int64_t offset, int64_t length) const { + return std::make_shared(field_, data_->Slice(offset, length)); + } + + /// \brief Slice from offset until end of the column + std::shared_ptr Slice(int64_t offset) const { + return std::make_shared(field_, data_->Slice(offset)); + } + bool Equals(const Column& other) const; bool Equals(const std::shared_ptr& other) const; From e4460847f3387c6c1a8bb77edd2aedc69e7250d3 Mon Sep 17 00:00:00 2001 From: Robert Nishihara Date: Fri, 19 Jan 2018 14:49:17 -0800 Subject: [PATCH 06/46] ARROW-2011: [Python] Allow setting the pickler in the serialization context. Author: Robert Nishihara Closes #1493 from robertnishihara/cloudpickle and squashes the following commits: 57fb46f [Robert Nishihara] Fix test (it didn't work without cloudpickle). a884bb4 [Robert Nishihara] Add test. 14e1536 [Robert Nishihara] Allow setting the pickler in the serialization context. --- python/pyarrow/serialization.pxi | 26 +++++++++++++-- python/pyarrow/tests/test_serialization.py | 39 ++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/python/pyarrow/serialization.pxi b/python/pyarrow/serialization.pxi index d95d582fe537e..e7a39905f1f65 100644 --- a/python/pyarrow/serialization.pxi +++ b/python/pyarrow/serialization.pxi @@ -50,6 +50,8 @@ cdef class SerializationContext: object types_to_pickle object custom_serializers object custom_deserializers + object pickle_serializer + object pickle_deserializer def __init__(self): # Types with special serialization handlers @@ -58,6 +60,23 @@ cdef class SerializationContext: self.types_to_pickle = set() self.custom_serializers = dict() self.custom_deserializers = dict() + self.pickle_serializer = pickle.dumps + self.pickle_deserializer = pickle.loads + + def set_pickle(self, serializer, deserializer): + """ + Set the serializer and deserializer to use for objects that are to be + pickled. + + Parameters + ---------- + serializer : callable + The serializer to use (e.g., pickle.dumps or cloudpickle.dumps). + deserializer : callable + The deserializer to use (e.g., pickle.dumps or cloudpickle.dumps). + """ + self.pickle_serializer = serializer + self.pickle_deserializer = deserializer def clone(self): """ @@ -72,6 +91,8 @@ cdef class SerializationContext: result.whitelisted_types = self.whitelisted_types.copy() result.custom_serializers = self.custom_serializers.copy() result.custom_deserializers = self.custom_deserializers.copy() + result.pickle_serializer = self.pickle_serializer + result.pickle_deserializer = self.pickle_deserializer return result @@ -119,7 +140,8 @@ cdef class SerializationContext: # use the closest match to type(obj) type_id = self.type_to_type_id[type_] if type_id in self.types_to_pickle: - serialized_obj = {"data": pickle.dumps(obj), "pickle": True} + serialized_obj = {"data": self.pickle_serializer(obj), + "pickle": True} elif type_id in self.custom_serializers: serialized_obj = {"data": self.custom_serializers[type_id](obj)} else: @@ -139,7 +161,7 @@ cdef class SerializationContext: if "pickle" in serialized_obj: # The object was pickled, so unpickle it. - obj = pickle.loads(serialized_obj["data"]) + obj = self.pickle_deserializer(serialized_obj["data"]) else: assert type_id not in self.types_to_pickle if type_id not in self.whitelisted_types: diff --git a/python/pyarrow/tests/test_serialization.py b/python/pyarrow/tests/test_serialization.py index 6116556386b1a..e4681e3a59751 100644 --- a/python/pyarrow/tests/test_serialization.py +++ b/python/pyarrow/tests/test_serialization.py @@ -555,3 +555,42 @@ def test_deserialize_buffer_in_different_process(): dir_path = os.path.dirname(os.path.realpath(__file__)) python_file = os.path.join(dir_path, 'deserialize_buffer.py') subprocess.check_call(['python', python_file, f.name]) + + +def test_set_pickle(): + # Use a custom type to trigger pickling. + class Foo(object): + pass + + context = pa.SerializationContext() + context.register_type(Foo, 'Foo', pickle=True) + + test_object = Foo() + + # Define a custom serializer and deserializer to use in place of pickle. + + def dumps1(obj): + return b'custom' + + def loads1(serialized_obj): + return serialized_obj + b' serialization 1' + + # Test that setting a custom pickler changes the behavior. + context.set_pickle(dumps1, loads1) + serialized = pa.serialize(test_object, context=context).to_buffer() + deserialized = pa.deserialize(serialized.to_pybytes(), context=context) + assert deserialized == b'custom serialization 1' + + # Define another custom serializer and deserializer. + + def dumps2(obj): + return b'custom' + + def loads2(serialized_obj): + return serialized_obj + b' serialization 2' + + # Test that setting another custom pickler changes the behavior again. + context.set_pickle(dumps2, loads2) + serialized = pa.serialize(test_object, context=context).to_buffer() + deserialized = pa.deserialize(serialized.to_pybytes(), context=context) + assert deserialized == b'custom serialization 2' From d135974a0d3dd9a9fbbb10da4c5dbc65f9324234 Mon Sep 17 00:00:00 2001 From: Robert Nishihara Date: Sat, 20 Jan 2018 13:41:23 -0800 Subject: [PATCH 07/46] ARROW-2000: [Plasma] Deduplicate file descriptors when replying to GetRequest. Author: Robert Nishihara Closes #1479 from robertnishihara/deduplicatefiledescriptors and squashes the following commits: 9be9643 [Robert Nishihara] Fix bug. 8a827cf [Robert Nishihara] Remove mmap_size from PlasmaObject. ab30d7d [Robert Nishihara] Fix tests. 2916e87 [Robert Nishihara] Remove mmap_size from PlasmaObjectSpec, and file_descriptor -> fd. 7f5c618 [Robert Nishihara] Deduplicate file descriptors when store replies to Get. ab12d63 [Robert Nishihara] Make Create return a MutableBuffer. --- cpp/src/plasma/client.cc | 45 +++++++-------- cpp/src/plasma/client.h | 5 +- cpp/src/plasma/format/plasma.fbs | 20 +++++-- cpp/src/plasma/malloc.cc | 10 ++++ cpp/src/plasma/malloc.h | 6 ++ cpp/src/plasma/plasma.h | 12 +--- cpp/src/plasma/protocol.cc | 49 ++++++++++------ cpp/src/plasma/protocol.h | 11 ++-- cpp/src/plasma/store.cc | 67 +++++++++++++--------- cpp/src/plasma/test/client_tests.cc | 14 ++--- cpp/src/plasma/test/serialization_tests.cc | 26 +++++++-- python/pyarrow/plasma.pyx | 4 +- 12 files changed, 165 insertions(+), 104 deletions(-) diff --git a/cpp/src/plasma/client.cc b/cpp/src/plasma/client.cc index d74c0f412d97f..a683da0022b18 100644 --- a/cpp/src/plasma/client.cc +++ b/cpp/src/plasma/client.cc @@ -54,8 +54,6 @@ namespace plasma { -using arrow::MutableBuffer; - // Number of threads used for memcopy and hash computations. constexpr int64_t kThreadPoolSize = 8; constexpr int64_t kBytesInMB = 1 << 20; @@ -130,7 +128,7 @@ void PlasmaClient::increment_object_count(const ObjectID& object_id, PlasmaObjec // Increment the count of the number of objects in the memory-mapped file // that are being used. The corresponding decrement should happen in // PlasmaClient::Release. - auto entry = mmap_table_.find(object->handle.store_fd); + auto entry = mmap_table_.find(object->store_fd); ARROW_CHECK(entry != mmap_table_.end()); ARROW_CHECK(entry->second.count >= 0); // Update the in_use_object_bytes_. @@ -149,7 +147,7 @@ void PlasmaClient::increment_object_count(const ObjectID& object_id, PlasmaObjec Status PlasmaClient::Create(const ObjectID& object_id, int64_t data_size, uint8_t* metadata, int64_t metadata_size, - std::shared_ptr* data) { + std::shared_ptr* data) { ARROW_LOG(DEBUG) << "called plasma_create on conn " << store_conn_ << " with size " << data_size << " and metadata size " << metadata_size; RETURN_NOT_OK(SendCreateRequest(store_conn_, object_id, data_size, metadata_size)); @@ -157,7 +155,10 @@ Status PlasmaClient::Create(const ObjectID& object_id, int64_t data_size, RETURN_NOT_OK(PlasmaReceive(store_conn_, MessageType_PlasmaCreateReply, &buffer)); ObjectID id; PlasmaObject object; - RETURN_NOT_OK(ReadCreateReply(buffer.data(), buffer.size(), &id, &object)); + int store_fd; + int64_t mmap_size; + RETURN_NOT_OK( + ReadCreateReply(buffer.data(), buffer.size(), &id, &object, &store_fd, &mmap_size)); // If the CreateReply included an error, then the store will not send a file // descriptor. int fd = recv_fd(store_conn_); @@ -167,9 +168,7 @@ Status PlasmaClient::Create(const ObjectID& object_id, int64_t data_size, // The metadata should come right after the data. ARROW_CHECK(object.metadata_offset == object.data_offset + data_size); *data = std::make_shared( - lookup_or_mmap(fd, object.handle.store_fd, object.handle.mmap_size) + - object.data_offset, - data_size); + lookup_or_mmap(fd, store_fd, mmap_size) + object.data_offset, data_size); // If plasma_create is being called from a transfer, then we will not copy the // metadata here. The metadata will be written along with the data streamed // from the transfer. @@ -209,7 +208,7 @@ Status PlasmaClient::Get(const ObjectID* object_ids, int64_t num_objects, ARROW_CHECK(object_entry->second->is_sealed) << "Plasma client called get on an unsealed object that it created"; PlasmaObject* object = &object_entry->second->object; - uint8_t* data = lookup_mmapped_file(object->handle.store_fd); + uint8_t* data = lookup_mmapped_file(object->store_fd); object_buffers[i].data = std::make_shared(data + object->data_offset, object->data_size); object_buffers[i].metadata = std::make_shared( @@ -236,8 +235,19 @@ Status PlasmaClient::Get(const ObjectID* object_ids, int64_t num_objects, std::vector received_object_ids(num_objects); std::vector object_data(num_objects); PlasmaObject* object; + std::vector store_fds; + std::vector mmap_sizes; RETURN_NOT_OK(ReadGetReply(buffer.data(), buffer.size(), received_object_ids.data(), - object_data.data(), num_objects)); + object_data.data(), num_objects, store_fds, mmap_sizes)); + + // We mmap all of the file descriptors here so that we can avoid look them up + // in the subsequent loop based on just the store file descriptor and without + // having to know the relevant file descriptor received from recv_fd. + for (size_t i = 0; i < store_fds.size(); i++) { + int fd = recv_fd(store_conn_); + ARROW_CHECK(fd >= 0); + lookup_or_mmap(fd, store_fds[i], mmap_sizes[i]); + } for (int i = 0; i < num_objects; ++i) { DCHECK(received_object_ids[i] == object_ids[i]); @@ -246,12 +256,6 @@ Status PlasmaClient::Get(const ObjectID* object_ids, int64_t num_objects, // If the object was already in use by the client, then the store should // have returned it. DCHECK_NE(object->data_size, -1); - // We won't use this file descriptor, but the store sent us one, so we - // need to receive it and then close it right away so we don't leak file - // descriptors. - int fd = recv_fd(store_conn_); - close(fd); - ARROW_CHECK(fd >= 0); // We've already filled out the information for this object, so we can // just continue. continue; @@ -259,12 +263,7 @@ Status PlasmaClient::Get(const ObjectID* object_ids, int64_t num_objects, // If we are here, the object was not currently in use, so we need to // process the reply from the object store. if (object->data_size != -1) { - // The object was retrieved. The user will be responsible for releasing - // this object. - int fd = recv_fd(store_conn_); - uint8_t* data = - lookup_or_mmap(fd, object->handle.store_fd, object->handle.mmap_size); - ARROW_CHECK(fd >= 0); + uint8_t* data = lookup_mmapped_file(object->store_fd); // Finish filling out the return values. object_buffers[i].data = std::make_shared(data + object->data_offset, object->data_size); @@ -296,7 +295,7 @@ Status PlasmaClient::UnmapObject(const ObjectID& object_id) { // Decrement the count of the number of objects in this memory-mapped file // that the client is using. The corresponding increment should have // happened in plasma_get. - int fd = object_entry->second->object.handle.store_fd; + int fd = object_entry->second->object.store_fd; auto entry = mmap_table_.find(fd); ARROW_CHECK(entry != mmap_table_.end()); ARROW_CHECK(entry->second.count >= 1); diff --git a/cpp/src/plasma/client.h b/cpp/src/plasma/client.h index 35182f8403201..a1e10a9c29969 100644 --- a/cpp/src/plasma/client.h +++ b/cpp/src/plasma/client.h @@ -31,8 +31,9 @@ #include "arrow/util/visibility.h" #include "plasma/common.h" -using arrow::Status; using arrow::Buffer; +using arrow::MutableBuffer; +using arrow::Status; namespace plasma { @@ -115,7 +116,7 @@ class ARROW_EXPORT PlasmaClient { /// will be written here. /// \return The return status. Status Create(const ObjectID& object_id, int64_t data_size, uint8_t* metadata, - int64_t metadata_size, std::shared_ptr* data); + int64_t metadata_size, std::shared_ptr* data); /// Get some objects from the Plasma Store. This function will block until the /// objects have all been created and sealed in the Plasma Store or the /// timeout diff --git a/cpp/src/plasma/format/plasma.fbs b/cpp/src/plasma/format/plasma.fbs index ea6dc8bb98da5..33803f7799ba0 100644 --- a/cpp/src/plasma/format/plasma.fbs +++ b/cpp/src/plasma/format/plasma.fbs @@ -89,8 +89,6 @@ struct PlasmaObjectSpec { // Index of the memory segment (= memory mapped file) that // this object is allocated in. segment_index: int; - // Size in bytes of this segment (needed to call mmap). - mmap_size: ulong; // The offset in bytes in the memory mapped file of the data. data_offset: ulong; // The size in bytes of the data. @@ -117,6 +115,12 @@ table PlasmaCreateReply { plasma_object: PlasmaObjectSpec; // Error that occurred for this call. error: PlasmaError; + // The file descriptor in the store that corresponds to the file descriptor + // being sent to the client right after this message. + store_fd: int; + // The size in bytes of the segment for the store file descriptor (needed to + // call mmap). + mmap_size: long; } table PlasmaAbortRequest { @@ -156,9 +160,17 @@ table PlasmaGetReply { // objects if not all requested objects are stored and sealed // in the local Plasma store. object_ids: [string]; - // Plasma object information, in the same order as their IDs. + // Plasma object information, in the same order as their IDs. The number of + // elements in both object_ids and plasma_objects arrays must agree. plasma_objects: [PlasmaObjectSpec]; - // The number of elements in both object_ids and plasma_objects arrays must agree. + // A list of the file descriptors in the store that correspond to the file + // descriptors being sent to the client. The length of this list is the number + // of file descriptors that the store will send to the client after this + // message. + store_fds: [int]; + // Size in bytes of the segment for each store file descriptor (needed to call + // mmap). This list must have the same length as store_fds. + mmap_sizes: [long]; } table PlasmaReleaseRequest { diff --git a/cpp/src/plasma/malloc.cc b/cpp/src/plasma/malloc.cc index 52d362013f1ae..3c5d107b2bbe3 100644 --- a/cpp/src/plasma/malloc.cc +++ b/cpp/src/plasma/malloc.cc @@ -197,4 +197,14 @@ void get_malloc_mapinfo(void* addr, int* fd, int64_t* map_size, ptrdiff_t* offse *offset = 0; } +int64_t get_mmap_size(int fd) { + for (const auto& entry : mmap_records) { + if (entry.second.fd == fd) { + return entry.second.size; + } + } + ARROW_LOG(FATAL) << "failed to find entry in mmap_records for fd " << fd; + return -1; // This code is never reached. +} + void set_malloc_granularity(int value) { change_mparam(M_GRANULARITY, value); } diff --git a/cpp/src/plasma/malloc.h b/cpp/src/plasma/malloc.h index 0df720db59817..cb8c600b14b3b 100644 --- a/cpp/src/plasma/malloc.h +++ b/cpp/src/plasma/malloc.h @@ -23,6 +23,12 @@ void get_malloc_mapinfo(void* addr, int* fd, int64_t* map_length, ptrdiff_t* offset); +/// Get the mmap size corresponding to a specific file descriptor. +/// +/// @param fd The file descriptor to look up. +/// @return The size of the corresponding memory-mapped file. +int64_t get_mmap_size(int fd); + void set_malloc_granularity(int value); #endif // MALLOC_H diff --git a/cpp/src/plasma/plasma.h b/cpp/src/plasma/plasma.h index 603ff8a4fac6c..2d07c919a18f4 100644 --- a/cpp/src/plasma/plasma.h +++ b/cpp/src/plasma/plasma.h @@ -64,20 +64,12 @@ struct Client; /// Mapping from object IDs to type and status of the request. typedef std::unordered_map ObjectRequestMap; -/// Handle to access memory mapped file and map it into client address space. -struct object_handle { +// TODO(pcm): Replace this by the flatbuffers message PlasmaObjectSpec. +struct PlasmaObject { /// The file descriptor of the memory mapped file in the store. It is used as /// a unique identifier of the file in the client to look up the corresponding /// file descriptor on the client's side. int store_fd; - /// The size in bytes of the memory mapped file. - int64_t mmap_size; -}; - -// TODO(pcm): Replace this by the flatbuffers message PlasmaObjectSpec. -struct PlasmaObject { - /// Handle for memory mapped file the object is stored in. - object_handle handle; /// The offset in bytes in the memory mapped file of the data. ptrdiff_t data_offset; /// The offset in bytes in the memory mapped file of the metadata. diff --git a/cpp/src/plasma/protocol.cc b/cpp/src/plasma/protocol.cc index c0ebb88fe5019..6c0bc0cab28bb 100644 --- a/cpp/src/plasma/protocol.cc +++ b/cpp/src/plasma/protocol.cc @@ -73,30 +73,32 @@ Status ReadCreateRequest(uint8_t* data, size_t size, ObjectID* object_id, return Status::OK(); } -Status SendCreateReply(int sock, ObjectID object_id, PlasmaObject* object, - int error_code) { +Status SendCreateReply(int sock, ObjectID object_id, PlasmaObject* object, int error_code, + int64_t mmap_size) { flatbuffers::FlatBufferBuilder fbb; - PlasmaObjectSpec plasma_object(object->handle.store_fd, object->handle.mmap_size, - object->data_offset, object->data_size, + PlasmaObjectSpec plasma_object(object->store_fd, object->data_offset, object->data_size, object->metadata_offset, object->metadata_size); - auto message = - CreatePlasmaCreateReply(fbb, fbb.CreateString(object_id.binary()), &plasma_object, - static_cast(error_code)); + auto message = CreatePlasmaCreateReply( + fbb, fbb.CreateString(object_id.binary()), &plasma_object, + static_cast(error_code), object->store_fd, mmap_size); return PlasmaSend(sock, MessageType_PlasmaCreateReply, &fbb, message); } Status ReadCreateReply(uint8_t* data, size_t size, ObjectID* object_id, - PlasmaObject* object) { + PlasmaObject* object, int* store_fd, int64_t* mmap_size) { DCHECK(data); auto message = flatbuffers::GetRoot(data); DCHECK(verify_flatbuffer(message, data, size)); *object_id = ObjectID::from_binary(message->object_id()->str()); - object->handle.store_fd = message->plasma_object()->segment_index(); - object->handle.mmap_size = message->plasma_object()->mmap_size(); + object->store_fd = message->plasma_object()->segment_index(); object->data_offset = message->plasma_object()->data_offset(); object->data_size = message->plasma_object()->data_size(); object->metadata_offset = message->plasma_object()->metadata_offset(); object->metadata_size = message->plasma_object()->metadata_size(); + + *store_fd = message->store_fd(); + *mmap_size = message->mmap_size(); + return plasma_error_status(message->error()); } @@ -389,24 +391,29 @@ Status ReadGetRequest(uint8_t* data, size_t size, std::vector& object_ Status SendGetReply( int sock, ObjectID object_ids[], std::unordered_map& plasma_objects, - int64_t num_objects) { + int64_t num_objects, const std::vector& store_fds, + const std::vector& mmap_sizes) { flatbuffers::FlatBufferBuilder fbb; std::vector objects; - for (int i = 0; i < num_objects; ++i) { + ARROW_CHECK(store_fds.size() == mmap_sizes.size()); + + for (int64_t i = 0; i < num_objects; ++i) { const PlasmaObject& object = plasma_objects[object_ids[i]]; - objects.push_back(PlasmaObjectSpec(object.handle.store_fd, object.handle.mmap_size, - object.data_offset, object.data_size, - object.metadata_offset, object.metadata_size)); + objects.push_back(PlasmaObjectSpec(object.store_fd, object.data_offset, + object.data_size, object.metadata_offset, + object.metadata_size)); } auto message = CreatePlasmaGetReply(fbb, to_flatbuffer(&fbb, object_ids, num_objects), - fbb.CreateVectorOfStructs(objects.data(), num_objects)); + fbb.CreateVectorOfStructs(objects.data(), num_objects), + fbb.CreateVector(store_fds), fbb.CreateVector(mmap_sizes)); return PlasmaSend(sock, MessageType_PlasmaGetReply, &fbb, message); } Status ReadGetReply(uint8_t* data, size_t size, ObjectID object_ids[], - PlasmaObject plasma_objects[], int64_t num_objects) { + PlasmaObject plasma_objects[], int64_t num_objects, + std::vector& store_fds, std::vector& mmap_sizes) { DCHECK(data); auto message = flatbuffers::GetRoot(data); DCHECK(verify_flatbuffer(message, data, size)); @@ -415,13 +422,17 @@ Status ReadGetReply(uint8_t* data, size_t size, ObjectID object_ids[], } for (uoffset_t i = 0; i < num_objects; ++i) { const PlasmaObjectSpec* object = message->plasma_objects()->Get(i); - plasma_objects[i].handle.store_fd = object->segment_index(); - plasma_objects[i].handle.mmap_size = object->mmap_size(); + plasma_objects[i].store_fd = object->segment_index(); plasma_objects[i].data_offset = object->data_offset(); plasma_objects[i].data_size = object->data_size(); plasma_objects[i].metadata_offset = object->metadata_offset(); plasma_objects[i].metadata_size = object->metadata_size(); } + ARROW_CHECK(message->store_fds()->size() == message->mmap_sizes()->size()); + for (uoffset_t i = 0; i < message->store_fds()->size(); i++) { + store_fds.push_back(message->store_fds()->Get(i)); + mmap_sizes.push_back(message->mmap_sizes()->Get(i)); + } return Status::OK(); } diff --git a/cpp/src/plasma/protocol.h b/cpp/src/plasma/protocol.h index e8c334f9181fc..44263a6418439 100644 --- a/cpp/src/plasma/protocol.h +++ b/cpp/src/plasma/protocol.h @@ -46,10 +46,11 @@ Status SendCreateRequest(int sock, ObjectID object_id, int64_t data_size, Status ReadCreateRequest(uint8_t* data, size_t size, ObjectID* object_id, int64_t* data_size, int64_t* metadata_size); -Status SendCreateReply(int sock, ObjectID object_id, PlasmaObject* object, int error); +Status SendCreateReply(int sock, ObjectID object_id, PlasmaObject* object, int error, + int64_t mmap_size); Status ReadCreateReply(uint8_t* data, size_t size, ObjectID* object_id, - PlasmaObject* object); + PlasmaObject* object, int* store_fd, int64_t* mmap_size); Status SendAbortRequest(int sock, ObjectID object_id); @@ -81,10 +82,12 @@ Status ReadGetRequest(uint8_t* data, size_t size, std::vector& object_ Status SendGetReply( int sock, ObjectID object_ids[], std::unordered_map& plasma_objects, - int64_t num_objects); + int64_t num_objects, const std::vector& store_fds, + const std::vector& mmap_sizes); Status ReadGetReply(uint8_t* data, size_t size, ObjectID object_ids[], - PlasmaObject plasma_objects[], int64_t num_objects); + PlasmaObject plasma_objects[], int64_t num_objects, + std::vector& store_fds, std::vector& mmap_sizes); /* Plasma Release message functions. */ diff --git a/cpp/src/plasma/store.cc b/cpp/src/plasma/store.cc index dde7f9cdfa8eb..80dd525e3e3b4 100644 --- a/cpp/src/plasma/store.cc +++ b/cpp/src/plasma/store.cc @@ -192,8 +192,7 @@ int PlasmaStore::create_object(const ObjectID& object_id, int64_t data_size, entry->state = PLASMA_CREATED; store_info_.objects[object_id] = std::move(entry); - result->handle.store_fd = fd; - result->handle.mmap_size = map_size; + result->store_fd = fd; result->data_offset = offset; result->metadata_offset = offset + data_size; result->data_size = data_size; @@ -211,8 +210,7 @@ void PlasmaObject_init(PlasmaObject* object, ObjectTableEntry* entry) { DCHECK(object != NULL); DCHECK(entry != NULL); DCHECK(entry->state == PLASMA_SEALED); - object->handle.store_fd = entry->fd; - object->handle.mmap_size = entry->map_size; + object->store_fd = entry->fd; object->data_offset = entry->offset; object->metadata_offset = entry->offset + entry->info.data_size; object->data_size = entry->info.data_size; @@ -220,34 +218,44 @@ void PlasmaObject_init(PlasmaObject* object, ObjectTableEntry* entry) { } void PlasmaStore::return_from_get(GetRequest* get_req) { + // Figure out how many file descriptors we need to send. + std::unordered_set fds_to_send; + std::vector store_fds; + std::vector mmap_sizes; + for (const auto& object_id : get_req->object_ids) { + PlasmaObject& object = get_req->objects[object_id]; + int fd = object.store_fd; + if (object.data_size != -1 && fds_to_send.count(fd) == 0) { + fds_to_send.insert(fd); + store_fds.push_back(fd); + mmap_sizes.push_back(get_mmap_size(fd)); + } + } + // Send the get reply to the client. Status s = SendGetReply(get_req->client->fd, &get_req->object_ids[0], get_req->objects, - get_req->object_ids.size()); + get_req->object_ids.size(), store_fds, mmap_sizes); warn_if_sigpipe(s.ok() ? 0 : -1, get_req->client->fd); // If we successfully sent the get reply message to the client, then also send // the file descriptors. if (s.ok()) { // Send all of the file descriptors for the present objects. - for (const auto& object_id : get_req->object_ids) { - PlasmaObject& object = get_req->objects[object_id]; - // We use the data size to indicate whether the object is present or not. - if (object.data_size != -1) { - int error_code = send_fd(get_req->client->fd, object.handle.store_fd); - // If we failed to send the file descriptor, loop until we have sent it - // successfully. TODO(rkn): This is problematic for two reasons. First - // of all, sending the file descriptor should just succeed without any - // errors, but sometimes I see a "Message too long" error number. - // Second, looping like this allows a client to potentially block the - // plasma store event loop which should never happen. - while (error_code < 0) { - if (errno == EMSGSIZE) { - ARROW_LOG(WARNING) << "Failed to send file descriptor, retrying."; - error_code = send_fd(get_req->client->fd, object.handle.store_fd); - continue; - } - warn_if_sigpipe(error_code, get_req->client->fd); - break; + for (int store_fd : store_fds) { + int error_code = send_fd(get_req->client->fd, store_fd); + // If we failed to send the file descriptor, loop until we have sent it + // successfully. TODO(rkn): This is problematic for two reasons. First + // of all, sending the file descriptor should just succeed without any + // errors, but sometimes I see a "Message too long" error number. + // Second, looping like this allows a client to potentially block the + // plasma store event loop which should never happen. + while (error_code < 0) { + if (errno == EMSGSIZE) { + ARROW_LOG(WARNING) << "Failed to send file descriptor, retrying."; + error_code = send_fd(get_req->client->fd, store_fd); + continue; } + warn_if_sigpipe(error_code, get_req->client->fd); + break; } } } @@ -640,10 +648,15 @@ Status PlasmaStore::process_message(Client* client) { ReadCreateRequest(input, input_size, &object_id, &data_size, &metadata_size)); int error_code = create_object(object_id, data_size, metadata_size, client, &object); - HANDLE_SIGPIPE(SendCreateReply(client->fd, object_id, &object, error_code), - client->fd); + int64_t mmap_size = 0; + if (error_code == PlasmaError_OK) { + mmap_size = get_mmap_size(object.store_fd); + } + HANDLE_SIGPIPE( + SendCreateReply(client->fd, object_id, &object, error_code, mmap_size), + client->fd); if (error_code == PlasmaError_OK) { - warn_if_sigpipe(send_fd(client->fd, object.handle.store_fd), client->fd); + warn_if_sigpipe(send_fd(client->fd, object.store_fd), client->fd); } } break; case MessageType_PlasmaAbortRequest: { diff --git a/cpp/src/plasma/test/client_tests.cc b/cpp/src/plasma/test/client_tests.cc index f19c2bfbdb380..63b56934f3599 100644 --- a/cpp/src/plasma/test/client_tests.cc +++ b/cpp/src/plasma/test/client_tests.cc @@ -70,7 +70,7 @@ TEST_F(TestPlasmaStore, DeleteTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); ARROW_CHECK_OK(client_.Seal(object_id)); @@ -96,7 +96,7 @@ TEST_F(TestPlasmaStore, ContainsTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); ARROW_CHECK_OK(client_.Seal(object_id)); // Avoid race condition of Plasma Manager waiting for notification. @@ -119,7 +119,7 @@ TEST_F(TestPlasmaStore, GetTest) { int64_t data_size = 4; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data_buffer; + std::shared_ptr data_buffer; uint8_t* data; ARROW_CHECK_OK( client_.Create(object_id, data_size, metadata, metadata_size, &data_buffer)); @@ -145,7 +145,7 @@ TEST_F(TestPlasmaStore, MultipleGetTest) { int64_t data_size = 4; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id1, data_size, metadata, metadata_size, &data)); data->mutable_data()[0] = 1; ARROW_CHECK_OK(client_.Seal(object_id1)); @@ -172,7 +172,7 @@ TEST_F(TestPlasmaStore, AbortTest) { int64_t data_size = 4; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; uint8_t* data_ptr; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); data_ptr = data->mutable_data(); @@ -220,7 +220,7 @@ TEST_F(TestPlasmaStore, MultipleClientTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client2_.Create(object_id, data_size, metadata, metadata_size, &data)); ARROW_CHECK_OK(client2_.Seal(object_id)); // Test that the first client can get the object. @@ -260,7 +260,7 @@ TEST_F(TestPlasmaStore, ManyObjectTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); if (i % 3 == 0) { diff --git a/cpp/src/plasma/test/serialization_tests.cc b/cpp/src/plasma/test/serialization_tests.cc index b593b6ae94890..656b2cc6b9bca 100644 --- a/cpp/src/plasma/test/serialization_tests.cc +++ b/cpp/src/plasma/test/serialization_tests.cc @@ -63,8 +63,7 @@ PlasmaObject random_plasma_object(void) { int random = rand_r(&seed); PlasmaObject object; memset(&object, 0, sizeof(object)); - object.handle.store_fd = random + 7; - object.handle.mmap_size = random + 42; + object.store_fd = random + 7; object.data_offset = random + 1; object.metadata_offset = random + 2; object.data_size = random + 3; @@ -94,13 +93,19 @@ TEST(PlasmaSerialization, CreateReply) { int fd = create_temp_file(); ObjectID object_id1 = ObjectID::from_random(); PlasmaObject object1 = random_plasma_object(); - ARROW_CHECK_OK(SendCreateReply(fd, object_id1, &object1, 0)); + int64_t mmap_size1 = 1000000; + ARROW_CHECK_OK(SendCreateReply(fd, object_id1, &object1, 0, mmap_size1)); std::vector data = read_message_from_file(fd, MessageType_PlasmaCreateReply); ObjectID object_id2; PlasmaObject object2; memset(&object2, 0, sizeof(object2)); - ARROW_CHECK_OK(ReadCreateReply(data.data(), data.size(), &object_id2, &object2)); + int store_fd; + int64_t mmap_size2; + ARROW_CHECK_OK(ReadCreateReply(data.data(), data.size(), &object_id2, &object2, + &store_fd, &mmap_size2)); ASSERT_EQ(object_id1, object_id2); + ASSERT_EQ(object1.store_fd, store_fd); + ASSERT_EQ(mmap_size1, mmap_size2); ASSERT_EQ(memcmp(&object1, &object2, sizeof(object1)), 0); close(fd); } @@ -158,13 +163,20 @@ TEST(PlasmaSerialization, GetReply) { std::unordered_map plasma_objects; plasma_objects[object_ids[0]] = random_plasma_object(); plasma_objects[object_ids[1]] = random_plasma_object(); - ARROW_CHECK_OK(SendGetReply(fd, object_ids, plasma_objects, 2)); + std::vector store_fds = {1, 2, 3}; + std::vector mmap_sizes = {100, 200, 300}; + ARROW_CHECK_OK(SendGetReply(fd, object_ids, plasma_objects, 2, store_fds, mmap_sizes)); + std::vector data = read_message_from_file(fd, MessageType_PlasmaGetReply); ObjectID object_ids_return[2]; PlasmaObject plasma_objects_return[2]; + std::vector store_fds_return; + std::vector mmap_sizes_return; memset(&plasma_objects_return, 0, sizeof(plasma_objects_return)); ARROW_CHECK_OK(ReadGetReply(data.data(), data.size(), object_ids_return, - &plasma_objects_return[0], 2)); + &plasma_objects_return[0], 2, store_fds_return, + mmap_sizes_return)); + ASSERT_EQ(object_ids[0], object_ids_return[0]); ASSERT_EQ(object_ids[1], object_ids_return[1]); ASSERT_EQ(memcmp(&plasma_objects[object_ids[0]], &plasma_objects_return[0], @@ -173,6 +185,8 @@ TEST(PlasmaSerialization, GetReply) { ASSERT_EQ(memcmp(&plasma_objects[object_ids[1]], &plasma_objects_return[1], sizeof(PlasmaObject)), 0); + ASSERT_TRUE(store_fds == store_fds_return); + ASSERT_TRUE(mmap_sizes == mmap_sizes_return); close(fd); } diff --git a/python/pyarrow/plasma.pyx b/python/pyarrow/plasma.pyx index 32f6d189da08c..801d094194b71 100644 --- a/python/pyarrow/plasma.pyx +++ b/python/pyarrow/plasma.pyx @@ -81,7 +81,7 @@ cdef extern from "plasma/client.h" nogil: CStatus Create(const CUniqueID& object_id, int64_t data_size, const uint8_t* metadata, int64_t metadata_size, - const shared_ptr[CBuffer]* data) + const shared_ptr[CMutableBuffer]* data) CStatus Get(const CUniqueID* object_ids, int64_t num_objects, int64_t timeout_ms, CObjectBuffer* object_buffers) @@ -297,7 +297,7 @@ cdef class PlasmaClient: not be created because the plasma store is unable to evict enough objects to create room for it. """ - cdef shared_ptr[CBuffer] data + cdef shared_ptr[CMutableBuffer] data with nogil: check_status(self.client.get().Create(object_id.data, data_size, (metadata.data()), From 1bbaf7e669a580531be30cd2f8ade8b560466774 Mon Sep 17 00:00:00 2001 From: Simbarashe Nyatsanga Date: Sun, 21 Jan 2018 01:12:07 +0200 Subject: [PATCH 08/46] [Python] Fix small typos in bytes, String/UTF-8 and FixedSizeBinary type check exceptions. (#1495) --- cpp/src/arrow/python/numpy_to_arrow.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/arrow/python/numpy_to_arrow.cc b/cpp/src/arrow/python/numpy_to_arrow.cc index f21b40ed3c246..c5c02e355ded6 100644 --- a/cpp/src/arrow/python/numpy_to_arrow.cc +++ b/cpp/src/arrow/python/numpy_to_arrow.cc @@ -175,7 +175,7 @@ static Status AppendObjectBinaries(PyArrayObject* arr, PyArrayObject* mask, continue; } else if (!PyBytes_Check(obj)) { std::stringstream ss; - ss << "Error converting to Python objects to bytes: "; + ss << "Error converting from Python objects to bytes: "; RETURN_NOT_OK(InvalidConversion(obj, "str, bytes", &ss)); return Status::Invalid(ss.str()); } @@ -230,7 +230,7 @@ static Status AppendObjectStrings(PyArrayObject* arr, PyArrayObject* mask, int64 *have_bytes = true; } else { std::stringstream ss; - ss << "Error converting to Python objects to String/UTF8: "; + ss << "Error converting from Python objects to String/UTF8: "; RETURN_NOT_OK(InvalidConversion(obj, "str, bytes", &ss)); return Status::Invalid(ss.str()); } @@ -278,7 +278,7 @@ static Status AppendObjectFixedWidthBytes(PyArrayObject* arr, PyArrayObject* mas tmp_obj.reset(obj); } else if (!PyBytes_Check(obj)) { std::stringstream ss; - ss << "Error converting to Python objects to FixedSizeBinary: "; + ss << "Error converting from Python objects to FixedSizeBinary: "; RETURN_NOT_OK(InvalidConversion(obj, "str, bytes", &ss)); return Status::Invalid(ss.str()); } From ed272430e310102c750cf997cc2ad5dace2d3323 Mon Sep 17 00:00:00 2001 From: Kouhei Sutou Date: Sun, 21 Jan 2018 22:10:34 +0900 Subject: [PATCH 09/46] ARROW-2012: [GLib] Support "make distclean" Author: Kouhei Sutou Closes #1494 from kou/glib-support-distclean and squashes the following commits: d660e0f8 [Kouhei Sutou] [GLib] Support "make distclean" --- c_glib/configure.ac | 2 +- c_glib/doc/reference/Makefile.am | 4 +-- c_glib/doc/reference/arrow-glib-docs.xml | 4 +-- .../gtkdocentities.ent.in => entities.xml.in} | 12 +++---- c_glib/doc/reference/meson.build | 13 +++++++- c_glib/doc/reference/xml/Makefile.am | 20 ------------ c_glib/doc/reference/xml/meson.build | 31 ------------------- dev/gen_apidocs/create_documents.sh | 2 -- 8 files changed, 22 insertions(+), 66 deletions(-) rename c_glib/doc/reference/{xml/gtkdocentities.ent.in => entities.xml.in} (76%) delete mode 100644 c_glib/doc/reference/xml/Makefile.am delete mode 100644 c_glib/doc/reference/xml/meson.build diff --git a/c_glib/configure.ac b/c_glib/configure.ac index eabe7bad51227..f4f2c99bbc39e 100644 --- a/c_glib/configure.ac +++ b/c_glib/configure.ac @@ -143,7 +143,7 @@ AC_CONFIG_FILES([ arrow-gpu-glib/arrow-gpu-glib.pc doc/Makefile doc/reference/Makefile - doc/reference/xml/Makefile + doc/reference/entities.xml example/Makefile example/lua/Makefile tool/Makefile diff --git a/c_glib/doc/reference/Makefile.am b/c_glib/doc/reference/Makefile.am index 4c005c237b300..454c2b0692da6 100644 --- a/c_glib/doc/reference/Makefile.am +++ b/c_glib/doc/reference/Makefile.am @@ -15,9 +15,6 @@ # specific language governing permissions and limitations # under the License. -SUBDIRS = \ - xml - DOC_MODULE = arrow-glib DOC_MAIN_SGML_FILE = $(DOC_MODULE)-docs.xml @@ -72,4 +69,5 @@ CLEANFILES += \ $(DOC_MODULE).types EXTRA_DIST += \ + entities.xml.in \ meson.build diff --git a/c_glib/doc/reference/arrow-glib-docs.xml b/c_glib/doc/reference/arrow-glib-docs.xml index 51e7b2a6a6cf5..23d1e9a0f271a 100644 --- a/c_glib/doc/reference/arrow-glib-docs.xml +++ b/c_glib/doc/reference/arrow-glib-docs.xml @@ -21,10 +21,10 @@ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [ - + %gtkdocentities; ]> - + &package_name; Reference Manual diff --git a/c_glib/doc/reference/xml/gtkdocentities.ent.in b/c_glib/doc/reference/entities.xml.in similarity index 76% rename from c_glib/doc/reference/xml/gtkdocentities.ent.in rename to c_glib/doc/reference/entities.xml.in index dc0cf1a0d8d4a..aa5addb4e8431 100644 --- a/c_glib/doc/reference/xml/gtkdocentities.ent.in +++ b/c_glib/doc/reference/entities.xml.in @@ -16,9 +16,9 @@ specific language governing permissions and limitations under the License. --> - - - - - - + + + + + + diff --git a/c_glib/doc/reference/meson.build b/c_glib/doc/reference/meson.build index 3374fbde5b9ed..431aa0a5c82a1 100644 --- a/c_glib/doc/reference/meson.build +++ b/c_glib/doc/reference/meson.build @@ -17,7 +17,18 @@ # specific language governing permissions and limitations # under the License. -subdir('xml') +entities_conf = configuration_data() +entities_conf.set('PACKAGE', meson.project_name()) +entities_conf.set('PACKAGE_BUGREPORT', + 'https://issues.apache.org/jira/browse/ARROW') +entities_conf.set('PACKAGE_NAME', meson.project_name()) +entities_conf.set('PACKAGE_STRING', + ' '.join([meson.project_name(), version])) +entities_conf.set('PACKAGE_URL', 'https://arrow.apache.org/') +entities_conf.set('PACKAGE_VERSION', version) +configure_file(input: 'entities.xml.in', + output: 'entities.xml', + configuration: entities_conf) private_headers = [ ] diff --git a/c_glib/doc/reference/xml/Makefile.am b/c_glib/doc/reference/xml/Makefile.am deleted file mode 100644 index 833cfddc69078..0000000000000 --- a/c_glib/doc/reference/xml/Makefile.am +++ /dev/null @@ -1,20 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -EXTRA_DIST = \ - gtkdocentities.ent.in \ - meson.build diff --git a/c_glib/doc/reference/xml/meson.build b/c_glib/doc/reference/xml/meson.build deleted file mode 100644 index 5b65042764fee..0000000000000 --- a/c_glib/doc/reference/xml/meson.build +++ /dev/null @@ -1,31 +0,0 @@ -# -*- indent-tabs-mode: nil -*- -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -entities_conf = configuration_data() -entities_conf.set('package', meson.project_name()) -entities_conf.set('package_bugreport', - 'https://issues.apache.org/jira/browse/ARROW') -entities_conf.set('package_name', meson.project_name()) -entities_conf.set('package_string', - ' '.join([meson.project_name(), version])) -entities_conf.set('package_url', 'https://arrow.apache.org/') -entities_conf.set('package_version', version) -configure_file(input: 'gtkdocentities.ent.in', - output: 'gtkdocentities.ent', - configuration: entities_conf) diff --git a/dev/gen_apidocs/create_documents.sh b/dev/gen_apidocs/create_documents.sh index 54031262b3a5d..3100d3b984b3a 100755 --- a/dev/gen_apidocs/create_documents.sh +++ b/dev/gen_apidocs/create_documents.sh @@ -87,8 +87,6 @@ if [ -f Makefile ]; then # Ensure updating to prevent auto re-configure touch configure **/Makefile make distclean - # Work around for 'make distclean' removes doc/reference/xml/ - git checkout doc/reference/xml fi ./autogen.sh rm -rf build_docs From 422efd9635ea6f249adec7e1fda4834f6ac46cc4 Mon Sep 17 00:00:00 2001 From: Phillip Cloud Date: Mon, 22 Jan 2018 14:13:19 -0500 Subject: [PATCH 10/46] ARROW-1580: [Python] Instructions for setting up nightly builds on Linux Author: Phillip Cloud Closes #1489 from cpcloud/ARROW-1580 and squashes the following commits: ff815678 [Phillip Cloud] Move to sphinx 700d2c5e [Phillip Cloud] Remove link to nightlies 9fbe9ac9 [Phillip Cloud] Add build artifact location cb9f2a5a [Phillip Cloud] [Python] Instructions for setting up nightly builds on Linux --- python/doc/source/development.rst | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/python/doc/source/development.rst b/python/doc/source/development.rst index 01844fa18d133..af93d8d1a52c4 100644 --- a/python/doc/source/development.rst +++ b/python/doc/source/development.rst @@ -331,3 +331,76 @@ Getting ``python-test.exe`` to run is a bit tricky because your set PYTHONPATH=%CONDA_ENV%\Lib;%CONDA_ENV%\Lib\site-packages;%CONDA_ENV%\python35.zip;%CONDA_ENV%\DLLs;%CONDA_ENV% Now ``python-test.exe`` or simply ``ctest`` (to run all tests) should work. + +Nightly Builds of `arrow-cpp`, `parquet-cpp`, and `pyarrow` for Linux +--------------------------------------------------------------------- + +Nightly builds of Linux conda packages for ``arrow-cpp``, ``parquet-cpp``, and +``pyarrow`` can be automated using an open source tool called `scourge +`_. + +``scourge`` is new, so please report any feature requests or bugs to the +`scourge issue tracker `_. + +To get scourge you need to clone the source and install it in development mode. + +To setup your own nightly builds: + +#. Clone and install scourge +#. Create a script that calls scourge +#. Run that script as a cronjob once per day + +First, clone and install scourge (you also need to `install docker +`): + + +.. code:: sh + + git clone https://github.com/cpcloud/scourge + cd scourge + python setup.py develop + which scourge + +Second, create a shell script that calls scourge: + +.. code:: sh + + function build() { + # make sure we got a working directory + workingdir="${1}" + [ -z "${workingdir}" ] && echo "Must provide a working directory" && exit 1 + scourge="/path/to/scourge" + + # get the hash of master for building parquet + PARQUET_ARROW_VERSION="$("${scourge}" sha apache/arrow master)" + + # setup the build for each package + "${scourge}" init arrow-cpp@master parquet-cpp@master pyarrow@master + + # build the packages with some constraints (the -c arguments) + # -e sets environment variables on a per package basis + "${scourge}" build \ + -e parquet-cpp:PARQUET_ARROW_VERSION="${PARQUET_ARROW_VERSION}" \ + -c "python >=2.7,<3|>=3.5" \ + -c "numpy >= 1.11" \ + -c "r-base >=3.3.2" + } + + workingdir="$(date +'%Y%m%d_%H_%M_%S')" + mkdir -p "${workingdir}" + build "${workingdir}" > "${workingdir}"/scourge.log 2>&1 + +Third, run that script as a cronjob once per day: + +.. code:: sh + + crontab -e + +then in the scratch file that's opened: + +.. code:: sh + + @daily /path/to/the/above/script.sh + +The build artifacts (conda packages) will be located in +``${workingdir}/artifacts/linux-64``. From 72dea17fefde50676489470189c5e0492fd01510 Mon Sep 17 00:00:00 2001 From: Licht-T Date: Tue, 23 Jan 2018 08:53:04 -0500 Subject: [PATCH 11/46] ARROW-1997: [C++/Python] Ignore zero-copy-option in to_pandas when `strings_to_categorical` is True This closes [ARROW-1997](https://issues.apache.org/jira/browse/ARROW-1997). The problem is ```python >>> import pandas as pd >>> import pyarrow as pa >>> >>> df = pd.DataFrame({ ... 'Foo': ['A', 'A', 'B', 'B'] ... }) >>> table = pa.Table.from_pandas(df) >>> df = table.to_pandas(strings_to_categorical=True) Traceback (most recent call last): File "", line 1, in File "table.pxi", line 1043, in pyarrow.lib.Table.to_pandas File "pyarrow/pandas_compat.py", line 535, in table_to_blockmanager blocks = _table_to_blocks(options, block_table, nthreads, memory_pool) File "pyarrow/pandas_compat.py", line 629, in _table_to_blocks return [_reconstruct_block(item) for item in result] File "pyarrow/pandas_compat.py", line 436, in _reconstruct_block ordered=item['ordered']) File "/home/rito/miniconda3/envs/pyarrow-dev-27/lib/python2.7/site-packages/pandas/core/categorical.py", line 624, in from_codes raise ValueError("codes need to be between -1 and " ValueError: codes need to be between -1 and len(categories)-1 ``` When `strings_to_categorical=True`, the categorical index is newly created in `to_pandas` procedure. But, this passes data to Python by zero-copy, so the array is deallocated. https://github.com/Licht-T/arrow/blob/be58af6dd0333652abbe2333ee5968df3f2e371f/cpp/src/arrow/python/arrow_to_pandas.cc#L1040 Author: Licht-T Author: Wes McKinney Closes #1480 from Licht-T/fix-to_pandas-with-strings_to_categorical and squashes the following commits: 61eac9c1 [Wes McKinney] Adjust error message c1bc3539 [Licht-T] TST: Add test for to_pandas no-NA strings with strings_to_categorical cce3f50c [Licht-T] BUG: Avoid zero-copy-option in to_pandas when strings_to_categorical is True --- cpp/src/arrow/python/arrow_to_pandas.cc | 16 ++++++++++++---- python/pyarrow/tests/test_convert_pandas.py | 21 ++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/cpp/src/arrow/python/arrow_to_pandas.cc b/cpp/src/arrow/python/arrow_to_pandas.cc index e21bbda055953..5c8c970e1e058 100644 --- a/cpp/src/arrow/python/arrow_to_pandas.cc +++ b/cpp/src/arrow/python/arrow_to_pandas.cc @@ -963,7 +963,7 @@ class DatetimeTZBlock : public DatetimeBlock { class CategoricalBlock : public PandasBlock { public: explicit CategoricalBlock(PandasOptions options, MemoryPool* pool, int64_t num_rows) - : PandasBlock(options, num_rows, 1), pool_(pool) {} + : PandasBlock(options, num_rows, 1), pool_(pool), needs_copy_(false) {} Status Allocate() override { return Status::NotImplemented( @@ -996,14 +996,20 @@ class CategoricalBlock : public PandasBlock { return Status::OK(); }; - if (data.num_chunks() == 1 && indices_first.null_count() == 0) { + if (!needs_copy_ && data.num_chunks() == 1 && indices_first.null_count() == 0) { RETURN_NOT_OK(CheckIndices(indices_first, dict_arr_first.dictionary()->length())); RETURN_NOT_OK(AllocateNDArrayFromIndices(npy_type, indices_first)); } else { if (options_.zero_copy_only) { std::stringstream ss; - ss << "Needed to copy " << data.num_chunks() << " chunks with " - << indices_first.null_count() << " indices nulls, but zero_copy_only was True"; + if (needs_copy_) { + ss << "Need to allocate categorical memory, " + << "but only zero-copy conversions allowed."; + } else { + ss << "Needed to copy " << data.num_chunks() << " chunks with " + << indices_first.null_count() + << " indices nulls, but zero_copy_only was True"; + } return Status::Invalid(ss.str()); } RETURN_NOT_OK(AllocateNDArray(npy_type, 1)); @@ -1034,6 +1040,7 @@ class CategoricalBlock : public PandasBlock { std::shared_ptr converted_col; if (options_.strings_to_categorical && (col->type()->id() == Type::STRING || col->type()->id() == Type::BINARY)) { + needs_copy_ = true; compute::FunctionContext ctx(pool_); Datum out; @@ -1135,6 +1142,7 @@ class CategoricalBlock : public PandasBlock { MemoryPool* pool_; OwnedRef dictionary_; bool ordered_; + bool needs_copy_; }; Status MakeBlock(PandasOptions options, PandasBlock::type type, int64_t num_rows, diff --git a/python/pyarrow/tests/test_convert_pandas.py b/python/pyarrow/tests/test_convert_pandas.py index 83b1da135eea4..5acb9c3dbe9a1 100644 --- a/python/pyarrow/tests/test_convert_pandas.py +++ b/python/pyarrow/tests/test_convert_pandas.py @@ -1237,7 +1237,22 @@ def test_decimal_metadata(self): assert data_column['numpy_type'] == 'object' assert data_column['metadata'] == {'precision': 26, 'scale': 11} - def test_table_str_to_categorical(self): + def test_table_str_to_categorical_without_na(self): + values = ['a', 'a', 'b', 'b', 'c'] + df = pd.DataFrame({'strings': values}) + field = pa.field('strings', pa.string()) + schema = pa.schema([field]) + table = pa.Table.from_pandas(df, schema=schema) + + result = table.to_pandas(strings_to_categorical=True) + expected = pd.DataFrame({'strings': pd.Categorical(values)}) + tm.assert_frame_equal(result, expected, check_dtype=True) + + with pytest.raises(pa.ArrowInvalid): + table.to_pandas(strings_to_categorical=True, + zero_copy_only=True) + + def test_table_str_to_categorical_with_na(self): values = [None, 'a', 'b', np.nan] df = pd.DataFrame({'strings': values}) field = pa.field('strings', pa.string()) @@ -1248,6 +1263,10 @@ def test_table_str_to_categorical(self): expected = pd.DataFrame({'strings': pd.Categorical(values)}) tm.assert_frame_equal(result, expected, check_dtype=True) + with pytest.raises(pa.ArrowInvalid): + table.to_pandas(strings_to_categorical=True, + zero_copy_only=True) + def test_table_batch_empty_dataframe(self): df = pd.DataFrame({}) _check_pandas_roundtrip(df) From 0930b1d0ed9b649ba3e538a13960c8407ac6bc12 Mon Sep 17 00:00:00 2001 From: yosuke shiro Date: Tue, 23 Jan 2018 09:02:02 -0500 Subject: [PATCH 12/46] ARROW-2018: [C++] fix Build instruction on macOS and Homebrew Author: yosuke shiro Closes #1496 from shiro615/build-instruction-on-macos-and-homebrew-is-incomplete and squashes the following commits: 6b32f687 [yosuke shiro] [C++] fix Build instruction on macOS and Homebrew --- cpp/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cpp/README.md b/cpp/README.md index 39a1ccac64818..d2262a68512ce 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -42,6 +42,8 @@ sudo apt-get install cmake \ On OS X, you can use [Homebrew][1]: ```shell +git clone https://github.com/apache/arrow.git +cd arrow brew update && brew bundle --file=c_glib/Brewfile ``` From 0a490222c4078a7cb0ff085cd5c9884fdda57998 Mon Sep 17 00:00:00 2001 From: Panchen Xue Date: Tue, 23 Jan 2018 22:03:50 -0500 Subject: [PATCH 13/46] ARROW-1712: [C++] Add method to BinaryBuilder to reserve space for value data Modified BinaryBuilder::Resize(int64_t) so that when building BinaryArrays with a known size, space is also reserved for value_data_builder_ to prevent internal reallocation. Author: Panchen Xue Closes #1481 from xuepanchen/master and squashes the following commits: 707b67bf [Panchen Xue] ARROW-1712: [C++] Fix lint errors 360e6018 [Panchen Xue] Merge branch 'master' of https://github.com/xuepanchen/arrow d4bbd151 [Panchen Xue] ARROW-1712: [C++] Modify test case for BinaryBuilder::ReserveData() and change arguments for offsets_builder_.Resize() 77f8f3c1 [Panchen Xue] Merge pull request #5 from apache/master bc5db7d3 [Panchen Xue] ARROW-1712: [C++] Remove unneeded data member in BinaryBuilder and modify test case 5a5b70e2 [Panchen Xue] Merge pull request #4 from apache/master 8e4c8925 [Panchen Xue] Merge pull request #3 from xuepanchen/xuepanchen-arrow-1712 d3c8202b [Panchen Xue] ARROW-1945: [C++] Fix a small typo 0b078955 [Panchen Xue] ARROW-1945: [C++] Add data_capacity_ to track capacity of value data 18f90fb8 [Panchen Xue] ARROW-1945: [C++] Add data_capacity_ to track capacity of value data bbc65270 [Panchen Xue] ARROW-1945: [C++] Update test case for BinaryBuild data value space reservation 15e045cc [Panchen Xue] Add test case for array-test.cc 5a5593e5 [Panchen Xue] Update again ReserveData(int64_t) method for BinaryBuilder 9b5e8059 [Panchen Xue] Update ReserveData(int64_t) method signature for BinaryBuilder 8dd5eaa9 [Panchen Xue] Update builder.cc b002e0bd [Panchen Xue] Remove override keyword from ReserveData(int64_t) method for BinaryBuilder de318f47 [Panchen Xue] Implement ReserveData(int64_t) method for BinaryBuilder e0434e61 [Panchen Xue] Add ReserveData(int64_t) and value_data_capacity() for methods for BinaryBuilder 5ebfb320 [Panchen Xue] Add capacity() method for TypedBufferBuilder 5b73c1c5 [Panchen Xue] Update again BinaryBuilder::Resize(int64_t capacity) in builder.cc d021c54b [Panchen Xue] Merge pull request #2 from xuepanchen/xuepanchen-arrow-1712 232024e3 [Panchen Xue] Update BinaryBuilder::Resize(int64_t capacity) in builder.cc c2f8dc4e [Panchen Xue] Merge pull request #1 from apache/master --- cpp/src/arrow/array-test.cc | 39 +++++++++++++++++++++++++++++++++++++ cpp/src/arrow/buffer.h | 1 + cpp/src/arrow/builder.cc | 18 +++++++++++++---- cpp/src/arrow/builder.h | 5 +++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/cpp/src/arrow/array-test.cc b/cpp/src/arrow/array-test.cc index 7ff3261ecba5e..c53da8591e94e 100644 --- a/cpp/src/arrow/array-test.cc +++ b/cpp/src/arrow/array-test.cc @@ -1155,6 +1155,45 @@ TEST_F(TestBinaryBuilder, TestScalarAppend) { } } +TEST_F(TestBinaryBuilder, TestCapacityReserve) { + vector strings = {"aaaaa", "bbbbbbbbbb", "ccccccccccccccc", "dddddddddd"}; + int N = static_cast(strings.size()); + int reps = 15; + int64_t length = 0; + int64_t capacity = 1000; + int64_t expected_capacity = BitUtil::RoundUpToMultipleOf64(capacity); + + ASSERT_OK(builder_->ReserveData(capacity)); + + ASSERT_EQ(length, builder_->value_data_length()); + ASSERT_EQ(expected_capacity, builder_->value_data_capacity()); + + for (int j = 0; j < reps; ++j) { + for (int i = 0; i < N; ++i) { + ASSERT_OK(builder_->Append(strings[i])); + length += static_cast(strings[i].size()); + + ASSERT_EQ(length, builder_->value_data_length()); + ASSERT_EQ(expected_capacity, builder_->value_data_capacity()); + } + } + + int extra_capacity = 500; + expected_capacity = BitUtil::RoundUpToMultipleOf64(length + extra_capacity); + + ASSERT_OK(builder_->ReserveData(extra_capacity)); + + ASSERT_EQ(length, builder_->value_data_length()); + ASSERT_EQ(expected_capacity, builder_->value_data_capacity()); + + Done(); + + ASSERT_EQ(reps * N, result_->length()); + ASSERT_EQ(0, result_->null_count()); + ASSERT_EQ(reps * 40, result_->value_data()->size()); + ASSERT_EQ(expected_capacity, result_->value_data()->capacity()); +} + TEST_F(TestBinaryBuilder, TestZeroLength) { // All buffers are null Done(); diff --git a/cpp/src/arrow/buffer.h b/cpp/src/arrow/buffer.h index b50b1a1aa041d..44c352a93f273 100644 --- a/cpp/src/arrow/buffer.h +++ b/cpp/src/arrow/buffer.h @@ -333,6 +333,7 @@ class ARROW_EXPORT TypedBufferBuilder : public BufferBuilder { const T* data() const { return reinterpret_cast(data_); } int64_t length() const { return size_ / sizeof(T); } + int64_t capacity() const { return capacity_ / sizeof(T); } }; /// \brief Allocate a fixed size mutable buffer from a memory pool diff --git a/cpp/src/arrow/builder.cc b/cpp/src/arrow/builder.cc index de132b5f6a0d1..db901526fc2ee 100644 --- a/cpp/src/arrow/builder.cc +++ b/cpp/src/arrow/builder.cc @@ -1165,13 +1165,13 @@ Status ListBuilder::Init(int64_t elements) { DCHECK_LT(elements, std::numeric_limits::max()); RETURN_NOT_OK(ArrayBuilder::Init(elements)); // one more then requested for offsets - return offsets_builder_.Resize((elements + 1) * sizeof(int64_t)); + return offsets_builder_.Resize((elements + 1) * sizeof(int32_t)); } Status ListBuilder::Resize(int64_t capacity) { DCHECK_LT(capacity, std::numeric_limits::max()); // one more then requested for offsets - RETURN_NOT_OK(offsets_builder_.Resize((capacity + 1) * sizeof(int64_t))); + RETURN_NOT_OK(offsets_builder_.Resize((capacity + 1) * sizeof(int32_t))); return ArrayBuilder::Resize(capacity); } @@ -1216,16 +1216,26 @@ Status BinaryBuilder::Init(int64_t elements) { DCHECK_LT(elements, std::numeric_limits::max()); RETURN_NOT_OK(ArrayBuilder::Init(elements)); // one more then requested for offsets - return offsets_builder_.Resize((elements + 1) * sizeof(int64_t)); + return offsets_builder_.Resize((elements + 1) * sizeof(int32_t)); } Status BinaryBuilder::Resize(int64_t capacity) { DCHECK_LT(capacity, std::numeric_limits::max()); // one more then requested for offsets - RETURN_NOT_OK(offsets_builder_.Resize((capacity + 1) * sizeof(int64_t))); + RETURN_NOT_OK(offsets_builder_.Resize((capacity + 1) * sizeof(int32_t))); return ArrayBuilder::Resize(capacity); } +Status BinaryBuilder::ReserveData(int64_t elements) { + if (value_data_length() + elements > value_data_capacity()) { + if (value_data_length() + elements > std::numeric_limits::max()) { + return Status::Invalid("Cannot reserve capacity larger than 2^31 - 1 for binary"); + } + RETURN_NOT_OK(value_data_builder_.Reserve(elements)); + } + return Status::OK(); +} + Status BinaryBuilder::AppendNextOffset() { const int64_t num_bytes = value_data_builder_.length(); if (ARROW_PREDICT_FALSE(num_bytes > kMaximumCapacity)) { diff --git a/cpp/src/arrow/builder.h b/cpp/src/arrow/builder.h index ce7b8cd197da3..d1611f60cd924 100644 --- a/cpp/src/arrow/builder.h +++ b/cpp/src/arrow/builder.h @@ -682,10 +682,15 @@ class ARROW_EXPORT BinaryBuilder : public ArrayBuilder { Status Init(int64_t elements) override; Status Resize(int64_t capacity) override; + /// \brief Ensures there is enough allocated capacity to append the indicated + /// number of bytes to the value data buffer without additional allocations + Status ReserveData(int64_t elements); Status FinishInternal(std::shared_ptr* out) override; /// \return size of values buffer so far int64_t value_data_length() const { return value_data_builder_.length(); } + /// \return capacity of values buffer + int64_t value_data_capacity() const { return value_data_builder_.capacity(); } /// Temporary access to a value. /// From 2126ebf8a755e3ee884058be4aae83585a55107e Mon Sep 17 00:00:00 2001 From: Jim Crist Date: Wed, 24 Jan 2018 20:33:06 -0500 Subject: [PATCH 14/46] ARROW-2025: [C++] Creating multiple equivalent `HadoopFileSystem`s works fine Previously creating two instances of `HadoopFileSystem` using the same init parameters would result in both pointing to the same `hdfsFS` object. If one `HadoopFileSystem` disconnected then the underlying `hdfsFS` would be closed for both instances. To fix this, we force a new instance of `hdfsFS` on connect, removing this cacheing behavior. Author: Jim Crist Closes #1499 from jcrist/no-cache-hdfs and squashes the following commits: f8ff1351 [Jim Crist] Add test bf6627e8 [Jim Crist] Force libhdfs/libhdfs3 to return new FS on connect --- cpp/src/arrow/io/hdfs-internal.cc | 5 +++++ cpp/src/arrow/io/hdfs-internal.h | 4 ++++ cpp/src/arrow/io/hdfs.cc | 1 + cpp/src/arrow/io/io-hdfs-test.cc | 15 +++++++++++++++ 4 files changed, 25 insertions(+) diff --git a/cpp/src/arrow/io/hdfs-internal.cc b/cpp/src/arrow/io/hdfs-internal.cc index 545b2d17d2e78..efceb8ae6b403 100644 --- a/cpp/src/arrow/io/hdfs-internal.cc +++ b/cpp/src/arrow/io/hdfs-internal.cc @@ -310,6 +310,10 @@ void LibHdfsShim::BuilderSetKerbTicketCachePath(hdfsBuilder* bld, this->hdfsBuilderSetKerbTicketCachePath(bld, kerbTicketCachePath); } +void LibHdfsShim::BuilderSetForceNewInstance(hdfsBuilder* bld) { + this->hdfsBuilderSetForceNewInstance(bld); +} + hdfsFS LibHdfsShim::BuilderConnect(hdfsBuilder* bld) { return this->hdfsBuilderConnect(bld); } @@ -490,6 +494,7 @@ Status LibHdfsShim::GetRequiredSymbols() { GET_SYMBOL_REQUIRED(this, hdfsBuilderSetNameNodePort); GET_SYMBOL_REQUIRED(this, hdfsBuilderSetUserName); GET_SYMBOL_REQUIRED(this, hdfsBuilderSetKerbTicketCachePath); + GET_SYMBOL_REQUIRED(this, hdfsBuilderSetForceNewInstance); GET_SYMBOL_REQUIRED(this, hdfsBuilderConnect); GET_SYMBOL_REQUIRED(this, hdfsCreateDirectory); GET_SYMBOL_REQUIRED(this, hdfsDelete); diff --git a/cpp/src/arrow/io/hdfs-internal.h b/cpp/src/arrow/io/hdfs-internal.h index df925cf62823a..f0fce23c229ab 100644 --- a/cpp/src/arrow/io/hdfs-internal.h +++ b/cpp/src/arrow/io/hdfs-internal.h @@ -51,6 +51,7 @@ struct LibHdfsShim { void (*hdfsBuilderSetUserName)(hdfsBuilder* bld, const char* userName); void (*hdfsBuilderSetKerbTicketCachePath)(hdfsBuilder* bld, const char* kerbTicketCachePath); + void (*hdfsBuilderSetForceNewInstance)(hdfsBuilder* bld); hdfsFS (*hdfsBuilderConnect)(hdfsBuilder* bld); int (*hdfsDisconnect)(hdfsFS fs); @@ -95,6 +96,7 @@ struct LibHdfsShim { this->hdfsBuilderSetNameNodePort = nullptr; this->hdfsBuilderSetUserName = nullptr; this->hdfsBuilderSetKerbTicketCachePath = nullptr; + this->hdfsBuilderSetForceNewInstance = nullptr; this->hdfsBuilderConnect = nullptr; this->hdfsDisconnect = nullptr; this->hdfsOpenFile = nullptr; @@ -138,6 +140,8 @@ struct LibHdfsShim { void BuilderSetKerbTicketCachePath(hdfsBuilder* bld, const char* kerbTicketCachePath); + void BuilderSetForceNewInstance(hdfsBuilder* bld); + hdfsFS BuilderConnect(hdfsBuilder* bld); int Disconnect(hdfsFS fs); diff --git a/cpp/src/arrow/io/hdfs.cc b/cpp/src/arrow/io/hdfs.cc index 6e3e4a7a1c7e7..6c569ae1e2786 100644 --- a/cpp/src/arrow/io/hdfs.cc +++ b/cpp/src/arrow/io/hdfs.cc @@ -335,6 +335,7 @@ class HadoopFileSystem::HadoopFileSystemImpl { if (!config->kerb_ticket.empty()) { driver_->BuilderSetKerbTicketCachePath(builder, config->kerb_ticket.c_str()); } + driver_->BuilderSetForceNewInstance(builder); fs_ = driver_->BuilderConnect(builder); if (fs_ == nullptr) { diff --git a/cpp/src/arrow/io/io-hdfs-test.cc b/cpp/src/arrow/io/io-hdfs-test.cc index 5305b4774624d..f2ded6ff4b945 100644 --- a/cpp/src/arrow/io/io-hdfs-test.cc +++ b/cpp/src/arrow/io/io-hdfs-test.cc @@ -178,6 +178,21 @@ TYPED_TEST(TestHadoopFileSystem, ConnectsAgain) { ASSERT_OK(client->Disconnect()); } +TYPED_TEST(TestHadoopFileSystem, MultipleClients) { + SKIP_IF_NO_DRIVER(); + + std::shared_ptr client1; + std::shared_ptr client2; + ASSERT_OK(HadoopFileSystem::Connect(&this->conf_, &client1)); + ASSERT_OK(HadoopFileSystem::Connect(&this->conf_, &client2)); + ASSERT_OK(client1->Disconnect()); + + // client2 continues to function after equivalent client1 has shutdown + std::vector listing; + EXPECT_OK(client2->ListDirectory(this->scratch_dir_, &listing)); + ASSERT_OK(client2->Disconnect()); +} + TYPED_TEST(TestHadoopFileSystem, MakeDirectory) { SKIP_IF_NO_DRIVER(); From 6bb1d1b35f21ce34327cf92893bda417c2a9a4f1 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 25 Jan 2018 10:38:32 -0500 Subject: [PATCH 15/46] ARROW-2003: [Python] Remove use of fastpath parameter to pandas.core.internals.make_block Apparently this argument is not used at all in pandas, and the pandas developers wish to simply remove the argument rather than go through a deprecation cycle Author: Wes McKinney Closes #1507 from wesm/ARROW-2003 and squashes the following commits: a8382262 [Wes McKinney] Remove use of fastpath parameter to pandas.core.internals.make_block --- python/pyarrow/pandas_compat.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/pyarrow/pandas_compat.py b/python/pyarrow/pandas_compat.py index f3089d2a012a6..4a30fb3b44a4e 100644 --- a/python/pyarrow/pandas_compat.py +++ b/python/pyarrow/pandas_compat.py @@ -435,13 +435,12 @@ def _reconstruct_block(item): categories=item['dictionary'], ordered=item['ordered']) block = _int.make_block(cat, placement=placement, - klass=_int.CategoricalBlock, - fastpath=True) + klass=_int.CategoricalBlock) elif 'timezone' in item: dtype = _make_datetimetz(item['timezone']) block = _int.make_block(block_arr, placement=placement, klass=_int.DatetimeTZBlock, - dtype=dtype, fastpath=True) + dtype=dtype) else: block = _int.make_block(block_arr, placement=placement) From db83fb400d932782ebb32a93582f8ab9cbd1130b Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Thu, 25 Jan 2018 17:07:17 +0100 Subject: [PATCH 16/46] [C++] Update README for linting (#1515) * [C++] Update README for linting There are hidden gotchas when trying to move the linting Makefile targets, mention them. * Mention the LLVM releases page --- cpp/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/cpp/README.md b/cpp/README.md index d2262a68512ce..b063248b30af4 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -252,6 +252,24 @@ Logging IWYU to /tmp/arrow-cpp-iwyu.gT7XXV ... ``` +### Linting + +We require that you follow a certain coding style in the C++ code base. +You can check your code abides by that coding style by running: + + make lint + +You can also fix any formatting errors automatically: + + make format + +These commands require `clang-format-4.0` (and not any other version). +You may find the required packages at http://releases.llvm.org/download.html +or use the Debian/Ubuntu APT repositories on https://apt.llvm.org/. + +Also, if under a Python 3 environment, you need to install a compatible +version of `cpplint` using `pip install cpplint`. + ## Continuous Integration Pull requests are run through travis-ci for continuous integration. You can avoid From 68b119b7c290f240c47cf54a2932bfd4794a10f8 Mon Sep 17 00:00:00 2001 From: Jim Crist Date: Thu, 25 Jan 2018 11:37:42 -0500 Subject: [PATCH 17/46] ARROW-2029: [Python] NativeFile.tell errors after close Previously checking if the file was closed was subclass specific, and wasn't caught in the hdfs backed file, leading to program crashes. This adds a check in `NativeFile.tell` for the file being open, and a test on a few subclasses of `NativeFile` to assure the error is raised. Note that since most python file-like objects raise a `ValueError` for operations after close, I changed the type of the existing error for these cases. This could be changed back, but an error should at least be thrown. Author: Jim Crist Closes #1502 from jcrist/hdfs-tell-on-closed and squashes the following commits: 8a9dc947 [Jim Crist] NativeFile.tell errors after close --- python/pyarrow/io.pxi | 13 +++++++------ python/pyarrow/tests/test_io.py | 26 +++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/python/pyarrow/io.pxi b/python/pyarrow/io.pxi index 5449872ff101f..bb363bacc2e24 100644 --- a/python/pyarrow/io.pxi +++ b/python/pyarrow/io.pxi @@ -91,20 +91,20 @@ cdef class NativeFile: self._assert_writeable() file[0] = self.wr_file + def _assert_open(self): + if not self.is_open: + raise ValueError("I/O operation on closed file") + def _assert_readable(self): + self._assert_open() if not self.is_readable: raise IOError("only valid on readonly files") - if not self.is_open: - raise IOError("file not open") - def _assert_writeable(self): + self._assert_open() if not self.is_writeable: raise IOError("only valid on writeable files") - if not self.is_open: - raise IOError("file not open") - def size(self): """ Return file size @@ -120,6 +120,7 @@ cdef class NativeFile: Return current stream position """ cdef int64_t position + self._assert_open() with nogil: if self.is_readable: check_status(self.rd_file.get().Tell(&position)) diff --git a/python/pyarrow/tests/test_io.py b/python/pyarrow/tests/test_io.py index e60dd35de66fe..3f7aa2e1c83bd 100644 --- a/python/pyarrow/tests/test_io.py +++ b/python/pyarrow/tests/test_io.py @@ -257,7 +257,7 @@ def test_inmemory_write_after_closed(): f.write(b'ok') f.get_result() - with pytest.raises(IOError): + with pytest.raises(ValueError): f.write(b'not ok') @@ -503,3 +503,27 @@ def test_native_file_modes(tmpdir): with pa.memory_map(path, 'r+b') as f: assert f.mode == 'rb+' + + +def test_native_file_raises_ValueError_after_close(tmpdir): + path = os.path.join(str(tmpdir), guid()) + with open(path, 'wb') as f: + f.write(b'foooo') + + with pa.OSFile(path, mode='rb') as os_file: + pass + + with pa.memory_map(path, mode='rb') as mmap_file: + pass + + files = [os_file, + mmap_file] + + methods = [('tell', ()), + ('seek', (0,)), + ('size', ())] + + for f in files: + for method, args in methods: + with pytest.raises(ValueError): + getattr(f, method)(*args) From 1a9d024781e8435e6ae010c55c32c9a9d7fa1e16 Mon Sep 17 00:00:00 2001 From: Sidd Date: Thu, 25 Jan 2018 10:33:27 -0800 Subject: [PATCH 18/46] ARROW-2019: [JAVA] Control the memory allocated for inner vector in LIST (#1497) * ARROW-2019: [JAVA] Control the memory allocated for inner vector in LIST * address review comments --- .../arrow/vector/BaseVariableWidthVector.java | 36 ++++++++++ .../complex/BaseRepeatedValueVector.java | 32 +++++++++ .../arrow/vector/complex/ListVector.java | 57 ++++++++++++++-- .../apache/arrow/vector/TestListVector.java | 68 +++++++++++++++++++ .../apache/arrow/vector/TestValueVector.java | 36 ++++++++++ .../arrow/vector/TestVectorReAlloc.java | 4 +- 6 files changed, 224 insertions(+), 9 deletions(-) diff --git a/java/vector/src/main/java/org/apache/arrow/vector/BaseVariableWidthVector.java b/java/vector/src/main/java/org/apache/arrow/vector/BaseVariableWidthVector.java index fff329a9b9d66..d1190ceb7b672 100644 --- a/java/vector/src/main/java/org/apache/arrow/vector/BaseVariableWidthVector.java +++ b/java/vector/src/main/java/org/apache/arrow/vector/BaseVariableWidthVector.java @@ -169,6 +169,42 @@ public void setInitialCapacity(int valueCount) { offsetAllocationSizeInBytes = (valueCount + 1) * OFFSET_WIDTH; } + /** + * Sets the desired value capacity for the vector. This function doesn't + * allocate any memory for the vector. + * @param valueCount desired number of elements in the vector + * @param density average number of bytes per variable width element + */ + public void setInitialCapacity(int valueCount, double density) { + final long size = (long) (valueCount * density); + if (size < 1) { + throw new IllegalArgumentException("With the provided density and value count, potential capacity of the data buffer is 0"); + } + if (size > MAX_ALLOCATION_SIZE) { + throw new OversizedAllocationException("Requested amount of memory is more than max allowed"); + } + valueAllocationSizeInBytes = (int) size; + validityAllocationSizeInBytes = getValidityBufferSizeFromCount(valueCount); + /* to track the end offset of last data element in vector, we need + * an additional slot in offset buffer. + */ + offsetAllocationSizeInBytes = (valueCount + 1) * OFFSET_WIDTH; + } + + /** + * Get the density of this ListVector + * @return density + */ + public double getDensity() { + if (valueCount == 0) { + return 0.0D; + } + final int startOffset = offsetBuffer.getInt(0); + final int endOffset = offsetBuffer.getInt(valueCount * OFFSET_WIDTH); + final double totalListSize = endOffset - startOffset; + return totalListSize/valueCount; + } + /** * Get the current value capacity for the vector * @return number of elements that vector can hold. diff --git a/java/vector/src/main/java/org/apache/arrow/vector/complex/BaseRepeatedValueVector.java b/java/vector/src/main/java/org/apache/arrow/vector/complex/BaseRepeatedValueVector.java index d0a664ac01da2..50ee3a7573efe 100644 --- a/java/vector/src/main/java/org/apache/arrow/vector/complex/BaseRepeatedValueVector.java +++ b/java/vector/src/main/java/org/apache/arrow/vector/complex/BaseRepeatedValueVector.java @@ -143,6 +143,38 @@ public void setInitialCapacity(int numRecords) { } } + /** + * Specialized version of setInitialCapacity() for ListVector. This is + * used by some callers when they want to explicitly control and be + * conservative about memory allocated for inner data vector. This is + * very useful when we are working with memory constraints for a query + * and have a fixed amount of memory reserved for the record batch. In + * such cases, we are likely to face OOM or related problems when + * we reserve memory for a record batch with value count x and + * do setInitialCapacity(x) such that each vector allocates only + * what is necessary and not the default amount but the multiplier + * forces the memory requirement to go beyond what was needed. + * + * @param numRecords value count + * @param density density of ListVector. Density is the average size of + * list per position in the List vector. For example, a + * density value of 10 implies each position in the list + * vector has a list of 10 values. + * A density value of 0.1 implies out of 10 positions in + * the list vector, 1 position has a list of size 1 and + * remaining positions are null (no lists) or empty lists. + * This helps in tightly controlling the memory we provision + * for inner data vector. + */ + public void setInitialCapacity(int numRecords, double density) { + offsetAllocationSizeInBytes = (numRecords + 1) * OFFSET_WIDTH; + final int innerValueCapacity = (int)(numRecords * density); + if (innerValueCapacity < 1) { + throw new IllegalArgumentException("With the provided density and value count, potential value capacity for the data vector is 0"); + } + vector.setInitialCapacity(innerValueCapacity); + } + @Override public int getValueCapacity() { final int offsetValueCapacity = Math.max(getOffsetBufferValueCapacity() - 1, 0); diff --git a/java/vector/src/main/java/org/apache/arrow/vector/complex/ListVector.java b/java/vector/src/main/java/org/apache/arrow/vector/complex/ListVector.java index 8aeeb7e5a2886..b472dae069431 100644 --- a/java/vector/src/main/java/org/apache/arrow/vector/complex/ListVector.java +++ b/java/vector/src/main/java/org/apache/arrow/vector/complex/ListVector.java @@ -31,12 +31,7 @@ import org.apache.arrow.memory.BaseAllocator; import org.apache.arrow.memory.BufferAllocator; import org.apache.arrow.memory.OutOfMemoryException; -import org.apache.arrow.vector.AddOrGetResult; -import org.apache.arrow.vector.BufferBacked; -import org.apache.arrow.vector.FieldVector; -import org.apache.arrow.vector.ValueVector; -import org.apache.arrow.vector.ZeroVector; -import org.apache.arrow.vector.BitVectorHelper; +import org.apache.arrow.vector.*; import org.apache.arrow.vector.complex.impl.ComplexCopier; import org.apache.arrow.vector.complex.impl.UnionListReader; import org.apache.arrow.vector.complex.impl.UnionListWriter; @@ -102,6 +97,54 @@ public void initializeChildrenFromFields(List children) { addOrGetVector.getVector().initializeChildrenFromFields(field.getChildren()); } + @Override + public void setInitialCapacity(int numRecords) { + validityAllocationSizeInBytes = getValidityBufferSizeFromCount(numRecords); + super.setInitialCapacity(numRecords); + } + + /** + * Specialized version of setInitialCapacity() for ListVector. This is + * used by some callers when they want to explicitly control and be + * conservative about memory allocated for inner data vector. This is + * very useful when we are working with memory constraints for a query + * and have a fixed amount of memory reserved for the record batch. In + * such cases, we are likely to face OOM or related problems when + * we reserve memory for a record batch with value count x and + * do setInitialCapacity(x) such that each vector allocates only + * what is necessary and not the default amount but the multiplier + * forces the memory requirement to go beyond what was needed. + * + * @param numRecords value count + * @param density density of ListVector. Density is the average size of + * list per position in the List vector. For example, a + * density value of 10 implies each position in the list + * vector has a list of 10 values. + * A density value of 0.1 implies out of 10 positions in + * the list vector, 1 position has a list of size 1 and + * remaining positions are null (no lists) or empty lists. + * This helps in tightly controlling the memory we provision + * for inner data vector. + */ + public void setInitialCapacity(int numRecords, double density) { + validityAllocationSizeInBytes = getValidityBufferSizeFromCount(numRecords); + super.setInitialCapacity(numRecords, density); + } + + /** + * Get the density of this ListVector + * @return density + */ + public double getDensity() { + if (valueCount == 0) { + return 0.0D; + } + final int startOffset = offsetBuffer.getInt(0); + final int endOffset = offsetBuffer.getInt(valueCount * OFFSET_WIDTH); + final double totalListSize = endOffset - startOffset; + return totalListSize/valueCount; + } + @Override public List getChildrenFromFields() { return singletonList(getDataVector()); @@ -623,7 +666,7 @@ public int getNullCount() { */ @Override public int getValueCapacity() { - return Math.min(getValidityBufferValueCapacity(), super.getValueCapacity()); + return getValidityAndOffsetValueCapacity(); } private int getValidityAndOffsetValueCapacity() { diff --git a/java/vector/src/test/java/org/apache/arrow/vector/TestListVector.java b/java/vector/src/test/java/org/apache/arrow/vector/TestListVector.java index e2023f4461879..d49a677f67922 100644 --- a/java/vector/src/test/java/org/apache/arrow/vector/TestListVector.java +++ b/java/vector/src/test/java/org/apache/arrow/vector/TestListVector.java @@ -112,6 +112,9 @@ public void testCopyFrom() throws Exception { result = outVector.getObject(2); resultSet = (ArrayList) result; assertEquals(0, resultSet.size()); + + /* 3+0+0/3 */ + assertEquals(1.0D, inVector.getDensity(), 0); } } @@ -209,6 +212,9 @@ public void testSetLastSetUsage() throws Exception { listVector.setLastSet(3); listVector.setValueCount(10); + /* (3+2+3)/10 */ + assertEquals(0.8D, listVector.getDensity(), 0); + index = 0; offset = offsetBuffer.getInt(index * ListVector.OFFSET_WIDTH); assertEquals(Integer.toString(0), Integer.toString(offset)); @@ -709,6 +715,8 @@ public void testGetBufferAddress() throws Exception { listWriter.bigInt().writeBigInt(300); listWriter.endList(); + listVector.setValueCount(2); + /* check listVector contents */ Object result = listVector.getObject(0); ArrayList resultSet = (ArrayList) result; @@ -739,6 +747,9 @@ public void testGetBufferAddress() throws Exception { assertEquals(2, buffers.size()); assertEquals(bitAddress, buffers.get(0).memoryAddress()); assertEquals(offsetAddress, buffers.get(1).memoryAddress()); + + /* (3+2)/2 */ + assertEquals(2.5, listVector.getDensity(), 0); } } @@ -753,4 +764,61 @@ public void testConsistentChildName() throws Exception { assertTrue(emptyVectorStr.contains(ListVector.DATA_VECTOR_NAME)); } } + + @Test + public void testSetInitialCapacity() { + try (final ListVector vector = ListVector.empty("", allocator)) { + vector.addOrGetVector(FieldType.nullable(MinorType.INT.getType())); + + /** + * use the default multiplier of 5, + * 512 * 5 => 2560 * 4 => 10240 bytes => 16KB => 4096 value capacity. + */ + vector.setInitialCapacity(512); + vector.allocateNew(); + assertEquals(512, vector.getValueCapacity()); + assertEquals(4096, vector.getDataVector().getValueCapacity()); + + /* use density as 4 */ + vector.setInitialCapacity(512, 4); + vector.allocateNew(); + assertEquals(512, vector.getValueCapacity()); + assertEquals(512*4, vector.getDataVector().getValueCapacity()); + + /** + * inner value capacity we pass to data vector is 512 * 0.1 => 51 + * For an int vector this is 204 bytes of memory for data buffer + * and 7 bytes for validity buffer. + * and with power of 2 allocation, we allocate 256 bytes and 8 bytes + * for the data buffer and validity buffer of the inner vector. Thus + * value capacity of inner vector is 64 + */ + vector.setInitialCapacity(512, 0.1); + vector.allocateNew(); + assertEquals(512, vector.getValueCapacity()); + assertEquals(64, vector.getDataVector().getValueCapacity()); + + /** + * inner value capacity we pass to data vector is 512 * 0.01 => 5 + * For an int vector this is 20 bytes of memory for data buffer + * and 1 byte for validity buffer. + * and with power of 2 allocation, we allocate 32 bytes and 1 bytes + * for the data buffer and validity buffer of the inner vector. Thus + * value capacity of inner vector is 8 + */ + vector.setInitialCapacity(512, 0.01); + vector.allocateNew(); + assertEquals(512, vector.getValueCapacity()); + assertEquals(8, vector.getDataVector().getValueCapacity()); + + boolean error = false; + try { + vector.setInitialCapacity(5, 0.1); + } catch (IllegalArgumentException e) { + error = true; + } finally { + assertTrue(error); + } + } + } } diff --git a/java/vector/src/test/java/org/apache/arrow/vector/TestValueVector.java b/java/vector/src/test/java/org/apache/arrow/vector/TestValueVector.java index 601b2062ff698..992bb6264a1cf 100644 --- a/java/vector/src/test/java/org/apache/arrow/vector/TestValueVector.java +++ b/java/vector/src/test/java/org/apache/arrow/vector/TestValueVector.java @@ -1908,4 +1908,40 @@ public static void setBytes(int index, byte[] bytes, VarCharVector vector) { vector.offsetBuffer.setInt((index + 1) * vector.OFFSET_WIDTH, currentOffset + bytes.length); vector.valueBuffer.setBytes(currentOffset, bytes, 0, bytes.length); } + + @Test /* VarCharVector */ + public void testSetInitialCapacity() { + try (final VarCharVector vector = new VarCharVector(EMPTY_SCHEMA_PATH, allocator)) { + + /* use the default 8 data bytes on average per element */ + vector.setInitialCapacity(4096); + vector.allocateNew(); + assertEquals(4096, vector.getValueCapacity()); + assertEquals(4096 * 8, vector.getDataBuffer().capacity()); + + vector.setInitialCapacity(4096, 1); + vector.allocateNew(); + assertEquals(4096, vector.getValueCapacity()); + assertEquals(4096, vector.getDataBuffer().capacity()); + + vector.setInitialCapacity(4096, 0.1); + vector.allocateNew(); + assertEquals(4096, vector.getValueCapacity()); + assertEquals(512, vector.getDataBuffer().capacity()); + + vector.setInitialCapacity(4096, 0.01); + vector.allocateNew(); + assertEquals(4096, vector.getValueCapacity()); + assertEquals(64, vector.getDataBuffer().capacity()); + + boolean error = false; + try { + vector.setInitialCapacity(5, 0.1); + } catch (IllegalArgumentException e) { + error = true; + } finally { + assertTrue(error); + } + } + } } diff --git a/java/vector/src/test/java/org/apache/arrow/vector/TestVectorReAlloc.java b/java/vector/src/test/java/org/apache/arrow/vector/TestVectorReAlloc.java index f8edf8904c53e..ca039c52f9715 100644 --- a/java/vector/src/test/java/org/apache/arrow/vector/TestVectorReAlloc.java +++ b/java/vector/src/test/java/org/apache/arrow/vector/TestVectorReAlloc.java @@ -104,7 +104,7 @@ public void testListType() { vector.setInitialCapacity(512); vector.allocateNew(); - assertEquals(1023, vector.getValueCapacity()); + assertEquals(512, vector.getValueCapacity()); try { vector.getInnerValueCountAt(2014); @@ -114,7 +114,7 @@ public void testListType() { } vector.reAlloc(); - assertEquals(2047, vector.getValueCapacity()); // note: size - 1 + assertEquals(1024, vector.getValueCapacity()); assertEquals(0, vector.getOffsetBuffer().getInt(2014 * ListVector.OFFSET_WIDTH)); } } From 8edd62e1bda0bf38f0fce872167be99826d28da5 Mon Sep 17 00:00:00 2001 From: Jim Crist Date: Thu, 25 Jan 2018 17:01:02 -0500 Subject: [PATCH 19/46] ARROW-2031: [Python] HadoopFileSystem is pickleable Adds support for pickling `HadoopFileSystem` A few additional small fixes: - Adds a check that `driver` is one of {'libhdfs3', 'libhdfs'} - A few small tweaks to the hdfs tests to make them easier to run locally. Author: Jim Crist Closes #1505 from jcrist/pickle-hdfs-filesystem and squashes the following commits: b19ed3e0 [Jim Crist] Compat with older cython versions 1f747264 [Jim Crist] HadoopFileSystem is pickleable --- python/pyarrow/hdfs.py | 4 ++++ python/pyarrow/io-hdfs.pxi | 20 ++++++++++++++++---- python/pyarrow/tests/test_hdfs.py | 23 ++++++++++++++++++++--- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/python/pyarrow/hdfs.py b/python/pyarrow/hdfs.py index 3c9d04188a6ca..3f2014b65c097 100644 --- a/python/pyarrow/hdfs.py +++ b/python/pyarrow/hdfs.py @@ -36,6 +36,10 @@ def __init__(self, host="default", port=0, user=None, kerb_ticket=None, self._connect(host, port, user, kerb_ticket, driver) + def __reduce__(self): + return (HadoopFileSystem, (self.host, self.port, self.user, + self.kerb_ticket, self.driver)) + @implements(FileSystem.isdir) def isdir(self, path): return super(HadoopFileSystem, self).isdir(path) diff --git a/python/pyarrow/io-hdfs.pxi b/python/pyarrow/io-hdfs.pxi index e653813235862..3abf045f93336 100644 --- a/python/pyarrow/io-hdfs.pxi +++ b/python/pyarrow/io-hdfs.pxi @@ -59,29 +59,41 @@ cdef class HadoopFileSystem: cdef readonly: bint is_open - - def __cinit__(self): - pass + str host + str user + str kerb_ticket + str driver + int port def _connect(self, host, port, user, kerb_ticket, driver): cdef HdfsConnectionConfig conf if host is not None: conf.host = tobytes(host) + self.host = host + conf.port = port + self.port = port + if user is not None: conf.user = tobytes(user) + self.user = user + if kerb_ticket is not None: conf.kerb_ticket = tobytes(kerb_ticket) + self.kerb_ticket = kerb_ticket if driver == 'libhdfs': with nogil: check_status(HaveLibHdfs()) conf.driver = HdfsDriver_LIBHDFS - else: + elif driver == 'libhdfs3': with nogil: check_status(HaveLibHdfs3()) conf.driver = HdfsDriver_LIBHDFS3 + else: + raise ValueError("unknown driver: %r" % driver) + self.driver = driver with nogil: check_status(CHadoopFileSystem.Connect(&conf, &self.client)) diff --git a/python/pyarrow/tests/test_hdfs.py b/python/pyarrow/tests/test_hdfs.py index 51b6ba25bd657..b62458cd73689 100644 --- a/python/pyarrow/tests/test_hdfs.py +++ b/python/pyarrow/tests/test_hdfs.py @@ -18,6 +18,7 @@ from io import BytesIO from os.path import join as pjoin import os +import pickle import random import unittest @@ -36,7 +37,7 @@ def hdfs_test_client(driver='libhdfs'): host = os.environ.get('ARROW_HDFS_TEST_HOST', 'localhost') - user = os.environ['ARROW_HDFS_TEST_USER'] + user = os.environ.get('ARROW_HDFS_TEST_USER', None) try: port = int(os.environ.get('ARROW_HDFS_TEST_PORT', 20500)) except ValueError: @@ -72,6 +73,22 @@ def tearDownClass(cls): cls.hdfs.delete(cls.tmp_path, recursive=True) cls.hdfs.close() + def test_unknown_driver(self): + with pytest.raises(ValueError): + hdfs_test_client(driver="not_a_driver_name") + + def test_pickle(self): + s = pickle.dumps(self.hdfs) + h2 = pickle.loads(s) + assert h2.is_open + assert h2.host == self.hdfs.host + assert h2.port == self.hdfs.port + assert h2.user == self.hdfs.user + assert h2.kerb_ticket == self.hdfs.kerb_ticket + assert h2.driver == self.hdfs.driver + # smoketest unpickled client works + h2.ls(self.tmp_path) + def test_cat(self): path = pjoin(self.tmp_path, 'cat-test') @@ -299,7 +316,7 @@ class TestLibHdfs(HdfsTestCases, unittest.TestCase): @classmethod def check_driver(cls): if not pa.have_libhdfs(): - pytest.fail('No libhdfs available on system') + pytest.skip('No libhdfs available on system') def test_orphaned_file(self): hdfs = hdfs_test_client() @@ -318,4 +335,4 @@ class TestLibHdfs3(HdfsTestCases, unittest.TestCase): @classmethod def check_driver(cls): if not pa.have_libhdfs3(): - pytest.fail('No libhdfs3 available on system') + pytest.skip('No libhdfs3 available on system') From 51046a0ac80913df99605ca4d78d8561fe3101d5 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 25 Jan 2018 23:17:28 +0100 Subject: [PATCH 20/46] ARROW-1961: [Python] Preserve pre-existing schema metadata in Parquet files when passing flavor='spark' Author: Wes McKinney Closes #1511 from wesm/ARROW-1961 and squashes the following commits: e13b6b4 [Wes McKinney] Preserve pre-existing schema metadata when sanitizing fields when passing flavor='spark' --- python/pyarrow/parquet.py | 4 +++- python/pyarrow/tests/test_parquet.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/python/pyarrow/parquet.py b/python/pyarrow/parquet.py index 151e0df8a22d0..3a0924a27ceb2 100644 --- a/python/pyarrow/parquet.py +++ b/python/pyarrow/parquet.py @@ -215,7 +215,9 @@ def _sanitize_schema(schema, flavor): sanitized_fields.append(sanitized_field) else: sanitized_fields.append(field) - return pa.schema(sanitized_fields), schema_changed + + new_schema = pa.schema(sanitized_fields, metadata=schema.metadata) + return new_schema, schema_changed else: return schema, False diff --git a/python/pyarrow/tests/test_parquet.py b/python/pyarrow/tests/test_parquet.py index c2bb31c9bcf51..7c2edb378df61 100644 --- a/python/pyarrow/tests/test_parquet.py +++ b/python/pyarrow/tests/test_parquet.py @@ -748,6 +748,28 @@ def test_sanitized_spark_field_names(): assert result.schema[0].name == expected_name +def _roundtrip_pandas_dataframe(df, write_kwargs): + table = pa.Table.from_pandas(df) + + buf = io.BytesIO() + _write_table(table, buf, **write_kwargs) + + buf.seek(0) + table1 = _read_table(buf) + return table1.to_pandas() + + +@parquet +def test_spark_flavor_preserves_pandas_metadata(): + df = _test_dataframe(size=100) + df.index = np.arange(0, 10 * len(df), 10) + df.index.name = 'foo' + + result = _roundtrip_pandas_dataframe(df, {'version': '2.0', + 'flavor': 'spark'}) + tm.assert_frame_equal(result, df) + + @parquet def test_fixed_size_binary(): t0 = pa.binary(10) From bfce44beb918807b17b5c94a6c4efdb3d7ff6e5f Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 25 Jan 2018 23:39:29 +0100 Subject: [PATCH 21/46] ARROW-2017: [Python] Use unsigned PyLong API for uint64 values over int64 range Author: Wes McKinney Closes #1504 from wesm/ARROW-2017 and squashes the following commits: 56e67dc [Wes McKinney] Use unsigned PyLong API for uint64 values over int64 range --- cpp/src/arrow/python/builtin_convert.cc | 2 +- python/pyarrow/tests/test_array.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc index cd88d557d4830..0879b3f98d770 100644 --- a/cpp/src/arrow/python/builtin_convert.cc +++ b/cpp/src/arrow/python/builtin_convert.cc @@ -511,7 +511,7 @@ class UInt32Converter : public TypedConverterVisitor { public: Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + const auto val = static_cast(PyLong_AsUnsignedLongLong(item.obj())); RETURN_IF_PYERROR(); return typed_builder_->Append(val); } diff --git a/python/pyarrow/tests/test_array.py b/python/pyarrow/tests/test_array.py index fa38c9257854e..2d991119f85b1 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -485,6 +485,12 @@ def test_logical_type(type, expected): assert get_logical_type(type) == expected +def test_array_uint64_from_py_over_range(): + arr = pa.array([2 ** 63], type=pa.uint64()) + expected = pa.array(np.array([2 ** 63], dtype='u8')) + assert arr.equals(expected) + + def test_array_conversions_no_sentinel_values(): arr = np.array([1, 2, 3, 4], dtype='int8') refcount = sys.getrefcount(arr) From f680dac68ef5bc911499ae0b62e14c46046816a1 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Fri, 26 Jan 2018 10:28:08 -0500 Subject: [PATCH 22/46] ARROW-2007: [Python] Implement float32 conversions, use NumPy dtype when possible for inner arrays Author: Wes McKinney Closes #1509 from wesm/ARROW-2007 and squashes the following commits: cd12626d [Wes McKinney] Pin thrift-cpp in Appveyor 326c82e1 [Wes McKinney] Pin Thrift 0.10.0 in toolchain e334f4e2 [Wes McKinney] Add explicit type check db046597 [Wes McKinney] Implement float32 conversions, use NumPy dtype when possible for inner arrays rather than dispatching to the generic sequence routine --- ci/msvc-build.bat | 2 +- ci/travis_before_script_cpp.sh | 2 +- cpp/src/arrow/python/builtin_convert.cc | 11 +++++++++++ cpp/src/arrow/python/numpy_to_arrow.cc | 13 ++++++++++++- python/pyarrow/tests/test_array.py | 17 +++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/ci/msvc-build.bat b/ci/msvc-build.bat index 62ebcf364e77b..94eb16a5e506b 100644 --- a/ci/msvc-build.bat +++ b/ci/msvc-build.bat @@ -81,7 +81,7 @@ conda info -a conda create -n arrow -q -y python=%PYTHON% ^ six pytest setuptools numpy pandas cython ^ - thrift-cpp + thrift-cpp=0.10.0 if "%JOB%" == "Toolchain" ( diff --git a/ci/travis_before_script_cpp.sh b/ci/travis_before_script_cpp.sh index fd2c1644638c4..2f164c4168d0d 100755 --- a/ci/travis_before_script_cpp.sh +++ b/ci/travis_before_script_cpp.sh @@ -47,7 +47,7 @@ if [ "$ARROW_TRAVIS_USE_TOOLCHAIN" == "1" ]; then zlib \ cmake \ curl \ - thrift-cpp \ + thrift-cpp=0.10.0 \ ninja # HACK(wesm): We started experiencing OpenSSL failures when Miniconda was diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc index 0879b3f98d770..71f2fde5b3920 100644 --- a/cpp/src/arrow/python/builtin_convert.cc +++ b/cpp/src/arrow/python/builtin_convert.cc @@ -586,6 +586,15 @@ class TimestampConverter TimeUnit::type unit_; }; +class Float32Converter : public TypedConverterVisitor { + public: + Status AppendItem(const OwnedRef& item) { + float val = static_cast(PyFloat_AsDouble(item.obj())); + RETURN_IF_PYERROR(); + return typed_builder_->Append(val); + } +}; + class DoubleConverter : public TypedConverterVisitor { public: Status AppendItem(const OwnedRef& item) { @@ -740,6 +749,8 @@ std::shared_ptr GetConverter(const std::shared_ptr& type case Type::TIMESTAMP: return std::make_shared( static_cast(*type).unit()); + case Type::FLOAT: + return std::make_shared(); case Type::DOUBLE: return std::make_shared(); case Type::BINARY: diff --git a/cpp/src/arrow/python/numpy_to_arrow.cc b/cpp/src/arrow/python/numpy_to_arrow.cc index c5c02e355ded6..b5a75aeedd5eb 100644 --- a/cpp/src/arrow/python/numpy_to_arrow.cc +++ b/cpp/src/arrow/python/numpy_to_arrow.cc @@ -1008,10 +1008,21 @@ Status NumPyConverter::ConvertObjectsInfer() { return ConvertTimes(); } else if (PyObject_IsInstance(const_cast(obj), Decimal.obj())) { return ConvertDecimals(); - } else if (PyList_Check(obj) || PyArray_Check(obj)) { + } else if (PyList_Check(obj)) { std::shared_ptr inferred_type; RETURN_NOT_OK(InferArrowType(obj, &inferred_type)); return ConvertLists(inferred_type); + } else if (PyArray_Check(obj)) { + std::shared_ptr inferred_type; + PyArray_Descr* dtype = PyArray_DESCR(reinterpret_cast(obj)); + + if (dtype->type_num == NPY_OBJECT) { + RETURN_NOT_OK(InferArrowType(obj, &inferred_type)); + } else { + RETURN_NOT_OK( + NumPyDtypeToArrow(reinterpret_cast(dtype), &inferred_type)); + } + return ConvertLists(inferred_type); } else { const std::string supported_types = "string, bool, float, int, date, time, decimal, list, array"; diff --git a/python/pyarrow/tests/test_array.py b/python/pyarrow/tests/test_array.py index 2d991119f85b1..1d5d30071902a 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -513,6 +513,23 @@ def test_array_from_numpy_datetimeD(): assert result.equals(expected) +def test_array_from_py_float32(): + data = [[1.2, 3.4], [9.0, 42.0]] + + t = pa.float32() + + arr1 = pa.array(data[0], type=t) + arr2 = pa.array(data, type=pa.list_(t)) + + expected1 = np.array(data[0], dtype=np.float32) + expected2 = pd.Series([np.array(data[0], dtype=np.float32), + np.array(data[1], dtype=np.float32)]) + + assert arr1.type == t + assert arr1.equals(pa.array(expected1)) + assert arr2.equals(pa.array(expected2)) + + def test_array_from_numpy_ascii(): arr = np.array(['abcde', 'abc', ''], dtype='|S5') From a95465b8ce7a32feeaae3e13d0a64102ffa590d9 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Fri, 26 Jan 2018 10:30:08 -0500 Subject: [PATCH 23/46] ARROW-2035: [C++] Update vendored cpplint.py to a Py3-compatible one Author: Wes McKinney Author: Antoine Pitrou Closes #1516 from pitrou/ARROW-2035-update-cpplint and squashes the following commits: 8f2f892b [Wes McKinney] Fix IWYU errors surfaced by newer cpplint f5175dda [Antoine Pitrou] ARROW-2035: [C++] Update vendored cpplint.py to a Py3-compatible one --- cpp/README.md | 3 - cpp/build-support/cpplint.py | 1638 +++++++++-------- cpp/src/arrow/adapters/orc/adapter.cc | 1 + cpp/src/arrow/array.cc | 1 + cpp/src/arrow/array.h | 1 + cpp/src/arrow/builder.cc | 1 + cpp/src/arrow/compute/context.h | 2 + cpp/src/arrow/compute/kernels/hash.cc | 1 + .../arrow/compute/kernels/util-internal.cc | 1 + cpp/src/arrow/compute/kernels/util-internal.h | 1 + cpp/src/arrow/ipc/feather.cc | 1 + cpp/src/arrow/ipc/reader.cc | 1 + cpp/src/arrow/pretty_print.cc | 1 + cpp/src/arrow/python/arrow_to_python.cc | 1 + cpp/src/arrow/python/io.cc | 1 + cpp/src/arrow/python/io.h | 2 + cpp/src/arrow/python/numpy_to_arrow.cc | 1 + cpp/src/arrow/record_batch.cc | 1 + cpp/src/arrow/table.cc | 1 + cpp/src/arrow/table_builder.cc | 1 + cpp/src/arrow/type.cc | 2 + cpp/src/arrow/type_traits.h | 1 + cpp/src/arrow/util/io-util.h | 1 + cpp/src/plasma/events.cc | 2 + cpp/src/plasma/plasma.h | 1 + cpp/src/plasma/protocol.h | 2 + cpp/src/plasma/store.cc | 2 + cpp/src/plasma/store.h | 2 + 28 files changed, 928 insertions(+), 746 deletions(-) diff --git a/cpp/README.md b/cpp/README.md index b063248b30af4..ef2e1fd1b1259 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -267,9 +267,6 @@ These commands require `clang-format-4.0` (and not any other version). You may find the required packages at http://releases.llvm.org/download.html or use the Debian/Ubuntu APT repositories on https://apt.llvm.org/. -Also, if under a Python 3 environment, you need to install a compatible -version of `cpplint` using `pip install cpplint`. - ## Continuous Integration Pull requests are run through travis-ci for continuous integration. You can avoid diff --git a/cpp/build-support/cpplint.py b/cpp/build-support/cpplint.py index ccc25d4c56b1a..95c0c32595d81 100755 --- a/cpp/build-support/cpplint.py +++ b/cpp/build-support/cpplint.py @@ -44,6 +44,8 @@ import codecs import copy import getopt +import glob +import itertools import math # for log import os import re @@ -51,16 +53,47 @@ import string import sys import unicodedata +import xml.etree.ElementTree + +# if empty, use defaults +_header_extensions = set([]) + +# if empty, use defaults +_valid_extensions = set([]) + + +# Files with any of these extensions are considered to be +# header files (and will undergo different style checks). +# This set can be extended by using the --headers +# option (also supported in CPPLINT.cfg) +def GetHeaderExtensions(): + if not _header_extensions: + return set(['h', 'hpp', 'hxx', 'h++', 'cuh']) + return _header_extensions + +# The allowed extensions for file names +# This is set by --extensions flag +def GetAllExtensions(): + if not _valid_extensions: + return GetHeaderExtensions().union(set(['c', 'cc', 'cpp', 'cxx', 'c++', 'cu'])) + return _valid_extensions + +def GetNonHeaderExtensions(): + return GetAllExtensions().difference(GetHeaderExtensions()) _USAGE = """ -Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] - [--counting=total|toplevel|detailed] [--root=subdir] - [--linelength=digits] +Syntax: cpplint.py [--verbose=#] [--output=emacs|eclipse|vs7|junit] + [--filter=-x,+y,...] + [--counting=total|toplevel|detailed] [--repository=path] + [--root=subdir] [--linelength=digits] [--recursive] + [--exclude=path] + [--headers=ext1,ext2] + [--extensions=hpp,cpp,...] [file] ... The style guidelines this tries to follow are those in - http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml + https://google.github.io/styleguide/cppguide.html Every problem is given a confidence score from 1-5, with 5 meaning we are certain of the problem, and 1 meaning it could be a legitimate construct. @@ -71,17 +104,26 @@ suppresses errors of all categories on that line. The files passed in will be linted; at least one file must be provided. - Default linted extensions are .cc, .cpp, .cu, .cuh and .h. Change the - extensions with the --extensions flag. + Default linted extensions are %s. + Other file types will be ignored. + Change the extensions with the --extensions flag. Flags: - output=vs7 - By default, the output is formatted to ease emacs parsing. Visual Studio - compatible output (vs7) may also be used. Other formats are unsupported. + output=emacs|eclipse|vs7|junit + By default, the output is formatted to ease emacs parsing. Output + compatible with eclipse (eclipse), Visual Studio (vs7), and JUnit + XML parsers such as those used in Jenkins and Bamboo may also be + used. Other formats are unsupported. verbose=# Specify a number 0-5 to restrict errors to certain verbosity levels. + Errors with lower verbosity levels have lower confidence and are more + likely to be false positives. + + quiet + Supress output other than linting errors, such as information about + which files have been processed and excluded. filter=-x,+y,... Specify a comma-separated list of category-filters to apply: only @@ -105,17 +147,40 @@ also be printed. If 'detailed' is provided, then a count is provided for each category like 'build/class'. + repository=path + The top level directory of the repository, used to derive the header + guard CPP variable. By default, this is determined by searching for a + path that contains .git, .hg, or .svn. When this flag is specified, the + given path is used instead. This option allows the header guard CPP + variable to remain consistent even if members of a team have different + repository root directories (such as when checking out a subdirectory + with SVN). In addition, users of non-mainstream version control systems + can use this flag to ensure readable header guard CPP variables. + + Examples: + Assuming that Alice checks out ProjectName and Bob checks out + ProjectName/trunk and trunk contains src/chrome/ui/browser.h, then + with no --repository flag, the header guard CPP variable will be: + + Alice => TRUNK_SRC_CHROME_BROWSER_UI_BROWSER_H_ + Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_ + + If Alice uses the --repository=trunk flag and Bob omits the flag or + uses --repository=. then the header guard CPP variable will be: + + Alice => SRC_CHROME_BROWSER_UI_BROWSER_H_ + Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_ + root=subdir - The root directory used for deriving header guard CPP variable. - By default, the header guard CPP variable is calculated as the relative - path to the directory that contains .git, .hg, or .svn. When this flag - is specified, the relative path is calculated from the specified - directory. If the specified directory does not exist, this flag is - ignored. + The root directory used for deriving header guard CPP variables. This + directory is relative to the top level directory of the repository which + by default is determined by searching for a directory that contains .git, + .hg, or .svn but can also be controlled with the --repository flag. If + the specified directory does not exist, this flag is ignored. Examples: - Assuming that src/.git exists, the header guard CPP variables for - src/chrome/browser/ui/browser.h are: + Assuming that src is the top level directory of the repository, the + header guard CPP variables for src/chrome/browser/ui/browser.h are: No flag => CHROME_BROWSER_UI_BROWSER_H_ --root=chrome => BROWSER_UI_BROWSER_H_ @@ -128,11 +193,36 @@ Examples: --linelength=120 + recursive + Search for files to lint recursively. Each directory given in the list + of files to be linted is replaced by all files that descend from that + directory. Files with extensions not in the valid extensions list are + excluded. + + exclude=path + Exclude the given path from the list of files to be linted. Relative + paths are evaluated relative to the current directory and shell globbing + is performed. This flag can be provided multiple times to exclude + multiple files. + + Examples: + --exclude=one.cc + --exclude=src/*.cc + --exclude=src/*.cc --exclude=test/*.cc + extensions=extension,extension,... The allowed file extensions that cpplint will check Examples: - --extensions=hpp,cpp + --extensions=%s + + headers=extension,extension,... + The allowed header extensions that cpplint will consider to be header files + (by default, only files with extensions %s + will be assumed to be headers) + + Examples: + --headers=%s cpplint.py supports per-directory configurations specified in CPPLINT.cfg files. CPPLINT.cfg file can contain a number of key=value pairs. @@ -142,6 +232,7 @@ filter=+filter1,-filter2,... exclude_files=regex linelength=80 + root=subdir "set noparent" option prevents cpplint from traversing directory tree upwards looking for more .cfg files in parent directories. This option @@ -153,22 +244,28 @@ "exclude_files" allows to specify a regular expression to be matched against a file name. If the expression matches, the file is skipped and not run - through liner. + through the linter. + + "linelength" specifies the allowed line length for the project. - "linelength" allows to specify the allowed line length for the project. + The "root" option is similar in function to the --root flag (see example + above). CPPLINT.cfg has an effect on files in the same directory and all - sub-directories, unless overridden by a nested configuration file. + subdirectories, unless overridden by a nested configuration file. Example file: filter=-build/include_order,+build/include_alpha - exclude_files=.*\.cc + exclude_files=.*\\.cc The above example disables build/include_order warning and enables build/include_alpha as well as excludes all .cc from being processed by linter, in the current directory (where the .cfg - file is located) and all sub-directories. -""" + file is located) and all subdirectories. +""" % (list(GetAllExtensions()), + ','.join(list(GetAllExtensions())), + GetHeaderExtensions(), + ','.join(GetHeaderExtensions())) # We categorize each error message we print. Here are the categories. # We want an explicit list so we can list them all in cpplint --filter=. @@ -177,15 +274,19 @@ _ERROR_CATEGORIES = [ 'build/class', 'build/c++11', + 'build/c++14', + 'build/c++tr1', 'build/deprecated', 'build/endif_comment', 'build/explicit_make_pair', 'build/forward_decl', 'build/header_guard', 'build/include', + 'build/include_subdir', 'build/include_alpha', 'build/include_order', 'build/include_what_you_use', + 'build/namespaces_literals', 'build/namespaces', 'build/printf_format', 'build/storage_class', @@ -196,7 +297,6 @@ 'readability/check', 'readability/constructors', 'readability/fn_size', - 'readability/function', 'readability/inheritance', 'readability/multiline_comment', 'readability/multiline_string', @@ -227,6 +327,7 @@ 'whitespace/comma', 'whitespace/comments', 'whitespace/empty_conditional_body', + 'whitespace/empty_if_body', 'whitespace/empty_loop_body', 'whitespace/end_of_line', 'whitespace/ending_newline', @@ -245,6 +346,7 @@ # compatibility they may still appear in NOLINT comments. _LEGACY_ERROR_CATEGORIES = [ 'readability/streams', + 'readability/function', ] # The default state of the category filter. This is overridden by the --filter= @@ -253,6 +355,16 @@ # All entries here should start with a '-' or '+', as in the --filter= flag. _DEFAULT_FILTERS = ['-build/include_alpha'] +# The default list of categories suppressed for C (not C++) files. +_DEFAULT_C_SUPPRESSED_CATEGORIES = [ + 'readability/casting', + ] + +# The default list of categories suppressed for Linux Kernel files. +_DEFAULT_KERNEL_SUPPRESSED_CATEGORIES = [ + 'whitespace/tab', + ] + # We used to check for high-bit characters, but after much discussion we # decided those were OK, as long as they were in UTF-8 and didn't represent # hard-coded international strings, which belong in a separate i18n file. @@ -346,6 +458,7 @@ 'random', 'ratio', 'regex', + 'scoped_allocator', 'set', 'sstream', 'stack', @@ -393,6 +506,19 @@ 'cwctype', ]) +# Type names +_TYPES = re.compile( + r'^(?:' + # [dcl.type.simple] + r'(char(16_t|32_t)?)|wchar_t|' + r'bool|short|int|long|signed|unsigned|float|double|' + # [support.types] + r'(ptrdiff_t|size_t|max_align_t|nullptr_t)|' + # [cstdint.syn] + r'(u?int(_fast|_least)?(8|16|32|64)_t)|' + r'(u?int(max|ptr)_t)|' + r')$') + # These headers are excluded from [build/include] and [build/include_order] # checks: @@ -402,20 +528,23 @@ _THIRD_PARTY_HEADERS_PATTERN = re.compile( r'^(?:[^/]*[A-Z][^/]*\.h|lua\.h|lauxlib\.h|lualib\.h)$') +# Pattern for matching FileInfo.BaseName() against test file name +_test_suffixes = ['_test', '_regtest', '_unittest'] +_TEST_FILE_SUFFIX = '(' + '|'.join(_test_suffixes) + r')$' + +# Pattern that matches only complete whitespace, possibly across multiple lines. +_EMPTY_CONDITIONAL_BODY_PATTERN = re.compile(r'^\s*$', re.DOTALL) # Assertion macros. These are defined in base/logging.h and -# testing/base/gunit.h. Note that the _M versions need to come first -# for substring matching to work. +# testing/base/public/gunit.h. _CHECK_MACROS = [ 'DCHECK', 'CHECK', - 'EXPECT_TRUE_M', 'EXPECT_TRUE', - 'ASSERT_TRUE_M', 'ASSERT_TRUE', - 'EXPECT_FALSE_M', 'EXPECT_FALSE', - 'ASSERT_FALSE_M', 'ASSERT_FALSE', + 'EXPECT_TRUE', 'ASSERT_TRUE', + 'EXPECT_FALSE', 'ASSERT_FALSE', ] # Replacement macros for CHECK/DCHECK/EXPECT_TRUE/EXPECT_FALSE -_CHECK_REPLACEMENT = dict([(m, {}) for m in _CHECK_MACROS]) +_CHECK_REPLACEMENT = dict([(macro_var, {}) for macro_var in _CHECK_MACROS]) for op, replacement in [('==', 'EQ'), ('!=', 'NE'), ('>=', 'GE'), ('>', 'GT'), @@ -424,16 +553,12 @@ _CHECK_REPLACEMENT['CHECK'][op] = 'CHECK_%s' % replacement _CHECK_REPLACEMENT['EXPECT_TRUE'][op] = 'EXPECT_%s' % replacement _CHECK_REPLACEMENT['ASSERT_TRUE'][op] = 'ASSERT_%s' % replacement - _CHECK_REPLACEMENT['EXPECT_TRUE_M'][op] = 'EXPECT_%s_M' % replacement - _CHECK_REPLACEMENT['ASSERT_TRUE_M'][op] = 'ASSERT_%s_M' % replacement for op, inv_replacement in [('==', 'NE'), ('!=', 'EQ'), ('>=', 'LT'), ('>', 'LE'), ('<=', 'GT'), ('<', 'GE')]: _CHECK_REPLACEMENT['EXPECT_FALSE'][op] = 'EXPECT_%s' % inv_replacement _CHECK_REPLACEMENT['ASSERT_FALSE'][op] = 'ASSERT_%s' % inv_replacement - _CHECK_REPLACEMENT['EXPECT_FALSE_M'][op] = 'EXPECT_%s_M' % inv_replacement - _CHECK_REPLACEMENT['ASSERT_FALSE_M'][op] = 'ASSERT_%s_M' % inv_replacement # Alternative tokens and their replacements. For full list, see section 2.5 # Alternative tokens [lex.digraph] in the C++ standard. @@ -482,6 +607,12 @@ r'(?:\s+(volatile|__volatile__))?' r'\s*[{(]') +# Match strings that indicate we're working on a C (not C++) file. +_SEARCH_C_FILE = re.compile(r'\b(?:LINT_C_FILE|' + r'vim?:\s*.*(\s*|:)filetype=c(\s*|:|$))') + +# Match string that indicates we're working on a Linux Kernel file. +_SEARCH_KERNEL_FILE = re.compile(r'\b(?:LINT_KERNEL_FILE)') _regexp_compile_cache = {} @@ -493,16 +624,64 @@ # This is set by --root flag. _root = None +# The top level repository directory. If set, _root is calculated relative to +# this directory instead of the directory containing version control artifacts. +# This is set by the --repository flag. +_repository = None + +# Files to exclude from linting. This is set by the --exclude flag. +_excludes = None + +# Whether to supress PrintInfo messages +_quiet = False + # The allowed line length of files. # This is set by --linelength flag. _line_length = 80 -# The allowed extensions for file names -# This is set by --extensions flag. -_valid_extensions = set(['cc', 'h', 'cpp', 'cu', 'cuh']) +try: + xrange(1, 0) +except NameError: + # -- pylint: disable=redefined-builtin + xrange = range + +try: + unicode +except NameError: + # -- pylint: disable=redefined-builtin + basestring = unicode = str + +try: + long(2) +except NameError: + # -- pylint: disable=redefined-builtin + long = int + +if sys.version_info < (3,): + # -- pylint: disable=no-member + # BINARY_TYPE = str + itervalues = dict.itervalues + iteritems = dict.iteritems +else: + # BINARY_TYPE = bytes + itervalues = dict.values + iteritems = dict.items + +def unicode_escape_decode(x): + if sys.version_info < (3,): + return codecs.unicode_escape_decode(x)[0] + else: + return x + +# {str, bool}: a map from error categories to booleans which indicate if the +# category should be suppressed for every line. +_global_error_suppressions = {} + + + def ParseNolintSuppressions(filename, raw_line, linenum, error): - """Updates the global list of error-suppressions. + """Updates the global list of line error-suppressions. Parses any NOLINT comments on the current line, updating the global error_suppressions store. Reports an error if the NOLINT comment @@ -533,24 +712,45 @@ def ParseNolintSuppressions(filename, raw_line, linenum, error): 'Unknown NOLINT error category: %s' % category) +def ProcessGlobalSuppresions(lines): + """Updates the list of global error suppressions. + + Parses any lint directives in the file that have global effect. + + Args: + lines: An array of strings, each representing a line of the file, with the + last element being empty if the file is terminated with a newline. + """ + for line in lines: + if _SEARCH_C_FILE.search(line): + for category in _DEFAULT_C_SUPPRESSED_CATEGORIES: + _global_error_suppressions[category] = True + if _SEARCH_KERNEL_FILE.search(line): + for category in _DEFAULT_KERNEL_SUPPRESSED_CATEGORIES: + _global_error_suppressions[category] = True + + def ResetNolintSuppressions(): """Resets the set of NOLINT suppressions to empty.""" _error_suppressions.clear() + _global_error_suppressions.clear() def IsErrorSuppressedByNolint(category, linenum): """Returns true if the specified error category is suppressed on this line. Consults the global error_suppressions map populated by - ParseNolintSuppressions/ResetNolintSuppressions. + ParseNolintSuppressions/ProcessGlobalSuppresions/ResetNolintSuppressions. Args: category: str, the category of the error. linenum: int, the current line number. Returns: - bool, True iff the error should be suppressed due to a NOLINT comment. + bool, True iff the error should be suppressed due to a NOLINT comment or + global suppression. """ - return (linenum in _error_suppressions.get(category, set()) or + return (_global_error_suppressions.get(category, False) or + linenum in _error_suppressions.get(category, set()) or linenum in _error_suppressions.get(None, set())) @@ -589,6 +789,11 @@ def Search(pattern, s): return _regexp_compile_cache[pattern].search(s) +def _IsSourceExtension(s): + """File extension (excluding dot) matches a source file extension.""" + return s in GetNonHeaderExtensions() + + class _IncludeState(object): """Tracks line numbers for includes, and the order in which includes appear. @@ -626,6 +831,8 @@ class _IncludeState(object): def __init__(self): self.include_list = [[]] + self._section = None + self._last_header = None self.ResetSection('') def FindHeader(self, header): @@ -769,9 +976,16 @@ def __init__(self): # output format: # "emacs" - format that emacs can parse (default) + # "eclipse" - format that eclipse can parse # "vs7" - format that Microsoft Visual Studio 7 can parse + # "junit" - format that Jenkins, Bamboo, etc can parse self.output_format = 'emacs' + # For JUnit output, save errors and failures until the end so that they + # can be written into the XML + self._junit_errors = [] + self._junit_failures = [] + def SetOutputFormat(self, output_format): """Sets the output format for errors.""" self.output_format = output_format @@ -840,10 +1054,69 @@ def IncrementErrorCount(self, category): def PrintErrorCounts(self): """Print a summary of errors by category, and the total.""" - for category, count in self.errors_by_category.iteritems(): - sys.stderr.write('Category \'%s\' errors found: %d\n' % + for category, count in sorted(iteritems(self.errors_by_category)): + self.PrintInfo('Category \'%s\' errors found: %d\n' % (category, count)) - sys.stderr.write('Total errors found: %d\n' % self.error_count) + if self.error_count > 0: + self.PrintInfo('Total errors found: %d\n' % self.error_count) + + def PrintInfo(self, message): + if not _quiet and self.output_format != 'junit': + sys.stderr.write(message) + + def PrintError(self, message): + if self.output_format == 'junit': + self._junit_errors.append(message) + else: + sys.stderr.write(message) + + def AddJUnitFailure(self, filename, linenum, message, category, confidence): + self._junit_failures.append((filename, linenum, message, category, + confidence)) + + def FormatJUnitXML(self): + num_errors = len(self._junit_errors) + num_failures = len(self._junit_failures) + + testsuite = xml.etree.ElementTree.Element('testsuite') + testsuite.attrib['name'] = 'cpplint' + testsuite.attrib['errors'] = str(num_errors) + testsuite.attrib['failures'] = str(num_failures) + + if num_errors == 0 and num_failures == 0: + testsuite.attrib['tests'] = str(1) + xml.etree.ElementTree.SubElement(testsuite, 'testcase', name='passed') + + else: + testsuite.attrib['tests'] = str(num_errors + num_failures) + if num_errors > 0: + testcase = xml.etree.ElementTree.SubElement(testsuite, 'testcase') + testcase.attrib['name'] = 'errors' + error = xml.etree.ElementTree.SubElement(testcase, 'error') + error.text = '\n'.join(self._junit_errors) + if num_failures > 0: + # Group failures by file + failed_file_order = [] + failures_by_file = {} + for failure in self._junit_failures: + failed_file = failure[0] + if failed_file not in failed_file_order: + failed_file_order.append(failed_file) + failures_by_file[failed_file] = [] + failures_by_file[failed_file].append(failure) + # Create a testcase for each file + for failed_file in failed_file_order: + failures = failures_by_file[failed_file] + testcase = xml.etree.ElementTree.SubElement(testsuite, 'testcase') + testcase.attrib['name'] = failed_file + failure = xml.etree.ElementTree.SubElement(testcase, 'failure') + template = '{0}: {1} [{2}] [{3}]' + texts = [template.format(f[1], f[2], f[3], f[4]) for f in failures] + failure.text = '\n'.join(texts) + + xml_decl = '\n' + return xml_decl + xml.etree.ElementTree.tostring(testsuite, 'utf-8').decode('utf-8') + _cpplint_state = _CppLintState() @@ -944,6 +1217,9 @@ def Check(self, error, filename, linenum): filename: The name of the current file. linenum: The number of the line to check. """ + if not self.in_a_function: + return + if Match(r'T(EST|est)', self.current_function): base_trigger = self._TEST_TRIGGER else: @@ -986,7 +1262,7 @@ def FullName(self): return os.path.abspath(self._filename).replace('\\', '/') def RepositoryName(self): - """FullName after removing the local path to the repository. + r"""FullName after removing the local path to the repository. If we have a real absolute path name here we can try to do something smart: detecting the root of the checkout and truncating /path/to/checkout from @@ -1000,6 +1276,20 @@ def RepositoryName(self): if os.path.exists(fullname): project_dir = os.path.dirname(fullname) + # If the user specified a repository path, it exists, and the file is + # contained in it, use the specified repository path + if _repository: + repo = FileInfo(_repository).FullName() + root_dir = project_dir + while os.path.exists(root_dir): + # allow case insensitive compare on Windows + if os.path.normcase(root_dir) == os.path.normcase(repo): + return os.path.relpath(fullname, root_dir).replace('\\', '/') + one_up_dir = os.path.dirname(root_dir) + if one_up_dir == root_dir: + break + root_dir = one_up_dir + if os.path.exists(os.path.join(project_dir, ".svn")): # If there's a .svn file in the current directory, we recursively look # up the directory tree for the top of the SVN checkout @@ -1014,12 +1304,13 @@ def RepositoryName(self): # Not SVN <= 1.6? Try to find a git, hg, or svn top level directory by # searching up from the current path. - root_dir = os.path.dirname(fullname) - while (root_dir != os.path.dirname(root_dir) and - not os.path.exists(os.path.join(root_dir, ".git")) and - not os.path.exists(os.path.join(root_dir, ".hg")) and - not os.path.exists(os.path.join(root_dir, ".svn"))): - root_dir = os.path.dirname(root_dir) + root_dir = current_dir = os.path.dirname(fullname) + while current_dir != os.path.dirname(current_dir): + if (os.path.exists(os.path.join(current_dir, ".git")) or + os.path.exists(os.path.join(current_dir, ".hg")) or + os.path.exists(os.path.join(current_dir, ".svn"))): + root_dir = current_dir + current_dir = os.path.dirname(current_dir) if (os.path.exists(os.path.join(root_dir, ".git")) or os.path.exists(os.path.join(root_dir, ".hg")) or @@ -1049,7 +1340,7 @@ def BaseName(self): return self.Split()[1] def Extension(self): - """File extension - text following the final period.""" + """File extension - text following the final period, includes that period.""" return self.Split()[2] def NoExtension(self): @@ -1058,7 +1349,7 @@ def NoExtension(self): def IsSource(self): """File has a source file extension.""" - return self.Extension()[1:] in ('c', 'cc', 'cpp', 'cxx') + return _IsSourceExtension(self.Extension()[1:]) def _ShouldPrintError(category, confidence, linenum): @@ -1114,15 +1405,18 @@ def Error(filename, linenum, category, confidence, message): if _ShouldPrintError(category, confidence, linenum): _cpplint_state.IncrementErrorCount(category) if _cpplint_state.output_format == 'vs7': - sys.stderr.write('%s(%s): %s [%s] [%d]\n' % ( + _cpplint_state.PrintError('%s(%s): warning: %s [%s] [%d]\n' % ( filename, linenum, message, category, confidence)) elif _cpplint_state.output_format == 'eclipse': sys.stderr.write('%s:%s: warning: %s [%s] [%d]\n' % ( filename, linenum, message, category, confidence)) + elif _cpplint_state.output_format == 'junit': + _cpplint_state.AddJUnitFailure(filename, linenum, message, category, + confidence) else: - sys.stderr.write('%s:%s: %s [%s] [%d]\n' % ( - filename, linenum, message, category, confidence)) - + final_message = '%s:%s: %s [%s] [%d]\n' % ( + filename, linenum, message, category, confidence) + sys.stderr.write(final_message) # Matches standard C++ escape sequences per 2.13.2.3 of the C++ standard. _RE_PATTERN_CLEANSE_LINE_ESCAPES = re.compile( @@ -1204,8 +1498,18 @@ def CleanseRawStrings(raw_lines): while delimiter is None: # Look for beginning of a raw string. # See 2.14.15 [lex.string] for syntax. - matched = Match(r'^(.*)\b(?:R|u8R|uR|UR|LR)"([^\s\\()]*)\((.*)$', line) - if matched: + # + # Once we have matched a raw string, we check the prefix of the + # line to make sure that the line is not part of a single line + # comment. It's done this way because we remove raw strings + # before removing comments as opposed to removing comments + # before removing raw strings. This is because there are some + # cpplint checks that requires the comments to be preserved, but + # we don't want to check comments that are inside raw strings. + matched = Match(r'^(.*?)\b(?:R|u8R|uR|UR|LR)"([^\s\\()]*)\((.*)$', line) + if (matched and + not Match(r'^([^\'"]|\'(\\.|[^\'])*\'|"(\\.|[^"])*")*//', + matched.group(1))): delimiter = ')' + matched.group(2) + '"' end = matched.group(3).find(delimiter) @@ -1624,7 +1928,7 @@ def CheckForCopyright(filename, lines, error): # We'll say it should occur by line 10. Don't forget there's a # dummy line at the front. - for line in xrange(1, min(len(lines), 11)): + for line in range(1, min(len(lines), 11)): if re.search(r'Copyright', lines[line], re.I): break else: # means no copyright line was found error(filename, 0, 'legal/copyright', 5, @@ -1666,11 +1970,16 @@ def GetHeaderGuardCPPVariable(filename): filename = re.sub(r'/\.flymake/([^/]*)$', r'/\1', filename) # Replace 'c++' with 'cpp'. filename = filename.replace('C++', 'cpp').replace('c++', 'cpp') - + fileinfo = FileInfo(filename) file_path_from_root = fileinfo.RepositoryName() if _root: - file_path_from_root = re.sub('^' + _root + os.sep, '', file_path_from_root) + suffix = os.sep + # On Windows using directory separator will leave us with + # "bogus escape error" unless we properly escape regex. + if suffix == '\\': + suffix += '\\' + file_path_from_root = re.sub('^' + _root + suffix, '', file_path_from_root) return re.sub(r'[^a-zA-Z0-9]', '_', file_path_from_root).upper() + '_' @@ -1697,6 +2006,11 @@ def CheckForHeaderGuard(filename, clean_lines, error): if Search(r'//\s*NOLINT\(build/header_guard\)', i): return + # Allow pragma once instead of header guards + for i in raw_lines: + if Search(r'^\s*#pragma\s+once', i): + return + cppvar = GetHeaderGuardCPPVariable(filename) ifndef = '' @@ -1773,28 +2087,30 @@ def CheckForHeaderGuard(filename, clean_lines, error): def CheckHeaderFileIncluded(filename, include_state, error): - """Logs an error if a .cc file does not include its header.""" + """Logs an error if a source file does not include its header.""" # Do not check test files - if filename.endswith('_test.cc') or filename.endswith('_unittest.cc'): - return - fileinfo = FileInfo(filename) - headerfile = filename[0:len(filename) - 2] + 'h' - if not os.path.exists(headerfile): + if Search(_TEST_FILE_SUFFIX, fileinfo.BaseName()): return - headername = FileInfo(headerfile).RepositoryName() - first_include = 0 - for section_list in include_state.include_list: - for f in section_list: - if headername in f[0] or f[0] in headername: - return - if not first_include: - first_include = f[1] - error(filename, first_include, 'build/include', 5, - '%s should include its header file %s' % (fileinfo.RepositoryName(), - headername)) + for ext in GetHeaderExtensions(): + basefilename = filename[0:len(filename) - len(fileinfo.Extension())] + headerfile = basefilename + '.' + ext + if not os.path.exists(headerfile): + continue + headername = FileInfo(headerfile).RepositoryName() + first_include = None + for section_list in include_state.include_list: + for f in section_list: + if headername in f[0] or f[0] in headername: + return + if not first_include: + first_include = f[1] + + error(filename, first_include, 'build/include', 5, + '%s should include its header file %s' % (fileinfo.RepositoryName(), + headername)) def CheckForBadCharacters(filename, lines, error): @@ -1815,7 +2131,7 @@ def CheckForBadCharacters(filename, lines, error): error: The function to call with any errors found. """ for linenum, line in enumerate(lines): - if u'\ufffd' in line: + if unicode_escape_decode('\ufffd') in line: error(filename, linenum, 'readability/utf8', 5, 'Line contains invalid UTF-8 (or Unicode replacement character).') if '\0' in line: @@ -1997,7 +2313,8 @@ def IsForwardClassDeclaration(clean_lines, linenum): class _BlockInfo(object): """Stores information about a generic block of code.""" - def __init__(self, seen_open_brace): + def __init__(self, linenum, seen_open_brace): + self.starting_linenum = linenum self.seen_open_brace = seen_open_brace self.open_parentheses = 0 self.inline_asm = _NO_ASM @@ -2046,17 +2363,16 @@ def IsBlockInfo(self): class _ExternCInfo(_BlockInfo): """Stores information about an 'extern "C"' block.""" - def __init__(self): - _BlockInfo.__init__(self, True) + def __init__(self, linenum): + _BlockInfo.__init__(self, linenum, True) class _ClassInfo(_BlockInfo): """Stores information about a class.""" def __init__(self, name, class_or_struct, clean_lines, linenum): - _BlockInfo.__init__(self, False) + _BlockInfo.__init__(self, linenum, False) self.name = name - self.starting_linenum = linenum self.is_derived = False self.check_namespace_indentation = True if class_or_struct == 'struct': @@ -2124,9 +2440,8 @@ class _NamespaceInfo(_BlockInfo): """Stores information about a namespace.""" def __init__(self, name, linenum): - _BlockInfo.__init__(self, False) + _BlockInfo.__init__(self, linenum, False) self.name = name or '' - self.starting_linenum = linenum self.check_namespace_indentation = True def CheckEnd(self, filename, clean_lines, linenum, error): @@ -2145,7 +2460,7 @@ def CheckEnd(self, filename, clean_lines, linenum, error): # deciding what these nontrivial things are, so this check is # triggered by namespace size only, which works most of the time. if (linenum - self.starting_linenum < 10 - and not Match(r'};*\s*(//|/\*).*\bnamespace\b', line)): + and not Match(r'^\s*};*\s*(//|/\*).*\bnamespace\b', line)): return # Look for matching comment at end of namespace. @@ -2162,18 +2477,18 @@ def CheckEnd(self, filename, clean_lines, linenum, error): # expected namespace. if self.name: # Named namespace - if not Match((r'};*\s*(//|/\*).*\bnamespace\s+' + re.escape(self.name) + - r'[\*/\.\\\s]*$'), + if not Match((r'^\s*};*\s*(//|/\*).*\bnamespace\s+' + + re.escape(self.name) + r'[\*/\.\\\s]*$'), line): error(filename, linenum, 'readability/namespace', 5, 'Namespace should be terminated with "// namespace %s"' % self.name) else: # Anonymous namespace - if not Match(r'};*\s*(//|/\*).*\bnamespace[\*/\.\\\s]*$', line): + if not Match(r'^\s*};*\s*(//|/\*).*\bnamespace[\*/\.\\\s]*$', line): # If "// namespace anonymous" or "// anonymous namespace (more text)", # mention "// anonymous namespace" as an acceptable form - if Match(r'}.*\b(namespace anonymous|anonymous namespace)\b', line): + if Match(r'^\s*}.*\b(namespace anonymous|anonymous namespace)\b', line): error(filename, linenum, 'readability/namespace', 5, 'Anonymous namespace should be terminated with "// namespace"' ' or "// anonymous namespace"') @@ -2445,7 +2760,7 @@ def Update(self, filename, clean_lines, linenum, error): # class LOCKABLE API Object { # }; class_decl_match = Match( - r'^(\s*(?:template\s*<[\w\s<>,:]*>\s*)?' + r'^(\s*(?:template\s*<[\w\s<>,:=]*>\s*)?' r'(class|struct)\s+(?:[A-Z_]+\s+)*(\w+(?:::\w+)*))' r'(.*)$', line) if (class_decl_match and @@ -2512,9 +2827,9 @@ def Update(self, filename, clean_lines, linenum, error): if not self.SeenOpenBrace(): self.stack[-1].seen_open_brace = True elif Match(r'^extern\s*"[^"]*"\s*\{', line): - self.stack.append(_ExternCInfo()) + self.stack.append(_ExternCInfo(linenum)) else: - self.stack.append(_BlockInfo(True)) + self.stack.append(_BlockInfo(linenum, True)) if _MATCH_ASM.match(line): self.stack[-1].inline_asm = _BLOCK_ASM @@ -2626,7 +2941,8 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, r'\s+(register|static|extern|typedef)\b', line): error(filename, linenum, 'build/storage_class', 5, - 'Storage class (static, extern, typedef, etc) should be first.') + 'Storage-class specifier (static, extern, typedef, etc) should be ' + 'at the beginning of the declaration.') if Match(r'\s*#\s*endif\s*[^/\s]+', line): error(filename, linenum, 'build/endif_comment', 5, @@ -2665,9 +2981,7 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, base_classname = classinfo.name.split('::')[-1] # Look for single-argument constructors that aren't marked explicit. - # Technically a valid construct, but against style. Also look for - # non-single-argument constructors which are also technically valid, but - # strongly suggest something is wrong. + # Technically a valid construct, but against style. explicit_constructor_match = Match( r'\s+(?:inline\s+)?(explicit\s+)?(?:inline\s+)?%s\s*' r'\(((?:[^()]|\([^()]*\))*)\)' @@ -2694,6 +3008,7 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, constructor_args[i] = constructor_arg i += 1 + variadic_args = [arg for arg in constructor_args if '&&...' in arg] defaulted_args = [arg for arg in constructor_args if '=' in arg] noarg_constructor = (not constructor_args or # empty arg list # 'void' arg specifier @@ -2704,7 +3019,10 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, # all but at most one arg defaulted (len(constructor_args) >= 1 and not noarg_constructor and - len(defaulted_args) >= len(constructor_args) - 1)) + len(defaulted_args) >= len(constructor_args) - 1) or + # variadic arguments with zero or one argument + (len(constructor_args) <= 2 and + len(variadic_args) >= 1)) initializer_list_constructor = bool( onearg_constructor and Search(r'\bstd\s*::\s*initializer_list\b', constructor_args[0])) @@ -2717,7 +3035,7 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, onearg_constructor and not initializer_list_constructor and not copy_constructor): - if defaulted_args: + if defaulted_args or variadic_args: error(filename, linenum, 'runtime/explicit', 5, 'Constructors callable with one argument ' 'should be marked explicit.') @@ -2728,10 +3046,6 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum, if noarg_constructor: error(filename, linenum, 'runtime/explicit', 5, 'Zero-parameter constructors should not be marked explicit.') - else: - error(filename, linenum, 'runtime/explicit', 0, - 'Constructors that require multiple arguments ' - 'should not be marked explicit.') def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error): @@ -2786,6 +3100,7 @@ def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error): error(filename, linenum, 'whitespace/parens', 2, 'Extra space after (') if (Search(r'\w\s+\(', fncall) and + not Search(r'_{0,2}asm_{0,2}\s+_{0,2}volatile_{0,2}\s+\(', fncall) and not Search(r'#\s*define|typedef|using\s+\w+\s*=', fncall) and not Search(r'\w\s+\((\w+::)*\*\w+\)\(', fncall) and not Search(r'\bcase\s+\(', fncall)): @@ -2844,7 +3159,7 @@ def CheckForFunctionLengths(filename, clean_lines, linenum, """Reports for long function bodies. For an overview why this is done, see: - http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Write_Short_Functions + https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Write_Short_Functions Uses a simplistic algorithm assuming other style guidelines (especially spacing) are followed. @@ -2879,7 +3194,7 @@ def CheckForFunctionLengths(filename, clean_lines, linenum, if starting_func: body_found = False - for start_linenum in xrange(linenum, clean_lines.NumLines()): + for start_linenum in range(linenum, clean_lines.NumLines()): start_line = lines[start_linenum] joined_line += ' ' + start_line.lstrip() if Search(r'(;|})', start_line): # Declarations and trivial functions @@ -2923,9 +3238,7 @@ def CheckComment(line, filename, linenum, next_line_start, error): commentpos = line.find('//') if commentpos != -1: # Check if the // may be in quotes. If so, ignore it - # Comparisons made explicit for clarity -- pylint: disable=g-explicit-bool-comparison - if (line.count('"', 0, commentpos) - - line.count('\\"', 0, commentpos)) % 2 == 0: # not in quotes + if re.sub(r'\\.', '', line[0:commentpos]).count('"') % 2 == 0: # Allow one space for new scopes, two spaces otherwise: if (not (Match(r'^.*{ *//', line) and next_line_start == commentpos) and ((commentpos >= 1 and @@ -3174,8 +3487,8 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error): # macro context and don't do any checks. This avoids false # positives. # - # Note that && is not included here. Those are checked separately - # in CheckRValueReference + # Note that && is not included here. This is because there are too + # many false positives due to RValue references. match = Search(r'[^<>=!\s](==|!=|<=|>=|\|\|)[^<>=!\s,;\)]', line) if match: error(filename, linenum, 'whitespace/operators', 3, @@ -3209,7 +3522,7 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error): # # We also allow operators following an opening parenthesis, since # those tend to be macros that deal with operators. - match = Search(r'(operator|[^\s(<])(?:L|UL|ULL|l|ul|ull)?<<([^\s,=<])', line) + match = Search(r'(operator|[^\s(<])(?:L|UL|LL|ULL|l|ul|ll|ull)?<<([^\s,=<])', line) if (match and not (match.group(1).isdigit() and match.group(2).isdigit()) and not (match.group(1) == 'operator' and match.group(2) == ';')): error(filename, linenum, 'whitespace/operators', 3, @@ -3313,22 +3626,90 @@ def CheckCommaSpacing(filename, clean_lines, linenum, error): 'Missing space after ;') -def CheckBracesSpacing(filename, clean_lines, linenum, error): +def _IsType(clean_lines, nesting_state, expr): + """Check if expression looks like a type name, returns true if so. + + Args: + clean_lines: A CleansedLines instance containing the file. + nesting_state: A NestingState instance which maintains information about + the current stack of nested blocks being parsed. + expr: The expression to check. + Returns: + True, if token looks like a type. + """ + # Keep only the last token in the expression + last_word = Match(r'^.*(\b\S+)$', expr) + if last_word: + token = last_word.group(1) + else: + token = expr + + # Match native types and stdint types + if _TYPES.match(token): + return True + + # Try a bit harder to match templated types. Walk up the nesting + # stack until we find something that resembles a typename + # declaration for what we are looking for. + typename_pattern = (r'\b(?:typename|class|struct)\s+' + re.escape(token) + + r'\b') + block_index = len(nesting_state.stack) - 1 + while block_index >= 0: + if isinstance(nesting_state.stack[block_index], _NamespaceInfo): + return False + + # Found where the opening brace is. We want to scan from this + # line up to the beginning of the function, minus a few lines. + # template + # class C + # : public ... { // start scanning here + last_line = nesting_state.stack[block_index].starting_linenum + + next_block_start = 0 + if block_index > 0: + next_block_start = nesting_state.stack[block_index - 1].starting_linenum + first_line = last_line + while first_line >= next_block_start: + if clean_lines.elided[first_line].find('template') >= 0: + break + first_line -= 1 + if first_line < next_block_start: + # Didn't find any "template" keyword before reaching the next block, + # there are probably no template things to check for this block + block_index -= 1 + continue + + # Look for typename in the specified range + for i in xrange(first_line, last_line + 1, 1): + if Search(typename_pattern, clean_lines.elided[i]): + return True + block_index -= 1 + + return False + + +def CheckBracesSpacing(filename, clean_lines, linenum, nesting_state, error): """Checks for horizontal spacing near commas. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. + nesting_state: A NestingState instance which maintains information about + the current stack of nested blocks being parsed. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Except after an opening paren, or after another opening brace (in case of # an initializer list, for instance), you should have spaces before your - # braces. And since you should never have braces at the beginning of a line, - # this is an easy test. + # braces when they are delimiting blocks, classes, namespaces etc. + # And since you should never have braces at the beginning of a line, + # this is an easy test. Except that braces used for initialization don't + # follow the same rule; we often don't want spaces before those. match = Match(r'^(.*[^ ({>]){', line) + if match: # Try a bit harder to check for brace initialization. This # happens in one of the following forms: @@ -3358,6 +3739,7 @@ def CheckBracesSpacing(filename, clean_lines, linenum, error): # There is a false negative with this approach if people inserted # spurious semicolons, e.g. "if (cond){};", but we will catch the # spurious semicolon with a separate check. + leading_text = match.group(1) (endline, endlinenum, endpos) = CloseExpression( clean_lines, linenum, len(match.group(1))) trailing_text = '' @@ -3366,7 +3748,11 @@ def CheckBracesSpacing(filename, clean_lines, linenum, error): for offset in xrange(endlinenum + 1, min(endlinenum + 3, clean_lines.NumLines() - 1)): trailing_text += clean_lines.elided[offset] - if not Match(r'^[\s}]*[{.;,)<>\]:]', trailing_text): + # We also suppress warnings for `uint64_t{expression}` etc., as the style + # guide recommends brace initialization for integral types to avoid + # overflow/truncation. + if (not Match(r'^[\s}]*[{.;,)<>\]:]', trailing_text) + and not _IsType(clean_lines, nesting_state, leading_text)): error(filename, linenum, 'whitespace/braces', 5, 'Missing space before {') @@ -3409,406 +3795,6 @@ def IsDecltype(clean_lines, linenum, column): return True return False - -def IsTemplateParameterList(clean_lines, linenum, column): - """Check if the token ending on (linenum, column) is the end of template<>. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: the number of the line to check. - column: end column of the token to check. - Returns: - True if this token is end of a template parameter list, False otherwise. - """ - (_, startline, startpos) = ReverseCloseExpression( - clean_lines, linenum, column) - if (startpos > -1 and - Search(r'\btemplate\s*$', clean_lines.elided[startline][0:startpos])): - return True - return False - - -def IsRValueType(typenames, clean_lines, nesting_state, linenum, column): - """Check if the token ending on (linenum, column) is a type. - - Assumes that text to the right of the column is "&&" or a function - name. - - Args: - typenames: set of type names from template-argument-list. - clean_lines: A CleansedLines instance containing the file. - nesting_state: A NestingState instance which maintains information about - the current stack of nested blocks being parsed. - linenum: the number of the line to check. - column: end column of the token to check. - Returns: - True if this token is a type, False if we are not sure. - """ - prefix = clean_lines.elided[linenum][0:column] - - # Get one word to the left. If we failed to do so, this is most - # likely not a type, since it's unlikely that the type name and "&&" - # would be split across multiple lines. - match = Match(r'^(.*)(\b\w+|[>*)&])\s*$', prefix) - if not match: - return False - - # Check text following the token. If it's "&&>" or "&&," or "&&...", it's - # most likely a rvalue reference used inside a template. - suffix = clean_lines.elided[linenum][column:] - if Match(r'&&\s*(?:[>,]|\.\.\.)', suffix): - return True - - # Check for known types and end of templates: - # int&& variable - # vector&& variable - # - # Because this function is called recursively, we also need to - # recognize pointer and reference types: - # int* Function() - # int& Function() - if (match.group(2) in typenames or - match.group(2) in ['char', 'char16_t', 'char32_t', 'wchar_t', 'bool', - 'short', 'int', 'long', 'signed', 'unsigned', - 'float', 'double', 'void', 'auto', '>', '*', '&']): - return True - - # If we see a close parenthesis, look for decltype on the other side. - # decltype would unambiguously identify a type, anything else is - # probably a parenthesized expression and not a type. - if match.group(2) == ')': - return IsDecltype( - clean_lines, linenum, len(match.group(1)) + len(match.group(2)) - 1) - - # Check for casts and cv-qualifiers. - # match.group(1) remainder - # -------------- --------- - # const_cast< type&& - # const type&& - # type const&& - if Search(r'\b(?:const_cast\s*<|static_cast\s*<|dynamic_cast\s*<|' - r'reinterpret_cast\s*<|\w+\s)\s*$', - match.group(1)): - return True - - # Look for a preceding symbol that might help differentiate the context. - # These are the cases that would be ambiguous: - # match.group(1) remainder - # -------------- --------- - # Call ( expression && - # Declaration ( type&& - # sizeof ( type&& - # if ( expression && - # while ( expression && - # for ( type&& - # for( ; expression && - # statement ; type&& - # block { type&& - # constructor { expression && - start = linenum - line = match.group(1) - match_symbol = None - while start >= 0: - # We want to skip over identifiers and commas to get to a symbol. - # Commas are skipped so that we can find the opening parenthesis - # for function parameter lists. - match_symbol = Match(r'^(.*)([^\w\s,])[\w\s,]*$', line) - if match_symbol: - break - start -= 1 - line = clean_lines.elided[start] - - if not match_symbol: - # Probably the first statement in the file is an rvalue reference - return True - - if match_symbol.group(2) == '}': - # Found closing brace, probably an indicate of this: - # block{} type&& - return True - - if match_symbol.group(2) == ';': - # Found semicolon, probably one of these: - # for(; expression && - # statement; type&& - - # Look for the previous 'for(' in the previous lines. - before_text = match_symbol.group(1) - for i in xrange(start - 1, max(start - 6, 0), -1): - before_text = clean_lines.elided[i] + before_text - if Search(r'for\s*\([^{};]*$', before_text): - # This is the condition inside a for-loop - return False - - # Did not find a for-init-statement before this semicolon, so this - # is probably a new statement and not a condition. - return True - - if match_symbol.group(2) == '{': - # Found opening brace, probably one of these: - # block{ type&& = ... ; } - # constructor{ expression && expression } - - # Look for a closing brace or a semicolon. If we see a semicolon - # first, this is probably a rvalue reference. - line = clean_lines.elided[start][0:len(match_symbol.group(1)) + 1] - end = start - depth = 1 - while True: - for ch in line: - if ch == ';': - return True - elif ch == '{': - depth += 1 - elif ch == '}': - depth -= 1 - if depth == 0: - return False - end += 1 - if end >= clean_lines.NumLines(): - break - line = clean_lines.elided[end] - # Incomplete program? - return False - - if match_symbol.group(2) == '(': - # Opening parenthesis. Need to check what's to the left of the - # parenthesis. Look back one extra line for additional context. - before_text = match_symbol.group(1) - if linenum > 1: - before_text = clean_lines.elided[linenum - 1] + before_text - before_text = match_symbol.group(1) - - # Patterns that are likely to be types: - # [](type&& - # for (type&& - # sizeof(type&& - # operator=(type&& - # - if Search(r'(?:\]|\bfor|\bsizeof|\boperator\s*\S+\s*)\s*$', before_text): - return True - - # Patterns that are likely to be expressions: - # if (expression && - # while (expression && - # : initializer(expression && - # , initializer(expression && - # ( FunctionCall(expression && - # + FunctionCall(expression && - # + (expression && - # - # The last '+' represents operators such as '+' and '-'. - if Search(r'(?:\bif|\bwhile|[-+=%^(]*>)?\s*$', - match_symbol.group(1)) - if match_func: - # Check for constructors, which don't have return types. - if Search(r'\b(?:explicit|inline)$', match_func.group(1)): - return True - implicit_constructor = Match(r'\s*(\w+)\((?:const\s+)?(\w+)', prefix) - if (implicit_constructor and - implicit_constructor.group(1) == implicit_constructor.group(2)): - return True - return IsRValueType(typenames, clean_lines, nesting_state, linenum, - len(match_func.group(1))) - - # Nothing before the function name. If this is inside a block scope, - # this is probably a function call. - return not (nesting_state.previous_stack_top and - nesting_state.previous_stack_top.IsBlockInfo()) - - if match_symbol.group(2) == '>': - # Possibly a closing bracket, check that what's on the other side - # looks like the start of a template. - return IsTemplateParameterList( - clean_lines, start, len(match_symbol.group(1))) - - # Some other symbol, usually something like "a=b&&c". This is most - # likely not a type. - return False - - -def IsDeletedOrDefault(clean_lines, linenum): - """Check if current constructor or operator is deleted or default. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - Returns: - True if this is a deleted or default constructor. - """ - open_paren = clean_lines.elided[linenum].find('(') - if open_paren < 0: - return False - (close_line, _, close_paren) = CloseExpression( - clean_lines, linenum, open_paren) - if close_paren < 0: - return False - return Match(r'\s*=\s*(?:delete|default)\b', close_line[close_paren:]) - - -def IsRValueAllowed(clean_lines, linenum, typenames): - """Check if RValue reference is allowed on a particular line. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - typenames: set of type names from template-argument-list. - Returns: - True if line is within the region where RValue references are allowed. - """ - # Allow region marked by PUSH/POP macros - for i in xrange(linenum, 0, -1): - line = clean_lines.elided[i] - if Match(r'GOOGLE_ALLOW_RVALUE_REFERENCES_(?:PUSH|POP)', line): - if not line.endswith('PUSH'): - return False - for j in xrange(linenum, clean_lines.NumLines(), 1): - line = clean_lines.elided[j] - if Match(r'GOOGLE_ALLOW_RVALUE_REFERENCES_(?:PUSH|POP)', line): - return line.endswith('POP') - - # Allow operator= - line = clean_lines.elided[linenum] - if Search(r'\boperator\s*=\s*\(', line): - return IsDeletedOrDefault(clean_lines, linenum) - - # Allow constructors - match = Match(r'\s*(?:[\w<>]+::)*([\w<>]+)\s*::\s*([\w<>]+)\s*\(', line) - if match and match.group(1) == match.group(2): - return IsDeletedOrDefault(clean_lines, linenum) - if Search(r'\b(?:explicit|inline)\s+[\w<>]+\s*\(', line): - return IsDeletedOrDefault(clean_lines, linenum) - - if Match(r'\s*[\w<>]+\s*\(', line): - previous_line = 'ReturnType' - if linenum > 0: - previous_line = clean_lines.elided[linenum - 1] - if Match(r'^\s*$', previous_line) or Search(r'[{}:;]\s*$', previous_line): - return IsDeletedOrDefault(clean_lines, linenum) - - # Reject types not mentioned in template-argument-list - while line: - match = Match(r'^.*?(\w+)\s*&&(.*)$', line) - if not match: - break - if match.group(1) not in typenames: - return False - line = match.group(2) - - # All RValue types that were in template-argument-list should have - # been removed by now. Those were allowed, assuming that they will - # be forwarded. - # - # If there are no remaining RValue types left (i.e. types that were - # not found in template-argument-list), flag those as not allowed. - return line.find('&&') < 0 - - -def GetTemplateArgs(clean_lines, linenum): - """Find list of template arguments associated with this function declaration. - - Args: - clean_lines: A CleansedLines instance containing the file. - linenum: Line number containing the start of the function declaration, - usually one line after the end of the template-argument-list. - Returns: - Set of type names, or empty set if this does not appear to have - any template parameters. - """ - # Find start of function - func_line = linenum - while func_line > 0: - line = clean_lines.elided[func_line] - if Match(r'^\s*$', line): - return set() - if line.find('(') >= 0: - break - func_line -= 1 - if func_line == 0: - return set() - - # Collapse template-argument-list into a single string - argument_list = '' - match = Match(r'^(\s*template\s*)<', clean_lines.elided[func_line]) - if match: - # template-argument-list on the same line as function name - start_col = len(match.group(1)) - _, end_line, end_col = CloseExpression(clean_lines, func_line, start_col) - if end_col > -1 and end_line == func_line: - start_col += 1 # Skip the opening bracket - argument_list = clean_lines.elided[func_line][start_col:end_col] - - elif func_line > 1: - # template-argument-list one line before function name - match = Match(r'^(.*)>\s*$', clean_lines.elided[func_line - 1]) - if match: - end_col = len(match.group(1)) - _, start_line, start_col = ReverseCloseExpression( - clean_lines, func_line - 1, end_col) - if start_col > -1: - start_col += 1 # Skip the opening bracket - while start_line < func_line - 1: - argument_list += clean_lines.elided[start_line][start_col:] - start_col = 0 - start_line += 1 - argument_list += clean_lines.elided[func_line - 1][start_col:end_col] - - if not argument_list: - return set() - - # Extract type names - typenames = set() - while True: - match = Match(r'^[,\s]*(?:typename|class)(?:\.\.\.)?\s+(\w+)(.*)$', - argument_list) - if not match: - break - typenames.add(match.group(1)) - argument_list = match.group(2) - return typenames - - -def CheckRValueReference(filename, clean_lines, linenum, nesting_state, error): - """Check for rvalue references. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - nesting_state: A NestingState instance which maintains information about - the current stack of nested blocks being parsed. - error: The function to call with any errors found. - """ - # Find lines missing spaces around &&. - # TODO(unknown): currently we don't check for rvalue references - # with spaces surrounding the && to avoid false positives with - # boolean expressions. - line = clean_lines.elided[linenum] - match = Match(r'^(.*\S)&&', line) - if not match: - match = Match(r'(.*)&&\S', line) - if (not match) or '(&&)' in line or Search(r'\boperator\s*$', match.group(1)): - return - - # Either poorly formed && or an rvalue reference, check the context - # to get a more accurate error message. Mostly we want to determine - # if what's to the left of "&&" is a type or not. - typenames = GetTemplateArgs(clean_lines, linenum) - and_pos = len(match.group(1)) - if IsRValueType(typenames, clean_lines, nesting_state, linenum, and_pos): - if not IsRValueAllowed(clean_lines, linenum, typenames): - error(filename, linenum, 'build/c++11', 3, - 'RValue references are an unapproved C++ feature.') - else: - error(filename, linenum, 'whitespace/operators', 3, - 'Missing spaces around &&') - - def CheckSectionSpacing(filename, clean_lines, class_info, linenum, error): """Checks for additional blank line issues related to sections. @@ -3906,10 +3892,13 @@ def CheckBraces(filename, clean_lines, linenum, error): # used for brace initializers inside function calls. We don't detect this # perfectly: we just don't complain if the last non-whitespace character on # the previous non-blank line is ',', ';', ':', '(', '{', or '}', or if the - # previous line starts a preprocessor block. + # previous line starts a preprocessor block. We also allow a brace on the + # following line if it is part of an array initialization and would not fit + # within the 80 character limit of the preceding line. prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0] if (not Search(r'[,;:}{(]\s*$', prevline) and - not Match(r'\s*#', prevline)): + not Match(r'\s*#', prevline) and + not (GetLineWidth(prevline) > _line_length - 2 and '[]' in prevline)): error(filename, linenum, 'whitespace/braces', 4, '{ should almost always be at the end of the previous line') @@ -4085,13 +4074,14 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error): # In addition to macros, we also don't want to warn on # - Compound literals # - Lambdas - # - alignas specifier with anonymous structs: + # - alignas specifier with anonymous structs + # - decltype closing_brace_pos = match.group(1).rfind(')') opening_parenthesis = ReverseCloseExpression( clean_lines, linenum, closing_brace_pos) if opening_parenthesis[2] > -1: line_prefix = opening_parenthesis[0][0:opening_parenthesis[2]] - macro = Search(r'\b([A-Z_]+)\s*$', line_prefix) + macro = Search(r'\b([A-Z_][A-Z0-9_]*)\s*$', line_prefix) func = Match(r'^(.*\])\s*$', line_prefix) if ((macro and macro.group(1) not in ( @@ -4100,6 +4090,7 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error): 'LOCKS_EXCLUDED', 'INTERFACE_DEF')) or (func and not Search(r'\boperator\s*\[\s*\]', func.group(1))) or Search(r'\b(?:struct|union)\s+alignas\s*$', line_prefix) or + Search(r'\bdecltype$', line_prefix) or Search(r'\s+=\s*$', line_prefix)): match = None if (match and @@ -4136,6 +4127,14 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error): # outputting warnings for the matching closing brace, if there are # nested blocks with trailing semicolons, we will get the error # messages in reversed order. + + # We need to check the line forward for NOLINT + raw_lines = clean_lines.raw_lines + ParseNolintSuppressions(filename, raw_lines[endlinenum-1], endlinenum-1, + error) + ParseNolintSuppressions(filename, raw_lines[endlinenum], endlinenum, + error) + error(filename, endlinenum, 'readability/braces', 4, "You don't need a ; after a }") @@ -4159,7 +4158,7 @@ def CheckEmptyBlockBody(filename, clean_lines, linenum, error): line = clean_lines.elided[linenum] matched = Match(r'\s*(for|while|if)\s*\(', line) if matched: - # Find the end of the conditional expression + # Find the end of the conditional expression. (end_line, end_linenum, end_pos) = CloseExpression( clean_lines, linenum, line.find('(')) @@ -4174,6 +4173,75 @@ def CheckEmptyBlockBody(filename, clean_lines, linenum, error): error(filename, end_linenum, 'whitespace/empty_loop_body', 5, 'Empty loop bodies should use {} or continue') + # Check for if statements that have completely empty bodies (no comments) + # and no else clauses. + if end_pos >= 0 and matched.group(1) == 'if': + # Find the position of the opening { for the if statement. + # Return without logging an error if it has no brackets. + opening_linenum = end_linenum + opening_line_fragment = end_line[end_pos:] + # Loop until EOF or find anything that's not whitespace or opening {. + while not Search(r'^\s*\{', opening_line_fragment): + if Search(r'^(?!\s*$)', opening_line_fragment): + # Conditional has no brackets. + return + opening_linenum += 1 + if opening_linenum == len(clean_lines.elided): + # Couldn't find conditional's opening { or any code before EOF. + return + opening_line_fragment = clean_lines.elided[opening_linenum] + # Set opening_line (opening_line_fragment may not be entire opening line). + opening_line = clean_lines.elided[opening_linenum] + + # Find the position of the closing }. + opening_pos = opening_line_fragment.find('{') + if opening_linenum == end_linenum: + # We need to make opening_pos relative to the start of the entire line. + opening_pos += end_pos + (closing_line, closing_linenum, closing_pos) = CloseExpression( + clean_lines, opening_linenum, opening_pos) + if closing_pos < 0: + return + + # Now construct the body of the conditional. This consists of the portion + # of the opening line after the {, all lines until the closing line, + # and the portion of the closing line before the }. + if (clean_lines.raw_lines[opening_linenum] != + CleanseComments(clean_lines.raw_lines[opening_linenum])): + # Opening line ends with a comment, so conditional isn't empty. + return + if closing_linenum > opening_linenum: + # Opening line after the {. Ignore comments here since we checked above. + bodylist = list(opening_line[opening_pos+1:]) + # All lines until closing line, excluding closing line, with comments. + bodylist.extend(clean_lines.raw_lines[opening_linenum+1:closing_linenum]) + # Closing line before the }. Won't (and can't) have comments. + bodylist.append(clean_lines.elided[closing_linenum][:closing_pos-1]) + body = '\n'.join(bodylist) + else: + # If statement has brackets and fits on a single line. + body = opening_line[opening_pos+1:closing_pos-1] + + # Check if the body is empty + if not _EMPTY_CONDITIONAL_BODY_PATTERN.search(body): + return + # The body is empty. Now make sure there's not an else clause. + current_linenum = closing_linenum + current_line_fragment = closing_line[closing_pos:] + # Loop until EOF or find anything that's not whitespace or else clause. + while Search(r'^\s*$|^(?=\s*else)', current_line_fragment): + if Search(r'^(?=\s*else)', current_line_fragment): + # Found an else clause, so don't log an error. + return + current_linenum += 1 + if current_linenum == len(clean_lines.elided): + break + current_line_fragment = clean_lines.elided[current_linenum] + + # The body is empty and there's no else clause until EOF or other code. + error(filename, end_linenum, 'whitespace/empty_if_body', 4, + ('If statement had no body and no else clause')) + def FindCheckMacro(line): """Find a replaceable CHECK-like macro. @@ -4393,6 +4461,7 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, # raw strings, raw_lines = clean_lines.lines_without_raw_strings line = raw_lines[linenum] + prev = raw_lines[linenum - 1] if linenum > 0 else '' if line.find('\t') != -1: error(filename, linenum, 'whitespace/tab', 1, @@ -4416,22 +4485,27 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, cleansed_line = clean_lines.elided[linenum] while initial_spaces < len(line) and line[initial_spaces] == ' ': initial_spaces += 1 - if line and line[-1].isspace(): - error(filename, linenum, 'whitespace/end_of_line', 4, - 'Line ends in whitespace. Consider deleting these extra spaces.') # There are certain situations we allow one space, notably for # section labels, and also lines containing multi-line raw strings. - elif ((initial_spaces == 1 or initial_spaces == 3) and - not Match(scope_or_label_pattern, cleansed_line) and - not (clean_lines.raw_lines[linenum] != line and - Match(r'^\s*""', line))): + # We also don't check for lines that look like continuation lines + # (of lines ending in double quotes, commas, equals, or angle brackets) + # because the rules for how to indent those are non-trivial. + if (not Search(r'[",=><] *$', prev) and + (initial_spaces == 1 or initial_spaces == 3) and + not Match(scope_or_label_pattern, cleansed_line) and + not (clean_lines.raw_lines[linenum] != line and + Match(r'^\s*""', line))): error(filename, linenum, 'whitespace/indent', 3, 'Weird number of spaces at line-start. ' 'Are you using a 2-space indent?') + if line and line[-1].isspace(): + error(filename, linenum, 'whitespace/end_of_line', 4, + 'Line ends in whitespace. Consider deleting these extra spaces.') + # Check if the line is a header guard. is_header_guard = False - if file_extension == 'h': + if file_extension in GetHeaderExtensions(): cppvar = GetHeaderGuardCPPVariable(filename) if (line.startswith('#ifndef %s' % cppvar) or line.startswith('#define %s' % cppvar) or @@ -4445,20 +4519,23 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, # # The "$Id:...$" comment may also get very long without it being the # developers fault. + # + # Doxygen documentation copying can get pretty long when using an overloaded + # function declaration if (not line.startswith('#include') and not is_header_guard and not Match(r'^\s*//.*http(s?)://\S*$', line) and - not Match(r'^// \$Id:.*#[0-9]+ \$$', line)): + not Match(r'^\s*//\s*[^\s]*$', line) and + not Match(r'^// \$Id:.*#[0-9]+ \$$', line) and + not Match(r'^\s*/// [@\\](copydoc|copydetails|copybrief) .*$', line)): line_width = GetLineWidth(line) - extended_length = int((_line_length * 1.25)) - if line_width > extended_length: - error(filename, linenum, 'whitespace/line_length', 4, - 'Lines should very rarely be longer than %i characters' % - extended_length) - elif line_width > _line_length: + if line_width > _line_length: error(filename, linenum, 'whitespace/line_length', 2, 'Lines should be <= %i characters long' % _line_length) if (cleansed_line.count(';') > 1 and + # allow simple single line lambdas + not Match(r'^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}\n\r]*\}', + line) and # for loops are allowed two ;'s (and may run over two lines). cleansed_line.find('for') == -1 and (GetPreviousNonBlankLine(clean_lines, linenum)[0].find('for') == -1 or @@ -4479,9 +4556,8 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, CheckOperatorSpacing(filename, clean_lines, linenum, error) CheckParenthesisSpacing(filename, clean_lines, linenum, error) CheckCommaSpacing(filename, clean_lines, linenum, error) - CheckBracesSpacing(filename, clean_lines, linenum, error) + CheckBracesSpacing(filename, clean_lines, linenum, nesting_state, error) CheckSpacingForFunctionCall(filename, clean_lines, linenum, error) - CheckRValueReference(filename, clean_lines, linenum, nesting_state, error) CheckCheck(filename, clean_lines, linenum, error) CheckAltTokens(filename, clean_lines, linenum, error) classinfo = nesting_state.InnermostClass() @@ -4517,31 +4593,17 @@ def _DropCommonSuffixes(filename): Returns: The filename with the common suffix removed. """ - for suffix in ('test.cc', 'regtest.cc', 'unittest.cc', - 'inl.h', 'impl.h', 'internal.h'): + for suffix in itertools.chain( + ('%s.%s' % (test_suffix.lstrip('_'), ext) + for test_suffix, ext in itertools.product(_test_suffixes, GetNonHeaderExtensions())), + ('%s.%s' % (suffix, ext) + for suffix, ext in itertools.product(['inl', 'imp', 'internal'], GetHeaderExtensions()))): if (filename.endswith(suffix) and len(filename) > len(suffix) and filename[-len(suffix) - 1] in ('-', '_')): return filename[:-len(suffix) - 1] return os.path.splitext(filename)[0] -def _IsTestFilename(filename): - """Determines if the given filename has a suffix that identifies it as a test. - - Args: - filename: The input filename. - - Returns: - True if 'filename' looks like a test, False otherwise. - """ - if (filename.endswith('_test.cc') or - filename.endswith('_unittest.cc') or - filename.endswith('_regtest.cc')): - return True - else: - return False - - def _ClassifyInclude(fileinfo, include, is_system): """Figures out what kind of header 'include' is. @@ -4570,6 +4632,10 @@ def _ClassifyInclude(fileinfo, include, is_system): # those already checked for above. is_cpp_h = include in _CPP_HEADERS + # Headers with C++ extensions shouldn't be considered C system headers + if is_system and os.path.splitext(include)[1] in ['.hpp', '.hxx', '.h++']: + is_system = False + if is_system: if is_cpp_h: return _CPP_SYS_HEADER @@ -4582,9 +4648,11 @@ def _ClassifyInclude(fileinfo, include, is_system): target_dir, target_base = ( os.path.split(_DropCommonSuffixes(fileinfo.RepositoryName()))) include_dir, include_base = os.path.split(_DropCommonSuffixes(include)) + target_dir_pub = os.path.normpath(target_dir + '/../public') + target_dir_pub = target_dir_pub.replace('\\', '/') if target_base == include_base and ( include_dir == target_dir or - include_dir == os.path.normpath(target_dir + '/../public')): + include_dir == target_dir_pub): return _LIKELY_MY_HEADER # If the target and include share some initial basename @@ -4628,7 +4696,7 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): # naming convention but not the include convention. match = Match(r'#include\s*"([^/]+\.h)"', line) if match and not _THIRD_PARTY_HEADERS_PATTERN.match(match.group(1)): - error(filename, linenum, 'build/include', 4, + error(filename, linenum, 'build/include_subdir', 4, 'Include the directory when naming .h files') # we shouldn't include a file more than once. actually, there are a @@ -4643,11 +4711,16 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): error(filename, linenum, 'build/include', 4, '"%s" already included at %s:%s' % (include, filename, duplicate_line)) - elif (include.endswith('.cc') and + return + + for extension in GetNonHeaderExtensions(): + if (include.endswith('.' + extension) and os.path.dirname(fileinfo.RepositoryName()) != os.path.dirname(include)): - error(filename, linenum, 'build/include', 4, - 'Do not include .cc files from other packages') - elif not _THIRD_PARTY_HEADERS_PATTERN.match(include): + error(filename, linenum, 'build/include', 4, + 'Do not include .' + extension + ' files from other packages') + return + + if not _THIRD_PARTY_HEADERS_PATTERN.match(include): include_state.include_list[-1].append((include, linenum)) # We want to ensure that headers appear in the right order: @@ -4701,7 +4774,7 @@ def _GetTextInside(text, start_pattern): # Give opening punctuations to get the matching close-punctuations. matching_punctuation = {'(': ')', '{': '}', '[': ']'} - closing_punctuation = set(matching_punctuation.itervalues()) + closing_punctuation = set(itervalues(matching_punctuation)) # Find the position to start extracting text. match = re.search(start_pattern, text, re.M) @@ -4756,6 +4829,9 @@ def _GetTextInside(text, start_pattern): _RE_PATTERN_CONST_REF_PARAM = ( r'(?:.*\s*\bconst\s*&\s*' + _RE_PATTERN_IDENT + r'|const\s+' + _RE_PATTERN_TYPE + r'\s*&\s*' + _RE_PATTERN_IDENT + r')') +# Stream types. +_RE_PATTERN_REF_STREAM_PARAM = ( + r'(?:.*stream\s*&\s*' + _RE_PATTERN_IDENT + r')') def CheckLanguage(filename, clean_lines, linenum, file_extension, @@ -4792,15 +4868,13 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, if match: include_state.ResetSection(match.group(1)) - # Make Windows paths like Unix. - fullname = os.path.abspath(filename).replace('\\', '/') - + # Perform other checks now that we are sure that this is not an include line CheckCasts(filename, clean_lines, linenum, error) CheckGlobalStatic(filename, clean_lines, linenum, error) CheckPrintf(filename, clean_lines, linenum, error) - if file_extension == 'h': + if file_extension in GetHeaderExtensions(): # TODO(unknown): check that 1-arg constructors are explicit. # How to tell it's a constructor? # (handled in CheckForNonStandardConstructs for now) @@ -4861,9 +4935,14 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, % (match.group(1), match.group(2))) if Search(r'\busing namespace\b', line): - error(filename, linenum, 'build/namespaces', 5, - 'Do not use namespace using-directives. ' - 'Use using-declarations instead.') + if Search(r'\bliterals\b', line): + error(filename, linenum, 'build/namespaces_literals', 5, + 'Do not use namespace using-directives. ' + 'Use using-declarations instead.') + else: + error(filename, linenum, 'build/namespaces', 5, + 'Do not use namespace using-directives. ' + 'Use using-declarations instead.') # Detect variable-length arrays. match = Match(r'\s*(.+::)?(\w+) [a-z]\w*\[(.+)];', line) @@ -4907,12 +4986,12 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension, # Check for use of unnamed namespaces in header files. Registration # macros are typically OK, so we allow use of "namespace {" on lines # that end with backslashes. - if (file_extension == 'h' + if (file_extension in GetHeaderExtensions() and Search(r'\bnamespace\s*{', line) and line[-1] != '\\'): error(filename, linenum, 'build/namespaces', 4, 'Do not use unnamed namespaces in header files. See ' - 'http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces' + 'https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces' ' for more information.') @@ -4933,9 +5012,13 @@ def CheckGlobalStatic(filename, clean_lines, linenum, error): # Check for people declaring static/global STL strings at the top level. # This is dangerous because the C++ language does not guarantee that - # globals with constructors are initialized before the first access. + # globals with constructors are initialized before the first access, and + # also because globals can be destroyed when some threads are still running. + # TODO(unknown): Generalize this to also find static unique_ptr instances. + # TODO(unknown): File bugs for clang-tidy to find these. match = Match( - r'((?:|static +)(?:|const +))string +([a-zA-Z0-9_:]+)\b(.*)', + r'((?:|static +)(?:|const +))(?::*std::)?string( +const)? +' + r'([a-zA-Z0-9_:]+)\b(.*)', line) # Remove false positives: @@ -4955,15 +5038,20 @@ def CheckGlobalStatic(filename, clean_lines, linenum, error): # matching identifiers. # string Class::operator*() if (match and - not Search(r'\bstring\b(\s+const)?\s*\*\s*(const\s+)?\w', line) and + not Search(r'\bstring\b(\s+const)?\s*[\*\&]\s*(const\s+)?\w', line) and not Search(r'\boperator\W', line) and - not Match(r'\s*(<.*>)?(::[a-zA-Z0-9_]+)*\s*\(([^"]|$)', match.group(3))): - error(filename, linenum, 'runtime/string', 4, - 'For a static/global string constant, use a C style string instead: ' - '"%schar %s[]".' % - (match.group(1), match.group(2))) + not Match(r'\s*(<.*>)?(::[a-zA-Z0-9_]+)*\s*\(([^"]|$)', match.group(4))): + if Search(r'\bconst\b', line): + error(filename, linenum, 'runtime/string', 4, + 'For a static/global string constant, use a C style string ' + 'instead: "%schar%s %s[]".' % + (match.group(1), match.group(2) or '', match.group(3))) + else: + error(filename, linenum, 'runtime/string', 4, + 'Static/global string variables are not permitted.') - if Search(r'\b([A-Za-z0-9_]*_)\(\1\)', line): + if (Search(r'\b([A-Za-z0-9_]*_)\(\1\)', line) or + Search(r'\b([A-Za-z0-9_]*_)\(CHECK_NOTNULL\(\1\)\)', line)): error(filename, linenum, 'runtime/init', 4, 'You seem to be initializing a member variable with itself.') @@ -5208,7 +5296,8 @@ def CheckForNonConstReference(filename, clean_lines, linenum, decls = ReplaceAll(r'{[^}]*}', ' ', line) # exclude function body for parameter in re.findall(_RE_PATTERN_REF_PARAM, decls): - if not Match(_RE_PATTERN_CONST_REF_PARAM, parameter): + if (not Match(_RE_PATTERN_CONST_REF_PARAM, parameter) and + not Match(_RE_PATTERN_REF_STREAM_PARAM, parameter)): error(filename, linenum, 'runtime/references', 2, 'Is this a non-const reference? ' 'If so, make const or use a pointer: ' + @@ -5231,7 +5320,7 @@ def CheckCasts(filename, clean_lines, linenum, error): # Parameterless conversion functions, such as bool(), are allowed as they are # probably a member operator declaration or default constructor. match = Search( - r'(\bnew\s+|\S<\s*(?:const\s+)?)?\b' + r'(\bnew\s+(?:const\s+)?|\S<\s*(?:const\s+)?)?\b' r'(int|float|double|bool|char|int32|uint32|int64|uint64)' r'(\([^)].*)', line) expecting_function = ExpectingFunctionArgs(clean_lines, linenum) @@ -5372,63 +5461,12 @@ def CheckCStyleCast(filename, clean_lines, linenum, cast_type, pattern, error): if context.endswith(' operator++') or context.endswith(' operator--'): return False - # A single unnamed argument for a function tends to look like old - # style cast. If we see those, don't issue warnings for deprecated - # casts, instead issue warnings for unnamed arguments where - # appropriate. - # - # These are things that we want warnings for, since the style guide - # explicitly require all parameters to be named: - # Function(int); - # Function(int) { - # ConstMember(int) const; - # ConstMember(int) const { - # ExceptionMember(int) throw (...); - # ExceptionMember(int) throw (...) { - # PureVirtual(int) = 0; - # [](int) -> bool { - # - # These are functions of some sort, where the compiler would be fine - # if they had named parameters, but people often omit those - # identifiers to reduce clutter: - # (FunctionPointer)(int); - # (FunctionPointer)(int) = value; - # Function((function_pointer_arg)(int)) - # Function((function_pointer_arg)(int), int param) - # ; - # <(FunctionPointerTemplateArgument)(int)>; + # A single unnamed argument for a function tends to look like old style cast. + # If we see those, don't issue warnings for deprecated casts. remainder = line[match.end(0):] if Match(r'^\s*(?:;|const\b|throw\b|final\b|override\b|[=>{),]|->)', remainder): - # Looks like an unnamed parameter. - - # Don't warn on any kind of template arguments. - if Match(r'^\s*>', remainder): - return False - - # Don't warn on assignments to function pointers, but keep warnings for - # unnamed parameters to pure virtual functions. Note that this pattern - # will also pass on assignments of "0" to function pointers, but the - # preferred values for those would be "nullptr" or "NULL". - matched_zero = Match(r'^\s=\s*(\S+)\s*;', remainder) - if matched_zero and matched_zero.group(1) != '0': - return False - - # Don't warn on function pointer declarations. For this we need - # to check what came before the "(type)" string. - if Match(r'.*\)\s*$', line[0:match.start(0)]): - return False - - # Don't warn if the parameter is named with block comments, e.g.: - # Function(int /*unused_param*/); - raw_line = clean_lines.raw_lines[linenum] - if '/*' in raw_line: - return False - - # Passed all filters, issue warning here. - error(filename, linenum, 'readability/function', 3, - 'All parameters should be named in a function') - return True + return False # At this point, all that should be left is actual casts. error(filename, linenum, 'readability/casting', 4, @@ -5482,12 +5520,15 @@ def ExpectingFunctionArgs(clean_lines, linenum): ('', ('numeric_limits',)), ('', ('list',)), ('', ('map', 'multimap',)), - ('', ('allocator',)), + ('', ('allocator', 'make_shared', 'make_unique', 'shared_ptr', + 'unique_ptr', 'weak_ptr')), ('', ('queue', 'priority_queue',)), ('', ('set', 'multiset',)), ('', ('stack',)), ('', ('char_traits', 'basic_string',)), ('', ('tuple',)), + ('', ('unordered_map', 'unordered_multimap')), + ('', ('unordered_set', 'unordered_multiset')), ('', ('pair',)), ('', ('vector',)), @@ -5498,18 +5539,26 @@ def ExpectingFunctionArgs(clean_lines, linenum): ('', ('slist',)), ) -_RE_PATTERN_STRING = re.compile(r'\bstring\b') +_HEADERS_MAYBE_TEMPLATES = ( + ('', ('copy', 'max', 'min', 'min_element', 'sort', + 'transform', + )), + ('', ('forward', 'make_pair', 'move', 'swap')), + ) -_re_pattern_algorithm_header = [] -for _template in ('copy', 'max', 'min', 'min_element', 'sort', 'swap', - 'transform'): - # Match max(..., ...), max(..., ...), but not foo->max, foo.max or - # type::max(). - _re_pattern_algorithm_header.append( - (re.compile(r'[^>.]\b' + _template + r'(<.*?>)?\([^\)]'), - _template, - '')) +_RE_PATTERN_STRING = re.compile(r'\bstring\b') +_re_pattern_headers_maybe_templates = [] +for _header, _templates in _HEADERS_MAYBE_TEMPLATES: + for _template in _templates: + # Match max(..., ...), max(..., ...), but not foo->max, foo.max or + # type::max(). + _re_pattern_headers_maybe_templates.append( + (re.compile(r'[^>.]\b' + _template + r'(<.*?>)?\([^\)]'), + _template, + _header)) + +# Other scripts may reach in and modify this pattern. _re_pattern_templates = [] for _header, _templates in _HEADERS_CONTAINING_TEMPLATES: for _template in _templates: @@ -5540,7 +5589,7 @@ def FilesBelongToSameModule(filename_cc, filename_h): some false positives. This should be sufficiently rare in practice. Args: - filename_cc: is the path for the .cc file + filename_cc: is the path for the source (e.g. .cc) file filename_h: is the path for the header path Returns: @@ -5548,20 +5597,23 @@ def FilesBelongToSameModule(filename_cc, filename_h): bool: True if filename_cc and filename_h belong to the same module. string: the additional prefix needed to open the header file. """ + fileinfo_cc = FileInfo(filename_cc) + if not fileinfo_cc.Extension().lstrip('.') in GetNonHeaderExtensions(): + return (False, '') - if not filename_cc.endswith('.cc'): + fileinfo_h = FileInfo(filename_h) + if not fileinfo_h.Extension().lstrip('.') in GetHeaderExtensions(): return (False, '') - filename_cc = filename_cc[:-len('.cc')] - if filename_cc.endswith('_unittest'): - filename_cc = filename_cc[:-len('_unittest')] - elif filename_cc.endswith('_test'): - filename_cc = filename_cc[:-len('_test')] + + filename_cc = filename_cc[:-(len(fileinfo_cc.Extension()))] + matched_test_suffix = Search(_TEST_FILE_SUFFIX, fileinfo_cc.BaseName()) + if matched_test_suffix: + filename_cc = filename_cc[:-len(matched_test_suffix.group(1))] + filename_cc = filename_cc.replace('/public/', '/') filename_cc = filename_cc.replace('/internal/', '/') - if not filename_h.endswith('.h'): - return (False, '') - filename_h = filename_h[:-len('.h')] + filename_h = filename_h[:-(len(fileinfo_h.Extension()))] if filename_h.endswith('-inl'): filename_h = filename_h[:-len('-inl')] filename_h = filename_h.replace('/public/', '/') @@ -5622,7 +5674,7 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, required = {} # A map of header name to linenumber and the template entity. # Example of required: { '': (1219, 'less<>') } - for linenum in xrange(clean_lines.NumLines()): + for linenum in range(clean_lines.NumLines()): line = clean_lines.elided[linenum] if not line or line[0] == '#': continue @@ -5636,7 +5688,7 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, if prefix.endswith('std::') or not prefix.endswith('::'): required[''] = (linenum, 'string') - for pattern, template, header in _re_pattern_algorithm_header: + for pattern, template, header in _re_pattern_headers_maybe_templates: if pattern.search(line): required[header] = (linenum, template) @@ -5645,8 +5697,13 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, continue for pattern, template, header in _re_pattern_templates: - if pattern.search(line): - required[header] = (linenum, template) + matched = pattern.search(line) + if matched: + # Don't warn about IWYU in non-STL namespaces: + # (We check only the first match per line; good enough.) + prefix = line[:matched.start()] + if prefix.endswith('std::') or not prefix.endswith('::'): + required[header] = (linenum, template) # The policy is that if you #include something in foo.h you don't need to # include it again in foo.cc. Here, we will look at possible includes. @@ -5671,7 +5728,7 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # include_dict is modified during iteration, so we iterate over a copy of # the keys. - header_keys = include_dict.keys() + header_keys = list(include_dict.keys()) for header in header_keys: (same_module, common_path) = FilesBelongToSameModule(abs_filename, header) fullpath = common_path + header @@ -5683,11 +5740,13 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # didn't include it in the .h file. # TODO(unknown): Do a better job of finding .h files so we are confident that # not having the .h file means there isn't one. - if filename.endswith('.cc') and not header_found: - return + if not header_found: + for extension in GetNonHeaderExtensions(): + if filename.endswith('.' + extension): + return # All the lines have been processed, report the errors found. - for required_header_unstripped in required: + for required_header_unstripped in sorted(required, key=required.__getitem__): template = required[required_header_unstripped][1] if required_header_unstripped.strip('<>"') not in include_dict: error(filename, required[required_header_unstripped][0], @@ -5719,31 +5778,6 @@ def CheckMakePairUsesDeduction(filename, clean_lines, linenum, error): ' OR use pair directly OR if appropriate, construct a pair directly') -def CheckDefaultLambdaCaptures(filename, clean_lines, linenum, error): - """Check that default lambda captures are not used. - - Args: - filename: The name of the current file. - clean_lines: A CleansedLines instance containing the file. - linenum: The number of the line to check. - error: The function to call with any errors found. - """ - line = clean_lines.elided[linenum] - - # A lambda introducer specifies a default capture if it starts with "[=" - # or if it starts with "[&" _not_ followed by an identifier. - match = Match(r'^(.*)\[\s*(?:=|&[^\w])', line) - if match: - # Found a potential error, check what comes after the lambda-introducer. - # If it's not open parenthesis (for lambda-declarator) or open brace - # (for compound-statement), it's not a lambda. - line, _, pos = CloseExpression(clean_lines, linenum, len(match.group(1))) - if pos >= 0 and Match(r'^\s*[{(]', line[pos:]): - error(filename, linenum, 'build/c++11', - 4, # 4 = high confidence - 'Default lambda captures are an unapproved C++ feature.') - - def CheckRedundantVirtual(filename, clean_lines, linenum, error): """Check if line contains a redundant "virtual" function-specifier. @@ -5851,11 +5885,9 @@ def IsBlockInNameSpace(nesting_state, is_forward_declaration): Whether or not the new block is directly in a namespace. """ if is_forward_declaration: - if len(nesting_state.stack) >= 1 and ( - isinstance(nesting_state.stack[-1], _NamespaceInfo)): - return True - else: - return False + return len(nesting_state.stack) >= 1 and ( + isinstance(nesting_state.stack[-1], _NamespaceInfo)) + return (len(nesting_state.stack) > 1 and nesting_state.stack[-1].check_namespace_indentation and @@ -5905,7 +5937,7 @@ def CheckItemIndentationInNamespace(filename, raw_lines_no_comments, linenum, def ProcessLine(filename, file_extension, clean_lines, line, include_state, function_state, nesting_state, error, - extra_check_functions=[]): + extra_check_functions=None): """Processes a single line in the file. Args: @@ -5942,11 +5974,11 @@ def ProcessLine(filename, file_extension, clean_lines, line, CheckPosixThreading(filename, clean_lines, line, error) CheckInvalidIncrement(filename, clean_lines, line, error) CheckMakePairUsesDeduction(filename, clean_lines, line, error) - CheckDefaultLambdaCaptures(filename, clean_lines, line, error) CheckRedundantVirtual(filename, clean_lines, line, error) CheckRedundantOverrideOrFinal(filename, clean_lines, line, error) - for check_fn in extra_check_functions: - check_fn(filename, clean_lines, line, error) + if extra_check_functions: + for check_fn in extra_check_functions: + check_fn(filename, clean_lines, line, error) def FlagCxx11Features(filename, clean_lines, linenum, error): """Flag those c++11 features that we only allow in certain places. @@ -5959,8 +5991,14 @@ def FlagCxx11Features(filename, clean_lines, linenum, error): """ line = clean_lines.elided[linenum] - # Flag unapproved C++11 headers. include = Match(r'\s*#\s*include\s+[<"]([^<"]+)[">]', line) + + # Flag unapproved C++ TR1 headers. + if include and include.group(1).startswith('tr1/'): + error(filename, linenum, 'build/c++tr1', 5, + ('C++ TR1 headers such as <%s> are unapproved.') % include.group(1)) + + # Flag unapproved C++11 headers. if include and include.group(1) in ('cfenv', 'condition_variable', 'fenv.h', @@ -5994,8 +6032,27 @@ def FlagCxx11Features(filename, clean_lines, linenum, error): 'they may let you use it.') % top_name) +def FlagCxx14Features(filename, clean_lines, linenum, error): + """Flag those C++14 features that we restrict. + + Args: + filename: The name of the current file. + clean_lines: A CleansedLines instance containing the file. + linenum: The number of the line to check. + error: The function to call with any errors found. + """ + line = clean_lines.elided[linenum] + + include = Match(r'\s*#\s*include\s+[<"]([^<"]+)[">]', line) + + # Flag unapproved C++14 headers. + if include and include.group(1) in ('scoped_allocator', 'shared_mutex'): + error(filename, linenum, 'build/c++14', 5, + ('<%s> is an unapproved C++14 header.') % include.group(1)) + + def ProcessFileData(filename, file_extension, lines, error, - extra_check_functions=[]): + extra_check_functions=None): """Performs lint checks and reports any errors to the given error function. Args: @@ -6019,14 +6076,14 @@ def ProcessFileData(filename, file_extension, lines, error, ResetNolintSuppressions() CheckForCopyright(filename, lines, error) - + ProcessGlobalSuppresions(lines) RemoveMultiLineComments(filename, lines, error) clean_lines = CleansedLines(lines) - if file_extension == 'h': + if file_extension in GetHeaderExtensions(): CheckForHeaderGuard(filename, clean_lines, error) - for line in xrange(clean_lines.NumLines()): + for line in range(clean_lines.NumLines()): ProcessLine(filename, file_extension, clean_lines, line, include_state, function_state, nesting_state, error, extra_check_functions) @@ -6034,9 +6091,9 @@ def ProcessFileData(filename, file_extension, lines, error, nesting_state.CheckCompletedBlocks(filename, error) CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error) - + # Check that the .cc file has included its header if it exists. - if file_extension == 'cc': + if _IsSourceExtension(file_extension): CheckHeaderFileIncluded(filename, include_state, error) # We check here rather than inside ProcessLine so that we see raw @@ -6092,36 +6149,56 @@ def ProcessConfigOverrides(filename): if base_name: pattern = re.compile(val) if pattern.match(base_name): - sys.stderr.write('Ignoring "%s": file excluded by "%s". ' - 'File path component "%s" matches ' - 'pattern "%s"\n' % - (filename, cfg_file, base_name, val)) + _cpplint_state.PrintInfo('Ignoring "%s": file excluded by ' + '"%s". File path component "%s" matches pattern "%s"\n' % + (filename, cfg_file, base_name, val)) return False elif name == 'linelength': global _line_length try: _line_length = int(val) except ValueError: - sys.stderr.write('Line length must be numeric.') + _cpplint_state.PrintError('Line length must be numeric.') + elif name == 'extensions': + global _valid_extensions + try: + extensions = [ext.strip() for ext in val.split(',')] + _valid_extensions = set(extensions) + except ValueError: + sys.stderr.write('Extensions should be a comma-separated list of values;' + 'for example: extensions=hpp,cpp\n' + 'This could not be parsed: "%s"' % (val,)) + elif name == 'headers': + global _header_extensions + try: + extensions = [ext.strip() for ext in val.split(',')] + _header_extensions = set(extensions) + except ValueError: + sys.stderr.write('Extensions should be a comma-separated list of values;' + 'for example: extensions=hpp,cpp\n' + 'This could not be parsed: "%s"' % (val,)) + elif name == 'root': + global _root + _root = val else: - sys.stderr.write( + _cpplint_state.PrintError( 'Invalid configuration option (%s) in file %s\n' % (name, cfg_file)) except IOError: - sys.stderr.write( + _cpplint_state.PrintError( "Skipping config file '%s': Can't open for reading\n" % cfg_file) keep_looking = False # Apply all the accumulated filters in reverse order (top-level directory # config options having the least priority). - for filter in reversed(cfg_filters): - _AddFilters(filter) + for cfg_filter in reversed(cfg_filters): + _AddFilters(cfg_filter) return True -def ProcessFile(filename, vlevel, extra_check_functions=[]): +def ProcessFile(filename, vlevel, extra_check_functions=None): """Does google-lint on a single file. Args: @@ -6170,7 +6247,7 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): lf_lines.append(linenum + 1) except IOError: - sys.stderr.write( + _cpplint_state.PrintError( "Skipping input '%s': Can't open for reading\n" % filename) _RestoreFilters() return @@ -6180,9 +6257,9 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): # When reading from stdin, the extension is unknown, so no cpplint tests # should rely on the extension. - if filename != '-' and file_extension not in _valid_extensions: - sys.stderr.write('Ignoring %s; not a valid file name ' - '(%s)\n' % (filename, ', '.join(_valid_extensions))) + if filename != '-' and file_extension not in GetAllExtensions(): + _cpplint_state.PrintError('Ignoring %s; not a valid file name ' + '(%s)\n' % (filename, ', '.join(GetAllExtensions()))) else: ProcessFileData(filename, file_extension, lines, Error, extra_check_functions) @@ -6205,7 +6282,7 @@ def ProcessFile(filename, vlevel, extra_check_functions=[]): Error(filename, linenum, 'whitespace/newline', 1, 'Unexpected \\r (^M) found; better to use only \\n') - sys.stderr.write('Done processing %s\n' % filename) + _cpplint_state.PrintInfo('Done processing %s\n' % filename) _RestoreFilters() @@ -6216,10 +6293,11 @@ def PrintUsage(message): message: The optional error message. """ sys.stderr.write(_USAGE) + if message: sys.exit('\nFATAL ERROR: ' + message) else: - sys.exit(1) + sys.exit(0) def PrintCategories(): @@ -6247,8 +6325,13 @@ def ParseArguments(args): 'counting=', 'filter=', 'root=', + 'repository=', 'linelength=', - 'extensions=']) + 'extensions=', + 'exclude=', + 'headers=', + 'quiet', + 'recursive']) except getopt.GetoptError: PrintUsage('Invalid arguments.') @@ -6256,13 +6339,15 @@ def ParseArguments(args): output_format = _OutputFormat() filters = '' counting_style = '' + recursive = False for (opt, val) in opts: if opt == '--help': PrintUsage(None) elif opt == '--output': - if val not in ('emacs', 'vs7', 'eclipse'): - PrintUsage('The only allowed output formats are emacs, vs7 and eclipse.') + if val not in ('emacs', 'vs7', 'eclipse', 'junit'): + PrintUsage('The only allowed output formats are emacs, vs7, eclipse ' + 'and junit.') output_format = val elif opt == '--verbose': verbosity = int(val) @@ -6277,22 +6362,47 @@ def ParseArguments(args): elif opt == '--root': global _root _root = val + elif opt == '--repository': + global _repository + _repository = val elif opt == '--linelength': global _line_length try: - _line_length = int(val) + _line_length = int(val) except ValueError: - PrintUsage('Line length must be digits.') + PrintUsage('Line length must be digits.') + elif opt == '--exclude': + global _excludes + if not _excludes: + _excludes = set() + _excludes.update(glob.glob(val)) elif opt == '--extensions': global _valid_extensions try: - _valid_extensions = set(val.split(',')) + _valid_extensions = set(val.split(',')) except ValueError: PrintUsage('Extensions must be comma seperated list.') + elif opt == '--headers': + global _header_extensions + try: + _header_extensions = set(val.split(',')) + except ValueError: + PrintUsage('Extensions must be comma seperated list.') + elif opt == '--recursive': + recursive = True + elif opt == '--quiet': + global _quiet + _quiet = True if not filenames: PrintUsage('No files were specified.') + if recursive: + filenames = _ExpandDirectories(filenames) + + if _excludes: + filenames = _FilterExcludedFiles(filenames) + _SetOutputFormat(output_format) _SetVerboseLevel(verbosity) _SetFilters(filters) @@ -6300,21 +6410,63 @@ def ParseArguments(args): return filenames +def _ExpandDirectories(filenames): + """Searches a list of filenames and replaces directories in the list with + all files descending from those directories. Files with extensions not in + the valid extensions list are excluded. + + Args: + filenames: A list of files or directories + + Returns: + A list of all files that are members of filenames or descended from a + directory in filenames + """ + expanded = set() + for filename in filenames: + if not os.path.isdir(filename): + expanded.add(filename) + continue + + for root, _, files in os.walk(filename): + for loopfile in files: + fullname = os.path.join(root, loopfile) + if fullname.startswith('.' + os.path.sep): + fullname = fullname[len('.' + os.path.sep):] + expanded.add(fullname) + + filtered = [] + for filename in expanded: + if os.path.splitext(filename)[1][1:] in GetAllExtensions(): + filtered.append(filename) + + return filtered + +def _FilterExcludedFiles(filenames): + """Filters out files listed in the --exclude command line switch. File paths + in the switch are evaluated relative to the current working directory + """ + exclude_paths = [os.path.abspath(f) for f in _excludes] + return [f for f in filenames if os.path.abspath(f) not in exclude_paths] def main(): filenames = ParseArguments(sys.argv[1:]) + backup_err = sys.stderr + try: + # Change stderr to write with replacement characters so we don't die + # if we try to print something containing non-ASCII characters. + sys.stderr = codecs.StreamReader(sys.stderr, 'replace') - # Change stderr to write with replacement characters so we don't die - # if we try to print something containing non-ASCII characters. - sys.stderr = codecs.StreamReaderWriter(sys.stderr, - codecs.getreader('utf8'), - codecs.getwriter('utf8'), - 'replace') + _cpplint_state.ResetErrorCounts() + for filename in filenames: + ProcessFile(filename, _cpplint_state.verbose_level) + _cpplint_state.PrintErrorCounts() - _cpplint_state.ResetErrorCounts() - for filename in filenames: - ProcessFile(filename, _cpplint_state.verbose_level) - _cpplint_state.PrintErrorCounts() + if _cpplint_state.output_format == 'junit': + sys.stderr.write(_cpplint_state.FormatJUnitXML()) + + finally: + sys.stderr = backup_err sys.exit(_cpplint_state.error_count > 0) diff --git a/cpp/src/arrow/adapters/orc/adapter.cc b/cpp/src/arrow/adapters/orc/adapter.cc index 473c90f925124..dd8cc7d9e60a8 100644 --- a/cpp/src/arrow/adapters/orc/adapter.cc +++ b/cpp/src/arrow/adapters/orc/adapter.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "arrow/buffer.h" diff --git a/cpp/src/arrow/array.cc b/cpp/src/arrow/array.cc index 144fbcd05c205..3d72761ed18e5 100644 --- a/cpp/src/arrow/array.cc +++ b/cpp/src/arrow/array.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include "arrow/buffer.h" #include "arrow/compare.h" diff --git a/cpp/src/arrow/array.h b/cpp/src/arrow/array.h index 0ae1ddd8ea221..f0a786131b2b5 100644 --- a/cpp/src/arrow/array.h +++ b/cpp/src/arrow/array.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "arrow/buffer.h" diff --git a/cpp/src/arrow/builder.cc b/cpp/src/arrow/builder.cc index db901526fc2ee..a740299dfe194 100644 --- a/cpp/src/arrow/builder.cc +++ b/cpp/src/arrow/builder.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "arrow/array.h" diff --git a/cpp/src/arrow/compute/context.h b/cpp/src/arrow/compute/context.h index 051c91bf049fa..09838195a52ee 100644 --- a/cpp/src/arrow/compute/context.h +++ b/cpp/src/arrow/compute/context.h @@ -18,6 +18,8 @@ #ifndef ARROW_COMPUTE_CONTEXT_H #define ARROW_COMPUTE_CONTEXT_H +#include + #include "arrow/memory_pool.h" #include "arrow/status.h" #include "arrow/type_fwd.h" diff --git a/cpp/src/arrow/compute/kernels/hash.cc b/cpp/src/arrow/compute/kernels/hash.cc index 1face78bdebfb..8fac7965d0ed9 100644 --- a/cpp/src/arrow/compute/kernels/hash.cc +++ b/cpp/src/arrow/compute/kernels/hash.cc @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "arrow/builder.h" diff --git a/cpp/src/arrow/compute/kernels/util-internal.cc b/cpp/src/arrow/compute/kernels/util-internal.cc index 28428bfcba6c6..0734365859b5a 100644 --- a/cpp/src/arrow/compute/kernels/util-internal.cc +++ b/cpp/src/arrow/compute/kernels/util-internal.cc @@ -17,6 +17,7 @@ #include "arrow/compute/kernels/util-internal.h" +#include #include #include "arrow/array.h" diff --git a/cpp/src/arrow/compute/kernels/util-internal.h b/cpp/src/arrow/compute/kernels/util-internal.h index 7633fed4a8fe7..2f611320a7687 100644 --- a/cpp/src/arrow/compute/kernels/util-internal.h +++ b/cpp/src/arrow/compute/kernels/util-internal.h @@ -18,6 +18,7 @@ #ifndef ARROW_COMPUTE_KERNELS_UTIL_INTERNAL_H #define ARROW_COMPUTE_KERNELS_UTIL_INTERNAL_H +#include #include #include "arrow/compute/kernel.h" diff --git a/cpp/src/arrow/ipc/feather.cc b/cpp/src/arrow/ipc/feather.cc index d3872503edf19..f440c19efe414 100644 --- a/cpp/src/arrow/ipc/feather.cc +++ b/cpp/src/arrow/ipc/feather.cc @@ -22,6 +22,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include "flatbuffers/flatbuffers.h" diff --git a/cpp/src/arrow/ipc/reader.cc b/cpp/src/arrow/ipc/reader.cc index ae0f8f39806b7..cc3b6e55783e3 100644 --- a/cpp/src/arrow/ipc/reader.cc +++ b/cpp/src/arrow/ipc/reader.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include // IWYU pragma: export diff --git a/cpp/src/arrow/pretty_print.cc b/cpp/src/arrow/pretty_print.cc index bd5f8ce10ea68..994f528ea4bad 100644 --- a/cpp/src/arrow/pretty_print.cc +++ b/cpp/src/arrow/pretty_print.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include #include #include #include diff --git a/cpp/src/arrow/python/arrow_to_python.cc b/cpp/src/arrow/python/arrow_to_python.cc index c060ab8bfd6db..c67e5410eb6ee 100644 --- a/cpp/src/arrow/python/arrow_to_python.cc +++ b/cpp/src/arrow/python/arrow_to_python.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include diff --git a/cpp/src/arrow/python/io.cc b/cpp/src/arrow/python/io.cc index cc3892928c455..9d32ead524b93 100644 --- a/cpp/src/arrow/python/io.cc +++ b/cpp/src/arrow/python/io.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include diff --git a/cpp/src/arrow/python/io.h b/cpp/src/arrow/python/io.h index f550de7b2848c..0632d28faf789 100644 --- a/cpp/src/arrow/python/io.h +++ b/cpp/src/arrow/python/io.h @@ -18,6 +18,8 @@ #ifndef PYARROW_IO_H #define PYARROW_IO_H +#include + #include "arrow/io/interfaces.h" #include "arrow/io/memory.h" #include "arrow/util/visibility.h" diff --git a/cpp/src/arrow/python/numpy_to_arrow.cc b/cpp/src/arrow/python/numpy_to_arrow.cc index b5a75aeedd5eb..a1161fe32e100 100644 --- a/cpp/src/arrow/python/numpy_to_arrow.cc +++ b/cpp/src/arrow/python/numpy_to_arrow.cc @@ -29,6 +29,7 @@ #include #include #include +#include #include #include "arrow/array.h" diff --git a/cpp/src/arrow/record_batch.cc b/cpp/src/arrow/record_batch.cc index 60932bdf3e4bb..d418cc4a2e66c 100644 --- a/cpp/src/arrow/record_batch.cc +++ b/cpp/src/arrow/record_batch.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include "arrow/array.h" #include "arrow/status.h" diff --git a/cpp/src/arrow/table.cc b/cpp/src/arrow/table.cc index 14877ccb537c2..8cfd67faef1ee 100644 --- a/cpp/src/arrow/table.cc +++ b/cpp/src/arrow/table.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include "arrow/array.h" #include "arrow/record_batch.h" diff --git a/cpp/src/arrow/table_builder.cc b/cpp/src/arrow/table_builder.cc index 379d886deacba..8e9babcc3997a 100644 --- a/cpp/src/arrow/table_builder.cc +++ b/cpp/src/arrow/table_builder.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include "arrow/array.h" #include "arrow/builder.h" diff --git a/cpp/src/arrow/type.cc b/cpp/src/arrow/type.cc index 31ad53458112c..0a2889f040026 100644 --- a/cpp/src/arrow/type.cc +++ b/cpp/src/arrow/type.cc @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include "arrow/array.h" #include "arrow/compare.h" diff --git a/cpp/src/arrow/type_traits.h b/cpp/src/arrow/type_traits.h index 4bfce9b5f0c53..ede52e9b84bb6 100644 --- a/cpp/src/arrow/type_traits.h +++ b/cpp/src/arrow/type_traits.h @@ -18,6 +18,7 @@ #ifndef ARROW_TYPE_TRAITS_H #define ARROW_TYPE_TRAITS_H +#include #include #include "arrow/type_fwd.h" diff --git a/cpp/src/arrow/util/io-util.h b/cpp/src/arrow/util/io-util.h index 7e2a94ca82320..d1af6c666a156 100644 --- a/cpp/src/arrow/util/io-util.h +++ b/cpp/src/arrow/util/io-util.h @@ -19,6 +19,7 @@ #define ARROW_UTIL_IO_UTIL_H #include +#include #include "arrow/buffer.h" #include "arrow/io/interfaces.h" diff --git a/cpp/src/plasma/events.cc b/cpp/src/plasma/events.cc index 4e4ecfaaaca31..ce29e6c321d5d 100644 --- a/cpp/src/plasma/events.cc +++ b/cpp/src/plasma/events.cc @@ -17,6 +17,8 @@ #include "plasma/events.h" +#include + #include namespace plasma { diff --git a/cpp/src/plasma/plasma.h b/cpp/src/plasma/plasma.h index 2d07c919a18f4..bb9cdae601146 100644 --- a/cpp/src/plasma/plasma.h +++ b/cpp/src/plasma/plasma.h @@ -27,6 +27,7 @@ #include #include // pid_t +#include #include #include #include diff --git a/cpp/src/plasma/protocol.h b/cpp/src/plasma/protocol.h index 44263a6418439..101a3faa7675e 100644 --- a/cpp/src/plasma/protocol.h +++ b/cpp/src/plasma/protocol.h @@ -18,6 +18,8 @@ #ifndef PLASMA_PROTOCOL_H #define PLASMA_PROTOCOL_H +#include +#include #include #include "arrow/status.h" diff --git a/cpp/src/plasma/store.cc b/cpp/src/plasma/store.cc index 80dd525e3e3b4..316a27f63f680 100644 --- a/cpp/src/plasma/store.cc +++ b/cpp/src/plasma/store.cc @@ -44,9 +44,11 @@ #include #include +#include #include #include #include +#include #include #include "plasma/common.h" diff --git a/cpp/src/plasma/store.h b/cpp/src/plasma/store.h index 7eada5a126991..7e716d284f389 100644 --- a/cpp/src/plasma/store.h +++ b/cpp/src/plasma/store.h @@ -19,7 +19,9 @@ #define PLASMA_STORE_H #include +#include #include +#include #include #include "plasma/common.h" From 074eafc686db58178de4439fa63ecf728ec9d4ab Mon Sep 17 00:00:00 2001 From: yosuke shiro Date: Fri, 26 Jan 2018 10:32:08 -0500 Subject: [PATCH 24/46] ARROW-2043: [C++] change description from OS X to macOS Author: yosuke shiro Closes #1521 from shiro615/change-description-from-osx-to-macos and squashes the following commits: ab03f72f [yosuke shiro] [C++] change description from OS X to macOS --- cpp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/README.md b/cpp/README.md index ef2e1fd1b1259..52169974de41e 100644 --- a/cpp/README.md +++ b/cpp/README.md @@ -39,7 +39,7 @@ sudo apt-get install cmake \ libboost-system-dev ``` -On OS X, you can use [Homebrew][1]: +On macOS, you can use [Homebrew][1]: ```shell git clone https://github.com/apache/arrow.git From a71bc838337856b72c2d1b22f8cc9741a103438c Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Fri, 26 Jan 2018 18:13:03 -0500 Subject: [PATCH 25/46] ARROW-2010: [C++] Do not suppress shorten-64-to-32 warnings from clang, fix warnings in ORC adapter These are warnings we want to see to prevent silent truncation of values without an explicit cast. See also bug ARROW-2032 that I encountered while working on this Author: Wes McKinney Closes #1506 from wesm/ARROW-2010 and squashes the following commits: b999a815 [Wes McKinney] Add explicit cast to socklen_t f06e1fd2 [Wes McKinney] Do not suppress shorten-64-to-32 warnings from clang, fix warnings in ORC adapter --- cpp/cmake_modules/SetupCxxFlags.cmake | 2 +- cpp/src/arrow/adapters/orc/adapter.cc | 47 ++++++++++++++------------- cpp/src/plasma/fling.cc | 2 +- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/cpp/cmake_modules/SetupCxxFlags.cmake b/cpp/cmake_modules/SetupCxxFlags.cmake index 97aed6b274976..d901bde47c631 100644 --- a/cpp/cmake_modules/SetupCxxFlags.cmake +++ b/cpp/cmake_modules/SetupCxxFlags.cmake @@ -100,7 +100,7 @@ if ("${UPPERCASE_BUILD_WARNING_LEVEL}" STREQUAL "CHECKIN") -Wno-cast-align -Wno-vla-extension -Wno-shift-sign-overflow \ -Wno-used-but-marked-unused -Wno-missing-variable-declarations \ -Wno-gnu-zero-variadic-macro-arguments -Wconversion -Wno-sign-conversion \ --Wno-disabled-macro-expansion -Wno-shorten-64-to-32") +-Wno-disabled-macro-expansion") # Version numbers where warnings are introduced if ("${COMPILER_VERSION}" VERSION_GREATER "3.3") diff --git a/cpp/src/arrow/adapters/orc/adapter.cc b/cpp/src/arrow/adapters/orc/adapter.cc index dd8cc7d9e60a8..f253808e34ffc 100644 --- a/cpp/src/arrow/adapters/orc/adapter.cc +++ b/cpp/src/arrow/adapters/orc/adapter.cc @@ -106,6 +106,8 @@ Status GetArrowType(const liborc::Type* type, std::shared_ptr* out) { return Status::OK(); } liborc::TypeKind kind = type->getKind(); + const int subtype_count = static_cast(type->getSubtypeCount()); + switch (kind) { case liborc::BOOLEAN: *out = boolean(); @@ -136,7 +138,7 @@ Status GetArrowType(const liborc::Type* type, std::shared_ptr* out) { *out = binary(); break; case liborc::CHAR: - *out = fixed_size_binary(type->getMaximumLength()); + *out = fixed_size_binary(static_cast(type->getMaximumLength())); break; case liborc::TIMESTAMP: *out = timestamp(TimeUnit::NANO); @@ -145,16 +147,18 @@ Status GetArrowType(const liborc::Type* type, std::shared_ptr* out) { *out = date32(); break; case liborc::DECIMAL: { - if (type->getPrecision() == 0) { + const int precision = static_cast(type->getPrecision()); + const int scale = static_cast(type->getScale()); + if (precision == 0) { // In HIVE 0.11/0.12 precision is set as 0, but means max precision *out = decimal(38, 6); } else { - *out = decimal(type->getPrecision(), type->getScale()); + *out = decimal(precision, scale); } break; } case liborc::LIST: { - if (type->getSubtypeCount() != 1) { + if (subtype_count != 1) { return Status::Invalid("Invalid Orc List type"); } std::shared_ptr elemtype; @@ -163,7 +167,7 @@ Status GetArrowType(const liborc::Type* type, std::shared_ptr* out) { break; } case liborc::MAP: { - if (type->getSubtypeCount() != 2) { + if (subtype_count != 2) { return Status::Invalid("Invalid Orc Map type"); } std::shared_ptr keytype; @@ -174,9 +178,8 @@ Status GetArrowType(const liborc::Type* type, std::shared_ptr* out) { break; } case liborc::STRUCT: { - int size = type->getSubtypeCount(); std::vector> fields; - for (int child = 0; child < size; ++child) { + for (int child = 0; child < subtype_count; ++child) { std::shared_ptr elemtype; RETURN_NOT_OK(GetArrowType(type->getSubtype(child), &elemtype)); std::string name = type->getFieldName(child); @@ -186,10 +189,9 @@ Status GetArrowType(const liborc::Type* type, std::shared_ptr* out) { break; } case liborc::UNION: { - int size = type->getSubtypeCount(); std::vector> fields; std::vector type_codes; - for (int child = 0; child < size; ++child) { + for (int child = 0; child < subtype_count; ++child) { std::shared_ptr elemtype; RETURN_NOT_OK(GetArrowType(type->getSubtype(child), &elemtype)); fields.push_back(field("_union_" + std::to_string(child), elemtype)); @@ -260,7 +262,7 @@ class ORCFileReader::Impl { "Only ORC files with a top-level struct " "can be handled"); } - int size = type.getSubtypeCount(); + int size = static_cast(type.getSubtypeCount()); std::vector> fields; for (int child = 0; child < size; ++child) { std::shared_ptr elemtype; @@ -450,7 +452,7 @@ class ORCFileReader::Impl { const liborc::Type* elemtype = type->getSubtype(0); const bool has_nulls = batch->hasNulls; - for (int i = offset; i < length + offset; i++) { + for (int64_t i = offset; i < length + offset; i++) { if (!has_nulls || batch->notNull[i]) { int64_t start = batch->offsets[i]; int64_t end = batch->offsets[i + 1]; @@ -475,7 +477,7 @@ class ORCFileReader::Impl { const liborc::Type* valtype = type->getSubtype(1); const bool has_nulls = batch->hasNulls; - for (int i = offset; i < length + offset; i++) { + for (int64_t i = offset; i < length + offset; i++) { RETURN_NOT_OK(list_builder->Append()); int64_t start = batch->offsets[i]; int64_t list_length = batch->offsets[i + 1] - start; @@ -517,7 +519,7 @@ class ORCFileReader::Impl { if (length == 0) { return Status::OK(); } - int start = builder->length(); + int64_t start = builder->length(); const uint8_t* valid_bytes = nullptr; if (batch->hasNulls) { @@ -541,7 +543,7 @@ class ORCFileReader::Impl { if (length == 0) { return Status::OK(); } - int start = builder->length(); + int64_t start = builder->length(); const uint8_t* valid_bytes = nullptr; if (batch->hasNulls) { @@ -552,7 +554,7 @@ class ORCFileReader::Impl { const int64_t* source = batch->data.data() + offset; uint8_t* target = reinterpret_cast(builder->data()->mutable_data()); - for (int i = 0; i < length; i++) { + for (int64_t i = 0; i < length; i++) { if (source[i]) { BitUtil::SetBit(target, start + i); } else { @@ -570,7 +572,7 @@ class ORCFileReader::Impl { if (length == 0) { return Status::OK(); } - int start = builder->length(); + int64_t start = builder->length(); const uint8_t* valid_bytes = nullptr; if (batch->hasNulls) { @@ -582,7 +584,7 @@ class ORCFileReader::Impl { const int64_t* nanos = batch->nanoseconds.data() + offset; int64_t* target = reinterpret_cast(builder->data()->mutable_data()); - for (int i = 0; i < length; i++) { + for (int64_t i = 0; i < length; i++) { // TODO: boundscheck this, as ORC supports higher resolution timestamps // than arrow for nanosecond resolution target[start + i] = seconds[i] * kOneSecondNanos + nanos[i]; @@ -597,9 +599,10 @@ class ORCFileReader::Impl { auto batch = static_cast(cbatch); const bool has_nulls = batch->hasNulls; - for (int i = offset; i < length + offset; i++) { + for (int64_t i = offset; i < length + offset; i++) { if (!has_nulls || batch->notNull[i]) { - RETURN_NOT_OK(builder->Append(batch->data[i], batch->length[i])); + RETURN_NOT_OK( + builder->Append(batch->data[i], static_cast(batch->length[i]))); } else { RETURN_NOT_OK(builder->AppendNull()); } @@ -613,7 +616,7 @@ class ORCFileReader::Impl { auto batch = static_cast(cbatch); const bool has_nulls = batch->hasNulls; - for (int i = offset; i < length + offset; i++) { + for (int64_t i = offset; i < length + offset; i++) { if (!has_nulls || batch->notNull[i]) { RETURN_NOT_OK(builder->Append(batch->data[i])); } else { @@ -630,7 +633,7 @@ class ORCFileReader::Impl { const bool has_nulls = cbatch->hasNulls; if (type->getPrecision() == 0 || type->getPrecision() > 18) { auto batch = static_cast(cbatch); - for (int i = offset; i < length + offset; i++) { + for (int64_t i = offset; i < length + offset; i++) { if (!has_nulls || batch->notNull[i]) { RETURN_NOT_OK(builder->Append( Decimal128(batch->values[i].getHighBits(), batch->values[i].getLowBits()))); @@ -640,7 +643,7 @@ class ORCFileReader::Impl { } } else { auto batch = static_cast(cbatch); - for (int i = offset; i < length + offset; i++) { + for (int64_t i = offset; i < length + offset; i++) { if (!has_nulls || batch->notNull[i]) { RETURN_NOT_OK(builder->Append(Decimal128(batch->values[i]))); } else { diff --git a/cpp/src/plasma/fling.cc b/cpp/src/plasma/fling.cc index b84648b25a9e7..819ec1623055b 100644 --- a/cpp/src/plasma/fling.cc +++ b/cpp/src/plasma/fling.cc @@ -23,7 +23,7 @@ void init_msg(struct msghdr* msg, struct iovec* iov, char* buf, size_t buf_len) msg->msg_iov = iov; msg->msg_iovlen = 1; msg->msg_control = buf; - msg->msg_controllen = buf_len; + msg->msg_controllen = static_cast(buf_len); msg->msg_name = NULL; msg->msg_namelen = 0; } From 6299a9cfb314dc7a03fdafca41419a6be4300225 Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Fri, 26 Jan 2018 18:59:05 -0500 Subject: [PATCH 26/46] ARROW-2032: [C++] ORC ep installs on each call to ninja build ExternalProject_add using git is sadly always triggering something. Using HTTP archives instead don't need to check if they are at the correct revision. Author: Uwe L. Korn Closes #1519 from xhochy/ARROW-2032 and squashes the following commits: f5ac8228 [Uwe L. Korn] ARROW-2032: [C++] ORC ep installs on each call to ninja build --- cpp/cmake_modules/ThirdpartyToolchain.cmake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake b/cpp/cmake_modules/ThirdpartyToolchain.cmake index 4f64434170655..69812b97cc770 100644 --- a/cpp/cmake_modules/ThirdpartyToolchain.cmake +++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake @@ -926,8 +926,7 @@ if (ARROW_ORC) -DZLIB_HOME=${ZLIB_HOME}) ExternalProject_Add(orc_ep - GIT_REPOSITORY "https://github.com/apache/orc" - GIT_TAG ${ORC_VERSION} + URL "https://github.com/apache/orc/archive/${ORC_VERSION}.tar.gz" BUILD_BYPRODUCTS ${ORC_STATIC_LIB} CMAKE_ARGS ${ORC_CMAKE_ARGS}) From edde5c19ad5c441429ae80cce32db80bd52ed364 Mon Sep 17 00:00:00 2001 From: Jim Crist Date: Sun, 28 Jan 2018 16:47:41 +0100 Subject: [PATCH 27/46] ARROW-1999: [Python] Type checking in `from_numpy_dtype` - Adds type checking to the C++ `NumPyDtypeToArrow` and `GetTensorType` to ensure `dtype` is actually a dtype object. - Add conversion of non-dtype objects in `pa.from_numpy_dtype`. - Adds tests to check a wider variety of inputs to `pa.from_numpy_dtype`, and ensure proper errors. Author: Jim Crist Closes #1523 from jcrist/from_numpy_dtype and squashes the following commits: e9de101 [Jim Crist] Type checking in `from_numpy_dtype` --- cpp/src/arrow/python/numpy_convert.cc | 6 ++++++ python/pyarrow/tests/test_schema.py | 27 ++++++++++++++++++++++++++- python/pyarrow/types.pxi | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/python/numpy_convert.cc b/cpp/src/arrow/python/numpy_convert.cc index 9ed2d73d42b57..124745edecf81 100644 --- a/cpp/src/arrow/python/numpy_convert.cc +++ b/cpp/src/arrow/python/numpy_convert.cc @@ -84,6 +84,9 @@ NumPyBuffer::~NumPyBuffer() { Py_XDECREF(arr_); } break; Status GetTensorType(PyObject* dtype, std::shared_ptr* out) { + if (!PyArray_DescrCheck(dtype)) { + return Status::TypeError("Did not pass numpy.dtype object"); + } PyArray_Descr* descr = reinterpret_cast(dtype); int type_num = cast_npy_type_compat(descr->type_num); @@ -145,6 +148,9 @@ Status GetNumPyType(const DataType& type, int* type_num) { } Status NumPyDtypeToArrow(PyObject* dtype, std::shared_ptr* out) { + if (!PyArray_DescrCheck(dtype)) { + return Status::TypeError("Did not pass numpy.dtype object"); + } PyArray_Descr* descr = reinterpret_cast(dtype); int type_num = cast_npy_type_compat(descr->type_num); diff --git a/python/pyarrow/tests/test_schema.py b/python/pyarrow/tests/test_schema.py index dbca139e20570..90efe3f7e950a 100644 --- a/python/pyarrow/tests/test_schema.py +++ b/python/pyarrow/tests/test_schema.py @@ -154,8 +154,21 @@ def test_time_types(): pa.time64('s') -def test_type_from_numpy_dtype_timestamps(): +def test_from_numpy_dtype(): cases = [ + (np.dtype('bool'), pa.bool_()), + (np.dtype('int8'), pa.int8()), + (np.dtype('int16'), pa.int16()), + (np.dtype('int32'), pa.int32()), + (np.dtype('int64'), pa.int64()), + (np.dtype('uint8'), pa.uint8()), + (np.dtype('uint16'), pa.uint16()), + (np.dtype('uint32'), pa.uint32()), + (np.dtype('float16'), pa.float16()), + (np.dtype('float32'), pa.float32()), + (np.dtype('float64'), pa.float64()), + (np.dtype('U'), pa.string()), + (np.dtype('S'), pa.binary()), (np.dtype('datetime64[s]'), pa.timestamp('s')), (np.dtype('datetime64[ms]'), pa.timestamp('ms')), (np.dtype('datetime64[us]'), pa.timestamp('us')), @@ -166,6 +179,18 @@ def test_type_from_numpy_dtype_timestamps(): result = pa.from_numpy_dtype(dt) assert result == pt + # Things convertible to numpy dtypes work + assert pa.from_numpy_dtype('U') == pa.string() + assert pa.from_numpy_dtype(np.unicode) == pa.string() + assert pa.from_numpy_dtype('int32') == pa.int32() + assert pa.from_numpy_dtype(bool) == pa.bool_() + + with pytest.raises(NotImplementedError): + pa.from_numpy_dtype(np.dtype('O')) + + with pytest.raises(TypeError): + pa.from_numpy_dtype('not_convertible_to_dtype') + def test_field(): t = pa.string() diff --git a/python/pyarrow/types.pxi b/python/pyarrow/types.pxi index 1563b57855cd9..a3cbeefb028c7 100644 --- a/python/pyarrow/types.pxi +++ b/python/pyarrow/types.pxi @@ -1207,6 +1207,7 @@ def from_numpy_dtype(object dtype): Convert NumPy dtype to pyarrow.DataType """ cdef shared_ptr[CDataType] c_type + dtype = np.dtype(dtype) with nogil: check_status(NumPyDtypeToArrow(dtype, &c_type)) From 450bf474f5add5f0ab09008a3057d1b57811ad6b Mon Sep 17 00:00:00 2001 From: "Korn, Uwe" Date: Sun, 28 Jan 2018 16:59:41 +0100 Subject: [PATCH 28/46] ARROW-1835: [C++] Create Arrow schema from std::tuple types Author: Korn, Uwe Closes #1478 from xhochy/ARROW-1835 and squashes the following commits: 9728740 [Korn, Uwe] Remove documentation tag that conflicts with gcc a690248 [Korn, Uwe] ARROW-1835: [C++] Create Arrow schema from std::tuple types --- cpp/src/arrow/CMakeLists.txt | 2 + cpp/src/arrow/stl-test.cc | 78 ++++++++++++++++++ cpp/src/arrow/stl.h | 153 +++++++++++++++++++++++++++++++++++ cpp/src/arrow/type.h | 13 +++ 4 files changed, 246 insertions(+) create mode 100644 cpp/src/arrow/stl-test.cc create mode 100644 cpp/src/arrow/stl.h diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index ad86256e0be34..74674bebb43be 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -153,6 +153,7 @@ install(FILES pretty_print.h record_batch.h status.h + stl.h table.h table_builder.h tensor.h @@ -183,6 +184,7 @@ ADD_ARROW_TEST(memory_pool-test) ADD_ARROW_TEST(pretty_print-test) ADD_ARROW_TEST(public-api-test) ADD_ARROW_TEST(status-test) +ADD_ARROW_TEST(stl-test) ADD_ARROW_TEST(type-test) ADD_ARROW_TEST(table-test) ADD_ARROW_TEST(table_builder-test) diff --git a/cpp/src/arrow/stl-test.cc b/cpp/src/arrow/stl-test.cc new file mode 100644 index 0000000000000..c85baa3a11e3f --- /dev/null +++ b/cpp/src/arrow/stl-test.cc @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "gtest/gtest.h" + +#include "arrow/stl.h" + +namespace arrow { +namespace stl { + +TEST(TestSchemaFromTuple, PrimitiveTypesVector) { + Schema expected_schema( + {field("column1", int8(), false), field("column2", int16(), false), + field("column3", int32(), false), field("column4", int64(), false), + field("column5", uint8(), false), field("column6", uint16(), false), + field("column7", uint32(), false), field("column8", uint64(), false), + field("column9", boolean(), false), field("column10", utf8(), false)}); + + std::shared_ptr schema = + SchemaFromTuple>:: + MakeSchema(std::vector({"column1", "column2", "column3", "column4", + "column5", "column6", "column7", "column8", + "column9", "column10"})); + ASSERT_TRUE(expected_schema.Equals(*schema)); +} + +TEST(TestSchemaFromTuple, PrimitiveTypesTuple) { + Schema expected_schema( + {field("column1", int8(), false), field("column2", int16(), false), + field("column3", int32(), false), field("column4", int64(), false), + field("column5", uint8(), false), field("column6", uint16(), false), + field("column7", uint32(), false), field("column8", uint64(), false), + field("column9", boolean(), false), field("column10", utf8(), false)}); + + std::shared_ptr schema = SchemaFromTuple< + std::tuple>::MakeSchema(std::make_tuple("column1", "column2", + "column3", "column4", + "column5", "column6", + "column7", "column8", + "column9", "column10")); + ASSERT_TRUE(expected_schema.Equals(*schema)); +} + +TEST(TestSchemaFromTuple, SimpleList) { + Schema expected_schema({field("column1", list(utf8()), false)}); + std::shared_ptr schema = + SchemaFromTuple>>::MakeSchema({"column1"}); + + ASSERT_TRUE(expected_schema.Equals(*schema)); +} + +TEST(TestSchemaFromTuple, NestedList) { + Schema expected_schema({field("column1", list(list(boolean())), false)}); + std::shared_ptr schema = + SchemaFromTuple>>>::MakeSchema( + {"column1"}); + + ASSERT_TRUE(expected_schema.Equals(*schema)); +} + +} // namespace stl +} // namespace arrow diff --git a/cpp/src/arrow/stl.h b/cpp/src/arrow/stl.h new file mode 100644 index 0000000000000..3250b5a320464 --- /dev/null +++ b/cpp/src/arrow/stl.h @@ -0,0 +1,153 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#ifndef ARROW_STL_H +#define ARROW_STL_H + +#include +#include +#include + +#include "arrow/type.h" + +namespace arrow { + +class Schema; + +namespace stl { + +/// Traits meta class to map standard C/C++ types to equivalent Arrow types. +template +struct ConversionTraits {}; + +#define ARROW_STL_CONVERSION(c_type, ArrowType_) \ + template <> \ + struct ConversionTraits { \ + using ArrowType = ArrowType_; \ + constexpr static bool nullable = false; \ + }; + +ARROW_STL_CONVERSION(bool, BooleanType) +ARROW_STL_CONVERSION(int8_t, Int8Type) +ARROW_STL_CONVERSION(int16_t, Int16Type) +ARROW_STL_CONVERSION(int32_t, Int32Type) +ARROW_STL_CONVERSION(int64_t, Int64Type) +ARROW_STL_CONVERSION(uint8_t, UInt8Type) +ARROW_STL_CONVERSION(uint16_t, UInt16Type) +ARROW_STL_CONVERSION(uint32_t, UInt32Type) +ARROW_STL_CONVERSION(uint64_t, UInt64Type) +ARROW_STL_CONVERSION(float, FloatType) +ARROW_STL_CONVERSION(double, DoubleType) +ARROW_STL_CONVERSION(std::string, StringType) + +template +struct ConversionTraits> { + using ArrowType = meta::ListType::ArrowType>; + constexpr static bool nullable = false; +}; + +/// Build an arrow::Schema based upon the types defined in a std::tuple-like structure. +/// +/// While the type information is available at compile-time, we still need to add the +/// column names at runtime, thus these methods are not constexpr. +template ::value> +struct SchemaFromTuple { + using Element = typename std::tuple_element::type; + using ArrowType = typename ConversionTraits::ArrowType; + + // Implementations that take a vector-like object for the column names. + + /// Recursively build a vector of arrow::Field from the defined types. + /// + /// In most cases MakeSchema is the better entrypoint for the Schema creation. + static std::vector> MakeSchemaRecursion( + const std::vector& names) { + std::vector> ret = + SchemaFromTuple::MakeSchemaRecursion(names); + ret.push_back(field(names[N - 1], std::make_shared(), + ConversionTraits::nullable)); + return ret; + } + + /// Build a Schema from the types of the tuple-like structure passed in as template + /// parameter assign the column names at runtime. + /// + /// An example usage of this API can look like the following: + /// + /// \code{.cpp} + /// using TupleType = std::tuple>; + /// std::shared_ptr schema = + /// SchemaFromTuple::MakeSchema({"int_column", "list_of_strings_column"}); + /// \endcode + static std::shared_ptr MakeSchema(const std::vector& names) { + return std::make_shared(MakeSchemaRecursion(names)); + } + + // Implementations that take a tuple-like object for the column names. + + /// Recursively build a vector of arrow::Field from the defined types. + /// + /// In most cases MakeSchema is the better entrypoint for the Schema creation. + template + static std::vector> MakeSchemaRecursionT( + const NamesTuple& names) { + std::vector> ret = + SchemaFromTuple::MakeSchemaRecursionT(names); + ret.push_back(field(std::get(names), std::make_shared(), + ConversionTraits::nullable)); + return ret; + } + + /// Build a Schema from the types of the tuple-like structure passed in as template + /// parameter assign the column names at runtime. + /// + /// An example usage of this API can look like the following: + /// + /// \code{.cpp} + /// using TupleType = std::tuple>; + /// std::shared_ptr schema = + /// SchemaFromTuple::MakeSchema({"int_column", "list_of_strings_column"}); + /// \endcode + template + static std::shared_ptr MakeSchema(const NamesTuple& names) { + return std::make_shared(MakeSchemaRecursionT(names)); + } +}; + +template +struct SchemaFromTuple { + static std::vector> MakeSchemaRecursion( + const std::vector& names) { + std::vector> ret; + ret.reserve(names.size()); + return ret; + } + + template + static std::vector> MakeSchemaRecursionT( + const NamesTuple& names) { + std::vector> ret; + ret.reserve(std::tuple_size::value); + return ret; + } +}; +/// @endcond + +} // namespace stl +} // namespace arrow + +#endif // ARROW_STL_H diff --git a/cpp/src/arrow/type.h b/cpp/src/arrow/type.h index 009e07db07744..cfee6fd0e2363 100644 --- a/cpp/src/arrow/type.h +++ b/cpp/src/arrow/type.h @@ -407,6 +407,19 @@ class ARROW_EXPORT ListType : public NestedType { std::string name() const override { return "list"; } }; +namespace meta { + +/// Additional ListType class that can be instantiated with only compile-time arguments. +template +class ARROW_EXPORT ListType : public ::arrow::ListType { + public: + using ValueType = T; + + ListType() : ::arrow::ListType(std::make_shared()) {} +}; + +} // namespace meta + // BinaryType type is represents lists of 1-byte values. class ARROW_EXPORT BinaryType : public DataType, public NoExtraMeta { public: From 05439532e70c105f8f282e2963dc31e0340ec503 Mon Sep 17 00:00:00 2001 From: "Korn, Uwe" Date: Sun, 28 Jan 2018 17:30:59 +0100 Subject: [PATCH 29/46] ARROW-1646: [Python] Handle NumPy scalar types Author: Korn, Uwe Author: Uwe L. Korn Closes #1475 from xhochy/ARROW-1646 and squashes the following commits: 7d85879 [Uwe L. Korn] flake8 eb4c08d [Korn, Uwe] ARROW-1646: [Python] pyarrow.array cannot handle NumPy scalar types --- cpp/src/arrow/python/builtin_convert.cc | 34 + cpp/src/arrow/python/numpy_convert.cc | 3 + cpp/src/arrow/python/numpy_convert.h | 2 + cpp/src/arrow/python/numpy_interop.h | 1 + python/pyarrow/tests/test_convert_builtin.py | 805 +++++++++++-------- 5 files changed, 492 insertions(+), 353 deletions(-) diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc index 71f2fde5b3920..f7a370cdc4c06 100644 --- a/cpp/src/arrow/python/builtin_convert.cc +++ b/cpp/src/arrow/python/builtin_convert.cc @@ -32,6 +32,7 @@ #include "arrow/util/logging.h" #include "arrow/python/helpers.h" +#include "arrow/python/numpy_convert.h" #include "arrow/python/util/datetime.h" namespace arrow { @@ -93,6 +94,21 @@ class ScalarVisitor { ++binary_count_; } else if (PyUnicode_Check(obj)) { ++unicode_count_; + } else if (PyArray_CheckAnyScalarExact(obj)) { + std::shared_ptr type; + RETURN_NOT_OK(NumPyDtypeToArrow(PyArray_DescrFromScalar(obj), &type)); + if (is_integer(type->id())) { + ++int_count_; + } else if (is_floating(type->id())) { + ++float_count_; + } else if (type->id() == Type::TIMESTAMP) { + ++timestamp_count_; + } else { + std::ostringstream ss; + ss << "Found a NumPy scalar with Arrow dtype that we cannot handle: "; + ss << type->ToString(); + return Status::Invalid(ss.str()); + } } else { // TODO(wesm): accumulate error information somewhere static std::string supported_types = @@ -575,6 +591,24 @@ class TimestampConverter t = PyDateTime_to_ns(pydatetime); break; } + } else if (PyArray_CheckAnyScalarExact(item.obj())) { + // numpy.datetime64 + std::shared_ptr type; + RETURN_NOT_OK(NumPyDtypeToArrow(PyArray_DescrFromScalar(item.obj()), &type)); + if (type->id() != Type::TIMESTAMP) { + std::ostringstream ss; + ss << "Expected np.datetime64 but got: "; + ss << type->ToString(); + return Status::Invalid(ss.str()); + } + const TimestampType& ttype = static_cast(*type); + if (unit_ != ttype.unit()) { + return Status::NotImplemented( + "Cannot convert NumPy datetime64 objects with differing unit"); + } + + PyDatetimeScalarObject* obj = reinterpret_cast(item.obj()); + t = obj->obval; } else { t = static_cast(PyLong_AsLongLong(item.obj())); RETURN_IF_PYERROR(); diff --git a/cpp/src/arrow/python/numpy_convert.cc b/cpp/src/arrow/python/numpy_convert.cc index 124745edecf81..c2d055fceed5a 100644 --- a/cpp/src/arrow/python/numpy_convert.cc +++ b/cpp/src/arrow/python/numpy_convert.cc @@ -152,7 +152,10 @@ Status NumPyDtypeToArrow(PyObject* dtype, std::shared_ptr* out) { return Status::TypeError("Did not pass numpy.dtype object"); } PyArray_Descr* descr = reinterpret_cast(dtype); + return NumPyDtypeToArrow(descr, out); +} +Status NumPyDtypeToArrow(PyArray_Descr* descr, std::shared_ptr* out) { int type_num = cast_npy_type_compat(descr->type_num); switch (type_num) { diff --git a/cpp/src/arrow/python/numpy_convert.h b/cpp/src/arrow/python/numpy_convert.h index 93c4848926cfc..220e38f2e1e02 100644 --- a/cpp/src/arrow/python/numpy_convert.h +++ b/cpp/src/arrow/python/numpy_convert.h @@ -56,6 +56,8 @@ bool is_contiguous(PyObject* array); ARROW_EXPORT Status NumPyDtypeToArrow(PyObject* dtype, std::shared_ptr* out); +ARROW_EXPORT +Status NumPyDtypeToArrow(PyArray_Descr* descr, std::shared_ptr* out); Status GetTensorType(PyObject* dtype, std::shared_ptr* out); Status GetNumPyType(const DataType& type, int* type_num); diff --git a/cpp/src/arrow/python/numpy_interop.h b/cpp/src/arrow/python/numpy_interop.h index b93200cc8972d..8c569e232c121 100644 --- a/cpp/src/arrow/python/numpy_interop.h +++ b/cpp/src/arrow/python/numpy_interop.h @@ -40,6 +40,7 @@ #endif #include +#include #include namespace arrow { diff --git a/python/pyarrow/tests/test_convert_builtin.py b/python/pyarrow/tests/test_convert_builtin.py index d7760da2f9b47..fa603b1a92fa2 100644 --- a/python/pyarrow/tests/test_convert_builtin.py +++ b/python/pyarrow/tests/test_convert_builtin.py @@ -23,6 +23,8 @@ import datetime import decimal +import numpy as np +import six class StrangeIterable: @@ -33,356 +35,453 @@ def __iter__(self): return self.lst.__iter__() -class TestConvertIterable(unittest.TestCase): - - def test_iterable_types(self): - arr1 = pa.array(StrangeIterable([0, 1, 2, 3])) - arr2 = pa.array((0, 1, 2, 3)) - - assert arr1.equals(arr2) - - def test_empty_iterable(self): - arr = pa.array(StrangeIterable([])) - assert len(arr) == 0 - assert arr.null_count == 0 - assert arr.type == pa.null() - assert arr.to_pylist() == [] - - -class TestLimitedConvertIterator(unittest.TestCase): - def test_iterator_types(self): - arr1 = pa.array(iter(range(3)), type=pa.int64(), size=3) - arr2 = pa.array((0, 1, 2)) - assert arr1.equals(arr2) - - def test_iterator_size_overflow(self): - arr1 = pa.array(iter(range(3)), type=pa.int64(), size=2) - arr2 = pa.array((0, 1)) - assert arr1.equals(arr2) - - def test_iterator_size_underflow(self): - arr1 = pa.array(iter(range(3)), type=pa.int64(), size=10) - arr2 = pa.array((0, 1, 2)) - assert arr1.equals(arr2) - - -class TestConvertSequence(unittest.TestCase): - - def test_sequence_types(self): - arr1 = pa.array([1, 2, 3]) - arr2 = pa.array((1, 2, 3)) - - assert arr1.equals(arr2) - - def test_boolean(self): - expected = [True, None, False, None] - arr = pa.array(expected) - assert len(arr) == 4 - assert arr.null_count == 2 - assert arr.type == pa.bool_() - assert arr.to_pylist() == expected - - def test_empty_list(self): - arr = pa.array([]) - assert len(arr) == 0 - assert arr.null_count == 0 - assert arr.type == pa.null() - assert arr.to_pylist() == [] - - def test_all_none(self): - arr = pa.array([None, None]) - assert len(arr) == 2 - assert arr.null_count == 2 - assert arr.type == pa.null() - assert arr.to_pylist() == [None, None] - - def test_integer(self): - expected = [1, None, 3, None] - arr = pa.array(expected) - assert len(arr) == 4 - assert arr.null_count == 2 - assert arr.type == pa.int64() - assert arr.to_pylist() == expected - - def test_garbage_collection(self): - import gc - - # Force the cyclic garbage collector to run - gc.collect() - - bytes_before = pa.total_allocated_bytes() - pa.array([1, None, 3, None]) - gc.collect() - assert pa.total_allocated_bytes() == bytes_before - - def test_double(self): - data = [1.5, 1, None, 2.5, None, None] - arr = pa.array(data) - assert len(arr) == 6 - assert arr.null_count == 3 - assert arr.type == pa.float64() - assert arr.to_pylist() == data - - def test_unicode(self): - data = [u'foo', u'bar', None, u'mañana'] - arr = pa.array(data) - assert len(arr) == 4 - assert arr.null_count == 1 - assert arr.type == pa.string() - assert arr.to_pylist() == data - - def test_bytes(self): - u1 = b'ma\xc3\xb1ana' - data = [b'foo', - u1.decode('utf-8'), # unicode gets encoded, - None] - arr = pa.array(data) - assert len(arr) == 3 - assert arr.null_count == 1 - assert arr.type == pa.binary() - assert arr.to_pylist() == [b'foo', u1, None] - - def test_utf8_to_unicode(self): - # ARROW-1225 - data = [b'foo', None, b'bar'] - arr = pa.array(data, type=pa.string()) - assert arr[0].as_py() == u'foo' - - # test a non-utf8 unicode string - val = (u'mañana').encode('utf-16-le') - with pytest.raises(pa.ArrowException): - pa.array([val], type=pa.string()) - - def test_fixed_size_bytes(self): - data = [b'foof', None, b'barb', b'2346'] - arr = pa.array(data, type=pa.binary(4)) - assert len(arr) == 4 - assert arr.null_count == 1 - assert arr.type == pa.binary(4) - assert arr.to_pylist() == data - - def test_fixed_size_bytes_does_not_accept_varying_lengths(self): - data = [b'foo', None, b'barb', b'2346'] - with self.assertRaises(pa.ArrowInvalid): - pa.array(data, type=pa.binary(4)) - - def test_date(self): - data = [datetime.date(2000, 1, 1), None, datetime.date(1970, 1, 1), - datetime.date(2040, 2, 26)] - arr = pa.array(data) - assert len(arr) == 4 - assert arr.type == pa.date64() - assert arr.null_count == 1 - assert arr[0].as_py() == datetime.date(2000, 1, 1) - assert arr[1].as_py() is None - assert arr[2].as_py() == datetime.date(1970, 1, 1) - assert arr[3].as_py() == datetime.date(2040, 2, 26) - - def test_date32(self): - data = [datetime.date(2000, 1, 1), None] - arr = pa.array(data, type=pa.date32()) - - data2 = [10957, None] - arr2 = pa.array(data2, type=pa.date32()) - - for x in [arr, arr2]: - assert len(x) == 2 - assert x.type == pa.date32() - assert x.null_count == 1 - assert x[0].as_py() == datetime.date(2000, 1, 1) - assert x[1] is pa.NA - - # Overflow - data3 = [2**32, None] - with pytest.raises(pa.ArrowException): - pa.array(data3, type=pa.date32()) - - def test_timestamp(self): - data = [ - datetime.datetime(2007, 7, 13, 1, 23, 34, 123456), - None, - datetime.datetime(2006, 1, 13, 12, 34, 56, 432539), - datetime.datetime(2010, 8, 13, 5, 46, 57, 437699) - ] - arr = pa.array(data) - assert len(arr) == 4 - assert arr.type == pa.timestamp('us') - assert arr.null_count == 1 - assert arr[0].as_py() == datetime.datetime(2007, 7, 13, 1, - 23, 34, 123456) - assert arr[1].as_py() is None - assert arr[2].as_py() == datetime.datetime(2006, 1, 13, 12, - 34, 56, 432539) - assert arr[3].as_py() == datetime.datetime(2010, 8, 13, 5, - 46, 57, 437699) - - def test_timestamp_with_unit(self): - data = [ - datetime.datetime(2007, 7, 13, 1, 23, 34, 123456), - ] - - s = pa.timestamp('s') - ms = pa.timestamp('ms') - us = pa.timestamp('us') - ns = pa.timestamp('ns') - - arr_s = pa.array(data, type=s) - assert len(arr_s) == 1 - assert arr_s.type == s - assert arr_s[0].as_py() == datetime.datetime(2007, 7, 13, 1, - 23, 34, 0) - - arr_ms = pa.array(data, type=ms) - assert len(arr_ms) == 1 - assert arr_ms.type == ms - assert arr_ms[0].as_py() == datetime.datetime(2007, 7, 13, 1, - 23, 34, 123000) - - arr_us = pa.array(data, type=us) - assert len(arr_us) == 1 - assert arr_us.type == us - assert arr_us[0].as_py() == datetime.datetime(2007, 7, 13, 1, - 23, 34, 123456) - - arr_ns = pa.array(data, type=ns) - assert len(arr_ns) == 1 - assert arr_ns.type == ns - assert arr_ns[0].as_py() == datetime.datetime(2007, 7, 13, 1, - 23, 34, 123456) - - def test_timestamp_from_int_with_unit(self): - data = [1] - - s = pa.timestamp('s') - ms = pa.timestamp('ms') - us = pa.timestamp('us') - ns = pa.timestamp('ns') - - arr_s = pa.array(data, type=s) - assert len(arr_s) == 1 - assert arr_s.type == s - assert str(arr_s[0]) == "Timestamp('1970-01-01 00:00:01')" - - arr_ms = pa.array(data, type=ms) - assert len(arr_ms) == 1 - assert arr_ms.type == ms - assert str(arr_ms[0]) == "Timestamp('1970-01-01 00:00:00.001000')" - - arr_us = pa.array(data, type=us) - assert len(arr_us) == 1 - assert arr_us.type == us - assert str(arr_us[0]) == "Timestamp('1970-01-01 00:00:00.000001')" - - arr_ns = pa.array(data, type=ns) - assert len(arr_ns) == 1 - assert arr_ns.type == ns - assert str(arr_ns[0]) == "Timestamp('1970-01-01 00:00:00.000000001')" - - with pytest.raises(pa.ArrowException): - class CustomClass(): - pass - pa.array([1, CustomClass()], type=ns) - pa.array([1, CustomClass()], type=pa.date32()) - pa.array([1, CustomClass()], type=pa.date64()) - - def test_mixed_nesting_levels(self): - pa.array([1, 2, None]) - pa.array([[1], [2], None]) - pa.array([[1], [2], [None]]) - - with self.assertRaises(pa.ArrowInvalid): - pa.array([1, 2, [1]]) - - with self.assertRaises(pa.ArrowInvalid): - pa.array([1, 2, []]) - - with self.assertRaises(pa.ArrowInvalid): - pa.array([[1], [2], [None, [1]]]) - - def test_list_of_int(self): - data = [[1, 2, 3], [], None, [1, 2]] - arr = pa.array(data) - assert len(arr) == 4 - assert arr.null_count == 1 - assert arr.type == pa.list_(pa.int64()) - assert arr.to_pylist() == data - - def test_mixed_types_fails(self): - data = ['a', 1, 2.0] - with self.assertRaises(pa.ArrowException): - pa.array(data) - - def test_mixed_types_with_specified_type_fails(self): - data = ['-10', '-5', {'a': 1}, '0', '5', '10'] - - type = pa.string() - with self.assertRaises(pa.ArrowInvalid): - pa.array(data, type=type) - - def test_decimal(self): - data = [decimal.Decimal('1234.183'), decimal.Decimal('8094.234')] - type = pa.decimal128(precision=7, scale=3) - arr = pa.array(data, type=type) - assert arr.to_pylist() == data - - def test_decimal_different_precisions(self): - data = [ - decimal.Decimal('1234234983.183'), decimal.Decimal('80943244.234') - ] - type = pa.decimal128(precision=13, scale=3) - arr = pa.array(data, type=type) - assert arr.to_pylist() == data - - def test_decimal_no_scale(self): - data = [decimal.Decimal('1234234983'), decimal.Decimal('8094324')] - type = pa.decimal128(precision=10) - arr = pa.array(data, type=type) - assert arr.to_pylist() == data - - def test_decimal_negative(self): - data = [decimal.Decimal('-1234.234983'), decimal.Decimal('-8.094324')] - type = pa.decimal128(precision=10, scale=6) - arr = pa.array(data, type=type) - assert arr.to_pylist() == data - - def test_decimal_no_whole_part(self): - data = [decimal.Decimal('-.4234983'), decimal.Decimal('.0103943')] - type = pa.decimal128(precision=7, scale=7) - arr = pa.array(data, type=type) - assert arr.to_pylist() == data - - def test_decimal_large_integer(self): - data = [decimal.Decimal('-394029506937548693.42983'), - decimal.Decimal('32358695912932.01033')] - type = pa.decimal128(precision=23, scale=5) - arr = pa.array(data, type=type) - assert arr.to_pylist() == data - - def test_range_types(self): - arr1 = pa.array(range(3)) - arr2 = pa.array((0, 1, 2)) - assert arr1.equals(arr2) - - def test_empty_range(self): - arr = pa.array(range(0)) - assert len(arr) == 0 - assert arr.null_count == 0 - assert arr.type == pa.null() - assert arr.to_pylist() == [] - - def test_structarray(self): - ints = pa.array([None, 2, 3], type=pa.int64()) - strs = pa.array([u'a', None, u'c'], type=pa.string()) - bools = pa.array([True, False, None], type=pa.bool_()) - arr = pa.StructArray.from_arrays( - ['ints', 'strs', 'bools'], - [ints, strs, bools]) - - expected = [ - {'ints': None, 'strs': u'a', 'bools': True}, - {'ints': 2, 'strs': None, 'bools': False}, - {'ints': 3, 'strs': u'c', 'bools': None}, - ] - - pylist = arr.to_pylist() - assert pylist == expected, (pylist, expected) +def test_iterable_types(): + arr1 = pa.array(StrangeIterable([0, 1, 2, 3])) + arr2 = pa.array((0, 1, 2, 3)) + + assert arr1.equals(arr2) + + +def test_empty_iterable(): + arr = pa.array(StrangeIterable([])) + assert len(arr) == 0 + assert arr.null_count == 0 + assert arr.type == pa.null() + assert arr.to_pylist() == [] + + +def test_limited_iterator_types(): + arr1 = pa.array(iter(range(3)), type=pa.int64(), size=3) + arr2 = pa.array((0, 1, 2)) + assert arr1.equals(arr2) + + +def test_limited_iterator_size_overflow(): + arr1 = pa.array(iter(range(3)), type=pa.int64(), size=2) + arr2 = pa.array((0, 1)) + assert arr1.equals(arr2) + + +def test_limited_iterator_size_underflow(): + arr1 = pa.array(iter(range(3)), type=pa.int64(), size=10) + arr2 = pa.array((0, 1, 2)) + assert arr1.equals(arr2) + + +def _as_list(xs): + return xs + + +def _as_tuple(xs): + return tuple(xs) + + +def _as_dict_values(xs): + dct = {k: v for k, v in enumerate(xs)} + return six.viewvalues(dct) + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +def test_sequence_types(seq): + arr1 = pa.array(seq([1, 2, 3])) + arr2 = pa.array([1, 2, 3]) + + assert arr1.equals(arr2) + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +def test_sequence_boolean(seq): + expected = [True, None, False, None] + arr = pa.array(seq(expected)) + assert len(arr) == 4 + assert arr.null_count == 2 + assert arr.type == pa.bool_() + assert arr.to_pylist() == expected + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +def test_sequence_numpy_boolean(seq): + expected = [np.bool(True), None, np.bool(False), None] + arr = pa.array(seq(expected)) + assert len(arr) == 4 + assert arr.null_count == 2 + assert arr.type == pa.bool_() + assert arr.to_pylist() == expected + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +def test_empty_list(seq): + arr = pa.array(seq([])) + assert len(arr) == 0 + assert arr.null_count == 0 + assert arr.type == pa.null() + assert arr.to_pylist() == [] + + +def test_sequence_all_none(): + arr = pa.array([None, None]) + assert len(arr) == 2 + assert arr.null_count == 2 + assert arr.type == pa.null() + assert arr.to_pylist() == [None, None] + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +def test_sequence_integer(seq): + expected = [1, None, 3, None] + arr = pa.array(seq(expected)) + assert len(arr) == 4 + assert arr.null_count == 2 + assert arr.type == pa.int64() + assert arr.to_pylist() == expected + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +@pytest.mark.parametrize("np_scalar", [np.int16, np.int32, np.int64, np.uint16, + np.uint32, np.uint64]) +def test_sequence_numpy_integer(seq, np_scalar): + expected = [np_scalar(1), None, np_scalar(3), None] + arr = pa.array(seq(expected)) + assert len(arr) == 4 + assert arr.null_count == 2 + assert arr.type == pa.int64() + assert arr.to_pylist() == expected + + +def test_garbage_collection(): + import gc + + # Force the cyclic garbage collector to run + gc.collect() + + bytes_before = pa.total_allocated_bytes() + pa.array([1, None, 3, None]) + gc.collect() + assert pa.total_allocated_bytes() == bytes_before + + +def test_sequence_double(): + data = [1.5, 1, None, 2.5, None, None] + arr = pa.array(data) + assert len(arr) == 6 + assert arr.null_count == 3 + assert arr.type == pa.float64() + assert arr.to_pylist() == data + + +@pytest.mark.parametrize("seq", [_as_list, _as_tuple, _as_dict_values]) +@pytest.mark.parametrize("np_scalar", [np.float16, np.float32, np.float64]) +def test_sequence_numpy_double(seq, np_scalar): + data = [np_scalar(1.5), np_scalar(1), None, np_scalar(2.5), None, None] + arr = pa.array(seq(data)) + assert len(arr) == 6 + assert arr.null_count == 3 + assert arr.type == pa.float64() + assert arr.to_pylist() == data + + +def test_sequence_unicode(): + data = [u'foo', u'bar', None, u'mañana'] + arr = pa.array(data) + assert len(arr) == 4 + assert arr.null_count == 1 + assert arr.type == pa.string() + assert arr.to_pylist() == data + + +def test_sequence_bytes(): + u1 = b'ma\xc3\xb1ana' + data = [b'foo', + u1.decode('utf-8'), # unicode gets encoded, + None] + arr = pa.array(data) + assert len(arr) == 3 + assert arr.null_count == 1 + assert arr.type == pa.binary() + assert arr.to_pylist() == [b'foo', u1, None] + + +def test_sequence_utf8_to_unicode(): + # ARROW-1225 + data = [b'foo', None, b'bar'] + arr = pa.array(data, type=pa.string()) + assert arr[0].as_py() == u'foo' + + # test a non-utf8 unicode string + val = (u'mañana').encode('utf-16-le') + with pytest.raises(pa.ArrowException): + pa.array([val], type=pa.string()) + + +def test_sequence_fixed_size_bytes(): + data = [b'foof', None, b'barb', b'2346'] + arr = pa.array(data, type=pa.binary(4)) + assert len(arr) == 4 + assert arr.null_count == 1 + assert arr.type == pa.binary(4) + assert arr.to_pylist() == data + + +def test_fixed_size_bytes_does_not_accept_varying_lengths(): + data = [b'foo', None, b'barb', b'2346'] + with pytest.raises(pa.ArrowInvalid): + pa.array(data, type=pa.binary(4)) + + +def test_sequence_date(): + data = [datetime.date(2000, 1, 1), None, datetime.date(1970, 1, 1), + datetime.date(2040, 2, 26)] + arr = pa.array(data) + assert len(arr) == 4 + assert arr.type == pa.date64() + assert arr.null_count == 1 + assert arr[0].as_py() == datetime.date(2000, 1, 1) + assert arr[1].as_py() is None + assert arr[2].as_py() == datetime.date(1970, 1, 1) + assert arr[3].as_py() == datetime.date(2040, 2, 26) + + +def test_sequence_date32(): + data = [datetime.date(2000, 1, 1), None] + arr = pa.array(data, type=pa.date32()) + + data2 = [10957, None] + arr2 = pa.array(data2, type=pa.date32()) + + for x in [arr, arr2]: + assert len(x) == 2 + assert x.type == pa.date32() + assert x.null_count == 1 + assert x[0].as_py() == datetime.date(2000, 1, 1) + assert x[1] is pa.NA + + # Overflow + data3 = [2**32, None] + with pytest.raises(pa.ArrowException): + pa.array(data3, type=pa.date32()) + + +def test_sequence_timestamp(): + data = [ + datetime.datetime(2007, 7, 13, 1, 23, 34, 123456), + None, + datetime.datetime(2006, 1, 13, 12, 34, 56, 432539), + datetime.datetime(2010, 8, 13, 5, 46, 57, 437699) + ] + arr = pa.array(data) + assert len(arr) == 4 + assert arr.type == pa.timestamp('us') + assert arr.null_count == 1 + assert arr[0].as_py() == datetime.datetime(2007, 7, 13, 1, + 23, 34, 123456) + assert arr[1].as_py() is None + assert arr[2].as_py() == datetime.datetime(2006, 1, 13, 12, + 34, 56, 432539) + assert arr[3].as_py() == datetime.datetime(2010, 8, 13, 5, + 46, 57, 437699) + + +def test_sequence_numpy_timestamp(): + data = [ + np.datetime64(datetime.datetime(2007, 7, 13, 1, 23, 34, 123456)), + None, + np.datetime64(datetime.datetime(2006, 1, 13, 12, 34, 56, 432539)), + np.datetime64(datetime.datetime(2010, 8, 13, 5, 46, 57, 437699)) + ] + arr = pa.array(data) + assert len(arr) == 4 + assert arr.type == pa.timestamp('us') + assert arr.null_count == 1 + assert arr[0].as_py() == datetime.datetime(2007, 7, 13, 1, + 23, 34, 123456) + assert arr[1].as_py() is None + assert arr[2].as_py() == datetime.datetime(2006, 1, 13, 12, + 34, 56, 432539) + assert arr[3].as_py() == datetime.datetime(2010, 8, 13, 5, + 46, 57, 437699) + + +def test_sequence_timestamp_with_unit(): + data = [ + datetime.datetime(2007, 7, 13, 1, 23, 34, 123456), + ] + + s = pa.timestamp('s') + ms = pa.timestamp('ms') + us = pa.timestamp('us') + ns = pa.timestamp('ns') + + arr_s = pa.array(data, type=s) + assert len(arr_s) == 1 + assert arr_s.type == s + assert arr_s[0].as_py() == datetime.datetime(2007, 7, 13, 1, + 23, 34, 0) + + arr_ms = pa.array(data, type=ms) + assert len(arr_ms) == 1 + assert arr_ms.type == ms + assert arr_ms[0].as_py() == datetime.datetime(2007, 7, 13, 1, + 23, 34, 123000) + + arr_us = pa.array(data, type=us) + assert len(arr_us) == 1 + assert arr_us.type == us + assert arr_us[0].as_py() == datetime.datetime(2007, 7, 13, 1, + 23, 34, 123456) + + arr_ns = pa.array(data, type=ns) + assert len(arr_ns) == 1 + assert arr_ns.type == ns + assert arr_ns[0].as_py() == datetime.datetime(2007, 7, 13, 1, + 23, 34, 123456) + + +def test_sequence_timestamp_from_int_with_unit(): + data = [1] + + s = pa.timestamp('s') + ms = pa.timestamp('ms') + us = pa.timestamp('us') + ns = pa.timestamp('ns') + + arr_s = pa.array(data, type=s) + assert len(arr_s) == 1 + assert arr_s.type == s + assert str(arr_s[0]) == "Timestamp('1970-01-01 00:00:01')" + + arr_ms = pa.array(data, type=ms) + assert len(arr_ms) == 1 + assert arr_ms.type == ms + assert str(arr_ms[0]) == "Timestamp('1970-01-01 00:00:00.001000')" + + arr_us = pa.array(data, type=us) + assert len(arr_us) == 1 + assert arr_us.type == us + assert str(arr_us[0]) == "Timestamp('1970-01-01 00:00:00.000001')" + + arr_ns = pa.array(data, type=ns) + assert len(arr_ns) == 1 + assert arr_ns.type == ns + assert str(arr_ns[0]) == "Timestamp('1970-01-01 00:00:00.000000001')" + + with pytest.raises(pa.ArrowException): + class CustomClass(): + pass + pa.array([1, CustomClass()], type=ns) + pa.array([1, CustomClass()], type=pa.date32()) + pa.array([1, CustomClass()], type=pa.date64()) + + +def test_sequence_mixed_nesting_levels(): + pa.array([1, 2, None]) + pa.array([[1], [2], None]) + pa.array([[1], [2], [None]]) + + with pytest.raises(pa.ArrowInvalid): + pa.array([1, 2, [1]]) + + with pytest.raises(pa.ArrowInvalid): + pa.array([1, 2, []]) + + with pytest.raises(pa.ArrowInvalid): + pa.array([[1], [2], [None, [1]]]) + + +def test_sequence_list_of_int(): + data = [[1, 2, 3], [], None, [1, 2]] + arr = pa.array(data) + assert len(arr) == 4 + assert arr.null_count == 1 + assert arr.type == pa.list_(pa.int64()) + assert arr.to_pylist() == data + + +def test_sequence_mixed_types_fails(): + data = ['a', 1, 2.0] + with pytest.raises(pa.ArrowException): + pa.array(data) + + +def test_sequence_mixed_types_with_specified_type_fails(): + data = ['-10', '-5', {'a': 1}, '0', '5', '10'] + + type = pa.string() + with pytest.raises(pa.ArrowInvalid): + pa.array(data, type=type) + + +def test_sequence_decimal(): + data = [decimal.Decimal('1234.183'), decimal.Decimal('8094.234')] + type = pa.decimal128(precision=7, scale=3) + arr = pa.array(data, type=type) + assert arr.to_pylist() == data + + +def test_sequence_decimal_different_precisions(): + data = [ + decimal.Decimal('1234234983.183'), decimal.Decimal('80943244.234') + ] + type = pa.decimal128(precision=13, scale=3) + arr = pa.array(data, type=type) + assert arr.to_pylist() == data + + +def test_sequence_decimal_no_scale(): + data = [decimal.Decimal('1234234983'), decimal.Decimal('8094324')] + type = pa.decimal128(precision=10) + arr = pa.array(data, type=type) + assert arr.to_pylist() == data + + +def test_sequence_decimal_negative(): + data = [decimal.Decimal('-1234.234983'), decimal.Decimal('-8.094324')] + type = pa.decimal128(precision=10, scale=6) + arr = pa.array(data, type=type) + assert arr.to_pylist() == data + + +def test_sequence_decimal_no_whole_part(): + data = [decimal.Decimal('-.4234983'), decimal.Decimal('.0103943')] + type = pa.decimal128(precision=7, scale=7) + arr = pa.array(data, type=type) + assert arr.to_pylist() == data + + +def test_sequence_decimal_large_integer(): + data = [decimal.Decimal('-394029506937548693.42983'), + decimal.Decimal('32358695912932.01033')] + type = pa.decimal128(precision=23, scale=5) + arr = pa.array(data, type=type) + assert arr.to_pylist() == data + + +def test_range_types(): + arr1 = pa.array(range(3)) + arr2 = pa.array((0, 1, 2)) + assert arr1.equals(arr2) + + +def test_empty_range(): + arr = pa.array(range(0)) + assert len(arr) == 0 + assert arr.null_count == 0 + assert arr.type == pa.null() + assert arr.to_pylist() == [] + + +def test_structarray(): + ints = pa.array([None, 2, 3], type=pa.int64()) + strs = pa.array([u'a', None, u'c'], type=pa.string()) + bools = pa.array([True, False, None], type=pa.bool_()) + arr = pa.StructArray.from_arrays( + ['ints', 'strs', 'bools'], + [ints, strs, bools]) + + expected = [ + {'ints': None, 'strs': u'a', 'bools': True}, + {'ints': 2, 'strs': None, 'bools': False}, + {'ints': 3, 'strs': u'c', 'bools': None}, + ] + + pylist = arr.to_pylist() + assert pylist == expected, (pylist, expected) From e50b1b3263007f20d9d0e29ace7c15685e334296 Mon Sep 17 00:00:00 2001 From: "Korn, Uwe" Date: Sun, 28 Jan 2018 17:31:52 +0100 Subject: [PATCH 30/46] ARROW-2028: [Python] extra_cmake_args needs to be passed through shlex.split Author: Korn, Uwe Closes #1501 from xhochy/ARROW-2028 and squashes the following commits: 212f365 [Korn, Uwe] ARROW-2028: [Python] extra_cmake_args needs to be passed through shlex.split --- python/setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/setup.py b/python/setup.py index 3d3831dc048c6..076d7e489b5f3 100644 --- a/python/setup.py +++ b/python/setup.py @@ -21,6 +21,7 @@ import os import os.path as osp import re +import shlex import shutil import sys @@ -180,8 +181,9 @@ def _run_cmake(self): cmake_options.append('-DCMAKE_BUILD_TYPE={0}' .format(self.build_type.lower())) + extra_cmake_args = shlex.split(self.extra_cmake_args) if sys.platform != 'win32': - cmake_command = (['cmake', self.extra_cmake_args] + + cmake_command = (['cmake'] + extra_cmake_args + cmake_options + [source]) print("-- Runnning cmake for pyarrow") @@ -197,13 +199,11 @@ def _run_cmake(self): self.spawn(args) print("-- Finished cmake --build for pyarrow") else: - import shlex cmake_generator = 'Visual Studio 14 2015 Win64' if not is_64_bit: raise RuntimeError('Not supported on 32-bit Windows') # Generate the build files - extra_cmake_args = shlex.split(self.extra_cmake_args) cmake_command = (['cmake'] + extra_cmake_args + cmake_options + [source, '-G', cmake_generator]) From d3226349fc61a0ffbb2139f259053ae787e500c8 Mon Sep 17 00:00:00 2001 From: Licht-T Date: Sun, 28 Jan 2018 17:33:20 +0100 Subject: [PATCH 31/46] ARROW-1992: [C++/Python] Fix segfault when string to categorical empty string array This closes [ARROW-1992](https://issues.apache.org/jira/browse/ARROW-1992). Author: Licht-T Closes #1508 from Licht-T/fix-segfault-when-string_to_categorical-empty-string-array and squashes the following commits: afea4be [Licht-T] BUG: Fix segfault when to_pandas the empty string array with string_to_categorical=True f90e7b8 [Licht-T] TST: Add test for to_pandas the empty string array with string_to_categorical=True --- cpp/src/arrow/compute/kernels/hash.cc | 8 +++++++- python/pyarrow/tests/test_convert_pandas.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cpp/src/arrow/compute/kernels/hash.cc b/cpp/src/arrow/compute/kernels/hash.cc index 8fac7965d0ed9..acbf403987b40 100644 --- a/cpp/src/arrow/compute/kernels/hash.cc +++ b/cpp/src/arrow/compute/kernels/hash.cc @@ -407,12 +407,18 @@ class HashTableKernel> : public HashTable { } Status Append(const ArrayData& arr) override { + constexpr uint8_t empty_value = 0; if (!initialized_) { RETURN_NOT_OK(Init()); } const int32_t* offsets = GetValues(arr, 1); - const uint8_t* data = GetValues(arr, 2); + const uint8_t* data; + if (arr.buffers[2].get() == nullptr) { + data = &empty_value; + } else { + data = GetValues(arr, 2); + } auto action = static_cast(this); RETURN_NOT_OK(action->Reserve(arr.length)); diff --git a/python/pyarrow/tests/test_convert_pandas.py b/python/pyarrow/tests/test_convert_pandas.py index 5acb9c3dbe9a1..fa265e55cfd76 100644 --- a/python/pyarrow/tests/test_convert_pandas.py +++ b/python/pyarrow/tests/test_convert_pandas.py @@ -1237,6 +1237,21 @@ def test_decimal_metadata(self): assert data_column['numpy_type'] == 'object' assert data_column['metadata'] == {'precision': 26, 'scale': 11} + def test_table_empty_str(self): + values = ['', '', '', '', ''] + df = pd.DataFrame({'strings': values}) + field = pa.field('strings', pa.string()) + schema = pa.schema([field]) + table = pa.Table.from_pandas(df, schema=schema) + + result1 = table.to_pandas(strings_to_categorical=False) + expected1 = pd.DataFrame({'strings': values}) + tm.assert_frame_equal(result1, expected1, check_dtype=True) + + result2 = table.to_pandas(strings_to_categorical=True) + expected2 = pd.DataFrame({'strings': pd.Categorical(values)}) + tm.assert_frame_equal(result2, expected2, check_dtype=True) + def test_table_str_to_categorical_without_na(self): values = ['a', 'a', 'b', 'b', 'c'] df = pd.DataFrame({'strings': values}) From 0621765defc022e0fae68f4cac52835699b98500 Mon Sep 17 00:00:00 2001 From: "Uwe L. Korn" Date: Tue, 30 Jan 2018 10:31:20 +0100 Subject: [PATCH 32/46] ARROW-2048: [Python/C++] Upate Thrift pin to 0.11 Author: Uwe L. Korn Closes #1528 from xhochy/ARROW-2048 and squashes the following commits: aa0212a [Uwe L. Korn] ARROW-2048: [Python/C++] Upate Thrift pin to 0.11 --- ci/msvc-build.bat | 2 +- ci/travis_before_script_cpp.sh | 2 +- cpp/src/arrow/stl.h | 1 + python/manylinux1/Dockerfile-x86_64 | 2 +- python/manylinux1/Dockerfile-x86_64_base | 3 +++ python/manylinux1/scripts/build_bison.sh | 26 +++++++++++++++++++++++ python/manylinux1/scripts/build_thrift.sh | 2 +- 7 files changed, 34 insertions(+), 4 deletions(-) create mode 100755 python/manylinux1/scripts/build_bison.sh diff --git a/ci/msvc-build.bat b/ci/msvc-build.bat index 94eb16a5e506b..9651772ca3fe9 100644 --- a/ci/msvc-build.bat +++ b/ci/msvc-build.bat @@ -81,7 +81,7 @@ conda info -a conda create -n arrow -q -y python=%PYTHON% ^ six pytest setuptools numpy pandas cython ^ - thrift-cpp=0.10.0 + thrift-cpp=0.11.0 if "%JOB%" == "Toolchain" ( diff --git a/ci/travis_before_script_cpp.sh b/ci/travis_before_script_cpp.sh index 2f164c4168d0d..7c1d726d4d37e 100755 --- a/ci/travis_before_script_cpp.sh +++ b/ci/travis_before_script_cpp.sh @@ -47,7 +47,7 @@ if [ "$ARROW_TRAVIS_USE_TOOLCHAIN" == "1" ]; then zlib \ cmake \ curl \ - thrift-cpp=0.10.0 \ + thrift-cpp=0.11.0 \ ninja # HACK(wesm): We started experiencing OpenSSL failures when Miniconda was diff --git a/cpp/src/arrow/stl.h b/cpp/src/arrow/stl.h index 3250b5a320464..1e31ca769ae0b 100644 --- a/cpp/src/arrow/stl.h +++ b/cpp/src/arrow/stl.h @@ -18,6 +18,7 @@ #ifndef ARROW_STL_H #define ARROW_STL_H +#include #include #include #include diff --git a/python/manylinux1/Dockerfile-x86_64 b/python/manylinux1/Dockerfile-x86_64 index 919a32be715b0..9c00e7ea256c9 100644 --- a/python/manylinux1/Dockerfile-x86_64 +++ b/python/manylinux1/Dockerfile-x86_64 @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -FROM quay.io/xhochy/arrow_manylinux1_x86_64_base:latest +FROM quay.io/xhochy/arrow_manylinux1_x86_64_base:ARROW-2048 ADD arrow /arrow WORKDIR /arrow/cpp diff --git a/python/manylinux1/Dockerfile-x86_64_base b/python/manylinux1/Dockerfile-x86_64_base index 0160aa4eea509..ec7893080f65b 100644 --- a/python/manylinux1/Dockerfile-x86_64_base +++ b/python/manylinux1/Dockerfile-x86_64_base @@ -42,6 +42,9 @@ ADD scripts/build_flatbuffers.sh / RUN /build_flatbuffers.sh ENV FLATBUFFERS_HOME /usr +ADD scripts/build_bison.sh / +RUN /build_bison.sh + ADD scripts/build_thrift.sh / RUN /build_thrift.sh ENV THRIFT_HOME /usr diff --git a/python/manylinux1/scripts/build_bison.sh b/python/manylinux1/scripts/build_bison.sh new file mode 100755 index 0000000000000..29cc0be6adf6c --- /dev/null +++ b/python/manylinux1/scripts/build_bison.sh @@ -0,0 +1,26 @@ +#!/bin/bash -ex +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +wget http://ftp.gnu.org/gnu/bison/bison-3.0.4.tar.gz +tar xf bison-3.0.4.tar.gz +pushd bison-3.0.4 +./configure --prefix=/usr +make -j4 +make install +popd +rm -rf bison-3.0.4 bison-3.0.4.tar.gz diff --git a/python/manylinux1/scripts/build_thrift.sh b/python/manylinux1/scripts/build_thrift.sh index 28aa75b7413de..aaec4ad6bad41 100755 --- a/python/manylinux1/scripts/build_thrift.sh +++ b/python/manylinux1/scripts/build_thrift.sh @@ -16,7 +16,7 @@ # specific language governing permissions and limitations # under the License. -export THRIFT_VERSION=0.10.0 +export THRIFT_VERSION=0.11.0 wget http://archive.apache.org/dist/thrift/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}.tar.gz tar xf thrift-${THRIFT_VERSION}.tar.gz pushd thrift-${THRIFT_VERSION} From 40dd9cc25a46aa56a5d852fbc8ebdbc55b5fe8d6 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Tue, 30 Jan 2018 10:16:51 -0500 Subject: [PATCH 33/46] ARROW-2033: [Python] Fix pa.array() with iterator input Iterator (not iterable) input was broken with pa.array() unless both type and size were explicitly passed. Author: Wes McKinney Author: Antoine Pitrou Closes #1513 from pitrou/ARROW-2033-pa-array-iterator and squashes the following commits: 0013889a [Wes McKinney] Code review comments dc95be29 [Antoine Pitrou] Fix pyarrow.array with iterator input Change-Id: I930c3309e3fde12e65ede066b47985f67f7f4037 --- cpp/src/arrow/python/builtin_convert.cc | 174 ++++++++++--------- cpp/src/arrow/python/builtin_convert.h | 18 +- python/pyarrow/array.pxi | 15 +- python/pyarrow/includes/libarrow.pxd | 13 +- python/pyarrow/tests/test_convert_builtin.py | 19 ++ 5 files changed, 141 insertions(+), 98 deletions(-) diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc index f7a370cdc4c06..b41c55d9c3773 100644 --- a/cpp/src/arrow/python/builtin_convert.cc +++ b/cpp/src/arrow/python/builtin_convert.cc @@ -172,38 +172,26 @@ class SeqVisitor { Status Visit(PyObject* obj, int level = 0) { max_nesting_level_ = std::max(max_nesting_level_, level); - // Loop through either a sequence or an iterator. - if (PySequence_Check(obj)) { - Py_ssize_t size = PySequence_Size(obj); - for (int64_t i = 0; i < size; ++i) { - OwnedRef ref; - if (PyArray_Check(obj)) { - auto array = reinterpret_cast(obj); - auto ptr = reinterpret_cast(PyArray_GETPTR1(array, i)); - - ref.reset(PyArray_GETITEM(array, ptr)); - RETURN_IF_PYERROR(); + // Loop through a sequence + if (!PySequence_Check(obj)) + return Status::TypeError("Object is not a sequence or iterable"); - RETURN_NOT_OK(VisitElem(ref, level)); - } else { - ref.reset(PySequence_GetItem(obj, i)); - RETURN_IF_PYERROR(); - RETURN_NOT_OK(VisitElem(ref, level)); - } - } - } else if (PyObject_HasAttrString(obj, "__iter__")) { - OwnedRef iter(PyObject_GetIter(obj)); - RETURN_IF_PYERROR(); + Py_ssize_t size = PySequence_Size(obj); + for (int64_t i = 0; i < size; ++i) { + OwnedRef ref; + if (PyArray_Check(obj)) { + auto array = reinterpret_cast(obj); + auto ptr = reinterpret_cast(PyArray_GETPTR1(array, i)); - PyObject* item = NULLPTR; - while ((item = PyIter_Next(iter.obj()))) { + ref.reset(PyArray_GETITEM(array, ptr)); RETURN_IF_PYERROR(); - OwnedRef ref(item); + RETURN_NOT_OK(VisitElem(ref, level)); + } else { + ref.reset(PySequence_GetItem(obj, i)); + RETURN_IF_PYERROR(); RETURN_NOT_OK(VisitElem(ref, level)); } - } else { - return Status::TypeError("Object is not a sequence or iterable"); } return Status::OK(); } @@ -285,25 +273,45 @@ class SeqVisitor { } }; -Status InferArrowSize(PyObject* obj, int64_t* size) { +// Convert *obj* to a sequence if necessary +// Fill *size* to its length. If >= 0 on entry, *size* is an upper size +// bound that may lead to truncation. +Status ConvertToSequenceAndInferSize(PyObject* obj, PyObject** seq, int64_t* size) { if (PySequence_Check(obj)) { - *size = static_cast(PySequence_Size(obj)); - } else if (PyObject_HasAttrString(obj, "__iter__")) { + // obj is already a sequence + int64_t real_size = static_cast(PySequence_Size(obj)); + if (*size < 0) { + *size = real_size; + } else { + *size = std::min(real_size, *size); + } + Py_INCREF(obj); + *seq = obj; + } else if (*size < 0) { + // unknown size, exhaust iterator + *seq = PySequence_List(obj); + RETURN_IF_PYERROR(); + *size = static_cast(PyList_GET_SIZE(*seq)); + } else { + // size is known but iterator could be infinite + Py_ssize_t i, n = *size; PyObject* iter = PyObject_GetIter(obj); + RETURN_IF_PYERROR(); OwnedRef iter_ref(iter); - *size = 0; - PyObject* item; - while ((item = PyIter_Next(iter))) { - OwnedRef item_ref(item); - *size += 1; + PyObject* lst = PyList_New(n); + RETURN_IF_PYERROR(); + for (i = 0; i < n; i++) { + PyObject* item = PyIter_Next(iter); + if (!item) break; + PyList_SET_ITEM(lst, i, item); } - } else { - return Status::TypeError("Object is not a sequence or iterable"); - } - if (PyErr_Occurred()) { - // Not a sequence - PyErr_Clear(); - return Status::TypeError("Object is not a sequence or iterable"); + // Shrink list if len(iterator) < size + if (i < n && PyList_SetSlice(lst, i, n, NULL)) { + Py_DECREF(lst); + return Status::UnknownError("failed to resize list"); + } + *seq = lst; + *size = std::min(i, *size); } return Status::OK(); } @@ -325,7 +333,10 @@ Status InferArrowType(PyObject* obj, std::shared_ptr* out_type) { Status InferArrowTypeAndSize(PyObject* obj, int64_t* size, std::shared_ptr* out_type) { - RETURN_NOT_OK(InferArrowSize(obj, size)); + if (!PySequence_Check(obj)) { + return Status::TypeError("Object is not a sequence"); + } + *size = static_cast(PySequence_Size(obj)); // For 0-length sequences, refuse to guess if (*size == 0) { @@ -382,27 +393,8 @@ class TypedConverterVisitor : public TypedConverter { RETURN_NOT_OK(static_cast(this)->AppendItem(ref)); } } - } else if (PyObject_HasAttrString(obj, "__iter__")) { - PyObject* iter = PyObject_GetIter(obj); - OwnedRef iter_ref(iter); - PyObject* item; - int64_t i = 0; - // To allow people with long generators to only convert a subset, stop - // consuming at size. - while ((item = PyIter_Next(iter)) && i < size) { - OwnedRef ref(item); - if (ref.obj() == Py_None) { - RETURN_NOT_OK(this->typed_builder_->AppendNull()); - } else { - RETURN_NOT_OK(static_cast(this)->AppendItem(ref)); - } - ++i; - } - if (size != i) { - RETURN_NOT_OK(this->typed_builder_->Resize(i)); - } } else { - return Status::TypeError("Object is not a sequence or iterable"); + return Status::TypeError("Object is not a sequence"); } return Status::OK(); } @@ -830,38 +822,56 @@ Status AppendPySequence(PyObject* obj, int64_t size, return converter->AppendData(obj, size); } -Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out) { +static Status ConvertPySequenceReal(PyObject* obj, int64_t size, + const std::shared_ptr* type, + MemoryPool* pool, std::shared_ptr* out) { PyAcquireGIL lock; - std::shared_ptr type; - int64_t size; - RETURN_NOT_OK(InferArrowTypeAndSize(obj, &size, &type)); - return ConvertPySequence(obj, pool, out, type, size); -} -Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out, - const std::shared_ptr& type, int64_t size) { - PyAcquireGIL lock; + PyObject* seq; + ScopedRef tmp_seq_nanny; + + std::shared_ptr real_type; + + RETURN_NOT_OK(ConvertToSequenceAndInferSize(obj, &seq, &size)); + tmp_seq_nanny.reset(seq); + if (type == nullptr) { + RETURN_NOT_OK(InferArrowType(seq, &real_type)); + } else { + real_type = *type; + } + DCHECK_GE(size, 0); + // Handle NA / NullType case - if (type->id() == Type::NA) { + if (real_type->id() == Type::NA) { out->reset(new NullArray(size)); return Status::OK(); } // Give the sequence converter an array builder std::unique_ptr builder; - RETURN_NOT_OK(MakeBuilder(pool, type, &builder)); - RETURN_NOT_OK(AppendPySequence(obj, size, type, builder.get())); + RETURN_NOT_OK(MakeBuilder(pool, real_type, &builder)); + RETURN_NOT_OK(AppendPySequence(seq, size, real_type, builder.get())); return builder->Finish(out); } -Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out, - const std::shared_ptr& type) { - int64_t size; - { - PyAcquireGIL lock; - RETURN_NOT_OK(InferArrowSize(obj, &size)); - } - return ConvertPySequence(obj, pool, out, type, size); +Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out) { + return ConvertPySequenceReal(obj, -1, nullptr, pool, out); +} + +Status ConvertPySequence(PyObject* obj, const std::shared_ptr& type, + MemoryPool* pool, std::shared_ptr* out) { + return ConvertPySequenceReal(obj, -1, &type, pool, out); +} + +Status ConvertPySequence(PyObject* obj, int64_t size, MemoryPool* pool, + std::shared_ptr* out) { + return ConvertPySequenceReal(obj, size, nullptr, pool, out); +} + +Status ConvertPySequence(PyObject* obj, int64_t size, + const std::shared_ptr& type, MemoryPool* pool, + std::shared_ptr* out) { + return ConvertPySequenceReal(obj, size, &type, pool, out); } Status CheckPythonBytesAreFixedLength(PyObject* obj, Py_ssize_t expected_length) { diff --git a/cpp/src/arrow/python/builtin_convert.h b/cpp/src/arrow/python/builtin_convert.h index cde7a1bd4cfdc..4bd3f08edf162 100644 --- a/cpp/src/arrow/python/builtin_convert.h +++ b/cpp/src/arrow/python/builtin_convert.h @@ -39,11 +39,11 @@ class Status; namespace py { +// These three functions take a sequence input, not arbitrary iterables ARROW_EXPORT arrow::Status InferArrowType(PyObject* obj, std::shared_ptr* out_type); ARROW_EXPORT arrow::Status InferArrowTypeAndSize( PyObject* obj, int64_t* size, std::shared_ptr* out_type); -ARROW_EXPORT arrow::Status InferArrowSize(PyObject* obj, int64_t* size); ARROW_EXPORT arrow::Status AppendPySequence(PyObject* obj, int64_t size, const std::shared_ptr& type, @@ -53,15 +53,21 @@ ARROW_EXPORT arrow::Status AppendPySequence(PyObject* obj, int64_t size, ARROW_EXPORT Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out); -// Size inference +// Type inference only ARROW_EXPORT -Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out, - const std::shared_ptr& type); +Status ConvertPySequence(PyObject* obj, int64_t size, MemoryPool* pool, + std::shared_ptr* out); + +// Size inference only +ARROW_EXPORT +Status ConvertPySequence(PyObject* obj, const std::shared_ptr& type, + MemoryPool* pool, std::shared_ptr* out); // No inference ARROW_EXPORT -Status ConvertPySequence(PyObject* obj, MemoryPool* pool, std::shared_ptr* out, - const std::shared_ptr& type, int64_t size); +Status ConvertPySequence(PyObject* obj, int64_t size, + const std::shared_ptr& type, MemoryPool* pool, + std::shared_ptr* out); ARROW_EXPORT Status InvalidConversion(PyObject* obj, const std::string& expected_type_name, diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi index cca9425881b00..caeefd2ff4f6a 100644 --- a/python/pyarrow/array.pxi +++ b/python/pyarrow/array.pxi @@ -21,14 +21,21 @@ cdef _sequence_to_array(object sequence, object size, DataType type, cdef shared_ptr[CArray] out cdef int64_t c_size if type is None: - with nogil: - check_status(ConvertPySequence(sequence, pool, &out)) + if size is None: + with nogil: + check_status(ConvertPySequence(sequence, pool, &out)) + else: + c_size = size + with nogil: + check_status( + ConvertPySequence(sequence, c_size, pool, &out) + ) else: if size is None: with nogil: check_status( ConvertPySequence( - sequence, pool, &out, type.sp_type + sequence, type.sp_type, pool, &out, ) ) else: @@ -36,7 +43,7 @@ cdef _sequence_to_array(object sequence, object size, DataType type, with nogil: check_status( ConvertPySequence( - sequence, pool, &out, type.sp_type, c_size + sequence, c_size, type.sp_type, pool, &out, ) ) diff --git a/python/pyarrow/includes/libarrow.pxd b/python/pyarrow/includes/libarrow.pxd index 91bc96dc63f89..2e83f0701ce2e 100644 --- a/python/pyarrow/includes/libarrow.pxd +++ b/python/pyarrow/includes/libarrow.pxd @@ -852,13 +852,14 @@ cdef extern from "arrow/python/api.h" namespace "arrow::py" nogil: shared_ptr[CDataType] GetTimestampType(TimeUnit unit) CStatus ConvertPySequence(object obj, CMemoryPool* pool, shared_ptr[CArray]* out) - CStatus ConvertPySequence(object obj, CMemoryPool* pool, - shared_ptr[CArray]* out, - const shared_ptr[CDataType]& type) - CStatus ConvertPySequence(object obj, CMemoryPool* pool, - shared_ptr[CArray]* out, + CStatus ConvertPySequence(object obj, const shared_ptr[CDataType]& type, + CMemoryPool* pool, shared_ptr[CArray]* out) + CStatus ConvertPySequence(object obj, int64_t size, CMemoryPool* pool, + shared_ptr[CArray]* out) + CStatus ConvertPySequence(object obj, int64_t size, const shared_ptr[CDataType]& type, - int64_t size) + CMemoryPool* pool, + shared_ptr[CArray]* out) CStatus NumPyDtypeToArrow(object dtype, shared_ptr[CDataType]* type) diff --git a/python/pyarrow/tests/test_convert_builtin.py b/python/pyarrow/tests/test_convert_builtin.py index fa603b1a92fa2..2b317dfbc3fee 100644 --- a/python/pyarrow/tests/test_convert_builtin.py +++ b/python/pyarrow/tests/test_convert_builtin.py @@ -23,6 +23,7 @@ import datetime import decimal +import itertools import numpy as np import six @@ -68,6 +69,24 @@ def test_limited_iterator_size_underflow(): assert arr1.equals(arr2) +def test_iterator_without_size(): + expected = pa.array((0, 1, 2)) + arr1 = pa.array(iter(range(3))) + assert arr1.equals(expected) + # Same with explicit type + arr1 = pa.array(iter(range(3)), type=pa.int64()) + assert arr1.equals(expected) + + +def test_infinite_iterator(): + expected = pa.array((0, 1, 2)) + arr1 = pa.array(itertools.count(0), size=3) + assert arr1.equals(expected) + # Same with explicit type + arr1 = pa.array(itertools.count(0), type=pa.int64(), size=3) + assert arr1.equals(expected) + + def _as_list(xs): return xs From 673125fd416cbd2e5c2cb9cb6a4c925adecdaf2c Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Tue, 30 Jan 2018 11:29:37 -0500 Subject: [PATCH 34/46] ARROW-2054: [C++] Fix compilation warnings One of them is a pointer aliasing offense (thus a real bug), the other ones could merely be ignored. Author: Antoine Pitrou Closes #1533 from pitrou/ARROW-2054-cpp-warnings and squashes the following commits: 2c0d17eb [Antoine Pitrou] ARROW-2054: [C++] Fix compilation warnings --- cpp/src/arrow/ipc/json-internal.cc | 4 ++-- cpp/src/arrow/python/builtin_convert.cc | 2 ++ cpp/src/arrow/python/io.cc | 16 ++++++++++------ cpp/src/plasma/fling.cc | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cpp/src/arrow/ipc/json-internal.cc b/cpp/src/arrow/ipc/json-internal.cc index 4088a8f20e6a0..204bbc40b0072 100644 --- a/cpp/src/arrow/ipc/json-internal.cc +++ b/cpp/src/arrow/ipc/json-internal.cc @@ -866,7 +866,7 @@ static Status GetField(const rj::Value& obj, const DictionaryMemo* dictionary_me if (dictionary_memo != nullptr && it_dictionary != json_field.MemberEnd()) { // Field is dictionary encoded. We must have already RETURN_NOT_OBJECT("dictionary", it_dictionary, json_field); - int64_t dictionary_id; + int64_t dictionary_id = -1; bool is_ordered; std::shared_ptr index_type; RETURN_NOT_OK(ParseDictionary(it_dictionary->value.GetObject(), &dictionary_id, @@ -1346,7 +1346,7 @@ static Status ReadDictionaries(const rj::Value& doc, const DictionaryTypeMap& id for (const rj::Value& val : dictionary_array) { DCHECK(val.IsObject()); - int64_t dictionary_id; + int64_t dictionary_id = -1; std::shared_ptr dictionary; RETURN_NOT_OK( ReadDictionary(val.GetObject(), id_to_field, pool, &dictionary_id, &dictionary)); diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc index b41c55d9c3773..63d388925c711 100644 --- a/cpp/src/arrow/python/builtin_convert.cc +++ b/cpp/src/arrow/python/builtin_convert.cc @@ -582,6 +582,8 @@ class TimestampConverter case TimeUnit::NANO: t = PyDateTime_to_ns(pydatetime); break; + default: + return Status::UnknownError("Invalid time unit"); } } else if (PyArray_CheckAnyScalarExact(item.obj())) { // numpy.datetime64 diff --git a/cpp/src/arrow/python/io.cc b/cpp/src/arrow/python/io.cc index 9d32ead524b93..2cff046085e69 100644 --- a/cpp/src/arrow/python/io.cc +++ b/cpp/src/arrow/python/io.cc @@ -26,6 +26,7 @@ #include "arrow/io/memory.h" #include "arrow/memory_pool.h" #include "arrow/status.h" +#include "arrow/util/logging.h" #include "arrow/python/common.h" @@ -133,12 +134,14 @@ Status PyReadableFile::Tell(int64_t* position) const { Status PyReadableFile::Read(int64_t nbytes, int64_t* bytes_read, void* out) { PyAcquireGIL lock; - PyObject* bytes_obj; + + PyObject* bytes_obj = NULL; ARROW_RETURN_NOT_OK(file_->Read(nbytes, &bytes_obj)); + DCHECK(bytes_obj != NULL); *bytes_read = PyBytes_GET_SIZE(bytes_obj); std::memcpy(out, PyBytes_AS_STRING(bytes_obj), *bytes_read); - Py_DECREF(bytes_obj); + Py_XDECREF(bytes_obj); return Status::OK(); } @@ -146,11 +149,12 @@ Status PyReadableFile::Read(int64_t nbytes, int64_t* bytes_read, void* out) { Status PyReadableFile::Read(int64_t nbytes, std::shared_ptr* out) { PyAcquireGIL lock; - PyObject* bytes_obj; + PyObject* bytes_obj = NULL; ARROW_RETURN_NOT_OK(file_->Read(nbytes, &bytes_obj)); + DCHECK(bytes_obj != NULL); *out = std::make_shared(bytes_obj); - Py_DECREF(bytes_obj); + Py_XDECREF(bytes_obj); return Status::OK(); } @@ -172,13 +176,13 @@ Status PyReadableFile::ReadAt(int64_t position, int64_t nbytes, Status PyReadableFile::GetSize(int64_t* size) { PyAcquireGIL lock; - int64_t current_position; + int64_t current_position = -1; ARROW_RETURN_NOT_OK(file_->Tell(¤t_position)); ARROW_RETURN_NOT_OK(file_->Seek(0, 2)); - int64_t file_size; + int64_t file_size = -1; ARROW_RETURN_NOT_OK(file_->Tell(&file_size)); // Restore previous file position diff --git a/cpp/src/plasma/fling.cc b/cpp/src/plasma/fling.cc index 819ec1623055b..26afd87066c2b 100644 --- a/cpp/src/plasma/fling.cc +++ b/cpp/src/plasma/fling.cc @@ -43,7 +43,7 @@ int send_fd(int conn, int fd) { header->cmsg_level = SOL_SOCKET; header->cmsg_type = SCM_RIGHTS; header->cmsg_len = CMSG_LEN(sizeof(int)); - *reinterpret_cast(CMSG_DATA(header)) = fd; + memcpy(CMSG_DATA(header), reinterpret_cast(&fd), sizeof(int)); // Send file descriptor. ssize_t r = sendmsg(conn, &msg, 0); From 8d78376979965a203207899084820631394de017 Mon Sep 17 00:00:00 2001 From: moriyoshi Date: Tue, 30 Jan 2018 11:42:57 -0500 Subject: [PATCH 35/46] ARROW-2047: [Python] Use sys.executable instead of one in the search path. * It currently relies on the behavior of `subprocess.check_call()` that searches for the executable in the search path. Because of it, the test run needs a valid search path setup. Author: moriyoshi Closes #1525 from moriyoshi/moriyoshi/use-sys-executable and squashes the following commits: f4c92227 [moriyoshi] Use sys.executable instead of one in path --- python/pyarrow/tests/test_serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pyarrow/tests/test_serialization.py b/python/pyarrow/tests/test_serialization.py index e4681e3a59751..9cad81fc98f1e 100644 --- a/python/pyarrow/tests/test_serialization.py +++ b/python/pyarrow/tests/test_serialization.py @@ -554,7 +554,7 @@ def test_deserialize_buffer_in_different_process(): dir_path = os.path.dirname(os.path.realpath(__file__)) python_file = os.path.join(dir_path, 'deserialize_buffer.py') - subprocess.check_call(['python', python_file, f.name]) + subprocess.check_call([sys.executable, python_file, f.name]) def test_set_pickle(): From 5c704bce42e3fa71ea4586368962d41173b3e17b Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Tue, 30 Jan 2018 15:05:14 -0500 Subject: [PATCH 36/46] ARROW-1705: [Python] allow building array from dicts Accept passing a list of dicts to pa.array() if a struct type is given. Based on PR #1513. Author: Antoine Pitrou Closes #1530 from pitrou/ARROW-1705-struct-array-from-dicts and squashes the following commits: 2b9133af [Antoine Pitrou] ARROW-1705: [Python] allow building array from dicts --- cpp/src/arrow/python/builtin_convert.cc | 289 +++++++++++++------ python/pyarrow/tests/test_convert_builtin.py | 25 ++ 2 files changed, 220 insertions(+), 94 deletions(-) diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc index 63d388925c711..1b3c101758eec 100644 --- a/cpp/src/arrow/python/builtin_convert.cc +++ b/cpp/src/arrow/python/builtin_convert.cc @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include "arrow/python/builtin_convert.h" @@ -356,7 +358,11 @@ class SeqConverter { return Status::OK(); } - virtual Status AppendData(PyObject* seq, int64_t size) = 0; + // Append a single (non-sequence) Python datum to the underlying builder + virtual Status AppendSingle(PyObject* obj) = 0; + + // Append the contents of a Python sequence to the underlying builder + virtual Status AppendMultiple(PyObject* seq, int64_t size) = 0; virtual ~SeqConverter() = default; @@ -377,47 +383,57 @@ class TypedConverter : public SeqConverter { BuilderType* typed_builder_; }; +// We use the CRTP trick here to devirtualize the AppendItem() and AppendNull() +// method calls. template class TypedConverterVisitor : public TypedConverter { public: - Status AppendData(PyObject* obj, int64_t size) override { + Status AppendSingle(PyObject* obj) override { + if (obj == Py_None) { + return static_cast(this)->AppendNull(); + } else { + return static_cast(this)->AppendItem(obj); + } + } + + Status AppendMultiple(PyObject* obj, int64_t size) override { /// Ensure we've allocated enough space RETURN_NOT_OK(this->typed_builder_->Reserve(size)); // Iterate over the items adding each one if (PySequence_Check(obj)) { for (int64_t i = 0; i < size; ++i) { OwnedRef ref(PySequence_GetItem(obj, i)); - if (ref.obj() == Py_None) { - RETURN_NOT_OK(this->typed_builder_->AppendNull()); - } else { - RETURN_NOT_OK(static_cast(this)->AppendItem(ref)); - } + RETURN_NOT_OK(static_cast(this)->AppendSingle(ref.obj())); } } else { return Status::TypeError("Object is not a sequence"); } return Status::OK(); } + + // Append a missing item (default implementation) + Status AppendNull() { return this->typed_builder_->AppendNull(); } }; class NullConverter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { return Status::Invalid("NullConverter: passed non-None value"); } }; class BoolConverter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - return typed_builder_->Append(item.obj() == Py_True); - } + // Append a non-missing item + Status AppendItem(PyObject* obj) { return typed_builder_->Append(obj == Py_True); } }; class Int8Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); if (ARROW_PREDICT_FALSE(val > std::numeric_limits::max() || val < std::numeric_limits::min())) { @@ -432,8 +448,9 @@ class Int8Converter : public TypedConverterVisitor { class Int16Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); if (ARROW_PREDICT_FALSE(val > std::numeric_limits::max() || val < std::numeric_limits::min())) { @@ -448,8 +465,9 @@ class Int16Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); if (ARROW_PREDICT_FALSE(val > std::numeric_limits::max() || val < std::numeric_limits::min())) { @@ -464,8 +482,9 @@ class Int32Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); return typed_builder_->Append(val); } @@ -473,8 +492,9 @@ class Int64Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); if (ARROW_PREDICT_FALSE(val > std::numeric_limits::max())) { @@ -488,8 +508,9 @@ class UInt8Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); if (ARROW_PREDICT_FALSE(val > std::numeric_limits::max())) { @@ -503,8 +524,9 @@ class UInt16Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); if (ARROW_PREDICT_FALSE(val > std::numeric_limits::max())) { @@ -518,8 +540,9 @@ class UInt32Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - const auto val = static_cast(PyLong_AsUnsignedLongLong(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + const auto val = static_cast(PyLong_AsUnsignedLongLong(obj)); RETURN_IF_PYERROR(); return typed_builder_->Append(val); } @@ -527,13 +550,14 @@ class UInt64Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { int32_t t; - if (PyDate_Check(item.obj())) { - auto pydate = reinterpret_cast(item.obj()); + if (PyDate_Check(obj)) { + auto pydate = reinterpret_cast(obj); t = static_cast(PyDate_to_s(pydate)); } else { - const auto casted_val = static_cast(PyLong_AsLongLong(item.obj())); + const auto casted_val = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); if (casted_val > std::numeric_limits::max()) { return Status::Invalid("Integer as date32 larger than INT32_MAX"); @@ -546,13 +570,14 @@ class Date32Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { int64_t t; - if (PyDate_Check(item.obj())) { - auto pydate = reinterpret_cast(item.obj()); + if (PyDate_Check(obj)) { + auto pydate = reinterpret_cast(obj); t = PyDate_to_ms(pydate); } else { - t = static_cast(PyLong_AsLongLong(item.obj())); + t = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); } return typed_builder_->Append(t); @@ -564,10 +589,11 @@ class TimestampConverter public: explicit TimestampConverter(TimeUnit::type unit) : unit_(unit) {} - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { int64_t t; - if (PyDateTime_Check(item.obj())) { - auto pydatetime = reinterpret_cast(item.obj()); + if (PyDateTime_Check(obj)) { + auto pydatetime = reinterpret_cast(obj); switch (unit_) { case TimeUnit::SECOND: @@ -585,10 +611,10 @@ class TimestampConverter default: return Status::UnknownError("Invalid time unit"); } - } else if (PyArray_CheckAnyScalarExact(item.obj())) { + } else if (PyArray_CheckAnyScalarExact(obj)) { // numpy.datetime64 std::shared_ptr type; - RETURN_NOT_OK(NumPyDtypeToArrow(PyArray_DescrFromScalar(item.obj()), &type)); + RETURN_NOT_OK(NumPyDtypeToArrow(PyArray_DescrFromScalar(obj), &type)); if (type->id() != Type::TIMESTAMP) { std::ostringstream ss; ss << "Expected np.datetime64 but got: "; @@ -601,10 +627,9 @@ class TimestampConverter "Cannot convert NumPy datetime64 objects with differing unit"); } - PyDatetimeScalarObject* obj = reinterpret_cast(item.obj()); - t = obj->obval; + t = reinterpret_cast(obj)->obval; } else { - t = static_cast(PyLong_AsLongLong(item.obj())); + t = static_cast(PyLong_AsLongLong(obj)); RETURN_IF_PYERROR(); } return typed_builder_->Append(t); @@ -616,8 +641,9 @@ class TimestampConverter class Float32Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - float val = static_cast(PyFloat_AsDouble(item.obj())); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + float val = static_cast(PyFloat_AsDouble(obj)); RETURN_IF_PYERROR(); return typed_builder_->Append(val); } @@ -625,8 +651,9 @@ class Float32Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { - double val = PyFloat_AsDouble(item.obj()); + // Append a non-missing item + Status AppendItem(PyObject* obj) { + double val = PyFloat_AsDouble(obj); RETURN_IF_PYERROR(); return typed_builder_->Append(val); } @@ -634,22 +661,23 @@ class DoubleConverter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { PyObject* bytes_obj; const char* bytes; Py_ssize_t length; OwnedRef tmp; - if (PyUnicode_Check(item.obj())) { - tmp.reset(PyUnicode_AsUTF8String(item.obj())); + if (PyUnicode_Check(obj)) { + tmp.reset(PyUnicode_AsUTF8String(obj)); RETURN_IF_PYERROR(); bytes_obj = tmp.obj(); - } else if (PyBytes_Check(item.obj())) { - bytes_obj = item.obj(); + } else if (PyBytes_Check(obj)) { + bytes_obj = obj; } else { std::stringstream ss; ss << "Error converting to Binary type: "; - RETURN_NOT_OK(InvalidConversion(item.obj(), "bytes", &ss)); + RETURN_NOT_OK(InvalidConversion(obj, "bytes", &ss)); return Status::Invalid(ss.str()); } // No error checking @@ -662,22 +690,23 @@ class BytesConverter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { PyObject* bytes_obj; OwnedRef tmp; Py_ssize_t expected_length = std::dynamic_pointer_cast(typed_builder_->type()) ->byte_width(); - if (PyUnicode_Check(item.obj())) { - tmp.reset(PyUnicode_AsUTF8String(item.obj())); + if (PyUnicode_Check(obj)) { + tmp.reset(PyUnicode_AsUTF8String(obj)); RETURN_IF_PYERROR(); bytes_obj = tmp.obj(); - } else if (PyBytes_Check(item.obj())) { - bytes_obj = item.obj(); + } else if (PyBytes_Check(obj)) { + bytes_obj = obj; } else { std::stringstream ss; ss << "Error converting to FixedSizeBinary type: "; - RETURN_NOT_OK(InvalidConversion(item.obj(), "bytes", &ss)); + RETURN_NOT_OK(InvalidConversion(obj, "bytes", &ss)); return Status::Invalid(ss.str()); } // No error checking @@ -689,13 +718,13 @@ class FixedWidthBytesConverter class UTF8Converter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { PyObject* bytes_obj; OwnedRef tmp; const char* bytes; Py_ssize_t length; - PyObject* obj = item.obj(); if (PyBytes_Check(obj)) { tmp.reset( PyUnicode_FromStringAndSize(PyBytes_AS_STRING(obj), PyBytes_GET_SIZE(obj))); @@ -724,75 +753,114 @@ class ListConverter : public TypedConverterVisitor { public: Status Init(ArrayBuilder* builder) override; - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { RETURN_NOT_OK(typed_builder_->Append()); - PyObject* item_obj = item.obj(); - const auto list_size = static_cast(PySequence_Size(item_obj)); - return value_converter_->AppendData(item_obj, list_size); + const auto list_size = static_cast(PySequence_Size(obj)); + return value_converter_->AppendMultiple(obj, list_size); } protected: - std::shared_ptr value_converter_; + std::unique_ptr value_converter_; +}; + +class StructConverter : public TypedConverterVisitor { + public: + Status Init(ArrayBuilder* builder) override; + + // Append a non-missing item + Status AppendItem(PyObject* obj) { + RETURN_NOT_OK(typed_builder_->Append()); + if (!PyDict_Check(obj)) { + return Status::TypeError("dict value expected for struct type"); + } + // NOTE we're ignoring any extraneous dict items + for (int i = 0; i < num_fields_; i++) { + PyObject* nameobj = PyList_GET_ITEM(field_name_list_.obj(), i); + PyObject* valueobj = PyDict_GetItem(obj, nameobj); // borrowed + RETURN_IF_PYERROR(); + RETURN_NOT_OK(value_converters_[i]->AppendSingle(valueobj ? valueobj : Py_None)); + } + + return Status::OK(); + } + + // Append a missing item + Status AppendNull() { + RETURN_NOT_OK(typed_builder_->AppendNull()); + // Need to also insert a missing item on all child builders + // (compare with ListConverter) + for (int i = 0; i < num_fields_; i++) { + RETURN_NOT_OK(value_converters_[i]->AppendSingle(Py_None)); + } + return Status::OK(); + } + + protected: + std::vector> value_converters_; + OwnedRef field_name_list_; + int num_fields_; }; class DecimalConverter : public TypedConverterVisitor { public: - Status AppendItem(const OwnedRef& item) { + // Append a non-missing item + Status AppendItem(PyObject* obj) { /// TODO(phillipc): Check for nan? Decimal128 value; const auto& type = static_cast(*typed_builder_->type()); - RETURN_NOT_OK(internal::DecimalFromPythonDecimal(item.obj(), type, &value)); + RETURN_NOT_OK(internal::DecimalFromPythonDecimal(obj, type, &value)); return typed_builder_->Append(value); } }; // Dynamic constructor for sequence converters -std::shared_ptr GetConverter(const std::shared_ptr& type) { +std::unique_ptr GetConverter(const std::shared_ptr& type) { switch (type->id()) { case Type::NA: - return std::make_shared(); + return std::unique_ptr(new NullConverter); case Type::BOOL: - return std::make_shared(); + return std::unique_ptr(new BoolConverter); case Type::INT8: - return std::make_shared(); + return std::unique_ptr(new Int8Converter); case Type::INT16: - return std::make_shared(); + return std::unique_ptr(new Int16Converter); case Type::INT32: - return std::make_shared(); + return std::unique_ptr(new Int32Converter); case Type::INT64: - return std::make_shared(); + return std::unique_ptr(new Int64Converter); case Type::UINT8: - return std::make_shared(); + return std::unique_ptr(new UInt8Converter); case Type::UINT16: - return std::make_shared(); + return std::unique_ptr(new UInt16Converter); case Type::UINT32: - return std::make_shared(); + return std::unique_ptr(new UInt32Converter); case Type::UINT64: - return std::make_shared(); + return std::unique_ptr(new UInt64Converter); case Type::DATE32: - return std::make_shared(); + return std::unique_ptr(new Date32Converter); case Type::DATE64: - return std::make_shared(); + return std::unique_ptr(new Date64Converter); case Type::TIMESTAMP: - return std::make_shared( - static_cast(*type).unit()); + return std::unique_ptr( + new TimestampConverter(static_cast(*type).unit())); case Type::FLOAT: - return std::make_shared(); + return std::unique_ptr(new Float32Converter); case Type::DOUBLE: - return std::make_shared(); + return std::unique_ptr(new DoubleConverter); case Type::BINARY: - return std::make_shared(); + return std::unique_ptr(new BytesConverter); case Type::FIXED_SIZE_BINARY: - return std::make_shared(); + return std::unique_ptr(new FixedWidthBytesConverter); case Type::STRING: - return std::make_shared(); + return std::unique_ptr(new UTF8Converter); case Type::LIST: - return std::make_shared(); - case Type::DECIMAL: { - return std::make_shared(); - } + return std::unique_ptr(new ListConverter); case Type::STRUCT: + return std::unique_ptr(new StructConverter); + case Type::DECIMAL: + return std::unique_ptr(new DecimalConverter); default: return nullptr; } @@ -811,17 +879,50 @@ Status ListConverter::Init(ArrayBuilder* builder) { return value_converter_->Init(typed_builder_->value_builder()); } +Status StructConverter::Init(ArrayBuilder* builder) { + builder_ = builder; + typed_builder_ = static_cast(builder); + StructType* struct_type = static_cast(builder->type().get()); + + num_fields_ = typed_builder_->num_fields(); + DCHECK_EQ(num_fields_, struct_type->num_children()); + + field_name_list_.reset(PyList_New(num_fields_)); + RETURN_IF_PYERROR(); + + // Initialize the child converters and field names + for (int i = 0; i < num_fields_; i++) { + const std::string& field_name(struct_type->child(i)->name()); + std::shared_ptr field_type(struct_type->child(i)->type()); + + auto value_converter = GetConverter(field_type); + if (value_converter == nullptr) { + return Status::NotImplemented("value type not implemented"); + } + RETURN_NOT_OK(value_converter->Init(typed_builder_->field_builder(i))); + value_converters_.push_back(std::move(value_converter)); + + // Store the field name as a PyObject, for dict matching + PyObject* nameobj = + PyUnicode_FromStringAndSize(field_name.c_str(), field_name.size()); + RETURN_IF_PYERROR(); + PyList_SET_ITEM(field_name_list_.obj(), i, nameobj); + } + + return Status::OK(); +} + Status AppendPySequence(PyObject* obj, int64_t size, const std::shared_ptr& type, ArrayBuilder* builder) { PyDateTime_IMPORT; - std::shared_ptr converter = GetConverter(type); + auto converter = GetConverter(type); if (converter == nullptr) { std::stringstream ss; ss << "No type converter implemented for " << type->ToString(); return Status::NotImplemented(ss.str()); } RETURN_NOT_OK(converter->Init(builder)); - return converter->AppendData(obj, size); + return converter->AppendMultiple(obj, size); } static Status ConvertPySequenceReal(PyObject* obj, int64_t size, diff --git a/python/pyarrow/tests/test_convert_builtin.py b/python/pyarrow/tests/test_convert_builtin.py index 2b317dfbc3fee..bbdf6e71e0f1d 100644 --- a/python/pyarrow/tests/test_convert_builtin.py +++ b/python/pyarrow/tests/test_convert_builtin.py @@ -504,3 +504,28 @@ def test_structarray(): pylist = arr.to_pylist() assert pylist == expected, (pylist, expected) + + +def test_struct_from_dicts(): + ty = pa.struct([pa.field('a', pa.int32()), + pa.field('b', pa.string()), + pa.field('c', pa.bool_())]) + arr = pa.array([], type=ty) + assert arr.to_pylist() == [] + + data = [{'a': 5, 'b': 'foo', 'c': True}, + {'a': 6, 'b': 'bar', 'c': False}] + arr = pa.array(data, type=ty) + assert arr.to_pylist() == data + + # With omitted values + data = [{'a': 5, 'c': True}, + None, + {}, + {'a': None, 'b': 'bar'}] + arr = pa.array(data, type=ty) + expected = [{'a': 5, 'b': None, 'c': True}, + None, + {'a': None, 'b': None, 'c': None}, + {'a': None, 'b': 'bar', 'c': None}] + assert arr.to_pylist() == expected From 3e63084bee72c097932cc07ca9deae15a2e97fbe Mon Sep 17 00:00:00 2001 From: Jim Crist Date: Tue, 30 Jan 2018 17:20:09 -0500 Subject: [PATCH 37/46] ARROW-2036: [Python] Support standard IOBase methods on NativeFile Adds support for most common file methods, adding enough to use `io.TextIOWrapper`. Added attribtes/methods: - `closed` attribute - `readable`, `writable`, `seekable` methods - `read1` alias for `read` to support `TextIOWrapper` on python 2 - No-op `flush` method Also refactored the cython internals a bit, adding default settings for `is_readable` and `is_writable`, which makes subclasses not need to set them in all places. Also renamed `is_writeable` to `is_writable` for common spelling with the standard python method `writable`. Author: Jim Crist Closes #1517 from jcrist/full-python-file-interface and squashes the following commits: f3bc9546 [Jim Crist] Support standard IOBase methods on NativeFile --- python/pyarrow/io-hdfs.pxi | 7 +- python/pyarrow/io.pxi | 122 ++++++++++++++++++-------------- python/pyarrow/ipc.pxi | 2 +- python/pyarrow/ipc.py | 4 +- python/pyarrow/lib.pxd | 4 +- python/pyarrow/tests/test_io.py | 66 +++++++++++++++-- 6 files changed, 136 insertions(+), 69 deletions(-) diff --git a/python/pyarrow/io-hdfs.pxi b/python/pyarrow/io-hdfs.pxi index 3abf045f93336..83b14b687830d 100644 --- a/python/pyarrow/io-hdfs.pxi +++ b/python/pyarrow/io-hdfs.pxi @@ -413,9 +413,7 @@ cdef class HadoopFileSystem: &wr_handle)) out.wr_file = wr_handle - - out.is_readable = False - out.is_writeable = 1 + out.is_writable = True else: with nogil: check_status(self.client.get() @@ -423,7 +421,6 @@ cdef class HadoopFileSystem: out.rd_file = rd_handle out.is_readable = True - out.is_writeable = 0 if c_buffer_size == 0: c_buffer_size = 2 ** 16 @@ -431,7 +428,7 @@ cdef class HadoopFileSystem: out.mode = mode out.buffer_size = c_buffer_size out.parent = _HdfsFileNanny(self, out) - out.is_open = True + out.closed = False out.own_file = True return out diff --git a/python/pyarrow/io.pxi b/python/pyarrow/io.pxi index bb363bacc2e24..bd508cf57ee8d 100644 --- a/python/pyarrow/io.pxi +++ b/python/pyarrow/io.pxi @@ -39,13 +39,14 @@ cdef extern from "Python.h": cdef class NativeFile: - def __cinit__(self): - self.is_open = False + self.closed = True self.own_file = False + self.is_readable = False + self.is_writable = False def __dealloc__(self): - if self.is_open and self.own_file: + if self.own_file and not self.closed: self.close() def __enter__(self): @@ -65,34 +66,52 @@ cdef class NativeFile: def __get__(self): # Emulate built-in file modes - if self.is_readable and self.is_writeable: + if self.is_readable and self.is_writable: return 'rb+' elif self.is_readable: return 'rb' - elif self.is_writeable: + elif self.is_writable: return 'wb' else: raise ValueError('File object is malformed, has no mode') + def readable(self): + self._assert_open() + return self.is_readable + + def writable(self): + self._assert_open() + return self.is_writable + + def seekable(self): + self._assert_open() + return self.is_readable + def close(self): - if self.is_open: + if not self.closed: with nogil: if self.is_readable: check_status(self.rd_file.get().Close()) else: check_status(self.wr_file.get().Close()) - self.is_open = False + self.closed = True + + def flush(self): + """Flush the buffer stream, if applicable. + + No-op to match the IOBase interface.""" + self._assert_open() cdef read_handle(self, shared_ptr[RandomAccessFile]* file): self._assert_readable() file[0] = self.rd_file cdef write_handle(self, shared_ptr[OutputStream]* file): - self._assert_writeable() + self._assert_writable() file[0] = self.wr_file def _assert_open(self): - if not self.is_open: + if self.closed: raise ValueError("I/O operation on closed file") def _assert_readable(self): @@ -100,10 +119,10 @@ cdef class NativeFile: if not self.is_readable: raise IOError("only valid on readonly files") - def _assert_writeable(self): + def _assert_writable(self): self._assert_open() - if not self.is_writeable: - raise IOError("only valid on writeable files") + if not self.is_writable: + raise IOError("only valid on writable files") def size(self): """ @@ -175,7 +194,7 @@ cdef class NativeFile: Write byte from any object implementing buffer protocol (bytes, bytearray, ndarray, pyarrow.Buffer) """ - self._assert_writeable() + self._assert_writable() if isinstance(data, six.string_types): data = tobytes(data) @@ -224,6 +243,12 @@ cdef class NativeFile: return PyObject_to_object(obj) + def read1(self, nbytes=None): + """Read and return up to n bytes. + + Alias for read, needed to match the IOBase interface.""" + return self.read(nbytes=None) + def read_buffer(self, nbytes=None): cdef: int64_t c_nbytes @@ -333,7 +358,7 @@ cdef class NativeFile: Pipe file-like object to file """ write_queue = Queue(50) - self._assert_writeable() + self._assert_writable() buffer_size = buffer_size or DEFAULT_BUFFER_SIZE @@ -390,16 +415,14 @@ cdef class PythonFile(NativeFile): if mode.startswith('w'): self.wr_file.reset(new PyOutputStream(handle)) - self.is_readable = 0 - self.is_writeable = 1 + self.is_writable = True elif mode.startswith('r'): self.rd_file.reset(new PyReadableFile(handle)) - self.is_readable = 1 - self.is_writeable = 0 + self.is_readable = True else: raise ValueError('Invalid file mode: {0}'.format(mode)) - self.is_open = True + self.closed = False cdef class MemoryMappedFile(NativeFile): @@ -409,11 +432,6 @@ cdef class MemoryMappedFile(NativeFile): cdef: object path - def __cinit__(self): - self.is_open = False - self.is_readable = 0 - self.is_writeable = 0 - @staticmethod def create(path, size): cdef: @@ -426,11 +444,11 @@ cdef class MemoryMappedFile(NativeFile): cdef MemoryMappedFile result = MemoryMappedFile() result.path = path - result.is_readable = 1 - result.is_writeable = 1 + result.is_readable = True + result.is_writable = True result.wr_file = handle result.rd_file = handle - result.is_open = True + result.closed = False return result @@ -444,14 +462,14 @@ cdef class MemoryMappedFile(NativeFile): if mode in ('r', 'rb'): c_mode = FileMode_READ - self.is_readable = 1 + self.is_readable = True elif mode in ('w', 'wb'): c_mode = FileMode_WRITE - self.is_writeable = 1 + self.is_writable = True elif mode in ('r+', 'r+b', 'rb+'): c_mode = FileMode_READWRITE - self.is_readable = 1 - self.is_writeable = 1 + self.is_readable = True + self.is_writable = True else: raise ValueError('Invalid file mode: {0}'.format(mode)) @@ -460,7 +478,7 @@ cdef class MemoryMappedFile(NativeFile): self.wr_file = handle self.rd_file = handle - self.is_open = True + self.closed = False def memory_map(path, mode='r'): @@ -484,7 +502,7 @@ def memory_map(path, mode='r'): def create_memory_map(path, size): """ Create memory map at indicated path of the given size, return open - writeable file object + writable file object Parameters ---------- @@ -513,16 +531,14 @@ cdef class OSFile(NativeFile): shared_ptr[Readable] handle c_string c_path = encode_file_path(path) - self.is_readable = self.is_writeable = 0 - if mode in ('r', 'rb'): self._open_readable(c_path, maybe_unbox_memory_pool(memory_pool)) elif mode in ('w', 'wb'): - self._open_writeable(c_path) + self._open_writable(c_path) else: raise ValueError('Invalid file mode: {0}'.format(mode)) - self.is_open = True + self.closed = False cdef _open_readable(self, c_string path, CMemoryPool* pool): cdef shared_ptr[ReadableFile] handle @@ -530,15 +546,15 @@ cdef class OSFile(NativeFile): with nogil: check_status(ReadableFile.Open(path, pool, &handle)) - self.is_readable = 1 + self.is_readable = True self.rd_file = handle - cdef _open_writeable(self, c_string path): + cdef _open_writable(self, c_string path): cdef shared_ptr[FileOutputStream] handle with nogil: check_status(FileOutputStream.Open(path, &handle)) - self.is_writeable = 1 + self.is_writable = True self.wr_file = handle @@ -546,9 +562,8 @@ cdef class FixedSizeBufferWriter(NativeFile): def __cinit__(self, Buffer buffer): self.wr_file.reset(new CFixedSizeBufferWriter(buffer.buffer)) - self.is_readable = 0 - self.is_writeable = 1 - self.is_open = True + self.is_writable = True + self.closed = False def set_memcopy_threads(self, int num_threads): cdef CFixedSizeBufferWriter* writer = \ @@ -738,14 +753,13 @@ cdef class BufferOutputStream(NativeFile): self.buffer = _allocate_buffer(maybe_unbox_memory_pool(memory_pool)) self.wr_file.reset(new CBufferOutputStream( self.buffer)) - self.is_readable = 0 - self.is_writeable = 1 - self.is_open = True + self.is_writable = True + self.closed = False def get_result(self): with nogil: check_status(self.wr_file.get().Close()) - self.is_open = False + self.closed = True return pyarrow_wrap_buffer( self.buffer) @@ -753,9 +767,8 @@ cdef class MockOutputStream(NativeFile): def __cinit__(self): self.wr_file.reset(new CMockOutputStream()) - self.is_readable = 0 - self.is_writeable = 1 - self.is_open = True + self.is_writable = True + self.closed = False def size(self): return (self.wr_file.get()).GetExtentBytesWritten() @@ -780,9 +793,8 @@ cdef class BufferReader(NativeFile): self.buffer = frombuffer(obj) self.rd_file.reset(new CBufferReader(self.buffer.buffer)) - self.is_readable = 1 - self.is_writeable = 0 - self.is_open = True + self.is_readable = True + self.closed = False def frombuffer(object obj): @@ -834,8 +846,8 @@ cdef get_writer(object source, shared_ptr[OutputStream]* writer): if isinstance(source, NativeFile): nf = source - if not nf.is_writeable: - raise IOError('Native file is not writeable') + if not nf.is_writable: + raise IOError('Native file is not writable') nf.write_handle(writer) else: diff --git a/python/pyarrow/ipc.pxi b/python/pyarrow/ipc.pxi index 7534b0d0e87ec..a30a228ae878f 100644 --- a/python/pyarrow/ipc.pxi +++ b/python/pyarrow/ipc.pxi @@ -429,7 +429,7 @@ def write_tensor(Tensor tensor, NativeFile dest): int32_t metadata_length int64_t body_length - dest._assert_writeable() + dest._assert_writable() with nogil: check_status( diff --git a/python/pyarrow/ipc.py b/python/pyarrow/ipc.py index f264f089c4071..4081fc50e6df6 100644 --- a/python/pyarrow/ipc.py +++ b/python/pyarrow/ipc.py @@ -65,7 +65,7 @@ class RecordBatchStreamWriter(lib._RecordBatchWriter): Parameters ---------- sink : str, pyarrow.NativeFile, or file-like Python object - Either a file path, or a writeable file object + Either a file path, or a writable file object schema : pyarrow.Schema The Arrow schema for data to be written to the file """ @@ -96,7 +96,7 @@ class RecordBatchFileWriter(lib._RecordBatchFileWriter): Parameters ---------- sink : str, pyarrow.NativeFile, or file-like Python object - Either a file path, or a writeable file object + Either a file path, or a writable file object schema : pyarrow.Schema The Arrow schema for data to be written to the file """ diff --git a/python/pyarrow/lib.pxd b/python/pyarrow/lib.pxd index 90f749d6db633..161562c040c30 100644 --- a/python/pyarrow/lib.pxd +++ b/python/pyarrow/lib.pxd @@ -333,8 +333,8 @@ cdef class NativeFile: shared_ptr[RandomAccessFile] rd_file shared_ptr[OutputStream] wr_file bint is_readable - bint is_writeable - bint is_open + bint is_writable + readonly bint closed bint own_file # By implementing these "virtual" functions (all functions in Cython diff --git a/python/pyarrow/tests/test_io.py b/python/pyarrow/tests/test_io.py index 3f7aa2e1c83bd..da26b101db260 100644 --- a/python/pyarrow/tests/test_io.py +++ b/python/pyarrow/tests/test_io.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from io import BytesIO +from io import BytesIO, TextIOWrapper import gc import os import pytest @@ -482,27 +482,48 @@ def test_native_file_modes(tmpdir): with pa.OSFile(path, mode='r') as f: assert f.mode == 'rb' + assert f.readable() + assert not f.writable() + assert f.seekable() with pa.OSFile(path, mode='rb') as f: assert f.mode == 'rb' + assert f.readable() + assert not f.writable() + assert f.seekable() with pa.OSFile(path, mode='w') as f: assert f.mode == 'wb' + assert not f.readable() + assert f.writable() + assert not f.seekable() with pa.OSFile(path, mode='wb') as f: assert f.mode == 'wb' + assert not f.readable() + assert f.writable() + assert not f.seekable() with open(path, 'wb') as f: f.write(b'foooo') with pa.memory_map(path, 'r') as f: assert f.mode == 'rb' + assert f.readable() + assert not f.writable() + assert f.seekable() with pa.memory_map(path, 'r+') as f: assert f.mode == 'rb+' + assert f.readable() + assert f.writable() + assert f.seekable() with pa.memory_map(path, 'r+b') as f: assert f.mode == 'rb+' + assert f.readable() + assert f.writable() + assert f.seekable() def test_native_file_raises_ValueError_after_close(tmpdir): @@ -511,19 +532,56 @@ def test_native_file_raises_ValueError_after_close(tmpdir): f.write(b'foooo') with pa.OSFile(path, mode='rb') as os_file: - pass + assert not os_file.closed + assert os_file.closed with pa.memory_map(path, mode='rb') as mmap_file: - pass + assert not mmap_file.closed + assert mmap_file.closed files = [os_file, mmap_file] methods = [('tell', ()), ('seek', (0,)), - ('size', ())] + ('size', ()), + ('flush', ()), + ('readable', ()), + ('writable', ()), + ('seekable', ())] for f in files: for method, args in methods: with pytest.raises(ValueError): getattr(f, method)(*args) + + +def test_native_file_TextIOWrapper(tmpdir): + data = (u'foooo\n' + u'barrr\n' + u'bazzz\n') + + path = os.path.join(str(tmpdir), guid()) + with open(path, 'wb') as f: + f.write(data.encode('utf-8')) + + with TextIOWrapper(pa.OSFile(path, mode='rb')) as fil: + assert fil.readable() + res = fil.read() + assert res == data + assert fil.closed + + with TextIOWrapper(pa.OSFile(path, mode='rb')) as fil: + # Iteration works + lines = list(fil) + assert ''.join(lines) == data + + # Writing + path2 = os.path.join(str(tmpdir), guid()) + with TextIOWrapper(pa.OSFile(path2, mode='wb')) as fil: + assert fil.writable() + fil.write(data) + + with TextIOWrapper(pa.OSFile(path2, mode='rb')) as fil: + res = fil.read() + assert res == data From cd0676f86097a7461d550b08ff17bdb0d45c4929 Mon Sep 17 00:00:00 2001 From: yosuke shiro Date: Wed, 31 Jan 2018 11:29:48 +0900 Subject: [PATCH 38/46] ARROW-2064: [GLib] Add common build problems link to the install section I think I should add "Add common build problems" link to the install section. I could not find the the link when installing. Author: yosuke shiro Closes #1537 from shiro615/add-common-build-problems-link-to-the-install-section and squashes the following commits: 12dfc9bc [yosuke shiro] [GLib] Add common build problems link to the install section --- c_glib/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/c_glib/README.md b/c_glib/README.md index 1f67d7ea52ccb..d801fc83b5324 100644 --- a/c_glib/README.md +++ b/c_glib/README.md @@ -53,6 +53,8 @@ recommended that you use packages. Note that the packages are "unofficial". "Official" packages will be released in the future. +If you find problems when installing please see [common build problems](https://github.com/apache/arrow/blob/master/c_glib/README.md#common-build-problems). + ### Package See [install document](../site/install.md) for details. From e112995fdfa4917ec5b683eead5b07a7921d1600 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Tue, 30 Jan 2018 22:35:06 -0500 Subject: [PATCH 39/46] ARROW-2062: [Python] Do not use memory maps in test_serialization.py to try to improve Travis CI flakiness Author: Wes McKinney Closes #1536 from wesm/ARROW-2062 and squashes the following commits: 22300cd8 [Wes McKinney] Do not use memory maps in serialization tests ff5141fc [Wes McKinney] Add large_buffer fixture --- python/pyarrow/tests/test_serialization.py | 103 +++++++++++---------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/python/pyarrow/tests/test_serialization.py b/python/pyarrow/tests/test_serialization.py index 9cad81fc98f1e..284c7fb4ca78d 100644 --- a/python/pyarrow/tests/test_serialization.py +++ b/python/pyarrow/tests/test_serialization.py @@ -210,11 +210,12 @@ def make_serialization_context(): serialization_context = make_serialization_context() -def serialization_roundtrip(value, f, ctx=serialization_context): - f.seek(0) - pa.serialize_to(value, f, ctx) - f.seek(0) - result = pa.deserialize_from(f, None, ctx) +def serialization_roundtrip(value, scratch_buffer, ctx=serialization_context): + writer = pa.FixedSizeBufferWriter(scratch_buffer) + pa.serialize_to(value, writer, ctx) + + reader = pa.BufferReader(scratch_buffer) + result = pa.deserialize_from(reader, None, ctx) assert_equal(value, result) _check_component_roundtrip(value) @@ -230,6 +231,10 @@ def _check_component_roundtrip(value): @pytest.yield_fixture(scope='session') +def large_buffer(size=100*1024*1024): + return pa.allocate_buffer(size) + + def large_memory_map(tmpdir_factory, size=100*1024*1024): path = (tmpdir_factory.mktemp('data') .join('pyarrow-serialization-tmp-file').strpath) @@ -243,11 +248,11 @@ def large_memory_map(tmpdir_factory, size=100*1024*1024): return path -def test_primitive_serialization(large_memory_map): - with pa.memory_map(large_memory_map, mode="r+") as mmap: - for obj in PRIMITIVE_OBJECTS: - serialization_roundtrip(obj, mmap) - serialization_roundtrip(obj, mmap, pa.pandas_serialization_context) +def test_primitive_serialization(large_buffer): + for obj in PRIMITIVE_OBJECTS: + serialization_roundtrip(obj, large_buffer) + serialization_roundtrip(obj, large_buffer, + pa.pandas_serialization_context) def test_serialize_to_buffer(): @@ -258,34 +263,31 @@ def test_serialize_to_buffer(): assert_equal(value, result) -def test_complex_serialization(large_memory_map): - with pa.memory_map(large_memory_map, mode="r+") as mmap: - for obj in COMPLEX_OBJECTS: - serialization_roundtrip(obj, mmap) +def test_complex_serialization(large_buffer): + for obj in COMPLEX_OBJECTS: + serialization_roundtrip(obj, large_buffer) -def test_custom_serialization(large_memory_map): - with pa.memory_map(large_memory_map, mode="r+") as mmap: - for obj in CUSTOM_OBJECTS: - serialization_roundtrip(obj, mmap) +def test_custom_serialization(large_buffer): + for obj in CUSTOM_OBJECTS: + serialization_roundtrip(obj, large_buffer) -def test_default_dict_serialization(large_memory_map): +def test_default_dict_serialization(large_buffer): pytest.importorskip("cloudpickle") - with pa.memory_map(large_memory_map, mode="r+") as mmap: - obj = defaultdict(lambda: 0, [("hello", 1), ("world", 2)]) - serialization_roundtrip(obj, mmap) + + obj = defaultdict(lambda: 0, [("hello", 1), ("world", 2)]) + serialization_roundtrip(obj, large_buffer) -def test_numpy_serialization(large_memory_map): - with pa.memory_map(large_memory_map, mode="r+") as mmap: - for t in ["bool", "int8", "uint8", "int16", "uint16", "int32", - "uint32", "float16", "float32", "float64"]: - obj = np.random.randint(0, 10, size=(100, 100)).astype(t) - serialization_roundtrip(obj, mmap) +def test_numpy_serialization(large_buffer): + for t in ["bool", "int8", "uint8", "int16", "uint16", "int32", + "uint32", "float16", "float32", "float64"]: + obj = np.random.randint(0, 10, size=(100, 100)).astype(t) + serialization_roundtrip(obj, large_buffer) -def test_datetime_serialization(large_memory_map): +def test_datetime_serialization(large_buffer): data = [ # Principia Mathematica published datetime.datetime(year=1687, month=7, day=5), @@ -309,32 +311,31 @@ def test_datetime_serialization(large_memory_map): datetime.datetime(year=1970, month=1, day=3, hour=4, minute=0, second=0) ] - with pa.memory_map(large_memory_map, mode="r+") as mmap: - for d in data: - serialization_roundtrip(d, mmap) + for d in data: + serialization_roundtrip(d, large_buffer) -def test_torch_serialization(large_memory_map): +def test_torch_serialization(large_buffer): pytest.importorskip("torch") import torch - with pa.memory_map(large_memory_map, mode="r+") as mmap: - # These are the only types that are supported for the - # PyTorch to NumPy conversion - for t in ["float32", "float64", - "uint8", "int16", "int32", "int64"]: - obj = torch.from_numpy(np.random.randn(1000).astype(t)) - serialization_roundtrip(obj, mmap) - - -def test_numpy_immutable(large_memory_map): - with pa.memory_map(large_memory_map, mode="r+") as mmap: - obj = np.zeros([10]) - mmap.seek(0) - pa.serialize_to(obj, mmap, serialization_context) - mmap.seek(0) - result = pa.deserialize_from(mmap, None, serialization_context) - with pytest.raises(ValueError): - result[0] = 1.0 + # These are the only types that are supported for the + # PyTorch to NumPy conversion + for t in ["float32", "float64", + "uint8", "int16", "int32", "int64"]: + obj = torch.from_numpy(np.random.randn(1000).astype(t)) + serialization_roundtrip(obj, large_buffer) + + +def test_numpy_immutable(large_buffer): + obj = np.zeros([10]) + + writer = pa.FixedSizeBufferWriter(large_buffer) + pa.serialize_to(obj, writer, serialization_context) + + reader = pa.BufferReader(large_buffer) + result = pa.deserialize_from(reader, None, serialization_context) + with pytest.raises(ValueError): + result[0] = 1.0 # see https://issues.apache.org/jira/browse/ARROW-1695 From 0d6817a05e1316c60dbdaa055c18fabf30240a5e Mon Sep 17 00:00:00 2001 From: Philipp Moritz Date: Tue, 30 Jan 2018 22:36:10 -0500 Subject: [PATCH 40/46] ARROW-2042: [Plasma] Revert API change of plasma::Create to output a MutableBuffer This reverts a part of the changes from https://github.com/apache/arrow/pull/1479. This is needed for https://github.com/apache/arrow/pull/1445 so we can return a CudaBuffer from plasma::Create. Author: Philipp Moritz Closes #1520 from pcmoritz/revert-mutable-buffer and squashes the following commits: 0d36c734 [Philipp Moritz] fix plasma python bindings 259127d5 [Philipp Moritz] revert plasma::Create API back to Buffer --- cpp/src/plasma/client.cc | 4 +++- cpp/src/plasma/client.h | 3 +-- cpp/src/plasma/test/client_tests.cc | 14 +++++++------- python/pyarrow/plasma.pyx | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cpp/src/plasma/client.cc b/cpp/src/plasma/client.cc index a683da0022b18..6e9b6968a8673 100644 --- a/cpp/src/plasma/client.cc +++ b/cpp/src/plasma/client.cc @@ -54,6 +54,8 @@ namespace plasma { +using arrow::MutableBuffer; + // Number of threads used for memcopy and hash computations. constexpr int64_t kThreadPoolSize = 8; constexpr int64_t kBytesInMB = 1 << 20; @@ -147,7 +149,7 @@ void PlasmaClient::increment_object_count(const ObjectID& object_id, PlasmaObjec Status PlasmaClient::Create(const ObjectID& object_id, int64_t data_size, uint8_t* metadata, int64_t metadata_size, - std::shared_ptr* data) { + std::shared_ptr* data) { ARROW_LOG(DEBUG) << "called plasma_create on conn " << store_conn_ << " with size " << data_size << " and metadata size " << metadata_size; RETURN_NOT_OK(SendCreateRequest(store_conn_, object_id, data_size, metadata_size)); diff --git a/cpp/src/plasma/client.h b/cpp/src/plasma/client.h index a1e10a9c29969..d6372f44a7f28 100644 --- a/cpp/src/plasma/client.h +++ b/cpp/src/plasma/client.h @@ -32,7 +32,6 @@ #include "plasma/common.h" using arrow::Buffer; -using arrow::MutableBuffer; using arrow::Status; namespace plasma { @@ -116,7 +115,7 @@ class ARROW_EXPORT PlasmaClient { /// will be written here. /// \return The return status. Status Create(const ObjectID& object_id, int64_t data_size, uint8_t* metadata, - int64_t metadata_size, std::shared_ptr* data); + int64_t metadata_size, std::shared_ptr* data); /// Get some objects from the Plasma Store. This function will block until the /// objects have all been created and sealed in the Plasma Store or the /// timeout diff --git a/cpp/src/plasma/test/client_tests.cc b/cpp/src/plasma/test/client_tests.cc index 63b56934f3599..f19c2bfbdb380 100644 --- a/cpp/src/plasma/test/client_tests.cc +++ b/cpp/src/plasma/test/client_tests.cc @@ -70,7 +70,7 @@ TEST_F(TestPlasmaStore, DeleteTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); ARROW_CHECK_OK(client_.Seal(object_id)); @@ -96,7 +96,7 @@ TEST_F(TestPlasmaStore, ContainsTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); ARROW_CHECK_OK(client_.Seal(object_id)); // Avoid race condition of Plasma Manager waiting for notification. @@ -119,7 +119,7 @@ TEST_F(TestPlasmaStore, GetTest) { int64_t data_size = 4; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data_buffer; + std::shared_ptr data_buffer; uint8_t* data; ARROW_CHECK_OK( client_.Create(object_id, data_size, metadata, metadata_size, &data_buffer)); @@ -145,7 +145,7 @@ TEST_F(TestPlasmaStore, MultipleGetTest) { int64_t data_size = 4; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id1, data_size, metadata, metadata_size, &data)); data->mutable_data()[0] = 1; ARROW_CHECK_OK(client_.Seal(object_id1)); @@ -172,7 +172,7 @@ TEST_F(TestPlasmaStore, AbortTest) { int64_t data_size = 4; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; uint8_t* data_ptr; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); data_ptr = data->mutable_data(); @@ -220,7 +220,7 @@ TEST_F(TestPlasmaStore, MultipleClientTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client2_.Create(object_id, data_size, metadata, metadata_size, &data)); ARROW_CHECK_OK(client2_.Seal(object_id)); // Test that the first client can get the object. @@ -260,7 +260,7 @@ TEST_F(TestPlasmaStore, ManyObjectTest) { int64_t data_size = 100; uint8_t metadata[] = {5}; int64_t metadata_size = sizeof(metadata); - std::shared_ptr data; + std::shared_ptr data; ARROW_CHECK_OK(client_.Create(object_id, data_size, metadata, metadata_size, &data)); if (i % 3 == 0) { diff --git a/python/pyarrow/plasma.pyx b/python/pyarrow/plasma.pyx index 801d094194b71..32f6d189da08c 100644 --- a/python/pyarrow/plasma.pyx +++ b/python/pyarrow/plasma.pyx @@ -81,7 +81,7 @@ cdef extern from "plasma/client.h" nogil: CStatus Create(const CUniqueID& object_id, int64_t data_size, const uint8_t* metadata, int64_t metadata_size, - const shared_ptr[CMutableBuffer]* data) + const shared_ptr[CBuffer]* data) CStatus Get(const CUniqueID* object_ids, int64_t num_objects, int64_t timeout_ms, CObjectBuffer* object_buffers) @@ -297,7 +297,7 @@ cdef class PlasmaClient: not be created because the plasma store is unable to evict enough objects to create room for it. """ - cdef shared_ptr[CMutableBuffer] data + cdef shared_ptr[CBuffer] data with nogil: check_status(self.client.get().Create(object_id.data, data_size, (metadata.data()), From 0e04f6d2bd6984b5afd33ae0dd1b9eae96a681a9 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Wed, 31 Jan 2018 16:34:14 -0500 Subject: [PATCH 41/46] ARROW-2070: [Python] Fix chdir logic in setup.py In some conditions, setup.py may change the current directory and omit restoring the previous one, which can fail some operations. Author: Antoine Pitrou Closes #1540 from pitrou/ARROW-2070-chdir-in-setuppy and squashes the following commits: f043b681 [Antoine Pitrou] ARROW-2070: [Python] Fix chdir logic in setup.py --- python/setup.py | 308 +++++++++++++++++++++++++----------------------- 1 file changed, 159 insertions(+), 149 deletions(-) diff --git a/python/setup.py b/python/setup.py index 076d7e489b5f3..cfc771fe870ab 100644 --- a/python/setup.py +++ b/python/setup.py @@ -17,6 +17,7 @@ # specific language governing permissions and limitations # under the License. +import contextlib import glob import os import os.path as osp @@ -47,6 +48,16 @@ setup_dir = os.path.abspath(os.path.dirname(__file__)) +@contextlib.contextmanager +def changed_dir(dirname): + oldcwd = os.getcwd() + os.chdir(dirname) + try: + yield + finally: + os.chdir(oldcwd) + + class clean(_clean): def run(self): @@ -59,6 +70,7 @@ def run(self): class build_ext(_build_ext): + _found_names = () def build_extensions(self): numpy_incl = pkg_resources.resource_filename('numpy', 'core/include') @@ -129,162 +141,160 @@ def _run_cmake(self): # The staging directory for the module being built build_temp = pjoin(os.getcwd(), self.build_temp) build_lib = os.path.join(os.getcwd(), self.build_lib) - - # Change to the build directory saved_cwd = os.getcwd() + if not os.path.isdir(self.build_temp): self.mkpath(self.build_temp) - os.chdir(self.build_temp) - - # Detect if we built elsewhere - if os.path.isfile('CMakeCache.txt'): - cachefile = open('CMakeCache.txt', 'r') - cachedir = re.search('CMAKE_CACHEFILE_DIR:INTERNAL=(.*)', - cachefile.read()).group(1) - cachefile.close() - if (cachedir != build_temp): - return - - static_lib_option = '' - - cmake_options = [ - '-DPYTHON_EXECUTABLE=%s' % sys.executable, - static_lib_option, - ] - if self.with_parquet: - cmake_options.append('-DPYARROW_BUILD_PARQUET=on') - if self.with_static_parquet: - cmake_options.append('-DPYARROW_PARQUET_USE_SHARED=off') - if not self.with_static_boost: - cmake_options.append('-DPYARROW_BOOST_USE_SHARED=on') - - if self.with_plasma: - cmake_options.append('-DPYARROW_BUILD_PLASMA=on') - - if self.with_orc: - cmake_options.append('-DPYARROW_BUILD_ORC=on') - - if len(self.cmake_cxxflags) > 0: - cmake_options.append('-DPYARROW_CXXFLAGS="{0}"' - .format(self.cmake_cxxflags)) - - if self.bundle_arrow_cpp: - cmake_options.append('-DPYARROW_BUNDLE_ARROW_CPP=ON') - # ARROW-1090: work around CMake rough edges - if 'ARROW_HOME' in os.environ and sys.platform != 'win32': - pkg_config = pjoin(os.environ['ARROW_HOME'], 'lib', - 'pkgconfig') - os.environ['PKG_CONFIG_PATH'] = pkg_config - del os.environ['ARROW_HOME'] - - cmake_options.append('-DCMAKE_BUILD_TYPE={0}' - .format(self.build_type.lower())) - - extra_cmake_args = shlex.split(self.extra_cmake_args) - if sys.platform != 'win32': - cmake_command = (['cmake'] + extra_cmake_args + - cmake_options + [source]) - - print("-- Runnning cmake for pyarrow") - self.spawn(cmake_command) - print("-- Finished cmake for pyarrow") - args = ['make'] - if os.environ.get('PYARROW_BUILD_VERBOSE', '0') == '1': - args.append('VERBOSE=1') - - if 'PYARROW_PARALLEL' in os.environ: - args.append('-j{0}'.format(os.environ['PYARROW_PARALLEL'])) - print("-- Running cmake --build for pyarrow") - self.spawn(args) - print("-- Finished cmake --build for pyarrow") - else: - cmake_generator = 'Visual Studio 14 2015 Win64' - if not is_64_bit: - raise RuntimeError('Not supported on 32-bit Windows') - - # Generate the build files - cmake_command = (['cmake'] + extra_cmake_args + - cmake_options + - [source, '-G', cmake_generator]) - if "-G" in self.extra_cmake_args: - cmake_command = cmake_command[:-2] - - print("-- Runnning cmake for pyarrow") - self.spawn(cmake_command) - print("-- Finished cmake for pyarrow") - # Do the build - print("-- Running cmake --build for pyarrow") - self.spawn(['cmake', '--build', '.', '--config', self.build_type]) - print("-- Finished cmake --build for pyarrow") - - if self.inplace: - # a bit hacky - build_lib = saved_cwd - - # Move the libraries to the place expected by the Python - # build - - try: - os.makedirs(pjoin(build_lib, 'pyarrow')) - except OSError: - pass + # Change to the build directory + with changed_dir(self.build_temp): + # Detect if we built elsewhere + if os.path.isfile('CMakeCache.txt'): + cachefile = open('CMakeCache.txt', 'r') + cachedir = re.search('CMAKE_CACHEFILE_DIR:INTERNAL=(.*)', + cachefile.read()).group(1) + cachefile.close() + if (cachedir != build_temp): + return + + static_lib_option = '' + + cmake_options = [ + '-DPYTHON_EXECUTABLE=%s' % sys.executable, + static_lib_option, + ] + + if self.with_parquet: + cmake_options.append('-DPYARROW_BUILD_PARQUET=on') + if self.with_static_parquet: + cmake_options.append('-DPYARROW_PARQUET_USE_SHARED=off') + if not self.with_static_boost: + cmake_options.append('-DPYARROW_BOOST_USE_SHARED=on') - if sys.platform == 'win32': - build_prefix = '' - else: - build_prefix = self.build_type + if self.with_plasma: + cmake_options.append('-DPYARROW_BUILD_PLASMA=on') + + if self.with_orc: + cmake_options.append('-DPYARROW_BUILD_ORC=on') + + if len(self.cmake_cxxflags) > 0: + cmake_options.append('-DPYARROW_CXXFLAGS="{0}"' + .format(self.cmake_cxxflags)) + + if self.bundle_arrow_cpp: + cmake_options.append('-DPYARROW_BUNDLE_ARROW_CPP=ON') + # ARROW-1090: work around CMake rough edges + if 'ARROW_HOME' in os.environ and sys.platform != 'win32': + pkg_config = pjoin(os.environ['ARROW_HOME'], 'lib', + 'pkgconfig') + os.environ['PKG_CONFIG_PATH'] = pkg_config + del os.environ['ARROW_HOME'] + + cmake_options.append('-DCMAKE_BUILD_TYPE={0}' + .format(self.build_type.lower())) + + extra_cmake_args = shlex.split(self.extra_cmake_args) + if sys.platform != 'win32': + cmake_command = (['cmake'] + extra_cmake_args + + cmake_options + [source]) + + print("-- Runnning cmake for pyarrow") + self.spawn(cmake_command) + print("-- Finished cmake for pyarrow") + args = ['make'] + if os.environ.get('PYARROW_BUILD_VERBOSE', '0') == '1': + args.append('VERBOSE=1') + + if 'PYARROW_PARALLEL' in os.environ: + args.append('-j{0}'.format(os.environ['PYARROW_PARALLEL'])) + print("-- Running cmake --build for pyarrow") + self.spawn(args) + print("-- Finished cmake --build for pyarrow") + else: + cmake_generator = 'Visual Studio 14 2015 Win64' + if not is_64_bit: + raise RuntimeError('Not supported on 32-bit Windows') + + # Generate the build files + cmake_command = (['cmake'] + extra_cmake_args + + cmake_options + + [source, '-G', cmake_generator]) + if "-G" in self.extra_cmake_args: + cmake_command = cmake_command[:-2] + + print("-- Runnning cmake for pyarrow") + self.spawn(cmake_command) + print("-- Finished cmake for pyarrow") + # Do the build + print("-- Running cmake --build for pyarrow") + self.spawn(['cmake', '--build', '.', '--config', self.build_type]) + print("-- Finished cmake --build for pyarrow") + + if self.inplace: + # a bit hacky + build_lib = saved_cwd + + # Move the libraries to the place expected by the Python + # build + + try: + os.makedirs(pjoin(build_lib, 'pyarrow')) + except OSError: + pass - if self.bundle_arrow_cpp: - print(pjoin(build_lib, 'pyarrow')) - move_shared_libs(build_prefix, build_lib, "arrow") - move_shared_libs(build_prefix, build_lib, "arrow_python") + if sys.platform == 'win32': + build_prefix = '' + else: + build_prefix = self.build_type + + if self.bundle_arrow_cpp: + print(pjoin(build_lib, 'pyarrow')) + move_shared_libs(build_prefix, build_lib, "arrow") + move_shared_libs(build_prefix, build_lib, "arrow_python") + if self.with_plasma: + move_shared_libs(build_prefix, build_lib, "plasma") + if self.with_parquet and not self.with_static_parquet: + move_shared_libs(build_prefix, build_lib, "parquet") + + print('Bundling includes: ' + pjoin(build_prefix, 'include')) + if os.path.exists(pjoin(build_lib, 'pyarrow', 'include')): + shutil.rmtree(pjoin(build_lib, 'pyarrow', 'include')) + shutil.move(pjoin(build_prefix, 'include'), + pjoin(build_lib, 'pyarrow')) + + # Move the built C-extension to the place expected by the Python build + self._found_names = [] + for name in self.CYTHON_MODULE_NAMES: + built_path = self.get_ext_built(name) + if not os.path.exists(built_path): + print(built_path) + if self._failure_permitted(name): + print('Cython module {0} failure permitted'.format(name)) + continue + raise RuntimeError('pyarrow C-extension failed to build:', + os.path.abspath(built_path)) + + ext_path = pjoin(build_lib, self._get_cmake_ext_path(name)) + if os.path.exists(ext_path): + os.remove(ext_path) + self.mkpath(os.path.dirname(ext_path)) + print('Moving built C-extension', built_path, + 'to build path', ext_path) + shutil.move(self.get_ext_built(name), ext_path) + self._found_names.append(name) + + if os.path.exists(self.get_ext_built_api_header(name)): + shutil.move(self.get_ext_built_api_header(name), + pjoin(os.path.dirname(ext_path), name + '_api.h')) + + # Move the plasma store if self.with_plasma: - move_shared_libs(build_prefix, build_lib, "plasma") - if self.with_parquet and not self.with_static_parquet: - move_shared_libs(build_prefix, build_lib, "parquet") - - print('Bundling includes: ' + pjoin(build_prefix, 'include')) - if os.path.exists(pjoin(build_lib, 'pyarrow', 'include')): - shutil.rmtree(pjoin(build_lib, 'pyarrow', 'include')) - shutil.move(pjoin(build_prefix, 'include'), - pjoin(build_lib, 'pyarrow')) - - # Move the built C-extension to the place expected by the Python build - self._found_names = [] - for name in self.CYTHON_MODULE_NAMES: - built_path = self.get_ext_built(name) - if not os.path.exists(built_path): - print(built_path) - if self._failure_permitted(name): - print('Cython module {0} failure permitted'.format(name)) - continue - raise RuntimeError('pyarrow C-extension failed to build:', - os.path.abspath(built_path)) - - ext_path = pjoin(build_lib, self._get_cmake_ext_path(name)) - if os.path.exists(ext_path): - os.remove(ext_path) - self.mkpath(os.path.dirname(ext_path)) - print('Moving built C-extension', built_path, - 'to build path', ext_path) - shutil.move(self.get_ext_built(name), ext_path) - self._found_names.append(name) - - if os.path.exists(self.get_ext_built_api_header(name)): - shutil.move(self.get_ext_built_api_header(name), - pjoin(os.path.dirname(ext_path), name + '_api.h')) - - # Move the plasma store - if self.with_plasma: - build_py = self.get_finalized_command('build_py') - source = os.path.join(self.build_type, "plasma_store") - target = os.path.join(build_lib, - build_py.get_package_dir('pyarrow'), - "plasma_store") - shutil.move(source, target) - - os.chdir(saved_cwd) + build_py = self.get_finalized_command('build_py') + source = os.path.join(self.build_type, "plasma_store") + target = os.path.join(build_lib, + build_py.get_package_dir('pyarrow'), + "plasma_store") + shutil.move(source, target) def _failure_permitted(self, name): if name == '_parquet' and not self.with_parquet: From 1ed4019a2249be68de81854c79acf5f353c18d51 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Thu, 1 Feb 2018 08:19:42 -0500 Subject: [PATCH 42/46] ARROW-2072: [Python] Fix crash in decimal128.byte_width Author: Antoine Pitrou Closes #1542 from pitrou/ARROW-2072-decimal-byte-width-crash and squashes the following commits: bc1e15b1 [Antoine Pitrou] ARROW-2072: [Python] Fix crash in decimal128.byte_width --- python/pyarrow/tests/test_types.py | 10 ++++++++++ python/pyarrow/types.pxi | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/python/pyarrow/tests/test_types.py b/python/pyarrow/tests/test_types.py index 68dc499cf48b4..ad683e9a2ea00 100644 --- a/python/pyarrow/tests/test_types.py +++ b/python/pyarrow/tests/test_types.py @@ -184,3 +184,13 @@ def test_types_hashable(): ]) def test_exact_primitive_types(t, check_func): assert check_func(t) + + +def test_fixed_size_binary_byte_width(): + ty = pa.binary(5) + assert ty.byte_width == 5 + + +def test_decimal_byte_width(): + ty = pa.decimal128(19, 4) + assert ty.byte_width == 16 diff --git a/python/pyarrow/types.pxi b/python/pyarrow/types.pxi index a3cbeefb028c7..849a0e016a60d 100644 --- a/python/pyarrow/types.pxi +++ b/python/pyarrow/types.pxi @@ -293,7 +293,7 @@ cdef class FixedSizeBinaryType(DataType): cdef class Decimal128Type(FixedSizeBinaryType): cdef void init(self, const shared_ptr[CDataType]& type): - DataType.init(self, type) + FixedSizeBinaryType.init(self, type) self.decimal128_type = type.get() def __getstate__(self): From 2d649f9a75250b5a908369ed0246218712830fc4 Mon Sep 17 00:00:00 2001 From: Panchen Xue Date: Thu, 1 Feb 2018 12:14:48 -0500 Subject: [PATCH 43/46] ARROW-1623: [C++] Add convenience method to construct Buffer from a string that owns its memory Add static member function Buffer::FromString to create a new buffer that owns its memory from given std::string. The memory is allocated from a given memory pool or the default one if not specified. Author: Panchen Xue Author: Wes McKinney Closes #1518 from xuepanchen/ARROW-1623 and squashes the following commits: e6f7355f [Wes McKinney] clang-format ce18950a [Panchen Xue] Add Buffer::FromString method that takes default memory pool and modify test cast f2c5e3ea [Panchen Xue] ARROW-1623: [C++] Add test case for Buffer::FromString method 2385637c [Panchen Xue] ARROW-1623: [C++] Add Buffer::FromString to construct a buffer that owns its memory from a std::string --- cpp/src/arrow/buffer-test.cc | 17 +++++++++++++++++ cpp/src/arrow/buffer.cc | 12 ++++++++++++ cpp/src/arrow/buffer.h | 14 ++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/cpp/src/arrow/buffer-test.cc b/cpp/src/arrow/buffer-test.cc index 398cc06363a6f..a24384a383395 100644 --- a/cpp/src/arrow/buffer-test.cc +++ b/cpp/src/arrow/buffer-test.cc @@ -52,6 +52,23 @@ TEST(TestBuffer, FromStdString) { ASSERT_EQ(static_cast(val.size()), buf.size()); } +TEST(TestBuffer, FromStdStringWithMemory) { + std::string expected = "hello, world"; + std::shared_ptr buf; + + { + std::string temp = "hello, world"; + ASSERT_OK(Buffer::FromString(temp, &buf)); + ASSERT_EQ(0, memcmp(buf->data(), temp.c_str(), temp.size())); + ASSERT_EQ(static_cast(temp.size()), buf->size()); + } + + // Now temp goes out of scope and we check if created buffer + // is still valid to make sure it actually owns its space + ASSERT_EQ(0, memcmp(buf->data(), expected.c_str(), expected.size())); + ASSERT_EQ(static_cast(expected.size()), buf->size()); +} + TEST(TestBuffer, Resize) { PoolBuffer buf; diff --git a/cpp/src/arrow/buffer.cc b/cpp/src/arrow/buffer.cc index 1b8e4375445bb..29e2c242a3f4a 100644 --- a/cpp/src/arrow/buffer.cc +++ b/cpp/src/arrow/buffer.cc @@ -58,6 +58,18 @@ bool Buffer::Equals(const Buffer& other) const { !memcmp(data_, other.data_, static_cast(size_)))); } +Status Buffer::FromString(const std::string& data, MemoryPool* pool, + std::shared_ptr* out) { + auto size = static_cast(data.size()); + RETURN_NOT_OK(AllocateBuffer(pool, size, out)); + std::copy(data.c_str(), data.c_str() + size, (*out)->mutable_data()); + return Status::OK(); +} + +Status Buffer::FromString(const std::string& data, std::shared_ptr* out) { + return FromString(data, default_memory_pool(), out); +} + PoolBuffer::PoolBuffer(MemoryPool* pool) : ResizableBuffer(nullptr, 0) { if (pool == nullptr) { pool = default_memory_pool(); diff --git a/cpp/src/arrow/buffer.h b/cpp/src/arrow/buffer.h index 44c352a93f273..d12eeb4df9eed 100644 --- a/cpp/src/arrow/buffer.h +++ b/cpp/src/arrow/buffer.h @@ -97,6 +97,20 @@ class ARROW_EXPORT Buffer { Status Copy(const int64_t start, const int64_t nbytes, std::shared_ptr* out) const; + /// \brief Construct a new buffer that owns its memory from a std::string + /// + /// \param[in] data a std::string object + /// \param[in] pool a memory pool + /// \param[out] out the created buffer + /// + /// \return Status message + static Status FromString(const std::string& data, MemoryPool* pool, + std::shared_ptr* out); + + /// \brief Construct a new buffer that owns its memory from a std::string + /// using the default memory pool + static Status FromString(const std::string& data, std::shared_ptr* out); + int64_t capacity() const { return capacity_; } const uint8_t* data() const { return data_; } uint8_t* mutable_data() { return mutable_data_; } From ff28c7647c1910f1a0d1d97b8ba68b2b554e5ce1 Mon Sep 17 00:00:00 2001 From: Robert Nishihara Date: Thu, 1 Feb 2018 12:17:46 -0500 Subject: [PATCH 44/46] ARROW-2024: [Python] Remove torch serialization from default serialization context. See discussion in #1223. Author: Robert Nishihara Closes #1538 from robertnishihara/dontimportpytorch and squashes the following commits: 09a8cfa1 [Robert Nishihara] Fix bug. 8992e0fb [Robert Nishihara] Fix. 83ffb70f [Robert Nishihara] Remove torch serialization from default serialization context. --- python/pyarrow/__init__.py | 5 +- python/pyarrow/serialization.py | 66 ++++++++++++--------- python/pyarrow/tests/test_convert_pandas.py | 2 +- python/pyarrow/tests/test_serialization.py | 56 +++++++++-------- 4 files changed, 73 insertions(+), 56 deletions(-) diff --git a/python/pyarrow/__init__.py b/python/pyarrow/__init__.py index a245fe6796023..8b3cba92414f8 100644 --- a/python/pyarrow/__init__.py +++ b/python/pyarrow/__init__.py @@ -124,9 +124,10 @@ localfs = LocalFileSystem.get_instance() -from pyarrow.serialization import (_default_serialization_context, +from pyarrow.serialization import (default_serialization_context, pandas_serialization_context, - register_default_serialization_handlers) + register_default_serialization_handlers, + register_torch_serialization_handlers) import pyarrow.types as types diff --git a/python/pyarrow/serialization.py b/python/pyarrow/serialization.py index 61f2e83f3193d..c8b72b74896c9 100644 --- a/python/pyarrow/serialization.py +++ b/python/pyarrow/serialization.py @@ -22,7 +22,8 @@ import numpy as np from pyarrow.compat import builtin_pickle -from pyarrow.lib import _default_serialization_context, frombuffer +from pyarrow.lib import (SerializationContext, _default_serialization_context, + frombuffer) try: import cloudpickle @@ -102,6 +103,31 @@ def _deserialize_pandas_series(data): custom_deserializer=_deserialize_pandas_dataframe) +def register_torch_serialization_handlers(serialization_context): + # ---------------------------------------------------------------------- + # Set up serialization for pytorch tensors + + try: + import torch + + def _serialize_torch_tensor(obj): + return obj.numpy() + + def _deserialize_torch_tensor(data): + return torch.from_numpy(data) + + for t in [torch.FloatTensor, torch.DoubleTensor, torch.HalfTensor, + torch.ByteTensor, torch.CharTensor, torch.ShortTensor, + torch.IntTensor, torch.LongTensor]: + serialization_context.register_type( + t, "torch." + t.__name__, + custom_serializer=_serialize_torch_tensor, + custom_deserializer=_deserialize_torch_tensor) + except ImportError: + # no torch + pass + + def register_default_serialization_handlers(serialization_context): # ---------------------------------------------------------------------- @@ -154,37 +180,21 @@ def _deserialize_default_dict(data): custom_serializer=_serialize_numpy_array_list, custom_deserializer=_deserialize_numpy_array_list) - # ---------------------------------------------------------------------- - # Set up serialization for pytorch tensors - - try: - import torch - - def _serialize_torch_tensor(obj): - return obj.numpy() + _register_custom_pandas_handlers(serialization_context) - def _deserialize_torch_tensor(data): - return torch.from_numpy(data) - for t in [torch.FloatTensor, torch.DoubleTensor, torch.HalfTensor, - torch.ByteTensor, torch.CharTensor, torch.ShortTensor, - torch.IntTensor, torch.LongTensor]: - serialization_context.register_type( - t, "torch." + t.__name__, - custom_serializer=_serialize_torch_tensor, - custom_deserializer=_deserialize_torch_tensor) - except ImportError: - # no torch - pass - - _register_custom_pandas_handlers(serialization_context) +def default_serialization_context(): + context = SerializationContext() + register_default_serialization_handlers(context) + return context register_default_serialization_handlers(_default_serialization_context) -pandas_serialization_context = _default_serialization_context.clone() -pandas_serialization_context.register_type( - np.ndarray, 'np.array', - custom_serializer=_serialize_numpy_array_pickle, - custom_deserializer=_deserialize_numpy_array_pickle) +def pandas_serialization_context(): + context = default_serialization_context() + context.register_type(np.ndarray, 'np.array', + custom_serializer=_serialize_numpy_array_pickle, + custom_deserializer=_deserialize_numpy_array_pickle) + return context diff --git a/python/pyarrow/tests/test_convert_pandas.py b/python/pyarrow/tests/test_convert_pandas.py index fa265e55cfd76..ca2f1e3611963 100644 --- a/python/pyarrow/tests/test_convert_pandas.py +++ b/python/pyarrow/tests/test_convert_pandas.py @@ -1404,7 +1404,7 @@ def _fully_loaded_dataframe_example(): def _check_serialize_components_roundtrip(df): - ctx = pa.pandas_serialization_context + ctx = pa.pandas_serialization_context() components = ctx.serialize(df).to_components() deserialized = ctx.deserialize_components(components) diff --git a/python/pyarrow/tests/test_serialization.py b/python/pyarrow/tests/test_serialization.py index 284c7fb4ca78d..7a420106f9fb6 100644 --- a/python/pyarrow/tests/test_serialization.py +++ b/python/pyarrow/tests/test_serialization.py @@ -190,8 +190,7 @@ class CustomError(Exception): def make_serialization_context(): - - context = pa._default_serialization_context + context = pa.default_serialization_context() context.register_type(Foo, "Foo") context.register_type(Bar, "Bar") @@ -207,26 +206,27 @@ def make_serialization_context(): return context -serialization_context = make_serialization_context() +global_serialization_context = make_serialization_context() -def serialization_roundtrip(value, scratch_buffer, ctx=serialization_context): +def serialization_roundtrip(value, scratch_buffer, + context=global_serialization_context): writer = pa.FixedSizeBufferWriter(scratch_buffer) - pa.serialize_to(value, writer, ctx) + pa.serialize_to(value, writer, context=context) reader = pa.BufferReader(scratch_buffer) - result = pa.deserialize_from(reader, None, ctx) + result = pa.deserialize_from(reader, None, context=context) assert_equal(value, result) - _check_component_roundtrip(value) + _check_component_roundtrip(value, context=context) -def _check_component_roundtrip(value): +def _check_component_roundtrip(value, context=global_serialization_context): # Test to/from components - serialized = pa.serialize(value) + serialized = pa.serialize(value, context=context) components = serialized.to_components() from_comp = pa.SerializedPyObject.from_components(components) - recons = from_comp.deserialize() + recons = from_comp.deserialize(context=context) assert_equal(value, recons) @@ -252,7 +252,7 @@ def test_primitive_serialization(large_buffer): for obj in PRIMITIVE_OBJECTS: serialization_roundtrip(obj, large_buffer) serialization_roundtrip(obj, large_buffer, - pa.pandas_serialization_context) + pa.pandas_serialization_context()) def test_serialize_to_buffer(): @@ -318,22 +318,26 @@ def test_datetime_serialization(large_buffer): def test_torch_serialization(large_buffer): pytest.importorskip("torch") import torch + + serialization_context = pa.default_serialization_context() + pa.register_torch_serialization_handlers(serialization_context) # These are the only types that are supported for the # PyTorch to NumPy conversion for t in ["float32", "float64", "uint8", "int16", "int32", "int64"]: obj = torch.from_numpy(np.random.randn(1000).astype(t)) - serialization_roundtrip(obj, large_buffer) + serialization_roundtrip(obj, large_buffer, + context=serialization_context) def test_numpy_immutable(large_buffer): obj = np.zeros([10]) writer = pa.FixedSizeBufferWriter(large_buffer) - pa.serialize_to(obj, writer, serialization_context) + pa.serialize_to(obj, writer, global_serialization_context) reader = pa.BufferReader(large_buffer) - result = pa.deserialize_from(reader, None, serialization_context) + result = pa.deserialize_from(reader, None, global_serialization_context) with pytest.raises(ValueError): result[0] = 1.0 @@ -351,12 +355,12 @@ def serialize_dummy_class(obj): def deserialize_dummy_class(serialized_obj): return serialized_obj - pa._default_serialization_context.register_type( - DummyClass, "DummyClass", - custom_serializer=serialize_dummy_class, - custom_deserializer=deserialize_dummy_class) + context = pa.default_serialization_context() + context.register_type(DummyClass, "DummyClass", + custom_serializer=serialize_dummy_class, + custom_deserializer=deserialize_dummy_class) - pa.serialize(DummyClass()) + pa.serialize(DummyClass(), context=context) def test_buffer_serialization(): @@ -370,13 +374,14 @@ def serialize_buffer_class(obj): def deserialize_buffer_class(serialized_obj): return serialized_obj - pa._default_serialization_context.register_type( + context = pa.default_serialization_context() + context.register_type( BufferClass, "BufferClass", custom_serializer=serialize_buffer_class, custom_deserializer=deserialize_buffer_class) - b = pa.serialize(BufferClass()).to_buffer() - assert pa.deserialize(b).to_pybytes() == b"hello" + b = pa.serialize(BufferClass(), context=context).to_buffer() + assert pa.deserialize(b, context=context).to_pybytes() == b"hello" @pytest.mark.skip(reason="extensive memory requirements") @@ -485,15 +490,16 @@ def test_serialize_subclasses(): # with register_type will result in faster and more memory # efficient serialization. - serialization_context.register_type( + context = pa.default_serialization_context() + context.register_type( Serializable, "Serializable", custom_serializer=serialize_serializable, custom_deserializer=deserialize_serializable) a = SerializableClass() - serialized = pa.serialize(a) + serialized = pa.serialize(a, context=context) - deserialized = serialized.deserialize() + deserialized = serialized.deserialize(context=context) assert type(deserialized).__name__ == SerializableClass.__name__ assert deserialized.value == 3 From 0ada87531dca52d51d4f60d3148a9ba733d96a48 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Thu, 1 Feb 2018 19:11:28 +0100 Subject: [PATCH 45/46] ARROW-1861: [Python] Rework benchmark suite [skip ci] This PR focusses on: * ASV setup fixes * splitting the benchmark file * improving the array from/to pylist conversion benchmarks Author: Antoine Pitrou Closes #1543 from pitrou/ARROW-1861-rework-benchmarks and squashes the following commits: b608579 [Antoine Pitrou] ARROW-1861: [Python] Rework benchmark suite [skip ci] --- python/README-benchmarks.md | 54 ++++ python/asv.conf.json | 98 +++++- python/benchmarks/array_ops.py | 35 +++ python/benchmarks/common.py | 84 +++++ python/benchmarks/convert_builtins.py | 295 ++++++++++++++++++ .../{array.py => convert_pandas.py} | 42 +-- 6 files changed, 561 insertions(+), 47 deletions(-) create mode 100644 python/README-benchmarks.md create mode 100644 python/benchmarks/array_ops.py create mode 100644 python/benchmarks/common.py create mode 100644 python/benchmarks/convert_builtins.py rename python/benchmarks/{array.py => convert_pandas.py} (59%) diff --git a/python/README-benchmarks.md b/python/README-benchmarks.md new file mode 100644 index 0000000000000..6389665b075d9 --- /dev/null +++ b/python/README-benchmarks.md @@ -0,0 +1,54 @@ + + +# Benchmarks + +The `pyarrow` package comes with a suite of benchmarks meant to +run with [ASV](https://asv.readthedocs.io). You'll need to install +the `asv` package first (`pip install asv`). + +## Running with your local tree + +When developing, the simplest and fastest way to run the benchmark suite +against your local changes is to use the `asv dev` command. This will +use your current Python interpreter and environment. + +## Running with arbitrary revisions + +ASV allows to store results and generate graphs of the benchmarks over +the project's evolution. Doing this requires a bit more massaging +currently. + +First you have to install our ASV fork: + +```shell +pip install git+https://github.com/pitrou/asv.git@issue-547-specify-project-subdir +``` + + + +Then you need to set up a few environment variables: + +```shell +export SETUPTOOLS_SCM_PRETEND_VERSION=0.0.1 +export PYARROW_BUNDLE_ARROW_CPP=1 +``` + +Now you should be ready to run `asv run` or whatever other command +suits your needs. diff --git a/python/asv.conf.json b/python/asv.conf.json index 2a1dd42aba136..150153c8020f9 100644 --- a/python/asv.conf.json +++ b/python/asv.conf.json @@ -28,12 +28,17 @@ // The URL or local path of the source code repository for the // project being benchmarked - "repo": "https://github.com/apache/arrow/", + "repo": "..", + + // The Python project's subdirectory in your repo. If missing or + // the empty string, the project is assumed to be located at the root + // of the repository. + "repo_subdir": "python", // List of branches to benchmark. If not provided, defaults to "master" - // (for git) or "tip" (for mercurial). + // (for git) or "default" (for mercurial). // "branches": ["master"], // for git - // "branches": ["tip"], // for mercurial + // "branches": ["default"], // for mercurial // The DVCS being used. If not set, it will be automatically // determined from "repo" by looking at the protocol in the URL @@ -46,22 +51,72 @@ // If missing or the empty string, the tool will be automatically // determined by looking for tools on the PATH environment // variable. - "environment_type": "virtualenv", + "environment_type": "conda", + "conda_channels": ["conda-forge", "defaults"], // the base URL to show a commit for the project. "show_commit_url": "https://github.com/apache/arrow/commit/", // The Pythons you'd like to test against. If not provided, defaults // to the current version of Python used to run `asv`. - // "pythons": ["2.7", "3.3"], + "pythons": ["3.6"], // The matrix of dependencies to test. Each key is the name of a // package (in PyPI) and the values are version numbers. An empty - // list indicates to just test against the default (latest) - // version. + // list or empty string indicates to just test against the default + // (latest) version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed via + // pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // // "matrix": { - // "numpy": ["1.6", "1.7"] + // "numpy": ["1.6", "1.7"], + // "six": ["", null], // test with and without six installed + // "pip+emcee": [""], // emcee is only available for install with pip. // }, + "matrix": { + "boost-cpp": [], + "cmake": [], + "cython": [], + "numpy": ["1.14"], + "pandas": ["0.22"], + "pip+setuptools_scm": [], + }, + + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // + // "exclude": [ + // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows + // {"environment_type": "conda", "six": null}, // don't run without six on conda + // ], + // + // "include": [ + // // additional env for python2.7 + // {"python": "2.7", "numpy": "1.8"}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, + // ], // The directory (relative to the current directory) that benchmarks are // stored in. If not provided, defaults to "benchmarks" @@ -71,7 +126,6 @@ // environments in. If not provided, defaults to "env" "env_dir": ".asv/env", - // The directory (relative to the current directory) that raw benchmark // results are stored in. If not provided, defaults to "results". "results_dir": ".asv/results", @@ -86,5 +140,29 @@ // `asv` will cache wheels of the recent builds in each // environment, making them faster to install next time. This is // number of builds to keep, per environment. - // "wheel_cache_size": 0 + // "wheel_cache_size": 0, + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // } + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // } } diff --git a/python/benchmarks/array_ops.py b/python/benchmarks/array_ops.py new file mode 100644 index 0000000000000..70ee7f1e1fcfc --- /dev/null +++ b/python/benchmarks/array_ops.py @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import numpy as np +import pyarrow as pa + + +class ScalarAccess(object): + n = 10 ** 5 + + def setUp(self): + self._array = pa.array(list(range(self.n)), type=pa.int64()) + self._array_items = list(self._array) + + def time_getitem(self): + for i in range(self.n): + self._array[i] + + def time_as_py(self): + for item in self._array_items: + item.as_py() diff --git a/python/benchmarks/common.py b/python/benchmarks/common.py new file mode 100644 index 0000000000000..7dd42fde5abe1 --- /dev/null +++ b/python/benchmarks/common.py @@ -0,0 +1,84 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import codecs +import os +import sys +import unicodedata + +import numpy as np + + +def _multiplicate_sequence(base, target_size): + q, r = divmod(target_size, len(base)) + return [base] * q + [base[:r]] + + +def get_random_bytes(n): + rnd = np.random.RandomState(42) + # Computing a huge random bytestring can be costly, so we get at most + # 100KB and duplicate the result as needed + base_size = 100003 + q, r = divmod(n, base_size) + if q == 0: + result = rnd.bytes(r) + else: + base = rnd.bytes(base_size) + result = b''.join(_multiplicate_sequence(base, n)) + assert len(result) == n + return result + + +def get_random_ascii(n): + arr = np.frombuffer(get_random_bytes(n), dtype=np.int8) & 0x7f + result, _ = codecs.ascii_decode(arr) + assert isinstance(result, str) + assert len(result) == n + return result + + +def _random_unicode_letters(n): + """ + Generate a string of random unicode letters (slow). + """ + def _get_more_candidates(): + return rnd.randint(0, sys.maxunicode, size=n).tolist() + + rnd = np.random.RandomState(42) + out = [] + candidates = [] + + while len(out) < n: + if not candidates: + candidates = _get_more_candidates() + ch = chr(candidates.pop()) + # XXX Do we actually care that the code points are valid? + if unicodedata.category(ch)[0] == 'L': + out.append(ch) + return out + + +_1024_random_unicode_letters = _random_unicode_letters(1024) + + +def get_random_unicode(n): + indices = np.frombuffer(get_random_bytes(n * 2), dtype=np.int16) & 1023 + unicode_arr = np.array(_1024_random_unicode_letters)[indices] + + result = ''.join(unicode_arr.tolist()) + assert len(result) == n, (len(result), len(unicode_arr)) + return result diff --git a/python/benchmarks/convert_builtins.py b/python/benchmarks/convert_builtins.py new file mode 100644 index 0000000000000..92b2b850f2a0a --- /dev/null +++ b/python/benchmarks/convert_builtins.py @@ -0,0 +1,295 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from functools import partial +import itertools + +import numpy as np +import pyarrow as pa + +from . import common + + +DEFAULT_NONE_PROB = 0.3 + + +# TODO: +# - test dates and times +# - test decimals + +class BuiltinsGenerator(object): + + def __init__(self, seed=42): + self.rnd = np.random.RandomState(seed) + + def sprinkle_nones(self, lst, prob): + """ + Sprinkle None entries in list *lst* with likelihood *prob*. + """ + for i, p in enumerate(self.rnd.random_sample(size=len(lst))): + if p < prob: + lst[i] = None + + def generate_int_list(self, n, none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of Python ints with *none_prob* probability of + an entry being None. + """ + data = list(range(n)) + self.sprinkle_nones(data, none_prob) + return data + + def generate_float_list(self, n, none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of Python floats with *none_prob* probability of + an entry being None. + """ + # Make sure we get Python floats, not np.float64 + data = list(map(float, self.rnd.uniform(0.0, 1.0, n))) + assert len(data) == n + self.sprinkle_nones(data, none_prob) + return data + + def generate_bool_list(self, n, none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of Python bools with *none_prob* probability of + an entry being None. + """ + # Make sure we get Python bools, not np.bool_ + data = [bool(x >= 0.5) for x in self.rnd.uniform(0.0, 1.0, n)] + assert len(data) == n + self.sprinkle_nones(data, none_prob) + return data + + def _generate_varying_sequences(self, random_factory, n, min_size, max_size, none_prob): + """ + Generate a list of *n* sequences of varying size between *min_size* + and *max_size*, with *none_prob* probability of an entry being None. + The base material for each sequence is obtained by calling + `random_factory()` + """ + base_size = 10000 + base = random_factory(base_size + max_size) + data = [] + for i in range(n): + off = self.rnd.randint(base_size) + if min_size == max_size: + size = min_size + else: + size = self.rnd.randint(min_size, max_size + 1) + data.append(base[off:off + size]) + self.sprinkle_nones(data, none_prob) + assert len(data) == n + return data + + def generate_fixed_binary_list(self, n, size, none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of bytestrings with a fixed *size*. + """ + return self._generate_varying_sequences(common.get_random_bytes, n, + size, size, none_prob) + + + def generate_varying_binary_list(self, n, min_size, max_size, + none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of bytestrings with a random size between + *min_size* and *max_size*. + """ + return self._generate_varying_sequences(common.get_random_bytes, n, + min_size, max_size, none_prob) + + + def generate_ascii_string_list(self, n, min_size, max_size, + none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of ASCII strings with a random size between + *min_size* and *max_size*. + """ + return self._generate_varying_sequences(common.get_random_ascii, n, + min_size, max_size, none_prob) + + + def generate_unicode_string_list(self, n, min_size, max_size, + none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of unicode strings with a random size between + *min_size* and *max_size*. + """ + return self._generate_varying_sequences(common.get_random_unicode, n, + min_size, max_size, none_prob) + + + def generate_int_list_list(self, n, min_size, max_size, + none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of lists of Python ints with a random size between + *min_size* and *max_size*. + """ + return self._generate_varying_sequences( + partial(self.generate_int_list, none_prob=none_prob), + n, min_size, max_size, none_prob) + + + def generate_dict_list(self, n, none_prob=DEFAULT_NONE_PROB): + """ + Generate a list of dicts with a random size between *min_size* and + *max_size*. + Each dict has the form `{'u': int value, 'v': float value, 'w': bool value}` + """ + ints = self.generate_int_list(n, none_prob=none_prob) + floats = self.generate_float_list(n, none_prob=none_prob) + bools = self.generate_bool_list(n, none_prob=none_prob) + dicts = [] + # Keep half the Nones, omit the other half + keep_nones = itertools.cycle([True, False]) + for u, v, w in zip(ints, floats, bools): + d = {} + if u is not None or next(keep_nones): + d['u'] = u + if v is not None or next(keep_nones): + d['v'] = v + if w is not None or next(keep_nones): + d['w'] = w + dicts.append(d) + self.sprinkle_nones(dicts, none_prob) + assert len(dicts) == n + return dicts + + def get_type_and_builtins(self, n, type_name): + """ + Return a `(arrow type, list)` tuple where the arrow type + corresponds to the given logical *type_name*, and the list + is a list of *n* random-generated Python objects compatible + with the arrow type. + """ + size = None + + if type_name in ('bool', 'ascii', 'unicode', 'int64 list', 'struct'): + kind = type_name + elif type_name.startswith(('int', 'uint')): + kind = 'int' + elif type_name.startswith('float'): + kind = 'float' + elif type_name == 'binary': + kind = 'varying binary' + elif type_name.startswith('binary'): + kind = 'fixed binary' + size = int(type_name[6:]) + assert size > 0 + else: + raise ValueError("unrecognized type %r" % (type_name,)) + + if kind in ('int', 'float'): + ty = getattr(pa, type_name)() + elif kind == 'bool': + ty = pa.bool_() + elif kind == 'fixed binary': + ty = pa.binary(size) + elif kind == 'varying binary': + ty = pa.binary() + elif kind in ('ascii', 'unicode'): + ty = pa.string() + elif kind == 'int64 list': + ty = pa.list_(pa.int64()) + elif kind == 'struct': + ty = pa.struct([pa.field('u', pa.int64()), + pa.field('v', pa.float64()), + pa.field('w', pa.bool_())]) + + factories = { + 'int': self.generate_int_list, + 'float': self.generate_float_list, + 'bool': self.generate_bool_list, + 'fixed binary': partial(self.generate_fixed_binary_list, + size=size), + 'varying binary': partial(self.generate_varying_binary_list, + min_size=3, max_size=40), + 'ascii': partial(self.generate_ascii_string_list, + min_size=3, max_size=40), + 'unicode': partial(self.generate_unicode_string_list, + min_size=3, max_size=40), + 'int64 list': partial(self.generate_int_list_list, + min_size=0, max_size=20), + 'struct': self.generate_dict_list, + } + data = factories[kind](n) + return ty, data + + +class ConvertPyListToArray(object): + """ + Benchmark pa.array(list of values, type=...) + """ + size = 10 ** 5 + types = ('int32', 'uint32', 'int64', 'uint64', + 'float32', 'float64', 'bool', + 'binary', 'binary10', 'ascii', 'unicode', + 'int64 list', 'struct') + + param_names = ['type'] + params = [types] + + def setup(self, type_name): + gen = BuiltinsGenerator() + self.ty, self.data = gen.get_type_and_builtins(self.size, type_name) + + def time_convert(self, *args): + pa.array(self.data, type=self.ty) + + +class InferPyListToArray(object): + """ + Benchmark pa.array(list of values) with type inference + """ + size = 10 ** 5 + types = ('int64', 'float64', 'bool', 'binary', 'ascii', 'unicode', + 'int64 list') + # TODO add 'struct' when supported + + param_names = ['type'] + params = [types] + + def setup(self, type_name): + gen = BuiltinsGenerator() + self.ty, self.data = gen.get_type_and_builtins(self.size, type_name) + + def time_infer(self, *args): + arr = pa.array(self.data) + assert arr.type == self.ty + + +class ConvertArrayToPyList(object): + """ + Benchmark pa.array.to_pylist() + """ + size = 10 ** 5 + types = ('int32', 'uint32', 'int64', 'uint64', + 'float32', 'float64', 'bool', + 'binary', 'binary10', 'ascii', 'unicode', + 'int64 list', 'struct') + + param_names = ['type'] + params = [types] + + def setup(self, type_name): + gen = BuiltinsGenerator() + self.ty, self.data = gen.get_type_and_builtins(self.size, type_name) + self.arr = pa.array(self.data, type=self.ty) + + def time_convert(self, *args): + self.arr.to_pylist() diff --git a/python/benchmarks/array.py b/python/benchmarks/convert_pandas.py similarity index 59% rename from python/benchmarks/array.py rename to python/benchmarks/convert_pandas.py index e22c0f7fc9e70..c4a7a59cb77dc 100644 --- a/python/benchmarks/array.py +++ b/python/benchmarks/convert_pandas.py @@ -17,21 +17,7 @@ import numpy as np import pandas as pd -import pyarrow as A - - -class PyListConversions(object): - param_names = ('size',) - params = (1, 10 ** 5, 10 ** 6, 10 ** 7) - - def setup(self, n): - self.data = list(range(n)) - - def time_from_pylist(self, n): - A.from_pylist(self.data) - - def peakmem_from_pylist(self, n): - A.from_pylist(self.data) +import pyarrow as pa class PandasConversionsBase(object): @@ -46,37 +32,19 @@ def setup(self, n, dtype): class PandasConversionsToArrow(PandasConversionsBase): param_names = ('size', 'dtype') - params = ((1, 10 ** 5, 10 ** 6, 10 ** 7), ('int64', 'float64', 'float64_nans', 'str')) + params = ((10, 10 ** 6), ('int64', 'float64', 'float64_nans', 'str')) def time_from_series(self, n, dtype): - A.Table.from_pandas(self.data) - - def peakmem_from_series(self, n, dtype): - A.Table.from_pandas(self.data) + pa.Table.from_pandas(self.data) class PandasConversionsFromArrow(PandasConversionsBase): param_names = ('size', 'dtype') - params = ((1, 10 ** 5, 10 ** 6, 10 ** 7), ('int64', 'float64', 'float64_nans', 'str')) + params = ((10, 10 ** 6), ('int64', 'float64', 'float64_nans', 'str')) def setup(self, n, dtype): super(PandasConversionsFromArrow, self).setup(n, dtype) - self.arrow_data = A.Table.from_pandas(self.data) + self.arrow_data = pa.Table.from_pandas(self.data) def time_to_series(self, n, dtype): self.arrow_data.to_pandas() - - def peakmem_to_series(self, n, dtype): - self.arrow_data.to_pandas() - - -class ScalarAccess(object): - param_names = ('size',) - params = (1, 10 ** 5, 10 ** 6, 10 ** 7) - - def setUp(self, n): - self._array = A.from_pylist(list(range(n))) - - def time_as_py(self, n): - for i in range(n): - self._array[i].as_py() From c1d77a130fc571c5d7e8016a5405f9833cb6ac78 Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Thu, 1 Feb 2018 19:19:14 +0100 Subject: [PATCH 46/46] ARROW-2076: [Python] Display slowest test durations Author: Antoine Pitrou Closes #1541 from pitrou/slowest-test-durations and squashes the following commits: cf5e9c8 [Antoine Pitrou] [Python] Display slowest test durations --- ci/msvc-build.bat | 2 +- ci/travis_script_python.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/msvc-build.bat b/ci/msvc-build.bat index 9651772ca3fe9..58dfc2a146572 100644 --- a/ci/msvc-build.bat +++ b/ci/msvc-build.bat @@ -145,6 +145,6 @@ pushd python set PYARROW_CXXFLAGS=/WX python setup.py build_ext --inplace --with-parquet --bundle-arrow-cpp bdist_wheel || exit /B -py.test pyarrow -v -s --parquet || exit /B +py.test pyarrow -r sxX --durations=15 -v -s --parquet || exit /B popd diff --git a/ci/travis_script_python.sh b/ci/travis_script_python.sh index 9e74906d03739..7c896df9c840f 100755 --- a/ci/travis_script_python.sh +++ b/ci/travis_script_python.sh @@ -96,7 +96,7 @@ if [ $TRAVIS_OS_NAME == "linux" ]; then fi PYARROW_PATH=$CONDA_PREFIX/lib/python$PYTHON_VERSION/site-packages/pyarrow -python -m pytest -vv -r sxX -s $PYARROW_PATH --parquet +python -m pytest -vv -r sxX --durations=15 -s $PYARROW_PATH --parquet if [ "$PYTHON_VERSION" == "3.6" ] && [ $TRAVIS_OS_NAME == "linux" ]; then # Build documentation once