├── .gitignore ├── AUTHORS.txt ├── LICENSE.txt ├── README.rst ├── TODO.txt ├── setup.cfg ├── setup.py └── setuptools_git ├── __init__.py ├── tests.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | /dist 4 | /build 5 | /ez_setup.py 6 | /*.egg-info 7 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Stefan H. Holek 2 | Support for Python 3.3 3 | Support for OS X and Windows 4 | 5 | Chris McDonough 6 | Support for non-ASCII filenames 7 | 8 | Wichert Akkerman 9 | Handle in-git symlinks, small code cleanups 10 | Current maintainer 11 | 12 | Vinay Sajip : 13 | Python 3 support 14 | 15 | Humberto Diogenes : 16 | Modernization and portability 17 | 18 | Yannick Gingras : 19 | Original implementation 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) The Regents of the University of California. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. Neither the name of the University nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Looking for a new maintainer 2 | ---------------------------- 3 | 4 | Please let me know if interested! 5 | 6 | 7 | About 8 | ----- 9 | 10 | This is a plugin for setuptools that enables git integration. Once 11 | installed, Setuptools can be told to include in a package distribution 12 | all the files tracked by git. This is an alternative to explicit 13 | inclusion specifications with ``MANIFEST.in``. 14 | 15 | A package distribution here refers to a package that you create using 16 | setup.py, for example:: 17 | 18 | $> python setup.py sdist 19 | $> python setup.py bdist_rpm 20 | $> python setup.py bdist_egg 21 | 22 | This package was formerly known as gitlsfiles. The name change is the 23 | result of an effort by the setuptools plugin developers to provide a 24 | uniform naming convention. 25 | 26 | 27 | Installation 28 | ------------ 29 | 30 | With easy_install:: 31 | 32 | $> easy_install setuptools_git 33 | 34 | Alternative manual installation:: 35 | 36 | $> tar -zxvf setuptools_git-X.Y.Z.tar.gz 37 | $> cd setuptools_git-X.Y.Z 38 | $> python setup.py install 39 | 40 | Where X.Y.Z is a version number. 41 | 42 | 43 | 44 | Usage 45 | ----- 46 | 47 | To activate this plugin, you must first package your python module 48 | with ``setup.py`` and use setuptools. The former is well documented in 49 | the `distutils manual `_. 50 | 51 | To use setuptools instead of distutils, just edit ``setup.py`` and 52 | change: 53 | 54 | .. code-block:: python 55 | 56 | from distutils.core import setup 57 | 58 | to: 59 | 60 | .. code-block:: python 61 | 62 | from setuptools import setup, find_packages 63 | 64 | When Setuptools builds a source package, it always includes all files 65 | tracked by your revision control system, if it knows how to learn what 66 | those files are. 67 | 68 | When Setuptools builds a binary package, you can ask it to include all 69 | files tracked by your revision control system, by adding these argument 70 | to your invocation of `setup()`: 71 | 72 | .. code-block:: python 73 | 74 | setup(..., 75 | packages=find_packages(), 76 | include_package_data=True, 77 | ...) 78 | 79 | which will detect that a directory is a package if it contains a 80 | ``__init__.py`` file. Alternatively, you can do without ``__init__.py`` 81 | files and tell Setuptools explicitly which packages to process: 82 | 83 | .. code-block:: python 84 | 85 | setup(..., 86 | packages=["a_package", "another_one"], 87 | include_package_data=True, 88 | ...) 89 | 90 | This plugin lets setuptools know what files are tracked by your git 91 | revision control tool. Setuptools ships with support for cvs and 92 | subversion. Other plugins like this one are available for bzr, darcs, 93 | monotone, mercurial, and many others. 94 | 95 | It might happen that you track files with your revision control system 96 | that you don't want to include in your packages. In that case, you 97 | can prevent setuptools from packaging those files with a directive in 98 | your ``MANIFEST.in``, for example:: 99 | 100 | exclude .gitignore 101 | recursive-exclude images *.xcf *.blend 102 | 103 | In this example, we prevent setuptools from packaging ``.gitignore`` and 104 | the Gimp and Blender source files found under the ``images`` directory. 105 | 106 | Files to exclude from the package can also be listed in the `setup()` 107 | directive. To do the same as the MANIFEST.in above, do: 108 | 109 | .. code-block:: python 110 | 111 | setup(..., 112 | exclude_package_data={'': ['.gitignore'], 113 | 'images': ['*.xcf', '*.blend']}, 114 | ...) 115 | 116 | Here is another example: 117 | 118 | .. code-block:: python 119 | 120 | setup(..., 121 | exclude_package_data={'': ['.gitignore', 'artwork/*'], 122 | 'model': ['config.py']}, 123 | ...) 124 | 125 | 126 | Gotchas 127 | ------- 128 | 129 | Be aware that for this module to work properly, git and the git 130 | meta-data must be available. That means that if someone tries to make 131 | a package distribution out of a non-git distribution of yours, say a 132 | tarball, setuptools will lack the information necessary to know which 133 | files to include. A similar problem will happen if someone clones 134 | your git repository but does not install this plugin. 135 | 136 | Resolving those problems is out of the scope of this plugin; you 137 | should add relevant warnings to your documentation if those situations 138 | are a concern to you. 139 | 140 | You can make sure that anyone who clones your git repository and uses 141 | your setup.py file has this plugin by adding a `setup_requires` 142 | argument: 143 | 144 | .. code-block:: python 145 | 146 | setup(..., 147 | setup_requires=[ "setuptools_git >= 0.3", ], 148 | ...) 149 | 150 | 151 | Changes 152 | ------- 153 | 154 | 1.2; 2017-02-17 155 | ~~~~~~~~~~~~~~~~ 156 | - Add ability to get version from git tags (https://github.com/msabramo/setuptools-git/pull/9) 157 | - Return early if a directory isn't managed by git (https://github.com/msabramo/setuptools-git/pull/10) 158 | - Support universal wheels (https://github.com/msabramo/setuptools-git/pull/11) 159 | - Optimize directory scanning to skip ignored directories (https://github.com/msabramo/setuptools-git/pull/12) 160 | 161 | 162 | References 163 | ---------- 164 | 165 | * `How to distribute Python modules with Distutils 166 | `_ 167 | 168 | * `Setuptools complete manual 169 | `_ 170 | 171 | Thanks to `Zooko O'Whielacronx`_ for many improvements to the documentation. 172 | 173 | 174 | .. _Zooko O'Whielacronx: https://bitbucket.org/zooko 175 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | TODO 2 | ---- 3 | 4 | * Git thinks that filenames are bytestreams, not character sequences. 5 | http://article.gmane.org/gmane.comp.version-control.git/122860 6 | 7 | * Git can not work across different encodings, and admits as much. 8 | Then again Subversion can't either. 9 | 10 | * Our only hope is to ask Git for bytestreams and apply some heuristics 11 | as to the encoding and composition used. 12 | 13 | * ATM, setuptools-git returns files by their OS path, not the POSIX 14 | path that is returned by Git. It also resolves symbolic links within 15 | the same repository. 16 | 17 | * We might get away with passing along the bytestreams in Python 2, 18 | but in Python 3 we must return Unicode. 19 | 20 | * For passing through non-UTF-8 filenames under Python 3, use the 21 | 'surrogateescape' error handler. 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = '1.2' 4 | 5 | setup( 6 | name="setuptools-git", 7 | version=version, 8 | maintainer='Marc Abramowitz', 9 | maintainer_email='msabramo@gmail.com', 10 | author="Yannick Gingras", 11 | author_email="ygingras@ygingras.net", 12 | url="https://github.com/msabramo/setuptools-git", 13 | keywords='distutils setuptools git', 14 | description="Setuptools revision control system plugin for Git", 15 | long_description=open('README.rst').read(), 16 | license='BSD', 17 | packages=find_packages(), 18 | test_suite='setuptools_git', 19 | zip_safe=True, 20 | classifiers=[ 21 | "Topic :: Software Development :: Version Control", 22 | "Development Status :: 4 - Beta", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: BSD License", 25 | "Programming Language :: Python :: 2", 26 | "Programming Language :: Python :: 2.7", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3.1", 29 | "Programming Language :: Python :: 3.2", 30 | "Programming Language :: Python :: 3.3", 31 | "Programming Language :: Python :: 3.4", 32 | "Programming Language :: Python :: 3.5", 33 | "Programming Language :: Python :: 3.6", 34 | ], 35 | entry_points=""" 36 | [setuptools.file_finders] 37 | git=setuptools_git:listfiles 38 | 39 | [distutils.setup_keywords] 40 | use_vcs_version=setuptools_git:version_calc 41 | """ 42 | ) 43 | -------------------------------------------------------------------------------- /setuptools_git/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A hook into setuptools for Git. 3 | """ 4 | import sys 5 | import os 6 | import posixpath 7 | 8 | from os.path import realpath, join 9 | from subprocess import PIPE 10 | 11 | from setuptools_git.utils import check_output 12 | from setuptools_git.utils import b 13 | from setuptools_git.utils import posix 14 | from setuptools_git.utils import fsdecode 15 | from setuptools_git.utils import hfs_quote 16 | from setuptools_git.utils import compose 17 | from setuptools_git.utils import decompose 18 | from setuptools_git.utils import CalledProcessError 19 | 20 | 21 | def version_calc(dist, attr, value): 22 | """ 23 | Handler for parameter to setup(use_vcs_version=value) 24 | bool(value) should be true to invoke this plugin. 25 | """ 26 | if attr == 'use_vcs_version' and value: 27 | dist.metadata.version = calculate_version() 28 | 29 | 30 | def calculate_version(): 31 | return check_output(['git', 'describe', '--tags', '--dirty']).strip() 32 | 33 | 34 | def ntfsdecode(path): 35 | # We receive the raw bytes from Git and must decode by hand 36 | if sys.version_info >= (3,): 37 | try: 38 | path = path.decode('utf-8') 39 | except UnicodeDecodeError: 40 | path = path.decode(sys.getfilesystemencoding()) 41 | else: 42 | try: 43 | path = path.decode('utf-8').encode(sys.getfilesystemencoding()) 44 | except UnicodeError: 45 | pass # Already in filesystem encoding (hopefully) 46 | return path 47 | 48 | 49 | def gitlsfiles(dirname=''): 50 | # NB: Passing the '-z' option to 'git ls-files' below returns the 51 | # output as a blob of null-terminated filenames without canonical- 52 | # ization or use of double-quoting. 53 | # 54 | # So we'll get back e.g.: 55 | # 56 | # 'pyramid/tests/fixtures/static/h\xc3\xa9h\xc3\xa9.html' 57 | # 58 | # instead of: 59 | # 60 | # '"pyramid/tests/fixtures/static/h\\303\\251h\\303\\251.html"' 61 | # 62 | # for each file. 63 | res = set() 64 | 65 | try: 66 | topdir = check_output( 67 | ['git', 'rev-parse', '--show-toplevel'], cwd=dirname or None, 68 | stderr=PIPE).strip() 69 | 70 | if sys.platform == 'win32': 71 | cwd = ntfsdecode(topdir) 72 | else: 73 | cwd = topdir 74 | 75 | filenames = check_output( 76 | ['git', 'ls-files', '-z'], cwd=cwd, stderr=PIPE) 77 | except (CalledProcessError, OSError): 78 | # Setuptools mandates we fail silently 79 | return res 80 | 81 | for filename in filenames.split(b('\x00')): 82 | if filename: 83 | filename = posixpath.join(topdir, filename) 84 | if sys.platform == 'darwin': 85 | filename = hfs_quote(filename) 86 | if sys.platform == 'win32': 87 | filename = ntfsdecode(filename) 88 | else: 89 | filename = fsdecode(filename) 90 | if sys.platform == 'darwin': 91 | filename = decompose(filename) 92 | res.add(filename) 93 | return res 94 | 95 | 96 | def _gitlsdirs(files, prefix_length): 97 | # Return directories managed by Git 98 | dirs = set() 99 | for file in files: 100 | dir = posixpath.dirname(file) 101 | while len(dir) > prefix_length: 102 | dirs.add(dir) 103 | dir = posixpath.dirname(dir) 104 | return dirs 105 | 106 | 107 | def listfiles(dirname=''): 108 | git_files = gitlsfiles(dirname) 109 | if not git_files: 110 | return 111 | 112 | cwd = realpath(dirname or os.curdir) 113 | prefix_length = len(cwd) + 1 114 | 115 | git_dirs = _gitlsdirs(git_files, prefix_length) 116 | 117 | if sys.version_info >= (2, 6): 118 | walker = os.walk(cwd, followlinks=True) 119 | else: 120 | walker = os.walk(cwd) 121 | 122 | for root, dirs, files in walker: 123 | dirs[:] = [x for x in dirs if posix(realpath(join(root, x))) in git_dirs] 124 | for file in files: 125 | filename = join(root, file) 126 | if posix(realpath(filename)) in git_files: 127 | yield filename[prefix_length:] 128 | 129 | 130 | if __name__ == '__main__': 131 | if len(sys.argv) > 1: 132 | dirname = sys.argv[1] 133 | else: 134 | dirname = '' 135 | for filename in listfiles(dirname): 136 | try: 137 | print(compose(filename)) 138 | except UnicodeEncodeError: 139 | print(repr(filename)[1:-1]) 140 | 141 | -------------------------------------------------------------------------------- /setuptools_git/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import os 4 | import tempfile 5 | import unittest 6 | 7 | from os.path import realpath, join 8 | from setuptools_git.utils import rmtree 9 | from setuptools_git.utils import posix 10 | from setuptools_git.utils import fsdecode 11 | from setuptools_git.utils import hfs_quote 12 | from setuptools_git.utils import decompose 13 | 14 | 15 | class GitTestCase(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.old_cwd = os.getcwd() 19 | self.directory = self.new_repo() 20 | 21 | def tearDown(self): 22 | os.chdir(self.old_cwd) 23 | rmtree(self.directory) 24 | 25 | def new_repo(self): 26 | from setuptools_git.utils import check_call 27 | directory = realpath(tempfile.mkdtemp()) 28 | os.chdir(directory) 29 | check_call(['git', 'init', '--quiet', os.curdir]) 30 | return directory 31 | 32 | def create_file(self, *path): 33 | fd = open(join(*path), 'wt') 34 | fd.write('dummy\n') 35 | fd.close() 36 | 37 | def create_dir(self, *path): 38 | os.makedirs(join(*path)) 39 | 40 | def create_git_file(self, *path): 41 | from setuptools_git.utils import check_call 42 | filename = join(*path) 43 | fd = open(filename, 'wt') 44 | fd.write('dummy\n') 45 | fd.close() 46 | check_call(['git', 'add', filename]) 47 | check_call(['git', 'commit', '--quiet', '-m', 'add new file']) 48 | 49 | 50 | class gitlsfiles_tests(GitTestCase): 51 | 52 | def gitlsfiles(self, *a, **kw): 53 | from setuptools_git import gitlsfiles 54 | return gitlsfiles(*a, **kw) 55 | 56 | def test_at_repo_root(self): 57 | self.create_git_file('root.txt') 58 | self.assertEqual( 59 | set(self.gitlsfiles(self.directory)), 60 | set([posix(realpath('root.txt'))])) 61 | 62 | def test_at_repo_root_with_subdir(self): 63 | self.create_git_file('root.txt') 64 | self.create_dir('subdir') 65 | self.create_git_file('subdir', 'entry.txt') 66 | self.assertEqual( 67 | set(self.gitlsfiles(self.directory)), 68 | set([posix(realpath('root.txt')), 69 | posix(realpath('subdir/entry.txt'))])) 70 | 71 | def test_at_repo_subdir(self): 72 | self.create_git_file('root.txt') 73 | self.create_dir('subdir') 74 | self.create_git_file('subdir', 'entry.txt') 75 | self.assertEqual( 76 | set(self.gitlsfiles(join(self.directory, 'subdir'))), 77 | set([posix(realpath('root.txt')), 78 | posix(realpath('subdir/entry.txt'))])) 79 | 80 | def test_nonascii_filename(self): 81 | filename = 'héhé.html' 82 | 83 | # HFS Plus uses decomposed UTF-8 84 | if sys.platform == 'darwin': 85 | filename = decompose(filename) 86 | 87 | self.create_git_file(filename) 88 | 89 | self.assertEqual( 90 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 91 | [filename]) 92 | 93 | self.assertEqual( 94 | set(self.gitlsfiles(self.directory)), 95 | set([posix(realpath(filename))])) 96 | 97 | def test_utf8_filename(self): 98 | if sys.version_info >= (3,): 99 | filename = 'héhé.html'.encode('utf-8') 100 | else: 101 | filename = 'héhé.html' 102 | 103 | # HFS Plus uses decomposed UTF-8 104 | if sys.platform == 'darwin': 105 | filename = decompose(filename) 106 | 107 | # Windows does not like byte filenames under Python 3 108 | if sys.platform == 'win32' and sys.version_info >= (3,): 109 | filename = filename.decode('utf-8') 110 | 111 | self.create_git_file(filename) 112 | 113 | self.assertEqual( 114 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 115 | [fsdecode(filename)]) 116 | 117 | self.assertEqual( 118 | set(self.gitlsfiles(self.directory)), 119 | set([posix(realpath(fsdecode(filename)))])) 120 | 121 | def test_latin1_filename(self): 122 | if sys.version_info >= (3,): 123 | filename = 'héhé.html'.encode('latin-1') 124 | else: 125 | filename = 'h\xe9h\xe9.html' 126 | 127 | # HFS Plus quotes unknown bytes 128 | if sys.platform == 'darwin': 129 | filename = hfs_quote(filename) 130 | 131 | # Windows does not like byte filenames under Python 3 132 | if sys.platform == 'win32' and sys.version_info >= (3,): 133 | filename = filename.decode('latin-1') 134 | 135 | self.create_git_file(filename) 136 | 137 | self.assertEqual( 138 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 139 | [fsdecode(filename)]) 140 | 141 | self.assertEqual( 142 | set(self.gitlsfiles(self.directory)), 143 | set([posix(realpath(fsdecode(filename)))])) 144 | 145 | def test_empty_repo(self): 146 | self.assertEqual( 147 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 148 | []) 149 | 150 | self.assertEqual( 151 | set(self.gitlsfiles(self.directory)), 152 | set([])) 153 | 154 | def test_empty_dirname(self): 155 | self.create_git_file('root.txt') 156 | self.assertEqual( 157 | set(self.gitlsfiles()), 158 | set([posix(realpath('root.txt'))])) 159 | 160 | def test_directory_only_contains_another_directory(self): 161 | self.create_dir('foo/bar') 162 | self.create_git_file('foo/bar/root.txt') 163 | self.assertEqual( 164 | set(self.gitlsfiles()), 165 | set([posix(realpath(join('foo', 'bar', 'root.txt')))]) 166 | ) 167 | 168 | def test_empty_dirname_in_subdir(self): 169 | self.create_git_file('root.txt') 170 | self.create_dir('subdir') 171 | self.create_git_file('subdir', 'entry.txt') 172 | os.chdir(join(self.directory, 'subdir')) 173 | self.assertEqual( 174 | set(self.gitlsfiles()), 175 | set([posix(realpath('../root.txt')), 176 | posix(realpath('../subdir/entry.txt'))])) 177 | 178 | def test_git_error(self): 179 | import setuptools_git 180 | from setuptools_git.utils import CalledProcessError 181 | 182 | def do_raise(*args, **kw): 183 | raise CalledProcessError(1, 'git') 184 | 185 | self.create_git_file('root.txt') 186 | saved = setuptools_git.check_output 187 | setuptools_git.check_output = do_raise 188 | try: 189 | self.assertEqual(self.gitlsfiles(), set()) 190 | finally: 191 | setuptools_git.check_output = saved 192 | 193 | 194 | class listfiles_tests(GitTestCase): 195 | 196 | def listfiles(self, *a, **kw): 197 | from setuptools_git import listfiles 198 | return listfiles(*a, **kw) 199 | 200 | def test_at_repo_root(self): 201 | self.create_git_file('root.txt') 202 | self.assertEqual( 203 | set(self.listfiles(self.directory)), 204 | set(['root.txt'])) 205 | 206 | def test_at_repo_root_with_subdir(self): 207 | self.create_git_file('root.txt') 208 | self.create_dir('subdir') 209 | self.create_git_file('subdir', 'entry.txt') 210 | self.assertEqual( 211 | set(self.listfiles(self.directory)), 212 | set(['root.txt', join('subdir', 'entry.txt')])) 213 | 214 | def test_at_repo_subdir(self): 215 | self.create_git_file('root.txt') 216 | self.create_dir('subdir') 217 | self.create_git_file('subdir', 'entry.txt') 218 | self.assertEqual( 219 | set(self.listfiles(join(self.directory, 'subdir'))), 220 | set(['entry.txt'])) 221 | 222 | def test_nonascii_filename(self): 223 | filename = 'héhé.html' 224 | 225 | # HFS Plus uses decomposed UTF-8 226 | if sys.platform == 'darwin': 227 | filename = decompose(filename) 228 | 229 | self.create_git_file(filename) 230 | 231 | self.assertEqual( 232 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 233 | [filename]) 234 | 235 | self.assertEqual( 236 | set(self.listfiles(self.directory)), 237 | set([filename])) 238 | 239 | def test_utf8_filename(self): 240 | if sys.version_info >= (3,): 241 | filename = 'héhé.html'.encode('utf-8') 242 | else: 243 | filename = 'héhé.html' 244 | 245 | # HFS Plus uses decomposed UTF-8 246 | if sys.platform == 'darwin': 247 | filename = decompose(filename) 248 | 249 | # Windows does not like byte filenames under Python 3 250 | if sys.platform == 'win32' and sys.version_info >= (3,): 251 | filename = filename.decode('utf-8') 252 | 253 | self.create_git_file(filename) 254 | 255 | self.assertEqual( 256 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 257 | [fsdecode(filename)]) 258 | 259 | self.assertEqual( 260 | set(self.listfiles(self.directory)), 261 | set([fsdecode(filename)])) 262 | 263 | def test_latin1_filename(self): 264 | if sys.version_info >= (3,): 265 | filename = 'héhé.html'.encode('latin-1') 266 | else: 267 | filename = 'h\xe9h\xe9.html' 268 | 269 | # HFS Plus quotes unknown bytes 270 | if sys.platform == 'darwin': 271 | filename = hfs_quote(filename) 272 | 273 | # Windows does not like byte filenames under Python 3 274 | if sys.platform == 'win32' and sys.version_info >= (3,): 275 | filename = filename.decode('latin-1') 276 | 277 | self.create_git_file(filename) 278 | 279 | self.assertEqual( 280 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 281 | [fsdecode(filename)]) 282 | 283 | self.assertEqual( 284 | set(self.listfiles(self.directory)), 285 | set([fsdecode(filename)])) 286 | 287 | def test_empty_repo(self): 288 | self.assertEqual( 289 | [fn for fn in os.listdir(self.directory) if fn[0] != '.'], 290 | []) 291 | 292 | self.assertEqual( 293 | set(self.listfiles(self.directory)), 294 | set([])) 295 | 296 | def test_empty_dirname(self): 297 | self.create_git_file('root.txt') 298 | self.assertEqual( 299 | set(self.listfiles()), 300 | set(['root.txt'])) 301 | 302 | def test_directory_only_contains_another_directory(self): 303 | self.create_dir('foo/bar') 304 | self.create_git_file('foo/bar/root.txt') 305 | self.assertEqual( 306 | set(self.listfiles()), 307 | set([join('foo', 'bar', 'root.txt')]) 308 | ) 309 | 310 | def test_empty_dirname_in_subdir(self): 311 | self.create_git_file('root.txt') 312 | self.create_dir('subdir') 313 | self.create_git_file('subdir', 'entry.txt') 314 | os.chdir(join(self.directory, 'subdir')) 315 | self.assertEqual( 316 | set(self.listfiles()), 317 | set(['entry.txt'])) 318 | 319 | def test_git_error(self): 320 | import setuptools_git 321 | from setuptools_git.utils import CalledProcessError 322 | 323 | def do_raise(*args, **kw): 324 | raise CalledProcessError(1, 'git') 325 | 326 | self.create_git_file('root.txt') 327 | saved = setuptools_git.check_output 328 | setuptools_git.check_output = do_raise 329 | try: 330 | for filename in self.listfiles(): 331 | self.fail('unexpected results') 332 | finally: 333 | setuptools_git.check_output = saved 334 | 335 | -------------------------------------------------------------------------------- /setuptools_git/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import stat 4 | import shutil 5 | import unicodedata 6 | import posixpath 7 | 8 | if sys.version_info >= (3,): 9 | from urllib.parse import quote as url_quote 10 | unicode = str 11 | else: 12 | from urllib import quote as url_quote 13 | 14 | __all__ = ['check_call', 'check_output', 'rmtree', 15 | 'b', 'posix', 'fsdecode', 'hfs_quote', 'compose', 'decompose'] 16 | 17 | 18 | try: 19 | from subprocess import CalledProcessError 20 | except ImportError: 21 | # BBB for Python < 2.5 22 | class CalledProcessError(Exception): 23 | """ 24 | This exception is raised when a process run by check_call() or 25 | check_output() returns a non-zero exit status. 26 | 27 | The exit status will be stored in the returncode attribute; 28 | check_output() will also store the output in the output attribute. 29 | """ 30 | def __init__(self, returncode, cmd, output=None): 31 | self.returncode = returncode 32 | self.cmd = cmd 33 | self.output = output 34 | 35 | def __str__(self): 36 | return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) 37 | 38 | 39 | try: 40 | from subprocess import check_call 41 | except ImportError: 42 | # BBB for Python < 2.5 43 | def check_call(*popenargs, **kwargs): 44 | from subprocess import call 45 | retcode = call(*popenargs, **kwargs) 46 | cmd = kwargs.get("args") 47 | if cmd is None: 48 | cmd = popenargs[0] 49 | if retcode: 50 | raise CalledProcessError(retcode, cmd) 51 | return retcode 52 | 53 | 54 | try: 55 | from subprocess import check_output 56 | except ImportError: 57 | # BBB for Python < 2.7 58 | def check_output(*popenargs, **kwargs): 59 | from subprocess import PIPE 60 | from subprocess import Popen 61 | if 'stdout' in kwargs: 62 | raise ValueError( 63 | 'stdout argument not allowed, it will be overridden.') 64 | process = Popen(stdout=PIPE, *popenargs, **kwargs) 65 | output, unused_err = process.communicate() 66 | retcode = process.poll() 67 | if retcode: 68 | cmd = kwargs.get("args") 69 | if cmd is None: 70 | cmd = popenargs[0] 71 | raise CalledProcessError(retcode, cmd) 72 | return output 73 | 74 | 75 | # Windows cannot delete read-only Git objects 76 | def rmtree(path): 77 | if sys.platform == 'win32': 78 | def onerror(func, path, excinfo): 79 | os.chmod(path, stat.S_IWRITE) 80 | func(path) 81 | shutil.rmtree(path, False, onerror) 82 | else: 83 | shutil.rmtree(path, False) 84 | 85 | 86 | # Fake byte literals for Python < 2.6 87 | def b(s, encoding='utf-8'): 88 | if sys.version_info >= (3,): 89 | return s.encode(encoding) 90 | return s 91 | 92 | 93 | # Convert path to POSIX path on Windows 94 | def posix(path): 95 | if sys.platform == 'win32': 96 | return path.replace(os.sep, posixpath.sep) 97 | return path 98 | 99 | 100 | # Decode path from fs encoding under Python 3 101 | def fsdecode(path): 102 | if sys.version_info >= (3,): 103 | if not isinstance(path, str): 104 | if sys.platform == 'win32': 105 | errors = 'strict' 106 | else: 107 | errors = 'surrogateescape' 108 | return path.decode(sys.getfilesystemencoding(), errors) 109 | return path 110 | 111 | 112 | # HFS Plus quotes unknown bytes like so: %F6 113 | def hfs_quote(path): 114 | if isinstance(path, unicode): 115 | raise TypeError('bytes are required') 116 | try: 117 | path.decode('utf-8') 118 | except UnicodeDecodeError: 119 | path = url_quote(path) # Not UTF-8 120 | if sys.version_info >= (3,): 121 | path = path.encode('ascii') 122 | return path 123 | 124 | 125 | # HFS Plus uses decomposed UTF-8 126 | def compose(path): 127 | if isinstance(path, unicode): 128 | return unicodedata.normalize('NFC', path) 129 | try: 130 | path = path.decode('utf-8') 131 | path = unicodedata.normalize('NFC', path) 132 | path = path.encode('utf-8') 133 | except UnicodeError: 134 | pass # Not UTF-8 135 | return path 136 | 137 | 138 | # HFS Plus uses decomposed UTF-8 139 | def decompose(path): 140 | if isinstance(path, unicode): 141 | return unicodedata.normalize('NFD', path) 142 | try: 143 | path = path.decode('utf-8') 144 | path = unicodedata.normalize('NFD', path) 145 | path = path.encode('utf-8') 146 | except UnicodeError: 147 | pass # Not UTF-8 148 | return path 149 | 150 | --------------------------------------------------------------------------------