From a11da7f914e117675d9e662fa7b5f78dd02c61ed Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Fri, 19 Jan 2018 12:46:40 -0500 Subject: [PATCH 01/22] 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 02/22] 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` 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 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 zcmeFZWl)@3(>4mj;5N8>@C0`lTmlJ_;O-XOJutXKf)g|Z3GNysI3&Ojg1fuBe?xZm z^Ss}wI)6{qTXm?LVp#ODUM<(^?wbe|B{@tqQZzU?I86DM(r@A50HSbk@S-S4u$Gt^ zQclhmQC34G))|K@1yx+*(b`RZH=;kg0v=+|`)c)6Ul3MaUCO`-egZ_W7xqotF9!iK`8mR!dQZTFSxMoSK)7hmC_) z6pfmiTEyARLg=lu?BDLN-@vq1uC9(k?Cc&M9&8@mY!1$r?3{vvg6tez>|9(gU=%N0 zyzE_#Jzv`#AT=Vaqx|IgU4t|CvZLQ)QPj?U&TF0k=Mc}4zE{$FkXo#!8VWoK)1*h-() zBg*-g{(rXpKj~Gh99$h>TywTImA7{_cZN;&w>Q_{-v93uf5#GGf7v{pNW{ErheO(oi?9Ow#|kp{e?iqfIz;*uV|{4d}@2Ne+TP*m`4TL$1~sDSNNH8-HO;bd9RwBrzRfO$D*Zr{Q`CnZbo z#0tsLMK5rU2tWz{=Mk?(?LUs-dy~%ypr!uvEa?E8ef4H}SW^EN(r8uVp{&$phc%#R zbrla7sE$_%06oQc%92lCFHptMU93N3o!a{0!Q(JtL4L1fd~uPc)<(^kVl&$Gcr{SI z5K2L#TR*V3DwuY$eiv`vdvt&F^=jH_UV_e`Spl$O3391iUO>ziN$m!&fK&X?T0j#wMA%7?PREiW$Jrk=f~uW5V_*`++58RlDyEJo^sPE0*}A=YVpZr52N zv}0rLE2wt?w_CgI-1`~B5<5m%?U@wdgbF$cT0a=@`19&KUN1m3fz_Mlb2R2<D$S2{K{46r@!9G@tp;bf zSY?e*A%rd@i&Cc~9sVcbkM#Y7y27Q$Ntv6WcV_Ed zuEi!y!52M-X%iLZkuid7J-zH0)0(H;cD===rZ??As60g<)%k>e8s8#fRcB;zVERRh z`=YMhj8QE9O!|b<7}8N^XfE)5=sF2mG**8KY=vxcwd--V;8^S|FGu%_{h%RX~AP@jsqD z?{mz54V0!nhH1{THcT}@3Z7AiQ+5G7*^O~1_wVkNiLdyzgj+q#Tm>b1n35h~glg7rt-2ElkJI%j$uMN@G?5ShteD3DkqDD5sYvnN9 zFAXZ0o=(k8`tAj7xo6zKJ6M&M+8SZL-5^&ICLSirETAF`iSGMkB(vzH^-MY06QI`; z_I;!FNJtb6LYOF#?^t-mi!wHe0H(owgD}kEILQeY;j=Lujb?zneSat zYut>Cmt?WKk7L5S%R&g~3-hO4Sqr)Sr?nJ@fOS;_cmK!I`e?nT>*A_3!HLTVhp&mB zP{PIE1Kl4dejdhOM2MT9p7}9@H&w-|da6iMBWf^Xu?wkEdWiA$Rg74}I*aMD&$TuS zb0*EzWJf?jSsuw%S)}(Y; zKo;}pGI5OB+|BDZamqY8n%3dst|-xxT(?qk(u{)nxN?A?51K?<7Xlk!OJUGRK|(xM znK@@07bhGCLh;gr_zbAVEbGNIy@h9_TQw9a^~O6-_YD0jzKl4q8pf85Fb-_B4K!%CcUm)o*X~lHLOE6vZ7ZFFkHcE)Je?0D5i^ND&lWEOFF}=Yaw|UVOjp zFFpKVxTP^8z}r`EL7XLv9ko=l>=6NsxxHBA;{abjWVNMZRp%4+^&{*sq;a8C<1a~P z2;nb5o|H1i-}(^`m_I6fjOqxvYQr?qeL6r9NpUHCN^2&9F%O_*5oeG&qxhlZ59%}@ z6dvg}#Is}Q{~-g5{vk^NK1i2Qg;8FZ@6YR7*AJLNZ9Rxo^I@j0UlGO#d)gZ|*!CQe zBgaTqggCi8KubceGO6ohU^bz{6lN1VOTVk>E#g3DL=WDb3^HL&?g?U>%=SMmMOHjE zu8;I}&pAD$;?3?cw5+YPwS@(Y+jDKvFsIIyBnyxQ6%Y8A*z*Kb~hi@ zqlPGgK4zJ`?VH3Flr?%s&!_<2@??+pi#7Tso4(B@n7MM%i){6AF7|VM7YXJR=nsiehM}j4e2JWZycr7ljs_`xS4aB;yxPkLjOj^qu2RW&9XE9tQ}X*ke17H@&*TSKPz2?kjp$00u=R#t9=+lGRwee)#Uu=@>LZZmNP9|As-{eX@HIIF$_(F zP;N@&PhE@lfU%Oew-SZPVCMYzJz(cZj1Pmw{g6Y*W@lWKl)Ko&2KF@eGNupma{?SJ zJfe`j56o%xbacCdZze1i-|M0K@U45aWnWzL^{;hJYY+Er<-N6)9?9Y6BNY@%2XGA) zBky4eu&E{o??tk6?ZuF!#U^l9x=o}JEb$r4(e*GaF|@7KVECY6jf~y`_q4)$c}UgK z1+XxQ>ihw%uMCGrc7Ol=YUb|Hj{m7n=viT(^5czyp=MiGR7K5_H&>&N87FN(Hr?>x z^yW%Jl{yy=Q=<4)lhZXk^99;_>cwNff_+VXqs!$(pQv;Tx+A))uZCQim5~XT^QRe1 zyEP0Rxsl#{>!OPei+;1KHI8}HO;i?B^0!F)hC=|pA~$HVEygrh%xnr{ID6VI&nZDE zbX7_>Ix7`{EX0;$_}Vg5K zfkw$^!AGX|`pM_$)EUX5GC3%KRYzZdE4r&4&gPEeHp*P+kG8-z*7^13!3yBiu9%(7 z1ozS>L>ghtR&Zfkf^kVh`ipLt4M!11p>gb)A7hsyVxNyoQBTY0vp}R8F?!#QArXbkm7KCtv% z({p7}-{H$GoWYUi-`E1&0~4#4w+IQR9?>T)O1IKR{P5-b-1sXh(a)QK&D2da(C{sW zlHYgY)FoQz4laye_;+E*3{wALCT87@b<}CN@tee=|qSvZm&Q44q+`q ze6N}959u5rP-A6oH+K|dNxqE)2xcPozi}MOD#@vP^HNzpTs#6baa~(=!csK?eYXnT zclV0tyQFdy=N!THmm&lBMJXB>j4ySE|A42Dvvx;BPB`OahcJO`1CQAgKfN)J1Lwwq z$eFAwa%oAoaCW-97e8JKs(w7f5^|Cx4@`&IwX+Oi*1*FW8GJ$0a(^4Wcm7^99ZxbYZkmj-U`j*a z6*-%amk`~>s{`c)Ud}Pft+?$|WMs||QEoY9H8Zc;7l3O}YJ{J%gx>6CqHnB?<}s>B zs47+P2H_)aCNsdC@o6bM*^1^BX}52gye_2^ccGX0XY=>;Z-|m z!b0Qaq@UKeDj6B78JSMzNQr4@k@~`HxICwOGt-ifZomJy!_{-23mJa(n!RO}DW zvqp>%u^oK}#rmdMPInY@B=KWT)OroW=mzxQ?Bd61XmwH zay~b&0=tC|M_6CKn3kaRM0aSu^rCfr_0##~La>9}cw2BUqXU6DH$N`4hT);?Gi5d) z)x}~`-PEP`7T>YhJu2W6iXNo%$c?ZeC3t9+Ema6^+OgEa0QMvpU zRx1ai(nc-nW7O8wI6o1FDRkE|Te&>lkL-@sDL=6@&4;i$O&ARcEb1cPtD&ALO&tZd z$hLq$N=@yBLLLV$%k?=SAH{+>x-U!>#QQ2!%j_D?kV+i zFbfQH9O1$4SMT5Lf8M49Q5VoA<)5?$8-xedjjf9q<{>PDQs_e6A>s8G!3*GY<57`VRU}2;(tl-!h&81 zhj}D`n2Oeg3PQ^Cs|m^v@>y>@+GwX1>&wf%;lcsIvxABPB(pI5;Ov48!eRr@I^cms zB$<>#V((L{`gnbkC0n1MWun|0rGSMHfvB?=TUtfr&tT^Wf>*{}ay3T!6&pqz-`~+4 zM13EL03;#~YmsfrA+KWVH0@vds%824EJ`MzwJS_wL^9+30|5{IXO|}_fC&q2M}RaE z!vofffU6GkoU-RmK;pO_@l7-PFa(u$b5Ph@vj{pUiS}uxm2H$^4r?w_JPeVF1AQd| zAdwhadO*dHatJY$DM}2o-0w`A7~SglwP@0HOYJ40Kj20DdJOb=cvmr9ZY#~x%@5@& z8IGzR!)T48TW3pmOXOwyFg%S?FW(hBDPiH=N*X4`yYem8C@P3h{|;^VCB7z_KTrtt z=V)O1Mcc8oX_~`NgCx_A?8JZ-ly6=|4}~8%rRmA7Fss@qLa{`ynv@{a1tfjAvxYK4 zy{#~KP)y=J{aLVd4l%HJC8LI>O)mjHeni5L21$0%L~2Rt+3h>qk4i6#vuP%6@IKav zapkK@ZH6jgWl{)%UEi}vJRJ{=QCy&+IGc)q_o2kZ18@Uu@i12_PC= zlDs?DcmhoT%A0Dx_+dNHVa!pxFeZ&GYWYW;r-@f^Q65k9Am|JdL{b=~iAgbVL>b@m zY~`Pw?J&j*)3cJnEAeh7x+sAS(dLnz6q^6pE%kSxKn5qso5ZB_dm$7Q4xdu}5pM~) z!cycnz^-^j;2JtT+hstdJ%H+n^F0?tL||r1K|~^g@z$1t`sq?3-4O#M*1C4sGPOcD zL*pTSTZ&Ab*fHGKgh#F9b{Sa}Lo(TQMG3RjiCtg&BpoE~_)pooCtoIp{ZR;N041#o znRzQH9i<9+^-U4uGuCuC3Q8=hEX}j8<$|k1iNURY)t^x-{hHnr8UhE;YuX^gA@9Wh z^7@@U&kO`G)X-H3%)`Rd1U8Ea!Epa-U zJ|=JA-YL(ikg2bkHc;T_4}r47 zTwds^io;7-9OX~&8w(!EnG(Ad!7g|qG&YF%YTJVh1x}CuYaqG=2ZK3@YXJHW!b93o zc`)J)NC&$Wi2`+i2KXl!0y8?WqawH=-?9l;^_G(g6URU~FKLBDE*^9aiiL22H#(hr zN?tPe7m1zGN?UHaY+)3k{iFFX;lRZ++8a@aI)F?AiHV!V*7!@}&|2*5vJIH@Dc%FUquBelr4amX)xf8*br?q+`0MBa5C?`$QeGBsRvw~sgn58d_CGQu4m42;fPBUv*2f$r$x-bH_+`c?B$ANpqOUTr`zAC_ zUnN$HD3_9}5hEQ?7C;%${8#+pabBW8)Yb;!ereK_d*9-8Ys(Vkn(^?L?vJ4v393en z)h3J#>=979}@p@l4oB%uQ> zMrMHyH`V`n^#*kQ4RjZDn2DW}K*K-$MXKtrDA2|yh*S8s0#~4*Feb`CZ}9=6B$Wt6 zl|`W12G5!xrUH`XN2xGm31x9|E(&bSuy>OG2q4VGJ%`5{n!^|hB~JBb{f%@|jVZaj zDRI2_AxEl`d?lKJeYTD1j>!uCOxTqov9!y*DE%K6b&LRsQwQPHMLS3;2Rqb}D!oS+ zq;nmjOux&~RmiQR8uHYs5`{fkxJX(I0MdKZ9NZ%lo?bZ zF)>YESQCW-TwYqCuF;F~sw|x5IONtrRaL0^F@M145dXW}ml*I^M47-=Y`cJkfY{Jq z)D5#COw^{TCf6K{VcH-=<`s8SU1z)8OX{|p;8v49aRaMY|H=Y2;F!KcAlVq|0VUm% z-!fuKjz#>)URSmGj>}c)t18n?Cz%u4qS(3yT~Hg;1u7I?Gdvvn?YD5q%D-nsT?I_1 zzO~8ShDvTJ6(OwgnU)SJgmX`Swk!KUO(9Q@AXEj$FuO8Q7WZ8Z+Z(T2adkE6Je z7UTlBYw+ga0h&BVEtF~pv|>7?(rXY(cak@MZPOh;8bBl?Y3UpvJS2Uux`tHi!X4G_YRkT3c~%&GB{Xd~wJK zX%eeuxoVY3PqU-SyHy`WfoJ0$`!njiU~ucn#r`boQjN%Sip_k@G9BJL*%;C^fyKcj zR-%`$UZJlLi;W$DF>kq#Nao7me({hG=1~Z{6aN|_JgLbP_6V(H4<7khrrS_pM8)TE z;-3b(JoVhTIo}N;=CYN_(#?J0SjmS3fSgaY6Hb=O>aZRUq^V~} z^rcz-pp%wkX&C(=Oh`Lx080C~uf>G$WCcAlp0>xI6M5(Z*n^OuWgX zz(DiZmiTv)YD_Xd{GvLW$pR^1!WTNVHrv1Q>%Gr+c=S|pVy7?A2v|zPTx;cow%Bq5K3jEO2!xu6=; zovH|RyhPD?^B>96wbo;{H)q=yW6EN7_v9{L4mZy_8L7P1n_lu< zx&HT8%gAJ6@9*jl+aEnSUmG5EF_vpl8Xz1kOQwAiiu2|hdV276L4I~ z1-8E=uwW8%P$vyz6?*)*Qw)F~H|>k9Qx>~-{c(HPo{d)M$!8LgB_T4YOWn170W&NR zV%+#uJgG*78BL&oxvu~ZV62-p!|!4`Nm+0`aY^)cZ+)s~rNNeZ{$bgT;Nog^ z!TZ9=VivZ6boNS*1+Tu!{%q?D-8Ro^59ijx!?xRU5mDv7;e%=9S{eYYiV5s~t`Pm2 z=qd@$n+>ZwUBt;Sd~@0|oA;aioDxhV$V7b`VFt{m-hRHBd|Dv_!n*UTikg|1@RWlseF$hrwbfE6$TfGnw%E>E~guvR`=#T7Gvh@L>`Mv z%B#`Y8ap@=nEN=B_v>fvxxck_9pP%M7EipUv&9CV#b^2)G+hj`RtRh^A(mbIEe^14$hOFAUYyG`l~Ui9sthc=fQw!inCwJp?I&vk_*Tb^JS^(VMc62eu#*NAa) z-AgZJWo1#Yw?O2SjBX-dq&$hk4%#0d$eX@Ppcq4><>J5>T3bzaXw4dVU(iBC>vwd4%){sC3s;n#I^4OA zq#^-HMN*D@87{PsM3g(7tYjG{t1-tky(PhRYyBT`XX!Qae9 z&o(|eghE#dywArqGrhSf!`*kw8idvbHZxo#RST6czZ)BqN8=%!Z{@|En19ZFylsC( zU!J!d=BxM~2pPTKF6u z0L%$yF7|>SLN5Ma?qT{m{6J*r%jt930YQ)=zWDh-xf^3QTOR^VXh2=xCoJ*N+l~yN zLa{3Pfn8QABpU+fEy~sbg=%2!cmFtox~~{sxw`{VgjYa6zj>Wr9TbxHEIAT|m7Z{) z4X#ZM<&ujOVS*(DFSs$WK5(+1A(1S+*uXG9e%4F!{hFin-#+eq{wPhQ*oh3+k^y?P`}HKBC~ z6H9-*Oqbt>N(1U1I&f9~yiMdRPVAvybJTd>PyS;Wf;hFmg?%lCpSzY~9A3`N$xkj9_DGMmU?>Qa)bATvy{7#TiaU?D11)${U^C;z2 zk{DSP4|!X}b~B4BNc8c}g^_aI@bN}7RtSn?63^{T;k?Iwkm4AvWWPys$HpmYWgTwq zOitLQ1A+9Xj#_AGjdXlxP|C3KoJ?E>n}xbrME*=y!U~PugDJM_f`L*Yb9+iHS9z#x zUo4jDmyjPsO%(#OrR)@~bLZ9MuCM(#808?^^%x618&|f9UR+W3L zL@;JhqJJBj<*-eV+Vi?vOHjr@XCh=Rn`3m_n;M-CIfpotf9CEKe>Y5-R}~gkGkDN; zYoAxsLy7yI(&Z=T#e}W{N$3jsCX*a3qHk`{cQ|9A*LyTVcSl{P-~{t7#{|VJk!e)6 z0y=!c9~z{lX)qXR)3p0TgwKA?%9t`FUy(6PiOQ#pW6eCp@V%FDq1-w#NgksOx=C^t zWGtb^hmA#$3FohWWuka zCuYm^UP)7it>OjN;54zyMPp-gaHK&dw~n1xlC|QZ*xLkxDH%3}dRI6&gnli(Tt}_$ zWu=l#rP#>m6}J{Jq!CC|>fkX=f!eGB-<*QAYwv!!VeLIoS$GO^N<_OwVz|bNFp|zj zb9Od^ea(AB=3?)oJYMZQ{}(!dA)%@E_gv%wMQvZsU>fOR@dAd4%Q`lhGHJW3g0G-# zF{?q#EQ~KnROd3o1Ob+rOQsk$>-`Db1zQ}_!&a+ORL4AC4vca9^tFP-5ltl%Dmnk?J*wsmM%VcuX;Do<^?v z?(LGaJ?qd8cJ@%KpN`FtDM(4|eV{6Heu{;`;HfZ#G;XCii$-Z&~Ui zvC;J5n|xRh1rET8IgD?Nb3_CG@M$N4CbH(p2#hF23or7Ug$7Q5V0TZ;I>^PZe>6#b zLozZN*(BcG8nY{s&wWp;OGsRo!!;$chtSe=W7-O+)gHr>lESC2|8H$#1cNwZ5r3%l zfy}pIa*VM34uy8ICU?xm1_A>fc2Y310^J3$GFUvk%UTcN>$6@&;-)E@z+4X#v`5)9 z40{>hUs`iy89x@ONBO^=5W%gb&-K7;7=e`5!P#P#y*h-%$cz#9-%VGgU% z|4dd`k7h27&geg3KIFn(@|NJX9Co!RkD(HKPqrYH3^A_aslDa_Li{5QIz~S9C_}S6 z-Y0S@>$AZS3)d53>3wAxCC-EDc>IZS3w|pGd39Nfx{8kcvong?LIeqzfOC~8ri%r? z(DcWgOu(U7*De=E#b{;r`x|T4z;+K~nH`1jQ=Pz%4{&l}$3h9YBp`%-bJ$&qjhecz z4>2Z(He4JC@t!I`s1>u|IhUT3V-%29tZc=rFq;awR8a3A6<)tyc~kystkzmJXcgb7 zi7!t+fSqHVWyoc7c%5}thOOP6l0q63?3 zlpdCdXckFIN9hiPBO|i@YojYrI}Ag%M}IB9h`#7yGW01kRWUj8E0kcFI)5o7 zXSk9iY?!HSS~ocXj>y6+hm$C5O)@cK0t?wv*g&WmVOD}X&m|#tX_fveBL)#zXzV`y z{0kS+ie&PxOF@wXY+8e-J))O=;fjPs)^t`thNg~aYN+X<$mco<ddSrqJ@R?sUo zikbl~kWp(M%r~TY-g<(vtx+mp#ASVUF)udM9HAMbX`=azz za6+cibBPImu|bY-4EgE7=La~3+|k#fMNT2|pKjh9QgA$t)ihloUIXX>9E89enZqf&_|UWz+pWs|M91?zPrzB?YS~7)i=Z z6wajbgcaHcb^lMOj20vVV15~eJ(og#q2-HqOruBQ90iUFgdRuKw}t}cWQ0e#zGxCm zW?{yJOr4&R|0XiIOZxYgC7pu-^j?|?qsAI=yxPGn!dl~7jfa{PDPW-WE;o*=+!BA$ zpLix{=81U5U5jDjph@)Kc~xEm%A6L;pn6t(uH3)ToJ*Xh-VwUmVxS9kiN#mc0Q9Ar_-}jkZbV zlC0vgx8RYql2-iuoPllGF}j_5D#aK+49cWk!23^7kp6Z=>u-+*tJ zwkBgMGwWIZ>LxZ@{QkR!{urc)FuH z-uG{@S>fECmW zKy{R8dkVRVK{KIk@!9*Y)UqJ3i(~yzi9M@Y#*Z$a1ai-cldSdDT?S-=f90VuMqCj; zyo+H`f?U=USBUr*N8oh(cL*4rIezxJ_QXX5lIB9mH%qU>YLD2<6OQo9`hWE%qt3_~ zIhGQW4PzzA9LGp+fU77h3W6a%y&*MFU9Zfea!R?;dWTdC?@c9yic9w=3mYDgje^gk z-dLPXotRUOk>ghj++^JOJsE#oByE0h;_Ky*jp)?q3_cTa73lUy|3~g1&*-5f=grtU zX!jWU@Z(`-h;Jdt1 zB1vDk6U2D(0^^a8QZrzA*LHT|-pAl<#Xm~g`D&#VKpsFVhEW#Ke`HdG^*8_GNCQz| z8v6<&nlz=}WQ7_r5qwO_fb$s?s2Pf4SKkkCzIC(9{@CEUz$)P4Kz+v5?U|#TOQT}` zS8(IS0e1*sIB(Pys;!ZnF`9b_QZU!dW@8pQulM+Y^moJacgy-}_76ErZ;_~^ZX}jJ zhrW3O(~>)x9O2?yP&eE40F*wyc@3UM!5!__GOeezkLB`xC8L*wa| ztW=+F^q&=TcyYfWrq}55uc}I<27t_0efk8Vb~~5~KZUPNkrmbXnMkz>ejEZ@=h)d+ zzb$@}?IkKehPw~*8C|z^1GePe{)e8}4!oB)$5ZrFmx+c_v3V<5CGA{$(L2s*;q*{; zXp)N5->s=&0Ms3aNkFErCnVSYh)`gi>(RcKKu!FKf zCL|3dq-fYn;X_RR61>wDkOjOQvp;an^|4iRT4@Kwo$5>%ML4_iM5SYx5867dJ@18$Q_tMM3 zGFH@O`*=iiUR+-x;7!Rz@jX;;(i@^@a`>R9aPxq20DOFdl)n^r&Tg(nJS-uePG=*O z{Q_4rICXOlUeJ-@?R@Iy7YsByC<<~#Xn|x@ZcA?LKPK#BBpd<-$x(lY9Fq6~x?7;x z0re4cq*Ix2B<|STxrpC_87wZy@V)vmfrR4Z7OhzC{*m8~1Rya~0N*5luBG8`@4-2# zrDyo-*i7SKnMiGUV(#9o-uIk8Bv4aA;ljRQ1!e_IoeEHrVvYZk_FQ@gz|a#xGNE3T z-O>+-k6oupyG4Nt>Z4t6gDntL49oUc+k80nKJ(a;Ber63GTk7A!(XOv!J#}U9jw;0 zqD~a53d{fKhC`wUc<4#Tq>MGy3bJ?lc?2ZB53qmjK z;GcR!uJDX1xAdX3R;1n95-~Ke!rU`)&J>9Zr&a!vFf^B!GB&rKh3nH6DH5vGY!){# z5BSHuiN8kWk=Su{ERaIa70&geNozpi$O;?Y`MSX3uJ%fp{HtQ{lqvHmA!LH-;IGT= z_WNTZ!qFhv%2bF=9YyCFAHV$J(4)P#BT1B zPNt#YD^M@DFX>|Y38`73x^dV8H^5@iV^BlL0)(&Ug{e8d4S*1V$s zZmDXoj(#czzWg5Zdhf zd#bp&0>!ZvuPyy^X2m=G>Oq6pF-vh%YAnAeHu;K+gGEKH%+yCcfC97YB;o84`kO5? zMD3ldjysr8tNtX3)SimSVSLyvkPQZJd+75|r8|UyY*KFo&2QBY@db_P{YV8*50{!y zSs`46uz)x#Jf>R@K?4^aBo)splDs?&E8I(I-~+rRTqd#~f*)MjpDG&p*RYA@=NgOC z*-=Fzb;RKA*MH2Ul>hQy;=;ld)oT9XmIQ+|DNhNRw>Do(X;lQ^%g4u%nhTG{TzRMf zbX66MzuB`*XXpX6@zPH(QM@rNW@%$kA*k~whE;3jw_w)RD{nV$v)~+&!`7W;!$qJ4 zJfU|KzHqinKRg8&XZFzK)cHk9$#25)KB!EM)2o<8D^YMLt|3a0nJC$`rxIu`IyIPC zw1-vrJ9*p|xzeiO!vmYb-fEoT6NhOP`1shN-u@J8q*gELX54P~!d18GeTn5@X<%X#)3>a(&{Jd2AtqV`!!v5|&!b(X2`6r{7G$o2L9 z`;*2B8gBT6EkRAer8biykI4dC-Uk!9*v+5umvc67dZCn(vMH+-sBZ)C8zzw>uTKJ=ZLte z4X&A|dg%f#5#W>M3ir!TVmg^SNu`%ReD( zk=0mdzPs4`%zJS+C1%@=I=^4^X|d`o8sm0nSaq+><9&6U|6EP-Tv4vyb6(;~o2>g` zc{;?|O{&p>*sak)(ur~tmz2_)$JW~f#q_%0Gkjmoi73WZn0Z!PiyhF1n;WzVhZqXP= z8+IwQYzft9c_(uQDyt+j%rHXNv|lsctuZuhIk`HRsD6*BTkl@eD(n;+^I+R-9xACZ z<j=G)5a5PH2k_36 z6swoVC$AFDmgva5Qk*^&l@^lJpZ(o%yO?Dn)-k>8I5(DpS&3)wvpA*O^T~_)_LHX;O^Vf<4ddQ8i(?m^1E;*gySrM)9-q@_(FvJr z$vDKc(X7)T_F_s&H?bofuYGmAdb{q#ZF`das%E5prO`eaLbXH$7MdtldtF2J2zx`+B`I~KdtK1SinFYQQ1(aV&jLeF!T!NV+f##v^mA_j?7 zUru$kebX0le2st1wfI;yWqoUE@5g>isa0tbL@`+sgBKv%$7s7Zbj=<*K)c_WCp7yr z#AYv)=;HaOlb@So`wK4E5h7*K{SC+B*u&O_QKy@|&Gt-}Y2n}PuUv$ilyzGuRy*)4 zpF3UD*tm8c>`UT}ONfCQF_Z4l26y<*q+;+i`}*truCREXRKjunc;431TQ=)Hi8b*@ zO`LGKXqB)mBG(II6fL_u;s>u`XP?y;F_#RVu~&G`zW)2O+I%NJgEFYb$SIhzlTGg6 z+r4y~hdQ|x{P*`sPWyV;T%?dVgpStxZX)SG6s z{v%(jM_lQ-;axSgZ}I5>C7JiOtLvc2{-+6dHY(A^giPT|Q}?%SHfr^Tn=eKl$}w@m zsi7pZ8RbR1KD0UAo~kyt-NcoX37anYmD>7EyM3o!VrIK)6nqqDK)#U984gl7hNw(3U~3_IgT?gvD0G?ycMW0;z^v3(oW+zN9?54>S5cY2YwXS-$kWtP#PAch-$- zI(N-(chJSKyDv<)e?=^7H@?doy0WYr>%z0>`g&}+aQl^8h_lG&RSO~wSu_H8`vO~W zB#9$GP%L5Ln7MHWC4Ou6JCeJdJ%YXG4?g=Lv?Qa75z&~_*m2o{8U+1x)~bTxg%_E2$YQlA(mtb|sV zu_{mZaUQTHE$VvvkYRjV9Se#GRtL^B1-1nfzJU=O9JcJ!{{m>8<#+b>k3?$zxkeXR$m>^bz!~a4Be?R|skUBQ`r~j1S!dv{ihOeSU^}^X8t%BS-W|-XFb=vH$r2f> zm$xf@m2wcRgAWty%y?yf-}%*3mcSkX(|0S6p;H_C!kOYLa#aPLRjn6nC+?nmd4`$W zo7kC)L}N?bF=J~~eUVW9%n8=pna9{Pt2l$-BL=MpOuBKorJC(&=E{Ct<3bG!ryi#h zDU6?mYd(QpzZ(;`yf{3=onH)2a&x+rWS31hQWz8HR`Lwu z?}n0e@C=h5Ot7r|zh4ili?Y^@zbZWFZd(Bpw%U#((vUy7qJ?xEQ z7dPsiQ*}l@wepz%%+!!r+ysj~U%r%*Fh9M1$WwW|&BcN$W!JoPJ!kz4r#EX!{H4wh zcb@cm?J(3*L9~ZuobBT7OFq!f4}Juja#awK!m=O)tf`=0gsZnUVHsKBb74ED-Q(gl~!H7nqT|-fx1;Rbr zInSKc#ebg08wG@Hk~G!FnXIB&=I7BavA_Vd-Z(i+G>REsBS=?|<}_pvJmW|PDSSiy zfVn;+`gHw>13^_%zZ;cisea;1sPsK$)n1^J*6h z;1VoHy54`xmn;rLdBzQcssnS)#h)fo)FdHPVOKhlpEbXo>Fj@O_y!wE?d8I|VvDC2 z_h5imPPmwGKw_!AsMiBDx&Of{Dq!N`Si|RQttdl|b*iYnZ-XW{Eji0Wc=z+*W_|vq zn*;v1$+7T3y${`cUQHzf`W#BCiu#Pf7k`Ud|64rq6iWQ_cqYLLdj7Z4=-UF;$KIC8 zuAlUXE|xJOA;1e4SlZa=G^FcB6*-* zT>Cqe>l!Yp6%`e!2}w%rA%|qbV9KOfsYX(aLx_^3!HSSZv^UBzIgDzQN~VQz8ss$7 za#%(*GY(}=hMZ5|#5fK!Sl`&~!@sbvYya~7@Lb<_z0do+-}Surdq4N{`meGx1Y?J) zE{}lH8f9sJ?2gDFcDU?(osI4zdz(WX96d#rwtA6D6B>HlmHgI_sq1lAhDSS_U zrVZdLL)!bbcm%bzLe5BqNAj|noUN>W1|@7hxE}nE&bB?yvqH-NyMA}iFWp8R%Tlw@ zviH{iC{1}o9mnfmre;#p)7Qo3y4Cs?FVC~&YlffuA616lj;jW>L!h)_P}CS#&~agN z)CL}`tVaxk2ACXCvds~|jY?a)$}d>d0PMmp>X=(b3Zyhyn@O(em9%#l9owd9%qSTM zpS*KLCxgt1XK_Ic?3udi>O5+w?S`qQZ+|y4_>II-ATIjyh(s8)*08;36z zojF7sTMS>^R5L3KWZ;z1bMs_{Qu(1vg1q+Lm=I26AzgqVs`>mK`+!#==pF(POMi61 z{H9jWFB#5dO}R|>be!dhxR>ap_*rXk{_bFUnEvx^mK#WK8>*C@C1kV9<~=;AN(`6Ph*P%FOAb$VD_k_*Gld4qg|?5vBS0RCjdf}dRFa* z*=CRPLUAS;b@>$KsdTGXntyQF(uh^}^v5Ao?<^+{91&V3l=bH*g;6U& z9gsZOM@001nf0HNN%VEBGe9=WUDwMKxr6|vYnZrfqYjK6Na3V;s=l7AhI2?&K0L2rS1cW?Eo!8hrd=aK2I zl}*^tJ~7bxdhtQEQa6EuR0a7vJFRSw*bi?mTVeN4DSE?8P&sF_`fV4z73IS3nsi^J zy2f3}Pcxod&XOaOLmTr)j}knH3w#!&Lig4I0*;+) zNB8@sGKV$s0NkdYNq3;n&&T96t& z_OPzgOF3Mo!=fhy6j%C??;v$ zQq1l!e%be{eux6LC*w!sU4dz5!WKKQHTQlD$(8wD#WyCz^mP@l`a{rft_9%rVKUJ> z+b%l32@PtCm&Hbmh4YGoV{rkk!h7JED1n0DFBJMq&(*eAc-iQ^~0}&|>grEaVR>fL-$Dm00%NdlpO(ew>#g`-r zqw3vK+wqExK?7O|ogJpSfs6L;aO3b@yxTA=`N^uPC&JvIG0ECv<{btw05%JACfZt! z=D-YtM==`#!e#Mfrp2koweVZNxlufk82a>8KwA&2Z>7_KMES^%-9?1>TulQP;q7#r zuvWHGO@>HjqVxxhFy4_C@%~z908~>3v_9?ep=qK6mVz;1@MF~Y2BOzP7Y`65vzqw9 zd2`WZ97k#3H-bGOXgcH9aF83E*19ONrAZ3gEO$Isit0kXPHH(8a0QynQ7GUyl!r|l zt?cZr*0P{2S*DB#mu9a%Z@TuRdoAd6Db|D>y!rxyY;Soz^kP9`;J8eB+(oh5KI1c4 z6qy}n_H~hu?5kzxXF^)O?Bi~oOqS@P%~D2aUOe!E4fF8=^V$n14J~`Hh;-^6**zL| znIwK0rMSU?Jr}L61La#8C7NLN5@Ycwoz=&0eaLCD2X@nSkH|3qotC&%wB4=I%FUuI zkN}lL{PJN%THTNqIJCf=zQ&0D#5dp@Y8sz<*XMoum*FvJ&@isZgZ>0X5r;w4HW?B* zuK&|uDTQ&UL{i?G92Wen7i`fKiq*51;(wy1WGQE9=as)!Q<07u0*?CMZv4}k|3Cl7 eBcOM9Y85j>xPU2dmfpX(?yNQXbfJ~UqrU(Kkb;T; 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 03/22] 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 04/22] 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 05/22] 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 06/22] [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 07/22] 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 08/22] 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 09/22] 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 10/22] 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 11/22] 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 12/22] 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 13/22] 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 14/22] [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 15/22] 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 16/22] 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 17/22] 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 18/22] 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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