From 5a3ff3151bc0978677237271118fd39cedafbef3 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 1 Jul 2015 16:52:29 +0200 Subject: [PATCH 001/436] Initial commit. --- src/commoncode/PSF.LICENSE | 635 ++++++++++++++++++++++++++++++ src/commoncode/__init__.py | 42 ++ src/commoncode/codec.py | 165 ++++++++ src/commoncode/command.py | 325 +++++++++++++++ src/commoncode/date.py | 59 +++ src/commoncode/fileset.py | 176 +++++++++ src/commoncode/filetype.py | 230 +++++++++++ src/commoncode/fileutils.py | 501 +++++++++++++++++++++++ src/commoncode/fileutils.py.ABOUT | 17 + src/commoncode/functional.py | 79 ++++ src/commoncode/hash.py | 128 ++++++ src/commoncode/ignore.py | 310 +++++++++++++++ src/commoncode/paths.py | 178 +++++++++ src/commoncode/system.py | 136 +++++++ src/commoncode/testcase.py | 395 +++++++++++++++++++ src/commoncode/text.py | 201 ++++++++++ src/commoncode/timeutils.py | 101 +++++ src/commoncode/urn.py | 151 +++++++ src/commoncode/version.py | 73 ++++ 19 files changed, 3902 insertions(+) create mode 100644 src/commoncode/PSF.LICENSE create mode 100644 src/commoncode/__init__.py create mode 100644 src/commoncode/codec.py create mode 100644 src/commoncode/command.py create mode 100644 src/commoncode/date.py create mode 100644 src/commoncode/fileset.py create mode 100644 src/commoncode/filetype.py create mode 100644 src/commoncode/fileutils.py create mode 100644 src/commoncode/fileutils.py.ABOUT create mode 100644 src/commoncode/functional.py create mode 100644 src/commoncode/hash.py create mode 100644 src/commoncode/ignore.py create mode 100644 src/commoncode/paths.py create mode 100644 src/commoncode/system.py create mode 100644 src/commoncode/testcase.py create mode 100644 src/commoncode/text.py create mode 100644 src/commoncode/timeutils.py create mode 100644 src/commoncode/urn.py create mode 100644 src/commoncode/version.py diff --git a/src/commoncode/PSF.LICENSE b/src/commoncode/PSF.LICENSE new file mode 100644 index 00000000000..5db3415f990 --- /dev/null +++ b/src/commoncode/PSF.LICENSE @@ -0,0 +1,635 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.2 2.1.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2.1 2.2 2002 PSF yes + 2.2.2 2.2.1 2002 PSF yes + 2.2.3 2.2.2 2003 PSF yes + 2.3 2.2.2 2002-2003 PSF yes + 2.3.1 2.3 2002-2003 PSF yes + 2.3.2 2.3.1 2002-2003 PSF yes + 2.3.3 2.3.2 2002-2003 PSF yes + 2.3.4 2.3.3 2004 PSF yes + 2.3.5 2.3.4 2005 PSF yes + 2.4 2.3 2004 PSF yes + 2.4.1 2.4 2005 PSF yes + 2.4.2 2.4.1 2005 PSF yes + 2.4.3 2.4.2 2006 PSF yes + 2.4.4 2.4.3 2006 PSF yes + 2.5 2.4 2006 PSF yes + 2.5.1 2.5 2007 PSF yes + 2.5.2 2.5.2 2008 PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python +alone or in any derivative version, provided, however, that PSF's +License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative +version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +This copy of Python includes a copy of bzip2, which is licensed under the following terms: + + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2005 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, Cambridge, UK. +jseward@acm.org +bzip2/libbzip2 version 1.0.3 of 15 February 2005 + + +This copy of Python includes a copy of db, which is licensed under the following terms: + +/*- + * $Id: LICENSE,v 12.1 2005/06/16 20:20:10 bostic Exp $ + */ + +The following is the license that applies to this copy of the Berkeley DB +software. For a license to use the Berkeley DB software under conditions +other than those described here, or to purchase support for this software, +please contact Sleepycat Software by email at info@sleepycat.com, or on +the Web at http://www.sleepycat.com. + +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +/* + * Copyright (c) 1990-2005 + * Sleepycat Software. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Redistributions in any form must be accompanied by information on + * how to obtain complete source code for the DB software and any + * accompanying software that uses the DB software. The source code + * must either be included in the distribution or be available for no + * more than the cost of distribution plus a nominal fee, and must be + * freely redistributable under reasonable conditions. For an + * executable file, complete source code means the source code for all + * modules it contains. It does not include source code for modules or + * files that typically accompany the major components of the operating + * system on which the executable file runs. + * + * THIS SOFTWARE IS PROVIDED BY SLEEPYCAT SOFTWARE ``AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR + * NON-INFRINGEMENT, ARE DISCLAIMED. IN NO EVENT SHALL SLEEPYCAT SOFTWARE + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * Copyright (c) 1990, 1993, 1994, 1995 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ +/* + * Copyright (c) 1995, 1996 + * The President and Fellows of Harvard University. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY HARVARD AND ITS CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL HARVARD OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +This copy of Python includes a copy of openssl, which is licensed under the following terms: + + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a dual license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. Actually both licenses are BSD-style + Open Source licenses. In case of any license issues related to OpenSSL + please contact openssl-core@openssl.org. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2005 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + + +This copy of Python includes a copy of tcl, which is licensed under the following terms: + +This software is copyrighted by the Regents of the University of +California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState +Corporation and other parties. The following terms apply to all files +associated with the software unless explicitly disclaimed in +individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. + +This copy of Python includes a copy of tk, which is licensed under the following terms: + +This software is copyrighted by the Regents of the University of +California, Sun Microsystems, Inc., and other parties. The following +terms apply to all files associated with the software unless explicitly +disclaimed in individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. diff --git a/src/commoncode/__init__.py b/src/commoncode/__init__.py new file mode 100644 index 00000000000..c069f8ccb34 --- /dev/null +++ b/src/commoncode/__init__.py @@ -0,0 +1,42 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + + +# set re and fnmatch _MAXCACHE to 1M to cache regex compiled aggressively +# their default is 100 and many utilities and libraries use a lot of regex + +import re + +remax = getattr(re, '_MAXCACHE', 0) +if remax < 1000000: + setattr(re, '_MAXCACHE', 1000000) +del remax + +import fnmatch + +fnmatchmax = getattr(fnmatch, '_MAXCACHE', 0) +if fnmatchmax < 1000000: + setattr(fnmatch, '_MAXCACHE', 1000000) +del fnmatchmax +del re diff --git a/src/commoncode/codec.py b/src/commoncode/codec.py new file mode 100644 index 00000000000..2ac767c6362 --- /dev/null +++ b/src/commoncode/codec.py @@ -0,0 +1,165 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +""" +Numbers to bytes or strings and URLs coder/decoders. +""" + +padding = '/' + +b85_symbols = ('0123456789' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '!#$%&()*+-;<=>?@^_`{|}~') + +len_b85_symbols = len(b85_symbols) + + +def to_base_n(num, base): + """ + Convert `num` number to a string representing this number in base `base` + where base <= 85. + + Use recursion for progressive encoding. + """ + # ensure that base is within bounds + assert base >= 2 and base <= len_b85_symbols + if num == 0: + return '0' + # recurse with a floor division to encode from left to right + based = to_base_n(num // base, base) + # remove leading zeroes resulting from floor-based encoding + stripped = based.lstrip('0') + # pick the symbol in the symbol table using a modulo + encoded = b85_symbols[num % base] + return stripped + encoded + + +MAXLEN = len(to_base_n(pow(2, 32) - 1, 85)) + + +def to_base85(num): + """ + Convert `num` number to a string representing this number in base 85, + padded as needed. + + The character set to encode 85 base85 digits is defined to be: + '0'..'9', 'A'..'Z', 'a'..'z', '!', '#', '$', '%', '&', '(', + ')', '*', '+', '-', ';', '<', '=', '>', '?', '@', '^', '_', + '`', '{', '|', '}', and '~'. + + From http://www.faqs.org/rfcs/rfc1924.html + + See also http://en.wikipedia.org/wiki/Base_85 for the rationale for Base + 85. Git also uses https://github.com/git/git/blob/master/base85.c + """ + encoded = to_base_n(num, 85) + # add padding + elen = len(encoded) + if elen < MAXLEN: + encoded = encoded + (padding * (MAXLEN - (elen))) + return encoded + + +def to_base10(s, b=36): + """ + Convert a string s representing a number in base b back to an integer. + """ + assert b <= len(b85_symbols) and b >= 2, ('Base must be in range(2, %d)' + % (len(b85_symbols))) + # strip padding + s = s.replace(padding, '') + + base10_num = 0 + i = len(s) - 1 + for digit in s: + base10_num += b85_symbols.index(digit) * pow(b, i) + i -= 1 + return base10_num + + +def num_to_bin(num): + """ + Convert a `num` integer or long to a binary string byte-ordered such that + the least significant bytes are at the beginning of the string (aka. little + endian). + + NOTE: The code below does not use struct for conversions to handle + arbitrary long binary strings (such as a SHA512 digest) and convert that + safely to a long: using structs does not work easily for this. + """ + binstr = [] + while num > 0: + # add the least significant byte value + binstr.append(chr(num & 0xFF)) + # shift the next byte to least significant and repeat + num = num >> 8 + + # reverse the list now to the most significant + # byte is at the start of ths string to speed decoding + return ''.join(reversed(binstr)) + + +def bin_to_num(binstr): + """ + Convert a little endian byte-ordered binary string to an integer or long. + """ + # this will cast to long as needed + num = 0 + for charac in binstr: + # the most significant byte is a the start of the string so we multiply + # that value by 256 (e.g. <<8) and add the value of the current byte, + # then move to next byte in the string and repeat + num = (num << 8) + ord(charac) + return num + + +from base64 import standard_b64decode as stddecode +from base64 import urlsafe_b64encode as b64encode + + +def urlsafe_b64encode(s): + """ + Encode a binary string to a url safe base64 encoding. + """ + return b64encode(s) + + +def urlsafe_b64decode(b64): + """ + Decode a url safe base64-encoded string. + Note that we use stddecode to work around a bug in the standard library. + """ + b = b64.replace('-', '+').replace('_', '/') + return stddecode(b) + + +def _encode(num): + """ + Encode a number (int or long) in url safe base64. + Used in simhash5 + """ + return b64encode(num_to_bin(num)) diff --git a/src/commoncode/command.py b/src/commoncode/command.py new file mode 100644 index 00000000000..5b0ba1a0d04 --- /dev/null +++ b/src/commoncode/command.py @@ -0,0 +1,325 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import ctypes +import os +import logging +import signal +import subprocess + +from commoncode.system import on_windows +from commoncode.system import current_os_arch +from commoncode.system import current_os_noarch +from commoncode.system import noarch +from commoncode import fileutils +from commoncode import text +from commoncode import system + + +""" +Minimal wrapper for executing external commands in sub-processes. The approach +is unconventionally relying on vendoring scripts or pre-built executable +binary command rather than relying on OS-provided binaries. + +While this could seem contrived at first this approach ensures that: + - a distributed scancode package is self-contained + - a non technical user does not have any extra installation to do, in + particular there is no compilation needed at installation time. + + - we have few dependencies on the OS. + + - that we control closely the version of these executables and how they were + built to ensure sanity, especially on Linux where several different + (oftentimes older) version may exist in the path for a given distro. + For instance this applies to tools such as 7z, binutils and file. +""" + +LOG = logging.getLogger(__name__) + +# current directory is the root dir of this library +curr_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def execute(cmd, args, root_dir=None, cwd=None, env=None, to_files=False): + """ + Run a `cmd` external command with the `args` arguments list and return the + return code, the stdout and stderr. + + To avoid RAM exhaustion, always write stdout and stderr streams to files. + + If `to_files` is False, return the content of stderr and stdout as ASCII + strings. Otherwise, return the locations to the stderr and stdout + temporary files. + + Resolve the `cmd` location using os/arch local/vendored location based on + using `root_dir`. Run the command `cwd` current working directory using an + `env` dict of environment variables. + """ + assert cmd + cmd_loc, bin_dir, lib_dir = get_locations(cmd, root_dir) + full_cmd = [cmd_loc or cmd] + args or [] + env = get_env(env, lib_dir) or None + cwd = cwd or curr_dir + + # temp files for stderr and stdout + tmp_dir = fileutils.get_temp_dir(base_dir='cmd') + sop = os.path.join(tmp_dir, 'stdout') + sep = os.path.join(tmp_dir, 'stderr') + + # shell==True is DANGEROUS but we are not running arbitrary commands + # though we can execute command that just happen to be in the path + shell = True if on_windows else False + + LOG.debug('Executing command %(cmd)r as %(full_cmd)r with: env=%(env)r, ' + 'shell=%(shell)r, cwd=%(cwd)r, stdout=%(sop)r, stderr=%(sep)r.' + % locals()) + + proc = None + try: + with open(sop, 'wb') as stdout, open(sep, 'wb') as stderr: + # -1 defaults bufsize to system bufsize + pargs = dict(cwd=cwd, env=env, stdout=stdout, stderr=stderr, + shell=shell, bufsize=-1, universal_newlines=True) + proc = subprocess.Popen(full_cmd, **pargs) + stdout, stderr = proc.communicate() + rc = proc.returncode if proc else 0 + finally: + close(proc) + + if not to_files: + # return output as ASCII string loaded from the output files + sop = text.toascii(open(sop, 'rb').read().strip()) + sep = text.toascii(open(sep, 'rb').read().strip()) + return rc, sop, sep + + +def os_arch_dir(root_dir, _os_arch=current_os_arch): + """ + Return a sub-directory of `root_dir` tailored for the current OS and + current processor architecture. + """ + return os.path.join(root_dir, _os_arch) + + +def os_noarch_dir(root_dir, _os_noarch=current_os_noarch): + """ + Return a sub-directory of `root_dir` tailored for the current OS and NOT + specific to a processor architecture. + """ + return os.path.join(root_dir, _os_noarch) + + +def noarch_dir(root_dir, _noarch=noarch): + """ + Return a sub-directory of `root_dir` that is NOT specific to an OS or + processor architecture. + """ + return os.path.join(root_dir, _noarch) + + +def get_base_dirs(root_dir, + _os_arch=current_os_arch, + _os_noarch=current_os_noarch, + _noarch=noarch): + """ + Return a sequence of existing directories relative to a `root_dir`. Each + returned directory is an existing local/vendored directory location + ordered from the most operating system and processor architecture specific + to the least specific: + + - 1. /-: a dir specific to the OS and arch. + - 2. /-noarch: a dir specific to the OS for any arch. + - 3. /noarch: a dir for any OS and any arch + + Return an empty sequence if no local/vendored directory was found. + + Rationale: Pre-built executable command binaries are typically stored for + convenience side-by-side with code that calls them. We support multiple + OSes and architectures and therefore have multiple vendored pre-built + binary of any given binary. This function resolves to an actual OS/arch + location in this context. + """ + if not root_dir or not os.path.exists(root_dir): + return [] + + dirs = [] + + def find_loc(fun, arg): + loc = fun(root_dir, arg) + if os.path.exists(loc): + dirs.append(loc) + + if _os_arch: + find_loc(os_arch_dir, _os_arch) + if _os_noarch: + find_loc(os_noarch_dir, _os_noarch) + if _noarch: + find_loc(noarch_dir, _noarch) + + return dirs + + +def get_bin_lib_dirs(base_dir): + """ + Return a tuple of bin and lib sub-directories of a `base_dir`. bin and lib + directories are None if they do not exist. + + On POSIX, all files in these directories are made executable. + The lib dir defaults to bin dir if lib did is not present. + """ + + if not base_dir: + return None, None + + bin_dir = os.path.join(base_dir, 'bin') + + if os.path.exists(bin_dir): + fileutils.chmod(bin_dir, fileutils.RX) + else: + bin_dir = None + + lib_dir = os.path.join(base_dir, 'lib') + + if os.path.exists(lib_dir): + fileutils.chmod(bin_dir, fileutils.RX) + else: + # default to bin for lib if it exists + lib_dir = bin_dir or None + + return bin_dir, lib_dir + + +def get_env(base_vars=None, lib_dir=None): + """ + Return a dictionary of environment variables for command execution with + appropriate LD paths. Use the optional `base_vars` environment variables + dictionary as a base if provided. Note: if `base_vars` contains LD + variables these will be overwritten. + """ + env_vars = {} + if base_vars: + env_vars.update(base_vars) + + # Create and add LD environment variables + if lib_dir: + path_var = '%(lib_dir)s' % locals() + # on Linux/posix + env_vars['LD_LIBRARY_PATH'] = path_var + # on Mac + env_vars['DYLD_LIBRARY_PATH'] = path_var + return env_vars + + +def get_locations(cmd, root_dir, + _os_arch=current_os_arch, + _os_noarch=current_os_noarch, + _noarch=noarch, + must_exist=True): + """ + Return a tuple of (cmd_loc, bin_dir, lib_dir), where `cmd_loc` is the + location of an `cmd` external command, bin_dir and lib_dir are the + corresponding bin and lib directories. `root_dir` is used to resolve where + a local/vendored executable `cmd` is stored. Return a tuple of None if no + local/vendored executable was found or no `root_dir` was provided or + found. + If `must_exist` is False, the existence of the cmd is not performed. + In this case the first existing bin and lib dirs will be returned. + + On POSIX, the command file is made executable when found locally. + On Windows, an '.exe' extension is added to `cmd`. + """ + cmd_loc = bin_dir = lib_dir = None + if not root_dir: + return cmd_loc, bin_dir, lib_dir + + if must_exist: + # we do not use on_windows here to support cross OS testing + if any(x and 'win' in x for x in (_os_arch, _os_noarch, _noarch)): + cmd = cmd + '.exe' + + for base_dir in get_base_dirs(root_dir, _os_arch, _os_noarch, _noarch): + bin_dir, lib_dir = get_bin_lib_dirs(base_dir) + cmd_loc = os.path.join(bin_dir, cmd) + if os.path.exists(cmd_loc): + fileutils.chmod(cmd_loc, fileutils.RX) + return cmd_loc, bin_dir, lib_dir + else: + # we just care for getting the dirs and grab the first one + for base_dir in get_base_dirs(root_dir, _os_arch, _os_noarch, _noarch): + bin_dir, lib_dir = get_bin_lib_dirs(base_dir) + return None, bin_dir, lib_dir + + return None, None, None + + +def close(proc): + """ + Close a `proc` process opened pipes and kill the process. + """ + if not proc: + return + + def close_pipe(p): + if not p: + return + try: + p.close() + except IOError: + pass + + close_pipe(getattr(proc, 'stdin', None)) + close_pipe(getattr(proc, 'stdout', None)) + close_pipe(getattr(proc, 'stderr', None)) + + try: + # Ensure process death otherwise proc.wait may hang in some cases + # NB: this will run only on POSIX OSes supporting signals + os.kill(proc.pid, signal.SIGKILL) # @UndefinedVariable + except: + pass + + # This may slow things down a tad on non-POSIX Oses but is safe: + # this calls os.waitpid() to make sure the process is dead + proc.wait() + + +def load_lib(libname, root_dir): + """ + Return the loaded `libname` shared library object from vendored paths. + """ + os_dir = get_base_dirs(root_dir)[0] + _bin_dir, lib_dir = get_bin_lib_dirs(os_dir) + so = os.path.join(lib_dir, libname + system.lib_ext) + + # add lib path to the front of the PATH env var + new_path = os.pathsep.join([lib_dir, os.environ['PATH']]) + os.environ['PATH'] = new_path + + if os.path.exists(so): + lib = ctypes.CDLL(so) + if lib and lib._name: + return lib + raise ImportError('Failed to load %(libname)s from %(so)r' % locals()) diff --git a/src/commoncode/date.py b/src/commoncode/date.py new file mode 100644 index 00000000000..cc74e75deec --- /dev/null +++ b/src/commoncode/date.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os +from datetime import datetime +import calendar + + +def isoformat(utc_date): + return datetime.isoformat(utc_date).replace('T', ' ') + + +def get_file_mtime(location, iso=True): + """ + Return a string containing the last modified date of a file formatted + as an ISO time stamp is ISO is True or a as raw number since epoch. + """ + date = '' + # FIXME: use file types + if not os.path.isdir(location): + mtime = os.stat(location).st_mtime + if iso: + utc_date = datetime.utcfromtimestamp(mtime) + date = isoformat(utc_date) + else: + date = str(mtime) + return date + + +def secs_from_epoch(d): + """ + Return a number of seconds since epoch for a date time stamp + """ + # FIXME: what does this do? + return calendar.timegm(datetime.strptime(d.split('.')[0], + '%Y-%m-%d %H:%M:%S').timetuple()) diff --git a/src/commoncode/fileset.py b/src/commoncode/fileset.py new file mode 100644 index 00000000000..d6d64070e1d --- /dev/null +++ b/src/commoncode/fileset.py @@ -0,0 +1,176 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import print_function, absolute_import + +import fnmatch +import logging + +import os +from commoncode import fileutils +from commoncode import paths + +LOG = logging.getLogger(__name__) +# import sys +# logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) +# LOG.setLevel(logging.DEBUG) + +""" +Match files and directories paths based on inclusion and exclusion glob-style +patterns. + +For example, this can be used to skip files that match ignore patterns, +similar to a version control ignore files such as .gitignore. + +The pattern syntax is the same as fnmatch(5) as implemented in Python. + +Patterns are applied to a path this way: + - Paths are converted to POSIX paths before matching. + - Patterns are NOT case-sensitive. + - Leading slashes are ignored. + - If the pattern contains a /, then the whole path must be matched; + otherwise, the pattern matches if any path segment matches. + - When matched, a directory content is matched recursively. + For instance, when using patterns for ignoring, a matched a directory will + be ignore with its file and sub-directories at full depth. + - The order of patterns does not matter. + - Exclusion patterns are prefixed with an exclamation mark (band or !) + meaning that matched paths by that pattern will be excluded. Exclusions + have precedences of inclusions. + - Patterns starting with # are comments and skipped. use [#] for a literal #. + - to match paths relative to some root path, you must design your patterns + and the path tested accordingly. This module does not handles this. + +Patterns may include glob wildcards such as: + - ? : matches any single character. + - * : matches 0 or more characters. + - [seq] : matches any character in seq + - [!seq] :matches any character not in seq +For a literal match, wrap the meta-characters in brackets. For example, '[?]' +matches the character '?'. +""" + + +def match(path, includes, excludes): + """ + Return a tuple of two strings if `path` is matched or False if it does + not. Matching is done based on the set of `includes` and `excludes` + patterns maps. The returned tuple contains these two strings: pattern + matched and associated message. The message explains why a path is + included when matched. The message is always a string (possibly empty). + + `includes` and `excludes` are maps of (fnmtch pattern -> message). + The order of the includes and excludes items does not matter. If one is + empty, it is not used for matching. If the `path` is empty, return False. + """ + + includes = includes or {} + excludes = excludes or {} + if not path or not path.strip(): + return False + + included = _match(path, includes) + excluded = _match(path, excludes) + LOG.debug('in_fileset: path: %(path)r included:%(included)r, ' + 'excluded:%(excluded)r .' % locals()) + if excluded: + return False + elif included: + return included + else: + return False + + +def _match(path, patterns): + """ + Return a message if `path` is matched by a pattern from the `patterns` map + or False. + """ + if not path or not patterns: + return False + + path = fileutils.as_posixpath(path).lower() + pathstripped = path.lstrip('/') + if not pathstripped: + return False + segments = paths.split(pathstripped) + LOG.debug('_match: path: %(path)r patterns:%(patterns)r.' % locals()) + mtch = False + for pat, msg in patterns.items(): + if not pat and not pat.strip(): + continue + msg = msg or '' + pat = pat.lstrip('/').lower() + is_plain = '/' not in pat + if is_plain: + if any(fnmatch.fnmatchcase(s, pat) for s in segments): + mtch = msg + break + elif (fnmatch.fnmatchcase(path, pat) + or fnmatch.fnmatchcase(pathstripped, pat)): + mtch = msg + break + LOG.debug('_match: match is %(mtch)r' % locals()) + return mtch + + +def load(location): + """ + Return a sequence of patterns from a file at location. + """ + if not location: + return tuple() + fn = os.path.abspath(os.path.normpath(os.path.expanduser(location))) + msg = ('File %(location)s does not exist or not a file.') % locals() + assert (os.path.exists(fn) and os.path.isfile(fn)), msg + with open(fn, 'rb') as f: + return [l.strip() for l in f if l and l.strip()] + + +def includes_excludes(patterns, message): + """ + Return a dict of included patterns and a dict of excluded patterns from a + sequence of `patterns` strings and a `message` setting the message as + value in the returned mappings. Ignore pattern as comments if prefixed + with #. Use an empty string is message is None. + """ + message = message or '' + BANG = '!' + DASH = '#' + included = {} + excluded = {} + if not patterns: + return included, excluded + for pat in patterns: + pat = pat.strip() + if not pat or pat.startswith(DASH): + continue + if pat.startswith(BANG): + cpat = pat.lstrip(BANG) + if cpat: + excluded[cpat] = message + continue + else: + included.add[pat] = message + return included, excluded diff --git a/src/commoncode/filetype.py b/src/commoncode/filetype.py new file mode 100644 index 00000000000..bcbc8dffe62 --- /dev/null +++ b/src/commoncode/filetype.py @@ -0,0 +1,230 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os +from collections import OrderedDict +from datetime import datetime + +from commoncode.system import on_posix +from commoncode.functional import memoize + + +def is_link(location): + """ + Return True if `location` is a symbolic link. + """ + return location and os.path.islink(location) + + +def is_file(location): + """ + Return True if `location` is a file. + """ + return (location and os.path.isfile(location) + and not is_link(location) and not is_broken_link(location)) + + +def is_dir(location): + """ + Return True if `location` is a directory. + """ + return (location and os.path.isdir(location) and not is_file(location) + and not is_link(location) and not is_broken_link(location)) + + +def is_regular(location): + """ + Return True if `location` is regular. A regular location is a file or a + dir and not a special file or symlink. + """ + return location and (is_file(location) or is_dir(location)) + + +def is_special(location): + """ + Return True if `location` is a special file . A special file is not a + regular file, i.e. anything such as a broken link, block file, fifo, + socket, character device or else. + """ + return not is_regular(location) + + +def is_broken_link(location): + """ + Return True if `location` is a broken link. + """ + # always false on windows, until Python supports junctions/links + if on_posix and is_link(location): + target = get_link_target(location) + target_loc = os.path.join(os.path.dirname(location), target) + return target and not os.path.exists(target_loc) + + +def get_link_target(location): + """ + Return the link target for `location` if this is a Link or an empty + string. + """ + target = '' + # always false on windows, until Python supports junctions/links + if on_posix and is_link(location): + try: + # return false on OSes not supporting links + target = os.readlink(location) # @UndefinedVariable + except UnicodeEncodeError: # @UnusedVariable + # location is unicode but readlink can fail in some cases + pass + return target + + +# Map of type checker function -> short type code +# The order of types check matters: link -> file -> directory -> special +TYPES = OrderedDict([(is_link, 'l'), + (is_file, 'f'), + (is_dir, 'd'), + (is_special, 's')]) + + +def get_type(location): + """ + Return the type of the `location` or None if it does not exist. + """ + if location: + for type_checker in TYPES: + tc = type_checker(location) + if tc: + return TYPES[type_checker] + + +def is_readable(location): + """ + Return True if the file at location has readable permission set. + Does not follow links. + """ + if location: + if is_dir(location): + return os.access(location, os.R_OK | os.X_OK) + else: + return os.access(location, os.R_OK) + + +def is_writable(location): + """ + Return True if the file at location has writeable permission set. + Does not follow links. + """ + if location: + if is_dir(location): + return os.access(location, os.R_OK | os.W_OK | os.X_OK) + else: + return os.access(location, os.R_OK | os.W_OK) + +def is_executable(location): + """ + Return True if the file at location has executable permission set. + Does not follow links. + """ + if location: + if is_dir(location): + return os.access(location, os.R_OK | os.W_OK | os.X_OK) + else: + return os.access(location, os.X_OK) + + +def is_rwx(location): + """ + Return True if the file at location has read, write and executable + permission set. Does not follow links. + """ + return is_readable(location) and is_writable(location) and is_executable(location) + + +def get_last_modified_date(location): + """ + Return the last modified date stamp of a file is YYYYMMDD format. The date + of non-files (dir, links, special) is always an empty string. + """ + yyyymmdd = '' + if is_file(location): + utc_date = datetime.isoformat( + datetime.utcfromtimestamp(os.path.getmtime(location)) + ) + yyyymmdd = utc_date[:10] + return yyyymmdd + + +counting_functions = { + 'file_count': lambda _: 1, + 'file_size': os.path.getsize, +} + +@memoize +def counter(location, counting_function): + """ + Return a count for a single file or a cumulative count for a directory + tree at `location`. + + Get a callable from the counting_functions registry using the + `counting_function` string. Call this callable with a `location` argument + to determine the count value for a single file. This allow memoization + with hashable arguments. + + Only regular files and directories have a count. The count for a directory + is the recursive count sum of the directory file and directory + descendants. + + Any other file type such as a special file or link has a zero size. Does + not follow links. + """ + if not (is_file(location) or is_dir(location)): + return 0 + + count = 0 + if is_file(location): + count_fun = counting_functions[counting_function] + return count_fun(location) + elif is_dir(location): + count += sum(counter(os.path.join(location, p), counting_function) + for p in os.listdir(location)) + return count + + +def get_file_count(location): + """ + Return the cumulative number of files in the directory tree at `location` + or 1 if `location` is a file. Only regular files are counted. Everything + else has a zero size. + """ + return counter(location, 'file_count') + + +def get_size(location): + """ + Return the size in bytes of a file at `location` or if `location` is a + directory, the cumulative size of all files in this directory tree. Only + regular files have a size. Everything else has a zero size. + """ + return counter(location, 'file_size') diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py new file mode 100644 index 00000000000..ca1de9f8853 --- /dev/null +++ b/src/commoncode/fileutils.py @@ -0,0 +1,501 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import codecs +import errno +import logging +import os +import ntpath +import posixpath +import shutil +import stat +import tempfile + +from commoncode import system +from commoncode import filetype +from commoncode import text +from commoncode.system import on_windows +from commoncode.filetype import is_rwx +from commoncode.filetype import is_file + +# this exception is not available on posix +try: + WindowsError # @UndefinedVariable +except NameError: + WindowsError = None # @ReservedAssignment + +logger = logging.getLogger(__name__) + +""" +File, paths and directory utility functions. +""" + +# +# DIRECTORIES +# + +def create_dir(location): + """ + Create directory and all sub-directories recursively at location ensuring + these are readable and writeable. + Raise Exceptions if it fails to create the directory. + """ + if os.path.exists(location): + if not os.path.isdir(location): + err = ('Cannot create directory: existing file ' + 'in the way ''%(location)s.') + raise OSError(err % locals()) + else: + # may fail on win if the path is too long + # FIXME: consider using UNC ?\\ paths + + try: + os.makedirs(location) + chmod(location, RW) + + # avoid multi-process TOCTOU conditions when creating dirs + # the directory may have been created since the exist check + except WindowsError, e: + # [Error 183] Cannot create a file when that file already exists + if e and e.winerror == 183: + if not os.path.isdir(location): + raise + else: + raise + except (IOError, OSError), o: + if o.errno == errno.EEXIST: + if not os.path.isdir(location): + raise + else: + raise + + +def system_temp_dir(): + """ + Return the global temp directory for the current user. + """ + temp_dir = os.getenv('SCANCODE_TMP') + if not temp_dir: + sc = text.python_safe_name('scancode_' + system.username) + temp_dir = os.path.join(tempfile.gettempdir(), sc) + create_dir(temp_dir) + return temp_dir + + +def get_temp_dir(base_dir, prefix=''): + """ + Return the path to base a new unique temporary directory, created under + the system-wide `system_temp_dir` temp directory and as a subdir of the + base_dir path, a path relative to the `system_temp_dir`. + """ + base = os.path.join(system_temp_dir(), base_dir) + create_dir(base) + return tempfile.mkdtemp(prefix=prefix, dir=base) + +# +# FILE READING +# + +def file_chunks(file_object, chunk_size=1024): + """ + Yield a file piece by piece. Default chunk size: 1k. + """ + while True: + data = file_object.read(chunk_size) + if data: + yield data + else: + break + + +# FIXME: reading a whole file could be an issue: could we stream by line? +def _text(location, encoding, universal_new_lines=True): + """ + Read file at `location` as a text file with the specified `encoding`. If + `universal_new_lines` is True, update lines endings to be posix LF \n. + Return a unicode string. + Note: Universal newlines in the codecs package was removed in + Python2.6 see http://bugs.python.org/issue691291 + """ + with codecs.open(location, 'r', encoding) as f: + text = f.read() + if universal_new_lines: + text = u'\n'.join(text.splitlines(False)) + return text + + +def read_text_file(location, universal_new_lines=True): + """ + Return the text content of file at `location` trying to find the best + encoding. + """ + try: + text = _text(location, 'utf-8', universal_new_lines) + except: + text = _text(location, 'latin-1', universal_new_lines) + return text + +# +# PATHS AND NAMES +# + +def as_posixpath(location): + """ + Return a posix-like path using posix path separators (slash or "/") for a + path. This also converts Windows paths to look like posix paths that + Python accepts gracefully on Windows for path handling. + """ + return location.replace(ntpath.sep, posixpath.sep) + + +def resource_name(path): + """ + Return the resource name (file name or directory name) from `path` which + is the last path segment. + """ + path = as_posixpath(path) + path = path.rstrip('/') + _left, right = posixpath.split(path) + return right or '' + + +def file_name(path): + """ + Return the file name (or directory name) of a path. + """ + return resource_name(path) + + +def parent_directory(path): + """ + Return the parent directory of a file or directory path. + """ + path = as_posixpath(path) + path = path.rstrip('/') + left, _ = posixpath.split(path) + trail = '/' if left != '/' else '' + return left + trail + + +def file_base_name(path): + """ + Return the file base name for a path. The base name is the base name of + the file minus the extension. For a directory return an empty string. + """ + return splitext(path)[0] + + +def file_extension(path): + """ + Return the file extension for a path. + """ + return splitext(path)[1] + + +def splitext(path): + """ + Return a tuple of (basename, extension) for a path. The basename is the + file name minus its extension. Return an empty extension string for a + directory. Not the same as os.path.splitext. + """ + base_name = '' + extension = '' + if not path: + return base_name, extension + + path = as_posixpath(path) + name = resource_name(path) + if path.endswith('/'): + # directories have no extension + base_name = name + extension = '' + elif name.startswith('.') and '.' not in name[1:]: + base_name = '' + extension = name + else: + base_name, extension = posixpath.splitext(name) + return base_name or '', extension or '' + +# +# DIRECTORY WALKING +# +# TODO: rename ignorer to ignore +def walk(location, ignorer=None): + """ + Walk location returning the same tuples as os.walk but with a different + behavior: + - always walk top-down, breadth-first. + - always ignore and never follow symlinks. + - always ignore special files (FIFOs, etc.) + - optionally ignore files and directories by invoking the `ignorer` + callable on files and directories. + """ + assert filetype.is_dir(location) + + if not ignorer: + # never ignore if we do not have an ignorer + ignorer = lambda _: False + + dirs = [] + files = [] + + # TODO: consider using scandir + for name in os.listdir(location): + loc = os.path.join(location, name) + if ignorer(loc): + continue + if filetype.is_dir(loc): + dirs.append(name) + elif filetype.is_file(loc): + files.append(name) + # else: pass: special files and symlinks are always ignored + yield location, dirs, files + + for dr in dirs: + to_walk = os.path.join(location, dr) + if not filetype.is_special(location): + for t, d, f in walk(to_walk, ignorer): + yield t, d, f + + +def file_walk(location): + """ + Return the files at location recursively. + + :param location: can be a file or a directory + :return: file paths + """ + if is_file(location): + yield location + else: + for root, _dirs, files in walk(location): + for f in files: + yield os.path.join(root, f) + +# +# +# COPY +# + +def copytree(src, dst, symlinks=False, ignore=None): + """ + Copy recursively the `src` directory to a non-existing `dst` directory. + Preserves: + - timestamps. + - symlinks if the optional `symlinks` argument is True. + + Ignores: + -`src` permissions: `dst` files are created with the default permissions. + - all special files such as FIFO or character devices + + Raise an shutil.Error with a list of reasons. + + The optional `ignore` argument is a callable called once for each copied + directory with src and names arguments. `src` is the current directory and + `names` is the list of `src` names as returned by os.listdir(). It must + return a list of names in the `src` directory that should be ignored from + the copy:: + callable(src, names) -> ignored_names + + Similar to and derived from Python shutil module. See fileutils.py.ABOUT + for details. + """ + if not filetype.is_readable(src): + chmod(src, R, recurse=False) + + names = os.listdir(src) + ignored_names = set() + + if ignore is not None: + ignored_names = ignore(src, names) or ignored_names + + os.makedirs(dst) + errors = [] + errors.extend(copytime(src, dst)) + + for name in names: + if name in ignored_names: + continue + srcname = os.path.join(src, name) + dstname = os.path.join(dst, name) + + if not filetype.is_readable(srcname): + chmod(srcname, R, recurse=False) + try: + if not on_windows and symlinks and os.path.islink(srcname): + linkto = os.readlink(srcname) # @UndefinedVariable + os.symlink(linkto, dstname) # @UndefinedVariable + elif os.path.isdir(srcname): + copytree(srcname, dstname, symlinks, ignore) + elif filetype.is_file(srcname): + copyfile(srcname, dstname) + else: + # skip anything that is not a regular file, dir or link + pass + # catch the Error from the recursive copytree so that we can + # continue with other files + except shutil.Error, err: + errors.extend(err.args[0]) + except EnvironmentError, why: + errors.append((srcname, dstname, str(why))) + + if errors: + raise shutil.Error, errors + + +def copyfile(src, dst): + """ + Copy src file to dst file preserving timestamps. + Ignore permissions and special files. + + Similar to and derived from Python shutil module. See fileutils.py.ABOUT + for details. + """ + if not filetype.is_regular(src): + return + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) + if not filetype.is_readable(src): + chmod(src, R, recurse=False) + shutil.copyfile(src, dst) + copytime(src, dst) + + +def copytime(src, dst): + """ + Copy timestamps from `src` to `dst`. + + Similar to and derived from Python shutil module. See fileutils.py.ABOUT + for details. + """ + errors = [] + st = os.stat(src) + if hasattr(os, 'utime'): + try: + os.utime(dst, (st.st_atime, st.st_mtime)) + except OSError, why: + if WindowsError is not None and isinstance(why, WindowsError): + # File access times cannot be copied on Windows + pass + else: + errors.append((src, dst, str(why))) + return errors + +# +# PERMISSIONS +# + +# modes: read, write, executable +R = stat.S_IRUSR +RW = stat.S_IRUSR | stat.S_IWUSR +RX = stat.S_IRUSR | stat.S_IXUSR +RWX = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR + + +def chmod(location, flags, recurse=True): + """ + Update permissions for `location` with with `flags`. + `flags` is one of R, RW, RX or RWX with the same meaning as in chmod. + Update is done recursively if `recurse`. + """ + if not location or not os.path.exists(location): + return + + location = os.path.abspath(location) + + new_flags = flags + if filetype.is_dir(location): + # POSIX dirs need to be executable to be readable, + # and to be writable so we can change perms of files inside + new_flags = RWX + + parent = os.path.dirname(location) + current_stat = stat.S_IMODE(os.stat(parent).st_mode) + if not is_rwx(parent): + os.chmod(parent, current_stat | RWX) + + if filetype.is_regular(location): + current_stat = stat.S_IMODE(os.stat(location).st_mode) + os.chmod(location, current_stat | new_flags) + + if recurse: + chmod_tree(location, flags) + + +def chmod_tree(location, flags): + """ + Update permissions recursively in a directory tree `location`. + """ + if filetype.is_dir(location): + for top, dirs, files in walk(location): + for d in dirs: + chmod(os.path.join(top, d), flags, recurse=False) + for f in files: + chmod(os.path.join(top, f), flags) + +# +# DELETION +# + +def _rm_handler(function, path, excinfo): # @UnusedVariable + """ + shutil.rmtree handler invoked on error when deleting a directory tree. + This retries deleting once before giving up. + """ + if function == os.rmdir: + try: + chmod(path, RW) + shutil.rmtree(path, True) + except Exception: + pass + + if os.path.exists(path): + logger.warning('Failed to delete directory %s', path) + + elif function == os.remove: + try: + delete(path, _err_handler=None) + except: + pass + + if os.path.exists(path): + logger.warning('Failed to delete file %s', path) + + +def delete(location, _err_handler=_rm_handler): + """ + Delete a directory or file at `location` recursively. Similar to "rm -rf" + in a shell or a combo of os.remove and shutil.rmtree. + """ + if not location: + return + + if os.path.exists(location) or filetype.is_broken_link(location): + chmod(os.path.dirname(location), RW) + if filetype.is_dir(location): + shutil.rmtree(location, False, _rm_handler) + else: + os.remove(location) diff --git a/src/commoncode/fileutils.py.ABOUT b/src/commoncode/fileutils.py.ABOUT new file mode 100644 index 00000000000..1d80f451ec1 --- /dev/null +++ b/src/commoncode/fileutils.py.ABOUT @@ -0,0 +1,17 @@ +about_resource: fileutils.py +version: 2.7.8 +download_url: https://hg.python.org/cpython/raw-file/ee879c0ffa11/Lib/shutil.py + +name: Python shutil + +description: fileutils borrows snippets of code from the shutil copytree, + copyfile and copymode functions modified to ignore special files and + permissions. +home_url: http://python.org/ + +owner: nexB + +dje_license: psf +license_text_file: PSF.LICENSE +copyright: Copyright (c) Python Software Foundation and others + diff --git a/src/commoncode/functional.py b/src/commoncode/functional.py new file mode 100644 index 00000000000..9b1cb73708d --- /dev/null +++ b/src/commoncode/functional.py @@ -0,0 +1,79 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +from types import ListType, TupleType, GeneratorType +import functools + + +def flatten(seq): + """ + Flatten a sequence sub-sequences: either a tuple, list or generator + (generators will be consumed) are converted to a flat list of elements. + + For example:: + >>> flatten([7, (6, [5, [4, ['a'], 3]], 3), 2, 1]) + [7, 6, 5, 4, 'a', 3, 3, 2, 1] + >>> def gen(): + ... for i in range(2): + ... yield range(5) + ... + >>> flatten(gen()) + [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] + + Originally derived from http://www.andreasen.org/misc/util.py + 2002-2005 by Erwin S. Andreasen -- http://www.andreasen.org/misc.shtml + This file is in the Public Domain + Version: Id: util.py,v 1.22 2005/12/16 00:08:21 erwin Exp erwin + """ + r = [] + for x in seq: + if isinstance(x, (ListType, TupleType)): + r.extend(flatten(x)) + # generator is not exposed as a built-in type by default + elif isinstance(x, GeneratorType): + r.extend(flatten([y for y in x])) + else: + r.append(x) + return r + + +def memoize(fun): + """ + Decorate fun function args and cache return values. Arguments must be + hashable. kwargs are not handled. Used to speed up some often executed + functions. + """ + memos = {} + @functools.wraps(fun) + def memoized(*args): + args = tuple(tuple(arg) if isinstance(arg, ListType) + else arg for arg in args) + try: + return memos[args] + except KeyError: + memos[args] = fun(*args) + return memos[args] + return functools.update_wrapper(memoized, fun) diff --git a/src/commoncode/hash.py b/src/commoncode/hash.py new file mode 100644 index 00000000000..8417407889f --- /dev/null +++ b/src/commoncode/hash.py @@ -0,0 +1,128 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import hashlib + +from commoncode import codec +from commoncode import filetype + + +""" +Hashes and checksums. + +Low level hash functions using standard crypto hashes used to construct hashes +of various lengths. Hashes that are smaller than 128 bits are based on a +truncated md5. Other length use SHA hashes. + +Checksums are operating on files. +""" + +def hash_mod(bitsize, hmodule): + """ + Return a hashing class returning hashes with a `bitsize` bit length. The + interface of this class is similar to the hash module API. + """ + class hash_cls(object): + def __init__(self, msg=None): + self.digest_size = size / 8 + if msg: + self.hd = module(msg).digest()[-(self.digest_size):] + else: + self.hd = None + + def digest(self): + return self.hd + + def hexdigest(self): + if self.hd: + return self.hd.encode('hex') + + def b64digest(self): + if self.hd: + return codec.urlsafe_b64encode(self.hd) + + def intdigest(self): + if self.hd: + return codec.bin_to_num(self.hd) + + size = bitsize + module = hmodule + return hash_cls + +# Base hashers for each bit size +bitsizes = { + # md5-based + 16: hashlib.md5, 24: hashlib.md5, 32: hashlib.md5, 48: hashlib.md5, + 64: hashlib.md5, 96: hashlib.md5, 128: hashlib.md5, + # sha-based + 160: hashlib.sha1, 224: hashlib.sha224, 256: hashlib.sha256, + 384: hashlib.sha384, 512: hashlib.sha512 +} + + +# All available hash modules keyed by the bit size of the hashed output. +hashmodules_by_bitsize = dict ((s, hash_mod(s, m),) + for s, m in bitsizes.items()) + + +def get_hasher(bitsize): + """ + Return a hasher for a given size in bits of the resulting hash. + """ + return hashmodules_by_bitsize[bitsize] + + +def checksum(location, bitsize, base64=False): + """ + Return a checksum of `bitsize` length from the content of the file at + `location`. The checksum is a hexdigest or base64-encoded is `base64` is + True. + """ + if not filetype.is_file(location): + return + hasher = get_hasher(bitsize) + + # fixme: we should read in chunks + with open(location, 'rb') as f: + hashable = f.read() + + hashed = hasher(hashable) + if base64: + return hashed.b64digest() + else: + return hashed.hexdigest() + + +def md5(location): + return checksum(location, bitsize=128, base64=False) + + +def sha1(location): + return checksum(location, bitsize=160, base64=False) + + +def b64sha1(location): + return checksum(location, bitsize=160, base64=True) diff --git a/src/commoncode/ignore.py b/src/commoncode/ignore.py new file mode 100644 index 00000000000..48fac3b6958 --- /dev/null +++ b/src/commoncode/ignore.py @@ -0,0 +1,310 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +from commoncode import fileset +from commoncode import filetype +from itertools import chain +from commoncode import fileutils + +""" +Handle .ignore-like files. +""" + + +def is_ignore_file(location): + """ + Return True if the location is an ignore file. + """ + return (filetype.is_file(location) + and fileutils.file_name(location) == '.scancodeignore') + + +def get_ignores(location, include_defaults=True): + """ + Return a ignores and unignores patterns mappings loaded from the + file at `location`. Optionally include defaults patterns + """ + ignores = {} + unignores = {} + if include_defaults: + ignores.update(default_ignores) + patterns = fileset.load(location) + ign, uni = fileset.includes_excludes(patterns, location) + ignores.update(ign) + unignores.update(uni) + return ignores, unignores + + +def is_ignored(location, ignores, unignores, skip_special=True): + """ + Return a tuple of (pattern , message) if a file at location is ignored + or False otherwise. + """ + if skip_special and filetype.is_special(location): + return True + return fileset.match(location, ignores, unignores) + + +# +# Default ignores +# + +ignores_MacOSX = { + '.DS_Store': 'Default ignore: MacOSX artifact', + '._.DS_Store': 'Default ignore: MacOSX artifact', + '__MACOSX': 'Default ignore: MacOSX artifact', + '.AppleDouble': 'Default ignore: MacOSX artifact', + '.LSOverride': 'Default ignore: MacOSX artifact', + '.DocumentRevisions-V100': 'Default ignore: MacOSX artifact', + '.fseventsd': 'Default ignore: MacOSX artifact', + '.Spotlight-V100': 'Default ignore: MacOSX artifact', + '.VolumeIcon.icns': 'Default ignore: MacOSX artifact', + + '.journal': 'Default ignore: MacOSX DMG/HFS+ artifact', + '.journal_info_block': 'Default ignore: MacOSX DMG/HFS+ artifact', + '.Trashes': 'Default ignore: MacOSX DMG/HFS+ artifact', + '[HFS+ Private Data]': 'Default ignore: MacOSX DMG/HFS+ artifact', +} + +ignores_Windows = { + 'Thumbs.db': 'Default ignore: Windows artifact', + 'ehthumbs.db': 'Default ignore: Windows artifact', + 'Desktop.ini': 'Default ignore: Windows artifact', + '$RECYCLE.BIN': 'Default ignore: Windows artifact', + '*.lnk': 'Default ignore: Windows artifact', + 'System Volume Information': 'Default ignore: Windows FS artifact', + 'NTUSER.DAT*': 'Default ignore: Windows FS artifact', +} + +ignores_Linux = { + '.directory': 'Default ignore: KDE artifact', + '.Trash-*': 'Default ignore: Linux/Gome/KDE artifact', +} + +ignores_IDEs = { + '*.el': 'Default ignore: EMACS Elisp artifact', + '*.swp': 'Default ignore: VIM artifact', + '.project': 'Default ignore: Eclipse IDE artifact', + '.pydevproject': 'Default ignore: Eclipse IDE artifact', + '.settings': 'Default ignore: Eclipse IDE artifact', + '.eclipse': 'Default ignore: Eclipse IDE artifact', + '.loadpath': 'Default ignore: Eclipse IDE artifact', + '*.launch': 'Default ignore: Eclipse IDE artifact', + '.cproject': 'Default ignore: Eclipse IDE artifact', + '.cdtproject': 'Default ignore: Eclipse IDE artifact', + '.classpath': 'Default ignore: Eclipse IDE artifact', + '.buildpath': 'Default ignore: Eclipse IDE artifact', + '.texlipse': 'Default ignore: Eclipse IDE artifact', + + '*.iml': 'Default ignore: JetBrains IDE artifact', + '*.ipr': 'Default ignore: JetBrains IDE artifact', + '*.iws': 'Default ignore: JetBrains IDE artifact', + '.idea/': 'Default ignore: JetBrains IDE artifact', + '.idea_modules/': 'Default ignore: JetBrains IDE artifact', + + '*.kdev4': 'Default ignore: Kdevelop artifact', + '.kdev4/': 'Default ignore: Kdevelop artifact', + + '*.nib': 'Default ignore: Apple Xcode artifact', + '*.plst': 'Default ignore: Apple Xcode plist artifact', + '*.pbxuser': 'Default ignore: Apple Xcode artifact', + '*.pbxproj': 'Default ignore: Apple Xcode artifact', + 'xcuserdata': 'Default ignore: Apple Xcode artifact', + '*.xcuserstate': 'Default ignore: Apple Xcode artifact', + + '*.csproj': 'Default ignore: Microsoft VS project artifact', + '*.unityproj': 'Default ignore: Microsoft VS project artifact', + '*.sln': 'Default ignore: Microsoft VS project artifact', + '*.sluo': 'Default ignore: Microsoft VS project artifact', + '*.suo': 'Default ignore: Microsoft VS project artifact', + '*.user': 'Default ignore: Microsoft VS project artifact', + '*.sln.docstates': 'Default ignore: Microsoft VS project artifact', + '*.dsw': 'Default ignore: Microsoft VS project artifact', + + '.editorconfig': 'Default ignore: Editor config artifact', + + ' Leiningen.gitignore': 'Default ignore: Leiningen artifact', + '.architect': 'Default ignore: ExtJS artifact', + '*.tmproj': 'Default ignore: Textmate artifact', + '*.tmproject': 'Default ignore: Textmate artifact', +} + +ignores_web = { + '.htaccess': 'Default ignore: .htaccess file', + 'robots.txt': 'Default ignore: robots file', + 'humans.txt': 'Default ignore: robots file', + 'web.config': 'Default ignore: web config', + '.htaccess.sample': 'Default ignore: .htaccess file', +} + +ignores_Maven = { + 'pom.xml.tag': 'Default ignore: Maven artifact', + 'pom.xml.releaseBackup': 'Default ignore: Maven artifact', + 'pom.xml.versionsBackup': 'Default ignore: Maven artifact', + 'pom.xml.next': 'Default ignore: Maven artifact', + 'release.properties': 'Default ignore: Maven artifact', + 'dependency-reduced-pom.xml': 'Default ignore: Maven artifact', + 'buildNumber.properties': 'Default ignore: Maven artifact', +} + +ignores_VCS = { + '.bzr': 'Default ignore: Bazaar artifact', + '.bzrignore' : 'Default ignore: Bazaar config artifact', + + '.git': 'Default ignore: Git artifact', + '.gitignore' : 'Default ignore: Git config artifact', + '.gitattributes': 'Default ignore: Git config artifact', + + '.hg': 'Default ignore: Mercurial artifact', + '.hgignore' : 'Default ignore: Mercurial config artifact', + + '.svn': 'Default ignore: SVN artifact', + '.svnignore': 'Default ignore: SVN config artifact', + + '.tfignore': 'Default ignore: Microsft TFS config artifact', + + 'vssver.scc': 'Default ignore: Visual Source Safe artifact', + + 'CVS': 'Default ignore: CVS artifact', + '.cvsignore': 'Default ignore: CVS config artifact', + '*/RCS': 'Default ignore: CVS artifact', + '*/SCCS': 'Default ignore: CVS artifact', + + '*/_MTN': 'Default ignore: Monotone artifact', + '*/_darcs': 'Default ignore: Darcs artifact', + '*/{arch}': 'Default ignore: GNU Arch artifact', + +} + +ignores_Medias = { + 'pspbrwse.jbf': 'Default ignore: Paintshop browse file', + 'Thumbs.db': 'Default ignore: Image thumbnails DB', + 'Thumbs.db:encryptable': 'Default ignore: Image thumbnails DB', + 'thumbs/': 'Default ignore: Image thumbnails DB', + '_thumbs/': 'Default ignore: Image thumbnails DB', +} + +ignores_Build_scripts = { + 'Makefile.in': 'Default ignore: automake artifact', + 'Makefile.am': 'Default ignore: automake artifact', + 'autom4te.cache': 'Default ignore: autoconf artifact', + '*.m4': 'Default ignore: autotools artifact', + 'configure': 'Default ignore: Configure script', + 'configure.bat': 'Default ignore: Configure script', + 'configure.sh': 'Default ignore: Configure script', + 'configure.ac': 'Default ignore: Configure script', + 'config.guess': 'Default ignore: Configure script', + 'config.sub': 'Default ignore: Configure script', + 'compile': 'Default ignore: autoconf artifact', + 'depcomp': 'Default ignore: autoconf artifact', + 'ltmain.sh': 'Default ignore: libtool autoconf artifact', + 'install-sh': 'Default ignore: autoconf artifact', + 'missing': 'Default ignore: autoconf artifact', + 'mkinstalldirs': 'Default ignore: autoconf artifact', + 'stamp-h1': 'Default ignore: autoconf artifact', + 'm4/': 'Default ignore: autoconf artifact', + 'autogen.sh': 'Default ignore: autotools artifact', + 'autogen.sh': 'Default ignore: autotools artifact', + + 'CMakeCache.txt': 'Default ignore: CMake artifact', + 'cmake_install.cmake': 'Default ignore: CMake artifact', + 'install_manifest.txt': 'Default ignore: CMake artifact', +} + +ignores_CI = { + '.travis.yml' : 'Default ignore: Travis config', + '.coveragerc' : 'Default ignore: Coverall config', +} + +ignores_Python = { + 'pip-selfcheck.json': 'Default ignore: Pip workfile', + 'pytest.ini': 'Default ignore: Python pytest config', + 'tox.ini': 'Default ignore: Python tox config', + '__pycache__/': 'Default ignore: Python bytecode cache', + '.installed.cfg': 'Default ignore: Python Buildout artifact', + 'pip-log.txt': 'Default ignore: Python pip artifact', + 'pip-delete-this-directory.txt': 'Default ignore: Python pip artifact', + 'pyvenv.cfg': 'Default ignore: Python virtualenv artifact', +} + +ignores_I18N = { + '*.mo': 'Default ignore: Translation file', + '*.pot': 'Default ignore: Translation file', + '.localized': 'Default ignore: localized file', +} + +ignores_coverage_and_tests = { + '*.gcno': 'Default ignore: GCC coverage', + '*.gcda': 'Default ignore: GCC coverage', + '*.gcov': 'Default ignore: GCC coverage', + '.last_cover_stats': 'Default ignore: Perl coverage', + 'htmlcov/': 'Default ignore: Python coverage', + '.tox/': 'Default ignore: Tox tem dir', + '.coverage': 'Default ignore: Python coverage', + '.coverage.*': 'Default ignore: Python coverage', + 'nosetests.xml': 'Default ignore: Python nose tests', + 'coverage.xml': 'Default ignore: Python coverage', + '/spec/reports/': 'Default ignore: Ruby Rails test report', + '/rdoc/': 'Default ignore: Ruby doc', + '.rvmrc': 'Default ignore: Ruby RVM', + '.sass-cache': 'Default ignore: Saas cache', + '*.css.map': 'Default ignore: Saas map', + 'phpunit.xml': 'Default ignore: phpunit', + '*.VisualState.xml': 'Default ignore: Nunit', + 'TestResult.xml': 'Default ignore: Nunit', +} + +ignores_Misc = { + 'pax_global_header': 'Default ignore: Pax header file', + 'C++.gitignore': 'Default ignore: C++.gitignore', + '.gwt/': 'Default ignore: GWT compilation logs', + '.gwt-tmp/': 'Default ignore: GWT temp files', + 'gradle-app.setting': 'Default ignore: Graddle app settings', + 'hs_err_pid*': 'Default ignore: Java VM crash logs', + '.grunt': 'Default ignore: Grunt intermediate storage', + '.history': 'Default ignore: History file', + '.~lock.*#': 'Default ignore: LibreOffice locks', + '/.ssh': 'Default ignore: SSH configuration', +} + +default_ignores = {} +default_ignores.update(chain(*[d.items() for d in [ + ignores_MacOSX, + ignores_Windows, + ignores_Linux, + ignores_IDEs, + ignores_web, + ignores_Maven, + ignores_VCS, + ignores_Medias, + ignores_Build_scripts, + ignores_CI, + ignores_Python, + ignores_I18N, + ignores_coverage_and_tests, + ignores_Misc, + ignores_Build_scripts, +]])) diff --git a/src/commoncode/paths.py b/src/commoncode/paths.py new file mode 100644 index 00000000000..a4bf71eab61 --- /dev/null +++ b/src/commoncode/paths.py @@ -0,0 +1,178 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + + +from os.path import commonprefix +import string +import posixpath + +from commoncode import fileutils +from commoncode import text + +""" +Various path utilities such as common prefix and suffix functions, conversion +to OS-safe paths and to POSIX paths. +""" + + +def resolve(path): + """ + Resolve and return a path-like string `path` to a posix relative path + string where extra slashes including leading and trailing slashes, dot + '.' and dotdot '..' path segments have been removed or normalized or + resolved with the provided path "tree". When a dotdot path segment cannot + be further resolved by "escaping" the provided path tree, it is replaced + by the string 'dotdot'. + """ + slash, dot = (u'/', u'.') if isinstance(path, unicode) else ('/', '.') + path = path.strip() + if not path: + return dot + + path = fileutils.as_posixpath(path) + path = path.strip('/') + segments = [s.strip() for s in path.split('/')] + # remove empty (// or ///) or blank (space only) or single dot segments + segments = [s for s in segments if s and s != '.'] + path = slash.join(segments) + # resolves .. + path = posixpath.normpath(path) + # replace .. with literal dotdot + segments = path.split('/') + segments = ['dotdot' if s == '..' else s for s in segments] + path = slash.join(segments) + return path + + +# +# Build OS-portable and safer paths +# + +""" +To convert a path to a safe cross-os path, we use a characters translation +table. This will replaces all non-safe characters by an underscore char. +""" +allchars = string.maketrans('', '') + + +# table of non safe characters: Exclude digit,letters and a select subset of +# supported punctuation, all the rest is junk. nb: we consider the backslash +# as path safe for now, but we convert these later to posix path +not_path_safe = string.translate(allchars, + allchars, + '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#+-./_\\') + +# create translation table to replace non-safe chars with underscore char +path_safe = string.maketrans(not_path_safe, '_' * len(not_path_safe)) + + +def safe_path(path, lowered=True, resolved=True): + """ + Convert a path-like string `path` to a posix path string safer to use as a + file path on all OSes. The path is lowercased. Non-ASCII alphanumeric + characters and spaces are replaced with an underscore. + The path is optionally resolved and lowercased. + """ + safe = path.strip() + # TODO: replace COM/PRN/LPT windows special names + # TODO: resolve 'UNC' windows paths + # TODO: strip leading windows drives + # remove any unsafe chars + safe = safe.translate(path_safe) + safe = text.toascii(safe) + safe = fileutils.as_posixpath(safe) + if lowered: + safe = safe.lower() + if resolved: + safe = resolve(safe) + return safe + + +# +# paths comparisons +# +def common_prefix(s1, s2): + """ + Return the common leading subsequence of two sequences and its length. + """ + if not s1 or not s2: + return None, 0 + common = commonprefix((s1, s2,)) + if common: + return common, len(common) + else: + return None, 0 + + +def common_suffix(s1, s2): + """ + Return the common trailing subsequence between two sequences and its length. + """ + if not s1 or not s2: + return None, 0 + # revert the seqs and get a common prefix + common, lgth = common_prefix(s1[::-1], s2[::-1]) + # revert again + common = common[::-1] if common else common + return common, lgth + + +def common_path_prefix(p1, p2): + """ + Return the common leading path between two posix paths and the number of + matched path segments. + """ + return _common_path(p1, p2, common_func=common_prefix) + + +def common_path_suffix(p1, p2): + """ + Return the common trailing path between two posix paths and the number of + matched path segments. + """ + return _common_path(p1, p2, common_func=common_suffix) + + +def split(p): + """ + Split a posix path in a sequence of segments, ignoring leading and + trailing slash. Return an empty sequence for an empty path and the root /. + """ + if not p: + return [] + p = p.strip('/').split('/') + return [] if p == [''] else p + + +def _common_path(p1, p2, common_func): + """ + Common function to compute common leading or trailing paths. + """ + common, lgth = common_func(split(p1), split(p2)) + common = '/'.join(common) if common else None + return common, lgth diff --git a/src/commoncode/system.py b/src/commoncode/system.py new file mode 100644 index 00000000000..3309168fc50 --- /dev/null +++ b/src/commoncode/system.py @@ -0,0 +1,136 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import sys +import os +import getpass +import subprocess + + +def os_arch(): + """ + Return a tuple for the current the OS and architecture. + """ + if sys.maxsize > 2 ** 32: + arch = '64' + else: + arch = '32' + + sys_platform = str(sys.platform).lower() + if 'linux' in sys_platform: + os = 'linux' + elif'win32' in sys_platform: + os = 'win' + elif 'darwin' in sys_platform: + os = 'mac' + else: + raise Exception('Unsupported OS/platform') + return os, arch + + +# FIXME use these for architectures +''' +darwin/386 +darwin/amd64 + +linux/386 +linux/amd64 +linux/arm + +windows/386 +windows/amd64 + +freebsd/386 +freebsd/amd64 +freebsd/arm + +openbsd/386 +openbsd/amd64 + +netbsd/386 +netbsd/amd64 +netbsd/arm + +plan9/386 +''' +# +# OS/Arch +# +current_os, current_arch = os_arch() +on_windows = current_os == 'win' +on_mac = current_os == 'mac' +on_linux = current_os == 'linux' +on_posix = not on_windows and (on_mac or on_linux) + + +current_os_arch = '%(current_os)s-%(current_arch)s' % locals() +noarch = 'noarch' +current_os_noarch = '%(current_os)s-%(noarch)s' % locals() + + +# +# Shared library file extensions +# +if on_windows: + lib_ext = '.dll' +if on_mac: + lib_ext = '.dylib' +if on_linux: + lib_ext = '.so' + + +# +# Python versions +# +py27 = (sys.version_info[0] == 2 and sys.version_info[1] == 7) +py34 = (sys.version_info[0] == 3 and sys.version_info[1] == 4) +py35 = (sys.version_info[0] == 3 and sys.version_info[1] == 5) + +# +# User related +# +if on_windows: + user_home = os.path.join(os.path.expandvars('$HOMEDRIVE'), + os.path.expandvars('$HOMEPATH')) +else: + user_home = os.path.expanduser('~') + +username = getpass.getuser() + + +# Do not let Windows error pop up messages with default SetErrorMode +# See http://msdn.microsoft.com/en-us/library/ms680621(VS100).aspx +# +# SEM_FAILCRITICALERRORS: +# The system does not display the critical-error-handler message box. +# Instead, the system sends the error to the calling process. +# +# SEM_NOGPFAULTERRORBOX: +# The system does not display the Windows Error Reporting dialog. +if on_windows: + import ctypes + # 3 is SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX + ctypes.windll.kernel32.SetErrorMode(3) # @UndefinedVariable diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py new file mode 100644 index 00000000000..058ab65a8ca --- /dev/null +++ b/src/commoncode/testcase.py @@ -0,0 +1,395 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + + +from __future__ import absolute_import, print_function + +from unittest import TestCase as TestCaseClass + +import os +import shutil +import sys +import filecmp +import posixpath +import ntpath +import zipfile +import tarfile +import stat + +from commoncode.system import on_windows +from commoncode import fileutils +from commoncode.system import on_posix +from commoncode import filetype + + + +class EnhancedAssertions(TestCaseClass): + """ + Common new new assertions made better or simpler. + """ + # always show full diff + maxDiff = None + + + def failUnlessRaisesInstance(self, excInstance, callableObj, + *args, **kwargs): + """ + This assertion accepts an instance instead of a class for refined + exception testing. + """ + excClass = excInstance.__class__ + try: + callableObj(*args, **kwargs) + except excClass, e: + self.assertEqual(str(excInstance), str(e)) + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException('%s not raised' % excName) + + assertRaisesInstance = failUnlessRaisesInstance + + +# a base test dir specific to a given test run +# to ensure that multiple tests run can be launched in parallel +test_run_temp_dir = None + + +# set to 1 to see the slow tests +timing_threshold = sys.maxint + + +def to_os_native_path(path): + """ + Normalize a path to use the native OS path separator. + """ + path = path.replace(posixpath.sep, os.path.sep) + path = path.replace(ntpath.sep, os.path.sep) + path = path.rstrip(os.path.sep) + return path + + +def get_test_loc(test_path, test_data_dir): + """ + Given a `test_path` relative to the `test_data_dir` directory, return the + location to a test file or directory for this path. No copy is done. + """ + assert test_path + assert test_data_dir + + + if not os.path.exists(test_data_dir): + raise IOError("[Errno 2] No such directory: test_data_dir not found:" + " '%(test_data_dir)s'" % locals()) + + tpath = to_os_native_path(test_path) + test_loc = os.path.abspath(os.path.join(test_data_dir, tpath)) + + if not os.path.exists(test_loc): + raise IOError("[Errno 2] No such file or directory: " + "test_path not found: '%(test_loc)s'" % locals()) + + return test_loc + + +class FileBasedTesting(EnhancedAssertions): + """ + Add support for handling test files and directories, including managing + temporary test resources and doing file-based assertions. + """ + test_data_dir = None + + def get_test_loc(self, test_path, copy=False): + """ + Given a `test_path` relative to the self.test_data_dir directory, return the + location to a test file or directory for this path. Copy to a temp + test location if `copy` is True. + """ + test_loc = get_test_loc(test_path, self.test_data_dir) + if copy: + base_name = os.path.basename(test_loc) + if filetype.is_file(test_loc): + # target must be an existing dir + target_dir = self.get_temp_dir() + fileutils.copyfile(test_loc, target_dir) + test_loc = os.path.join(target_dir, base_name) + else: + # target must be a NON existing dir + target_dir = os.path.join(self.get_temp_dir(), base_name) + fileutils.copytree(test_loc, target_dir) + # cleanup of VCS that could be left over from checkouts + self.remove_vcs(target_dir) + test_loc = target_dir + return test_loc + + def get_temp_file(self, extension=None, dir_name='td', file_name='tf'): + """ + Return a unique new temporary file location to a non-existing + temporary file that can safely be created without a risk of name + collision. + """ + if extension is None: + extension = '.txt' + + if extension and not extension.startswith('.'): + extension = '.' + extension + + file_name = file_name + extension + temp_dir = self.get_temp_dir(dir_name) + location = os.path.join(temp_dir, file_name) + return location + + def get_temp_dir(self, sub_dir_path=None): + """ + Create a unique new temporary directory location. Create directories + identified by sub_dir_path if provided in this temporary directory. + Return the location for this unique directory joined with the + sub_dir_path if any. + """ + # ensure that we have a new unique temp directory for each test run + global test_run_temp_dir + if not test_run_temp_dir: + test_run_temp_dir = fileutils.get_temp_dir(base_dir='tst', + prefix=' ') + + new_temp_dir = fileutils.get_temp_dir(base_dir=test_run_temp_dir) + + if sub_dir_path: + # create a sub directory hierarchy if requested + sub_dir_path = to_os_native_path(sub_dir_path) + new_temp_dir = os.path.join(new_temp_dir, sub_dir_path) + fileutils.create_dir(new_temp_dir) + return new_temp_dir + + def remove_vcs(self, test_dir): + """ + Remove some version control directories and some temp editor files. + """ + for root, dirs, files in os.walk(test_dir): + for vcs_dir in 'CVS', '.svn', '.git', '.hg': + if vcs_dir in dirs: + for vcsroot, vcsdirs, vcsfiles in os.walk(test_dir): + for vcsfile in vcsdirs + vcsfiles: + vfile = os.path.join(vcsroot, vcsfile) + fileutils.chmod(vfile, fileutils.RW) + shutil.rmtree(os.path.join(root, vcs_dir), False) + + # editors temp file leftovers + map(os.remove, [os.path.join(root, file_loc) + for file_loc in files if file_loc.endswith('~')]) + + def as_line_list(self, list_or_file, sort=False, skip_firstline=False): + """ + Given a list of file path as an input, return a list of text lines + suitable for comparison. Optionally skip the first line (for CSV + comparisons) and sort the list. + """ + L = [] + if isinstance(list_or_file, basestring): + L = open(list_or_file, 'rb').readlines() + elif isinstance(list_or_file, (list, tuple,)): + L = list_or_file + else: + raise Exception('unsupported object type: ' + 'must be a list, tuple or file name') + + if skip_firstline and L: + L = L[1:] + if sort: + L = sorted(L) + return L + + def failUnlessFilesLinesEqual(self, expected_list_or_file, + result_list_or_file, + msg=None, sort=False, skip_firstline=False): + """ + Check equality of two lists of lines or files lines content. + """ + expected = self.as_line_list(expected_list_or_file, sort, + skip_firstline) + result = self.as_line_list(result_list_or_file, sort, skip_firstline) + self.failUnlessEqual(expected, result, msg) + + assertLinesEqual = failUnlessFilesLinesEqual + + def __extract(self, test_path, extract_func=None, verbatim=False): + """ + Given an archive file identified by test_path relative + to a test files directory, return a new temp directory where the + archive file has been extracted using extract_func. + """ + assert test_path and test_path != '' + test_path = to_os_native_path(test_path) + target_path = os.path.basename(test_path) + target_dir = self.get_temp_dir(target_path) + origin_archive = self.get_test_loc(test_path) + extract_func(origin_archive, target_dir, verbatim) + return target_dir + + def extract_test_zip(self, test_path): + return self.__extract(test_path, extract_zip) + + def extract_test_tar(self, test_path, verbatim=False): + return self.__extract(test_path, extract_tar, verbatim) + + +def extract_tar(location, target_dir, verbatim=False): + """ + Extract a tar archive at location in the target_dir directory. + """ + with open(location, 'rb') as input_tar: + tar = tarfile.open(fileobj=input_tar) + tarinfos = tar.getmembers() + to_extract = [] + for tarinfo in tarinfos: + if tar_can_extract(tarinfo, verbatim): + if not verbatim: + tarinfo.mode = 0700 + to_extract.append(tarinfo) + tar.extractall(target_dir, members=to_extract) + + +def tar_can_extract(tarinfo, verbatim): + """ + Return True if a tar member can be extracted to handle OS specifics. + If verbatim is True, always return True. + """ + if tarinfo.ischr(): + # never extract char devices + return False + + if verbatim: + # extract all on all OSse + return True + + # FIXME: not sure hard links are working OK on Windows + include = tarinfo.type in tarfile.SUPPORTED_TYPES + exclude = tarinfo.isdev() or (on_windows and tarinfo.issym()) + + if include and not exclude: + return True + + +def extract_zip(location, target_dir, verbatim=True): + """ + Extract a zip archive file at location in the target_dir directory. + """ + if not os.path.isfile(location) and zipfile.is_zipfile(location): + raise Exception('Incorrect zip file %(location)r' % locals()) + + with zipfile.ZipFile(location) as zipf: + for info in zipf.infolist(): + name = info.filename + content = zipf.read(name) + target = os.path.join(target_dir, name) + if not os.path.exists(os.path.dirname(target)): + os.makedirs(os.path.dirname(target)) + if not content and target.endswith(os.path.sep): + if not os.path.exists(target): + os.makedirs(target) + if not os.path.exists(target): + with open(target, 'wb') as f: + f.write(content) + + +class dircmp(filecmp.dircmp): + """ + Compare the content of dir1 and dir2. In contrast with filecmp.dircmp, + this subclass also compares the content of files with the same path. + """ + + def phase3(self): + """ + Find out differences between common files. + Ensure we are using content comparison, not os.stat-only. + """ + comp = filecmp.cmpfiles(self.left, self.right, self.common_files, + shallow=False) + self.same_files, self.diff_files, self.funny_files = comp + + +def is_same(dir1, dir2): + """ + Compare two directory trees for structure and file content. + Return False if they differ, True is they are the same. + """ + compared = dircmp(dir1, dir2) + if (compared.left_only or compared.right_only or compared.diff_files + or compared.funny_files): + return False + + for subdir in compared.common_dirs: + if not is_same(os.path.join(dir1, subdir), + os.path.join(dir2, subdir)): + return False + return True + + +def file_cmp(file1, file2, ignore_line_endings=False): + """ + Compare two files content. + Return False if they differ, True is they are the same. + """ + with open(file1) as f1: + f1c = f1.read() + if ignore_line_endings: + f1c = '\n'.join(f1c.splitlines(False)) + with open(file2) as f2: + f2c = f2.read() + if ignore_line_endings: + f2c = '\n'.join(f2c.splitlines(False)) + assert f1c == f2c + + +def make_non_readable(location): + """ + Make location non readable for tests purpose. + """ + if on_posix: + current_stat = stat.S_IMODE(os.lstat(location).st_mode) + os.chmod(location, current_stat & ~stat.S_IREAD) + else: + os.chmod(location, 0555) + + +def make_non_writable(location): + """ + Make location non writable for tests purpose. + """ + if on_posix: + current_stat = stat.S_IMODE(os.lstat(location).st_mode) + os.chmod(location, current_stat & ~stat.S_IWRITE) + else: + make_non_readable(location) + + +def make_non_executable(location): + """ + Make location non executable for tests purpose. + """ + if on_posix: + current_stat = stat.S_IMODE(os.lstat(location).st_mode) + os.chmod(location, current_stat & ~stat.S_IEXEC) diff --git a/src/commoncode/text.py b/src/commoncode/text.py new file mode 100644 index 00000000000..e66be684e92 --- /dev/null +++ b/src/commoncode/text.py @@ -0,0 +1,201 @@ +# -*- coding: iso-8859-15 -*- +# NOTE: the iso-8859-15 charset is not a mistake. +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import re +import logging +import unicodedata + +""" +A text processing module providing functions to process and prepare text +before indexing or fingerprinting such as: + - case folding + - conversion of iso latin and unicode to ascii + - punctuation stripping + - line separator stripping and conversion + """ + + +LOG = logging.getLogger(__name__) + + +def lines(s): + """ + Split a string in lines using the following conventions: + - a line ending \r\n or \n is a separator and yields a new list element + - empty lines or lines with only white spaces are not returned. + - returned lines are stripped. + + Because of these constraints "".split() cannot be used directly. We first + replace things where we want to split with line endings, then we + splitlines. + + For example: + >>> t='''This problem is. + ... It is therefore + ... + ... + ... However,we + ... without introducing .. + ... However, I have + ... + ... + ... ''' + >>> len([p[1] for p in lines(t)]) + 5 + >>> [p for p in lines(t)] + ['This problem is.', 'It is therefore', 'However,we', 'without introducing ..', 'However, I have'] + """ + return [l.strip() for l in s.splitlines() if l.strip()] + + +def foldcase(text): + """ + Fold the cases of a text to lower case + """ + return text.lower() + + +def nopunc(): + return re.compile(r'[\W_]', re.MULTILINE) + + +def nopunctuation(text): + u""" + Replaces any non alphanum symbol (i.e. punctuation) in text with space. + Preserve the characters offsets by replacing punctuation with spaces. + Warning: this also drops line endings. + + For example: + >>> t = '''This problem is about sequence-bunching, %^$^%**^&*©©^(*&(*()()_+)_!@@#:><>>?/./,.,';][{}{]just''' + >>> nopunctuation(t).split() + ['This', 'problem', 'is', 'about', 'sequence', 'bunching', 'just'] + >>> t = r'''This problem is about: sequence-bunching + ... + ... just + ... ''' + >>> nopunctuation(t) + 'This problem is about sequence bunching just ' + """ + return re.sub(nopunc(), ' ', text) + + +CR = '\r' +LF = '\n' +CRLF = CR + LF +CRLF_NO_CR = ' ' + LF + + +def unixlinesep(text, preserve=False): + """ + Normalize a string to Unix line separators. Preserve character offset by + replacing with spaces if preserve is True. + + For example: + >>> t= CR + LF + LF + CR + CR + LF + >>> unixlinesep(t) == LF + LF + LF + LF + True + >>> unixlinesep(t, True) == ' ' + LF + LF + LF + ' ' + LF + True + """ + return text.replace(CRLF, CRLF_NO_CR if preserve else LF).replace(CR, LF) + + +def nolinesep(text): + """ + Removes line separators, replacing them with spaces. + For example: + >>> t = CR + LF + CR + CR + CR + LF + >>> nolinesep(t) == ' ' + True + """ + return text.replace(CR, ' ').replace(LF, ' ') + + +# additional non standard normalizations but quite common sense +unicode_translation_table = {u'ؘ': u'O', u'': u'o', u'': u'z', u'': u'Z'} + +# Other candidates +{ + u'': u'c', + # not space preserving + u'': u'a', + u'Ɔ': u'A', + + u'': u'o', + u'': u'O', + + u'': u'(c)', + u'': u'(r)', + + # see http://en.wikipedia.org/wiki/ß + u'ߟ': u'ss', + u'\u1e9e': u'SS' +} + + +def toascii(s): + u""" + Convert a Unicode string to ASCII characters, including replacing accented + characters with their non-accented NFKD equivalent. Non ISO-Latin and non + ASCII characters are stripped from the output. For Unicode NFKD + equivalence, see http://en.wikipedia.org/wiki/Unicode_equivalence + + Does not preserve the original length and character offsets. + + Inspired from: + http://code.activestate.com/recipes/251871/#c10 by Aaron Bentley. + + For example: + >>> acc = u"" + >>> noacc = r"AAAAAACEEEEIIIINOOOOOUUUUYaaaaaaceeeeiiiinooooouuuuyy" + >>> toascii(acc) == noacc + True + """ + try: + return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') + except: + return str(s.decode('ascii', 'ignore')) + + +def python_safe_name(s): + """ + Return a name derived from string `s` safe to use as a Python identifier. + + For example: + + >>> s = "not `\\a /`good` -safe name ??" + >>> python_safe_name(s) + 'not_good_safe_name' + + """ + s = toascii(s) + s = foldcase(s) + s = nopunctuation(s) + s = s.strip() + s = '_'.join(s.split()) + return s diff --git a/src/commoncode/timeutils.py b/src/commoncode/timeutils.py new file mode 100644 index 00000000000..bf31e3bf355 --- /dev/null +++ b/src/commoncode/timeutils.py @@ -0,0 +1,101 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + + +from datetime import datetime, tzinfo + +""" +Time is of the essence: path safe time stamps creation and conversion to +datetime objects. +""" + +class UTC(tzinfo): + """UTC timezone""" + def utcoffset(self, dt): # @UnusedVariable + return None + + def tzname(self, dt): # @UnusedVariable + return 'UTC' + + def dst(self, dt): # @UnusedVariable + return None + + +def time2tstamp(dt=None): + """ + Return a timestamp representing the datetime object (assumed to be in UTC + time) or the current UTC time (if dt == None) formatted using the ISO 8601 + standard as a basis, extended to be path safe. + + The Python isoformat returns a time stamp that complies with this standard + but has limitations when used in a file or directory name. Here we + transform the returned time stamp such that the result still complies with + the ISO standard and can be safely used as part of a of file or directory + name in a portable and OS safe fashion including on Windows where colons + are not allowed in file names, or on posix where / denotes a path segment + separator. + + For times, the ISO 8601 format specifies either a colon : (extended format) + or nothing as a separator (basic format). Here Python defaults to using a + colon. We therefore remove all the colons to be file system safe. + + Another character may show up in the ISO representation such as / for time + intervals. We could replace the forward slash with a double hyphen (--) as + a separator instead (see Section 4.4.2 of the ISO standard). However since + there are several places where hyphens are used, this makes it difficult to + parse back. Instead we use an _ (underscore) to make the time stamp easier + to convert back to a datetime object. + """ + # TODO: check that the dt is effectively in UTC + datim = dt or datetime.utcnow() + return datim.isoformat().replace(':', '').replace('/', '_') + + +def tstamp2time(stamp): + """ + Convert a UTC timestamp to a datetime object. + """ + # handle both basic and extended formats + tformat = '%Y-%m-%dT%H%M%S' if stamp[4] == '-' else '%Y%m%dT%H%M%S' + # normalize + dt_ms = stamp.strip().replace('Z', '').replace(':', '') + + dt_ms = dt_ms.split('.') + isodatim = dt_ms[0] + datim = datetime.strptime(isodatim, tformat) + # all stamps must be UTC + datim = datim.replace(tzinfo=UTC()) + + # deal with optional microsec + try: + microsec = dt_ms[1] + except: + microsec = None + if microsec: + microsec = int(microsec) + if 0 <= microsec <= 999999: + datim = datim.replace(microsecond=microsec) + return datim diff --git a/src/commoncode/urn.py b/src/commoncode/urn.py new file mode 100644 index 00000000000..9b416541bd1 --- /dev/null +++ b/src/commoncode/urn.py @@ -0,0 +1,151 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +""" +URN: Uniform Resource Name for DejaCode +======================================= + +In the DejaCode platform, a URN is a globally unique and universal key to +reference data across tools and data sources. URNs are used throughout the +platform, such as in ScanCode and related tools as an object key. + +URNs are both readable by humans and machines. + +The URN syntax is a Internet standard specified in: + * RFC2141 http://tools.ietf.org/html/rfc2141 + * RFC2396 http://tools.ietf.org/html/rfc2396 + +A DejaCode URN follows the URN syntax specification and is case sensitive. It +does support UTF-8 characters when these are URL-encoded (using the quote+ +encoding). + + +Syntax and Examples +------------------- + +The generic syntax of a DejaCode URN is:: + urn::: +where: + * is always "dje" + * is a DejaCode object type. Not all objects are supported. + Current support includes owner, license and component. + * is one or more object-specific field each field separated by a + colon. + +The syntax for an Owner is:: + urn:dje:owner: +where owner_name is the name of the owner. +Example:: + urn:dje:owner:Apache+Software+Foundation + +The syntax for a License is:: + urn:dje:license: +where license_key is the key of the license. +Example:: + urn:dje:license:apache-2.0 + +The syntax for a Component is:: + urn:dje:component:: +where: + * component_name is the name of the component + * component_version is the version of the component. If no version is + defined, use a trailing colon representing an empty value/undefined version. + +Examples: + * with version:: + urn:dje:component:dropbear:1.0 + * without version:: + urn:dje:component:dropbear: + +The product object type syntax is the same as the component syntax. +""" + +from urllib import quote_plus +from urllib import unquote_plus + + +class URNValidationError(Exception): + """The URN format is not valid.""" + pass + + +# Describes the URN schema for each object type +URN_SCHEMAS = { + 'license': { 'object': 'license', 'fields': ['key']}, + 'owner': { 'object': 'owner', 'fields': ['name']}, + 'component': { 'object': 'component', 'fields': ['name', 'version']}, + 'product': { 'object': 'product', 'fields': ['name', 'version']}, +} + + +def encode(object_type, **fields): + """ + Return an encoded URN based given an object_type string and a dictionary + of the object-specific fields. All field values must be provided even if + empty. In this case use an empty string. + + This is a string and local operation only: the URN is NOT resolved and + therefore validity of the data for the URN as a whole and for each URN + segment is not checked. + """ + + # case is not significant for the object type + object_type = object_type.strip().lower() + urn_prefix = u'urn:dje:{0}:'.format(quote_plus(object_type)) + + object_fields = URN_SCHEMAS[object_type]['fields'] + # leading and trailing white spaces are not significant + # each URN part is encoded individually BEFORE assembling the URN + encoded_fields = [quote_plus(fields[f].strip()) for f in object_fields] + encoded_fields = u':'.join(encoded_fields) + return urn_prefix + encoded_fields + + +def decode(urn): + """ + Decode a URN and return the object_type and a mapping of field/values. + Raise URNValidationError on errors. + """ + segments = [unquote_plus(p) for p in urn.split(':')] + + if not segments[0] == ('urn'): + raise URNValidationError("Invalid URN prefix. Expected 'urn'.") + + if not segments[1] == ('dje'): + raise URNValidationError("Invalid URN namespace. Expected 'dje'.") + + # object type is always lowercase + object_type = segments[2].lower() + if object_type not in URN_SCHEMAS.keys(): + raise URNValidationError('Unsupported URN object type.') + + fields = segments[3:] + schema_fields = URN_SCHEMAS[object_type]['fields'] + if len(schema_fields) != len(fields): + raise URNValidationError('Invalid number of fields in URN.') + decoded_fields = dict(zip(schema_fields, fields)) + + return object_type, decoded_fields diff --git a/src/commoncode/version.py b/src/commoncode/version.py new file mode 100644 index 00000000000..385e9d4b710 --- /dev/null +++ b/src/commoncode/version.py @@ -0,0 +1,73 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + + +import re + + +def VERSION_PATTERNS_REGEX(): + return [re.compile(x) for x in [ + # Eclipse features + 'v\d+\.feature\_(\d+\.){1,3}\d+', + # Common version patterns + '(M?(v\d+(\-|\_))?\d+\.){1,3}\d+[A-Za-z0-9]*((\.|\-|_|~)' + '(b|B|rc|r|v|RC|alpha|beta|BETA|M|m|pre|vm|G)?\d+((\-|\.)\d+)?)?' + '((\.|\-)(((alpha|dev|beta|rc|FINAL|final|pre)(\-|\_)\d+[A-Za-z]?' + '(\-RELEASE)?)|alpha|dev(\.\d+\.\d+)?' + '|beta|BETA|final|FINAL|release|fixed|(cr\d(\_\d*)?)))?', + # + '[A-Za-z]?(\d+\_){1,3}\d+\_?[A-Za-z]{0,2}\d+', + # + '(b|rc|r|v|RC|alpha|beta|BETA|M|m|pre|revision-)\d+(\-\d+)?', + # + 'current|previous|latest|alpha|beta', + # + '\d{4}-\d{2}-\d{2}', + # + '(\d(\-|\_)){1,2}\d', + # + '\d{5,14}', +]] + + +def hint(path): + """ + Return a version found in a path or None. Prefix the version with 'v ' if + the version does not start with v. + """ + for pattern in VERSION_PATTERNS_REGEX(): + segments = path.split('/') + # skip the first path segment unless there's only one segment + first_segment = 1 if len(segments) > 1 else 0 + interesting_segments = segments[first_segment:] + # we iterate backwards from the end of the paths segments list + for segment in interesting_segments[::-1]: + version = re.search(pattern, segment) + if version: + v = version.group(0) + if not v.lower().startswith('v'): + v = 'v ' + v + return v From f6876889fd78e62f70fdab444b7b903c8d871508 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 1 Jul 2015 16:52:29 +0200 Subject: [PATCH 002/436] Initial commit. --- .../data/command/bin/linux-32/bin/cmd | 0 .../command/bin/linux-32/lib/libmagic32.so | Bin 0 -> 24 bytes .../data/command/bin/linux-64/bin/cmd | 0 .../command/bin/linux-64/lib/libmagic64.so | Bin 0 -> 26 bytes .../data/command/bin/linux-noarch/bin/cmd | 0 .../data/command/bin/mac-32/bin/cmd | 0 .../command/bin/mac-32/lib/libmagic.dylib | Bin 0 -> 61 bytes .../data/command/bin/mac-64/bin/cmd | 0 .../command/bin/mac-64/lib/libmagic.dylib | Bin 0 -> 61 bytes .../data/command/bin/mac-noarch/bin/cmd | 0 .../data/command/bin/noarch/bin/cmd | 0 .../commoncode/data/command/bin/noarch/lib/l | 0 .../data/command/bin/win-32/bin/cmd.exe | 1 + .../data/command/bin/win-32/bin/magic1.dll | Bin 0 -> 138 bytes .../data/command/bin/win-64/bin/cmd.exe | 1 + .../data/command/bin/win-64/bin/magic1.dll | Bin 0 -> 138 bytes .../data/command/bin/win-noarch/bin/cmd.exe | 1 + .../data/command/bin/win-noarch/bin/some.dll | Bin 0 -> 138 bytes .../commoncode/data/count/filecount/dir/a.txt | 1 + .../commoncode/data/count/filecount/dir/b.txt | 1 + .../commoncode/data/count/filecount/dir/c.txt | 1 + .../data/count/filecount/dir/sub1/a.txt | 1 + .../data/count/filecount/dir/sub1/b.txt | 1 + .../data/count/filecount/dir/sub1/c.txt | 1 + .../count/filecount/dir/sub1/subsub/a.txt | 1 + .../count/filecount/dir/sub1/subsub/b.txt | 1 + .../data/count/filecount/dir/sub2/a.txt | 1 + .../data/date/ant-jsch-1.7.0.tar.gz | Bin 0 -> 596 bytes .../commoncode/commoncode/data/date/dir/a.txt | 1 + .../commoncode/commoncode/data/date/dir/b.txt | 1 + .../commoncode/commoncode/data/date/dir/c.txt | 1 + .../commoncode/data/date/dir/sub1/a.txt | 1 + .../commoncode/data/date/dir/sub1/b.txt | 1 + .../commoncode/data/date/dir/sub1/c.txt | 1 + .../data/date/dir/sub1/subsub/a.txt | 1 + .../data/date/dir/sub1/subsub/b.txt | 1 + .../commoncode/data/date/dir/sub2/a.txt | 1 + .../data/fileset/scancodeignore.lst | 5 + .../data/filetype/readwrite/sub/file | 0 .../commoncode/data/filetype/size/Image1.eps | Bin 0 -> 12388 bytes .../commoncode/data/filetype/size/exec/1.txt | 1 + .../commoncode/data/filetype/size/exec/a.bat | 1 + .../commoncode/data/filetype/size/exec/a.tar | 1 + .../data/filetype/size/exec/subtxt/1.txt | 1 + .../data/filetype/size/exec/subtxt/a.txt | 1 + .../data/filetype/size/exec/subtxt/b.txt | 1 + .../commoncode/data/filetype/types.tar | Bin 0 -> 109056 bytes .../data/fileutils/basename/a/.a/file | 0 .../commoncode/data/fileutils/basename/a/b/.a | 0 .../data/fileutils/basename/a/b/.a.b | 0 .../data/fileutils/basename/a/b/a.tag.gz | 0 .../commoncode/data/fileutils/basename/a/f.a | 0 .../data/fileutils/basename/f.a/a.c | 0 .../commoncode/data/fileutils/basename/tst | 0 .../commoncode/data/fileutils/exec/1.txt | 1 + .../commoncode/data/fileutils/exec/a.bat | 1 + .../commoncode/data/fileutils/exec/a.tar | 1 + .../data/fileutils/exec/subtxt/1.txt | 1 + .../data/fileutils/exec/subtxt/a.txt | 1 + .../data/fileutils/exec/subtxt/b.txt | 1 + .../fileutils/executable/deep1/deep2/ctags | Bin 0 -> 73 bytes .../commoncode/data/fileutils/filetype/a.html | 13 + .../commoncode/data/fileutils/filetype/stage1 | Bin 0 -> 512 bytes .../data/fileutils/perms/unreadable.tgz | Bin 0 -> 195 bytes .../data/fileutils/readwrite/sub/file | 0 .../data/fileutils/textfiles/dos_newlines.txt | 28 ++ .../data/fileutils/textfiles/mac_newlines.txt | 1 + .../fileutils/textfiles/unix_newlines.txt | 23 + .../commoncode/data/fileutils/unicode.zip | Bin 0 -> 1136 bytes .../commoncode/data/hash/dir1/a.png | Bin 0 -> 7859 bytes .../commoncode/data/hash/dir1/a.txt | 3 + .../commoncode/data/hash/dir2/a.txt | 3 + .../commoncode/data/hash/dir2/dos.txt | 3 + .../commoncode/data/ignore/.scancodeignore | 5 + .../data/ignore/excludes/.localized | 0 .../data/ignore/excludes/eclipse.tgz | Bin 0 -> 225 bytes .../commoncode/data/ignore/excludes/mac.tgz | Bin 0 -> 409 bytes .../data/ignore/excludes/msft-vs.tgz | Bin 0 -> 163 bytes .../commoncode/commoncode/data/ignore/vcs.tgz | Bin 0 -> 5555 bytes tests/commoncode/commoncode/test_codec.py | 125 ++++++ tests/commoncode/commoncode/test_command.py | 236 ++++++++++ tests/commoncode/commoncode/test_date.py | 68 +++ tests/commoncode/commoncode/test_fileset.py | 98 +++++ tests/commoncode/commoncode/test_filetype.py | 201 +++++++++ tests/commoncode/commoncode/test_fileutils.py | 403 ++++++++++++++++++ .../commoncode/commoncode/test_functional.py | 51 +++ tests/commoncode/commoncode/test_hash.py | 92 ++++ tests/commoncode/commoncode/test_ignore.py | 178 ++++++++ tests/commoncode/commoncode/test_paths.py | 253 +++++++++++ tests/commoncode/commoncode/test_timeutils.py | 91 ++++ tests/commoncode/commoncode/test_urn.py | 164 +++++++ tests/commoncode/commoncode/test_version.py | 139 ++++++ 92 files changed, 2216 insertions(+) create mode 100644 tests/commoncode/commoncode/data/command/bin/linux-32/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/linux-32/lib/libmagic32.so create mode 100644 tests/commoncode/commoncode/data/command/bin/linux-64/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/linux-64/lib/libmagic64.so create mode 100644 tests/commoncode/commoncode/data/command/bin/linux-noarch/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/mac-32/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/mac-32/lib/libmagic.dylib create mode 100644 tests/commoncode/commoncode/data/command/bin/mac-64/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/mac-64/lib/libmagic.dylib create mode 100644 tests/commoncode/commoncode/data/command/bin/mac-noarch/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/noarch/bin/cmd create mode 100644 tests/commoncode/commoncode/data/command/bin/noarch/lib/l create mode 100644 tests/commoncode/commoncode/data/command/bin/win-32/bin/cmd.exe create mode 100644 tests/commoncode/commoncode/data/command/bin/win-32/bin/magic1.dll create mode 100644 tests/commoncode/commoncode/data/command/bin/win-64/bin/cmd.exe create mode 100644 tests/commoncode/commoncode/data/command/bin/win-64/bin/magic1.dll create mode 100644 tests/commoncode/commoncode/data/command/bin/win-noarch/bin/cmd.exe create mode 100644 tests/commoncode/commoncode/data/command/bin/win-noarch/bin/some.dll create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/a.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/b.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/c.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/sub1/a.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/sub1/b.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/sub1/c.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/a.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/b.txt create mode 100644 tests/commoncode/commoncode/data/count/filecount/dir/sub2/a.txt create mode 100644 tests/commoncode/commoncode/data/date/ant-jsch-1.7.0.tar.gz create mode 100644 tests/commoncode/commoncode/data/date/dir/a.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/b.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/c.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/sub1/a.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/sub1/b.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/sub1/c.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/sub1/subsub/a.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/sub1/subsub/b.txt create mode 100644 tests/commoncode/commoncode/data/date/dir/sub2/a.txt create mode 100644 tests/commoncode/commoncode/data/fileset/scancodeignore.lst create mode 100644 tests/commoncode/commoncode/data/filetype/readwrite/sub/file create mode 100644 tests/commoncode/commoncode/data/filetype/size/Image1.eps create mode 100644 tests/commoncode/commoncode/data/filetype/size/exec/1.txt create mode 100644 tests/commoncode/commoncode/data/filetype/size/exec/a.bat create mode 100644 tests/commoncode/commoncode/data/filetype/size/exec/a.tar create mode 100644 tests/commoncode/commoncode/data/filetype/size/exec/subtxt/1.txt create mode 100644 tests/commoncode/commoncode/data/filetype/size/exec/subtxt/a.txt create mode 100644 tests/commoncode/commoncode/data/filetype/size/exec/subtxt/b.txt create mode 100644 tests/commoncode/commoncode/data/filetype/types.tar create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/a/.a/file create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/a/b/.a create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/a/b/.a.b create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/a/b/a.tag.gz create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/a/f.a create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/f.a/a.c create mode 100644 tests/commoncode/commoncode/data/fileutils/basename/tst create mode 100644 tests/commoncode/commoncode/data/fileutils/exec/1.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/exec/a.bat create mode 100644 tests/commoncode/commoncode/data/fileutils/exec/a.tar create mode 100644 tests/commoncode/commoncode/data/fileutils/exec/subtxt/1.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/exec/subtxt/a.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/exec/subtxt/b.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/executable/deep1/deep2/ctags create mode 100644 tests/commoncode/commoncode/data/fileutils/filetype/a.html create mode 100644 tests/commoncode/commoncode/data/fileutils/filetype/stage1 create mode 100644 tests/commoncode/commoncode/data/fileutils/perms/unreadable.tgz create mode 100644 tests/commoncode/commoncode/data/fileutils/readwrite/sub/file create mode 100644 tests/commoncode/commoncode/data/fileutils/textfiles/dos_newlines.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/textfiles/mac_newlines.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/textfiles/unix_newlines.txt create mode 100644 tests/commoncode/commoncode/data/fileutils/unicode.zip create mode 100644 tests/commoncode/commoncode/data/hash/dir1/a.png create mode 100644 tests/commoncode/commoncode/data/hash/dir1/a.txt create mode 100644 tests/commoncode/commoncode/data/hash/dir2/a.txt create mode 100644 tests/commoncode/commoncode/data/hash/dir2/dos.txt create mode 100644 tests/commoncode/commoncode/data/ignore/.scancodeignore create mode 100644 tests/commoncode/commoncode/data/ignore/excludes/.localized create mode 100644 tests/commoncode/commoncode/data/ignore/excludes/eclipse.tgz create mode 100644 tests/commoncode/commoncode/data/ignore/excludes/mac.tgz create mode 100644 tests/commoncode/commoncode/data/ignore/excludes/msft-vs.tgz create mode 100644 tests/commoncode/commoncode/data/ignore/vcs.tgz create mode 100644 tests/commoncode/commoncode/test_codec.py create mode 100644 tests/commoncode/commoncode/test_command.py create mode 100644 tests/commoncode/commoncode/test_date.py create mode 100644 tests/commoncode/commoncode/test_fileset.py create mode 100644 tests/commoncode/commoncode/test_filetype.py create mode 100644 tests/commoncode/commoncode/test_fileutils.py create mode 100644 tests/commoncode/commoncode/test_functional.py create mode 100644 tests/commoncode/commoncode/test_hash.py create mode 100644 tests/commoncode/commoncode/test_ignore.py create mode 100644 tests/commoncode/commoncode/test_paths.py create mode 100644 tests/commoncode/commoncode/test_timeutils.py create mode 100644 tests/commoncode/commoncode/test_urn.py create mode 100644 tests/commoncode/commoncode/test_version.py diff --git a/tests/commoncode/commoncode/data/command/bin/linux-32/bin/cmd b/tests/commoncode/commoncode/data/command/bin/linux-32/bin/cmd new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/command/bin/linux-32/lib/libmagic32.so b/tests/commoncode/commoncode/data/command/bin/linux-32/lib/libmagic32.so new file mode 100644 index 0000000000000000000000000000000000000000..0eacba4796e7afccc3fbbd77d147fc5351b56773 GIT binary patch literal 24 Wcmb<-^>JflWMqH=W(H;k5Dx$z-~nL( literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/command/bin/linux-64/bin/cmd b/tests/commoncode/commoncode/data/command/bin/linux-64/bin/cmd new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/command/bin/linux-64/lib/libmagic64.so b/tests/commoncode/commoncode/data/command/bin/linux-64/lib/libmagic64.so new file mode 100644 index 0000000000000000000000000000000000000000..78d3832570d0b9292639e8b09de99b077b671077 GIT binary patch literal 26 Ycmb<-^>JfjWMqH=W(GS35buE@040F}t^fc4 literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/command/bin/linux-noarch/bin/cmd b/tests/commoncode/commoncode/data/command/bin/linux-noarch/bin/cmd new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/command/bin/mac-32/bin/cmd b/tests/commoncode/commoncode/data/command/bin/mac-32/bin/cmd new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/command/bin/mac-32/lib/libmagic.dylib b/tests/commoncode/commoncode/data/command/bin/mac-32/lib/libmagic.dylib new file mode 100644 index 0000000000000000000000000000000000000000..15ad00c00b55b43931f6f6064461ae897e6093cb GIT binary patch literal 61 wcmaFAfA4!3VrO7rUu0x_cxh!K*JS*%b{ ml%HOdn5&SSn3tDdqL7rTP*j?ykeR38;vcM#o1c=Z#|r?FG$)(@ literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/command/bin/win-64/bin/cmd.exe b/tests/commoncode/commoncode/data/command/bin/win-64/bin/cmd.exe new file mode 100644 index 00000000000..0519ecba6ea --- /dev/null +++ b/tests/commoncode/commoncode/data/command/bin/win-64/bin/cmd.exe @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/commoncode/commoncode/data/command/bin/win-64/bin/magic1.dll b/tests/commoncode/commoncode/data/command/bin/win-64/bin/magic1.dll new file mode 100644 index 0000000000000000000000000000000000000000..e898ea427ebb05eb0cdf7910139c4aee601b2b7e GIT binary patch literal 138 zcmeZ`dcS`!12Y2y0}BuX*&r^62J%3_0Zig2AgcM~fixdTD<=>u0x_cxh!K*JS*%b{ ml%HOdn5&SSn3tDdqL7rTP*j?ykeR38;vcM#o1c=Z#|r?FG$)(@ literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/command/bin/win-noarch/bin/cmd.exe b/tests/commoncode/commoncode/data/command/bin/win-noarch/bin/cmd.exe new file mode 100644 index 00000000000..0519ecba6ea --- /dev/null +++ b/tests/commoncode/commoncode/data/command/bin/win-noarch/bin/cmd.exe @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/commoncode/commoncode/data/command/bin/win-noarch/bin/some.dll b/tests/commoncode/commoncode/data/command/bin/win-noarch/bin/some.dll new file mode 100644 index 0000000000000000000000000000000000000000..e898ea427ebb05eb0cdf7910139c4aee601b2b7e GIT binary patch literal 138 zcmeZ`dcS`!12Y2y0}BuX*&r^62J%3_0Zig2AgcM~fixdTD<=>u0x_cxh!K*JS*%b{ ml%HOdn5&SSn3tDdqL7rTP*j?ykeR38;vcM#o1c=Z#|r?FG$)(@ literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/a.txt b/tests/commoncode/commoncode/data/count/filecount/dir/a.txt new file mode 100644 index 00000000000..61780798228 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/a.txt @@ -0,0 +1 @@ +b diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/b.txt b/tests/commoncode/commoncode/data/count/filecount/dir/b.txt new file mode 100644 index 00000000000..61780798228 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/b.txt @@ -0,0 +1 @@ +b diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/c.txt b/tests/commoncode/commoncode/data/count/filecount/dir/c.txt new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/c.txt @@ -0,0 +1 @@ +1 diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/sub1/a.txt b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/a.txt new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/a.txt @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/sub1/b.txt b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/b.txt new file mode 100644 index 00000000000..61780798228 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/b.txt @@ -0,0 +1 @@ +b diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/sub1/c.txt b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/c.txt new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/c.txt @@ -0,0 +1 @@ +1 diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/a.txt b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/a.txt new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/a.txt @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/b.txt b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/b.txt new file mode 100644 index 00000000000..61780798228 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/sub1/subsub/b.txt @@ -0,0 +1 @@ +b diff --git a/tests/commoncode/commoncode/data/count/filecount/dir/sub2/a.txt b/tests/commoncode/commoncode/data/count/filecount/dir/sub2/a.txt new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/count/filecount/dir/sub2/a.txt @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/date/ant-jsch-1.7.0.tar.gz b/tests/commoncode/commoncode/data/date/ant-jsch-1.7.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..904ce06d81f1fc1ad511c64eb1f7647d2ed15679 GIT binary patch literal 596 zcmV-a0;~NWiwFSaYX4LK1MS$|ZrU&$hhZ*-gPp?s5y!_=1r@2FNw5oWQ}eOX7}=S$ z-u)a-Ikc7mlzzeklqn@Y5L{dY)IS&e?UVmG2=IVN8Rw?NM7BbDNO^k45kKb@mA zV?t(vWZ8gFP6Zzf#ZCU2Pky>e+2L?-G_OxqKdZ9T7k_VQ8czJ*)ZhIV)AeLDU(R}6 z2qFDBb^BnzCXzpJbb8x+^f}sBOd75)Z(LaT@ z{{DRDH4xUH`To*>i~cEG)4x*g$lIaYg9szcyH2;To06Ffuz z6yo|jw+mPUA@_e&*8Mq0|1{$JPo7+5RZ*?nZlRr1Tm6NOi-U6R^Lqr}|8u_s;Ql{} zUi*KuW^4WHg(>%yvG1!Pygz3dNBL)q_qN(p4I4tl=RcmKe-fSgugw+HA9ejdJ^w}j+v}(QzotKG{z-=ZNp$GH zdbm@rW%N%XKL6$Zfc^<|>OcLZi>8OZA4K$*c>b40=l(aEf10Cz3Z43YciPU&jp_gS ikAH#w00000000000000000000*Z2)J5#lufPyhf2&pTF2_8(Whjn*oSK6J;>;N@{yB~p% z;L)RBK;x0<#gE|8c=BX@w%r%2CypH6ZMM(5v-8ZK-#*9rc=Yv?BBCz^{N>kA)V4&_ z?Gnw;ZEO}Fh31tmR!U{Hwz0X6;(<3mzhZXcFp3*1rc+gA{3@z~=2feGXKYnH_0vFY znk~2ITfL-eaZtkRXOU~^s#>kqL%mzyv)e`EqkQcLCe%e|Olv!Gb*EFe6}5C)4@~TI z?A)6~*V{Ub{CchJH*}?>layzRO#Ik)7`2R+wK!Zg?KV8Ax4ajHu8D<(ddpjG>Zs9l z{FFqUf8OHx{|-Uwtm=0{g{}6-Nzx>V0=4foQh!(H9oXx==o>O1dE0Rj>&8oOKiIJr ztIM{hFitiHkcCa%$tOuna^OYTE>*(u}l-4M~YVWQCyqKD^*nxjOA=ZQXhM87T& zEsPO8zex1<648gtMBm4WrmtX}tME_2KMDU7{MX>0fqxeM>+s)z|0etj{yF$>!G9b6 zJMiCy{~r7c@ZX1j5&k9kvCcBqb=vMjaO_2rU;?a?2_eA*SS1ref(fun zCWHhNV3kY=2`0cQnGh08fK@UfB$xoJWI{+V0anR`kYEC=k_jQf1Xv{#LV^jfN+yH^ z6JV80oO%*t^qYozC%JJb8ArN@di4@cO&130GkJ|>8XBla2I}#`FySCJP2-*zXrDxT f=EFg7n#%jUM?F(Gd)z*o*EkSP3%M7E$$QryJ8u>J literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/filetype/size/exec/1.txt b/tests/commoncode/commoncode/data/filetype/size/exec/1.txt new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/tests/commoncode/commoncode/data/filetype/size/exec/1.txt @@ -0,0 +1 @@ +1 diff --git a/tests/commoncode/commoncode/data/filetype/size/exec/a.bat b/tests/commoncode/commoncode/data/filetype/size/exec/a.bat new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/filetype/size/exec/a.bat @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/filetype/size/exec/a.tar b/tests/commoncode/commoncode/data/filetype/size/exec/a.tar new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/filetype/size/exec/a.tar @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/1.txt b/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/1.txt new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/1.txt @@ -0,0 +1 @@ +1 diff --git a/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/a.txt b/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/a.txt new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/a.txt @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/b.txt b/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/b.txt new file mode 100644 index 00000000000..61780798228 --- /dev/null +++ b/tests/commoncode/commoncode/data/filetype/size/exec/subtxt/b.txt @@ -0,0 +1 @@ +b diff --git a/tests/commoncode/commoncode/data/filetype/types.tar b/tests/commoncode/commoncode/data/filetype/types.tar new file mode 100644 index 0000000000000000000000000000000000000000..458dc4476efa3cb3b3507135af450adc925a1bfa GIT binary patch literal 109056 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 E2Mr78)Bpeg literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/fileutils/basename/a/.a/file b/tests/commoncode/commoncode/data/fileutils/basename/a/.a/file new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/basename/a/b/.a b/tests/commoncode/commoncode/data/fileutils/basename/a/b/.a new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/basename/a/b/.a.b b/tests/commoncode/commoncode/data/fileutils/basename/a/b/.a.b new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/basename/a/b/a.tag.gz b/tests/commoncode/commoncode/data/fileutils/basename/a/b/a.tag.gz new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/basename/a/f.a b/tests/commoncode/commoncode/data/fileutils/basename/a/f.a new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/basename/f.a/a.c b/tests/commoncode/commoncode/data/fileutils/basename/f.a/a.c new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/basename/tst b/tests/commoncode/commoncode/data/fileutils/basename/tst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/exec/1.txt b/tests/commoncode/commoncode/data/fileutils/exec/1.txt new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/exec/1.txt @@ -0,0 +1 @@ +1 diff --git a/tests/commoncode/commoncode/data/fileutils/exec/a.bat b/tests/commoncode/commoncode/data/fileutils/exec/a.bat new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/exec/a.bat @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/fileutils/exec/a.tar b/tests/commoncode/commoncode/data/fileutils/exec/a.tar new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/exec/a.tar @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/fileutils/exec/subtxt/1.txt b/tests/commoncode/commoncode/data/fileutils/exec/subtxt/1.txt new file mode 100644 index 00000000000..d00491fd7e5 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/exec/subtxt/1.txt @@ -0,0 +1 @@ +1 diff --git a/tests/commoncode/commoncode/data/fileutils/exec/subtxt/a.txt b/tests/commoncode/commoncode/data/fileutils/exec/subtxt/a.txt new file mode 100644 index 00000000000..78981922613 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/exec/subtxt/a.txt @@ -0,0 +1 @@ +a diff --git a/tests/commoncode/commoncode/data/fileutils/exec/subtxt/b.txt b/tests/commoncode/commoncode/data/fileutils/exec/subtxt/b.txt new file mode 100644 index 00000000000..61780798228 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/exec/subtxt/b.txt @@ -0,0 +1 @@ +b diff --git a/tests/commoncode/commoncode/data/fileutils/executable/deep1/deep2/ctags b/tests/commoncode/commoncode/data/fileutils/executable/deep1/deep2/ctags new file mode 100644 index 0000000000000000000000000000000000000000..6f4414d1cd151895fd90f8771efc52a238f93dc6 GIT binary patch literal 73 zcmb<-^>JflWMqH=CI)5(5YOTL{=F<5CP3De9qeF969xqaP6iDIbp{m%HlPSd&IF + + + + + +

+ + + +
+
+ diff --git a/tests/commoncode/commoncode/data/fileutils/filetype/stage1 b/tests/commoncode/commoncode/data/fileutils/filetype/stage1 new file mode 100644 index 0000000000000000000000000000000000000000..1cd1292ac1fc9a84f37b9fad310dc176e7d4dd74 GIT binary patch literal 512 zcmaFuF@b@6z|8cYfuVsBWB|vnR{=E)42B2#ZuDK)!=UhcfkTbW{}QH-LqYo*YhTzi zemm4q62HZ9_o~pd!bM??ze87*1}t!_X*p2J+We5Q#AS=eK8E_^EKUqSY4fyhAAxd|oP__+QPbW?y^l3kQbReEWK8Usy1_X4^Nd z_Jsk%zS*@ev>0Cha}Nr2QebdT&Cg}{F|!uhrIB?m42&s?WLV)$7ue2=1S>Un?xIx2&tv)pCdWZ{;&Q# x{~`4Dl!xPH=}WMLPyLq*_dmAu{}<>#00000000000002%cLkB(Qr7?|007aKWE%hg literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/fileutils/readwrite/sub/file b/tests/commoncode/commoncode/data/fileutils/readwrite/sub/file new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/textfiles/dos_newlines.txt b/tests/commoncode/commoncode/data/fileutils/textfiles/dos_newlines.txt new file mode 100644 index 00000000000..8e721b4ccf0 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/textfiles/dos_newlines.txt @@ -0,0 +1,28 @@ +package com.somecompany.somepackage; + +/** + * Title: Some Title + * Description: + * Copyright: Copyright (c) 2001 + * Company: Private Company + * @author + * @version + */ + +import com.somecompany.someotherpackage.* ; + +public class SomeClass +{ + private static final String SOME_STATIC = "value"; + + private void usage() + { + System.out.print("usage: java " + new SomeClass().getClass().getName()); + } + + static public void main(String[] args) { + System.exit(0); + } + + +} diff --git a/tests/commoncode/commoncode/data/fileutils/textfiles/mac_newlines.txt b/tests/commoncode/commoncode/data/fileutils/textfiles/mac_newlines.txt new file mode 100644 index 00000000000..999332df3d4 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/textfiles/mac_newlines.txt @@ -0,0 +1 @@ +package com.mycompany.test.sort; /* * MergeSort.java * * Copyright (c) 1998 Sun Microsystems, Inc. All Rights Reserved. * * This software is the confidential and proprietary information of Sun * Microsystems, Inc. ("Confidential Information"). You shall not * disclose such Confidential Information and shall use it only in * accordance with the terms of the license agreement you entered into * with Sun. * * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR ANY DAMAGES * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING * THIS SOFTWARE OR ITS DERIVATIVES. * */ /** * An implementation of MergeSort, needs to be subclassed to * compare the terms. * * @author Scott Violet */ public abstract class MergeSort extends Object { protected Object toSort[]; protected Object swapSpace[]; public void sort(Object array[]) { if(array != null && array.length > 1) { int maxLength; maxLength = array.length; swapSpace = new Object[maxLength]; toSort = array; this.mergeSort(0, maxLength - 1); swapSpace = null; toSort = null; } } public abstract int compareElementsAt(int beginLoc, int endLoc); protected void mergeSort(int begin, int end) { if(begin != end) { int mid; mid = (begin + end) / 2; this.mergeSort(begin, mid); this.mergeSort(mid + 1, end); this.merge(begin, mid, end); } } protected void merge(int begin, int middle, int end) { int firstHalf, secondHalf, count; firstHalf = count = begin; secondHalf = middle + 1; while((firstHalf <= middle) && (secondHalf <= end)) { if(this.compareElementsAt(secondHalf, firstHalf) < 0) swapSpace[count++] = toSort[secondHalf++]; else swapSpace[count++] = toSort[firstHalf++]; } if(firstHalf <= middle) { while(firstHalf <= middle) swapSpace[count++] = toSort[firstHalf++]; } else { while(secondHalf <= end) swapSpace[count++] = toSort[secondHalf++]; } for(count = begin;count <= end;count++) toSort[count] = swapSpace[count]; } } \ No newline at end of file diff --git a/tests/commoncode/commoncode/data/fileutils/textfiles/unix_newlines.txt b/tests/commoncode/commoncode/data/fileutils/textfiles/unix_newlines.txt new file mode 100644 index 00000000000..d3bb4b93fb9 --- /dev/null +++ b/tests/commoncode/commoncode/data/fileutils/textfiles/unix_newlines.txt @@ -0,0 +1,23 @@ +/**************************************************************/ +/* ADDR.C */ +/* Author: John Doe, 7/2000 */ +/* Copyright 1999 Cornell University. All rights reserved. */ +/* Copyright 2000 Jon Doe. All rights reserved. */ +/* See license.txt for further information. */ +/**************************************************************/ + +#include "string.h" +#include "sys.h" + +tst_id tst_put(tst_id *id) { + tst_id id ; + memcpy(&id, *tst_id,sizeof(id)); + return id ; +} + +tst_id tst_get() { + tst_id id ; + memset(&id, 0, sizeof(id)) ; + return id ; +} + diff --git a/tests/commoncode/commoncode/data/fileutils/unicode.zip b/tests/commoncode/commoncode/data/fileutils/unicode.zip new file mode 100644 index 0000000000000000000000000000000000000000..0c73700c9ef69bac5cac4bf7bf19d2a223aed9e1 GIT binary patch literal 1136 zcmWIWW@h1H00D*Ad>b$WN^mg9FqGzHCg-Q5>W7AKGB8`$&-0F}VF)d);AUWCdBM!U z044(9Cj5y(HGvDsghYrLf9mFW8{jm9gMncS(2O1)j(1s%3=AMFf?`H`QR$hEJiUUv zbg+wxfu^+pU4`bN&Hz7mUM?w+w|G50T!MhK01)$VurV+&$~@o|2XZ(IJR*x37`TN% znDNrxx<4Sn64!_l=ltB<)VvY~hEA77srr_Ia33zxehr9)K+nP)tj~ZMeG-e zGN%oKYjoDE(eY7WT-y}Jwx%bd^K5b23YN6@^srnOB3(R9ag*9a@>3pbzg9IEOuM! z;=4q+)a&@H88;b!Fd40S_Qvwskvn`R#3#s}RsO3K_m-iy(SFsly$n1#ZP9x?jsb&- zfx*+&&t;ucLK7(NMSy$`aO!y`-2b)<$P)#|8!uA48R;b#m%)<|mIzyW))5%CJgpD+ zK9Joc-Rcz0zVHi!+<~nDPHI;ZJiTV!yI=PD(A3k%`QPvUoU~dve6eq9t%U3RH=DbU zeGF*7Fgy8#@9gX~tEEpJVLikUs%x;Ly@k6|MM2xgVTEQxX}p7e$&!S2Q`>85=CMqH zGjb;i1fQ^-As#p6LW?UIU0_1lf5MX%g2%-_$ zgB6lJ&=MlDX_%P<*)%64(?D4TXdWo5;4%+uMuC~fz_6sT4aGd!r#z{#pQWcVDHL literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/hash/dir1/a.png b/tests/commoncode/commoncode/data/hash/dir1/a.png new file mode 100644 index 0000000000000000000000000000000000000000..5f212358671a3ada8794cb14fb5227f596447a8c GIT binary patch literal 7859 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 a;o`s?y{X_%+%_?Qe_U6tUQzGBO#2@!$E_9s literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/hash/dir1/a.txt b/tests/commoncode/commoncode/data/hash/dir1/a.txt new file mode 100644 index 00000000000..de980441c3a --- /dev/null +++ b/tests/commoncode/commoncode/data/hash/dir1/a.txt @@ -0,0 +1,3 @@ +a +b +c diff --git a/tests/commoncode/commoncode/data/hash/dir2/a.txt b/tests/commoncode/commoncode/data/hash/dir2/a.txt new file mode 100644 index 00000000000..de980441c3a --- /dev/null +++ b/tests/commoncode/commoncode/data/hash/dir2/a.txt @@ -0,0 +1,3 @@ +a +b +c diff --git a/tests/commoncode/commoncode/data/hash/dir2/dos.txt b/tests/commoncode/commoncode/data/hash/dir2/dos.txt new file mode 100644 index 00000000000..0d2d3a69833 --- /dev/null +++ b/tests/commoncode/commoncode/data/hash/dir2/dos.txt @@ -0,0 +1,3 @@ +dos endlines: +a +b diff --git a/tests/commoncode/commoncode/data/ignore/.scancodeignore b/tests/commoncode/commoncode/data/ignore/.scancodeignore new file mode 100644 index 00000000000..5c0db29f6bc --- /dev/null +++ b/tests/commoncode/commoncode/data/ignore/.scancodeignore @@ -0,0 +1,5 @@ +/foo/* +!/foobar/* + +bar/* +#comment \ No newline at end of file diff --git a/tests/commoncode/commoncode/data/ignore/excludes/.localized b/tests/commoncode/commoncode/data/ignore/excludes/.localized new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/ignore/excludes/eclipse.tgz b/tests/commoncode/commoncode/data/ignore/excludes/eclipse.tgz new file mode 100644 index 0000000000000000000000000000000000000000..eded342698fffbd8022767d7b1f26ca14bc16979 GIT binary patch literal 225 zcmV<703QDziwFR60a{f61MSuOYJ)Ho$8jIUC+KoCiEpuY=;f?fykT5nZ@+YH_eU>a zVG#!34?-kIFyZHkx^3zq=v$VMIXjI~x$_vW>-2*>U8fJ9d+(nV{x`kkzZ%B=pxY_MJ7(rDz7Q!Z7xTBq$NWu-#oQwQOa5Ow zJ*N2xUeDi2N&buY%|GZg)!jZM_!G=M|GhQFS%2#P(w3b6OSsQJ^sRo>%^z;|_5Sz9 b3;hoO00000000000Ki<|K_CjP04M+e15Iz1 literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/ignore/excludes/mac.tgz b/tests/commoncode/commoncode/data/ignore/excludes/mac.tgz new file mode 100644 index 0000000000000000000000000000000000000000..bda64cdeede4c56af50a5cb21308c7818f13b0f7 GIT binary patch literal 409 zcmV;K0cQRmiwFP&{aIB21MSwqPQox42k>fQj1Qpay?|wHH+tYiG~STJap2aJt?10Q zjxh`x-@=P8q%Y!&&opU9TWs>jm!9x)ovDgCp|F zy?#}H1a^4-+Z3MvwOGpE*KsOhnfrHpZH4mryM!^wzYgp2AICC^GL`B*bVB+3Ibrbo zuf^v4vA9=x{wN5xMVJI?iv96JsWmp`&un-3{ZA?99Nz!6I66BZ1;LM2)}RSU-hPB=K-w+|!)^0000000000000000RD{`3`y&Z04M+e DJ^shY literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/data/ignore/excludes/msft-vs.tgz b/tests/commoncode/commoncode/data/ignore/excludes/msft-vs.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1e80d94574bd89efa6e0038c5affabb7dd3fcbfe GIT binary patch literal 163 zcmb2|=3o&18yU*L{Pv5doU8hO=1A3Rm!`sCy=`1uFYS90wA%1#+J&gQN##s?LvnolY5k*R!P1_bKwL%w~WTg_4N=QeNT9*_nNtsni8^y<6 zlg1^K%TO-ml2NK5moWx&e?Q-IX7=~{Z|0o$d7k(CxxAj&>rILMNvYM+z(q-U_5It?vr!Kga99)%-L{21<^+@<8sBQZ0@)SwbR3M((nbB;V1EPu z1tQ(JSX>gmI=<*Srb6up;Wvv@9U;Y5y$MX)TJ|^#ZLch}DOwj4@9g7UM%xO*N}G)U%yZLY${8WrmPu6%HKQv^(bDrYd1Bt$_SN2?nz&gd+L^4L+tN=HdG@nT-sUwMO8W zaSKd;Mbxy1fZ;M;78dUDD4vHLP0i7gX(*-X9e8y4Ph}q~ka$lSLquT|}UAp(->b&V3<_^-origivh5yB;koy(g1jaY18xah057(q@>Ht;? z|GjC7MEz0|GHlUOx@5-3CzG`E>!H<5*-MrZks%C_UJ6RPd$#D@=WdN|rTz|Jh&4No zx8X}Ymm`ml)Q-{BO=mSupM~aKZmL=`Y}x=cd|wBgoLQSPphT6!w7;+kaargEW#hl` zYJBW*9&DXHj(2K^aAx_Z()F~N=KfOZ904$7l&|nDBnEzjmUo&>=q$oeO4kut!i=ww zZzH4_PWf=KDX@5^_}#Tt9b;wZiS zUva#HIx*d{S1gK88V64nn}yb4ZTRpgz67YvYmi50UHjx>zn!|Ee}j)NCClj@+?nz$ z>9XQ)k_7!(C2Zb53^$dClMMiNiAd|!t~)JY886l(V`wAB3pyJ^2ROlTba-1Wak>L$ z&9Oqp$kbHIGvBJFnomf4hC5nZLP5R>&Cp>%*djAbm1?Fj7%r9%B?j73AFW9Wc{qL* zj2vUT+b+8-(yEvNS1UwRtv)hbOh=<a|Rh^u9<&d&WPO;n>HOus4F~Y!;q4W{U>HO3M?1U{f6j z_crdp?Gy#LU+s%0hFrl4u`&pqs9Z)tE+9|8takRCN-!AE=@KuM?Z@T#Ngq zL=(qDNEZ~6r;6{+`u%#90O}j>#d@u9mpZ;!CHf-#(qM&ED>`Y9$3Fj-Zhc~cbl75^F{ww%l zJC3m+(R?2Ka&Jygbk3v3#d=wrl57eKE>&MSUI$%)#S>DBK#{9gAYL_&)k*NB6a4|K zC1?>Z>mLdpX2{ba@LVQz2M+!802CbH_u36ERAJge# z?^=8&b$eK-KDUmEgY##ZwN@TD6S$}U>^D~a9GC2eQ-3`D#eGMz+RJvDjo-_sDdiAe z3Yw5IlK`=sa4z4bV5#rtgWbN|Aio1mM-80)yfTv$>ZxIW9wI$)>v^&2!9JU%^UrM5|NEe?vwg;J{v`WRg;Ws4qD{d!(DV=iqXkJ72bBd-IuEPryzAowU?@D*_(gsJ-U!*FTxh z9_HFMRMxE6x9oA=6No&KBS)&oTnK4{zEap)wMTkocVgI3)ZQGe11g3=XM5dpir2VA z8hce;2R-dtTy+vVp(r0BU9>)=skW8pR`g_rS8`0z)T*W5&YfBP_2(s)zw?*J{jVV% zoK=b3-h_t7Ts+)RL{Svz({tb5*Lz2;O?5F1Ssira?gzUfXC_QGS4l>fZBiO1lHR91t<7jL0@5y5-To3>78+ zE5~sXC=AHg@gBCF*(m}LfQDI*$MTp2a;m8IC}oM!?cL)csO|G zmX)stBnfuM6}9jRID4V>0iqJ_bI<%yg4}?^fKS|c^G9BzL%sVBU882X-kT)sV{=g6QfDthbH@_Wr z?ptx5x;co_wU=JvdNRq`wZpArAcr`U9V&SO+m(sICoeWRY!H2|AGI&@&$zkE%FcJ? zzFfEa`uVT)KW;ac{#i|It6S~e`E{J^y@%ZW>vzgIhpml$v48eMkJw>nYnxmH)l zeCIAVMA#MPDv*$<`nM9I>2?wI>F1r}wWYoDTmb&~U}pV7&Z(wT_bvL(j&_P516hv4 zI60DU0_(JC5x+>Ia#qK&%k@EzJ;L|vG$!B7F_@Q5Hl4~3>^(69kG+BC@3;GzSuR^? z==JA{{RT%jZuVLc{9(%rwQ`2eJzf6(VJ<5pEHPBM+E$Oqk)b!d`vTD|0q%Fu7>F^V`w3uE(wif zK~>Hbo4?Ai9(f*W$K3Vy232;`(mPwql@>&OxT{{dZ|&1r4J^=`!vAYJB5gVZuCydD zZDNJ}GRi_R4@(lz*MD)^f}wT-N(l}%PzO7Eagqy=FU3?{xYd#XsXb6EhiXpHb>>lB zNNS0NllQGH@#(cSv_jqof}Ac4<*{Fkjc9ynCI~3OggxpdotChqzn{7WnS2S61|-Jv z@maNXG}Cwoy|}l*!Q;0jS1eeZC()o8ZwP9B3_e8{%rt9pZ~iODKDU#NwbfpL)V%4) zndC*`!2MwDZxG=8^5rLYr;}bUp~pgjlivq$zDK_7JX_k0ldN%)W*i79WFof%h)Zg^ zbh%91dvFyRlnFa;p?xy6;Lw2b=1M%CPa{FzMu-QMx4ok)n~cCBsm@3RyBNj#>;ELd zX4n#Z<#d0?hr#nf;bnKrNc!89ha5=^w-w1X)a?^qRYE7V|F-fL`l_0w$rM99OQiif zWfXsaHqLJDhmY!u{4XCKZXGp>zwmA7TrCIpV({@d&~Sxlw`Li46mMeJMu{jy5al8x z2?C3Jw;f*~y6!Xvdq4$IHHl$p0s+s1J^!^@mIJ4wd2*eUE@hsReE+g~L@>I+!-}0| z(@mWAwyJ_lz6B3)uB3gnnYn~+h3X^$a%nd zPm*(588c|eWw+!QYioqI^urU_rcAizpx`3d{4e4JN_`C3_-`^3E!8hv|Fog|-Mx)d zTXZRSH4jl{2h?DvMvI@9^`P;}%8CI*E5x!|2sMP5r+UX>h5xZUyk5vYikxN;HmzmY zMbSM(>nG+Ffl|E!sQ+|0VDsNIY9-BOwFJx)4|k6-H|p#jVIH{ya(X-ixM}TX_?|HbDZWyLx--2C}boL^Oze)Y*J8?7{APC zld31A2I@R|Q!Of@L|Dp4VqO#99YT~@czCUtzFxXsYNDigvO5e<1g#-l{!s%|pWrr+ zQwY+ju+hHZF_&%wZew&(Xy*!X#;gRJ5B_T9|H*`<;S{I?wF&L=@PkY zC7fq(6qtQ$_Y)WLFaoHEb(Ve5e?RwTE+ZoBSgYwOaXTSVE4vih2Y z&24IN%@Dbg;Kk$&?VZ3M&#YJ$2Y=UyC~}!5#LoqPd*0!eoqaL+`IKAJx0AbG0d)^t zBN1gQ;wHwr1rO$n(1A&8)hPTtjmTsR6tjcf{AQKjHJQ0{j5108OK}*uem1E5#o=J% zip|+5!JEKCGQ_?l>Ov|9G=AW>NN6jA5CiwpR7ue0Ui>Pe|3vs{ma%(F!@K;fnl7=d zY9+brq*>&rK?Dc}TH^ZJL=FY?|LDt^sVzhXneYEIp9C}cvSa)a*2t@-4PyFQ#F@^) z?a3_k!2Ltl(mK1zEa*Zj={gnGj8sqe+c`!z?SZ8V7jBH_-@#^>nA>vlkS>KG_HADR zaI5}77)d8CO#-i!Ye!!HIK)n3a(O9bV>v!%!pTU14C}yexeh!Bx9EpRU8PRQ=yq7lbAXJ<}Xa*KB28Nkq{uqB|com{h?2dY5n+#?q>y{m5VBDN>+1I}oLme|WaXydNPywy+HqX4sTN`6 zl9!`!aY=+)Kh7fil*p+v;HgjwM0>Tg+!H#Wx@bmmO^y!33(=_ zI^KqbE)bjMFExp(kS0WB32~3-(z?H;x$9U80p*0?UU8c2Y3J~zR3>phw$gMAZ6tB# z1`4ohmh NXz-b#v_o0x{{V}Dqm%#u literal 0 HcmV?d00001 diff --git a/tests/commoncode/commoncode/test_codec.py b/tests/commoncode/commoncode/test_codec.py new file mode 100644 index 00000000000..eaac2599f4f --- /dev/null +++ b/tests/commoncode/commoncode/test_codec.py @@ -0,0 +1,125 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +from unittest import TestCase + +from commoncode.codec import bin_to_num, num_to_bin +from commoncode.codec import _encode, to_base_n +from commoncode.codec import to_base10, to_base85 + + +class TestCodec(TestCase): + def test_bin_to_num_basic(self): + expected = 123 + result = bin_to_num('{') + assert expected == result + + def test_bin_to_num_null(self): + expected = 0 + result = bin_to_num('\x00') + assert expected == result + + def test_bin_to_num_large_number(self): + expected = 432346237462348763 + result = bin_to_num('\x06\x00\x00\x9c\xbf\xeb\x83\xdb') + assert expected == result + + def test_bin_to_num_and_num_to_bin_is_idempotent(self): + expected = 432346237462348763 + result = bin_to_num(num_to_bin(432346237462348763)) + assert expected == result + + def test_num_to_bin_basic(self): + expected = '{' + result = num_to_bin(123) + assert expected == result + + def test_num_to_bin_null(self): + expected = '' + result = num_to_bin(0) + assert expected == result + + def test_num_to_bin_large_number(self): + expected = '\x06\x00\x00\x9c\xbf\xeb\x83\xdb' + result = num_to_bin(432346237462348763) + assert expected == result + + def test_num_to_bin_bin_to_num_is_idempotent(self): + expected = '\x06\x00\x00\x9c\xbf\xeb\x83\xdb' + result = num_to_bin(bin_to_num('\x06\x00\x00\x9c\xbf\xeb\x83\xdb')) + assert expected == result + + def test_encode_zero(self): + assert '' == _encode(0) + + def test_encode_basic(self): + assert 'HKq1w7M=' == _encode(123123123123) + + def test_encode_limit_8bits_255(self): + assert '_w==' == _encode(255) + + def test_encode_limit_8bits_256(self): + assert 'AQA=' == _encode(256) + + def test_encode_adds_no_padding_for_number_that_are_multiple_of_6_bits(self): + assert '____________' == _encode(0xFFFFFFFFFFFFFFFFFF) + assert 8 == len(_encode(0xFFFFFFFFFFFF)) + + def test_encode_very_large_number(self): + b64 = ('QAAAAAAgAAAAAQAACAAAAAAAAAAAAAAkAAIAAAAAAAAAAAAAAACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAiAAAAAAAIAAAAAAAAAAAAAAEAACAAAAAAAA=') + expected = b64 + num = 2678771517966886466622496485850735537232223496190189203248435106535830319026141316924949516664780383591425235756710588949364368366679435700855700642969357960349427980681242720502045830438444033569999428606714388704082526548154984676817460705606960919023941301616034362869262429593297635158449513824256L + result = _encode(num) + assert expected == result + + def test_base64_is_idempotent(self): + for i in [0, 63, 782963129, 99999999, 2147483647]: + assert i == to_base10(to_base_n(i, 64), 64) + + def test_base36_is_idempotent(self): + for i in [0, 63, 782963129, 99999999, 2147483647]: + assert i == to_base10(to_base_n(i, 36), 36) + + def test_base85_is_idempotent(self): + # we use this for 12-bit hashes + for i in [0, 63, 100, 1000, 4095, 4294967295]: + assert i == to_base10(to_base85(i), 85) + + def test_to_base_n_with_unknown_base_raise_exception(self): + try: + to_base_n(892103712, 86) + except AssertionError: + pass + + try: + to_base_n(892103712, 16522) + except AssertionError: + pass + + try: + to_base_n(892103712, 1) + except AssertionError: + pass diff --git a/tests/commoncode/commoncode/test_command.py b/tests/commoncode/commoncode/test_command.py new file mode 100644 index 00000000000..9c479c26068 --- /dev/null +++ b/tests/commoncode/commoncode/test_command.py @@ -0,0 +1,236 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os + +from commoncode import command +from commoncode import fileutils +from commoncode.testcase import FileBasedTesting + + +class TestCommand(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + # tuples of supported osarch, osnoarch, noarch + os_arches_test_matrix = [ + ('linux-32', 'linux-noarch', 'noarch'), + ('linux-64', 'linux-noarch', 'noarch'), + ('mac-32', 'mac-noarch', 'noarch'), + ('mac-64', 'mac-noarch', 'noarch'), + ('win-32', 'win-noarch', 'noarch'), + ('win-64', 'win-noarch', 'noarch'), + ] + + # os_arch -> (bin_dir, lib_dir, (bin_dir files,) (lib_dir files,) ,) + os_arches_files_test_matrix = { + 'linux-32': ( + 'command/bin/linux-32/bin', + 'command/bin/linux-32/lib', + ('cmd'), + ('libmagic32.so'), + ), + + 'linux-64': ( + 'command/bin/linux-64/bin', + 'command/bin/linux-64/lib', + ('cmd'), + ('libmagic64.so'), + ), + + 'linux-noarch': ( + 'command/bin/linux-noarch/bin', + 'command/bin/linux-noarch/bin', + ('cmd'), + (), + ), + + 'mac-32': ( + 'command/bin/mac-32/bin', + 'command/bin/mac-32/lib', + ('cmd'), + ('libmagic.dylib'), + ), + + 'mac-64': ( + 'command/bin/mac-64/bin', + 'command/bin/mac-64/lib', + ('cmd'), + ('libmagic.dylib'), + ), + + 'mac-noarch': ( + 'command/bin/mac-noarch/bin', + 'command/bin/mac-noarch/bin', + ('cmd'), + (), + ), + + 'win-32': ( + 'command/bin/win-32/bin', + 'command/bin/win-32/bin', + ('cmd.exe', + 'magic1.dll'), + ('cmd.exe', + 'magic1.dll'), + ), + + 'win-64': ( + 'command/bin/win-64/bin', + 'command/bin/win-64/bin', + ('cmd.exe', + 'magic1.dll'), + ('cmd.exe', + 'magic1.dll'), + ), + + 'win-noarch': ( + 'command/bin/win-noarch/bin', + 'command/bin/win-noarch/bin', + ('cmd.exe', + 'some.dll'), + ('cmd.exe', + 'some.dll'), + ), + + 'noarch': ( + 'command/bin/noarch/bin', + 'command/bin/noarch/lib', + ('cmd'), + ('l'), + ), + + 'junk': (None, None, (), (),), + } + + os_arches_locations_test_matrix = [ + ('linux-32', 'linux-noarch', 'noarch'), + ('linux-64', 'linux-noarch', 'noarch'), + ('linux-32', 'linux-noarch', None), + ('linux-64', 'linux-noarch', None), + ('linux-32', None, None), + ('linux-64', None, None), + (None, 'linux-noarch', 'noarch'), + (None, 'linux-noarch', None), + + ('mac-32', 'mac-noarch', 'noarch'), + ('mac-64', 'mac-noarch', 'noarch'), + ('mac-32', 'mac-noarch', None), + ('mac-64', 'mac-noarch', None), + ('mac-32', None, None), + ('mac-64', None, None), + (None, 'mac-noarch', 'noarch'), + (None, 'mac-noarch', None), + + ('win-32', 'win-noarch', 'noarch'), + ('win-64', 'win-noarch', 'noarch'), + ('win-32', 'win-noarch', None), + ('win-64', 'win-noarch', None), + ('win-32', None, None), + ('win-64', None, None), + (None, 'win-noarch', 'noarch'), + (None, 'win-noarch', None), + + (None, None, 'noarch'), + ] + + def test_execute_non_ascii_output(self): + # Popen returns a *binary* string with non-ascii chars: skips these + rc, stdout, stderr = command.execute( + 'python', ['-c', "print 'non ascii: \\xe4 just passed it !'"] + ) + assert rc == 0 + assert stderr == '' + + # converting to Unicode could cause an "ordinal not in range..." + # exception + assert stdout == 'non ascii: just passed it !' + unicode(stdout) + + def test_os_arch_dir(self): + root_dir = self.get_test_loc('command/bin', copy=True) + for _os_arch, _os_noarch, _noarch in self.os_arches_test_matrix: + assert command.os_arch_dir(root_dir, _os_arch).endswith(_os_arch) + assert command.os_noarch_dir(root_dir, _os_noarch).endswith(_os_noarch) + assert command.noarch_dir(root_dir, _noarch).endswith(_noarch) + + def test_get_base_dirs(self): + root_dir = self.get_test_loc('command/bin', copy=True) + for _os_arch, _os_noarch, _noarch in self.os_arches_test_matrix: + bds = command.get_base_dirs(root_dir, _os_arch, _os_noarch, _noarch) + assert bds + for bd in bds: + assert os.path.exists(bd) + + def test_get_bin_lib_dirs(self): + root_dir = self.get_test_loc('command/bin', copy=True) + for os_arch, paths in self.os_arches_files_test_matrix.items(): + base_dir = os.path.join(root_dir, os_arch) + + bin_dir, lib_dir = command.get_bin_lib_dirs(base_dir) + expected_bin, expected_lib, expected_bin_files, expected_lib_files = paths + + def norm(p): + return os.path.abspath(os.path.normpath(p)) + + if expected_bin: + assert os.path.exists(bin_dir) + assert os.path.isdir(bin_dir) + pbd = fileutils.as_posixpath(bin_dir) + assert pbd.endswith(expected_bin.replace('command/', '')) + if expected_bin_files: + assert all(f in expected_bin_files for f in os.listdir(bin_dir)) == True + else: + assert expected_bin == bin_dir + + if expected_lib: + assert os.path.exists(lib_dir) + assert os.path.isdir(lib_dir) + pld = fileutils.as_posixpath(lib_dir) + assert pld.endswith(expected_lib.replace('command/', '')) + if expected_lib_files: + assert all(f in expected_lib_files for f in os.listdir(lib_dir)) == True + else: + assert expected_lib == lib_dir + + def test_get_locations_missing(self): + assert command.get_locations('ctags', None) == (None, None, None) + assert command.get_locations('dir', None) == (None, None, None) + assert command.get_locations('ctags', '.') == (None, None, None) + + def test_get_locations(self): + root_dir = self.get_test_loc('command/bin', copy=True) + cmd = 'cmd' + for test_matrix in self.os_arches_locations_test_matrix: + _os_arch, _os_noarch, _noarch = test_matrix + cmd_loc, _ , _ = command.get_locations(cmd, root_dir, _os_arch, _os_noarch, _noarch) + extension = '' + if any(x and 'win' in x for x in (_os_arch, _os_noarch, _noarch)): + extension = '.exe' + expected_cmd = cmd + extension + if cmd_loc: + assert cmd_loc.endswith(expected_cmd) + assert os.path.exists(cmd_loc) + assert os.path.isfile(cmd_loc) diff --git a/tests/commoncode/commoncode/test_date.py b/tests/commoncode/commoncode/test_date.py new file mode 100644 index 00000000000..02bbf0924a9 --- /dev/null +++ b/tests/commoncode/commoncode/test_date.py @@ -0,0 +1,68 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os +from datetime import datetime + +from commoncode import testcase + +import commoncode.date + + +class TestDate(testcase.FileBasedTesting): + def test_secs_from_epoch_can_handle_micro_and_nano_secs(self): + test_file = self.get_temp_file() + open(test_file, 'w').close() + # setting modified time to desired values + os.utime(test_file, (1301420665.046481, 1301420665.046481)) + # otherwise the issue does not happen (ie. on mac) + if 1301420665.0 < os.stat(test_file).st_mtime: + file_date = commoncode.date.get_file_mtime(test_file) + commoncode.date.secs_from_epoch(file_date) + + def test_get_file_time1(self): + test_file = self.get_temp_file() + open(test_file, 'w').close() + now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + result = commoncode.date.get_file_mtime(test_file)[:10] + self.assertEqual(now[:10], result) + + def test_get_file_time2(self): + test_file = self.get_temp_file() + open(test_file, 'w').close() + expected = u'1992-05-09 00:00:00' + m_ts = (24 * 3600) * 134 + (24 * 3600 * 365) * 22 + # setting modified time to expected values + os.utime(test_file, (m_ts, m_ts)) + self.assertEqual(expected, commoncode.date.get_file_mtime(test_file)) + + def test_get_file_time3(self): + test_file = self.get_temp_file() + open(test_file, 'w').close() + # setting modified time to expected values + expected = u'2011-01-06 14:35:00' + os.utime(test_file, (1294324500, 1294324500)) + self.assertEqual(expected, commoncode.date.get_file_mtime(test_file)) diff --git a/tests/commoncode/commoncode/test_fileset.py b/tests/commoncode/commoncode/test_fileset.py new file mode 100644 index 00000000000..e12f2f29543 --- /dev/null +++ b/tests/commoncode/commoncode/test_fileset.py @@ -0,0 +1,98 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os + +import commoncode.testcase +from commoncode import fileset + +class FilesetTest(commoncode.testcase.FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_load(self): + irf = self.get_test_loc('fileset/scancodeignore.lst') + result = fileset.load(irf) + assert ['/foo/*', '!/foobar/*', 'bar/*', '#comment'] == result + + def test_match_basic(self): + assert not fileset.match('/common/src/', {}, {}) + assert not fileset.match('/common/src/', None, None) + assert not fileset.match(None, None, None) + + def test_in_fileset(self): + incs = {'/common/src/*': '.scanignore'} + excs = {'/common/src/*.so':'.scanignore'} + assert not fileset.match(None, incs, excs) + assert not fileset.match('', incs, excs) + assert not fileset.match('/', incs, excs) + assert fileset.match('/common/src/', incs, excs) + assert not fileset.match('/common/bin/', incs, excs) + + def test_in_fileset_2(self): + incs = {'src*': '.scanignore'} + excs = {'src/ab': '.scanignore'} + assert not fileset.match(None, incs, excs) + assert not fileset.match('', incs, excs) + assert not fileset.match('/', incs, excs) + assert fileset.match('/common/src/', incs, excs) + assert not fileset.match('src/ab', incs, excs) + assert fileset.match('src/abbab', incs, excs) + + def test_match_exclusions(self): + incs = {'/src/*': '.scanignore'} + excs = {'/src/*.so':'.scanignore'} + assert not fileset.match('/src/dist/build/mylib.so', incs, excs) + + def test_match_exclusions_2(self): + incs = {'src': '.scanignore'} + excs = {'src/*.so':'.scanignore'} + assert fileset.match('/some/src/this/that', incs, excs) + assert not fileset.match('/src/dist/build/mylib.so', incs, excs) + + def test_match_empty_exclusions(self): + incs = {'/src/*': '.scanignore'} + excs = {'': '.scanignore'} + assert fileset.match('/src/dist/build/mylib.so', incs, excs) + + def test_match_sources(self): + incs = {'/home/elf/elf-0.5/*': '.scanignore'} + excs = {'/home/elf/elf-0.5/src/elf': '.scanignore', + '/home/elf/elf-0.5/src/elf.o': '.scanignore'} + assert not fileset.match('/home/elf/elf-0.5/src/elf', incs, excs) + + def test_match_dot_svn(self): + incs = {'*/.svn/*': '.scanignore'} + excs = {} + assert fileset.match('home/common/tools/elf/.svn/', incs, excs) + assert fileset.match('home/common/tools/.svn/this', incs, excs) + assert not fileset.match('home/common/tools/this', incs, excs) + + def test_match_dot_svn_with_excludes(self): + incs = {'*/.svn/*': '.scanignore'} + excs = {'*/.git/*': '.scanignore'} + assert fileset.match('home/common/tools/elf/.svn/', incs, excs) + assert fileset.match('home/common/tools/.svn/this', incs, excs) + assert not fileset.match('home/common/.git/this', incs, excs) diff --git a/tests/commoncode/commoncode/test_filetype.py b/tests/commoncode/commoncode/test_filetype.py new file mode 100644 index 00000000000..8c52addd1bd --- /dev/null +++ b/tests/commoncode/commoncode/test_filetype.py @@ -0,0 +1,201 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os + +import commoncode.testcase +from commoncode.testcase import make_non_readable +from commoncode.testcase import make_non_writable + +from commoncode import filetype +from commoncode.system import on_posix +from commoncode import fileutils +from commoncode.testcase import FileBasedTesting +from os.path import join +from os.path import exists + + +class TypeTest(commoncode.testcase.FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_get_size_on_file(self): + test_file = self.get_test_loc('filetype/size/Image1.eps') + assert 12388 == filetype.get_size(test_file) + + def test_get_size_on_directory(self): + test_dir = self.get_test_loc('filetype/size', copy=True) + assert 12400 == filetype.get_size(test_dir) + + def test_get_type(self): + test_dir = self.extract_test_tar('filetype/types.tar', verbatim=True) + results = [] + for root, dirs, files in os.walk(test_dir): + for d in dirs: + results.append((d, filetype.get_type(os.path.join(root, d)))) + for f in files: + results.append((f, filetype.get_type(os.path.join(root, f)))) + + expected = [ + ('5-DIRTYPE', 'd'), + ('0-REGTYPE', 'f'), + ('0-REGTYPE-TEXT', 'f'), + ('0-REGTYPE-VEEEERY_LONG_NAME___________________________________' + '______________________________________________________________' + '____________________155', 'f'), + ('1-LNKTYPE', 'f'), + ('S-SPARSE', 'f'), + ('S-SPARSE-WITH-NULLS', 'f') + ] + + # symlinks and special files are not supported on win + if on_posix: + expected += [ ('2-SYMTYPE', 'l'), ('6-FIFOTYPE', 's'), ] + + assert sorted(expected) == sorted(results) + + def test_is_rwx_with_none(self): + assert not filetype.is_writable(None) + assert not filetype.is_readable(None) + assert not filetype.is_executable(None) + + def test_is_readable_is_writeable_file(self): + base_dir = self.get_test_loc('filetype/readwrite', copy=True) + test_file = os.path.join(os.path.join(base_dir, 'sub'), 'file') + + try: + assert filetype.is_readable(test_file) + assert filetype.is_writable(test_file) + + make_non_readable(test_file) + if on_posix: + assert not filetype.is_readable(test_file) + + make_non_writable(test_file) + assert not filetype.is_writable(test_file) + finally: + fileutils.chmod(base_dir, fileutils.RW) + + def test_is_readable_is_writeable_dir(self): + base_dir = self.get_test_loc('filetype/readwrite', copy=True) + test_dir = os.path.join(base_dir, 'sub') + + try: + assert filetype.is_readable(test_dir) + assert filetype.is_writable(test_dir) + + make_non_readable(test_dir) + if on_posix: + assert not filetype.is_readable(test_dir) + else: + # dirs are always RW on windows + assert filetype.is_readable(test_dir) + make_non_writable(test_dir) + if on_posix: + assert not filetype.is_writable(test_dir) + else: + # dirs are always RW on windows + assert filetype.is_writable(test_dir) + # finally + finally: + fileutils.chmod(base_dir, fileutils.RW) + + +class CountTest(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def get_test_count_dir(self): + test_dir = self.get_test_loc('count/filecount', copy=True) + sub3 = join(test_dir, 'dir', 'sub3') + if not exists(sub3): + os.makedirs(sub3) + return test_dir + + def test_get_file_count_with_empty_dir(self): + test_dir = self.get_temp_dir() + assert 0 == filetype.get_file_count(test_dir) + + def test_get_file_count_with_single_file(self): + test_file = self.get_temp_file() + with open(test_file, 'wb') as f: + f.write('') + assert filetype.is_file(test_file) + assert 1 == filetype.get_file_count(test_file) + + def test_get_file_count_with_empty_folders(self): + test_dir = self.get_test_count_dir() + result = filetype.get_file_count(test_dir) + assert 9 == result + + def test_get_file_size_and_count(self): + test_dir = self.get_test_count_dir() + result = filetype.get_size(test_dir) + assert 18 == result + + def test_get_file_size(self): + test_dir = self.get_test_count_dir() + tests = ( + ('dir/a.txt', 2), + ('dir/b.txt', 2), + ('dir/c.txt', 2), + ('dir/sub1/a.txt', 2), + ('dir/sub1/b.txt', 2), + ('dir/sub1/c.txt', 2), + ('dir/sub1/subsub/a.txt', 2), + ('dir/sub1/subsub/b.txt', 2), + ('dir/sub1/subsub', 4), + ('dir/sub1', 10), + ('dir/sub2/a.txt', 2), + ('dir/sub2', 2), + ('dir/sub3', 0), + ('dir/', 18), + ('', 18), + ) + for test_file, size in tests: + result = filetype.get_size(os.path.join(test_dir, test_file)) + assert size == result + + def test_get_file_count(self): + test_dir = self.get_test_count_dir() + tests = ( + ('dir/a.txt', 1), + ('dir/b.txt', 1), + ('dir/c.txt', 1), + ('dir/sub1/a.txt', 1), + ('dir/sub1/b.txt', 1), + ('dir/sub1/c.txt', 1), + ('dir/sub1/subsub/a.txt', 1), + ('dir/sub1/subsub/b.txt', 1), + ('dir/sub1/subsub', 2), + ('dir/sub1', 5), + ('dir/sub2/a.txt', 1), + ('dir/sub2', 1), + ('dir/sub3', 0), + ('dir/', 9), + ('', 9), + ) + for test_file, count in tests: + result = filetype.get_file_count(os.path.join(test_dir, test_file)) + assert count == result diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py new file mode 100644 index 00000000000..af26e25b368 --- /dev/null +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -0,0 +1,403 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os +from os.path import join +from os.path import sep +from unittest.case import skipIf + +from commoncode.system import on_windows +from commoncode.system import on_posix +from commoncode.testcase import FileBasedTesting +from commoncode.testcase import make_non_readable +from commoncode.testcase import make_non_writable +from commoncode.testcase import make_non_executable + +from commoncode import filetype +from commoncode import fileutils + + +class TestPermissions(FileBasedTesting): + """ + Several assertions or test are skipped on non posix OSes. + Windows handles permissions and special files differently. + """ + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_chmod_on_non_existing_file_throws_no_exception(self): + fileutils.chmod('some non existing dir', fileutils.RWX) + + def test_chmod_read_write_recursively_on_dir(self): + test_dir = self.get_test_loc('fileutils/executable', copy=True) + test_file = join(test_dir, 'deep1', 'deep2', 'ctags') + test_dir2 = join(test_dir, 'deep1', 'deep2') + parent = join(test_dir, 'deep1') + + try: + make_non_writable(test_file) + assert not filetype.is_writable(test_file) + if on_posix: + make_non_executable(test_file) + assert not filetype.is_executable(test_file) + + if on_posix: + make_non_executable(test_dir2) + assert not filetype.is_executable(test_dir2) + make_non_writable(test_dir) + if on_posix: + assert not filetype.is_writable(test_dir2) + + fileutils.chmod(parent, fileutils.RW, recurse=True) + + assert filetype.is_readable(test_dir2) is True + assert filetype.is_writable(test_dir2) + if on_posix: + assert filetype.is_executable(test_dir2) + finally: + fileutils.chmod(test_dir, fileutils.RW) + + def test_chmod_read_write_non_recursively_on_dir(self): + test_dir = self.get_test_loc('fileutils/executable', copy=True) + test_file = join(test_dir, 'deep1', 'deep2', 'ctags') + test_dir = join(test_dir, 'deep1', 'deep2') + parent = join(test_dir, 'deep1') + + try: + # setup + make_non_writable(test_file) + assert not filetype.is_writable(test_file) + + make_non_writable(test_dir) + if on_posix: + assert not filetype.is_writable(test_dir) + else: + # windows is different + assert filetype.is_writable(test_dir) + + fileutils.chmod(parent, fileutils.RW, recurse=False) + # test: the perms should be the same + assert not filetype.is_writable(test_file) + + if on_posix: + assert not filetype.is_writable(test_dir) + else: + # windows is different + assert filetype.is_writable(test_dir) + finally: + fileutils.chmod(test_dir, fileutils.RW) + + def test_chmod_read_write_file(self): + test_dir = self.get_test_loc('fileutils/executable', copy=True) + test_file = join(test_dir, 'deep1', 'deep2', 'ctags') + + try: + make_non_writable(test_file) + assert not filetype.is_writable(test_file) + + fileutils.chmod(test_file, fileutils.RW) + assert filetype.is_readable(test_file) + assert filetype.is_writable(test_file) + finally: + fileutils.chmod(test_dir, fileutils.RW) + + def test_chmod_read_write_exec_dir(self): + test_dir = self.get_test_loc('fileutils/executable', copy=True) + test_file = join(test_dir, 'deep1', 'deep2', 'ctags') + + try: + if on_posix: + make_non_executable(test_dir) + assert not filetype.is_executable(test_file) + make_non_writable(test_dir) + + fileutils.chmod(test_dir, fileutils.RWX) + assert filetype.is_readable(test_file) + assert filetype.is_writable(test_file) + if on_posix: + assert filetype.is_executable(test_file) + finally: + fileutils.chmod(test_dir, fileutils.RW) + + def test_copyfile_does_not_keep_permissions(self): + src_file = self.get_temp_file() + dest = self.get_temp_dir() + with open(src_file, 'wb') as f: + f.write('') + try: + make_non_readable(src_file) + if on_posix: + assert not filetype.is_readable(src_file) + + fileutils.copyfile(src_file, dest) + dest_file = join(dest, os.listdir(dest)[0]) + assert filetype.is_readable(dest_file) + finally: + fileutils.chmod(src_file, fileutils.RW) + fileutils.chmod(dest, fileutils.RW) + + def test_copytree_does_not_keep_non_writable_permissions(self): + src = self.get_test_loc('fileutils/exec', copy=True) + dst = self.get_temp_dir() + + try: + src_file = join(src, 'subtxt/a.txt') + make_non_writable(src_file) + assert not filetype.is_writable(src_file) + + src_dir = join(src, 'subtxt') + make_non_writable(src_dir) + if on_posix: + assert not filetype.is_writable(src_dir) + + # copy proper + dest_dir = join(dst, 'dest') + fileutils.copytree(src, dest_dir) + + dst_file = join(dest_dir, 'subtxt/a.txt') + assert os.path.exists(dst_file) + assert filetype.is_writable(dst_file) + + dest_dir2 = join(dest_dir, 'subtxt') + assert os.path.exists(dest_dir2) + assert filetype.is_writable(dest_dir) + finally: + fileutils.chmod(src, fileutils.RW) + fileutils.chmod(dst, fileutils.RW) + + def test_copytree_copies_unreadable_files(self): + src = self.get_test_loc('fileutils/exec', copy=True) + dst = self.get_temp_dir() + src_file1 = join(src, 'a.bat') + src_file2 = join(src, 'subtxt', 'a.txt') + + try: + # make some unreadable source files + make_non_readable(src_file1) + if on_posix: + assert not filetype.is_readable(src_file1) + + make_non_readable(src_file2) + if on_posix: + assert not filetype.is_readable(src_file2) + + # copy proper + dest_dir = join(dst, 'dest') + fileutils.copytree(src, dest_dir) + + dest_file1 = join(dest_dir, 'a.bat') + assert os.path.exists(dest_file1) + assert filetype.is_readable(dest_file1) + + dest_file2 = join(dest_dir, 'subtxt', 'a.txt') + assert os.path.exists(dest_file2) + assert filetype.is_readable(dest_file2) + + finally: + fileutils.chmod(src, fileutils.RW) + fileutils.chmod(dst, fileutils.RW) + + def test_delete_unwritable_directory_and_files(self): + base_dir = self.get_test_loc('fileutils/readwrite', copy=True) + test_dir = join(base_dir, 'sub') + test_file = join(test_dir, 'file') + + try: + # note: there are no unread/writable dir on windows + make_non_readable(test_file) + make_non_executable(test_file) + make_non_writable(test_file) + + make_non_readable(test_dir) + make_non_executable(test_dir) + make_non_writable(test_dir) + + fileutils.delete(test_dir) + assert not os.path.exists(test_dir) + finally: + fileutils.chmod(base_dir, fileutils.RW) + + +class TestFileUtils(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + @skipIf(on_windows, 'Windows handles special files differently.') + def test_copytree_does_not_copy_fifo(self): + # Windows does not support pipes + src = self.get_test_loc('fileutils/filetype', copy=True) + dest = self.get_temp_dir() + src_file = join(src, 'myfifo') + os.mkfifo(src_file) # @UndefinedVariable + dest_dir = join(dest, 'dest') + fileutils.copytree(src, dest_dir) + assert not os.path.exists(join(dest_dir, 'myfifo')) + + def test_copyfile_keeps_modified_date(self): + test_file = self.get_test_loc('fileutils/exec/subtxt/a.txt', copy=True) + dest = self.get_temp_file() + expected = 1289918700 + os.utime(test_file, (expected, expected)) + fileutils.copyfile(test_file, dest) + result = os.stat(dest).st_mtime + assert expected == result + + def test_copyfile_can_copy_file_to_dir_keeping_full_file_name(self): + test_file = self.get_test_loc('fileutils/exec/subtxt/a.txt', copy=True) + dest = self.get_temp_dir() + expected = os.path.join(dest, 'a.txt') + fileutils.copyfile(test_file, dest) + assert os.path.exists(expected) + + def test_read_text_file_with_posix_LF_line_endings(self): + test_file = self.get_test_loc('fileutils/textfiles/unix_newlines.txt') + result = fileutils.read_text_file(test_file)[:172] + expected = ( + '/**************************************************************/\n' + '/* ADDR.C */\n/* Author: John Doe, 7/2000 */\n' + '/* Copyright 1999 Cornell University. All rights reserved. */\n') + assert expected == result + + def test_read_text_file_with_dos_CRLF_line_endings(self): + test_file = self.get_test_loc('fileutils/textfiles/dos_newlines.txt') + result = fileutils.read_text_file(test_file)[:70] + expected = ('package com.somecompany.somepackage;\n' + '\n/**\n * Title: Some Title\n') + assert expected == result + + def test_read_text_file_with_mac_CR_lines_endings(self): + test_file = self.get_test_loc('fileutils/textfiles/mac_newlines.txt') + result = fileutils.read_text_file(test_file)[:55] + expected = ('package com.mycompany.test.sort;\n\n/*\n' + ' * MergeSort.java\n') + assert expected == result + + def test_resource_name(self): + assert 'f' == fileutils.resource_name('/a/b/d/f/f') + assert 'f' == fileutils.resource_name('/a/b/d/f/f/') + assert 'f' == fileutils.resource_name('a/b/d/f/f/') + assert 'f.a' == fileutils.resource_name('/a/b/d/f/f.a') + assert 'f.a' == fileutils.resource_name('/a/b/d/f/f.a/') + assert 'f.a' == fileutils.resource_name('a/b/d/f/f.a') + assert 'f.a' == fileutils.resource_name('f.a') + + def test_os_walk_with_unicode_path(self): + test_dir = self.extract_test_zip('fileutils/unicode.zip') + test_dir = join(test_dir, 'unicode') + + test_dir = unicode(test_dir) + result = list(os.walk(test_dir)) + expected = [ + (unicode(test_dir), ['a'], [u'2.csv']), + (unicode(test_dir) + sep + 'a', [], [u'gru\u0308n.png']) + ] + assert expected == result + + +class TestName(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_file_base_name_on_path_and_location(self): + test_dir = self.get_test_loc('fileutils/basename', copy=True) + tests = [ + ('a/.a/file', 'file'), + ('a/.a/', '.a'), + ('a/b/.a.b', '.a'), + ('a/b/a.tag.gz', 'a.tag'), + ('a/b/', 'b'), + ('a/f.a', 'f'), + ('a/', 'a'), + ('f.a/a.c', 'a'), + ('f.a/', 'f.a'), + ('tst', 'tst'), + ] + for test_file, name in tests: + result = fileutils.file_base_name(test_file) + assert name == result + # also test on location + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert name == result + + def test_file_name_on_path_and_location(self): + test_dir = self.get_test_loc('fileutils/basename', copy=True) + tests = [ + ('a/.a/file', 'file'), + ('a/.a/', '.a'), + ('a/b/.a.b', '.a.b'), + ('a/b/a.tag.gz', 'a.tag.gz'), + ('a/b/', 'b'), + ('a/f.a', 'f.a'), + ('a/', 'a'), + ('f.a/a.c', 'a.c'), + ('f.a/', 'f.a'), + ('tst', 'tst'), + ] + for test_file, name in tests: + result = fileutils.file_name(test_file) + assert name == result + # also test on location + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert name == result + + def test_file_extension_on_path_and_location(self): + test_dir = self.get_test_loc('fileutils/basename', copy=True) + tests = [ + ('a/.a/file', ''), + ('a/.a/', ''), + ('a/b/.a.b', '.b'), + ('a/b/a.tag.gz', '.gz'), + ('a/b/', ''), + ('a/f.a', '.a'), + ('a/', ''), + ('f.a/a.c', '.c'), + ('f.a/', ''), + ('tst', ''), + ] + for test_file, name in tests: + result = fileutils.file_extension(test_file) + assert name == result + # also test on location + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert name == result + + def test_parent_directory_on_path_and_location(self): + test_dir = self.get_test_loc('fileutils/basename', copy=True) + tests = [ + ('a/.a/file', 'a/.a/'), + ('a/.a/', 'a/'), + ('a/b/.a.b', 'a/b/'), + ('a/b/a.tag.gz', 'a/b/'), + ('a/b/', 'a/'), + ('a/f.a', 'a/'), + ('a/', '/'), + ('f.a/a.c', 'f.a/'), + ('f.a/', '/'), + ('tst', '/'), + ] + for test_file, name in tests: + result = fileutils.parent_directory(test_file) + assert name == result + # also test on location + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(name) diff --git a/tests/commoncode/commoncode/test_functional.py b/tests/commoncode/commoncode/test_functional.py new file mode 100644 index 00000000000..17774545de4 --- /dev/null +++ b/tests/commoncode/commoncode/test_functional.py @@ -0,0 +1,51 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +from unittest.case import TestCase + +from commoncode.functional import flatten + + +class TestFunctional(TestCase): + + def test_flatten(self): + expected = [7, 6, 5, 4, 'a', 3, 3, 2, 1] + test = flatten([7, (6, [5, [4, ["a"], 3]], 3), 2, 1]) + self.assertEqual(expected, test) + + def test_flatten_generator(self): + def gen(): + for _ in range(2): + yield range(5) + + expected = [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] + test = flatten(gen()) + self.assertEqual(expected, test) + + def test_flatten_empties(self): + expected = ['a'] + test = flatten([[], (), ['a']]) + self.assertEqual(expected, test) diff --git a/tests/commoncode/commoncode/test_hash.py b/tests/commoncode/commoncode/test_hash.py new file mode 100644 index 00000000000..00d7838848f --- /dev/null +++ b/tests/commoncode/commoncode/test_hash.py @@ -0,0 +1,92 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os + +from commoncode.testcase import FileBasedTesting + +import commoncode.hash +from commoncode.hash import sha1 +from commoncode.hash import md5 +from commoncode.hash import b64sha1 + + +class TestHash(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_get_hasher(self): + h = commoncode.hash.get_hasher(160) + self.assertEqual('hvfkN_qlp_zhXR3cuerq6jd2Z7g=', h('a').b64digest()) + self.assertEqual('4MkDWJjdUvxlxBRUzsnE0mEb-zc=', h('aa').b64digest()) + self.assertEqual('fiQN50-x7Qj6CNOAY_amqRRiqBU=', h('aaa').b64digest()) + + def test_hash_1(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert sha1(test_file) == '34ac5465d48a9b04fc275f09bc2230660df8f4f7' + + def test_hash_2(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert md5(test_file) == '4760fb467f1ebf3b0aeace4a3926f1a4' + + def test_hash_3(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert b64sha1(test_file) == 'NKxUZdSKmwT8J18JvCIwZg349Pc=' + + def test_hash_4(self): + test_file = self.get_test_loc('hash/dir1/a.txt') + assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' + + def test_hash_5(self): + test_file = self.get_test_loc('hash/dir1/a.txt') + assert md5(test_file) == '40c53c58fdafacc83cfff6ee3d2f6d69' + + def test_hash_6(self): + test_file = self.get_test_loc('hash/dir1/a.txt') + assert b64sha1(test_file) == 'PKaejWwjSkadFqwopKZYySJnxCM=' + + def test_hash_7(self): + test_file = self.get_test_loc('hash/dir2/a.txt') + assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' + + def test_hash_8(self): + test_file = self.get_test_loc('hash/dir2/a.txt') + assert md5(test_file) == '40c53c58fdafacc83cfff6ee3d2f6d69' + + def test_hash_9(self): + test_file = self.get_test_loc('hash/dir2/a.txt') + assert b64sha1(test_file) == 'PKaejWwjSkadFqwopKZYySJnxCM=' + + def test_hash_10(self): + test_file = self.get_test_loc('hash/dir2/dos.txt') + assert sha1(test_file) == 'a71718fb198630ae8ba32926015d8555a03cb06c' + + def test_hash_11(self): + test_file = self.get_test_loc('hash/dir2/dos.txt') + assert md5(test_file) == '095f5068940e41df9add5d4cc396c181' + + def test_hash_12(self): + test_file = self.get_test_loc('hash/dir2/dos.txt') + assert b64sha1(test_file) == 'pxcY-xmGMK6LoykmAV2FVaA8sGw=' diff --git a/tests/commoncode/commoncode/test_ignore.py b/tests/commoncode/commoncode/test_ignore.py new file mode 100644 index 00000000000..e320b38c686 --- /dev/null +++ b/tests/commoncode/commoncode/test_ignore.py @@ -0,0 +1,178 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import os + +import commoncode.testcase +from commoncode import fileutils + + +from commoncode import ignore +from commoncode.system import on_mac +from unittest.case import skipIf + + +class IgnoreTest(commoncode.testcase.FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def check_default(self, test_dir, expected_message): + for top, dirs, files in os.walk(test_dir, topdown=True): + not_ignored = [] + for d in dirs: + p = os.path.join(top, d) + ign = ignore.is_ignored(p, ignore.default_ignores, {}) + if not ign: + not_ignored.append(d) + dirs[:] = not_ignored + + for f in files: + p = os.path.join(top, f) + ign = ignore.is_ignored(p, ignore.default_ignores, {}) + if ign: + assert ign == expected_message + + @skipIf(on_mac, 'Return different result on Mac for reasons to investigate') + def test_default_ignores_eclipse1(self): + test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') + test_base = os.path.join(test_dir, 'eclipse') + + test = os.path.join(test_base, '.settings') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: Eclipse IDE artifact' == result + + def test_default_ignores_eclipse2(self): + test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') + test_base = os.path.join(test_dir, 'eclipse') + + test = os.path.join(test_base, '.settings/somefile') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: Eclipse IDE artifact' == result + + def test_default_ignores_eclipse3(self): + test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') + test_base = os.path.join(test_dir, 'eclipse') + + test = os.path.join(test_base, '.project') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: Eclipse IDE artifact' == result + + def test_default_ignores_eclipse4(self): + test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') + test_base = os.path.join(test_dir, 'eclipse') + + test = os.path.join(test_base, '.pydevproject') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: Eclipse IDE artifact' == result + + def test_default_ignores_mac1(self): + test_dir = self.extract_test_tar('ignore/excludes/mac.tgz') + test_base = os.path.join(test_dir, 'mac') + + test = os.path.join(test_base, '__MACOSX') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: MacOSX artifact' == result + + def test_default_ignores_mac2(self): + test_dir = self.extract_test_tar('ignore/excludes/mac.tgz') + test_base = os.path.join(test_dir, 'mac') + + test = os.path.join(test_base, '__MACOSX/comp_match/smallrepo/._jetty_1.0_index.csv') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: MacOSX artifact' == result + + def test_default_ignores_mac3(self): + test_dir = self.extract_test_tar('ignore/excludes/mac.tgz') + test_base = os.path.join(test_dir, 'mac') + + test = os.path.join(test_base, '.DS_Store') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: MacOSX artifact' == result + + def test_default_ignores_mac4(self): + test_dir = self.extract_test_tar('ignore/excludes/mac.tgz') + test_base = os.path.join(test_dir, 'mac') + + test = os.path.join(test_base, '.DS_Store/a') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: MacOSX artifact' == result + + @skipIf(on_mac, 'Return different result on Mac for reasons to investigate') + def test_default_ignores_mac5(self): + test_dir = self.extract_test_tar('ignore/excludes/mac.tgz') + test_base = os.path.join(test_dir, 'mac') + + test = os.path.join(test_base, '._.DS_Store') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + # this is really weird as a behavior + assert 'Default ignore: MacOSX artifact' == result + + @skipIf(on_mac, 'Return different result on Mac for reasons to investigate') + def test_default_ignores_msft(self): + test_dir = self.extract_test_tar('ignore/excludes/msft-vs.tgz') + test = os.path.join(test_dir, 'msft-vs/tst.sluo') + result = ignore.is_ignored(test, ignore.default_ignores, {}) + assert 'Default ignore: Microsoft VS project artifact' == result + + @skipIf(on_mac, 'Return different result on Mac for reasons to investigate') + def test_skip_vcs_files_and_dirs(self): + test_dir = self.extract_test_tar('ignore/vcs.tgz') + result = [] + for top, dirs, files in os.walk(test_dir, topdown=True): + not_ignored = [] + for d in dirs: + p = os.path.join(top, d) + ign = ignore.is_ignored(p, ignore.default_ignores, {}) + tp = fileutils.as_posixpath(p.replace(test_dir, '')) + result.append((tp, ign,)) + if not ign: + not_ignored.append(d) + + # skip ignored things + dirs[:] = not_ignored + + for f in files: + p = os.path.join(top, f) + ign = ignore.is_ignored(p, ignore.default_ignores, {}) + tp = fileutils.as_posixpath(p.replace(test_dir, '')) + result.append((tp, ign,)) + + expected = [ + ('/vcs', False), + ('/vcs/.bzr', 'Default ignore: Bazaar artifact'), + ('/vcs/.git', 'Default ignore: Git artifact'), + ('/vcs/.hg', 'Default ignore: Mercurial artifact'), + ('/vcs/.svn', 'Default ignore: SVN artifact'), + ('/vcs/CVS', 'Default ignore: CVS artifact'), + ('/vcs/_darcs', 'Default ignore: Darcs artifact'), + ('/vcs/_MTN', 'Default ignore: Monotone artifact'), + ('/vcs/.bzrignore', 'Default ignore: Bazaar config artifact'), + ('/vcs/.cvsignore', 'Default ignore: CVS config artifact'), + ('/vcs/.gitignore', 'Default ignore: Git config artifact'), + ('/vcs/.hgignore', 'Default ignore: Mercurial config artifact'), + ('/vcs/.svnignore', 'Default ignore: SVN config artifact'), + ('/vcs/vssver.scc', 'Default ignore: Visual Source Safe artifact'), + ] + assert sorted(expected) == sorted(result) diff --git a/tests/commoncode/commoncode/test_paths.py b/tests/commoncode/commoncode/test_paths.py new file mode 100644 index 00000000000..eee5531e19c --- /dev/null +++ b/tests/commoncode/commoncode/test_paths.py @@ -0,0 +1,253 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +from unittest import TestCase + +from commoncode import paths + + +class TestPortablePath(TestCase): + + def test_safe_path(self): + # tuples of test data, expected results + tests = [ + # mixed slashes + (r'C:\Documents and Settings\Boki\Desktop\head\patches\drupal6/drupal.js', + 'c_/documents_and_settings/boki/desktop/head/patches/drupal6/drupal.js'), + # mixed slashes and spaces + (r'C:\Documents and Settings\Boki\Desktop\head\patches\parallel uploads/drupal.js', + 'c_/documents_and_settings/boki/desktop/head/patches/parallel_uploads/drupal.js'), + # windows style + (r'C:\Documents and Settings\Administrator\Desktop\siftDemoV4_old\defs.h', + 'c_/documents_and_settings/administrator/desktop/siftdemov4_old/defs.h'), + # windows style, mixed slashes, no spaces + (r'C:\Documents and Settings\Boki\Desktop\head\patches\imagefield/imagefield.css', + 'c_/documents_and_settings/boki/desktop/head/patches/imagefield/imagefield.css'), + # windows style, spaces + (r'C:\Documents and Settings\Boki\Desktop\head\patches\js delete\imagefield.css', + 'c_/documents_and_settings/boki/desktop/head/patches/js_delete/imagefield.css'), + # windows style, posix slashes + (r'C:/Documents and Settings/Alex Burgel/workspace/Hibernate3.2/test/org/hibernate/test/AllTests.java', + 'c_/documents_and_settings/alex_burgel/workspace/hibernate3.2/test/org/hibernate/test/alltests.java'), + # windows style, relative + (r'includes\webform.components.inc', + 'includes/webform.components.inc'), + # windows style, absolute, trailing slash + ('\\includes\\webform.components.inc\\', + 'includes/webform.components.inc'), + # posix style, relative + (r'includes/webform.components.inc', + 'includes/webform.components.inc'), + # posix style, absolute, trailing slash + (r'/includes/webform.components.inc/', + 'includes/webform.components.inc'), + # posix style, french char + ('/includes/webform.compon\xc3nts.inc/', + 'includes/webform.compon_nts.inc'), + # posix style, chinese char + ('/includes/webform.compon\xd2\xaants.inc/', + 'includes/webform.compon__nts.inc'), + # windows style, dots + ('\\includes\\..\\webform.components.inc\\', + 'webform.components.inc'), + # windows style, many dots + ('.\\includes\\.\\..\\..\\..\webform.components.inc\\.', + 'dotdot/dotdot/webform.components.inc'), + # posix style, dots + (r'includes/../webform.components.inc', + 'webform.components.inc'), + # posix style, many dots + (r'./includes/./../../../../webform.components.inc/.', + 'dotdot/dotdot/dotdot/webform.components.inc'), + ] + for tst, expected in tests: + assert expected == paths.safe_path(tst) + + def test_resolve(self): + # tuples of test data, expected results + tests = [ + ('C:\\..\\./drupal.js', + 'drupal.js'), + ('\\includes\\..\\webform.components.inc\\', + 'webform.components.inc'), + ('includes/../webform.components.inc', + 'webform.components.inc'), + ('////.//includes/./../..//..///../webform.components.inc/.', + 'dotdot/dotdot/dotdot/webform.components.inc'), + (u'////.//includes/./../..//..///../webform.components.inc/.', + u'dotdot/dotdot/dotdot/webform.components.inc'), + ('includes/../', + '.'), + ] + for tst, expected in tests: + assert expected == paths.resolve(tst) + + +class TestCommonPath(TestCase): + + def test_common_path_prefix1(self): + test = paths.common_path_prefix('/a/b/c', '/a/b/c') + assert ('a/b/c', 3) == test + + def test_common_path_prefix2(self): + test = paths.common_path_prefix('/a/b/c', '/a/b') + assert ('a/b', 2) == test + + def test_common_path_prefix3(self): + test = paths.common_path_prefix('/a/b', '/a/b/c') + assert ('a/b', 2) == test + + def test_common_path_prefix4(self): + test = paths.common_path_prefix('/a', '/a') + assert ('a', 1) == test + + def test_common_path_prefix_path_root(self): + test = paths.common_path_prefix('/a/b/c', '/') + assert (None, 0) == test + + def test_common_path_prefix_root_path(self): + test = paths.common_path_prefix('/', '/a/b/c') + assert (None, 0) == test + + def test_common_path_prefix_root_root(self): + test = paths.common_path_prefix('/', '/') + assert (None, 0) == test + + def test_common_path_prefix_path_elements_are_similar(self): + test = paths.common_path_prefix('/a/b/c', '/a/b/d') + assert ('a/b', 2) == test + + def test_common_path_prefix_no_match(self): + test = paths.common_path_prefix('/abc/d', '/abe/f') + assert (None, 0) == test + + def test_common_path_prefix_ignore_training_slashes(self): + test = paths.common_path_prefix('/a/b/c/', '/a/b/c/') + assert ('a/b/c', 3) == test + + def test_common_path_prefix8(self): + test = paths.common_path_prefix('/a/b/c/', '/a/b') + assert ('a/b', 2) == test + + def test_common_path_prefix10(self): + test = paths.common_path_prefix('/a/b/c.txt', '/a/b/b.txt') + assert ('a/b', 2) == test + + def test_common_path_prefix11(self): + test = paths.common_path_prefix('/a/b/c.txt', '/a/b.txt') + assert ('a', 1) == test + + def test_common_path_prefix12(self): + test = paths.common_path_prefix('/a/c/e/x.txt', '/a/d/a.txt') + assert ('a', 1) == test + + def test_common_path_prefix13(self): + test = paths.common_path_prefix('/a/c/e/x.txt', '/a/d/') + assert ('a', 1) == test + + def test_common_path_prefix14(self): + test = paths.common_path_prefix('/a/c/e/', '/a/d/') + assert ('a', 1) == test + + def test_common_path_prefix15(self): + test = paths.common_path_prefix('/a/c/e/', '/a/c/a.txt') + assert ('a/c', 2) == test + + def test_common_path_prefix16(self): + test = paths.common_path_prefix('/a/c/e/', '/a/c/f/') + assert ('a/c', 2) == test + + def test_common_path_prefix17(self): + test = paths.common_path_prefix('/a/a.txt', '/a/b.txt/') + assert ('a', 1) == test + + def test_common_path_prefix18(self): + test = paths.common_path_prefix('/a/c/', '/a/') + assert ('a', 1) == test + + def test_common_path_prefix19(self): + test = paths.common_path_prefix('/a/c.txt', '/a/') + assert ('a', 1) == test + + def test_common_path_prefix20(self): + test = paths.common_path_prefix('/a/c/', '/a/d/') + assert ('a', 1) == test + + def test_common_path_suffix(self): + test = paths.common_path_suffix('/a/b/c', '/a/b/c') + assert ('a/b/c', 3) == test + + def test_common_path_suffix_absolute_relative(self): + test = paths.common_path_suffix('a/b/c', '/a/b/c') + assert ('a/b/c', 3) == test + + def test_common_path_suffix_find_subpath(self): + test = paths.common_path_suffix('/z/b/c', '/a/b/c') + assert ('b/c', 2) == test + + def test_common_path_suffix_handles_relative_path(self): + test = paths.common_path_suffix('a/b', 'a/b') + assert ('a/b', 2) == test + + def test_common_path_suffix_handles_relative_subpath(self): + test = paths.common_path_suffix('zsds/adsds/a/b/b/c', + 'a//a/d//b/c') + assert ('b/c', 2) == test + + def test_common_path_suffix_ignore_and_strip_trailing_slash(self): + test = paths.common_path_suffix('zsds/adsds/a/b/b/c/', + 'a//a/d//b/c/') + assert ('b/c', 2) == test + + def test_common_path_suffix_return_None_if_no_common_suffix(self): + test = paths.common_path_suffix('/a/b/c', '/') + assert (None, 0) == test + + def test_common_path_suffix_return_None_if_no_common_suffix2(self): + test = paths.common_path_suffix('/', '/a/b/c') + assert (None, 0) == test + + def test_common_path_suffix_match_only_whole_segments(self): + # only segments are honored, commonality within segment is ignored + test = paths.common_path_suffix( + 'this/is/aaaa/great/path', 'this/is/aaaaa/great/path') + assert ('great/path', 2) == test + + def test_common_path_suffix_two_root(self): + test = paths.common_path_suffix('/', '/') + assert (None, 0) == test + + def test_common_path_suffix_empty_root(self): + test = paths.common_path_suffix('', '/') + assert (None, 0) == test + + def test_common_path_suffix_root_empty(self): + test = paths.common_path_suffix('/', '') + assert (None, 0) == test + + def test_common_path_suffix_empty_empty(self): + test = paths.common_path_suffix('', '') + assert (None, 0) == test diff --git a/tests/commoncode/commoncode/test_timeutils.py b/tests/commoncode/commoncode/test_timeutils.py new file mode 100644 index 00000000000..0e8c8bd342e --- /dev/null +++ b/tests/commoncode/commoncode/test_timeutils.py @@ -0,0 +1,91 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from datetime import datetime + +from commoncode.testcase import FileBasedTesting +from commoncode.timeutils import time2tstamp, tstamp2time, UTC + +class TestTimeStamp(FileBasedTesting): + + def test_time2tstamp_is_path_safe_and_file_is_writable(self): + ts = time2tstamp() + tf = self.get_temp_file(extension='ext', dir_name=ts, file_name=ts) + fd = open(tf, 'w') + fd.write('a') + fd.close() + + def test_time2tstamp_accepts_existing_datetimes(self): + ts = time2tstamp() + tf = self.get_temp_file(extension='ext', dir_name=ts, file_name=ts) + fd = open(tf, 'w') + fd.write('a') + fd.close() + + def test_time2tstamp_raises_on_non_datetime(self): + self.assertRaises(AttributeError, time2tstamp, 'some') + self.assertRaises(AttributeError, time2tstamp, 1) + + def test_time2tstamp_tstamp2time_is_idempotent(self): + dt = datetime.utcnow() + ts = time2tstamp(dt) + dt_from_ts = tstamp2time(ts) + self.assertEqual(dt, dt_from_ts) + + def test_tstamp2time_format(self): + import re + ts = time2tstamp() + pat = '^20\d\d-[0-1][0-9]-[0-3]\dT[0-2]\d[0-6]\d[0-6]\d.\d\d\d\d\d\d$' + self.assertTrue(re.match(pat, ts)) + + def test_tstamp2time(self): + dt_from_ts = tstamp2time('2010-11-12T131415.000016') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('20101112T131415.000016') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('20101112T131415.000016Z') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('2010-11-12T131415') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('2010-11-12T13:14:15') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('20101112T13:14:15') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('20101112T13:14:15Z') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('20101112T13:14:15Z') + self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + + dt_from_ts = tstamp2time('2010-06-30T21:26:40.000Z') + self.assertEqual(datetime(year=2010, month=06, day=30, hour=21, minute=26, second=40, microsecond=0, tzinfo=UTC()), dt_from_ts) + + def test_tstamp2time_raise(self): + self.assertRaises(ValueError, tstamp2time, '201011A12T13:14:15Z') diff --git a/tests/commoncode/commoncode/test_urn.py b/tests/commoncode/commoncode/test_urn.py new file mode 100644 index 00000000000..ff18e3da084 --- /dev/null +++ b/tests/commoncode/commoncode/test_urn.py @@ -0,0 +1,164 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +from unittest import TestCase + +from commoncode import urn + + +class URNTestCase(TestCase): + + def test_encode_license(self): + u1 = urn.encode('license', key='somekey') + self.assertEquals('urn:dje:license:somekey', u1) + + def test_encode_owner(self): + u1 = urn.encode('owner', name='somekey') + self.assertEquals('urn:dje:owner:somekey', u1) + + def test_encode_component(self): + u1 = urn.encode('component', name='name', version='version') + self.assertEquals('urn:dje:component:name:version', u1) + + def test_encode_component_no_version(self): + u1 = urn.encode('component', name='name', version='') + self.assertEquals('urn:dje:component:name:', u1) + + def test_encode_license_with_extra_fields_are_ignored(self): + u1 = urn.encode('license', key='somekey', junk='somejunk') + self.assertEquals('urn:dje:license:somekey', u1) + + def test_encode_missing_field_raise_keyerror(self): + with self.assertRaises(KeyError): + urn.encode('license') + + def test_encode_missing_field_component_raise_keyerror(self): + with self.assertRaises(KeyError): + urn.encode('component', name='this') + + def test_encode_unknown_object_type_raise_keyerror(self): + with self.assertRaises(KeyError): + urn.encode('some', key='somekey') + + def test_encode_component_with_spaces_are_properly_quoted(self): + u1 = urn.encode('component', name='name space', + version='version space') + self.assertEquals('urn:dje:component:name+space:version+space', u1) + + def test_encode_leading_and_trailing_spaces_are_trimmed_and_ignored(self): + u1 = urn.encode(' component ', name=' name space ', + version=''' version space ''') + self.assertEquals('urn:dje:component:name+space:version+space', u1) + + def test_encode_component_with_semicolon_are_properly_quoted(self): + u1 = urn.encode('component', name='name:', version=':version') + self.assertEquals('urn:dje:component:name%3A:%3Aversion', u1) + + def test_encode_component_with_plus_are_properly_quoted(self): + u1 = urn.encode('component', name='name+', version='version+') + self.assertEquals('urn:dje:component:name%2B:version%2B', u1) + + def test_encode_component_with_percent_are_properly_quoted(self): + u1 = urn.encode('component', name='name%', version='version%') + self.assertEquals('urn:dje:component:name%25:version%25', u1) + + def test_encode_object_type_case_is_not_significant(self): + u1 = urn.encode('license', key='key') + u2 = urn.encode('lICENSe', key='key') + self.assertEquals(u1, u2) + + def test_decode_component(self): + u = 'urn:dje:component:name:version' + parsed = ('component', {'name': 'name', 'version': 'version'}) + self.assertEqual(parsed, urn.decode(u)) + + def test_decode_license(self): + u = 'urn:dje:license:lic' + parsed = ('license', {'key': 'lic'}) + self.assertEqual(parsed, urn.decode(u)) + + def test_decode_org(self): + u = 'urn:dje:owner:name' + parsed = ('owner', {'name': 'name'}) + self.assertEqual(parsed, urn.decode(u)) + + def test_decode_build_is_idempotent(self): + u1 = urn.encode('component', owner__name='org%', name='name%', + version='version%') + m, f = urn.decode(u1) + u3 = urn.encode(m, **f) + self.assertEqual(u1, u3) + + def test_decode_raise_exception_if_incorrect_prefix(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('arn:dje:a:a') + + def test_decode_raise_exception_if_incorrect_ns(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:x:x:x') + + def test_decode_raise_exception_if_incorrect_prefix_or_ns(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('x:x:x:x') + + def test_decode_raise_exception_if_too_short_license(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:license') + + def test_decode_raise_exception_if_too_short_component(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:component') + + def test_decode_raise_exception_if_too_long(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:owner:o:n') + + def test_decode_raise_exception_if_too_long1(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:component:o:n:v:junk') + + def test_decode_raise_exception_if_too_long2(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:owner:org:junk') + + def test_decode_raise_exception_if_too_long3(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:license:key:junk') + + def test_decode_raise_exception_if_unknown_object_type(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje:marshmallows:dsds') + + def test_decode_raise_exception_if_missing_object_type(self): + with self.assertRaises(urn.URNValidationError): + urn.decode('urn:dje::dsds') + + def test_encode_decode_is_idempotent(self): + object_type = 'component' + fields = {'name': 'SIP Servlets (MSS)', 'version': 'v 1.4.0.FINAL'} + encoded = 'urn:dje:component:SIP+Servlets+%28MSS%29:v+1.4.0.FINAL' + assert encoded == urn.encode(object_type, **fields) + assert object_type, fields == urn.decode(encoded) diff --git a/tests/commoncode/commoncode/test_version.py b/tests/commoncode/commoncode/test_version.py new file mode 100644 index 00000000000..4e383c4a13f --- /dev/null +++ b/tests/commoncode/commoncode/test_version.py @@ -0,0 +1,139 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import unittest + +from commoncode import version + +class TestVersionHint(unittest.TestCase): + def test_version_hint(self): + data = { + '/xmlgraphics/fop/source/fop-1.0-src.zip': '1.0', + '/xml/xindice/xml-xindice-1.2m1-src.zip': '1.2m1', + '/xmlgraphics/fop/binaries/fop-0.94-bin-jdk1.3.tar.gz': '0.94', + '/xmlgraphics/batik/batik-src-1.7beta1.zip': '1.7beta1', + '/xmlgraphics/batik/batik-1.7-jre13.zip': '1.7', + '/xmlbeans/source/xmlbeans-2.3.0-src.tgz': '2.3.0', + '/xml/xindice/source/xml-xindice-1.2m1-src.tar.gz': '1.2m1', + '/xml/xerces-p/binaries/XML-Xerces-2.3.0-4-win32.zip': '2.3.0-4', + '/xml/xerces-p/source/XML-Xerces-2.3.0-3.tar.gz': '2.3.0-3', + '/xml/xalan-j/source/xalan-j_2_7_0-src-2jars.tar.gz': '2_7_0', + '/xml/security/java-library/xml-security-src-1_0_5D2.zip': '1_0_5D2', + '/xml/commons/binaries/xml-commons-external-1.4.01-bin.zip': '1.4.01', + '/xml/commons/xml-commons-1.0.b2.zip': '1.0.b2', + '/xml/cocoon/3.0/cocoon-all-3.0.0-alpha-1-dist.tar.gz': '3.0.0-alpha-1', + '/xerces/j/source/Xerces-J-tools.2.10.0-xml-schema-1.1-beta.tar.gz': '2.10.0', + '/xerces/c/3/binaries/xerces-c-3.1.1-x86_64-solaris-cc-5.10.tar.gz': '3.1.1', + '/xerces/c/3/binaries/xerces-c-3.1.1-x86_64-windows-vc-8.0.zip': '3.1.1', + '/xerces/c/2/binaries/xerces-c_2_8_0-x86-windows-vc_7_1.zip': '2_8_0', + '/ws/woden/1.0M8/apache-woden-src-1.0M8.tar.gz': '1.0M8', + '/ws/scout/0_7rc1/source/scout-0.7rc1-src.zip': '0.7rc1', + '/ws/juddi/3_0/juddi-portal-bundle-3.0.0.rc1.zip': '3.0.0.rc1', + '/ws/juddi/3_0/juddi-portal-bundle-3.0.0.beta.zip': '3.0.0.beta', + '/ws/juddi/2_0RC7/juddi-tomcat-2.0rc7.zip': '2.0rc7', + '/ws/axis2/tools/1_4_1/axis2-wsdl2code-maven-plugin-1.4.1.jar': '1.4.1', + '/ws/axis/1_4/axis-src-1_4.zip': '1_4', + '/ws/axis-c/source/win32/axis-c-1.6b-Win32-trace-src.zip': '1.6b', + '/tuscany/java/sca/2.0-M5/apache-tuscany-sca-all-2.0-M5-src.tar.gz': '2.0-M5', + '/turbine/turbine-2.3.3-rc1/source/turbine-2.3.3-RC1-src.zip': '2.3.3-RC1', + '/tomcat/tomcat-connectors/jk/binaries/win64/jk-1.2.30/ia64/symbols-1.2.30.zip': '1.2.30', + '/tomcat/tomcat-7/v7.0.0-beta/bin/apache-tomcat-7.0.0-windows-i64.zip': '7.0.0', + '/tomcat/tomcat-4/v4.1.40/bin/apache-tomcat-4.1.40-LE-jdk14.exe': '4.1.40', + '/tapestry/tapestry-src-5.1.0.5.tar.gz': '5.1.0.5', + '/spamassassin/source/Mail-SpamAssassin-rules-3.3.0.r901671.tgz': '3.3.0.r901671', + '/spamassassin/Mail-SpamAssassin-rules-3.3.1.r923257.tgz': '3.3.1.r923257', + '/shindig/1.1-BETA5-incubating/shindig-1.1-BETA5-incubating-source.zip': '1.1-BETA5', + '/servicemix/nmr/1.0.0-m3/apache-servicemix-nmr-1.0.0-m3-src.tar.gz': '1.0.0-m3', + '/qpid/0.6/qpid-dotnet-0-10-0.6.zip': '0.6', + '/openjpa/2.0.0-beta/apache-openjpa-2.0.0-beta-binary.zip': '2.0.0-beta', + '/myfaces/source/portlet-bridge-2.0.0-alpha-2-src-all.tar.gz': '2.0.0-alpha-2', + '/myfaces/source/myfaces-extval20-2.0.3-src.tar.gz': '2.0.3', + '/harmony/milestones/6.0/debian/amd64/harmony-6.0-classlib_0.0r946981-1_amd64.deb': '6.0', + '/geronimo/eclipse/updates/plugins/org.apache.geronimo.st.v21.ui_2.1.1.jar': '2.1.1', + '/directory/studio/update/1.x/plugins/org.apache.directory.studio.aciitemeditor_1.5.2.v20091211.jar': '1.5.2.v20091211', + '/db/torque/torque-3.3/source/torque-gen-3.3-RC3-src.zip': '3.3-RC3', + '/cayenne/cayenne-3.0B1.tar.gz': '3.0B1', + '/cayenne/cayenne-3.0M4-macosx.dmg': '3.0M4', + '/xmlgraphics/batik/batik-docs-current.zip': 'current', + '/xmlgraphics/batik/batik-docs-previous.zip': 'previous', + '/poi/dev/bin/poi-bin-3.7-beta1-20100620.zip': '3.7-beta1-20100620', + '/excalibur/avalon-logkit/source/excalibur-logkit-2.0.dev-0-src.zip': '2.0.dev-0', + '/db/derby/db-derby-10.4.2.0/derby_core_plugin_10.4.2.zip': '10.4.2', + '/httpd/modpython/win/2.7.1/mp152dll.zip': '2.7.1', + '/perl/mod_perl-1.31/apaci/mod_perl.config.sh': '1.31', + '/xml/xerces-j/old_xerces2/Xerces-J-bin.2.0.0.alpha.zip': '2.0.0.alpha', + '/xml/xerces-p/archives/XML-Xerces-1.7.0_0.tar.gz': '1.7.0_0', + '/httpd/docs/tools-2004-05-04.zip': '2004-05-04', + '/ws/axis2/c/M0_5/axis2c-src-M0.5.tar.gz': 'M0.5', + '/jakarta/poi/dev/src/jakarta-poi-1.8.0-dev-src.zip': '1.8.0-dev', + '/tapestry/tapestry-4.0-beta-8.zip': '4.0-beta-8', + '/openejb/3.0-beta-1/openejb-3.0-beta-1.zip': '3.0-beta-1', + '/tapestry/tapestry-4.0-rc-1.zip': '4.0-rc-1', + '/jakarta/tapestry/source/3.0-rc-3/Tapestry-3.0-rc-3-src.zip': '3.0-rc-3', + '/jakarta/lucene/binaries/lucene-1.3-final.tar.gz': '1.3-final', + '/jakarta/tapestry/binaries/3.0-beta-1a/Tapestry-3.0-beta-1a-bin.zip': '3.0-beta-1a', + '/poi/release/bin/poi-bin-3.0-FINAL-20070503.tar.gz': '3.0-FINAL-20070503', + '/harmony/milestones/M4/apache-harmony-hdk-r603534-linux-x86-32-libstdc++v6-snapshot.tar.gz': 'r603534', + '/ant/antidote/antidote-20050330.tar.bz2': '20050330', + '/apr/not-released/apr_20020725223645.tar.gz': '20020725223645', + '/ibatis/source/ibatis.net/src-revision-709676.zip': 'revision-709676', + '/ws/axis-c/source/win32/axis-c-src-1-2-win32.zip': '1-2', + '/jakarta/slide/most-recent-2.0rc1-binaries/jakarta-slide 2.0rc1 jakarta-tomcat-4.1.30.zip': '2.0rc1', + '/httpd/modpython/win/3.0.1/python2.2.1-apache2.0.43.zip': '2.2.1', + '/ant/ivyde/updatesite/features/org.apache.ivy.feature_2.1.0.cr1_20090319213629.jar': '2.1.0.cr1_20090319213629', + '/jakarta/poi/dev/bin/poi-2.0-pre1-20030517.jar': '2.0-pre1-20030517', + '/jakarta/poi/release/bin/jakarta-poi-1.5.0-FINAL-bin.zip': '1.5.0-FINAL', + '/jakarta/poi/release/bin/poi-bin-2.0-final-20040126.zip': '2.0-final-20040126', + '/activemq/apache-activemq/5.0.0/apache-activemq-5.0.0-sources.jar': '5.0.0', + '/turbine/turbine-2.2/source/jakarta-turbine-2.2-B1.tar.gz': '2.2-B1', + '/ant/ivyde/updatesite/features/org.apache.ivy.feature_2.0.0.cr1.jar': '2.0.0.cr1', + '/ant/ivyde/updatesite/features/org.apache.ivy.feature_2.0.0.final_20090108225011.jar': '2.0.0.final_20090108225011', + '/ws/axis/1_2RC3/axis-src-1_2RC3.zip': '1_2RC3', + '/commons/lang/old/v1.0-b1.1/commons-lang-1.0-b1.1.zip': '1.0-b1.1', + '/commons/net/binaries/commons-net-1.2.0-release.tar.gz': '1.2.0-release', + '/ant/ivyde/2.0.0.final/apache-ivyde-2.0.0.final-200907011148-RELEASE.tgz': '2.0.0.final-200907011148-RELEASE', + '/geronimo/eclipse/updates/plugins/org.apache.geronimo.jetty.j2ee.server.v11_1.0.0.jar': 'v11_1.0.0', + '/jakarta/cactus/binaries/jakarta-cactus-13-1.7.1-fixed.zip': '1.7.1-fixed', + '/jakarta/jakarta-turbine-maven/maven/jars/maven-1.0-b5-dev.20020731.085427.jar': '1.0-b5-dev.20020731.085427', + '/xml/xalan-j/source/xalan-j_2_5_D1-src.tar.gz': '2_5_D1', + '/ws/woden/IBuilds/I20051002_1145/woden-I20051002_1145.tar.bz2': 'I20051002_1145', + '/commons/beanutils/source/commons-beanutils-1.8.0-BETA-src.tar.gz': '1.8.0-BETA', + '/cocoon/BINARIES/cocoon-2.0.3-vm14-bin.tar.gz': '2.0.3-vm14', + '/felix/xliff_filters_v1_2_7_unix.jar': 'v1_2_7', + '/excalibur/releases/200702/excalibur-javadoc-r508111-15022007.tar.gz': 'r508111-15022007', + '/geronimo/eclipse/updates/features/org.apache.geronimo.v20.feature_2.0.0.jar': 'v20.feature_2.0.0', + '/geronimo/2.1.6/axis2-jaxws-1.3-G20090406.jar': '1.3-G20090406', + '/cassandra/debian/pool/main/c/cassandra/cassandra_0.4.0~beta1-1.diff.gz': '0.4.0~beta1-1', + '/ha-api-3.1.6.jar': '3.1.6', + 'ha-api-3.1.6.jar': '3.1.6' + } + + # FIXME: generate a test function for each case + for path in data: + expected = data[path] + if not expected.lower().startswith('v'): + expected = 'v ' + expected + self.assertEqual(expected, version.hint(path)) From db8420727492998b9e2e248299908cc703814bda Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 17:00:24 +0200 Subject: [PATCH 003/436] Improved fileutils.walk to support walking a single file. * updated file_walk to use this code. * added ignore support to file_walk --- src/commoncode/fileutils.py | 97 +++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index ca1de9f8853..26de959fd9b 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -242,58 +242,61 @@ def splitext(path): # # DIRECTORY WALKING # -# TODO: rename ignorer to ignore -def walk(location, ignorer=None): +ignore_nothing = lambda _: False + +def walk(location, ignored=ignore_nothing): """ Walk location returning the same tuples as os.walk but with a different behavior: - always walk top-down, breadth-first. - - always ignore and never follow symlinks. + - always ignore and never follow symlinks, . - always ignore special files (FIFOs, etc.) - - optionally ignore files and directories by invoking the `ignorer` - callable on files and directories. - """ - assert filetype.is_dir(location) - - if not ignorer: - # never ignore if we do not have an ignorer - ignorer = lambda _: False - - dirs = [] - files = [] - - # TODO: consider using scandir - for name in os.listdir(location): - loc = os.path.join(location, name) - if ignorer(loc): - continue - if filetype.is_dir(loc): - dirs.append(name) - elif filetype.is_file(loc): - files.append(name) - # else: pass: special files and symlinks are always ignored - yield location, dirs, files - - for dr in dirs: - to_walk = os.path.join(location, dr) - if not filetype.is_special(location): - for t, d, f in walk(to_walk, ignorer): - yield t, d, f - - -def file_walk(location): - """ - Return the files at location recursively. - - :param location: can be a file or a directory - :return: file paths - """ - if is_file(location): - yield location - else: - for root, _dirs, files in walk(location): - for f in files: - yield os.path.join(root, f) + - optionally ignore files and directories by invoking the `ignored` + callable on files and directories returning True if it should be ignored. + - location is a directory or a file: for a file, the file is returned. + """ + if not ignored(location): + if filetype.is_file(location) : + yield parent_directory(location), [], [file_name(location)] + elif filetype.is_dir(location): + dirs = [] + files = [] + # TODO: consider using scandir + for name in os.listdir(location): + loc = os.path.join(location, name) + if filetype.is_special(loc) or ignored(loc): + continue + + if filetype.is_dir(loc): + dirs.append(name) + elif filetype.is_file(loc): + files.append(name) + # else: + # special files and symlinks are always ignored + # pass + yield location, dirs, files + + for dr in dirs: + for tripple in walk(os.path.join(location, dr), ignored): + yield tripple + + # else: + # special files and symlinks are always ignored + # pass + + +def file_walk(location, ignored=ignore_nothing): + """ + Return an iterable of files at `location` recursively. + + :param location: a file or a directory. + :param ignored: a callable accepting a location argument and returning True + if the location should be ignored. + :return: an iterable of file locations. + """ + for root, _dirs, files in walk(location, ignored): + for f in files: + yield os.path.join(root, f) # # From 4dc8e0d9285b4407f7bc8161462a2e7082f40955 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 17:00:24 +0200 Subject: [PATCH 004/436] Improved fileutils.walk to support walking a single file. * updated file_walk to use this code. * added ignore support to file_walk --- tests/commoncode/commoncode/test_fileutils.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index af26e25b368..fc2911f51df 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -314,6 +314,26 @@ def test_os_walk_with_unicode_path(self): ] assert expected == result + def test_fileutils_walk_with_unicode_path(self): + test_dir = self.extract_test_zip('fileutils/unicode.zip') + test_dir = join(test_dir, 'unicode') + + test_dir = unicode(test_dir) + result = list(fileutils.walk(test_dir)) + expected = [ + (unicode(test_dir), ['a'], [u'2.csv']), + (unicode(test_dir) + sep + 'a', [], [u'gru\u0308n.png']) + ] + assert expected == result + + def test_fileutils_walk_can_walk_a_single_file(self): + test_file = self.get_test_loc('fileutils/unicode.zip') + result = list(fileutils.walk(test_file)) + expected = [ + (fileutils.parent_directory(test_file), [], ['unicode.zip']) + ] + assert expected == result + class TestName(FileBasedTesting): test_data_dir = os.path.join(os.path.dirname(__file__), 'data') From f634171eea1e4c59bd848ab8d8858a4251369370 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 17:46:40 +0200 Subject: [PATCH 005/436] Renamed file_walk to file_iter and added tests. --- src/commoncode/fileutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index 26de959fd9b..a143e2de1df 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -285,7 +285,7 @@ def walk(location, ignored=ignore_nothing): # pass -def file_walk(location, ignored=ignore_nothing): +def file_iter(location, ignored=ignore_nothing): """ Return an iterable of files at `location` recursively. From b8745bc9a3fdd9626444cc4042dc48134706deac Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 17:46:40 +0200 Subject: [PATCH 006/436] Renamed file_walk to file_iter and added tests. --- .../data/fileutils/{ => walk}/unicode.zip | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/commoncode/commoncode/data/fileutils/{ => walk}/unicode.zip (100%) diff --git a/tests/commoncode/commoncode/data/fileutils/unicode.zip b/tests/commoncode/commoncode/data/fileutils/walk/unicode.zip similarity index 100% rename from tests/commoncode/commoncode/data/fileutils/unicode.zip rename to tests/commoncode/commoncode/data/fileutils/walk/unicode.zip From b7617767f6a52f3d591348787a9132a7df6d7e68 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 17:47:16 +0200 Subject: [PATCH 007/436] Guarded debug logging with a flag --- src/commoncode/fileset.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/commoncode/fileset.py b/src/commoncode/fileset.py index d6d64070e1d..3bdb3d84464 100644 --- a/src/commoncode/fileset.py +++ b/src/commoncode/fileset.py @@ -31,10 +31,11 @@ from commoncode import fileutils from commoncode import paths -LOG = logging.getLogger(__name__) +DEBUG = False +logger = logging.getLogger(__name__) # import sys # logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) -# LOG.setLevel(logging.DEBUG) +# logger.setLevel(logging.DEBUG) """ Match files and directories paths based on inclusion and exclusion glob-style @@ -47,7 +48,7 @@ Patterns are applied to a path this way: - Paths are converted to POSIX paths before matching. - - Patterns are NOT case-sensitive. + - Patterns are NOT case-sensitive. - Leading slashes are ignored. - If the pattern contains a /, then the whole path must be matched; otherwise, the pattern matches if any path segment matches. @@ -68,7 +69,7 @@ - [seq] : matches any character in seq - [!seq] :matches any character not in seq For a literal match, wrap the meta-characters in brackets. For example, '[?]' -matches the character '?'. +matches the character '?'. """ @@ -80,7 +81,7 @@ def match(path, includes, excludes): matched and associated message. The message explains why a path is included when matched. The message is always a string (possibly empty). - `includes` and `excludes` are maps of (fnmtch pattern -> message). + `includes` and `excludes` are maps of (fnmtch pattern -> message). The order of the includes and excludes items does not matter. If one is empty, it is not used for matching. If the `path` is empty, return False. """ @@ -92,8 +93,9 @@ def match(path, includes, excludes): included = _match(path, includes) excluded = _match(path, excludes) - LOG.debug('in_fileset: path: %(path)r included:%(included)r, ' - 'excluded:%(excluded)r .' % locals()) + if DEBUG: + logger.debug('in_fileset: path: %(path)r included:%(included)r, ' + 'excluded:%(excluded)r .' % locals()) if excluded: return False elif included: @@ -115,7 +117,8 @@ def _match(path, patterns): if not pathstripped: return False segments = paths.split(pathstripped) - LOG.debug('_match: path: %(path)r patterns:%(patterns)r.' % locals()) + if DEBUG: + logger.debug('_match: path: %(path)r patterns:%(patterns)r.' % locals()) mtch = False for pat, msg in patterns.items(): if not pat and not pat.strip(): @@ -131,7 +134,8 @@ def _match(path, patterns): or fnmatch.fnmatchcase(pathstripped, pat)): mtch = msg break - LOG.debug('_match: match is %(mtch)r' % locals()) + if DEBUG: + logger.debug('_match: match is %(mtch)r' % locals()) return mtch From ecf04df788ecd93910da6733b7e98b29175f7b16 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 17:47:46 +0200 Subject: [PATCH 008/436] Cosmetics. --- src/commoncode/ignore.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/commoncode/ignore.py b/src/commoncode/ignore.py index 48fac3b6958..423ec66a73c 100644 --- a/src/commoncode/ignore.py +++ b/src/commoncode/ignore.py @@ -30,10 +30,23 @@ from commoncode import fileutils """ -Handle .ignore-like files. +Support for ignoring some file patterns such as .git or .svn directories, used +typically when walking file systems. +Also handle .ignore-like file and provide common default ignores. """ +def is_ignored(location, ignores, unignores, skip_special=True): + """ + Return a tuple of (pattern , message) if a file at location is ignored + or False otherwise. + `ignores` and `unignores` are mappings of patterns to a reason. + """ + if skip_special and filetype.is_special(location): + return True + return fileset.match(location, includes=ignores, excludes=unignores) + + def is_ignore_file(location): """ Return True if the location is an ignore file. @@ -57,17 +70,6 @@ def get_ignores(location, include_defaults=True): unignores.update(uni) return ignores, unignores - -def is_ignored(location, ignores, unignores, skip_special=True): - """ - Return a tuple of (pattern , message) if a file at location is ignored - or False otherwise. - """ - if skip_special and filetype.is_special(location): - return True - return fileset.match(location, ignores, unignores) - - # # Default ignores # @@ -196,7 +198,6 @@ def is_ignored(location, ignores, unignores, skip_special=True): '*/_MTN': 'Default ignore: Monotone artifact', '*/_darcs': 'Default ignore: Darcs artifact', '*/{arch}': 'Default ignore: GNU Arch artifact', - } ignores_Medias = { @@ -290,7 +291,9 @@ def is_ignored(location, ignores, unignores, skip_special=True): '/.ssh': 'Default ignore: SSH configuration', } + default_ignores = {} + default_ignores.update(chain(*[d.items() for d in [ ignores_MacOSX, ignores_Windows, From c69a5f24688dd7e38a5bc377f2bee2e79eee2dd0 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 11 Jul 2015 18:03:43 +0200 Subject: [PATCH 009/436] Tests and test files for fileutils.file_iter and walk --- .../data/fileutils/walk/d1/d2/d3/f3 | 0 .../commoncode/data/fileutils/walk/d1/d2/f2 | 0 .../commoncode/data/fileutils/walk/d1/f1 | 0 .../commoncode/data/fileutils/walk/f | 0 tests/commoncode/commoncode/test_fileutils.py | 53 +++++++++++++++++-- 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 tests/commoncode/commoncode/data/fileutils/walk/d1/d2/d3/f3 create mode 100644 tests/commoncode/commoncode/data/fileutils/walk/d1/d2/f2 create mode 100644 tests/commoncode/commoncode/data/fileutils/walk/d1/f1 create mode 100644 tests/commoncode/commoncode/data/fileutils/walk/f diff --git a/tests/commoncode/commoncode/data/fileutils/walk/d1/d2/d3/f3 b/tests/commoncode/commoncode/data/fileutils/walk/d1/d2/d3/f3 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/walk/d1/d2/f2 b/tests/commoncode/commoncode/data/fileutils/walk/d1/d2/f2 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/walk/d1/f1 b/tests/commoncode/commoncode/data/fileutils/walk/d1/f1 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/data/fileutils/walk/f b/tests/commoncode/commoncode/data/fileutils/walk/f new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index fc2911f51df..7b05a1e72da 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -303,7 +303,7 @@ def test_resource_name(self): assert 'f.a' == fileutils.resource_name('f.a') def test_os_walk_with_unicode_path(self): - test_dir = self.extract_test_zip('fileutils/unicode.zip') + test_dir = self.extract_test_zip('fileutils/walk/unicode.zip') test_dir = join(test_dir, 'unicode') test_dir = unicode(test_dir) @@ -314,8 +314,20 @@ def test_os_walk_with_unicode_path(self): ] assert expected == result + def test_fileutils_walk(self): + test_dir = self.get_test_loc('fileutils/walk') + base = self.get_test_loc('fileutils') + result = [(t.replace(base, ''), d, f,) for t, d, f in fileutils.walk(test_dir)] + expected = [ + ('/walk', ['d1'], ['f', 'unicode.zip']), + ('/walk/d1', ['d2'], ['f1']), + ('/walk/d1/d2', ['d3'], ['f2']), + ('/walk/d1/d2/d3', [], ['f3']) + ] + assert expected == result + def test_fileutils_walk_with_unicode_path(self): - test_dir = self.extract_test_zip('fileutils/unicode.zip') + test_dir = self.extract_test_zip('fileutils/walk/unicode.zip') test_dir = join(test_dir, 'unicode') test_dir = unicode(test_dir) @@ -327,13 +339,46 @@ def test_fileutils_walk_with_unicode_path(self): assert expected == result def test_fileutils_walk_can_walk_a_single_file(self): - test_file = self.get_test_loc('fileutils/unicode.zip') + test_file = self.get_test_loc('fileutils/walk/f') result = list(fileutils.walk(test_file)) expected = [ - (fileutils.parent_directory(test_file), [], ['unicode.zip']) + (fileutils.parent_directory(test_file), [], ['f']) + ] + assert expected == result + + def test_fileutils_walk_can_walk_an_empty_dir(self): + test_dir = self.get_temp_dir() + result = list(fileutils.walk(test_dir)) + expected = [ + (test_dir, [], []) ] assert expected == result + def test_file_iter(self): + test_dir = self.get_test_loc('fileutils/walk') + base = self.get_test_loc('fileutils') + result = [f.replace(base, '') for f in fileutils.file_iter(test_dir)] + expected = [ + '/walk/f', + '/walk/unicode.zip', + '/walk/d1/f1', + '/walk/d1/d2/f2', + '/walk/d1/d2/d3/f3' + ] + assert expected == result + + def test_file_iter_can_iterate_a_single_file(self): + test_file = self.get_test_loc('fileutils/walk/f') + result = list(fileutils.file_iter(test_file)) + expected = [test_file] + assert expected == result + + def test_file_iter_can_walk_an_empty_dir(self): + test_dir = self.get_temp_dir() + result = list(fileutils.file_iter(test_dir)) + expected = [] + assert expected == result + class TestName(FileBasedTesting): test_data_dir = os.path.join(os.path.dirname(__file__), 'data') From f8174022b0052cf71d46c0a452f08816a0e7b540 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:08:49 +0200 Subject: [PATCH 010/436] Do not ignore files with single letter names. * this was due to a pattern with [] --- src/commoncode/ignore.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commoncode/ignore.py b/src/commoncode/ignore.py index 423ec66a73c..17a58787c4c 100644 --- a/src/commoncode/ignore.py +++ b/src/commoncode/ignore.py @@ -88,7 +88,7 @@ def get_ignores(location, include_defaults=True): '.journal': 'Default ignore: MacOSX DMG/HFS+ artifact', '.journal_info_block': 'Default ignore: MacOSX DMG/HFS+ artifact', '.Trashes': 'Default ignore: MacOSX DMG/HFS+ artifact', - '[HFS+ Private Data]': 'Default ignore: MacOSX DMG/HFS+ artifact', + '\[HFS+ Private Data\]': 'Default ignore: MacOSX DMG/HFS+ artifact private data', } ignores_Windows = { From 0fe971e3929c6d4c6c7a9e291ac0f0adb596ae24 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:08:49 +0200 Subject: [PATCH 011/436] Do not ignore files with single letter names. * this was due to a pattern with [] --- tests/commoncode/commoncode/test_ignore.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/commoncode/commoncode/test_ignore.py b/tests/commoncode/commoncode/test_ignore.py index e320b38c686..c2b3c8f0c36 100644 --- a/tests/commoncode/commoncode/test_ignore.py +++ b/tests/commoncode/commoncode/test_ignore.py @@ -176,3 +176,10 @@ def test_skip_vcs_files_and_dirs(self): ('/vcs/vssver.scc', 'Default ignore: Visual Source Safe artifact'), ] assert sorted(expected) == sorted(result) + + def test_default_ignore_does_not_skip_one_char_names(self): + # use fileset directly to work on strings not locations + from commoncode import fileset + tests = [c for c in 'HFS+ Private Data'] + 'HFS+ Private Data'.split() + for test in tests: + assert False == fileset.match(test, includes=ignore.default_ignores, excludes={}) From 9048c36a3aab9098c262c448b10a6d98f4644fa8 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:19:01 +0200 Subject: [PATCH 012/436] Cosmerics. --- src/commoncode/testcase.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index 058ab65a8ca..c95be5266ee 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -43,7 +43,6 @@ from commoncode import filetype - class EnhancedAssertions(TestCaseClass): """ Common new new assertions made better or simpler. @@ -194,7 +193,7 @@ def remove_vcs(self, test_dir): for vcsroot, vcsdirs, vcsfiles in os.walk(test_dir): for vcsfile in vcsdirs + vcsfiles: vfile = os.path.join(vcsroot, vcsfile) - fileutils.chmod(vfile, fileutils.RW) + fileutils.chmod(vfile, fileutils.RW, recurse=False) shutil.rmtree(os.path.join(root, vcs_dir), False) # editors temp file leftovers @@ -275,7 +274,7 @@ def extract_tar(location, target_dir, verbatim=False): def tar_can_extract(tarinfo, verbatim): """ Return True if a tar member can be extracted to handle OS specifics. - If verbatim is True, always return True. + If verbatim is True, always return True. """ if tarinfo.ischr(): # never extract char devices From 19cc333dceaba4a51418aba03fe72586a09ac572 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:20:20 +0200 Subject: [PATCH 013/436] Cleanup path resolver. --- src/commoncode/paths.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/commoncode/paths.py b/src/commoncode/paths.py index a4bf71eab61..f05c450ad1d 100644 --- a/src/commoncode/paths.py +++ b/src/commoncode/paths.py @@ -47,22 +47,28 @@ def resolve(path): be further resolved by "escaping" the provided path tree, it is replaced by the string 'dotdot'. """ - slash, dot = (u'/', u'.') if isinstance(path, unicode) else ('/', '.') + slash, dot, dotdot = '/', '.', 'dotdot' + if isinstance(path, unicode): + slash, dot, dotdot = u'/', u'.', u'dotdot' + + if not path: + return dot + path = path.strip() if not path: return dot path = fileutils.as_posixpath(path) - path = path.strip('/') - segments = [s.strip() for s in path.split('/')] + path = path.strip(slash) + segments = [s.strip() for s in path.split(slash)] # remove empty (// or ///) or blank (space only) or single dot segments segments = [s for s in segments if s and s != '.'] path = slash.join(segments) # resolves .. path = posixpath.normpath(path) # replace .. with literal dotdot - segments = path.split('/') - segments = ['dotdot' if s == '..' else s for s in segments] + segments = path.split(slash) + segments = [dotdot if s == '..' else s for s in segments] path = slash.join(segments) return path From af950fd725dbdb00cba07a04ec4c38b83362c028 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:23:02 +0200 Subject: [PATCH 014/436] Recurse chmod explicitly when needed. --- src/commoncode/command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commoncode/command.py b/src/commoncode/command.py index 5b0ba1a0d04..d7b01360ddf 100644 --- a/src/commoncode/command.py +++ b/src/commoncode/command.py @@ -197,14 +197,14 @@ def get_bin_lib_dirs(base_dir): bin_dir = os.path.join(base_dir, 'bin') if os.path.exists(bin_dir): - fileutils.chmod(bin_dir, fileutils.RX) + fileutils.chmod(bin_dir, fileutils.RX, recurse=True) else: bin_dir = None lib_dir = os.path.join(base_dir, 'lib') if os.path.exists(lib_dir): - fileutils.chmod(bin_dir, fileutils.RX) + fileutils.chmod(bin_dir, fileutils.RX, recurse=True) else: # default to bin for lib if it exists lib_dir = bin_dir or None @@ -264,7 +264,7 @@ def get_locations(cmd, root_dir, bin_dir, lib_dir = get_bin_lib_dirs(base_dir) cmd_loc = os.path.join(bin_dir, cmd) if os.path.exists(cmd_loc): - fileutils.chmod(cmd_loc, fileutils.RX) + fileutils.chmod(cmd_loc, fileutils.RX, recurse=False) return cmd_loc, bin_dir, lib_dir else: # we just care for getting the dirs and grab the first one From 4d658ec52d77995bfa32ebb6709ae98578f90d3b Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:28:20 +0200 Subject: [PATCH 015/436] Recurse chmod explicitly when needed. --- tests/commoncode/commoncode/test_filetype.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commoncode/commoncode/test_filetype.py b/tests/commoncode/commoncode/test_filetype.py index 8c52addd1bd..49dab9bcfb1 100644 --- a/tests/commoncode/commoncode/test_filetype.py +++ b/tests/commoncode/commoncode/test_filetype.py @@ -96,7 +96,7 @@ def test_is_readable_is_writeable_file(self): make_non_writable(test_file) assert not filetype.is_writable(test_file) finally: - fileutils.chmod(base_dir, fileutils.RW) + fileutils.chmod(base_dir, fileutils.RW, recurse=True) def test_is_readable_is_writeable_dir(self): base_dir = self.get_test_loc('filetype/readwrite', copy=True) @@ -120,7 +120,7 @@ def test_is_readable_is_writeable_dir(self): assert filetype.is_writable(test_dir) # finally finally: - fileutils.chmod(base_dir, fileutils.RW) + fileutils.chmod(base_dir, fileutils.RW, recurse=True) class CountTest(FileBasedTesting): From 670ec6dabeba411213b21eb94c930a9dcbdb73b7 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:28:47 +0200 Subject: [PATCH 016/436] Recurse chmod explicitly when needed. --- tests/commoncode/commoncode/test_fileutils.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 7b05a1e72da..196200132c7 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -77,7 +77,7 @@ def test_chmod_read_write_recursively_on_dir(self): if on_posix: assert filetype.is_executable(test_dir2) finally: - fileutils.chmod(test_dir, fileutils.RW) + fileutils.chmod(test_dir, fileutils.RW, recurse=True) def test_chmod_read_write_non_recursively_on_dir(self): test_dir = self.get_test_loc('fileutils/executable', copy=True) @@ -107,7 +107,7 @@ def test_chmod_read_write_non_recursively_on_dir(self): # windows is different assert filetype.is_writable(test_dir) finally: - fileutils.chmod(test_dir, fileutils.RW) + fileutils.chmod(test_dir, fileutils.RW, recurse=True) def test_chmod_read_write_file(self): test_dir = self.get_test_loc('fileutils/executable', copy=True) @@ -121,7 +121,7 @@ def test_chmod_read_write_file(self): assert filetype.is_readable(test_file) assert filetype.is_writable(test_file) finally: - fileutils.chmod(test_dir, fileutils.RW) + fileutils.chmod(test_dir, fileutils.RW, recurse=True) def test_chmod_read_write_exec_dir(self): test_dir = self.get_test_loc('fileutils/executable', copy=True) @@ -133,13 +133,13 @@ def test_chmod_read_write_exec_dir(self): assert not filetype.is_executable(test_file) make_non_writable(test_dir) - fileutils.chmod(test_dir, fileutils.RWX) + fileutils.chmod(test_dir, fileutils.RWX, recurse=True) assert filetype.is_readable(test_file) assert filetype.is_writable(test_file) if on_posix: assert filetype.is_executable(test_file) finally: - fileutils.chmod(test_dir, fileutils.RW) + fileutils.chmod(test_dir, fileutils.RW, recurse=True) def test_copyfile_does_not_keep_permissions(self): src_file = self.get_temp_file() @@ -155,8 +155,8 @@ def test_copyfile_does_not_keep_permissions(self): dest_file = join(dest, os.listdir(dest)[0]) assert filetype.is_readable(dest_file) finally: - fileutils.chmod(src_file, fileutils.RW) - fileutils.chmod(dest, fileutils.RW) + fileutils.chmod(src_file, fileutils.RW, recurse=True) + fileutils.chmod(dest, fileutils.RW, recurse=True) def test_copytree_does_not_keep_non_writable_permissions(self): src = self.get_test_loc('fileutils/exec', copy=True) @@ -184,8 +184,8 @@ def test_copytree_does_not_keep_non_writable_permissions(self): assert os.path.exists(dest_dir2) assert filetype.is_writable(dest_dir) finally: - fileutils.chmod(src, fileutils.RW) - fileutils.chmod(dst, fileutils.RW) + fileutils.chmod(src, fileutils.RW, recurse=True) + fileutils.chmod(dst, fileutils.RW, recurse=True) def test_copytree_copies_unreadable_files(self): src = self.get_test_loc('fileutils/exec', copy=True) @@ -216,8 +216,8 @@ def test_copytree_copies_unreadable_files(self): assert filetype.is_readable(dest_file2) finally: - fileutils.chmod(src, fileutils.RW) - fileutils.chmod(dst, fileutils.RW) + fileutils.chmod(src, fileutils.RW, recurse=True) + fileutils.chmod(dst, fileutils.RW, recurse=True) def test_delete_unwritable_directory_and_files(self): base_dir = self.get_test_loc('fileutils/readwrite', copy=True) @@ -237,7 +237,7 @@ def test_delete_unwritable_directory_and_files(self): fileutils.delete(test_dir) assert not os.path.exists(test_dir) finally: - fileutils.chmod(base_dir, fileutils.RW) + fileutils.chmod(base_dir, fileutils.RW, recurse=True) class TestFileUtils(FileBasedTesting): From 883f527332600768b4076d9df77f91cd89af2ac2 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 12 Jul 2015 23:51:00 +0200 Subject: [PATCH 017/436] Code simplfication and documentation * simplified copytree * changed chmod to not recurse by default and use explicit recurse * added debug logginf --- src/commoncode/fileutils.py | 109 ++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index a143e2de1df..913a3db47ab 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -35,11 +35,10 @@ import tempfile from commoncode import system -from commoncode import filetype from commoncode import text -from commoncode.system import on_windows +from commoncode import filetype from commoncode.filetype import is_rwx -from commoncode.filetype import is_file + # this exception is not available on posix try: @@ -47,6 +46,7 @@ except NameError: WindowsError = None # @ReservedAssignment +DEBUG = False logger = logging.getLogger(__name__) """ @@ -74,7 +74,7 @@ def create_dir(location): try: os.makedirs(location) - chmod(location, RW) + chmod(location, RW, recurse=False) # avoid multi-process TOCTOU conditions when creating dirs # the directory may have been created since the exist check @@ -159,13 +159,13 @@ def read_text_file(location, universal_new_lines=True): return text # -# PATHS AND NAMES +# PATHS AND NAMES MANIPULATIONS # def as_posixpath(location): """ Return a posix-like path using posix path separators (slash or "/") for a - path. This also converts Windows paths to look like posix paths that + `location` path. This converts Windows paths to look like posix paths that Python accepts gracefully on Windows for path handling. """ return location.replace(ntpath.sep, posixpath.sep) @@ -217,9 +217,10 @@ def file_extension(path): def splitext(path): """ - Return a tuple of (basename, extension) for a path. The basename is the - file name minus its extension. Return an empty extension string for a - directory. Not the same as os.path.splitext. + Return a tuple of strings (basename, extension) for a path. The basename is + the file name minus its extension. Return an empty extension string for a + directory. A directory is identified by ending with a path separator. Not + the same as os.path.splitext. """ base_name = '' extension = '' @@ -240,10 +241,12 @@ def splitext(path): return base_name or '', extension or '' # -# DIRECTORY WALKING +# DIRECTORY AND FILES WALKING/ITERATION # + ignore_nothing = lambda _: False + def walk(location, ignored=ignore_nothing): """ Walk location returning the same tuples as os.walk but with a different @@ -255,6 +258,9 @@ def walk(location, ignored=ignore_nothing): callable on files and directories returning True if it should be ignored. - location is a directory or a file: for a file, the file is returned. """ + if DEBUG: + ign = ignored(location) + logger.debug('walk: ignored:', location, ign) if not ignored(location): if filetype.is_file(location) : yield parent_directory(location), [], [file_name(location)] @@ -265,25 +271,21 @@ def walk(location, ignored=ignore_nothing): for name in os.listdir(location): loc = os.path.join(location, name) if filetype.is_special(loc) or ignored(loc): + if DEBUG: + ign = ignored(loc) + logger.debug('walk: ignored:', loc, ign) continue - + # special files and symlinks are always ignored if filetype.is_dir(loc): dirs.append(name) elif filetype.is_file(loc): files.append(name) - # else: - # special files and symlinks are always ignored - # pass yield location, dirs, files for dr in dirs: for tripple in walk(os.path.join(location, dr), ignored): yield tripple - # else: - # special files and symlinks are always ignored - # pass - def file_iter(location, ignored=ignore_nothing): """ @@ -294,70 +296,54 @@ def file_iter(location, ignored=ignore_nothing): if the location should be ignored. :return: an iterable of file locations. """ - for root, _dirs, files in walk(location, ignored): + for top, _dirs, files in walk(location, ignored): for f in files: - yield os.path.join(root, f) + yield os.path.join(top, f) -# # # COPY # -def copytree(src, dst, symlinks=False, ignore=None): +def copytree(src, dst): """ - Copy recursively the `src` directory to a non-existing `dst` directory. - Preserves: - - timestamps. - - symlinks if the optional `symlinks` argument is True. - + Copy recursively the `src` directory to the `dst` directory. If `dst` is an + existing directory, files in `dst` may be overwritten during the copy. + Preserve timestamps. Ignores: -`src` permissions: `dst` files are created with the default permissions. - - all special files such as FIFO or character devices + - all special files such as FIFO or character devices and symlinks. Raise an shutil.Error with a list of reasons. - The optional `ignore` argument is a callable called once for each copied - directory with src and names arguments. `src` is the current directory and - `names` is the list of `src` names as returned by os.listdir(). It must - return a list of names in the `src` directory that should be ignored from - the copy:: - callable(src, names) -> ignored_names - - Similar to and derived from Python shutil module. See fileutils.py.ABOUT - for details. + This function is similar to and derived from the Python shutil.copytree + function. See fileutils.py.ABOUT for details. """ if not filetype.is_readable(src): chmod(src, R, recurse=False) names = os.listdir(src) - ignored_names = set() - if ignore is not None: - ignored_names = ignore(src, names) or ignored_names + if not os.path.exists(dst): + os.makedirs(dst) - os.makedirs(dst) errors = [] errors.extend(copytime(src, dst)) for name in names: - if name in ignored_names: - continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) + # skip anything that is not a regular file, dir or link + if not filetype.is_regular(srcname): + continue + if not filetype.is_readable(srcname): chmod(srcname, R, recurse=False) try: - if not on_windows and symlinks and os.path.islink(srcname): - linkto = os.readlink(srcname) # @UndefinedVariable - os.symlink(linkto, dstname) # @UndefinedVariable - elif os.path.isdir(srcname): - copytree(srcname, dstname, symlinks, ignore) + if os.path.isdir(srcname): + copytree(srcname, dstname) elif filetype.is_file(srcname): copyfile(srcname, dstname) - else: - # skip anything that is not a regular file, dir or link - pass # catch the Error from the recursive copytree so that we can # continue with other files except shutil.Error, err: @@ -379,10 +365,10 @@ def copyfile(src, dst): """ if not filetype.is_regular(src): return - if os.path.isdir(dst): - dst = os.path.join(dst, os.path.basename(src)) if not filetype.is_readable(src): chmod(src, R, recurse=False) + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) shutil.copyfile(src, dst) copytime(src, dst) @@ -418,11 +404,12 @@ def copytime(src, dst): RWX = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR -def chmod(location, flags, recurse=True): +# FIXME: This was an expensive operation that used to recurse of the parent directory +def chmod(location, flags, recurse=False): """ - Update permissions for `location` with with `flags`. - `flags` is one of R, RW, RX or RWX with the same meaning as in chmod. - Update is done recursively if `recurse`. + Update permissions for `location` with with `flags`. `flags` is one of R, + RW, RX or RWX with the same semantics as in the chmod command. Update is + done recursively if `recurse`. """ if not location or not os.path.exists(location): return @@ -435,6 +422,8 @@ def chmod(location, flags, recurse=True): # and to be writable so we can change perms of files inside new_flags = RWX + # FIXME: do we really need to change the parent directory perms? + # FIXME: may just check them instead? parent = os.path.dirname(location) current_stat = stat.S_IMODE(os.stat(parent).st_mode) if not is_rwx(parent): @@ -457,7 +446,7 @@ def chmod_tree(location, flags): for d in dirs: chmod(os.path.join(top, d), flags, recurse=False) for f in files: - chmod(os.path.join(top, f), flags) + chmod(os.path.join(top, f), flags, recurse=False) # # DELETION @@ -470,7 +459,7 @@ def _rm_handler(function, path, excinfo): # @UnusedVariable """ if function == os.rmdir: try: - chmod(path, RW) + chmod(path, RW, recurse=True) shutil.rmtree(path, True) except Exception: pass @@ -497,7 +486,7 @@ def delete(location, _err_handler=_rm_handler): return if os.path.exists(location) or filetype.is_broken_link(location): - chmod(os.path.dirname(location), RW) + chmod(os.path.dirname(location), RW, recurse=False) if filetype.is_dir(location): shutil.rmtree(location, False, _rm_handler) else: From 8455f549bfcc67b3a0e1c80051079828634e0ed1 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Tue, 14 Jul 2015 14:30:39 +0200 Subject: [PATCH 018/436] Cosmetics. --- src/commoncode/command.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commoncode/command.py b/src/commoncode/command.py index d7b01360ddf..fc4afc4328d 100644 --- a/src/commoncode/command.py +++ b/src/commoncode/command.py @@ -48,16 +48,14 @@ - a distributed scancode package is self-contained - a non technical user does not have any extra installation to do, in particular there is no compilation needed at installation time. - - we have few dependencies on the OS. - - - that we control closely the version of these executables and how they were + - we control closely the version of these executables and how they were built to ensure sanity, especially on Linux where several different (oftentimes older) version may exist in the path for a given distro. For instance this applies to tools such as 7z, binutils and file. """ -LOG = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # current directory is the root dir of this library curr_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -93,9 +91,9 @@ def execute(cmd, args, root_dir=None, cwd=None, env=None, to_files=False): # though we can execute command that just happen to be in the path shell = True if on_windows else False - LOG.debug('Executing command %(cmd)r as %(full_cmd)r with: env=%(env)r, ' - 'shell=%(shell)r, cwd=%(cwd)r, stdout=%(sop)r, stderr=%(sep)r.' - % locals()) + logger.debug('Executing command %(cmd)r as %(full_cmd)r with: env=%(env)r, ' + 'shell=%(shell)r, cwd=%(cwd)r, stdout=%(sop)r, stderr=%(sep)r.' + % locals()) proc = None try: From 53764fe43d3d270ea19a457a1542b765e755020a Mon Sep 17 00:00:00 2001 From: pombredanne Date: Tue, 14 Jul 2015 15:50:45 +0200 Subject: [PATCH 019/436] Enable logging --- src/commoncode/command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commoncode/command.py b/src/commoncode/command.py index fc4afc4328d..c7384f65f0f 100644 --- a/src/commoncode/command.py +++ b/src/commoncode/command.py @@ -56,6 +56,9 @@ """ logger = logging.getLogger(__name__) +# import sys +# logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) +# logger.setLevel(logging.DEBUG) # current directory is the root dir of this library curr_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From e71aecdcc78f899d83186c7060bac8b484c7bdd2 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 15 Jul 2015 16:28:02 +0200 Subject: [PATCH 020/436] Fix for failing extractcode tests on Windowss --- tests/commoncode/commoncode/test_fileutils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 196200132c7..2a5e47d9c7c 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -38,6 +38,7 @@ from commoncode import filetype from commoncode import fileutils +from commoncode.fileutils import as_posixpath class TestPermissions(FileBasedTesting): @@ -317,7 +318,7 @@ def test_os_walk_with_unicode_path(self): def test_fileutils_walk(self): test_dir = self.get_test_loc('fileutils/walk') base = self.get_test_loc('fileutils') - result = [(t.replace(base, ''), d, f,) for t, d, f in fileutils.walk(test_dir)] + result = [(as_posixpath(t.replace(base, '')), d, f,) for t, d, f in fileutils.walk(test_dir)] expected = [ ('/walk', ['d1'], ['f', 'unicode.zip']), ('/walk/d1', ['d2'], ['f1']), @@ -357,7 +358,7 @@ def test_fileutils_walk_can_walk_an_empty_dir(self): def test_file_iter(self): test_dir = self.get_test_loc('fileutils/walk') base = self.get_test_loc('fileutils') - result = [f.replace(base, '') for f in fileutils.file_iter(test_dir)] + result = [as_posixpath(f.replace(base, '')) for f in fileutils.file_iter(test_dir)] expected = [ '/walk/f', '/walk/unicode.zip', @@ -369,7 +370,7 @@ def test_file_iter(self): def test_file_iter_can_iterate_a_single_file(self): test_file = self.get_test_loc('fileutils/walk/f') - result = list(fileutils.file_iter(test_file)) + result = [as_posixpath(f) for f in fileutils.file_iter(test_file)] expected = [test_file] assert expected == result From bc3448e255e023db3b5d76bae789aa153c7fb9b1 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 15 Jul 2015 22:22:41 +0200 Subject: [PATCH 021/436] Fixed extraction issue on Windows. Use os.path and abspath. --- tests/commoncode/commoncode/test_fileutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 2a5e47d9c7c..8cea3a4299d 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -371,7 +371,7 @@ def test_file_iter(self): def test_file_iter_can_iterate_a_single_file(self): test_file = self.get_test_loc('fileutils/walk/f') result = [as_posixpath(f) for f in fileutils.file_iter(test_file)] - expected = [test_file] + expected = [as_posixpath(test_file)] assert expected == result def test_file_iter_can_walk_an_empty_dir(self): From 1a82ad37be77fb0181ed8f53a921b96ed5a78c43 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Fri, 17 Jul 2015 23:10:06 +0200 Subject: [PATCH 022/436] New memoize_to_attribute decorator * Use to simplify caching methods results to instance variables * also added doctests --- src/commoncode/functional.py | 80 ++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/src/commoncode/functional.py b/src/commoncode/functional.py index 9b1cb73708d..13b6d368e4a 100644 --- a/src/commoncode/functional.py +++ b/src/commoncode/functional.py @@ -62,18 +62,90 @@ def flatten(seq): def memoize(fun): """ - Decorate fun function args and cache return values. Arguments must be + Decorate fun function and cache return values. Arguments must be hashable. kwargs are not handled. Used to speed up some often executed functions. + Usage example:: + + >>> @memoize + ... def expensive(*args, **kwargs): + ... print('Calling expensive with', args, kwargs) + ... return 'value expensive to compute' + repr(args) + >>> expensive(1, 2) + Calling expensive with (1, 2) {} + 'value expensive to compute(1, 2)' + >>> expensive(1, 2) + 'value expensive to compute(1, 2)' + >>> expensive(1, 2, a=0) + Calling expensive with (1, 2) {'a': 0} + 'value expensive to compute(1, 2)' + >>> expensive(1, 2, a=0) + Calling expensive with (1, 2) {'a': 0} + 'value expensive to compute(1, 2)' + >>> expensive(1, 2) + 'value expensive to compute(1, 2)' + >>> expensive(1, 2, 5) + Calling expensive with (1, 2, 5) {} + 'value expensive to compute(1, 2, 5)' + + The expensive function returned value will be cached based for each args + values and computed only once in its life. Call with kwargs are not cached """ memos = {} + @functools.wraps(fun) - def memoized(*args): - args = tuple(tuple(arg) if isinstance(arg, ListType) - else arg for arg in args) + def memoized(*args, **kwargs): + # calls with kwargs are not handled and not cached + if kwargs: + return fun(*args, **kwargs) + # convert any list arg to a tuple + args = tuple(tuple(arg) if isinstance(arg, ListType) else arg + for arg in args) try: return memos[args] except KeyError: memos[args] = fun(*args) return memos[args] + return functools.update_wrapper(memoized, fun) + + +def memoize_to_attribute(attr_name, _test=False): + """ + Decorate a method and cache return values in attr_name of the parent object. + Used to speed up some often called methods that cache their values in + instance variables. + Usage example:: + + >>> class Obj(object): + ... def __init__(self): + ... self._expensive = None + ... @property + ... @memoize_to_attribute('_expensive') + ... def expensive(self): + ... print('Calling expensive') + ... return 'value expensive to compute' + >>> o=Obj() + >>> o.expensive + Calling expensive + 'value expensive to compute' + >>> o.expensive + 'value expensive to compute' + >>> o.expensive + 'value expensive to compute' + + The Obj().expensive property value will be cached to attr_name + self._expensive and computed only once in the life of the Obj instance. + """ + def memoized_to_attr(meth): + @functools.wraps(meth) + def wrapper(self, *args, **kwargs): + if getattr(self, attr_name) is None: + res = meth(self, *args, **kwargs) + setattr(self, attr_name, res) + else: + res = getattr(self, attr_name) + return res + return wrapper + + return memoized_to_attr From 1141c741da571bd72a08d290cf9120ff5edb2643 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Fri, 24 Jul 2015 09:44:29 +0200 Subject: [PATCH 023/436] Ignore version control artifacts by default when scanning. #35 --- .../commoncode/commoncode/data/ignore/vcs.tgz | Bin 5555 -> 5610 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/commoncode/commoncode/data/ignore/vcs.tgz b/tests/commoncode/commoncode/data/ignore/vcs.tgz index b46c03bbf4813bb0767f6cc6a0921fc19d0cd0d3..866cdf5478359f2043c4b5aaf0af8cb3d1e09b1f 100644 GIT binary patch literal 5610 zcmX|FdmvQ#`&POkDP8EclM+RRR4DCfyHm2OqWdJtN|{R1jicP!2wSX>CY9{U#FAU& zB-ao{OA@2Tty~6U%ZOp9!EgP|(lkFLX2+0v7sD{c5i)k>L&_sNl$dq{Q!AHRG&etg zXHm$)Wy{g+gDaY)J6ubXSsypT`r0Md$c_w&K=vxE?Ol)rTStW%{|0Qhv?&Wo5X!DT=Yh$%$A0nIiI(__dIZJKRAa`l+wymYy^&|LfIi_Fm z6W*UN{fTyU=+XV9Q5}cW%CDZ=xOLWweBwf##nwiAqj~_+Qu`Y!PgFpD%iD%h*#!SV zG5621g|M4GC(p^>APjasM25~wP>EI8p4f97SyYSTvz_?8^> z^rAwe0av>?8v^ph80 zz377896XE%AB&}F|3XAy%pgv7WaN)Kp3T`=0UIB{wy{HWh{HH)#th!V;}Y3u%meVX zxCeXoQx7%_+v#I7-#|C~=})rP*rT+xHO=?dP5_CW7k4DaZc^1%mr7}b1pnP1TA zz6$0`B_C8-*`na8_h756OCd%u$dwUhdf*Wx!t|Mx?*Q@QX=$OEGb%re0w&NewMTgC zypf)~DI{^!0VYcDNIL({eZa{v7s}-UYA9#C%4l)x<+ZmpADB_B;uj+J>o!tHlSY;# zKFM#wuA6v{=wSRv`GYI4$tXa=-F|Tv_*QxqD#RsHsqx|gG*OL`3hVnuOa(ox-CsGD zn}8eGDBz>cQtN=%QfYID1e*#SGCxwdrqOdc$DyF0X;;s*i2I;cG=2mk{blT?t9%d} zJh}7il3(;7GQXu@xzovue8EKW&Ck2%LQHgy!MbTC=hM-~AS3pN3yHlGJSyTJHHEm}l%K`jW+1J#8X2uaH|`@4 zT#jA;KtMU|OhkrbPz;T|pa#EnG#ckV`xk905=nj1uxi+3>P)dcQsK^kH4>#!K4B5W ztXYGG&X$OOnK1Wtxk|aM>c=095wn}DC9pQn5+7sgOyl`@s|AN8pJ`ao zlQoYbnpT*JuN*yuy0k=i0cCy12*gje}$|JalM zbYiZ#5X1mulK?*3F%MoN_M9KQFu{K*@+g2^Ow%7hk6r8i_cHr%imPuOfy?K3_J2L4 zf$?>@u)47uZZiPg23qXJ9v9wz!H~xYhbHj(YNoJgwBbAu&X^Kfy{5F3sEve& z&y32#Y;BIm8r24A7(D)-xMbMK!(``y=SblA5GkXK1c$9h{ga;s&CVhxpgnR!aX3-#`x5R@x?S;#!uw&5`Mgw;ri3*Qp-TFs++3e|CPifz; z1Ls`0HLe{pzA1#YdLW=U3xmH3qi0;(u{-Hhn1yv*YC+qP50z?tQNwy&(~4pvVfHo} zhBD$*FqwAFA)gjupLss38?lP?Nd~NT1h~~CqWX&v8tjL# z2r)aHUTB*JIRTL=QDJ7U^&6)sDh~<*e>aAxjN@gC=cdqUB!SBr&{5N+XS@REJ?z@w z+3Rp~q3vJ$=l4WyQCwM*P#%Aq|-w4`kP+d!S9D`Sq`)%i_Ij-m9H> zQJA(mi}xw2`cUcJpjvQ-GNy#?+&Lad{S)W990;-$#3=bnSg~OHG7(e8HFjkfX+~U%@>{L2TE( ziKlpX9?H#Dk1@zJT3qXTaLmag8he|&Uxux@hi)yEv4l{^V1q)nvG+hw0W;`1pOmbc zqi4J{b9cJEBjGxSR)97MoN|h$BVG4_&_}V>~{;;67W+58eL(uqOx3}+k zYbmK)*Z+9<&viPnZx8&j=Z_^%k8hn^v%3txrC@Jo;Nc6k0)$-?Xrn zh`>atLO;{~+S8A&SNj8;qjMZj-r4Qg;MCBas>G&A?gofZjx8U~8+2jm5D@5xa=NDC zG=Z1=O72~rQn@eb7;n9X&838U8dfD0VehT8>o?^0lC|fmL76Hw2-Y>=0+=R+__3ei zhCCv$5W4<_9j}?}g?YE48?BpkkGbBh%3HH6w#_K<^kK6k-wt~^W_fKJY4}5!+1}l! z5%Y5DYqgHlN*nk1$67WA8*1QTj7BZ|KL{4pK0?Jx0T`2-QMtEr@lywfpLZ;Gm~6A8 zC49oQr9Ztuy>Dn`v-Q!avY9Mn$KEsIJPI!hvHjyg63=B_hl^G2zM#sI8GreLO&ek{ zJUiTlyJetI5urb<0Qs`7CjTP)jeIbVg)bx{HZ6j?DsRMTv?4*Q=HcdI;Y^mL zUHyB1!a0vC7k)FY4=>A7f35bXZ@kXKRjCPFZuL0e6;UiketwfcjR0|iB*gaaa@g4k zxey(Aglvp2NRSyGJw$Gqi!VQeVuJlBAA>GD9of@ZzvZ~lZGTA0^75>hjHlG5l$~uh zj+O3$y)S!7wgBnO`IQ!3kzg~yP+uc@$gKGzPa4v1Yp^&)cTsvn%6^c)Pij9# z$*r!usL5-pUL|;ju8cW0^W^^0-jr;W^yn(<6>?;r^H=c?(aScBH{X#lt3@yUlj6`p z$IN}@dz$xs%U<@Y-+K{peKx&~m}G>oE<(YwlH*Ks$a!DW7R#A6#dj=QJ8MtE;YP)d zEZgYSHBE6%YhB9@Znmc8?(PI_A#r=Ovce1U54`lUd-I9MEmj} zKX0I8@(BKg@|hzS;6H27gFpu;g4hBGrcJ;+0n~DF(mjzA_6+i7I3}WG6*1mqEyaDa z3O}8a&|mBv0B1@InP|}gOoVvF+ukjF{#ge~(ndsPCrHg!EGw32X@?I+^iZXqM9ble zjn&puX>E0&jkM6L09UVJ=tktCB6ktK^m&%!8PP{5RdDE&g`S=(koF7i^%MP!$ge&M zm>T_i(K4{8svc|5jz2jIi)c3qsF7CCNf0VxF;q_L!RJ=JjqW3#(p(C_mN|uHSz6$G z`s@(|xt8lEg=c^;}y6>(VKu~z&EBUAjL zkLW#YS^WfNK+^F6?pT`ARFUv=p^c~sbEV2J;PcURTHK#_g%d9m>X8jH{zI-b7OIHl zVD2&q?zdsPB6WD(NmckL0T74{Fj2*q9~*g>>9qgA=^+2K1Q&3(=dR~FFrQ>l`e7b&|s}rOB6l3w0L)RI?#g;Z;AXlqMxwONPaZ6!?T6s^1Y< z5o7RQA);v11*`>r?H&@3qvQJwcB*s$V}MnAHORVo<^tNUO%UNqB`@W_1goAg=|Tm6 z9VF4-!$kc^C@#UCM(4{eOz1tl;}<~}e&}2~u03i=48OU}Ri!6qQS@e%+{Tlp9cYLb zCo0727tz)x?F^_g6d-y6<{$j#PnSd8vC)dP4#VKANT1|uw27F>C{lW~I5Jjt&N?4y z7~qm?B4l`@fgZBT4>^cXB|g+!)mY6v@|x_Ld?F?y5GX$ScS zRZdurvvx+b=)b`<;ydE!O#wXaIXLYf9_>w;lsMoWzad`2QJqUb)q%ux2)#c2E9B0U zHXj&>Ds7{0UFt017e-#uC+6o|3zx801}WG~XT64`5dLVxbh2@t1Y?lec^u&4v}5PT z(kSidR<`@ZJ*dyVZa2vNVPY*InWt_S3tsnNArtl+M7Wq#Tx9n0W;)Z^19+DGif5kn z9bbCNTi}L-&A4szcId0*t$8XoN~=rRpJjDaRJh92k936`v5%06nGfyYcu)pCjo(}Po=%&zsqGS(zn@R???%GRAxzoy&&tao--JV_R>VCg?9YaCb~XXcBT87?BOi03D!0|g z@M>K(Usp(U=E5JN_ai2ck?QpCxE`(XMj6L^*0YvQqn1-{S4OCcalq=w1E5Nk&i@VY z;l*c*GKEQOnN$fXLtw4L>D!v?u$AIm@ws!|bZUUJbm!VqtMCtN|BhOD_bZhYbo20y zsd3BVBzYTjI^y}0zffF_Ay%`TObbgWvRI{nY6Y&-j%6;S^`zy&GFN-++ zhKTCw(JOr*-(~?8y>K`ZK05Zm4j%Q+mKa>}AQ$X4g&6bn3CT_4ou4~=N#21YOw5-> rb&28Zci2wlyi5bEq3AKWDHE-G2aOX2n2Pd~&gSTk_z6|EtE&7T9G26^ literal 5555 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 NXz-b#v_o0x{{V}Dqm%#u From 16e410c11e9cb83322fe9161bb86e4299f0e14bf Mon Sep 17 00:00:00 2001 From: pombredanne Date: Thu, 30 Jul 2015 17:32:22 +0200 Subject: [PATCH 024/436] Basic support for file info collection for #43 --- src/commoncode/filetype.py | 15 ++++++++------- src/commoncode/fileutils.py | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/commoncode/filetype.py b/src/commoncode/filetype.py index bcbc8dffe62..41bb87167b1 100644 --- a/src/commoncode/filetype.py +++ b/src/commoncode/filetype.py @@ -102,22 +102,23 @@ def get_link_target(location): # Map of type checker function -> short type code # The order of types check matters: link -> file -> directory -> special -TYPES = OrderedDict([(is_link, 'l'), - (is_file, 'f'), - (is_dir, 'd'), - (is_special, 's')]) +TYPES = OrderedDict([(is_link, ('l', 'link',)), + (is_file, ('f', 'file',)), + (is_dir, ('d', 'directory',)), + (is_special, ('s', 'special',))]) -def get_type(location): +def get_type(location, short=True): """ Return the type of the `location` or None if it does not exist. + Return the short form (single character) or long form if short=False """ if location: for type_checker in TYPES: tc = type_checker(location) if tc: - return TYPES[type_checker] - + short_form, long_form = TYPES[type_checker] + return short and short_form or long_form def is_readable(location): """ diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index 913a3db47ab..ae523a7b793 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -296,9 +296,25 @@ def file_iter(location, ignored=ignore_nothing): if the location should be ignored. :return: an iterable of file locations. """ - for top, _dirs, files in walk(location, ignored): + return resource_iter(location, ignored, with_dirs=False) + + +def resource_iter(location, ignored=ignore_nothing, with_dirs=True): + """ + Return an iterable of resource at `location` recursively. + + :param location: a file or a directory. + :param ignored: a callable accepting a location argument and returning True + if the location should be ignored. + :param with_dirs: If True, include the directory together with files. + :return: an iterable of file and directory locations. + """ + for top, dirs, files in walk(location, ignored): for f in files: yield os.path.join(top, f) + if with_dirs: + for d in dirs: + yield os.path.join(top, d) # # COPY From dc1b6e7c14d2df9da28f836937e58e23bafdf0ed Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 5 Aug 2015 18:10:10 +0200 Subject: [PATCH 025/436] Added new dir_iter function --- src/commoncode/fileutils.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index ae523a7b793..f7eee3a6c82 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -257,6 +257,7 @@ def walk(location, ignored=ignore_nothing): - optionally ignore files and directories by invoking the `ignored` callable on files and directories returning True if it should be ignored. - location is a directory or a file: for a file, the file is returned. + TODO: consider using scandir for speed-ups """ if DEBUG: ign = ignored(location) @@ -299,23 +300,37 @@ def file_iter(location, ignored=ignore_nothing): return resource_iter(location, ignored, with_dirs=False) -def resource_iter(location, ignored=ignore_nothing, with_dirs=True): +def dir_iter(location, ignored=ignore_nothing): """ - Return an iterable of resource at `location` recursively. + Return an iterable of directories at `location` recursively. + + :param location: a directory. + :param ignored: a callable accepting a location argument and returning True + if the location should be ignored. + :return: an iterable of directory locations. + """ + return resource_iter(location, ignored, with_files=False) + + +def resource_iter(location, ignored=ignore_nothing, with_files=True, with_dirs=True): + """ + Return an iterable of resources at `location` recursively. :param location: a file or a directory. :param ignored: a callable accepting a location argument and returning True if the location should be ignored. - :param with_dirs: If True, include the directory together with files. + :param with_dirs: If True, include the directories. + :param with_files: If True, include the files. :return: an iterable of file and directory locations. """ + assert with_dirs or with_files, "One or both of 'with_dirs' and 'with_files' is required" for top, dirs, files in walk(location, ignored): - for f in files: - yield os.path.join(top, f) + if with_files: + for f in files: + yield os.path.join(top, f) if with_dirs: for d in dirs: yield os.path.join(top, d) - # # COPY # From 49f379a9b6c2f70150a6744c4107ae8cdf05f50d Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 5 Aug 2015 21:05:55 +0200 Subject: [PATCH 026/436] Cosmetics --- src/commoncode/filetype.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commoncode/filetype.py b/src/commoncode/filetype.py index 41bb87167b1..55ef157428e 100644 --- a/src/commoncode/filetype.py +++ b/src/commoncode/filetype.py @@ -120,6 +120,7 @@ def get_type(location, short=True): short_form, long_form = TYPES[type_checker] return short and short_form or long_form + def is_readable(location): """ Return True if the file at location has readable permission set. From 445b27a9a5570fd252e8e02ffa2c3c5f2cd924ee Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 8 Aug 2015 11:52:45 +0200 Subject: [PATCH 027/436] Enhanced testcase by extracting file-related methods to a bare object * this allows using this object is bare py.test tests --- src/commoncode/testcase.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index c95be5266ee..13e47f7de92 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -114,10 +114,11 @@ def get_test_loc(test_path, test_data_dir): return test_loc -class FileBasedTesting(EnhancedAssertions): +class FileDrivenTesting(object): """ Add support for handling test files and directories, including managing temporary test resources and doing file-based assertions. + This can be used as a standalone object if needed. """ test_data_dir = None @@ -200,6 +201,9 @@ def remove_vcs(self, test_dir): map(os.remove, [os.path.join(root, file_loc) for file_loc in files if file_loc.endswith('~')]) + +class FileBasedTesting(EnhancedAssertions, FileDrivenTesting): + def as_line_list(self, list_or_file, sort=False, skip_firstline=False): """ Given a list of file path as an input, return a list of text lines From e264bbb12dc6e39d3ec90c8c1677b39cec65c3f0 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sat, 8 Aug 2015 17:15:39 +0200 Subject: [PATCH 028/436] Esnured FileDrivenTesting also does archive extraction --- src/commoncode/testcase.py | 73 +++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index 13e47f7de92..4410ffbcb08 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -202,42 +202,6 @@ def remove_vcs(self, test_dir): for file_loc in files if file_loc.endswith('~')]) -class FileBasedTesting(EnhancedAssertions, FileDrivenTesting): - - def as_line_list(self, list_or_file, sort=False, skip_firstline=False): - """ - Given a list of file path as an input, return a list of text lines - suitable for comparison. Optionally skip the first line (for CSV - comparisons) and sort the list. - """ - L = [] - if isinstance(list_or_file, basestring): - L = open(list_or_file, 'rb').readlines() - elif isinstance(list_or_file, (list, tuple,)): - L = list_or_file - else: - raise Exception('unsupported object type: ' - 'must be a list, tuple or file name') - - if skip_firstline and L: - L = L[1:] - if sort: - L = sorted(L) - return L - - def failUnlessFilesLinesEqual(self, expected_list_or_file, - result_list_or_file, - msg=None, sort=False, skip_firstline=False): - """ - Check equality of two lists of lines or files lines content. - """ - expected = self.as_line_list(expected_list_or_file, sort, - skip_firstline) - result = self.as_line_list(result_list_or_file, sort, skip_firstline) - self.failUnlessEqual(expected, result, msg) - - assertLinesEqual = failUnlessFilesLinesEqual - def __extract(self, test_path, extract_func=None, verbatim=False): """ Given an archive file identified by test_path relative @@ -275,6 +239,43 @@ def extract_tar(location, target_dir, verbatim=False): tar.extractall(target_dir, members=to_extract) +class FileBasedTesting(EnhancedAssertions, FileDrivenTesting): + + def as_line_list(self, list_or_file, sort=False, skip_firstline=False): + """ + Given a list of file path as an input, return a list of text lines + suitable for comparison. Optionally skip the first line (for CSV + comparisons) and sort the list. + """ + L = [] + if isinstance(list_or_file, basestring): + L = open(list_or_file, 'rb').readlines() + elif isinstance(list_or_file, (list, tuple,)): + L = list_or_file + else: + raise Exception('unsupported object type: ' + 'must be a list, tuple or file name') + + if skip_firstline and L: + L = L[1:] + if sort: + L = sorted(L) + return L + + def failUnlessFilesLinesEqual(self, expected_list_or_file, + result_list_or_file, + msg=None, sort=False, skip_firstline=False): + """ + Check equality of two lists of lines or files lines content. + """ + expected = self.as_line_list(expected_list_or_file, sort, + skip_firstline) + result = self.as_line_list(result_list_or_file, sort, skip_firstline) + self.failUnlessEqual(expected, result, msg) + + assertLinesEqual = failUnlessFilesLinesEqual + + def tar_can_extract(tarinfo, verbatim): """ Return True if a tar member can be extracted to handle OS specifics. From ed1dfb7e9c621d23dd2a1c6d04dbabd096f6a4a4 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Sun, 16 Aug 2015 15:26:12 +0200 Subject: [PATCH 029/436] Prefer while 1 over while True in loops for minute wins on Python 2. --- src/commoncode/fileutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index f7eee3a6c82..c00bb655e96 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -123,7 +123,7 @@ def file_chunks(file_object, chunk_size=1024): """ Yield a file piece by piece. Default chunk size: 1k. """ - while True: + while 1: data = file_object.read(chunk_size) if data: yield data From 563fbcce0197fbcc8a8881d27932d3e60e721e2b Mon Sep 17 00:00:00 2001 From: pombredanne Date: Wed, 19 Aug 2015 10:59:44 +0200 Subject: [PATCH 030/436] Added minimal support to download URLs --- src/commoncode/fetch.py | 84 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/commoncode/fetch.py diff --git a/src/commoncode/fetch.py b/src/commoncode/fetch.py new file mode 100644 index 00000000000..50c6aa1ed96 --- /dev/null +++ b/src/commoncode/fetch.py @@ -0,0 +1,84 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + +import logging + +import requests +from requests.exceptions import ConnectionError +from requests.exceptions import InvalidSchema + +from commoncode import fileutils +import os + + +logger = logging.getLogger(__name__) +# import sys +# logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) +# logger.setLevel(logging.DEBUG) + + +def download_url(url, file_name=None, verify=True): + """ + Return the temporary location of the file fetched at the remote url. Use + file_name if provided or create a file name base on the last url segment. If + verify is True, SSL certification is performed. Otherwise, no verification + is done but a warning will be printed. + """ + requests_args = dict(timeout=10, verify=verify) + file_name = file_name or fileutils.file_name(url) + + try: + response = requests.get(url, **requests_args) + except (ConnectionError, InvalidSchema) as e: + logger.error('fetch: Download failed for %(url)r' % locals()) + raise + + status = response.status_code + if status != 200: + msg = 'fetch: Download failed for %(url)r with %(status)r' % locals() + logger.error(msg) + raise Exception(msg) + + tmp_dir = fileutils.get_temp_dir(base_dir='fetch') + output_file = os.path.join(tmp_dir, file_name) + with open(output_file, 'wb') as out: + out.write(response.content) + + return output_file + + +def ping_url(url): + """ + Returns True is the URL is reachable. + """ + import urllib2 + + try: + urllib2.urlopen(url) + except Exception: + return False + else: + return True From b06c0f7ac5f91af5b0ffaaa1580734117aa28a27 Mon Sep 17 00:00:00 2001 From: pombredanne Date: Tue, 8 Dec 2015 10:54:28 +0100 Subject: [PATCH 031/436] Added new util to get pair chuncks from a sequence. * Also refined code and comments --- src/commoncode/functional.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/commoncode/functional.py b/src/commoncode/functional.py index 13b6d368e4a..54c5d3cc33f 100644 --- a/src/commoncode/functional.py +++ b/src/commoncode/functional.py @@ -24,14 +24,16 @@ from __future__ import absolute_import, print_function -from types import ListType, TupleType, GeneratorType import functools +from itertools import izip +from types import ListType, TupleType, GeneratorType def flatten(seq): """ - Flatten a sequence sub-sequences: either a tuple, list or generator - (generators will be consumed) are converted to a flat list of elements. + Flatten recursively a sequence and all its sub-sequences that can be tuples, + lists or generators (generators will be consumed): all are converted to a + flat list of elements. For example:: >>> flatten([7, (6, [5, [4, ['a'], 3]], 3), 2, 1]) @@ -52,14 +54,27 @@ def flatten(seq): for x in seq: if isinstance(x, (ListType, TupleType)): r.extend(flatten(x)) - # generator is not exposed as a built-in type by default elif isinstance(x, GeneratorType): - r.extend(flatten([y for y in x])) + r.extend(flatten(list(x))) else: r.append(x) return r +def pair_chunks(iterable): + """ + Return an iterable of chunks of elements pairs from iterable. The iterable + must contain an even number of elements or it will truncated. + + For example:: + >>> list(pair_chunks([1, 2, 3, 4, 5, 6])) + [(1, 2), (3, 4), (5, 6)] + >>> list(pair_chunks([1, 2, 3, 4, 5, 6, 7])) + [(1, 2), (3, 4), (5, 6)] + """ + return izip(*[iter(iterable)] * 2) + + def memoize(fun): """ Decorate fun function and cache return values. Arguments must be From 47d9df7b161494b13d7d827a68bea531727a90be Mon Sep 17 00:00:00 2001 From: pombredanne Date: Tue, 8 Dec 2015 11:44:00 +0100 Subject: [PATCH 032/436] Various refactorings and code cleanup * Ensured that plain asserts are used everywhere in tests. * Decreasing MAX_GAP to 25 in license detection. * Introduced the score for license detection functions. * Refined and cleaned debug code in license detection --- tests/commoncode/commoncode/test_date.py | 6 ++-- .../commoncode/commoncode/test_functional.py | 6 ++-- tests/commoncode/commoncode/test_hash.py | 6 ++-- tests/commoncode/commoncode/test_ignore.py | 8 ++--- tests/commoncode/commoncode/test_timeutils.py | 33 ++++++++++++------- tests/commoncode/commoncode/test_urn.py | 30 ++++++++--------- tests/commoncode/commoncode/test_version.py | 2 +- 7 files changed, 51 insertions(+), 40 deletions(-) diff --git a/tests/commoncode/commoncode/test_date.py b/tests/commoncode/commoncode/test_date.py index 02bbf0924a9..98269563cc0 100644 --- a/tests/commoncode/commoncode/test_date.py +++ b/tests/commoncode/commoncode/test_date.py @@ -48,7 +48,7 @@ def test_get_file_time1(self): open(test_file, 'w').close() now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') result = commoncode.date.get_file_mtime(test_file)[:10] - self.assertEqual(now[:10], result) + assert now[:10] == result def test_get_file_time2(self): test_file = self.get_temp_file() @@ -57,7 +57,7 @@ def test_get_file_time2(self): m_ts = (24 * 3600) * 134 + (24 * 3600 * 365) * 22 # setting modified time to expected values os.utime(test_file, (m_ts, m_ts)) - self.assertEqual(expected, commoncode.date.get_file_mtime(test_file)) + assert expected == commoncode.date.get_file_mtime(test_file) def test_get_file_time3(self): test_file = self.get_temp_file() @@ -65,4 +65,4 @@ def test_get_file_time3(self): # setting modified time to expected values expected = u'2011-01-06 14:35:00' os.utime(test_file, (1294324500, 1294324500)) - self.assertEqual(expected, commoncode.date.get_file_mtime(test_file)) + assert expected == commoncode.date.get_file_mtime(test_file) diff --git a/tests/commoncode/commoncode/test_functional.py b/tests/commoncode/commoncode/test_functional.py index 17774545de4..afc6004bebe 100644 --- a/tests/commoncode/commoncode/test_functional.py +++ b/tests/commoncode/commoncode/test_functional.py @@ -34,7 +34,7 @@ class TestFunctional(TestCase): def test_flatten(self): expected = [7, 6, 5, 4, 'a', 3, 3, 2, 1] test = flatten([7, (6, [5, [4, ["a"], 3]], 3), 2, 1]) - self.assertEqual(expected, test) + assert expected == test def test_flatten_generator(self): def gen(): @@ -43,9 +43,9 @@ def gen(): expected = [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] test = flatten(gen()) - self.assertEqual(expected, test) + assert expected == test def test_flatten_empties(self): expected = ['a'] test = flatten([[], (), ['a']]) - self.assertEqual(expected, test) + assert expected == test diff --git a/tests/commoncode/commoncode/test_hash.py b/tests/commoncode/commoncode/test_hash.py index 00d7838848f..0f740e6f079 100644 --- a/tests/commoncode/commoncode/test_hash.py +++ b/tests/commoncode/commoncode/test_hash.py @@ -39,9 +39,9 @@ class TestHash(FileBasedTesting): def test_get_hasher(self): h = commoncode.hash.get_hasher(160) - self.assertEqual('hvfkN_qlp_zhXR3cuerq6jd2Z7g=', h('a').b64digest()) - self.assertEqual('4MkDWJjdUvxlxBRUzsnE0mEb-zc=', h('aa').b64digest()) - self.assertEqual('fiQN50-x7Qj6CNOAY_amqRRiqBU=', h('aaa').b64digest()) + assert 'hvfkN_qlp_zhXR3cuerq6jd2Z7g=' == h('a').b64digest() + assert '4MkDWJjdUvxlxBRUzsnE0mEb-zc=' == h('aa').b64digest() + assert 'fiQN50-x7Qj6CNOAY_amqRRiqBU=' == h('aaa').b64digest() def test_hash_1(self): test_file = self.get_test_loc('hash/dir1/a.png') diff --git a/tests/commoncode/commoncode/test_ignore.py b/tests/commoncode/commoncode/test_ignore.py index c2b3c8f0c36..cf973b4b004 100644 --- a/tests/commoncode/commoncode/test_ignore.py +++ b/tests/commoncode/commoncode/test_ignore.py @@ -57,7 +57,7 @@ def check_default(self, test_dir, expected_message): @skipIf(on_mac, 'Return different result on Mac for reasons to investigate') def test_default_ignores_eclipse1(self): test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') - test_base = os.path.join(test_dir, 'eclipse') + test_base = os.path.join(test_dir, 'eclipse') test = os.path.join(test_base, '.settings') result = ignore.is_ignored(test, ignore.default_ignores, {}) @@ -65,7 +65,7 @@ def test_default_ignores_eclipse1(self): def test_default_ignores_eclipse2(self): test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') - test_base = os.path.join(test_dir, 'eclipse') + test_base = os.path.join(test_dir, 'eclipse') test = os.path.join(test_base, '.settings/somefile') result = ignore.is_ignored(test, ignore.default_ignores, {}) @@ -73,7 +73,7 @@ def test_default_ignores_eclipse2(self): def test_default_ignores_eclipse3(self): test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') - test_base = os.path.join(test_dir, 'eclipse') + test_base = os.path.join(test_dir, 'eclipse') test = os.path.join(test_base, '.project') result = ignore.is_ignored(test, ignore.default_ignores, {}) @@ -81,7 +81,7 @@ def test_default_ignores_eclipse3(self): def test_default_ignores_eclipse4(self): test_dir = self.extract_test_tar('ignore/excludes/eclipse.tgz') - test_base = os.path.join(test_dir, 'eclipse') + test_base = os.path.join(test_dir, 'eclipse') test = os.path.join(test_base, '.pydevproject') result = ignore.is_ignored(test, ignore.default_ignores, {}) diff --git a/tests/commoncode/commoncode/test_timeutils.py b/tests/commoncode/commoncode/test_timeutils.py index 0e8c8bd342e..e228115f536 100644 --- a/tests/commoncode/commoncode/test_timeutils.py +++ b/tests/commoncode/commoncode/test_timeutils.py @@ -22,11 +22,14 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. +from __future__ import absolute_import, print_function + from datetime import datetime from commoncode.testcase import FileBasedTesting from commoncode.timeutils import time2tstamp, tstamp2time, UTC + class TestTimeStamp(FileBasedTesting): def test_time2tstamp_is_path_safe_and_file_is_writable(self): @@ -51,41 +54,49 @@ def test_time2tstamp_tstamp2time_is_idempotent(self): dt = datetime.utcnow() ts = time2tstamp(dt) dt_from_ts = tstamp2time(ts) - self.assertEqual(dt, dt_from_ts) + assert dt == dt_from_ts def test_tstamp2time_format(self): import re ts = time2tstamp() pat = '^20\d\d-[0-1][0-9]-[0-3]\dT[0-2]\d[0-6]\d[0-6]\d.\d\d\d\d\d\d$' - self.assertTrue(re.match(pat, ts)) + assert re.match(pat, ts) def test_tstamp2time(self): dt_from_ts = tstamp2time('2010-11-12T131415.000016') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()) + def test_tstamp2time2(self): dt_from_ts = tstamp2time('20101112T131415.000016') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()) + def test_tstamp2time3(self): dt_from_ts = tstamp2time('20101112T131415.000016Z') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=16, tzinfo=UTC()) + def test_tstamp2time4(self): dt_from_ts = tstamp2time('2010-11-12T131415') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()) + def test_tstamp2time5(self): dt_from_ts = tstamp2time('2010-11-12T13:14:15') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()) + def test_tstamp2time6(self): dt_from_ts = tstamp2time('20101112T13:14:15') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()) + def test_tstamp2time7(self): dt_from_ts = tstamp2time('20101112T13:14:15Z') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()) + def test_tstamp2time8(self): dt_from_ts = tstamp2time('20101112T13:14:15Z') - self.assertEqual(datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=11, day=12, hour=13, minute=14, second=15, microsecond=0, tzinfo=UTC()) + def test_tstamp2time9(self): dt_from_ts = tstamp2time('2010-06-30T21:26:40.000Z') - self.assertEqual(datetime(year=2010, month=06, day=30, hour=21, minute=26, second=40, microsecond=0, tzinfo=UTC()), dt_from_ts) + assert dt_from_ts == datetime(year=2010, month=06, day=30, hour=21, minute=26, second=40, microsecond=0, tzinfo=UTC()) def test_tstamp2time_raise(self): self.assertRaises(ValueError, tstamp2time, '201011A12T13:14:15Z') diff --git a/tests/commoncode/commoncode/test_urn.py b/tests/commoncode/commoncode/test_urn.py index ff18e3da084..23d6988d214 100644 --- a/tests/commoncode/commoncode/test_urn.py +++ b/tests/commoncode/commoncode/test_urn.py @@ -33,23 +33,23 @@ class URNTestCase(TestCase): def test_encode_license(self): u1 = urn.encode('license', key='somekey') - self.assertEquals('urn:dje:license:somekey', u1) + assert 'urn:dje:license:somekey' == u1 def test_encode_owner(self): u1 = urn.encode('owner', name='somekey') - self.assertEquals('urn:dje:owner:somekey', u1) + assert 'urn:dje:owner:somekey' == u1 def test_encode_component(self): u1 = urn.encode('component', name='name', version='version') - self.assertEquals('urn:dje:component:name:version', u1) + assert 'urn:dje:component:name:version' == u1 def test_encode_component_no_version(self): u1 = urn.encode('component', name='name', version='') - self.assertEquals('urn:dje:component:name:', u1) + assert 'urn:dje:component:name:' == u1 def test_encode_license_with_extra_fields_are_ignored(self): u1 = urn.encode('license', key='somekey', junk='somejunk') - self.assertEquals('urn:dje:license:somekey', u1) + assert 'urn:dje:license:somekey' == u1 def test_encode_missing_field_raise_keyerror(self): with self.assertRaises(KeyError): @@ -66,51 +66,51 @@ def test_encode_unknown_object_type_raise_keyerror(self): def test_encode_component_with_spaces_are_properly_quoted(self): u1 = urn.encode('component', name='name space', version='version space') - self.assertEquals('urn:dje:component:name+space:version+space', u1) + assert 'urn:dje:component:name+space:version+space' == u1 def test_encode_leading_and_trailing_spaces_are_trimmed_and_ignored(self): u1 = urn.encode(' component ', name=' name space ', version=''' version space ''') - self.assertEquals('urn:dje:component:name+space:version+space', u1) + assert 'urn:dje:component:name+space:version+space' == u1 def test_encode_component_with_semicolon_are_properly_quoted(self): u1 = urn.encode('component', name='name:', version=':version') - self.assertEquals('urn:dje:component:name%3A:%3Aversion', u1) + assert 'urn:dje:component:name%3A:%3Aversion' == u1 def test_encode_component_with_plus_are_properly_quoted(self): u1 = urn.encode('component', name='name+', version='version+') - self.assertEquals('urn:dje:component:name%2B:version%2B', u1) + assert 'urn:dje:component:name%2B:version%2B' == u1 def test_encode_component_with_percent_are_properly_quoted(self): u1 = urn.encode('component', name='name%', version='version%') - self.assertEquals('urn:dje:component:name%25:version%25', u1) + assert 'urn:dje:component:name%25:version%25' == u1 def test_encode_object_type_case_is_not_significant(self): u1 = urn.encode('license', key='key') u2 = urn.encode('lICENSe', key='key') - self.assertEquals(u1, u2) + assert u1 == u2 def test_decode_component(self): u = 'urn:dje:component:name:version' parsed = ('component', {'name': 'name', 'version': 'version'}) - self.assertEqual(parsed, urn.decode(u)) + assert parsed == urn.decode(u) def test_decode_license(self): u = 'urn:dje:license:lic' parsed = ('license', {'key': 'lic'}) - self.assertEqual(parsed, urn.decode(u)) + assert parsed == urn.decode(u) def test_decode_org(self): u = 'urn:dje:owner:name' parsed = ('owner', {'name': 'name'}) - self.assertEqual(parsed, urn.decode(u)) + assert parsed == urn.decode(u) def test_decode_build_is_idempotent(self): u1 = urn.encode('component', owner__name='org%', name='name%', version='version%') m, f = urn.decode(u1) u3 = urn.encode(m, **f) - self.assertEqual(u1, u3) + assert u1 == u3 def test_decode_raise_exception_if_incorrect_prefix(self): with self.assertRaises(urn.URNValidationError): diff --git a/tests/commoncode/commoncode/test_version.py b/tests/commoncode/commoncode/test_version.py index 4e383c4a13f..49ed53b7f19 100644 --- a/tests/commoncode/commoncode/test_version.py +++ b/tests/commoncode/commoncode/test_version.py @@ -136,4 +136,4 @@ def test_version_hint(self): expected = data[path] if not expected.lower().startswith('v'): expected = 'v ' + expected - self.assertEqual(expected, version.hint(path)) + assert expected == version.hint(path) From 3eeab3e7d47d3d58485833025b824af734d4e138 Mon Sep 17 00:00:00 2001 From: Jillian Date: Tue, 29 Dec 2015 11:16:32 -0600 Subject: [PATCH 033/436] Refactored test for test_date.py --- tests/commoncode/commoncode/test_date.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/commoncode/commoncode/test_date.py b/tests/commoncode/commoncode/test_date.py index 98269563cc0..ed6d62af2fe 100644 --- a/tests/commoncode/commoncode/test_date.py +++ b/tests/commoncode/commoncode/test_date.py @@ -43,14 +43,18 @@ def test_secs_from_epoch_can_handle_micro_and_nano_secs(self): file_date = commoncode.date.get_file_mtime(test_file) commoncode.date.secs_from_epoch(file_date) - def test_get_file_time1(self): + def test_get_file_mtime_for_a_new_file(self): test_file = self.get_temp_file() open(test_file, 'w').close() + + def as_yyyymmdd(s): + return s[:10] + now = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') - result = commoncode.date.get_file_mtime(test_file)[:10] - assert now[:10] == result + result = commoncode.date.get_file_mtime(test_file) + assert as_yyyymmdd(now) == as_yyyymmdd(result) - def test_get_file_time2(self): + def test_get_file_mtime_for_a_modified_file(self): test_file = self.get_temp_file() open(test_file, 'w').close() expected = u'1992-05-09 00:00:00' @@ -59,7 +63,7 @@ def test_get_file_time2(self): os.utime(test_file, (m_ts, m_ts)) assert expected == commoncode.date.get_file_mtime(test_file) - def test_get_file_time3(self): + def test_get_file_mtime_for_a_modified_file_2(self): test_file = self.get_temp_file() open(test_file, 'w').close() # setting modified time to expected values From 77ede06d48ec97095684385a412bf04cbde6f4ca Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 11 Mar 2016 08:11:18 -0800 Subject: [PATCH 034/436] #86 Adding dictionary utilities and other common utilities --- src/commoncode/codec.py | 22 +-- src/commoncode/command.py | 4 +- src/commoncode/dict_utils.ABOUT | 9 ++ src/commoncode/dict_utils.LICENSE | 21 +++ src/commoncode/dict_utils.py | 250 ++++++++++++++++++++++++++++++ src/commoncode/filetype.py | 2 +- src/commoncode/testcase.py | 17 +- 7 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 src/commoncode/dict_utils.ABOUT create mode 100644 src/commoncode/dict_utils.LICENSE create mode 100644 src/commoncode/dict_utils.py diff --git a/src/commoncode/codec.py b/src/commoncode/codec.py index 2ac767c6362..5afe2da520a 100644 --- a/src/commoncode/codec.py +++ b/src/commoncode/codec.py @@ -30,11 +30,7 @@ padding = '/' -b85_symbols = ('0123456789' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - 'abcdefghijklmnopqrstuvwxyz' - '!#$%&()*+-;<=>?@^_`{|}~') - +b85_symbols = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~' len_b85_symbols = len(b85_symbols) @@ -86,10 +82,10 @@ def to_base85(num): def to_base10(s, b=36): """ - Convert a string s representing a number in base b back to an integer. + Convert a string s representing a number in base b back to an integer where base <= 85. + """ - assert b <= len(b85_symbols) and b >= 2, ('Base must be in range(2, %d)' - % (len(b85_symbols))) + assert b <= len(b85_symbols) and b >= 2, 'Base must be in range(2, %d)' % (len(b85_symbols)) # strip padding s = s.replace(padding, '') @@ -112,14 +108,19 @@ def num_to_bin(num): safely to a long: using structs does not work easily for this. """ binstr = [] + + # Zero is not encoded but returned as an empty value + if num == 0: + return '\x00' + while num > 0: # add the least significant byte value binstr.append(chr(num & 0xFF)) # shift the next byte to least significant and repeat num = num >> 8 - # reverse the list now to the most significant - # byte is at the start of ths string to speed decoding + # reverse the list now such that the most significant + # byte is at the start of this string to speed decoding return ''.join(reversed(binstr)) @@ -160,6 +161,5 @@ def urlsafe_b64decode(b64): def _encode(num): """ Encode a number (int or long) in url safe base64. - Used in simhash5 """ return b64encode(num_to_bin(num)) diff --git a/src/commoncode/command.py b/src/commoncode/command.py index c7384f65f0f..6672a87410c 100644 --- a/src/commoncode/command.py +++ b/src/commoncode/command.py @@ -76,7 +76,9 @@ def execute(cmd, args, root_dir=None, cwd=None, env=None, to_files=False): temporary files. Resolve the `cmd` location using os/arch local/vendored location based on - using `root_dir`. Run the command `cwd` current working directory using an + using `root_dir`. No resolution is done if root_dir is None + + Run the command using the `cwd` current working directory with an `env` dict of environment variables. """ assert cmd diff --git a/src/commoncode/dict_utils.ABOUT b/src/commoncode/dict_utils.ABOUT new file mode 100644 index 00000000000..049d2c9861b --- /dev/null +++ b/src/commoncode/dict_utils.ABOUT @@ -0,0 +1,9 @@ +about_resource: dict_utils.py +download_url: + - http://code.activestate.com/recipes/578375-proof-of-concept-for-a-more-space-efficient-faster/ + - http://code.activestate.com/recipes/198157-improve-dictionary-lookup-performance/ + +dje_license: mit and psf-2.0 +license_text_file: dict_utils.LICENSE +copyright: Copyright (c) 2012 Raymond Hettinger +owner: Raymond Hettinger diff --git a/src/commoncode/dict_utils.LICENSE b/src/commoncode/dict_utils.LICENSE new file mode 100644 index 00000000000..47da46b7a91 --- /dev/null +++ b/src/commoncode/dict_utils.LICENSE @@ -0,0 +1,21 @@ +# Copyright (c) 2003-2012 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/commoncode/dict_utils.py b/src/commoncode/dict_utils.py new file mode 100644 index 00000000000..8e38ffcb0c4 --- /dev/null +++ b/src/commoncode/dict_utils.py @@ -0,0 +1,250 @@ + +# Copyright (c) 2003-2012 Raymond Hettinger +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# http://code.activestate.com/recipes/578375-proof-of-concept-for-a-more-space-efficient-faster/ +# Created by Raymond Hettinger on Mon, 10 Dec 2012 (MIT) +# Proof-of-concept for a more space-efficient, faster-looping dictionary (Python +# recipe) Save space and improve iteration speed by moving the hash/key/value +# entries to a densely packed array keeping only a sparse array of indices. This +# eliminates wasted space without requiring any algorithmic changes. + +from __future__ import absolute_import, print_function + +import array +import collections +import itertools + + +# Placeholder constants +FREE = -1 +DUMMY = -2 + + + +class Dict(collections.MutableMapping): + """ + Space efficient dictionary with fast iteration and cheap resizes. + """ + + @staticmethod + def _gen_probes(hashvalue, mask): + """ + Same sequence of probes used in the current dictionary design. + """ + PERTURB_SHIFT = 5 + if hashvalue < 0: + hashvalue = -hashvalue + i = hashvalue & mask + yield i + perturb = hashvalue + while True: + i = (5 * i + perturb + 1) & 0xFFFFFFFFFFFFFFFF + yield i & mask + perturb >>= PERTURB_SHIFT + + def _lookup(self, key, hashvalue): + """ + Same lookup logic as currently used in real dicts. + """ + assert self.filled < len(self.indices) # At least one open slot + freeslot = None + for i in self._gen_probes(hashvalue, len(self.indices) - 1): + index = self.indices[i] + if index == FREE: + return (FREE, i) if freeslot is None else (DUMMY, freeslot) + elif index == DUMMY: + if freeslot is None: + freeslot = i + elif (self.keylist[index] is key or + self.hashlist[index] == hashvalue + and self.keylist[index] == key): + return (index, i) + + @staticmethod + def _make_index(n): + """ + New sequence of indices using the smallest possible datatype. + """ + if n <= 2 ** 7: return array.array('b', [FREE]) * n # signed char + if n <= 2 ** 15: return array.array('h', [FREE]) * n # signed short + if n <= 2 ** 31: return array.array('l', [FREE]) * n # signed long + return [FREE] * n # python integers + + def _resize(self, n): + """ + Reindex the existing hash/key/value entries. + Entries do not get moved, they only get new indices. + No calls are made to hash() or __eq__(). + """ + # round-up to power-of-two + n = 2 ** n.bit_length() + self.indices = self._make_index(n) + for index, hashvalue in enumerate(self.hashlist): + for i in Dict._gen_probes(hashvalue, n - 1): + if self.indices[i] == FREE: + break + self.indices[i] = index + self.filled = self.used + + def clear(self): + self.indices = self._make_index(8) + self.hashlist = [] + self.keylist = [] + self.valuelist = [] + self.used = 0 + # used + dummies + self.filled = 0 + + def __getitem__(self, key): + hashvalue = hash(key) + index, _i = self._lookup(key, hashvalue) + if index < 0: + raise KeyError(key) + return self.valuelist[index] + + def __setitem__(self, key, value): + hashvalue = hash(key) + index, i = self._lookup(key, hashvalue) + if index < 0: + self.indices[i] = self.used + self.hashlist.append(hashvalue) + self.keylist.append(key) + self.valuelist.append(value) + self.used += 1 + if index == FREE: + self.filled += 1 + if self.filled * 3 > len(self.indices) * 2: + self._resize(4 * len(self)) + else: + self.valuelist[index] = value + + def __delitem__(self, key): + hashvalue = hash(key) + index, i = self._lookup(key, hashvalue) + if index < 0: + raise KeyError(key) + self.indices[i] = DUMMY + self.used -= 1 + # If needed, swap with the last-most entry to avoid leaving a "hole" + if index != self.used: + lasthash = self.hashlist[-1] + lastkey = self.keylist[-1] + lastvalue = self.valuelist[-1] + lastindex, j = self._lookup(lastkey, lasthash) + assert lastindex >= 0 and i != j + self.indices[j] = index + self.hashlist[index] = lasthash + self.keylist[index] = lastkey + self.valuelist[index] = lastvalue + # Remove the lastmost entry + self.hashlist.pop() + self.keylist.pop() + self.valuelist.pop() + + def __init__(self, *args, **kwds): + if not hasattr(self, 'keylist'): + self.clear() + self.update(*args, **kwds) + + def __len__(self): + return self.used + + def __iter__(self): + return iter(self.keylist) + + def iterkeys(self): + return iter(self.keylist) + + def keys(self): + return list(self.keylist) + + def itervalues(self): + return iter(self.valuelist) + + def values(self): + return list(self.valuelist) + + def iteritems(self): + return itertools.izip(self.keylist, self.valuelist) + + def items(self): + return zip(self.keylist, self.valuelist) + + def __contains__(self, key): + index, _i = self._lookup(key, hash(key)) + return index >= 0 + + def get(self, key, default=None): + index, _i = self._lookup(key, hash(key)) + return self.valuelist[index] if index >= 0 else default + + def popitem(self): + if not self.keylist: + raise KeyError('popitem(): dictionary is empty') + key = self.keylist[-1] + value = self.valuelist[-1] + del self[key] + return key, value + + def __repr__(self): + return 'Dict(%r)' % self.items() + + def show_structure(self): + """ + Diagnostic method. Not part of the API. + """ + print('=' * 50) + print(self) + print('Indices:', self.indices) + for i, row in enumerate(zip(self.hashlist, self.keylist, self.valuelist)): + print(i, row) + print('-' * 50) + + +# http://code.activestate.com/recipes/198157-improve-dictionary-lookup-performance/ +# Created by Raymond Hettinger on Sun, 4 May 2003 (PSF) +# Reduce average dictionary lookup time by making the internal tables more sparse. + + +def sparsify(d): + """ + Improve dictionary sparsity. + + The dict.update() method makes space for non-overlapping keys. + Giving it a dictionary with 100% overlap will build the same + dictionary in the larger space. The resulting dictionary will + be no more that 1/3 full. As a result, lookups require less + than 1.5 probes on average. + + Example: + >>> import __builtin__ + >>> sparsify(__builtin__.__dict__) + """ + + e = d.copy() + d.update(e) + + +if __name__ == '__main__': + d = Dict([('timmy', 'red'), ('barry', 'green'), ('guido', 'blue')]) + d.show_structure() diff --git a/src/commoncode/filetype.py b/src/commoncode/filetype.py index 55ef157428e..b7b51d44e30 100644 --- a/src/commoncode/filetype.py +++ b/src/commoncode/filetype.py @@ -166,7 +166,7 @@ def is_rwx(location): def get_last_modified_date(location): """ - Return the last modified date stamp of a file is YYYYMMDD format. The date + Return the last modified date stamp of a file as YYYYMMDD format. The date of non-files (dir, links, special) is always an empty string. """ yyyymmdd = '' diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index 4410ffbcb08..37c7cf195c6 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -91,15 +91,19 @@ def to_os_native_path(path): return path -def get_test_loc(test_path, test_data_dir): +def get_test_loc(test_path, test_data_dir, debug=False): """ Given a `test_path` relative to the `test_data_dir` directory, return the location to a test file or directory for this path. No copy is done. """ + if debug: + import inspect + caller = inspect.stack()[1][3] + print('\nget_test_loc,%(caller)s,"%(test_path)s","%(test_data_dir)s"' % locals()) + assert test_path assert test_data_dir - if not os.path.exists(test_data_dir): raise IOError("[Errno 2] No such directory: test_data_dir not found:" " '%(test_data_dir)s'" % locals()) @@ -122,13 +126,18 @@ class FileDrivenTesting(object): """ test_data_dir = None - def get_test_loc(self, test_path, copy=False): + def get_test_loc(self, test_path, copy=False, debug=False): """ Given a `test_path` relative to the self.test_data_dir directory, return the location to a test file or directory for this path. Copy to a temp test location if `copy` is True. """ - test_loc = get_test_loc(test_path, self.test_data_dir) + if debug: + import inspect + caller = inspect.stack()[1][3] + print('\nself.get_test_loc,%(caller)s,"%(test_path)s"' % locals()) + + test_loc = get_test_loc(test_path, self.test_data_dir, debug=debug) if copy: base_name = os.path.basename(test_loc) if filetype.is_file(test_loc): From ffd535603a769bb4aed822b893182213cf604a2c Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 11 Mar 2016 08:11:18 -0800 Subject: [PATCH 035/436] #86 Adding dictionary utilities and other common utilities --- tests/commoncode/commoncode/test_codec.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/commoncode/commoncode/test_codec.py b/tests/commoncode/commoncode/test_codec.py index eaac2599f4f..cd667c079f8 100644 --- a/tests/commoncode/commoncode/test_codec.py +++ b/tests/commoncode/commoncode/test_codec.py @@ -37,7 +37,7 @@ def test_bin_to_num_basic(self): result = bin_to_num('{') assert expected == result - def test_bin_to_num_null(self): + def test_bin_to_num_zero(self): expected = 0 result = bin_to_num('\x00') assert expected == result @@ -57,8 +57,8 @@ def test_num_to_bin_basic(self): result = num_to_bin(123) assert expected == result - def test_num_to_bin_null(self): - expected = '' + def test_num_to_bin_zero(self): + expected = '\x00' result = num_to_bin(0) assert expected == result @@ -73,7 +73,7 @@ def test_num_to_bin_bin_to_num_is_idempotent(self): assert expected == result def test_encode_zero(self): - assert '' == _encode(0) + assert 'AA==' == _encode(0) def test_encode_basic(self): assert 'HKq1w7M=' == _encode(123123123123) From e2dff96ea1048d2d24c43dbab072135643918024 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 29 Mar 2016 14:40:32 +0200 Subject: [PATCH 036/436] #226 Add sha256 and sha512 functions Also refined the code and tests --- tests/commoncode/commoncode/test_hash.py | 73 +++++++++++++----------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/tests/commoncode/commoncode/test_hash.py b/tests/commoncode/commoncode/test_hash.py index 0f740e6f079..18f01d180c6 100644 --- a/tests/commoncode/commoncode/test_hash.py +++ b/tests/commoncode/commoncode/test_hash.py @@ -28,65 +28,70 @@ from commoncode.testcase import FileBasedTesting -import commoncode.hash -from commoncode.hash import sha1 -from commoncode.hash import md5 from commoncode.hash import b64sha1 +from commoncode.hash import get_hasher +from commoncode.hash import md5 +from commoncode.hash import sha1 +from commoncode.hash import sha256 +from commoncode.hash import sha512 class TestHash(FileBasedTesting): test_data_dir = os.path.join(os.path.dirname(__file__), 'data') def test_get_hasher(self): - h = commoncode.hash.get_hasher(160) + h = get_hasher(160) assert 'hvfkN_qlp_zhXR3cuerq6jd2Z7g=' == h('a').b64digest() assert '4MkDWJjdUvxlxBRUzsnE0mEb-zc=' == h('aa').b64digest() assert 'fiQN50-x7Qj6CNOAY_amqRRiqBU=' == h('aaa').b64digest() - def test_hash_1(self): + def test_short_hashes(self): + h = get_hasher(32) + assert '0cc175b9' == h('a').hexdigest() + assert '4124bc0a' == h('aa').hexdigest() + h = get_hasher(64) + assert '4124bc0a9335c27f' == h('aa').hexdigest() + + def test_sha1_checksum(self): test_file = self.get_test_loc('hash/dir1/a.png') assert sha1(test_file) == '34ac5465d48a9b04fc275f09bc2230660df8f4f7' - def test_hash_2(self): - test_file = self.get_test_loc('hash/dir1/a.png') - assert md5(test_file) == '4760fb467f1ebf3b0aeace4a3926f1a4' + def test_sha1_checksum_on_text(self): + test_file = self.get_test_loc('hash/dir1/a.txt') + assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' - def test_hash_3(self): + def test_sha1_checksum_on_text2(self): + test_file = self.get_test_loc('hash/dir2/a.txt') + assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' + + def test_sha1_checksum_on_dos_text(self): + test_file = self.get_test_loc('hash/dir2/dos.txt') + assert sha1(test_file) == 'a71718fb198630ae8ba32926015d8555a03cb06c' + + def test_sha1_checksum_base64(self): test_file = self.get_test_loc('hash/dir1/a.png') assert b64sha1(test_file) == 'NKxUZdSKmwT8J18JvCIwZg349Pc=' - def test_hash_4(self): - test_file = self.get_test_loc('hash/dir1/a.txt') - assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' + def test_md5_checksum(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert md5(test_file) == '4760fb467f1ebf3b0aeace4a3926f1a4' - def test_hash_5(self): + def test_md5_checksum_on_text(self): test_file = self.get_test_loc('hash/dir1/a.txt') assert md5(test_file) == '40c53c58fdafacc83cfff6ee3d2f6d69' - def test_hash_6(self): - test_file = self.get_test_loc('hash/dir1/a.txt') - assert b64sha1(test_file) == 'PKaejWwjSkadFqwopKZYySJnxCM=' - - def test_hash_7(self): - test_file = self.get_test_loc('hash/dir2/a.txt') - assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' - - def test_hash_8(self): + def test_md5_checksum_on_text2(self): test_file = self.get_test_loc('hash/dir2/a.txt') assert md5(test_file) == '40c53c58fdafacc83cfff6ee3d2f6d69' - def test_hash_9(self): - test_file = self.get_test_loc('hash/dir2/a.txt') - assert b64sha1(test_file) == 'PKaejWwjSkadFqwopKZYySJnxCM=' - - def test_hash_10(self): - test_file = self.get_test_loc('hash/dir2/dos.txt') - assert sha1(test_file) == 'a71718fb198630ae8ba32926015d8555a03cb06c' - - def test_hash_11(self): + def test_md5_checksum_on_dos_text(self): test_file = self.get_test_loc('hash/dir2/dos.txt') assert md5(test_file) == '095f5068940e41df9add5d4cc396c181' - def test_hash_12(self): - test_file = self.get_test_loc('hash/dir2/dos.txt') - assert b64sha1(test_file) == 'pxcY-xmGMK6LoykmAV2FVaA8sGw=' + def test_sha256_checksum(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert sha256(test_file) == '1b598db6fee8f1ec7bb919c0adf68956f3d20af8c9934a9cf2db52e1347efd35' + + def test_sha512_checksum(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert sha512(test_file) == '5be9e01cd20ff288fd3c3fc46be5c2747eaa2c526197125330947a95cdb418222176b182a4680f0e435ba8f114363c45a67b30eed9a9222407e63ccbde46d3b4' From 9cad8a48c31af13f653ce64600497411b06ceb32 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 29 Mar 2016 14:40:32 +0200 Subject: [PATCH 037/436] #226 Add sha256 and sha512 functions Also refined the code and tests --- src/commoncode/hash.py | 64 ++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/commoncode/hash.py b/src/commoncode/hash.py index 8417407889f..879779e3764 100644 --- a/src/commoncode/hash.py +++ b/src/commoncode/hash.py @@ -22,11 +22,12 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. -from __future__ import absolute_import, print_function +from __future__ import absolute_import, division, print_function import hashlib -from commoncode import codec +from commoncode.codec import bin_to_num +from commoncode.codec import urlsafe_b64encode from commoncode import filetype @@ -40,59 +41,50 @@ Checksums are operating on files. """ -def hash_mod(bitsize, hmodule): +def _hash_mod(bitsize, hmodule): """ Return a hashing class returning hashes with a `bitsize` bit length. The interface of this class is similar to the hash module API. """ - class hash_cls(object): + class hasher(object): def __init__(self, msg=None): - self.digest_size = size / 8 - if msg: - self.hd = module(msg).digest()[-(self.digest_size):] - else: - self.hd = None + digest_size = bitsize // 8 + self.h = msg and hmodule(msg).digest()[:digest_size] or None def digest(self): - return self.hd + return self.h def hexdigest(self): - if self.hd: - return self.hd.encode('hex') + return self.h and self.h.encode('hex') def b64digest(self): - if self.hd: - return codec.urlsafe_b64encode(self.hd) + return self.h and urlsafe_b64encode(self.h) def intdigest(self): - if self.hd: - return codec.bin_to_num(self.hd) + return self.h and bin_to_num(self.h) + + return hasher - size = bitsize - module = hmodule - return hash_cls # Base hashers for each bit size -bitsizes = { +_hashmodules_by_bitsize = { # md5-based - 16: hashlib.md5, 24: hashlib.md5, 32: hashlib.md5, 48: hashlib.md5, - 64: hashlib.md5, 96: hashlib.md5, 128: hashlib.md5, + 32: _hash_mod(32, hashlib.md5), + 64: _hash_mod(64, hashlib.md5), + 128: _hash_mod(128, hashlib.md5), # sha-based - 160: hashlib.sha1, 224: hashlib.sha224, 256: hashlib.sha256, - 384: hashlib.sha384, 512: hashlib.sha512 + 160: _hash_mod(160, hashlib.sha1), + 256: _hash_mod(256, hashlib.sha256), + 384: _hash_mod(384, hashlib.sha384), + 512: _hash_mod(512, hashlib.sha512) } -# All available hash modules keyed by the bit size of the hashed output. -hashmodules_by_bitsize = dict ((s, hash_mod(s, m),) - for s, m in bitsizes.items()) - - def get_hasher(bitsize): """ Return a hasher for a given size in bits of the resulting hash. """ - return hashmodules_by_bitsize[bitsize] + return _hashmodules_by_bitsize[bitsize] def checksum(location, bitsize, base64=False): @@ -112,17 +104,21 @@ def checksum(location, bitsize, base64=False): hashed = hasher(hashable) if base64: return hashed.b64digest() - else: - return hashed.hexdigest() + + return hashed.hexdigest() def md5(location): return checksum(location, bitsize=128, base64=False) - def sha1(location): return checksum(location, bitsize=160, base64=False) - def b64sha1(location): return checksum(location, bitsize=160, base64=True) + +def sha256(location): + return checksum(location, bitsize=256, base64=False) + +def sha512(location): + return checksum(location, bitsize=512, base64=False) From 7588a481836d934be9bcef0f12d57e3bf239cb65 Mon Sep 17 00:00:00 2001 From: rakesh balusa Date: Mon, 2 May 2016 11:26:10 -0700 Subject: [PATCH 038/436] fixed fileutils test case by sorting the results obtained --- tests/commoncode/commoncode/test_fileutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 8cea3a4299d..15a98bfe6f4 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -318,7 +318,7 @@ def test_os_walk_with_unicode_path(self): def test_fileutils_walk(self): test_dir = self.get_test_loc('fileutils/walk') base = self.get_test_loc('fileutils') - result = [(as_posixpath(t.replace(base, '')), d, f,) for t, d, f in fileutils.walk(test_dir)] + result = [(as_posixpath(t.replace(base, '')), d, sorted(f),) for t, d, f in fileutils.walk(test_dir)] expected = [ ('/walk', ['d1'], ['f', 'unicode.zip']), ('/walk/d1', ['d2'], ['f1']), @@ -366,7 +366,7 @@ def test_file_iter(self): '/walk/d1/d2/f2', '/walk/d1/d2/d3/f3' ] - assert expected == result + assert sorted(expected) == sorted(result) def test_file_iter_can_iterate_a_single_file(self): test_file = self.get_test_loc('fileutils/walk/f') From b0305405173c09afd6651aa83cf90297d703a956 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Mon, 9 May 2016 11:52:19 +0200 Subject: [PATCH 039/436] #86 Return the sparsified dict. --- src/commoncode/dict_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commoncode/dict_utils.py b/src/commoncode/dict_utils.py index 8e38ffcb0c4..be00415aa7d 100644 --- a/src/commoncode/dict_utils.py +++ b/src/commoncode/dict_utils.py @@ -221,13 +221,11 @@ def show_structure(self): print('-' * 50) -# http://code.activestate.com/recipes/198157-improve-dictionary-lookup-performance/ -# Created by Raymond Hettinger on Sun, 4 May 2003 (PSF) -# Reduce average dictionary lookup time by making the internal tables more sparse. - - def sparsify(d): """ + http://code.activestate.com/recipes/198157-improve-dictionary-lookup-performance/ + Created by Raymond Hettinger on Sun, 4 May 2003 (PSF) + Reduce average dictionary lookup time by making the internal tables more sparse. Improve dictionary sparsity. The dict.update() method makes space for non-overlapping keys. @@ -236,13 +234,15 @@ def sparsify(d): be no more that 1/3 full. As a result, lookups require less than 1.5 probes on average. + Example: - >>> import __builtin__ - >>> sparsify(__builtin__.__dict__) + >>> sparsify({1: 3, 4: 5}) + {1: 3, 4: 5} """ e = d.copy() d.update(e) + return d if __name__ == '__main__': From 46b9f077a00bc50a7987365871490c9880206a6e Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Tue, 10 May 2016 11:15:54 +0200 Subject: [PATCH 040/436] Ignore .repo directories created by the git-repo tool git-repo [1] is used to manage multiple Git repositories. The .repo directory contains the consolidated Git histories of the managed projects (which are symlinked from the individual projects' .git repositories), so we should ignore the .repo directory just like the .git directory. However, the .repo directory also contains the source code to the git-repo tool itself. One might argue that this source code should be picked up during a scan, but then again the code of git-repo is not integrated into the deliverable / what is being built from the source code as git-repo is just a VCS tool, so a user is probably not interested in any license findings in git-repo. [1] https://code.google.com/p/git-repo/ --- src/commoncode/ignore.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commoncode/ignore.py b/src/commoncode/ignore.py index 17a58787c4c..aaf875f035b 100644 --- a/src/commoncode/ignore.py +++ b/src/commoncode/ignore.py @@ -183,6 +183,8 @@ def get_ignores(location, include_defaults=True): '.hg': 'Default ignore: Mercurial artifact', '.hgignore' : 'Default ignore: Mercurial config artifact', + '.repo': 'Default ignore: Multiple Git repository artifact', + '.svn': 'Default ignore: SVN artifact', '.svnignore': 'Default ignore: SVN config artifact', From 71f694a26c8b9fdb4e3b1053f3d5d3390c681e3b Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Tue, 10 May 2016 11:15:54 +0200 Subject: [PATCH 041/436] Ignore .repo directories created by the git-repo tool git-repo [1] is used to manage multiple Git repositories. The .repo directory contains the consolidated Git histories of the managed projects (which are symlinked from the individual projects' .git repositories), so we should ignore the .repo directory just like the .git directory. However, the .repo directory also contains the source code to the git-repo tool itself. One might argue that this source code should be picked up during a scan, but then again the code of git-repo is not integrated into the deliverable / what is being built from the source code as git-repo is just a VCS tool, so a user is probably not interested in any license findings in git-repo. [1] https://code.google.com/p/git-repo/ --- .../commoncode/commoncode/data/ignore/vcs.tgz | Bin 5610 -> 6296 bytes tests/commoncode/commoncode/test_ignore.py | 1 + 2 files changed, 1 insertion(+) diff --git a/tests/commoncode/commoncode/data/ignore/vcs.tgz b/tests/commoncode/commoncode/data/ignore/vcs.tgz index 866cdf5478359f2043c4b5aaf0af8cb3d1e09b1f..9570eb00d1efe889ba3683b4d26a3dcc5c08b17e 100644 GIT binary patch literal 6296 zcmZ8_c_38#`~TQ#EQKh#s?nm-VvFE?>XGxKfn3!oO!?B&-;0v*YkQ_&tY9)GDd&4-@(x7 zzO|d%wE9}afRk#hHvS?nBKhqO*<|B?%C0xZR@==l-urwf^Z3xkmmUcAem4T>Bx# zylDcW`%u}?ZfEydAeTv4ZNiKP`H6_d+X4QZe(A|&9kQ%aM~;Rc5$@^)>}J^3RRL+U z1UwbTwi-;*+N9jOkewemPHCNR0rL~HqsLjIOtlPaEJGSz=GYfP%2vN_m|R36Ts0Z% z_GQ+G>cy6pQgpZ8=6;rTKmC@w0lM;Czbn=51~CIHCZW_E<>y$5s(0vYssPW~lo>Bl zm|6%KnJ02?yiC*=BW^*Yg)X&s6AAdBhJ~KWTWe)jkABlOAz~SXF!c&-b4ea}VEaVz zGK-P$8*Hf`fLCt%i%A`?nPmEts=$D@FU4w(@7GBS?8sljKQ)?Ma%9P7+}A*Jev52| zLXPQ5&}E@cxn>bI7TGQ#}k=$nrpX@pvYcXLF-A4UQU3dba2pRZ> z&j=-TXg8C{h7e7*N*F{Ad7VUZxHpfhgxLaeC?rlG?Nr-sPsvtPy|Pb-as}$O4CEbO zA;cFM7tZK!0gD2d`|9T)1T}$9*+V5;F*)=k?vo1t`jZx7%!St`8l@NT=?|-r#_Hp^ z+|oC_%z=PdB};_vP?Clw+}i(v@P-!n@(n^;(9?aK)1jbW4)gF&?X3kc;@GlhE`6eu z#utZ>F%ZxLMKC>-w2F58d8Xc_4=X%=#-bw}Zx3ivy01MQ4m(6782Cw=Z89KZVG+e3e#Gl}X-2sl6@x550TSi`k8dWZTdQq8+MO1&ehI-0vi z>$-3N9S1%5$f;YsDS+Nnijvzi-C^SRg@Ag(C37OydmdTX^lMSgyJv;B(-S3P;nH{f z%4g)g2|_fgcf{;BndAFm^E<<^#$=0zm)*?Y+7YA$-ZOhZON*+&vjbR`6M3aG7gnx2 zKfuymDZ>zhr6Qe+Y#x*oob}9bFpIvUqC`UWUlPFJ;Vt*+xZ#+d(C6kh0)5#vEx|$!A-+eV` z@gxC1o5jcn{bW$W{dLgyH=vGHx^Gsf#iZdO&_DJveIu~7@#GlGzk=$F-Glsc1z=EM_RIX$kKK3uob8Y_dQjw5^4r2vSnB21Grnha4IcS;jZMWAz5Zgy=zV zt~p?8*?E2PwH`xltVPiYOA33AYWr2=yeZA3`-5H*vD_rfk`8V-Jz}pp^t7`xzVVj7 zhNufY_zVGha#9%WqA8WH1j7vVrxM--o`@?xl z$+x>l2$VX#;W$^}c!HHm9T@~7j{!d8cv~-M&tAf z7(OLVY}^Su%%!MKt2b^HR!|h{Rieg{b4lZpQl$_Z6xlB&FZ(AT%u*T5Q#gBAiMtRR z8G%?nC#zCpNzp22g_z@eMU5tEFt5e$fQ|0SOq8kk4rit4*$5Tgrnpr$F=y5Y+R;-J zz@ZwfT@qmam1SbF=uE5zU2Yi%-B%c5ah7Ev=#^TuA&t}MMzUdO{{V;LZNBD1itu_J z7@k$2Q9<#TnvhnpPHZExz$MH|EXTg@+DFDRD9%m8IX~J(Vbgy=&l-xyL(M^4#g8o= zB#IZ29WQe7N(aAimnGIh!grOs^*oT&M1Ud-75+F=9xX#(8^w`2beyJwv>E&QrQs*6 ztV4e|RxAUKt8bU`D&b3|nQlcq9Q$3jvHIUL<)LtOb0}#@)AJ-1`1f!H_KaG1YC%6Y zfKT%)9R|xWM7~YStP^$pFX*Hayxb2n9KJBj)Tmo8X+h^~d?!e5>(V|pfuRm%Vng8fr@7Om z`}=3v?Sd^eRR5;|IT@B1N6(+?I_vU@Der$??$cZNVS{_*g(N+P715OPc^ov{@lTOm z&tqi#eT!k_3WN+8B})kfw$Vyi%Gud-6OHHiSNR3qUOg*u-TI@ohEK9DUMtK`v=yUC zV$|e0&_=HQoiw5*`&hg@rz53!SBH^@AjUd9>FJBk_P~w39I87TQPtI74*Mn!uzG;u zJ(JwKdeffWYTM;T8%jQ1j67a@aYd}3<@xn0)70pPltjyiQM%QXG*F11P6n|?w9M$8 zNWa#l{e9W}WRC@prcJn)G`3{Ezh21FbkpJ+7eW6WZmiuwrM*vdf2P5PILX_wx3+Bg z`E1&&N)Oj>*nyG_FMpD()r-|bcq3Fuc8uD+O~aYjF9)Z3l%l%`N@FczekgmCyl>XG zoJNaHv#WOQR+V30yk*fL?%ozbvWj(1K@}iDN1p8(v_O^|kZ@LS(-_+ore=rH2m1rI zWUyES(a)sQ zM%T*zCaM~*U#4Knv7~Cl4JCbTBrvlBWyZ8gZ(O~7h_5R-@9L92)#apj{@vTbEAOWt zXvKL_q=cp|HJP;^9Lon-CTiJ?CviyW0EYs*aO;YbuIS)UpIkj z3!#*@6@xpjHfi5z@PgT7^UQO8%`S*E7TUMHt!ixi)gV+X!zTYk~&nMs1w4eQL=Ik7C~*EdEI`!p{c>YalDdB zzo+g?b?+wMzcX<}r%yS0vrTEN+YRyuBFGZsNDsu`LE!ae4T~z|J5q2OM1%WTl*Dm< z92In#3_rSA5Ach^kYdHe$qu68D?&t9L7BZr{AR5hi>ne8Tws*w(FrIOcjwtQ$v4 zRp?L~JOfyg+L%(UKY8%$w>A-}B{nYJj9&>(%JBPxX%}C2f(#7l-k8){$!n%bY{}V)$u;cDz0RB&QoikJ{YFHQz0g5Is8Qt{R9atr=YoL{q8o`O974w3q#Yy?e(3J zKc`Gzv3n<73IdoQLiMyXG`nJr|9ZSCnRQin)MS&^?EX77?^jK8jcVKa-bV;)|kbfutu#{!}T zuxDOiQ3#_~V&MLymrqZ01dd%+h=Rb8`A`W}jZ)N;9+Z*xjfR=jCjd1)!HGQSId81R zq5Y9iK7jP2CuzN==7@gzveiq)t^=YEsNlGB@6=@fNC~oR@jE2o@r9M!CfTJu_3dE_ zBxq$X?r3zKn(KZ*C7}w7Oi6?#S9Tp<{e{5Ug)pMTmOPPHpnNG^syEOSxUm9OwUX$N zFo?oAYzKqu7vX%FhPC|v=`>q81$-Tv%a8RTlyWxn%_5ux8n)$ah#3}$H9gNiPv}*u zc^krEV$4Vx$8WS>IG$cak@_ha$IEE34{2PE&U1-F%|r_i%UB9Dve%ZZDl6Ro5k~~9 z=(9@Bp1{G>j?T`$-a<`k1Dna41ol&UNz^h3PjS>CHBX;*xt)S-M7e_RO--x^i(oMI zz53uH;-ONwWI0S8ei!w$b1|Rrs7L=&bp1pa6d@QXrSF^52HpEOL7A|8c-8cJhjHM0 z1>ZrzWEuU+kIc$%7T#n`PXPU54f5ZzSN{ka559aVd9P)L?-(;&U=Jn0K66Gx@Q{K6 zyd67p(r28d6lo8Ik+c{{!`*$dU)D?sOkjaw`~X6~3(tkam0hTy38v=OH514Rs_aab z3edw{9s4Z0zU?iihRw{0#gsf7`%c3eB3c%KHOKaS+;&3BJ;>d^f_TWh4SIB-d1Qmz zpL2uyB^B4zS$WEPd2rP~6Rblmp)~R7{%zN4G8M}HOXOzXsbQfqj}YyIxk57;hK?fNzKBh&>dbzr zMfIOc+H!|GsG5y0(CsbXA zW*cix4N8qt^de3lI$GiU;4_SA7=l5^2D0(jaaFb&jZwhR;A2L{9=&!6ey8;I6y4$R z&s{04I(_O_4$sU3r)2%eVQW~kGB-=?vam=O78c79+7Ed&q@IqG7IxtL5*2M9dmVBl zFHn0qqsDk)TdgFMY0|hp;I-F|tB(g84+pL78fPYbk+7|1iSgi6_@a`nE#o*&C5=CZ?bhiQqvK}S%Q%r4 z3ht(W6R9f|h_7By9O%ng2dv4igC9}jFl=-zj;49QRBieN$jvYQ<6Y!U2&Z;n=Of_l zjXtc2i&(_wWx~1&lA}xAu3UqD;%VJ5)Ag(cQ}P0}=uI&cj@tPZu4~W(MQC%=cN7`1 z_7uTWk+X+SJz@|MJKZZ*jwn$5v()-s(Dg(G#TleyWhKxj+}H_;|HdzCRQY%f4scFR zviXdz3o6whDcaa+!xJjY01P&4RU`DRJ4S$S%Gqxk)<}Ih$CWmhPYV6?j`kv>Zoo{+ zUdKT|MNIFBj0w1KWgjx-J9eZS51FlZOon&?n_3{KMPFyFu#><=l1(STQG?tYSJnb? zIA$^wX*UmxtN+yVV!4V`s35ZDQzr;;(any1 zkesll35Pg3McrN4iv_$#aM`Pm2>Xq3kOl5&8EP6wx!5oIWU@0BcTWq3Eas3MboIcg z<1@Ga+9LrEu%Sb>01hAWxc)vMkw4-2N-|c>olM;?g=B?%X#t1s2AGykZlDqJ9sM); zFjrrMJ+FcXGeEqcwgT1%smMGJKKL#!gJ>mNhr&S$i7b5g%rmR&?ja=mxIFt=xJzj| zIpc3wLnrb(8qyb*xyY@d)g$aoOab^;;4{Wk_M*TrLnM9`pvU zAD906rU;pUOC?w<^ife60T}G;89zvtFMqG5Lwm3n(p0-U#*Zd_^$C;?y`3CAzT2+X zC`FEL0*v;%7zQ5yxq_G!JAfn&XMa4-?PQm`d9XJV;v8~D8qjI=><0dzCMEDgerS^R zXc35`Nq0J1x8T97n;%PmcW+RB1%+DbQl%l#&&{xwQ1sz4vXMA(sn0lt;8YOUd=t=O T{Qp1F{frj5`v!w?U5D|1LyIx8 literal 5610 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 diff --git a/tests/commoncode/commoncode/test_ignore.py b/tests/commoncode/commoncode/test_ignore.py index cf973b4b004..aafd363023b 100644 --- a/tests/commoncode/commoncode/test_ignore.py +++ b/tests/commoncode/commoncode/test_ignore.py @@ -164,6 +164,7 @@ def test_skip_vcs_files_and_dirs(self): ('/vcs/.bzr', 'Default ignore: Bazaar artifact'), ('/vcs/.git', 'Default ignore: Git artifact'), ('/vcs/.hg', 'Default ignore: Mercurial artifact'), + ('/vcs/.repo', 'Default ignore: Multiple Git repository artifact'), ('/vcs/.svn', 'Default ignore: SVN artifact'), ('/vcs/CVS', 'Default ignore: CVS artifact'), ('/vcs/_darcs', 'Default ignore: Darcs artifact'), From 698b700d50c28bc50d6859785f54fcc6bb03a54c Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sat, 11 Jun 2016 10:43:45 +0200 Subject: [PATCH 042/436] Approximate license detection: all datadriven tests passing! --- src/commoncode/dict_utils.py | 2 -- src/commoncode/fileutils.py | 53 +++++++++++++++++---------------- src/commoncode/functional.py | 53 ++++++++++++++++++++++++++++++++- src/commoncode/misc.ABOUT | 8 +++++ src/commoncode/misc.LICENSE | 21 +++++++++++++ src/commoncode/misc.py | 57 ++++++++++++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 src/commoncode/misc.ABOUT create mode 100644 src/commoncode/misc.LICENSE create mode 100644 src/commoncode/misc.py diff --git a/src/commoncode/dict_utils.py b/src/commoncode/dict_utils.py index be00415aa7d..7e2ecea65fa 100644 --- a/src/commoncode/dict_utils.py +++ b/src/commoncode/dict_utils.py @@ -234,12 +234,10 @@ def sparsify(d): be no more that 1/3 full. As a result, lookups require less than 1.5 probes on average. - Example: >>> sparsify({1: 3, 4: 5}) {1: 3, 4: 5} """ - e = d.copy() d.update(e) return d diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index c00bb655e96..9cffd0e5f10 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -257,35 +257,38 @@ def walk(location, ignored=ignore_nothing): - optionally ignore files and directories by invoking the `ignored` callable on files and directories returning True if it should be ignored. - location is a directory or a file: for a file, the file is returned. - TODO: consider using scandir for speed-ups """ + # TODO: consider using the new "scandir" module for some speed-up. if DEBUG: ign = ignored(location) logger.debug('walk: ignored:', location, ign) - if not ignored(location): - if filetype.is_file(location) : - yield parent_directory(location), [], [file_name(location)] - elif filetype.is_dir(location): - dirs = [] - files = [] - # TODO: consider using scandir - for name in os.listdir(location): - loc = os.path.join(location, name) - if filetype.is_special(loc) or ignored(loc): - if DEBUG: - ign = ignored(loc) - logger.debug('walk: ignored:', loc, ign) - continue - # special files and symlinks are always ignored - if filetype.is_dir(loc): - dirs.append(name) - elif filetype.is_file(loc): - files.append(name) - yield location, dirs, files - - for dr in dirs: - for tripple in walk(os.path.join(location, dr), ignored): - yield tripple + if ignored(location): + return + + if filetype.is_file(location) : + yield parent_directory(location), [], [file_name(location)] + + elif filetype.is_dir(location): + dirs = [] + files = [] + # TODO: consider using scandir + for name in os.listdir(location): + loc = os.path.join(location, name) + if filetype.is_special(loc) or ignored(loc): + if DEBUG: + ign = ignored(loc) + logger.debug('walk: ignored:', loc, ign) + continue + # special files and symlinks are always ignored + if filetype.is_dir(loc): + dirs.append(name) + elif filetype.is_file(loc): + files.append(name) + yield location, dirs, files + + for dr in dirs: + for tripple in walk(os.path.join(location, dr), ignored): + yield tripple def file_iter(location, ignored=ignore_nothing): diff --git a/src/commoncode/functional.py b/src/commoncode/functional.py index 54c5d3cc33f..1175f98fd6c 100644 --- a/src/commoncode/functional.py +++ b/src/commoncode/functional.py @@ -27,6 +27,7 @@ import functools from itertools import izip from types import ListType, TupleType, GeneratorType +from array import array def flatten(seq): @@ -114,7 +115,7 @@ def memoized(*args, **kwargs): if kwargs: return fun(*args, **kwargs) # convert any list arg to a tuple - args = tuple(tuple(arg) if isinstance(arg, ListType) else arg + args = tuple(tuple(arg) if isinstance(arg, (ListType, tuple, array)) else arg for arg in args) try: return memos[args] @@ -164,3 +165,53 @@ def wrapper(self, *args, **kwargs): return wrapper return memoized_to_attr + + +def memoize_gen(fun): + """ + Decorate fun generator function and cache return values. Arguments must be + hashable. kwargs are not handled. Used to speed up some often executed + functions. + Usage example:: + + >>> @memoize + ... def expensive(*args, **kwargs): + ... print('Calling expensive with', args, kwargs) + ... return 'value expensive to compute' + repr(args) + >>> expensive(1, 2) + Calling expensive with (1, 2) {} + 'value expensive to compute(1, 2)' + >>> expensive(1, 2) + 'value expensive to compute(1, 2)' + >>> expensive(1, 2, a=0) + Calling expensive with (1, 2) {'a': 0} + 'value expensive to compute(1, 2)' + >>> expensive(1, 2, a=0) + Calling expensive with (1, 2) {'a': 0} + 'value expensive to compute(1, 2)' + >>> expensive(1, 2) + 'value expensive to compute(1, 2)' + >>> expensive(1, 2, 5) + Calling expensive with (1, 2, 5) {} + 'value expensive to compute(1, 2, 5)' + + The expensive function returned value will be cached based for each args + values and computed only once in its life. Call with kwargs are not cached + """ + memos = {} + + @functools.wraps(fun) + def memoized(*args, **kwargs): + # calls with kwargs are not handled and not cached + if kwargs: + return fun(*args, **kwargs) + # convert any list arg to a tuple + args = tuple(tuple(arg) if isinstance(arg, (ListType, tuple, array)) else arg + for arg in args) + try: + return memos[args] + except KeyError: + memos[args] = list(fun(*args)) + return memos[args] + + return functools.update_wrapper(memoized, fun) diff --git a/src/commoncode/misc.ABOUT b/src/commoncode/misc.ABOUT new file mode 100644 index 00000000000..5cd542f93b1 --- /dev/null +++ b/src/commoncode/misc.ABOUT @@ -0,0 +1,8 @@ +about_resource: misc.py +download_url: + - http://code.activestate.com/recipes/578433-mixin-for-pickling-objects-with-__slots__/ + +dje_license: mit +license_text_file: misc.LICENSE +copyright: Copyright (c) 2013 Oren Tirosh +owner: Oren Tirosh diff --git a/src/commoncode/misc.LICENSE b/src/commoncode/misc.LICENSE new file mode 100644 index 00000000000..4a72b80190d --- /dev/null +++ b/src/commoncode/misc.LICENSE @@ -0,0 +1,21 @@ +# Copyright (c) 2013 Oren Tirosh +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/commoncode/misc.py b/src/commoncode/misc.py new file mode 100644 index 00000000000..703e50e94ef --- /dev/null +++ b/src/commoncode/misc.py @@ -0,0 +1,57 @@ +# +# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# http://nexb.com and https://github.com/nexB/scancode-toolkit/ +# The ScanCode software is licensed under the Apache License version 2.0. +# Data generated with ScanCode require an acknowledgment. +# ScanCode is a trademark of nexB Inc. +# +# You may not use this software except in compliance with the License. +# You may obtain a copy of the License at: http://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. +# +# When you publish or redistribute any data created with ScanCode or any ScanCode +# derivative work, you must accompany this data with the following acknowledgment: +# +# Generated with ScanCode and provided on an "AS IS" BASIS, WITHOUT WARRANTIES +# OR CONDITIONS OF ANY KIND, either express or implied. No content created from +# ScanCode should be considered or used as legal advice. Consult an Attorney +# for any legal advice. +# ScanCode is a free software code scanning tool from nexB Inc. and others. +# Visit https://github.com/nexB/scancode-toolkit/ for support and download. + +from __future__ import absolute_import, print_function + + +class SlotPickleMixin(object): + # SlotPickelMixin is originally from: + # http://code.activestate.com/recipes/578433-mixin-for-pickling-objects-with-__slots__/ + # Copyright (c) 2013 Created by Oren Tirosh + # + # Permission is hereby granted, free of charge, to any person + # obtaining a copy of this software and associated documentation files + # (the "Software"), to deal in the Software without restriction, + # including without limitation the rights to use, copy, modify, merge, + # publish, distribute, sublicense, and/or sell copies of the Software, + # and to permit persons to whom the Software is furnished to do so, + # subject to the following conditions: + # + # The above copyright notice and this permission notice shall be + # included in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + # OTHER DEALINGS IN THE SOFTWARE. + def __getstate__(self): + return {slot: getattr(self, slot) for slot in self.__slots__ if hasattr(self, slot)} + + def __setstate__(self, state): + for slot, value in state.items(): + setattr(self, slot, value) \ No newline at end of file From 7a221dcbfee3552e5ec3ffb302bda577b19f087f Mon Sep 17 00:00:00 2001 From: Sebastian Schuberth Date: Wed, 12 Oct 2016 17:58:33 +0200 Subject: [PATCH 043/436] If the platform is unsupported, actually disclose its name This is useful to know for debugging. --- src/commoncode/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commoncode/system.py b/src/commoncode/system.py index 3309168fc50..76716c0a426 100644 --- a/src/commoncode/system.py +++ b/src/commoncode/system.py @@ -47,7 +47,7 @@ def os_arch(): elif 'darwin' in sys_platform: os = 'mac' else: - raise Exception('Unsupported OS/platform') + raise Exception('Unsupported OS/platform %r' % sys_platform) return os, arch From 71b716caf50f58e9c2d30beb749b2a59971874cf Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sun, 27 Nov 2016 18:42:45 +0100 Subject: [PATCH 044/436] Cosmetic formatting and import reorg Signed-off-by: Philippe Ombredanne --- src/commoncode/fileutils.py | 6 +++--- src/commoncode/testcase.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index 9cffd0e5f10..d394d2fb5ce 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -107,9 +107,9 @@ def system_temp_dir(): def get_temp_dir(base_dir, prefix=''): """ - Return the path to base a new unique temporary directory, created under - the system-wide `system_temp_dir` temp directory and as a subdir of the - base_dir path, a path relative to the `system_temp_dir`. + Return the path to a new unique temporary directory, created under + the system-wide `system_temp_dir` temp directory as a subdir of the + base_dir path (a path relative to the `system_temp_dir`). """ base = os.path.join(system_temp_dir(), base_dir) create_dir(base) diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index 37c7cf195c6..e38b6c16870 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -23,7 +23,8 @@ # Visit https://github.com/nexB/scancode-toolkit/ for support and download. -from __future__ import absolute_import, print_function +from __future__ import absolute_import +from __future__ import print_function from unittest import TestCase as TestCaseClass From 86cfefa68f8eabf3e4729536559b6d567575da8b Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 21 Dec 2016 19:18:06 +0100 Subject: [PATCH 045/436] Cosmetic formatting and documentation. Signed-off-by: Philippe Ombredanne --- src/commoncode/filetype.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/commoncode/filetype.py b/src/commoncode/filetype.py index b7b51d44e30..a9fa4199ae4 100644 --- a/src/commoncode/filetype.py +++ b/src/commoncode/filetype.py @@ -22,7 +22,8 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. -from __future__ import absolute_import, print_function +from __future__ import absolute_import +from __future__ import print_function import os from collections import OrderedDict @@ -32,6 +33,10 @@ from commoncode.functional import memoize +""" +Low level file type utilities, essentially a wrapper around os.path and stat. +""" + def is_link(location): """ Return True if `location` is a symbolic link. @@ -93,8 +98,8 @@ def get_link_target(location): if on_posix and is_link(location): try: # return false on OSes not supporting links - target = os.readlink(location) # @UndefinedVariable - except UnicodeEncodeError: # @UnusedVariable + target = os.readlink(location) + except UnicodeEncodeError: # location is unicode but readlink can fail in some cases pass return target @@ -102,10 +107,12 @@ def get_link_target(location): # Map of type checker function -> short type code # The order of types check matters: link -> file -> directory -> special -TYPES = OrderedDict([(is_link, ('l', 'link',)), - (is_file, ('f', 'file',)), - (is_dir, ('d', 'directory',)), - (is_special, ('s', 'special',))]) +TYPES = OrderedDict([ + (is_link, ('l', 'link',)), + (is_file, ('f', 'file',)), + (is_dir, ('d', 'directory',)), + (is_special, ('s', 'special',)) +]) def get_type(location, short=True): @@ -144,6 +151,7 @@ def is_writable(location): else: return os.access(location, os.R_OK | os.W_OK) + def is_executable(location): """ Return True if the file at location has executable permission set. From 30504a73316443c4eae10e42f9671c3103fb82fd Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 21 Dec 2016 19:19:00 +0100 Subject: [PATCH 046/436] Add upcoming Python 3.6 to detected versions. Signed-off-by: Philippe Ombredanne --- src/commoncode/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commoncode/system.py b/src/commoncode/system.py index 76716c0a426..c7f7e36ea8f 100644 --- a/src/commoncode/system.py +++ b/src/commoncode/system.py @@ -108,7 +108,7 @@ def os_arch(): py27 = (sys.version_info[0] == 2 and sys.version_info[1] == 7) py34 = (sys.version_info[0] == 3 and sys.version_info[1] == 4) py35 = (sys.version_info[0] == 3 and sys.version_info[1] == 5) - +py35 = (sys.version_info[0] == 3 and sys.version_info[1] == 6) # # User related # From 83383899c253af93a82263952289ba9818f7ed78 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 21 Dec 2016 19:30:29 +0100 Subject: [PATCH 047/436] #413 Use native paths and not POSIX paths for file names. * this ensure that a file name can still be extracted from a valid path even a path with backslah on Linux/POSIX. Signed-off-by: Philippe Ombredanne --- src/commoncode/fileutils.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index d394d2fb5ce..686730d3355 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -164,21 +164,27 @@ def read_text_file(location, universal_new_lines=True): def as_posixpath(location): """ - Return a posix-like path using posix path separators (slash or "/") for a - `location` path. This converts Windows paths to look like posix paths that - Python accepts gracefully on Windows for path handling. + Return a POSIX-like path using POSIX path separators (slash or "/") for a + `location` path. This converts Windows paths to look like POSIX paths that + Python accepts gracefully on Windows for paths handling. """ return location.replace(ntpath.sep, posixpath.sep) +def _split_parent_resource(path): + """ + Return a (tuple of parent directory path, resource name). + """ + path = path.rstrip('/').rstrip('\\') + return os.path.split(path) + + def resource_name(path): """ Return the resource name (file name or directory name) from `path` which is the last path segment. """ - path = as_posixpath(path) - path = path.rstrip('/') - _left, right = posixpath.split(path) + _left, right = _split_parent_resource(path) return right or '' @@ -193,9 +199,7 @@ def parent_directory(path): """ Return the parent directory of a file or directory path. """ - path = as_posixpath(path) - path = path.rstrip('/') - left, _ = posixpath.split(path) + left, _right = _split_parent_resource(path) trail = '/' if left != '/' else '' return left + trail @@ -267,7 +271,7 @@ def walk(location, ignored=ignore_nothing): if filetype.is_file(location) : yield parent_directory(location), [], [file_name(location)] - + elif filetype.is_dir(location): dirs = [] files = [] @@ -326,7 +330,7 @@ def resource_iter(location, ignored=ignore_nothing, with_files=True, with_dirs=T :param with_files: If True, include the files. :return: an iterable of file and directory locations. """ - assert with_dirs or with_files, "One or both of 'with_dirs' and 'with_files' is required" + assert with_dirs or with_files, "fileutils.resource_iter: One or both of 'with_dirs' and 'with_files' is required" for top, dirs, files in walk(location, ignored): if with_files: for f in files: From 872c0c0a2c332884395c67b2320732f8b658dec2 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 27 Dec 2016 17:43:51 +0100 Subject: [PATCH 048/436] #413 Work in progress to detect POSIX paths and filename accordingly Signed-off-by: Philippe Ombredanne --- src/commoncode/fileutils.py | 38 ++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index 686730d3355..57951210b63 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -38,6 +38,7 @@ from commoncode import text from commoncode import filetype from commoncode.filetype import is_rwx +from textcode.strings import is_posix_path # this exception is not available on posix @@ -162,11 +163,36 @@ def read_text_file(location, universal_new_lines=True): # PATHS AND NAMES MANIPULATIONS # +def is_posixpath(location): + """ + Return True if the `location` path is likely a POSIX-like path using POSIX path + separators (slash or "/"). + + Return None if the `location` path does not contain any slash or backslash (e.g. + "\" or "/") and the path is either POSIX or Windows. + + Return False if the `location` path is likely a Windows-like path using backslash + as path separators (e.g. "\"). + """ + slashes = location.count('/') + backslashes = location.count('\\') + is_posix = None + if backslashes and slashes: + # this is a case where posix is the only possibility, slash are illegal on Win + is_posix = True + elif backslashes: + is_posix = False + elif slashes: + is_posix = True + + return is_posix + + def as_posixpath(location): """ Return a POSIX-like path using POSIX path separators (slash or "/") for a - `location` path. This converts Windows paths to look like POSIX paths that - Python accepts gracefully on Windows for paths handling. + `location` path. This converts Windows paths to look like POSIX paths: Python + accepts gracefully POSIX paths on Windows. """ return location.replace(ntpath.sep, posixpath.sep) @@ -175,8 +201,9 @@ def _split_parent_resource(path): """ Return a (tuple of parent directory path, resource name). """ + splitter = is_posixpath(path) and posixpath or ntpath path = path.rstrip('/').rstrip('\\') - return os.path.split(path) + return splitter.split(path) def resource_name(path): @@ -197,10 +224,11 @@ def file_name(path): def parent_directory(path): """ - Return the parent directory of a file or directory path. + Return the parent directory path of a file or directory `path`. """ left, _right = _split_parent_resource(path) - trail = '/' if left != '/' else '' + sep = is_posixpath(path) and '/' or '\\' + trail = sep if left != sep else '' return left + trail From ac48304aff6602bc09f9a66271591544e5cdb9a3 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 27 Dec 2016 17:44:15 +0100 Subject: [PATCH 049/436] Cosmetics Signed-off-by: Philippe Ombredanne --- src/commoncode/fetch.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/commoncode/fetch.py b/src/commoncode/fetch.py index 50c6aa1ed96..2f9ce5de58f 100644 --- a/src/commoncode/fetch.py +++ b/src/commoncode/fetch.py @@ -40,25 +40,26 @@ # logger.setLevel(logging.DEBUG) -def download_url(url, file_name=None, verify=True): +def download_url(url, file_name=None, verify=True, timeout=10): """ - Return the temporary location of the file fetched at the remote url. Use - file_name if provided or create a file name base on the last url segment. If - verify is True, SSL certification is performed. Otherwise, no verification - is done but a warning will be printed. + Fetch `url` and return the temporary location where the fetched content was + saved. Use `file_name` if provided or create a new `file_name` base on the last + url segment. If `verify` is True, SSL certification is performed. Otherwise, no + verification is done but a warning will be printed. + `timeout` is the timeout in seconds. """ - requests_args = dict(timeout=10, verify=verify) + requests_args = dict(timeout=timeout, verify=verify) file_name = file_name or fileutils.file_name(url) try: response = requests.get(url, **requests_args) except (ConnectionError, InvalidSchema) as e: - logger.error('fetch: Download failed for %(url)r' % locals()) + logger.error('download_url: Download failed for %(url)r' % locals()) raise status = response.status_code if status != 200: - msg = 'fetch: Download failed for %(url)r with %(status)r' % locals() + msg = 'download_url: Download failed for %(url)r with %(status)r' % locals() logger.error(msg) raise Exception(msg) @@ -72,10 +73,11 @@ def download_url(url, file_name=None, verify=True): def ping_url(url): """ - Returns True is the URL is reachable. + Returns True is `url` is reachable. """ import urllib2 + # FIXME: if there is no 200 HTTP status, then the ULR may not be reachable. try: urllib2.urlopen(url) except Exception: From 30552b13dbcf6adddb1d048bc14f94b1e7678b3f Mon Sep 17 00:00:00 2001 From: pombredanne Date: Fri, 11 Sep 2015 14:36:05 -0700 Subject: [PATCH 050/436] #300 and #413 Handle extraction of file names not legal on windows * file and directory names are now transformed to POSIX portable names * COM, PRN and other illegal Windows names are updated to be legal names. * when scanning and not extracting, file_name is properly extracted by detecting possible backslash in a file name * as a result it is possible to process on Windows archives that contain illegal names. Ror instance the repo at https://github.com/remy/nodemon contains such files and cannot be cloned on Windows. Yet a tarball of this repo is extracted properly by extractcode and can then be scanned alright. Signed-off-by: Philippe Ombredanne --- src/commoncode/text.py | 55 +++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/commoncode/text.py b/src/commoncode/text.py index e66be684e92..bba6dc7dfbf 100644 --- a/src/commoncode/text.py +++ b/src/commoncode/text.py @@ -1,4 +1,4 @@ -# -*- coding: iso-8859-15 -*- +# -*- coding: utf-8 -*- # NOTE: the iso-8859-15 charset is not a mistake. # # Copyright (c) 2015 nexB Inc. and others. All rights reserved. @@ -24,7 +24,8 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. -from __future__ import absolute_import, print_function +from __future__ import absolute_import +from __future__ import print_function import re import logging @@ -91,7 +92,7 @@ def nopunctuation(text): Warning: this also drops line endings. For example: - >>> t = '''This problem is about sequence-bunching, %^$^%**^&*©©^(*&(*()()_+)_!@@#:><>>?/./,.,';][{}{]just''' + >>> t = '''This problem is about sequence-bunching, %^$^%**^&*©©^(*&(*()()_+)_!@@#:><>>?/./,.,';][{}{]just''' >>> nopunctuation(t).split() ['This', 'problem', 'is', 'about', 'sequence', 'bunching', 'just'] >>> t = r'''This problem is about: sequence-bunching @@ -137,27 +138,31 @@ def nolinesep(text): # additional non standard normalizations but quite common sense -unicode_translation_table = {u'ؘ': u'O', u'': u'o', u'': u'z', u'': u'Z'} - -# Other candidates -{ - u'': u'c', - # not space preserving - u'': u'a', - u'Ɔ': u'A', - - u'': u'o', - u'': u'O', - - u'': u'(c)', - u'': u'(r)', - - # see http://en.wikipedia.org/wiki/ß - u'ߟ': u'ss', - u'\u1e9e': u'SS' +unicode_translation_table = { + u'Ø': u'O', + u'ø': u'o', + u'ž': u'z', + u'Ž': u'Z', + u'¢': u'c', + u'¢': u'c', + u'₡': u'c', + u'₵': u'c', + # does not preserve offsets + u'æ': u'ae', + u'Æ': u'AE', + + u'œ': u'oe', + u'Œ': u'OE', + + u'©': u'(c)', + u'®': u'(r)', + + u'ß': u'ss', + u'ẞ': u'ss', } + def toascii(s): u""" Convert a Unicode string to ASCII characters, including replacing accented @@ -171,16 +176,16 @@ def toascii(s): http://code.activestate.com/recipes/251871/#c10 by Aaron Bentley. For example: - >>> acc = u"" + >>> acc = u"ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ" >>> noacc = r"AAAAAACEEEEIIIINOOOOOUUUUYaaaaaaceeeeiiiinooooouuuuyy" >>> toascii(acc) == noacc True """ try: - return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') + converted = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') except: - return str(s.decode('ascii', 'ignore')) - + converted =str(s.decode('ascii', 'ignore')) + return converted def python_safe_name(s): """ From d8f9e2ae752974fd7f11b9f2668c141683737641 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 6 Jan 2017 22:25:20 +0100 Subject: [PATCH 051/436] #413 Improve handling of POSIX path detection Signed-off-by: Philippe Ombredanne --- src/commoncode/fileutils.py | 55 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index 57951210b63..f554fbdf033 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# Copyright (c) 2017 nexB Inc. and others. All rights reserved. # http://nexb.com and https://github.com/nexB/scancode-toolkit/ # The ScanCode software is licensed under the Apache License version 2.0. # Data generated with ScanCode require an acknowledgment. @@ -38,7 +38,6 @@ from commoncode import text from commoncode import filetype from commoncode.filetype import is_rwx -from textcode.strings import is_posix_path # this exception is not available on posix @@ -60,8 +59,8 @@ def create_dir(location): """ - Create directory and all sub-directories recursively at location ensuring - these are readable and writeable. + Create directory and all sub-directories recursively at location ensuring these + are readable and writeable. Raise Exceptions if it fails to create the directory. """ if os.path.exists(location): @@ -163,28 +162,30 @@ def read_text_file(location, universal_new_lines=True): # PATHS AND NAMES MANIPULATIONS # +# TODO: move these functions to paths.py + def is_posixpath(location): """ Return True if the `location` path is likely a POSIX-like path using POSIX path - separators (slash or "/"). - - Return None if the `location` path does not contain any slash or backslash (e.g. - "\" or "/") and the path is either POSIX or Windows. - + separators (slash or "/")or has no path separator. + Return False if the `location` path is likely a Windows-like path using backslash as path separators (e.g. "\"). """ - slashes = location.count('/') - backslashes = location.count('\\') - is_posix = None - if backslashes and slashes: - # this is a case where posix is the only possibility, slash are illegal on Win - is_posix = True - elif backslashes: + has_slashes = '/' in location + has_backslashes = '\\' in location + # windows paths with drive + if location: + drive, _ = ntpath.splitdrive(location) + if drive: + return False + + + # a path is always POSIX unless it contains ONLY backslahes + # which is a rough approximation (it could still be posix) + is_posix = True + if has_backslashes and not has_slashes: is_posix = False - elif slashes: - is_posix = True - return is_posix @@ -197,12 +198,20 @@ def as_posixpath(location): return location.replace(ntpath.sep, posixpath.sep) -def _split_parent_resource(path): +def as_winpath(location): + """ + Return a Windows-like path using Windows path separators (backslash or "\") for a + `location` path. + """ + return location.replace(posixpath.sep, ntpath.sep) + + +def split_parent_resource(path, force_posix=False): """ Return a (tuple of parent directory path, resource name). """ splitter = is_posixpath(path) and posixpath or ntpath - path = path.rstrip('/').rstrip('\\') + path = path.rstrip('/\\') return splitter.split(path) @@ -211,7 +220,7 @@ def resource_name(path): Return the resource name (file name or directory name) from `path` which is the last path segment. """ - _left, right = _split_parent_resource(path) + _left, right = split_parent_resource(path) return right or '' @@ -226,7 +235,7 @@ def parent_directory(path): """ Return the parent directory path of a file or directory `path`. """ - left, _right = _split_parent_resource(path) + left, _right = split_parent_resource(path) sep = is_posixpath(path) and '/' or '\\' trail = sep if left != sep else '' return left + trail From 2b99f70d943036d8758f0416ebcb8e04d0b68cb3 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 6 Jan 2017 22:26:51 +0100 Subject: [PATCH 052/436] #413 update toascii to optionally transliterate unicode to ascii Signed-off-by: Philippe Ombredanne --- src/commoncode/text.py | 75 +++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/src/commoncode/text.py b/src/commoncode/text.py index bba6dc7dfbf..4c33b10bdda 100644 --- a/src/commoncode/text.py +++ b/src/commoncode/text.py @@ -30,9 +30,10 @@ import re import logging import unicodedata +from text_unidecode import unidecode """ -A text processing module providing functions to process and prepare text +A text processing module providing functions to process and prepare text before indexing or fingerprinting such as: - case folding - conversion of iso latin and unicode to ascii @@ -96,7 +97,7 @@ def nopunctuation(text): >>> nopunctuation(t).split() ['This', 'problem', 'is', 'about', 'sequence', 'bunching', 'just'] >>> t = r'''This problem is about: sequence-bunching - ... + ... ... just ... ''' >>> nopunctuation(t) @@ -137,56 +138,46 @@ def nolinesep(text): return text.replace(CR, ' ').replace(LF, ' ') -# additional non standard normalizations but quite common sense -unicode_translation_table = { - u'Ø': u'O', - u'ø': u'o', - u'ž': u'z', - u'Ž': u'Z', - u'¢': u'c', - u'¢': u'c', - u'₡': u'c', - u'₵': u'c', - # does not preserve offsets - u'æ': u'ae', - u'Æ': u'AE', - - u'œ': u'oe', - u'Œ': u'OE', - - u'©': u'(c)', - u'®': u'(r)', - - u'ß': u'ss', - u'ẞ': u'ss', -} - +def toascii(s, translit=False): + u""" Convert a Unicode string to ASCII characters, including replacing accented + characters with their non-accented equivalent. + + If `translit` is False use the Unicode NFKD equivalence. + + If `translit` is True, use a transliteration with the unidecode library. + + Non ISO-Latin and non ASCII characters are stripped from the + output. + When no transliteration is possible, the resulting character is replaced + by an underscore "_". + + For Unicode NFKD equivalence, see http://en.wikipedia.org/wiki/Unicode_equivalence + The convertion may NOT preserve the original string length and with NFKD some + characters may be deleted. -def toascii(s): - u""" - Convert a Unicode string to ASCII characters, including replacing accented - characters with their non-accented NFKD equivalent. Non ISO-Latin and non - ASCII characters are stripped from the output. For Unicode NFKD - equivalence, see http://en.wikipedia.org/wiki/Unicode_equivalence - - Does not preserve the original length and character offsets. - - Inspired from: - http://code.activestate.com/recipes/251871/#c10 by Aaron Bentley. + Inspired from: http://code.activestate.com/recipes/251871/#c10 by Aaron Bentley. For example: - >>> acc = u"ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ" - >>> noacc = r"AAAAAACEEEEIIIINOOOOOUUUUYaaaaaaceeeeiiiinooooouuuuyy" - >>> toascii(acc) == noacc + >>> acc = u"ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿẞß®©œŒØøÆæ₵₡¢¢Žž" + >>> noacc = r'AAAAAACEEEEIIIINOOOOOUUUUYaaaaaaceeeeiiiinooooouuuuyyZz' + >>> toascii(acc, translit=False) == noacc + True + >>> noacc = r'AAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyySsss(r)(c)oeOEOoAEae_CL/CC/Zz' + >>> toascii(acc, translit=True) == noacc True """ try: - converted = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') + if translit: + converted = unidecode(s).encode('ascii', 'ignore') + converted = converted.replace('[?]', '_') + else: + converted = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') except: - converted =str(s.decode('ascii', 'ignore')) + converted = str(s.decode('ascii', 'ignore')) return converted + def python_safe_name(s): """ Return a name derived from string `s` safe to use as a Python identifier. From 8dd64d2ffd55190da06b517300c64de637d95ed3 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 6 Jan 2017 22:28:39 +0100 Subject: [PATCH 053/436] #413 Improve paths resolution and safe paths creation * separate code from "new_name" for portable file name transforms * update safe_path accordingly * improve relative paths resolution for corner cases Signed-off-by: Philippe Ombredanne --- tests/commoncode/commoncode/test_paths.py | 193 +++++++++++++--------- 1 file changed, 113 insertions(+), 80 deletions(-) diff --git a/tests/commoncode/commoncode/test_paths.py b/tests/commoncode/commoncode/test_paths.py index eee5531e19c..85ea8053e13 100644 --- a/tests/commoncode/commoncode/test_paths.py +++ b/tests/commoncode/commoncode/test_paths.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# Copyright (c) 2017 nexB Inc. and others. All rights reserved. # http://nexb.com and https://github.com/nexB/scancode-toolkit/ # The ScanCode software is licensed under the Apache License version 2.0. # Data generated with ScanCode require an acknowledgment. @@ -31,79 +31,115 @@ class TestPortablePath(TestCase): - def test_safe_path(self): - # tuples of test data, expected results - tests = [ - # mixed slashes - (r'C:\Documents and Settings\Boki\Desktop\head\patches\drupal6/drupal.js', - 'c_/documents_and_settings/boki/desktop/head/patches/drupal6/drupal.js'), - # mixed slashes and spaces - (r'C:\Documents and Settings\Boki\Desktop\head\patches\parallel uploads/drupal.js', - 'c_/documents_and_settings/boki/desktop/head/patches/parallel_uploads/drupal.js'), - # windows style - (r'C:\Documents and Settings\Administrator\Desktop\siftDemoV4_old\defs.h', - 'c_/documents_and_settings/administrator/desktop/siftdemov4_old/defs.h'), - # windows style, mixed slashes, no spaces - (r'C:\Documents and Settings\Boki\Desktop\head\patches\imagefield/imagefield.css', - 'c_/documents_and_settings/boki/desktop/head/patches/imagefield/imagefield.css'), - # windows style, spaces - (r'C:\Documents and Settings\Boki\Desktop\head\patches\js delete\imagefield.css', - 'c_/documents_and_settings/boki/desktop/head/patches/js_delete/imagefield.css'), - # windows style, posix slashes - (r'C:/Documents and Settings/Alex Burgel/workspace/Hibernate3.2/test/org/hibernate/test/AllTests.java', - 'c_/documents_and_settings/alex_burgel/workspace/hibernate3.2/test/org/hibernate/test/alltests.java'), - # windows style, relative - (r'includes\webform.components.inc', - 'includes/webform.components.inc'), - # windows style, absolute, trailing slash - ('\\includes\\webform.components.inc\\', - 'includes/webform.components.inc'), - # posix style, relative - (r'includes/webform.components.inc', - 'includes/webform.components.inc'), - # posix style, absolute, trailing slash - (r'/includes/webform.components.inc/', - 'includes/webform.components.inc'), - # posix style, french char - ('/includes/webform.compon\xc3nts.inc/', - 'includes/webform.compon_nts.inc'), - # posix style, chinese char - ('/includes/webform.compon\xd2\xaants.inc/', - 'includes/webform.compon__nts.inc'), - # windows style, dots - ('\\includes\\..\\webform.components.inc\\', - 'webform.components.inc'), - # windows style, many dots - ('.\\includes\\.\\..\\..\\..\webform.components.inc\\.', - 'dotdot/dotdot/webform.components.inc'), - # posix style, dots - (r'includes/../webform.components.inc', - 'webform.components.inc'), - # posix style, many dots - (r'./includes/./../../../../webform.components.inc/.', - 'dotdot/dotdot/dotdot/webform.components.inc'), - ] - for tst, expected in tests: - assert expected == paths.safe_path(tst) - - def test_resolve(self): - # tuples of test data, expected results - tests = [ - ('C:\\..\\./drupal.js', - 'drupal.js'), - ('\\includes\\..\\webform.components.inc\\', - 'webform.components.inc'), - ('includes/../webform.components.inc', - 'webform.components.inc'), - ('////.//includes/./../..//..///../webform.components.inc/.', - 'dotdot/dotdot/dotdot/webform.components.inc'), - (u'////.//includes/./../..//..///../webform.components.inc/.', - u'dotdot/dotdot/dotdot/webform.components.inc'), - ('includes/../', - '.'), - ] - for tst, expected in tests: - assert expected == paths.resolve(tst) + def test_safe_path_mixed_slashes(self): + test = paths.safe_path('C:\\Documents and Settings\\Boki\\Desktop\\head\\patches\\drupal6/drupal.js') + expected = 'C/Documents_and_Settings/Boki/Desktop/head/patches/drupal6/drupal.js' + assert expected == test + + def test_safe_path_mixed_slashes_and_spaces(self): + test = paths.safe_path('C:\\Documents and Settings\\Boki\\Desktop\\head\\patches\\parallel uploads/drupal.js') + expected = 'C/Documents_and_Settings/Boki/Desktop/head/patches/parallel_uploads/drupal.js' + assert expected == test + + def test_safe_path_windows_style(self): + test = paths.safe_path('C:\\Documents and Settings\\Administrator\\Desktop\\siftDemoV4_old\\defs.h') + expected = 'C/Documents_and_Settings/Administrator/Desktop/siftDemoV4_old/defs.h' + assert expected == test + + def test_safe_path_windows_style_mixed_slashes_no_spaces(self): + test = paths.safe_path('C:\\Documents and Settings\\Boki\\Desktop\\head\\patches\\imagefield/imagefield.css') + expected = 'C/Documents_and_Settings/Boki/Desktop/head/patches/imagefield/imagefield.css' + assert expected == test + + def test_safe_path_windows_style_spaces(self): + test = paths.safe_path('C:\\Documents and Settings\\Boki\\Desktop\\head\\patches\\js delete\\imagefield.css') + expected = 'C/Documents_and_Settings/Boki/Desktop/head/patches/js_delete/imagefield.css' + assert expected == test + + def test_safe_path_windows_style_posix_slashes(self): + test = paths.safe_path('C:/Documents and Settings/Alex Burgel/workspace/Hibernate3.2/test/org/hibernate/test/AllTests.java') + expected = 'C/Documents_and_Settings/Alex_Burgel/workspace/Hibernate3.2/test/org/hibernate/test/AllTests.java' + assert expected == test + + def test_safe_path_windows_style_relative(self): + test = paths.safe_path('includes\\webform.components.inc') + expected = 'includes/webform.components.inc' + assert expected == test + + def test_safe_path_windows_style_absolute_trailing_slash(self): + test = paths.safe_path('\\includes\\webform.components.inc\\') + expected = 'includes/webform.components.inc' + assert expected == test + + def test_safe_path_posix_style_relative(self): + test = paths.safe_path('includes/webform.components.inc') + expected = 'includes/webform.components.inc' + assert expected == test + + def test_safe_path_posix_style_absolute_trailing_slash(self): + test = paths.safe_path('/includes/webform.components.inc/') + expected = 'includes/webform.components.inc' + assert expected == test + + def test_safe_path_posix_style_french_char(self): + test = paths.safe_path('/includes/webform.compon\xc3nts.inc/') + expected = 'includes/webform.componAnts.inc' + assert expected == test + + def test_safe_path_posix_style_chinese_char(self): + test = paths.safe_path('/includes/webform.compon\xd2\xaants.inc/') + expected = 'includes/webform.componS_nts.inc' + assert expected == test + + def test_safe_path_windows_style_dots(self): + test = paths.safe_path('\\includes\\..\\webform.components.inc\\') + expected = 'webform.components.inc' + assert expected == test + + def test_safe_path_windows_style_many_dots(self): + test = paths.safe_path('.\\includes\\.\\..\\..\\..\\webform.components.inc\\.') + expected = 'dotdot/dotdot/webform.components.inc' + assert expected == test + + def test_safe_path_posix_style_dots(self): + test = paths.safe_path('includes/../webform.components.inc') + expected = 'webform.components.inc' + assert expected == test + + def test_safe_path_posix_style_many_dots(self): + test = paths.safe_path('./includes/./../../../../webform.components.inc/.') + expected = 'dotdot/dotdot/dotdot/webform.components.inc' + assert expected == test + + def test_resolve_mixed_slash(self): + test = paths.resolve('C:\\..\\./drupal.js') + expected = 'C/drupal.js' + assert expected == test + + def test_resolve_2(self): + test = paths.resolve('\\includes\\..\\webform.components.inc\\') + expected = 'webform.components.inc' + assert expected == test + + def test_resolve_3(self): + test = paths.resolve('includes/../webform.components.inc') + expected = 'webform.components.inc' + assert expected == test + + def test_resolve_4(self): + test = paths.resolve('////.//includes/./../..//..///../webform.components.inc/.') + expected = 'dotdot/dotdot/dotdot/webform.components.inc' + assert expected == test + + def test_resolve_5(self): + test = paths.resolve(u'////.//includes/./../..//..///../webform.components.inc/.') + expected = u'dotdot/dotdot/dotdot/webform.components.inc' + assert expected == test + + def test_resolve_6(self): + test = paths.resolve('includes/../') + expected = '.' + assert expected == test class TestCommonPath(TestCase): @@ -213,13 +249,11 @@ def test_common_path_suffix_handles_relative_path(self): assert ('a/b', 2) == test def test_common_path_suffix_handles_relative_subpath(self): - test = paths.common_path_suffix('zsds/adsds/a/b/b/c', - 'a//a/d//b/c') + test = paths.common_path_suffix('zsds/adsds/a/b/b/c', 'a//a/d//b/c') assert ('b/c', 2) == test def test_common_path_suffix_ignore_and_strip_trailing_slash(self): - test = paths.common_path_suffix('zsds/adsds/a/b/b/c/', - 'a//a/d//b/c/') + test = paths.common_path_suffix('zsds/adsds/a/b/b/c/', 'a//a/d//b/c/') assert ('b/c', 2) == test def test_common_path_suffix_return_None_if_no_common_suffix(self): @@ -232,8 +266,7 @@ def test_common_path_suffix_return_None_if_no_common_suffix2(self): def test_common_path_suffix_match_only_whole_segments(self): # only segments are honored, commonality within segment is ignored - test = paths.common_path_suffix( - 'this/is/aaaa/great/path', 'this/is/aaaaa/great/path') + test = paths.common_path_suffix('this/is/aaaa/great/path', 'this/is/aaaaa/great/path') assert ('great/path', 2) == test def test_common_path_suffix_two_root(self): From 97716210b1494a64c09b8a4f268a173ca2f2802a Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Fri, 6 Jan 2017 22:28:39 +0100 Subject: [PATCH 054/436] #413 Improve paths resolution and safe paths creation * separate code from "new_name" for portable file name transforms * update safe_path accordingly * improve relative paths resolution for corner cases Signed-off-by: Philippe Ombredanne --- src/commoncode/paths.py | 248 +++++++++++++++++++++++++++++----------- 1 file changed, 181 insertions(+), 67 deletions(-) diff --git a/src/commoncode/paths.py b/src/commoncode/paths.py index f05c450ad1d..5e9799aebcb 100644 --- a/src/commoncode/paths.py +++ b/src/commoncode/paths.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# Copyright (c) 2017 nexB Inc. and others. All rights reserved. # http://nexb.com and https://github.com/nexB/scancode-toolkit/ # The ScanCode software is licensed under the Apache License version 2.0. # Data generated with ScanCode require an acknowledgment. @@ -22,34 +22,96 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. -from __future__ import absolute_import, print_function - +from __future__ import absolute_import +from __future__ import print_function from os.path import commonprefix -import string +import ntpath import posixpath +import re + +from commoncode.fileutils import is_posixpath +from commoncode.text import toascii +from commoncode.fileutils import as_winpath +from commoncode.fileutils import as_posixpath -from commoncode import fileutils -from commoncode import text """ Various path utilities such as common prefix and suffix functions, conversion to OS-safe paths and to POSIX paths. """ +# +# Build OS-portable and safer paths + +def safe_path(path, posix=False): + """ + Convert `path` to a safe and portable POSIX path usable on multiple OSes. The + returned path is an ASCII-only byte string, resolved for relative segments and + itself relative. + + The `path` is treated as a POSIX path if `posix` is True or as a Windows path + with blackslash separators otherwise. + """ + # if the path is UTF, try to use unicode instead + if not isinstance(path, unicode): + try: + path = unicode(path, 'utf-8') + except: + pass + + path = path.strip() + + if not is_posixpath(path): + path = as_winpath(path) + posix = False + + path = resolve(path, posix) + + _pathmod, path_sep = path_handlers(path, posix) + + segments = [s.strip() for s in path.split(path_sep) if s.strip()] + segments = [portable_filename(s) for s in segments] -def resolve(path): + # print('safe_path: orig:', orig_path, 'segments:', segments) + + if not segments: + return '_' + + # always return posix + sep = isinstance(path, unicode) and u'/' or '/' + path = sep.join(segments) + return as_posixpath(path) + + +def path_handlers(path, posix=True): + """ + Return a path module and path separator to use for handling (e.g. split and join) + `path` using either POSIX or Windows conventions depending on the `path` content. + Force usage of POSIX conventions if `posix` is True. + """ + # determine if we use posix or windows path handling + is_posix = is_posixpath(path) + use_posix = posix or is_posix + pathmod = use_posix and posixpath or ntpath + path_sep = use_posix and '/' or '\\' + path_sep = isinstance(path, unicode) and unicode(path_sep) or path_sep + return pathmod, path_sep + + +def resolve(path, posix=True): """ - Resolve and return a path-like string `path` to a posix relative path - string where extra slashes including leading and trailing slashes, dot - '.' and dotdot '..' path segments have been removed or normalized or - resolved with the provided path "tree". When a dotdot path segment cannot - be further resolved by "escaping" the provided path tree, it is replaced - by the string 'dotdot'. + Return a resolved relative POSIX path from `path` where extra slashes including + leading and trailing slashes are removed, dot '.' and dotdot '..' path segments + have been removed or resolved as possible. When a dotdot path segment cannot be + further resolved and would be "escaping" from the provided path "tree", it is + replaced by the string 'dotdot'. + + The `path` is treated as a POSIX path if `posix` is True (default) or as a + Windows path with blackslash separators otherwise. """ - slash, dot, dotdot = '/', '.', 'dotdot' - if isinstance(path, unicode): - slash, dot, dotdot = u'/', u'.', u'dotdot' + is_unicode = isinstance(path, unicode) + dot = is_unicode and u'.' or '.' if not path: return dot @@ -58,70 +120,120 @@ def resolve(path): if not path: return dot - path = fileutils.as_posixpath(path) - path = path.strip(slash) - segments = [s.strip() for s in path.split(slash)] + if not is_posixpath(path): + path = as_winpath(path) + posix = False + + pathmod, path_sep = path_handlers(path, posix) + + path = path.strip(path_sep) + segments = [s.strip() for s in path.split(path_sep) if s.strip()] + # remove empty (// or ///) or blank (space only) or single dot segments segments = [s for s in segments if s and s != '.'] - path = slash.join(segments) - # resolves .. - path = posixpath.normpath(path) - # replace .. with literal dotdot - segments = path.split(slash) - segments = [dotdot if s == '..' else s for s in segments] - path = slash.join(segments) - return path + path = path_sep.join(segments) -# -# Build OS-portable and safer paths -# + # resolves . dot, .. dotdot + path = pathmod.normpath(path) -""" -To convert a path to a safe cross-os path, we use a characters translation -table. This will replaces all non-safe characters by an underscore char. -""" -allchars = string.maketrans('', '') + segments = path.split(path_sep) + + # remove empty or blank segments + segments = [s.strip() for s in segments if s and s.strip()] + + # is this a windows absolute path? if yes strip the colon to make this relative + if segments and len(segments[0]) == 2 and segments[0].endswith(':'): + segments[0] = segments[0][:-1] + + # replace any remaining (usually leading) .. segment with a literal "dotdot" + dotdot = is_unicode and u'dotdot' or 'dotdot' + segments = [dotdot if s == '..' else s for s in segments if s] + if segments: + path = path_sep.join(segments) + else: + path = dot + + path = as_posixpath(path) + return path -# table of non safe characters: Exclude digit,letters and a select subset of -# supported punctuation, all the rest is junk. nb: we consider the backslash -# as path safe for now, but we convert these later to posix path -not_path_safe = string.translate(allchars, - allchars, - '0123456789' - 'abcdefghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#+-./_\\') -# create translation table to replace non-safe chars with underscore char -path_safe = string.maketrans(not_path_safe, '_' * len(not_path_safe)) +legal_punctuation = "!\#$%&\(\)\+,\-\.;\=@\[\]_\{\}\~" +legal_chars = 'A-Za-z0-9' + legal_punctuation +illegal_chars_re = '[^' + legal_chars + ']' +replace_illegal_chars = re.compile(illegal_chars_re).sub -def safe_path(path, lowered=True, resolved=True): +def portable_filename(filename): """ - Convert a path-like string `path` to a posix path string safer to use as a - file path on all OSes. The path is lowercased. Non-ASCII alphanumeric - characters and spaces are replaced with an underscore. - The path is optionally resolved and lowercased. + Return a new name for `filename` that is portable across operating systems. + + In particular the returned file name is guaranteed to be: + - a portable name on most OSses using a limited ASCII characters set including + some limited punctuation. + - a valid name on Linux, Windows and Mac. + + Unicode file names are transliterated to plain ASCII. + + See for more details: + - http://www.opengroup.org/onlinepubs/007904975/basedefs/xbd_chap03.html + - https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx + - http://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm + + Also inspired by Werkzeug: + https://raw.githubusercontent.com/pallets/werkzeug/8c2d63ce247ba1345e1b9332a68ceff93b2c07ab/werkzeug/utils.py + + For example: + >>> portable_filename("A:\\ file/ with Spaces.mov") + 'A___file__with_Spaces.mov' + + Unresolved reslative paths will be trated as a single filename. Use resolve + instead if you want to resolve paths: + + >>> portable_filename("../../../etc/passwd") + '___.._.._etc_passwd' + + Unicode name are transliterated: + + >>> portable_filename(u'This contain UMLAUT \xfcml\xe4uts.txt') + 'This_contain_UMLAUT_umlauts.txt' """ - safe = path.strip() - # TODO: replace COM/PRN/LPT windows special names - # TODO: resolve 'UNC' windows paths - # TODO: strip leading windows drives - # remove any unsafe chars - safe = safe.translate(path_safe) - safe = text.toascii(safe) - safe = fileutils.as_posixpath(safe) - if lowered: - safe = safe.lower() - if resolved: - safe = resolve(safe) - return safe + filename = toascii(filename, translit=True) + + if not filename: + return '_' + + filename = replace_illegal_chars('_', filename) + # these are illegal both upper and lowercase and with or without an extension + # we insert an underscore after the base name. + windows_illegal_names = set([ + 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', + 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9', + 'aux', 'con', 'nul', 'prn' + ]) + + basename, dot, extension = filename.partition('.') + if basename.lower() in windows_illegal_names: + filename = ''.join([basename, '_', dot, extension]) + + + # no name made only of dots. + if set(filename) == set(['.']): + filename = 'dot' * len(filename) + + # replaced any leading dotdot + if filename != '..' and filename.startswith('..'): + while filename.startswith('..'): + filename = filename.replace('..', '__', 1) + + return filename # -# paths comparisons +# paths comparisons, common prefix and suffix extraction # + def common_prefix(s1, s2): """ Return the common leading subsequence of two sequences and its length. @@ -166,8 +278,8 @@ def common_path_suffix(p1, p2): def split(p): """ - Split a posix path in a sequence of segments, ignoring leading and - trailing slash. Return an empty sequence for an empty path and the root /. + Split a posix path in a sequence of segments, ignoring leading and trailing + slash. Return an empty sequence for an empty path and the root path /. """ if not p: return [] @@ -177,7 +289,9 @@ def split(p): def _common_path(p1, p2, common_func): """ - Common function to compute common leading or trailing paths. + Return a common leading or trailing path brtween paths `p1` and `p2` and the + common length in number of segments using the `common_func` path comparison + function. """ common, lgth = common_func(split(p1), split(p2)) common = '/'.join(common) if common else None From 7a97577e5ee2d2bff7d058a8a2a2b19ff5b3f4a9 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Mon, 9 Jan 2017 22:31:37 +0100 Subject: [PATCH 055/436] #413 more traceable really unit tests Signed-off-by: Philippe Ombredanne --- tests/commoncode/commoncode/test_fileutils.py | 458 ++++++++++++++---- 1 file changed, 373 insertions(+), 85 deletions(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 15a98bfe6f4..653195e5e96 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# Copyright (c) 2017 nexB Inc. and others. All rights reserved. # http://nexb.com and https://github.com/nexB/scancode-toolkit/ # The ScanCode software is licensed under the Apache License version 2.0. # Data generated with ScanCode require an acknowledgment. @@ -381,89 +381,377 @@ def test_file_iter_can_walk_an_empty_dir(self): assert expected == result -class TestName(FileBasedTesting): +class TestBaseName(FileBasedTesting): test_data_dir = os.path.join(os.path.dirname(__file__), 'data') - def test_file_base_name_on_path_and_location(self): - test_dir = self.get_test_loc('fileutils/basename', copy=True) - tests = [ - ('a/.a/file', 'file'), - ('a/.a/', '.a'), - ('a/b/.a.b', '.a'), - ('a/b/a.tag.gz', 'a.tag'), - ('a/b/', 'b'), - ('a/f.a', 'f'), - ('a/', 'a'), - ('f.a/a.c', 'a'), - ('f.a/', 'f.a'), - ('tst', 'tst'), - ] - for test_file, name in tests: - result = fileutils.file_base_name(test_file) - assert name == result - # also test on location - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) - assert name == result - - def test_file_name_on_path_and_location(self): - test_dir = self.get_test_loc('fileutils/basename', copy=True) - tests = [ - ('a/.a/file', 'file'), - ('a/.a/', '.a'), - ('a/b/.a.b', '.a.b'), - ('a/b/a.tag.gz', 'a.tag.gz'), - ('a/b/', 'b'), - ('a/f.a', 'f.a'), - ('a/', 'a'), - ('f.a/a.c', 'a.c'), - ('f.a/', 'f.a'), - ('tst', 'tst'), - ] - for test_file, name in tests: - result = fileutils.file_name(test_file) - assert name == result - # also test on location - result = fileutils.file_name((os.path.join(test_dir, test_file))) - assert name == result - - def test_file_extension_on_path_and_location(self): - test_dir = self.get_test_loc('fileutils/basename', copy=True) - tests = [ - ('a/.a/file', ''), - ('a/.a/', ''), - ('a/b/.a.b', '.b'), - ('a/b/a.tag.gz', '.gz'), - ('a/b/', ''), - ('a/f.a', '.a'), - ('a/', ''), - ('f.a/a.c', '.c'), - ('f.a/', ''), - ('tst', ''), - ] - for test_file, name in tests: - result = fileutils.file_extension(test_file) - assert name == result - # also test on location - result = fileutils.file_extension((os.path.join(test_dir, test_file))) - assert name == result - - def test_parent_directory_on_path_and_location(self): - test_dir = self.get_test_loc('fileutils/basename', copy=True) - tests = [ - ('a/.a/file', 'a/.a/'), - ('a/.a/', 'a/'), - ('a/b/.a.b', 'a/b/'), - ('a/b/a.tag.gz', 'a/b/'), - ('a/b/', 'a/'), - ('a/f.a', 'a/'), - ('a/', '/'), - ('f.a/a.c', 'f.a/'), - ('f.a/', '/'), - ('tst', '/'), - ] - for test_file, name in tests: - result = fileutils.parent_directory(test_file) - assert name == result - # also test on location - result = fileutils.parent_directory((os.path.join(test_dir, test_file))) - assert result.endswith(name) + def test_file_base_name_on_path_and_location_1(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/file' + expected_name = 'file' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_2(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/' + expected_name = '.a' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_3(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/.a.b' + expected_name = '.a' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_4(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/a.tag.gz' + expected_name = 'a.tag' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_5(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/' + expected_name = 'b' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_6(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/f.a' + expected_name = 'f' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_7(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/' + expected_name = 'a' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_8(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/a.c' + expected_name = 'a' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_9(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/' + expected_name = 'f.a' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_base_name_on_path_and_location_10(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'tst' + expected_name = 'tst' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + +class TestFileName(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_file_name_on_path_and_location_1(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/file' + expected_name = 'file' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_2(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/' + expected_name = '.a' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_3(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/.a.b' + expected_name = '.a.b' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_4(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/a.tag.gz' + expected_name = 'a.tag.gz' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_5(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/' + expected_name = 'b' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_6(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/f.a' + expected_name = 'f.a' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_7(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/' + expected_name = 'a' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_8(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/a.c' + expected_name = 'a.c' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_9(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/' + expected_name = 'f.a' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_name_on_path_and_location_10(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'tst' + expected_name = 'tst' + result = fileutils.file_name(test_file) + assert expected_name == result + result = fileutils.file_name((os.path.join(test_dir, test_file))) + assert expected_name == result + + +class TestFileExtension(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_file_extension_on_path_and_location_1(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/file' + expected_name = '' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_2(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/' + expected_name = '' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_3(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/.a.b' + expected_name = '.b' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_4(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/a.tag.gz' + expected_name = '.gz' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_5(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/' + expected_name = '' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_6(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/f.a' + expected_name = '.a' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_7(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/' + expected_name = '' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_8(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/a.c' + expected_name = '.c' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_9(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/' + expected_name = '' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + def test_file_extension_on_path_and_location_10(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'tst' + expected_name = '' + result = fileutils.file_extension(test_file) + assert expected_name == result + result = fileutils.file_extension((os.path.join(test_dir, test_file))) + assert expected_name == result + + +class TestParentDir(FileBasedTesting): + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_parent_directory_on_path_and_location_1(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/file' + expected_name = 'a/.a/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_2(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/.a/' + expected_name = 'a/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_3(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/.a.b' + expected_name = 'a/b/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_4(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/a.tag.gz' + expected_name = 'a/b/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_5(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/' + expected_name = 'a/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_6(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/f.a' + expected_name = 'a/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_7(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/' + expected_name = '/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_8(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/a.c' + expected_name = 'f.a/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_9(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/' + expected_name = '/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) + + def test_parent_directory_on_path_and_location_10(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'tst' + expected_name = '/' + result = fileutils.parent_directory(test_file) + assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + assert result.endswith(expected_name) From 848849fed8ff2cfcfe9c1e0c9a662ed3116a4655 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 11 Jan 2017 14:42:59 +0100 Subject: [PATCH 056/436] #413 Improve parent directory tests expectations for Windows * test a posix version of the paths for portable assertions Signed-off-by: Philippe Ombredanne --- tests/commoncode/commoncode/test_fileutils.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 653195e5e96..73b1d6935b7 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -671,8 +671,11 @@ def test_parent_directory_on_path_and_location_1(self): test_file = 'a/.a/file' expected_name = 'a/.a/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_2(self): @@ -680,8 +683,11 @@ def test_parent_directory_on_path_and_location_2(self): test_file = 'a/.a/' expected_name = 'a/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_3(self): @@ -689,8 +695,11 @@ def test_parent_directory_on_path_and_location_3(self): test_file = 'a/b/.a.b' expected_name = 'a/b/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_4(self): @@ -698,8 +707,11 @@ def test_parent_directory_on_path_and_location_4(self): test_file = 'a/b/a.tag.gz' expected_name = 'a/b/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_5(self): @@ -707,8 +719,11 @@ def test_parent_directory_on_path_and_location_5(self): test_file = 'a/b/' expected_name = 'a/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_6(self): @@ -716,8 +731,11 @@ def test_parent_directory_on_path_and_location_6(self): test_file = 'a/f.a' expected_name = 'a/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_7(self): @@ -725,8 +743,11 @@ def test_parent_directory_on_path_and_location_7(self): test_file = 'a/' expected_name = '/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_8(self): @@ -734,8 +755,11 @@ def test_parent_directory_on_path_and_location_8(self): test_file = 'f.a/a.c' expected_name = 'f.a/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_9(self): @@ -743,8 +767,11 @@ def test_parent_directory_on_path_and_location_9(self): test_file = 'f.a/' expected_name = '/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) def test_parent_directory_on_path_and_location_10(self): @@ -752,6 +779,9 @@ def test_parent_directory_on_path_and_location_10(self): test_file = 'tst' expected_name = '/' result = fileutils.parent_directory(test_file) + result = fileutils.as_posixpath(result) assert expected_name == result + result = fileutils.parent_directory((os.path.join(test_dir, test_file))) + result = fileutils.as_posixpath(result) assert result.endswith(expected_name) From 6f569d79a31bcf1713e474d7228a1969865914c4 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 11 Jan 2017 18:01:18 +0100 Subject: [PATCH 057/436] Improve path splits (follow up from #413) Signed-off-by: Philippe Ombredanne --- src/commoncode/fileutils.py | 69 +++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index f554fbdf033..a5b39e04f8d 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -180,7 +180,6 @@ def is_posixpath(location): if drive: return False - # a path is always POSIX unless it contains ONLY backslahes # which is a rough approximation (it could still be posix) is_posix = True @@ -208,78 +207,104 @@ def as_winpath(location): def split_parent_resource(path, force_posix=False): """ - Return a (tuple of parent directory path, resource name). + Return a tuple of (parent directory path, resource name). """ - splitter = is_posixpath(path) and posixpath or ntpath + use_posix = force_posix or is_posixpath(path) + splitter = use_posix and posixpath or ntpath path = path.rstrip('/\\') return splitter.split(path) -def resource_name(path): +def resource_name(path, force_posix=False): """ Return the resource name (file name or directory name) from `path` which is the last path segment. """ - _left, right = split_parent_resource(path) + _left, right = split_parent_resource(path,force_posix) return right or '' -def file_name(path): +def file_name(path, force_posix=False): """ Return the file name (or directory name) of a path. """ - return resource_name(path) + return resource_name(path, force_posix) -def parent_directory(path): +def parent_directory(path, force_posix=False): """ Return the parent directory path of a file or directory `path`. """ - left, _right = split_parent_resource(path) - sep = is_posixpath(path) and '/' or '\\' + left, _right = split_parent_resource(path, force_posix) + use_posix = force_posix or is_posixpath(path) + sep = use_posix and '/' or '\\' trail = sep if left != sep else '' return left + trail -def file_base_name(path): +def file_base_name(path, force_posix=False): """ Return the file base name for a path. The base name is the base name of the file minus the extension. For a directory return an empty string. """ - return splitext(path)[0] + return splitext(path, force_posix)[0] -def file_extension(path): +def file_extension(path, force_posix=False): """ Return the file extension for a path. """ - return splitext(path)[1] + return splitext(path, force_posix)[1] -def splitext(path): +def splitext(path, force_posix=False): """ Return a tuple of strings (basename, extension) for a path. The basename is the file name minus its extension. Return an empty extension string for a directory. A directory is identified by ending with a path separator. Not the same as os.path.splitext. + + For example: + >>> splitext('C:\\dir\path.ext') + ('path', '.ext') + + Directories even with dotted names have no extension: + >>> import ntpath + >>> splitext('C:\\dir\\path.ext' + ntpath.sep) + ('path.ext', '') + + >>> splitext('/dir/path.ext/') + ('path.ext', '') + + >>> splitext('/some/file.txt') + ('file', '.txt') + + Composite extensions for tarballs are properly handled: + >>> splitext('archive.tar.gz') + ('archive', '.tar.gz') """ base_name = '' extension = '' if not path: return base_name, extension - path = as_posixpath(path) - name = resource_name(path) - if path.endswith('/'): - # directories have no extension + ppath= as_posixpath(path) + name = resource_name(path, force_posix) + name = name.strip('\\/') + if ppath.endswith('/'): + # directories never have an extension base_name = name extension = '' elif name.startswith('.') and '.' not in name[1:]: - base_name = '' - extension = name + base_name = name + extension = '' else: base_name, extension = posixpath.splitext(name) - return base_name or '', extension or '' + # handle composed extensions of tar.gz, bz, zx,etc + if base_name.endswith('.tar'): + base_name, extension2 = posixpath.splitext(base_name) + extension = extension2 + extension + return base_name, extension # # DIRECTORY AND FILES WALKING/ITERATION From 8d51c683edc113219632f467c07f4393ee1cbd02 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Mon, 16 Jan 2017 18:02:38 +0100 Subject: [PATCH 058/436] Add missing digestsize to hasher wrapper Signed-off-by: Philippe Ombredanne --- src/commoncode/hash.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commoncode/hash.py b/src/commoncode/hash.py index 879779e3764..23d54cb5c9e 100644 --- a/src/commoncode/hash.py +++ b/src/commoncode/hash.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# Copyright (c) 2017 nexB Inc. and others. All rights reserved. # http://nexb.com and https://github.com/nexB/scancode-toolkit/ # The ScanCode software is licensed under the Apache License version 2.0. # Data generated with ScanCode require an acknowledgment. @@ -48,8 +48,8 @@ def _hash_mod(bitsize, hmodule): """ class hasher(object): def __init__(self, msg=None): - digest_size = bitsize // 8 - self.h = msg and hmodule(msg).digest()[:digest_size] or None + self.digest_size = bitsize // 8 + self.h = msg and hmodule(msg).digest()[:self.digest_size] or None def digest(self): return self.h From 8d883636377bd93ae48a05ae1ed3d24b6e42e1a6 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 17 Jan 2017 13:36:34 +0100 Subject: [PATCH 059/436] Add arg to testcase.get_test_loc to skip file existence check * no behavior change otherwise, and the new arg is True by default. Signed-off-by: Philippe Ombredanne --- src/commoncode/testcase.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index e38b6c16870..4b032a6ded7 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2015 nexB Inc. and others. All rights reserved. +# Copyright (c) 2017 nexB Inc. and others. All rights reserved. # http://nexb.com and https://github.com/nexB/scancode-toolkit/ # The ScanCode software is licensed under the Apache License version 2.0. # Data generated with ScanCode require an acknowledgment. @@ -22,6 +22,9 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. +""" +FIXME: TEMPORARY COPY of commoncode.testcase to handle new semantics for get_test_loc +""" from __future__ import absolute_import from __future__ import print_function @@ -92,7 +95,7 @@ def to_os_native_path(path): return path -def get_test_loc(test_path, test_data_dir, debug=False): +def get_test_loc(test_path, test_data_dir, debug=False, exists=True): """ Given a `test_path` relative to the `test_data_dir` directory, return the location to a test file or directory for this path. No copy is done. @@ -112,7 +115,7 @@ def get_test_loc(test_path, test_data_dir, debug=False): tpath = to_os_native_path(test_path) test_loc = os.path.abspath(os.path.join(test_data_dir, tpath)) - if not os.path.exists(test_loc): + if exists and not os.path.exists(test_loc): raise IOError("[Errno 2] No such file or directory: " "test_path not found: '%(test_loc)s'" % locals()) From d52bd7280bbb70f32c66cbf6d5dca944f22dab9f Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 31 Jan 2017 19:36:41 +0100 Subject: [PATCH 060/436] Add multi_checksums function to compute several checksums at once. Signed-off-by: Philippe Ombredanne --- src/commoncode/hash.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/commoncode/hash.py b/src/commoncode/hash.py index 23d54cb5c9e..539d12f15e1 100644 --- a/src/commoncode/hash.py +++ b/src/commoncode/hash.py @@ -22,8 +22,11 @@ # ScanCode is a free software code scanning tool from nexB Inc. and others. # Visit https://github.com/nexB/scancode-toolkit/ for support and download. -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from collections import OrderedDict import hashlib from commoncode.codec import bin_to_num @@ -87,6 +90,16 @@ def get_hasher(bitsize): return _hashmodules_by_bitsize[bitsize] +_hashmodules_by_name = { + 'md5': get_hasher(128), + 'sha1': get_hasher(160), + 'sha256': get_hasher(256), + 'sha384': get_hasher(384), + 'sha512': get_hasher(512) +} + + + def checksum(location, bitsize, base64=False): """ Return a checksum of `bitsize` length from the content of the file at @@ -122,3 +135,23 @@ def sha256(location): def sha512(location): return checksum(location, bitsize=512, base64=False) + + +def multi_checksums(location, checksum_names=('md5', 'sha1', 'sha256')): + """ + Return a mapping of hexdigest checksums keyed by checksum name from the content + of the file at `location`. Use the `checksum_names` list of checksum names. + The mapping is guaranted to contains all the requested names as keys. + If the location is not a file, the values are None. + """ + results = OrderedDict([(name, None) for name in checksum_names]) + if not filetype.is_file(location): + return results + + # fixme: we should read in chunks + with open(location, 'rb') as f: + hashable = f.read() + + for name in checksum_names: + results[name] = _hashmodules_by_name[name](hashable).hexdigest() + return results From c061c03218760c0c154385613aad4f212413fc4b Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Tue, 31 Jan 2017 19:36:41 +0100 Subject: [PATCH 061/436] Add multi_checksums function to compute several checksums at once. Signed-off-by: Philippe Ombredanne --- tests/commoncode/commoncode/test_hash.py | 46 +++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/tests/commoncode/commoncode/test_hash.py b/tests/commoncode/commoncode/test_hash.py index 18f01d180c6..c1fd08dc0a8 100644 --- a/tests/commoncode/commoncode/test_hash.py +++ b/tests/commoncode/commoncode/test_hash.py @@ -34,6 +34,9 @@ from commoncode.hash import sha1 from commoncode.hash import sha256 from commoncode.hash import sha512 +from commoncode.hash import checksum +from collections import OrderedDict +from commoncode.hash import multi_checksums class TestHash(FileBasedTesting): @@ -52,10 +55,6 @@ def test_short_hashes(self): h = get_hasher(64) assert '4124bc0a9335c27f' == h('aa').hexdigest() - def test_sha1_checksum(self): - test_file = self.get_test_loc('hash/dir1/a.png') - assert sha1(test_file) == '34ac5465d48a9b04fc275f09bc2230660df8f4f7' - def test_sha1_checksum_on_text(self): test_file = self.get_test_loc('hash/dir1/a.txt') assert sha1(test_file) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' @@ -72,10 +71,6 @@ def test_sha1_checksum_base64(self): test_file = self.get_test_loc('hash/dir1/a.png') assert b64sha1(test_file) == 'NKxUZdSKmwT8J18JvCIwZg349Pc=' - def test_md5_checksum(self): - test_file = self.get_test_loc('hash/dir1/a.png') - assert md5(test_file) == '4760fb467f1ebf3b0aeace4a3926f1a4' - def test_md5_checksum_on_text(self): test_file = self.get_test_loc('hash/dir1/a.txt') assert md5(test_file) == '40c53c58fdafacc83cfff6ee3d2f6d69' @@ -88,6 +83,14 @@ def test_md5_checksum_on_dos_text(self): test_file = self.get_test_loc('hash/dir2/dos.txt') assert md5(test_file) == '095f5068940e41df9add5d4cc396c181' + def test_md5_checksum(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert md5(test_file) == '4760fb467f1ebf3b0aeace4a3926f1a4' + + def test_sha1_checksum(self): + test_file = self.get_test_loc('hash/dir1/a.png') + assert sha1(test_file) == '34ac5465d48a9b04fc275f09bc2230660df8f4f7' + def test_sha256_checksum(self): test_file = self.get_test_loc('hash/dir1/a.png') assert sha256(test_file) == '1b598db6fee8f1ec7bb919c0adf68956f3d20af8c9934a9cf2db52e1347efd35' @@ -95,3 +98,30 @@ def test_sha256_checksum(self): def test_sha512_checksum(self): test_file = self.get_test_loc('hash/dir1/a.png') assert sha512(test_file) == '5be9e01cd20ff288fd3c3fc46be5c2747eaa2c526197125330947a95cdb418222176b182a4680f0e435ba8f114363c45a67b30eed9a9222407e63ccbde46d3b4' + + def test_checksum_160(self): + test_file = self.get_test_loc('hash/dir1/a.txt') + assert checksum(test_file, 160) == '3ca69e8d6c234a469d16ac28a4a658c92267c423' + + def test_checksum_128(self): + test_file = self.get_test_loc('hash/dir1/a.txt') + assert checksum(test_file, 128) == '40c53c58fdafacc83cfff6ee3d2f6d69' + + def test_multi_checksums(self): + test_file = self.get_test_loc('hash/dir1/a.png') + expected = OrderedDict([ + ('md5', '4760fb467f1ebf3b0aeace4a3926f1a4'), + ('sha1', '34ac5465d48a9b04fc275f09bc2230660df8f4f7'), + ('sha256', '1b598db6fee8f1ec7bb919c0adf68956f3d20af8c9934a9cf2db52e1347efd35'), + ]) + result = multi_checksums(test_file) + assert expected == result + + def test_multi_checksums_custom(self): + test_file = self.get_test_loc('hash/dir1/a.png') + result = multi_checksums(test_file, ('sha512',)) + expected = OrderedDict([ + ('sha512', '5be9e01cd20ff288fd3c3fc46be5c2747eaa2c526197125330947a95cdb418222176b182a4680f0e435ba8f114363c45a67b30eed9a9222407e63ccbde46d3b4'), + ]) + assert expected == result + From cfd92815cef8a711c6d3bf33f966a495c859aa51 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Wed, 8 Feb 2017 17:51:21 +0100 Subject: [PATCH 062/436] Improve test archives extraction functions documentation. Signed-off-by: Philippe Ombredanne --- src/commoncode/testcase.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commoncode/testcase.py b/src/commoncode/testcase.py index 4b032a6ded7..ad98a1da2e5 100644 --- a/src/commoncode/testcase.py +++ b/src/commoncode/testcase.py @@ -220,6 +220,7 @@ def __extract(self, test_path, extract_func=None, verbatim=False): Given an archive file identified by test_path relative to a test files directory, return a new temp directory where the archive file has been extracted using extract_func. + If `verbatim` is True preserve the permissions. """ assert test_path and test_path != '' test_path = to_os_native_path(test_path) @@ -239,6 +240,7 @@ def extract_test_tar(self, test_path, verbatim=False): def extract_tar(location, target_dir, verbatim=False): """ Extract a tar archive at location in the target_dir directory. + If `verbatim` is True preserve the permissions. """ with open(location, 'rb') as input_tar: tar = tarfile.open(fileobj=input_tar) From caee460b830c33f2ec7c161817288abe27fc9ae4 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sat, 18 Feb 2017 15:14:10 +0100 Subject: [PATCH 063/436] Cleanup file_base_name tests Signed-off-by: Philippe Ombredanne --- tests/commoncode/commoncode/test_fileutils.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 73b1d6935b7..55c378e1b31 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -385,7 +385,7 @@ class TestBaseName(FileBasedTesting): test_data_dir = os.path.join(os.path.dirname(__file__), 'data') def test_file_base_name_on_path_and_location_1(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'a/.a/file' expected_name = 'file' result = fileutils.file_base_name(test_file) @@ -393,8 +393,8 @@ def test_file_base_name_on_path_and_location_1(self): result = fileutils.file_base_name((os.path.join(test_dir, test_file))) assert expected_name == result - def test_file_base_name_on_path_and_location_2(self): - test_dir = self.get_test_loc('fileutils/basename') + def test_file_base_name_on_path_and_location_for_dot_filex (self): + test_dir = '/fileutils/basename' test_file = 'a/.a/' expected_name = '.a' result = fileutils.file_base_name(test_file) @@ -403,7 +403,7 @@ def test_file_base_name_on_path_and_location_2(self): assert expected_name == result def test_file_base_name_on_path_and_location_3(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'a/b/.a.b' expected_name = '.a' result = fileutils.file_base_name(test_file) @@ -412,7 +412,7 @@ def test_file_base_name_on_path_and_location_3(self): assert expected_name == result def test_file_base_name_on_path_and_location_4(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'a/b/a.tag.gz' expected_name = 'a.tag' result = fileutils.file_base_name(test_file) @@ -421,7 +421,7 @@ def test_file_base_name_on_path_and_location_4(self): assert expected_name == result def test_file_base_name_on_path_and_location_5(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'a/b/' expected_name = 'b' result = fileutils.file_base_name(test_file) @@ -430,7 +430,7 @@ def test_file_base_name_on_path_and_location_5(self): assert expected_name == result def test_file_base_name_on_path_and_location_6(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'a/f.a' expected_name = 'f' result = fileutils.file_base_name(test_file) @@ -439,7 +439,7 @@ def test_file_base_name_on_path_and_location_6(self): assert expected_name == result def test_file_base_name_on_path_and_location_7(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'a/' expected_name = 'a' result = fileutils.file_base_name(test_file) @@ -448,7 +448,7 @@ def test_file_base_name_on_path_and_location_7(self): assert expected_name == result def test_file_base_name_on_path_and_location_8(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'f.a/a.c' expected_name = 'a' result = fileutils.file_base_name(test_file) @@ -457,7 +457,7 @@ def test_file_base_name_on_path_and_location_8(self): assert expected_name == result def test_file_base_name_on_path_and_location_9(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'f.a/' expected_name = 'f.a' result = fileutils.file_base_name(test_file) @@ -466,7 +466,7 @@ def test_file_base_name_on_path_and_location_9(self): assert expected_name == result def test_file_base_name_on_path_and_location_10(self): - test_dir = self.get_test_loc('fileutils/basename') + test_dir = '/fileutils/basename' test_file = 'tst' expected_name = 'tst' result = fileutils.file_base_name(test_file) From 06900ebec0e82b2607f010833181988e9993134a Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sat, 18 Feb 2017 15:14:10 +0100 Subject: [PATCH 064/436] Cleanup file_base_name tests Signed-off-by: Philippe Ombredanne --- src/commoncode/fileutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commoncode/fileutils.py b/src/commoncode/fileutils.py index a5b39e04f8d..051df16c8e9 100644 --- a/src/commoncode/fileutils.py +++ b/src/commoncode/fileutils.py @@ -296,6 +296,7 @@ def splitext(path, force_posix=False): base_name = name extension = '' elif name.startswith('.') and '.' not in name[1:]: + # .dot files base name is the full name and they do not have an extension base_name = name extension = '' else: From 4226854440c41e401cce72a9b6ee236fe2e2c493 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Sun, 19 Feb 2017 19:11:01 +0100 Subject: [PATCH 065/436] Correct test failures introduced by mistake in 76148753 * also improve the name of some test functions Signed-off-by: Philippe Ombredanne --- tests/commoncode/commoncode/test_fileutils.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/tests/commoncode/commoncode/test_fileutils.py b/tests/commoncode/commoncode/test_fileutils.py index 55c378e1b31..9f1a25634bf 100644 --- a/tests/commoncode/commoncode/test_fileutils.py +++ b/tests/commoncode/commoncode/test_fileutils.py @@ -385,93 +385,102 @@ class TestBaseName(FileBasedTesting): test_data_dir = os.path.join(os.path.dirname(__file__), 'data') def test_file_base_name_on_path_and_location_1(self): - test_dir = '/fileutils/basename' + test_dir = self.get_test_loc('fileutils/basename') test_file = 'a/.a/file' expected_name = 'file' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_for_dot_filex (self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_file_path_for_dot_file (self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'a/.a/' expected_name = '.a' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_3(self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_file_path_for_dot_file_with_extension(self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'a/b/.a.b' expected_name = '.a' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_4(self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_file_path_for_file_with_unknown_composed_extension(self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'a/b/a.tag.gz' expected_name = 'a.tag' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) + assert expected_name == result + + def test_file_base_name_on_file_path_for_file_with_known_composed_extension(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/b/a.tar.gz' + expected_name = 'a' + result = fileutils.file_base_name(test_file) + assert expected_name == result + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_5(self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_dir_path(self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'a/b/' expected_name = 'b' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_6(self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_plain_file(self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'a/f.a' expected_name = 'f' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_7(self): - test_dir = '/fileutils/basename' - test_file = 'a/' + def test_file_base_name_on_plain_file_with_parent_dir_extension(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'f.a/a.c' expected_name = 'a' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_8(self): - test_dir = '/fileutils/basename' - test_file = 'f.a/a.c' + def test_file_base_name_on_path_for_plain_dir(self): + test_dir = self.get_test_loc('fileutils/basename') + test_file = 'a/' expected_name = 'a' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_9(self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_path_for_plain_dir_with_extension(self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'f.a/' expected_name = 'f.a' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result - def test_file_base_name_on_path_and_location_10(self): - test_dir = '/fileutils/basename' + def test_file_base_name_on_path_for_plain_file(self): + test_dir = self.get_test_loc('fileutils/basename') test_file = 'tst' expected_name = 'tst' result = fileutils.file_base_name(test_file) assert expected_name == result - result = fileutils.file_base_name((os.path.join(test_dir, test_file))) + result = fileutils.file_base_name(join(test_dir, test_file)) assert expected_name == result From 754c2591e77b43c66ace800836970a35f35becd7 Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 23 Feb 2017 19:55:51 +0100 Subject: [PATCH 066/436] New tests for shattered sha1. Add sha512 as a default multichecksum. Signed-off-by: Philippe Ombredanne --- src/commoncode/hash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commoncode/hash.py b/src/commoncode/hash.py index 539d12f15e1..d7f89a5ec7e 100644 --- a/src/commoncode/hash.py +++ b/src/commoncode/hash.py @@ -137,7 +137,7 @@ def sha512(location): return checksum(location, bitsize=512, base64=False) -def multi_checksums(location, checksum_names=('md5', 'sha1', 'sha256')): +def multi_checksums(location, checksum_names=('md5', 'sha1', 'sha256', 'sha512')): """ Return a mapping of hexdigest checksums keyed by checksum name from the content of the file at `location`. Use the `checksum_names` list of checksum names. From 337af266bace63282287005df7aa9559f67ae6ef Mon Sep 17 00:00:00 2001 From: Philippe Ombredanne Date: Thu, 23 Feb 2017 19:55:51 +0100 Subject: [PATCH 067/436] New tests for shattered sha1. Add sha512 as a default multichecksum. Signed-off-by: Philippe Ombredanne --- .../data/hash/sha1-collision/shattered-1.pdf | Bin 0 -> 422435 bytes .../data/hash/sha1-collision/shattered-2.pdf | Bin 0 -> 422435 bytes .../data/hash/sha1-collision/shattered.ABOUT | 8 ++++++ tests/commoncode/commoncode/test_hash.py | 25 ++++++++++++++++-- 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/commoncode/commoncode/data/hash/sha1-collision/shattered-1.pdf create mode 100644 tests/commoncode/commoncode/data/hash/sha1-collision/shattered-2.pdf create mode 100644 tests/commoncode/commoncode/data/hash/sha1-collision/shattered.ABOUT diff --git a/tests/commoncode/commoncode/data/hash/sha1-collision/shattered-1.pdf b/tests/commoncode/commoncode/data/hash/sha1-collision/shattered-1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0 GIT binary patch literal 422435 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 z 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 z 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

# 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