├── .codecov.yml ├── .coveragerc ├── .gitattributes ├── .github └── workflows │ └── testing.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── slicerator ├── __init__.py └── _version.py ├── sphinx-requirements.txt ├── tests └── test_main.py └── versioneer.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | # show coverage in CI status, not as a comment. 2 | comment: off 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | patch: 9 | default: 10 | target: auto 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | slicerator 4 | [report] 5 | omit = 6 | */python?.?/* 7 | */site-packages/nose/* 8 | # ignore _version.py and versioneer.py 9 | .*version.* 10 | *_version.py 11 | 12 | exclude_lines = 13 | if __name__ == '__main__': 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | slicerator/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '00 4 * * *' # daily at 4AM 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.7', '3.8', '3.9', '3.10'] 16 | fail-fast: false 17 | steps: 18 | 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Install test requirements 29 | shell: bash -l {0} 30 | run: | 31 | set -vxeuo pipefail 32 | python -m pip install -r requirements-dev.txt 33 | python -m pip install -v . 34 | python -m pip list 35 | 36 | - name: Test with pytest 37 | shell: bash -l {0} 38 | run: | 39 | set -vxeuo pipefail 40 | coverage run -m pytest -v 41 | coverage report 42 | 43 | - name: Upload code coverage 44 | uses: codecov/codecov-action@v1 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .gitignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # tmp/backup files 8 | *~ 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | cover/* 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | 63 | 64 | ### Python template 65 | # Byte-compiled / optimized / DLL files 66 | __pycache__/ 67 | *.py[cod] 68 | 69 | # C extensions 70 | *.so 71 | 72 | # Distribution / packaging 73 | .Python 74 | env/ 75 | build/ 76 | develop-eggs/ 77 | dist/ 78 | downloads/ 79 | eggs/ 80 | lib/ 81 | lib64/ 82 | parts/ 83 | sdist/ 84 | var/ 85 | *.egg-info/ 86 | .installed.cfg 87 | *.egg 88 | 89 | # PyInstaller 90 | # Usually these files are written by a python script from a template 91 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 92 | *.manifest 93 | *.spec 94 | 95 | # Installer logs 96 | pip-log.txt 97 | pip-delete-this-directory.txt 98 | 99 | # Unit test / coverage reports 100 | htmlcov/ 101 | .tox/ 102 | .coverage 103 | .cache 104 | nosetests.xml 105 | coverage.xml 106 | 107 | # Translations 108 | *.mo 109 | *.pot 110 | 111 | # Django stuff: 112 | *.log 113 | 114 | # Sphinx documentation 115 | docs/_build/ 116 | 117 | # PyBuilder 118 | target/ 119 | 120 | # PyCharm 121 | .idea/ 122 | 123 | #ipython notebook stuff 124 | .ipynb_checkpoints/ 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Daniel B. Allan 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 are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the matplotlib project nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include versioneer.py 2 | include slicerator/_version.py 3 | include README.md 4 | include LICENSE 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slicerator 2 | ========== 3 | 4 | a lazy-loading, fancy-slicable iterable 5 | 6 | Think of it like a generator that is "reusable" and has a length. 7 | 8 | [Please see the documentation](http://slicerator.readthedocs.io/en/latest/) for examples and an API reference. 9 | 10 | [![build status](https://travis-ci.org/soft-matter/slicerator.png?branch=master)](https://travis-ci.org/soft-matter/slicerator) [![Documentation Status](https://readthedocs.org/projects/slicerator/badge/?version=latest)](http://slicerator.readthedocs.io/en/latest/?badge=latest) 11 | 12 | Installation 13 | ------------ 14 | 15 | On any platform, use pip or conda. 16 | 17 | `pip install slicerator` 18 | 19 | or 20 | 21 | `conda install -c conda-forge slicerator` 22 | 23 | Example 24 | ------- 25 | 26 | ```python 27 | from slicerator import Slicerator 28 | 29 | @Slicerator.from_class 30 | class MyLazyLoader: 31 | def __getitem__(self, i): 32 | # this method will be wrapped by Slicerator, so that it accepts slices, 33 | # lists of integers, or boolean masks. Code below will only be executed 34 | # when an integer is used. 35 | 36 | # load thing number i 37 | return thing 38 | 39 | def __len__(self): 40 | # do stuff 41 | return number_of_things 42 | 43 | 44 | # Demo: 45 | >>> a = MyLazyLoader() 46 | >>> s1 = a[::2] # no data is loaded yet 47 | >>> s2 = s1[1:] # no data is loaded yet 48 | >>> some_data = s2[0] 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/slicerator.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/slicerator.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/slicerator" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/slicerator" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # slicerator documentation build configuration file, created by 4 | # sphinx-quickstart on Thu May 10 10:55:56 2018. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | from datetime import date 19 | 20 | # Import slicerator to get the version string generated by versioneer 21 | import slicerator 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | #sys.path.insert(0, os.path.abspath('.')) 27 | 28 | # -- General configuration ------------------------------------------------ 29 | 30 | # If your documentation needs a minimal Sphinx version, state it here. 31 | #needs_sphinx = '1.0' 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.autosummary', 39 | 'sphinx.ext.mathjax', 40 | 'sphinx.ext.viewcode', 41 | 'numpydoc', 42 | #'IPython.sphinxext.ipython_directive', 43 | #'IPython.sphinxext.ipython_console_highlighting', 44 | ] 45 | 46 | import sphinx_rtd_theme 47 | 48 | html_theme = "sphinx_rtd_theme" 49 | 50 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # source_suffix = ['.rst', '.md'] 58 | source_suffix = '.rst' 59 | 60 | # The encoding of source files. 61 | #source_encoding = 'utf-8-sig' 62 | 63 | # The master toctree document. 64 | master_doc = 'index' 65 | 66 | # General information about the project. 67 | project = u'slicerator' 68 | author = 'Daniel B. Allan, Casper van der Wel, and others' 69 | copyright = u'2015-{}, Slicerator Contributors'.format(date.today().year) 70 | 71 | # The version info for the project you're documenting, acts as replacement for 72 | # |version| and |release|, also used in various other places throughout the 73 | # built documents. 74 | # 75 | # The short X.Y version. 76 | version = slicerator.__version__ 77 | # The full version, including alpha/beta/rc tags. 78 | release = slicerator.__version__ 79 | 80 | # The language for content autogenerated by Sphinx. Refer to documentation 81 | # for a list of supported languages. 82 | # 83 | # This is also used if you do content translation via gettext catalogs. 84 | # Usually you set "language" from the command line for these cases. 85 | language = None 86 | 87 | # There are two options for replacing |today|: either, you set today to some 88 | # non-false value, then it is used: 89 | #today = '' 90 | # Else, today_fmt is used as the format for a strftime call. 91 | #today_fmt = '%B %d, %Y' 92 | 93 | # List of patterns, relative to source directory, that match files and 94 | # directories to ignore when looking for source files. 95 | exclude_patterns = ['_build'] 96 | 97 | # The reST default role (used for this markup: `text`) to use for all 98 | # documents. 99 | #default_role = None 100 | 101 | # If true, '()' will be appended to :func: etc. cross-reference text. 102 | #add_function_parentheses = True 103 | 104 | # If true, the current module name will be prepended to all description 105 | # unit titles (such as .. function::). 106 | #add_module_names = True 107 | 108 | # If true, sectionauthor and moduleauthor directives will be shown in the 109 | # output. They are ignored by default. 110 | #show_authors = False 111 | 112 | # The name of the Pygments (syntax highlighting) style to use. 113 | pygments_style = 'sphinx' 114 | 115 | # A list of ignored prefixes for module index sorting. 116 | #modindex_common_prefix = [] 117 | 118 | # If true, keep warnings as "system message" paragraphs in the built documents. 119 | #keep_warnings = False 120 | 121 | # If true, `todo` and `todoList` produce output, else they produce nothing. 122 | todo_include_todos = False 123 | 124 | 125 | # -- Options for HTML output ---------------------------------------------- 126 | 127 | # The theme to use for HTML and HTML Help pages. See the documentation for 128 | # a list of builtin themes. 129 | #html_theme = 'alabaster' 130 | 131 | # Theme options are theme-specific and customize the look and feel of a theme 132 | # further. For a list of options available for each theme, see the 133 | # documentation. 134 | #html_theme_options = {} 135 | 136 | # Add any paths that contain custom themes here, relative to this directory. 137 | #html_theme_path = [] 138 | 139 | # The name for this set of Sphinx documents. If None, it defaults to 140 | # " v documentation". 141 | #html_title = None 142 | 143 | # A shorter title for the navigation bar. Default is the same as html_title. 144 | #html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | #html_logo = None 149 | 150 | # The name of an image file (within the static path) to use as favicon of the 151 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 152 | # pixels large. 153 | #html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = ['_static'] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | #html_extra_path = [] 164 | 165 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 166 | # using the given strftime format. 167 | #html_last_updated_fmt = '%b %d, %Y' 168 | 169 | # If true, SmartyPants will be used to convert quotes and dashes to 170 | # typographically correct entities. 171 | #html_use_smartypants = True 172 | 173 | # Custom sidebar templates, maps document names to template names. 174 | #html_sidebars = {} 175 | 176 | # Additional templates that should be rendered to pages, maps page names to 177 | # template names. 178 | #html_additional_pages = {} 179 | 180 | # If false, no module index is generated. 181 | #html_domain_indices = True 182 | 183 | # If false, no index is generated. 184 | #html_use_index = True 185 | 186 | # If true, the index is split into individual pages for each letter. 187 | #html_split_index = False 188 | 189 | # If true, links to the reST sources are added to the pages. 190 | #html_show_sourcelink = True 191 | 192 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 193 | #html_show_sphinx = True 194 | 195 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 196 | #html_show_copyright = True 197 | 198 | # If true, an OpenSearch description file will be output, and all pages will 199 | # contain a tag referring to it. The value of this option must be the 200 | # base URL from which the finished HTML is served. 201 | #html_use_opensearch = '' 202 | 203 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 204 | #html_file_suffix = None 205 | 206 | # Language to be used for generating the HTML full-text search index. 207 | # Sphinx supports the following languages: 208 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 209 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 210 | #html_search_language = 'en' 211 | 212 | # A dictionary with options for the search language support, empty by default. 213 | # Now only 'ja' uses this config value 214 | #html_search_options = {'type': 'default'} 215 | 216 | # The name of a javascript file (relative to the configuration directory) that 217 | # implements a search results scorer. If empty, the default will be used. 218 | #html_search_scorer = 'scorer.js' 219 | 220 | # Output file base name for HTML help builder. 221 | htmlhelp_basename = 'sliceratordoc' 222 | 223 | # -- Options for LaTeX output --------------------------------------------- 224 | 225 | latex_elements = { 226 | # The paper size ('letterpaper' or 'a4paper'). 227 | #'papersize': 'letterpaper', 228 | 229 | # The font size ('10pt', '11pt' or '12pt'). 230 | #'pointsize': '10pt', 231 | 232 | # Additional stuff for the LaTeX preamble. 233 | #'preamble': '', 234 | 235 | # Latex figure (float) alignment 236 | #'figure_align': 'htbp', 237 | } 238 | 239 | # Grouping the document tree into LaTeX files. List of tuples 240 | # (source start file, target name, title, 241 | # author, documentclass [howto, manual, or own class]). 242 | latex_documents = [ 243 | (master_doc, 'slicerator.tex', u'slicerator Documentation', 244 | u'Daniel B. Allan, Casper van der Wel, and others', 'manual'), 245 | ] 246 | 247 | # The name of an image file (relative to this directory) to place at the top of 248 | # the title page. 249 | #latex_logo = None 250 | 251 | # For "manual" documents, if this is true, then toplevel headings are parts, 252 | # not chapters. 253 | #latex_use_parts = False 254 | 255 | # If true, show page references after internal links. 256 | #latex_show_pagerefs = False 257 | 258 | # If true, show URL addresses after external links. 259 | #latex_show_urls = False 260 | 261 | # Documents to append as an appendix to all manuals. 262 | #latex_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | #latex_domain_indices = True 266 | 267 | 268 | # -- Options for manual page output --------------------------------------- 269 | 270 | # One entry per manual page. List of tuples 271 | # (source start file, name, description, authors, manual section). 272 | man_pages = [ 273 | (master_doc, 'slicerator', u'slicerator Documentation', 274 | [author], 1) 275 | ] 276 | 277 | # If true, show URL addresses after external links. 278 | #man_show_urls = False 279 | 280 | 281 | # -- Options for Texinfo output ------------------------------------------- 282 | 283 | # Grouping the document tree into Texinfo files. List of tuples 284 | # (source start file, target name, title, author, 285 | # dir menu entry, description, category) 286 | texinfo_documents = [ 287 | (master_doc, 'slicerator', u'slicerator Documentation', 288 | author, 'slicerator', 'One line description of project.', 289 | 'Miscellaneous'), 290 | ] 291 | 292 | # Documents to append as an appendix to all manuals. 293 | #texinfo_appendices = [] 294 | 295 | # If false, no module index is generated. 296 | #texinfo_domain_indices = True 297 | 298 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 299 | #texinfo_show_urls = 'footnote' 300 | 301 | # If true, do not generate a @detailmenu in the "Top" node's menu. 302 | #texinfo_no_detailmenu = False 303 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | slicerator: A Lazy-Loading, Fancy-Sliceable Iterable 2 | ==================================================== 3 | 4 | Think of it like a generator that is "reusable" and has a length. 5 | 6 | For installation instructions and to see a brief example, `visit the GitHub page `__. 7 | 8 | API Reference 9 | ------------- 10 | .. automodule:: slicerator 11 | :members: 12 | :undoc-members: 13 | 14 | Indices and tables 15 | ------------------ 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\slicerator.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\slicerator.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # These are required for developing the package (running the tests, building 2 | # the documentation) but not necessarily required for _using_ it. 3 | black 4 | codecov 5 | coverage 6 | flake8 7 | numpy 8 | pytest 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | # See the docstring in versioneer.py for instructions. Note that you must 3 | # re-run 'versioneer.py setup' after changing this section, and commit the 4 | # resulting files. 5 | 6 | [versioneer] 7 | VCS = git 8 | style = pep440 9 | versionfile_source = slicerator/_version.py 10 | versionfile_build = slicerator/_version.py 11 | tag_prefix = v 12 | # parentdir_prefix = 13 | 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import versioneer 3 | from setuptools import setup 4 | 5 | try: 6 | descr = open(os.path.join(os.path.dirname(__file__), "README.md")).read() 7 | except IOError: 8 | descr = "" 9 | 10 | try: 11 | from pypandoc import convert 12 | 13 | descr = convert(descr, "rst", format="md") 14 | except ImportError: 15 | pass 16 | 17 | setup( 18 | name="slicerator", 19 | version=versioneer.get_version(), 20 | author="Daniel B. Allan", 21 | author_email="daniel.b.allan@gmail.com", 22 | packages=["slicerator"], 23 | description="A lazy-loading, fancy-sliceable iterable.", 24 | url="http://github.com/soft-matter/slicerator", 25 | cmdclass=versioneer.get_cmdclass(), 26 | platforms="Cross platform (Linux, Mac OSX, Windows)", 27 | license="BSD", 28 | classifiers=[ 29 | "Development Status :: 4 - Beta", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.7", 32 | "Programming Language :: Python :: 3.8", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | ], 36 | long_description=descr, 37 | long_description_content_type="text/markdown", 38 | ) 39 | -------------------------------------------------------------------------------- /slicerator/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | 4 | import collections.abc 5 | import itertools 6 | from functools import wraps 7 | from copy import copy 8 | import inspect 9 | 10 | 11 | # set version string using versioneer 12 | from ._version import get_versions 13 | __version__ = get_versions()['version'] 14 | del get_versions 15 | 16 | 17 | def _iter_attr(obj): 18 | try: 19 | for ns in [obj] + obj.__class__.mro(): 20 | for attr in ns.__dict__: 21 | yield ns.__dict__[attr] 22 | except AttributeError: 23 | return # obj has no __dict__ 24 | 25 | 26 | class Slicerator(object): 27 | def __init__(self, ancestor, indices=None, length=None, 28 | propagate_attrs=None): 29 | """A generator that supports fancy indexing 30 | 31 | When sliced using any iterable with a known length, it returns another 32 | object like itself, a Slicerator. When sliced with an integer, 33 | it returns the data payload. 34 | 35 | Also, the attributes of the parent object can be propagated, exposed 36 | through the child Slicerators. By default, no attributes are 37 | propagated. Attributes can be white_listed by using the optional 38 | parameter `propagated_attrs`. 39 | 40 | Methods taking an index will be remapped if they are decorated 41 | with `index_attr`. They also have to be present in the 42 | `propagate_attrs` list. 43 | 44 | Parameters 45 | ---------- 46 | ancestor : object 47 | indices : iterable 48 | Giving indices into `ancestor`. 49 | Required if len(ancestor) is invalid. 50 | length : integer 51 | length of indices 52 | This is required if `indices` is a generator, 53 | that is, if `len(indices)` is invalid 54 | propagate_attrs : list of str, optional 55 | list of attributes to be propagated into Slicerator 56 | 57 | Examples 58 | -------- 59 | # Slicing on a Slicerator returns another Slicerator... 60 | >>> v = Slicerator([0, 1, 2, 3], range(4), 4) 61 | >>> v1 = v[:2] 62 | >>> type(v[:2]) 63 | Slicerator 64 | >>> v2 = v[::2] 65 | >>> type(v2) 66 | Slicerator 67 | >>> v2[0] 68 | 0 69 | # ...unless the slice itself has an unknown length, which makes 70 | # slicing impossible. 71 | >>> v3 = v2((i for i in [0])) # argument is a generator 72 | >>> type(v3) 73 | generator 74 | """ 75 | if indices is None and length is None: 76 | try: 77 | length = len(ancestor) 78 | indices = list(range(length)) 79 | except TypeError: 80 | raise ValueError("The length parameter is required in this " 81 | "case because len(ancestor) is not valid.") 82 | elif indices is None: 83 | indices = list(range(length)) 84 | elif length is None: 85 | try: 86 | length = len(indices) 87 | except TypeError: 88 | raise ValueError("The length parameter is required in this " 89 | "case because len(indices) is not valid.") 90 | 91 | # when list of propagated attributes are given explicitly, 92 | # take this list and ignore the class definition 93 | if propagate_attrs is not None: 94 | self._propagate_attrs = propagate_attrs 95 | else: 96 | # check propagated_attrs field from the ancestor definition 97 | self._propagate_attrs = [] 98 | if hasattr(ancestor, '_propagate_attrs'): 99 | self._propagate_attrs += ancestor._propagate_attrs 100 | if hasattr(ancestor, 'propagate_attrs'): 101 | self._propagate_attrs += ancestor.propagate_attrs 102 | 103 | # add methods having the _propagate flag 104 | for attr in _iter_attr(ancestor): 105 | if hasattr(attr, '_propagate_flag'): 106 | self._propagate_attrs.append(attr.__name__) 107 | 108 | self._len = length 109 | self._ancestor = ancestor 110 | self._indices = indices 111 | 112 | @classmethod 113 | def from_func(cls, func, length, propagate_attrs=None): 114 | """ 115 | Make a Slicerator from a function that accepts an integer index 116 | 117 | Parameters 118 | ---------- 119 | func : callback 120 | callable that accepts an integer as its argument 121 | length : int 122 | number of elements; used to supposed revserse slicing like [-1] 123 | propagate_attrs : list, optional 124 | list of attributes to be propagated into Slicerator 125 | """ 126 | class Dummy: 127 | 128 | def __getitem__(self, i): 129 | return func(i) 130 | 131 | def __len__(self): 132 | return length 133 | 134 | return cls(Dummy(), propagate_attrs=propagate_attrs) 135 | 136 | @classmethod 137 | def from_class(cls, some_class, propagate_attrs=None): 138 | """Make an existing class support fancy indexing via Slicerator objects. 139 | 140 | When sliced using any iterable with a known length, it returns a 141 | Slicerator. When sliced with an integer, it returns the data payload. 142 | 143 | Also, the attributes of the parent object can be propagated, exposed 144 | through the child Slicerators. By default, no attributes are 145 | propagated. Attributes can be white_listed in the following ways: 146 | 147 | 1. using the optional parameter `propagate_attrs`; the contents of this 148 | list will overwrite any other list of propagated attributes 149 | 2. using the @propagate_attr decorator inside the class definition 150 | 3. using a `propagate_attrs` class attribute inside the class definition 151 | 152 | The difference between options 2 and 3 appears when subclassing. As 153 | option 2 is bound to the method, the method will always be propagated. 154 | On the contrary, option 3 is bound to the class, so this can be 155 | overwritten by the subclass. 156 | 157 | Methods taking an index will be remapped if they are decorated 158 | with `index_attr`. This decorator does not ensure that the method is 159 | propagated. 160 | 161 | The existing class should support indexing (method __getitem__) and 162 | it should define a length (method __len__). 163 | 164 | The result will look exactly like the existing class (__name__, __doc__, 165 | __module__, __repr__ will be propagated), but the __getitem__ will be 166 | renamed to _get and __getitem__ will produce a Slicerator object 167 | when sliced. 168 | 169 | Parameters 170 | ---------- 171 | some_class : class 172 | propagated_attrs : list, optional 173 | list of attributes to be propagated into Slicerator 174 | this will overwrite any other propagation list 175 | """ 176 | 177 | class SliceratorSubclass(some_class): 178 | _slicerator_flag = True 179 | _get = some_class.__getitem__ 180 | if hasattr(some_class, '__doc__'): 181 | __doc__ = some_class.__doc__ # for Python 2, do it here 182 | 183 | def __getitem__(self, i): 184 | """Getitem supports repeated slicing via Slicerator objects.""" 185 | indices, new_length = key_to_indices(i, len(self)) 186 | if new_length is None: 187 | return self._get(indices) 188 | else: 189 | return cls(self, indices, new_length, propagate_attrs) 190 | 191 | for name in ['__name__', '__module__', '__repr__']: 192 | try: 193 | setattr(SliceratorSubclass, name, getattr(some_class, name)) 194 | except AttributeError: 195 | pass 196 | 197 | return SliceratorSubclass 198 | 199 | @property 200 | def indices(self): 201 | # Advancing indices won't affect this new copy of self._indices. 202 | indices, self._indices = itertools.tee(iter(self._indices)) 203 | return indices 204 | 205 | def _get(self, key): 206 | return self._ancestor[key] 207 | 208 | def _map_index(self, key): 209 | if key < -self._len or key >= self._len: 210 | raise IndexError("Key out of range") 211 | try: 212 | abs_key = self._indices[key] 213 | except TypeError: 214 | key = key if key >= 0 else self._len + key 215 | for _, i in zip(range(key + 1), self.indices): 216 | abs_key = i 217 | return abs_key 218 | 219 | def __repr__(self): 220 | msg = "Sliced {0}. Original repr:\n".format( 221 | type(self._ancestor).__name__) 222 | old = '\n'.join(" " + ln for ln in repr(self._ancestor).split('\n')) 223 | return msg + old 224 | 225 | def __iter__(self): 226 | return (self._get(i) for i in self.indices) 227 | 228 | def __len__(self): 229 | return self._len 230 | 231 | def __getitem__(self, key): 232 | """for data access""" 233 | if not (isinstance(key, slice) or 234 | isinstance(key, collections.abc.Iterable)): 235 | return self._get(self._map_index(key)) 236 | else: 237 | rel_indices, new_length = key_to_indices(key, len(self)) 238 | if new_length is None: 239 | return (self[k] for k in rel_indices) 240 | indices = _index_generator(rel_indices, self.indices) 241 | return Slicerator(self._ancestor, indices, new_length, 242 | self._propagate_attrs) 243 | 244 | def __getattr__(self, name): 245 | # to avoid infinite recursion, always check if public field is there 246 | if '_propagate_attrs' not in self.__dict__: 247 | self._propagate_attrs = [] 248 | if name in self._propagate_attrs: 249 | attr = getattr(self._ancestor, name) 250 | if (isinstance(attr, SliceableAttribute) or 251 | hasattr(attr, '_index_flag')): 252 | return SliceableAttribute(self, attr) 253 | else: 254 | return attr 255 | raise AttributeError 256 | 257 | def __getstate__(self): 258 | # When serializing, return a list of the sliced data 259 | # Any exposed attrs are lost. 260 | return list(self) 261 | 262 | def __setstate__(self, data_as_list): 263 | # When deserializing, restore a Slicerator instance 264 | return self.__init__(data_as_list) 265 | 266 | 267 | def key_to_indices(key, length): 268 | """Converts a fancy key into a list of indices. 269 | 270 | Parameters 271 | ---------- 272 | key : slice, iterable of numbers, or boolean mask 273 | length : integer 274 | length of object that will be indexed 275 | 276 | Returns 277 | ------- 278 | indices, new_length 279 | """ 280 | if isinstance(key, slice): 281 | # if we have a slice, return a range object returning the indices 282 | start, stop, step = key.indices(length) 283 | indices = range(start, stop, step) 284 | return indices, len(indices) 285 | 286 | if isinstance(key, collections.abc.Iterable): 287 | # if the input is an iterable, doing 'fancy' indexing 288 | if hasattr(key, '__array__') and hasattr(key, 'dtype'): 289 | if key.dtype == bool: 290 | # if we have a bool array, set up masking and return indices 291 | nums = range(length) 292 | # This next line fakes up numpy's bool masking without 293 | # importing numpy. 294 | indices = [x for x, y in zip(nums, key) if y] 295 | return indices, sum(key) 296 | try: 297 | new_length = len(key) 298 | except TypeError: 299 | # The key is a generator; return a plain old generator. 300 | # Withoug using the generator, we cannot know its length. 301 | # Also it cannot be checked if values are in range. 302 | gen = ((_k if _k >= 0 else length + _k) for _k in key) 303 | return gen, None 304 | else: 305 | # The key is a list of in-range values. Check if they are in range. 306 | if any(_k < -length or _k >= length for _k in key): 307 | raise IndexError("Keys out of range") 308 | rel_indices = ((_k if _k >= 0 else length + _k) for _k in key) 309 | return rel_indices, new_length 310 | 311 | # other cases: it's possibly a number 312 | try: 313 | key = int(key) 314 | except TypeError: 315 | pass 316 | else: 317 | # allow negative indexing 318 | if -length < key < 0: 319 | return length + key, None 320 | elif 0 <= key < length: 321 | return key, None 322 | else: 323 | raise IndexError('index out of range') 324 | 325 | # in all other case, just return the key and let user deal with the type. 326 | return key, None 327 | 328 | 329 | def _index_generator(new_indices, old_indices): 330 | """Find locations of new_indicies in the ref. frame of the old_indices. 331 | 332 | Example: (1, 3), (1, 3, 5, 10) -> (3, 10) 333 | 334 | The point of all this trouble is that this is done lazily, returning 335 | a generator without actually looping through the inputs.""" 336 | # Use iter() to be safe. On a generator, this returns an identical ref. 337 | new_indices = iter(new_indices) 338 | n = next(new_indices) 339 | last_n = None 340 | done = False 341 | while True: 342 | old_indices_, old_indices = itertools.tee(iter(old_indices)) 343 | for i, o in enumerate(old_indices_): 344 | # If new_indices is not strictly monotonically increasing, break 345 | # and start again from the beginning of old_indices. 346 | if last_n is not None and n <= last_n: 347 | last_n = None 348 | break 349 | if done: 350 | return 351 | if i == n: 352 | last_n = n 353 | try: 354 | n = next(new_indices) 355 | except StopIteration: 356 | done = True 357 | # Don't stop yet; we still have one last thing to yield. 358 | yield o 359 | else: 360 | continue 361 | 362 | 363 | class Pipeline(object): 364 | def __init__(self, proc_func, *ancestors, **kwargs): 365 | """A class to support lazy function evaluation on an iterable. 366 | 367 | When a ``Pipeline`` object is indexed, it returns an element of its 368 | ancestor modified with a process function. 369 | 370 | Parameters 371 | ---------- 372 | proc_func : function 373 | function that processes data returned by Slicerator. The function 374 | acts element-wise and is only evaluated when data is actually 375 | returned 376 | *ancestors : objects 377 | Object to be processed. 378 | propagate_attrs : set of str or None, optional 379 | Names of attributes to be propagated through the pipeline. If this 380 | is `None`, go through ancestors and look at `_propagate_attrs` 381 | and `propagate_attrs` attributes and search for attributes having 382 | a `_propagate_flag` attribute. Defaults to `None`. 383 | propagate_how : {'first', 'last'} or int, optional 384 | Where to look for attributes to propagate. If this is an integer, 385 | it specifies the index of the ancestor (in `ancestors`). If it is 386 | 'first', go through all ancestors starting with the first one until 387 | one is found that has the attribute. If it is 'last', go through 388 | the ancestors in reverse order. Defaults to 'first'. 389 | 390 | Example 391 | ------- 392 | Construct the pipeline object that multiplies elements by two: 393 | >>> ancestor = [0, 1, 2, 3, 4] 394 | >>> times_two = Pipeline(lambda x: 2*x, ancestor) 395 | 396 | Whenever the pipeline object is indexed, it takes the correct element 397 | from its ancestor, and then applies the process function. 398 | >>> times_two[3] # returns 6 399 | 400 | See also 401 | -------- 402 | pipeline 403 | """ 404 | # Python 2 does not allow default arguments in combination with 405 | # variable arguments; work around that 406 | propagate_attrs = kwargs.pop('propagate_attrs', None) 407 | propagate_how = kwargs.pop('propagate_how', 'first') 408 | if kwargs: 409 | # There are some left. This is an error. 410 | raise TypeError("Unexpected keyword argument '{}'.".format( 411 | next(iter(kwargs)))) 412 | 413 | # Only accept ancestors of the same length are accepted 414 | self._len = len(ancestors[0]) 415 | if not all(len(a) == self._len for a in ancestors): 416 | raise ValueError('Ancestors have to be of same length.') 417 | 418 | self._ancestors = ancestors 419 | self._proc_func = proc_func 420 | self._propagate_how = propagate_how 421 | 422 | # when list of propagated attributes are given explicitly, 423 | # take this list and ignore the class definition 424 | if propagate_attrs is not None: 425 | self._propagate_attrs = set(propagate_attrs) 426 | else: 427 | # check propagated_attrs field from the ancestor definition 428 | self._propagate_attrs = set() 429 | for a in self._get_prop_ancestors(): 430 | if hasattr(a, '_propagate_attrs'): 431 | self._propagate_attrs.update(a._propagate_attrs) 432 | if hasattr(a, 'propagate_attrs'): 433 | self._propagate_attrs.update(a.propagate_attrs) 434 | 435 | # add methods having the _propagate flag 436 | for attr in _iter_attr(a): 437 | if hasattr(attr, '_propagate_flag'): 438 | self._propagate_attrs.add(attr.__name__) 439 | 440 | def _get_prop_ancestors(self): 441 | """Get relevant ancestor(s) for attribute propagation 442 | 443 | Returns 444 | ------- 445 | list 446 | List of ancestors. 447 | """ 448 | if isinstance(self._propagate_how, int): 449 | return self._ancestors[self._propagate_how:self._propagate_how+1] 450 | if self._propagate_how == 'first': 451 | return self._ancestors 452 | if self._propagate_how == 'last': 453 | return self._ancestors[::-1] 454 | raise ValueError("propagate_how has to be an index, 'first', or " 455 | "'last'.") 456 | 457 | def _get(self, key): 458 | # We need to copy here: else any _proc_func that acts inplace would 459 | # change the ancestor value. 460 | return self._proc_func(*(copy(a[key]) for a in self._ancestors)) 461 | 462 | def __repr__(self): 463 | anc_str = ", ".join(type(a).__name__ for a in self._ancestors) 464 | msg = '({0},) processed through {1}. Original repr:\n '.format( 465 | anc_str, self._proc_func.__name__) 466 | old = [repr(a).replace('\n', '\n ') for a in self._ancestors] 467 | return msg + "\n ----\n ".join(old) 468 | 469 | def __len__(self): 470 | return self._len 471 | 472 | def __iter__(self): 473 | return (self._get(i) for i in range(len(self))) 474 | 475 | def __getitem__(self, i): 476 | """for data access""" 477 | indices, new_length = key_to_indices(i, len(self)) 478 | if new_length is None: 479 | return self._get(indices) 480 | else: 481 | return Slicerator(self, indices, new_length, self._propagate_attrs) 482 | 483 | def __getattr__(self, name): 484 | # to avoid infinite recursion, always check if public field is there 485 | pa = self.__dict__.get('_propagate_attrs', []) 486 | if not isinstance(pa, collections.abc.Iterable): 487 | raise TypeError('_propagate_attrs is not iterable') 488 | if name in pa: 489 | for a in self._get_prop_ancestors(): 490 | try: 491 | return getattr(a, name) 492 | except AttributeError: 493 | pass 494 | raise AttributeError('No attribute `{}` propagated.'.format(name)) 495 | 496 | def __getstate__(self): 497 | # When serializing, return a list of the processed data 498 | # Any exposed attrs are lost. 499 | return list(self) 500 | 501 | def __setstate__(self, data_as_list): 502 | # When deserializing, restore the Pipeline 503 | return self.__init__(lambda x: x, data_as_list) 504 | 505 | 506 | def pipeline(func=None, **kwargs): 507 | """Decorator to enable lazy evaluation of a function. 508 | 509 | When the function is applied to a Slicerator or Pipeline object, it 510 | returns another lazily-evaluated, Pipeline object. 511 | 512 | When the function is applied to any other object, it falls back on its 513 | normal behavior. 514 | 515 | Parameters 516 | ---------- 517 | func : callable or type 518 | Function or class type for lazy evaluation 519 | retain_doc : bool, optional 520 | If True, don't modify `func`'s doc string to say that it has been 521 | made lazy. Defaults to False 522 | ancestor_count : int or 'all', optional 523 | Number of inputs to the pipeline. For instance, 524 | a function taking three parameters that adds up the elements of 525 | two :py:class:`Slicerators` and a constant offset would have 526 | ``ancestor_count=2``. If 'all', all the function's arguments are used 527 | for the pipeline. Defaults to 1. 528 | 529 | Returns 530 | ------- 531 | Pipeline 532 | Lazy function evaluation :py:class:`Pipeline` for `func`. 533 | 534 | See also 535 | -------- 536 | Pipeline 537 | 538 | Examples 539 | -------- 540 | Apply the pipeline decorator to your image processing function. 541 | 542 | >>> @pipeline 543 | ... def color_channel(image, channel): 544 | ... return image[channel, :, :] 545 | ... 546 | 547 | 548 | In order to preserve the original function's doc string (i. e. do not add 549 | a note saying that it was made lazy), use the decorator like so: 550 | 551 | >>> @pipeline(retain_doc=True) 552 | ... def color_channel(image, channel): 553 | ... '''This doc string will not be changed''' 554 | ... return image[channel, :, :] 555 | 556 | 557 | Passing a Slicerator the function returns a Pipeline 558 | that "lazily" applies the function when the images come out. Different 559 | functions can be applied to the same underlying images, creating 560 | independent objects. 561 | 562 | >>> red_images = color_channel(images, 0) 563 | >>> green_images = color_channel(images, 1) 564 | 565 | Pipeline functions can also be composed. 566 | 567 | >>> @pipeline 568 | ... def rescale(image): 569 | ... return (image - image.min())/image.ptp() 570 | ... 571 | >>> rescale(color_channel(images, 0)) 572 | 573 | The function can still be applied to ordinary images. The decorator 574 | only takes affect when a Slicerator object is passed. 575 | 576 | >>> single_img = images[0] 577 | >>> red_img = red_channel(single_img) # normal behavior 578 | 579 | 580 | Pipeline functions can take more than one slicerator. 581 | 582 | >>> @pipeline(ancestor_count=2) 583 | ... def sum_offset(img1, img2, offset): 584 | ... return img1 + img2 + offset 585 | """ 586 | def wrapper(f): 587 | return _pipeline(f, **kwargs) 588 | 589 | if func is None: 590 | return wrapper 591 | else: 592 | return wrapper(func) 593 | 594 | 595 | def _pipeline(func_or_class, **kwargs): 596 | try: 597 | is_class = issubclass(func_or_class, Pipeline) 598 | except TypeError: 599 | is_class = False 600 | if is_class: 601 | return _pipeline_fromclass(func_or_class, **kwargs) 602 | else: 603 | return _pipeline_fromfunc(func_or_class, **kwargs) 604 | 605 | 606 | def _pipeline_fromclass(cls, retain_doc=False, ancestor_count=1): 607 | """Actual `pipeline` implementation 608 | 609 | Parameters 610 | ---------- 611 | func : class 612 | Class for lazy evaluation 613 | retain_doc : bool 614 | If True, don't modify `func`'s doc string to say that it has been 615 | made lazy 616 | ancestor_count : int or 'all', optional 617 | Number of inputs to the pipeline. Defaults to 1. 618 | 619 | Returns 620 | ------- 621 | Pipeline 622 | Lazy function evaluation :py:class:`Pipeline` for `func`. 623 | """ 624 | if ancestor_count == 'all': 625 | # subtract 1 for `self` 626 | ancestor_count = len(inspect.getfullargspec(cls).args) - 1 627 | 628 | @wraps(cls) 629 | def process(*args, **kwargs): 630 | ancestors = args[:ancestor_count] 631 | args = args[ancestor_count:] 632 | all_pipe = all(hasattr(a, '_slicerator_flag') or 633 | isinstance(a, Slicerator) or 634 | isinstance(a, Pipeline) for a in ancestors) 635 | if all_pipe: 636 | return cls(*(ancestors + args), **kwargs) 637 | else: 638 | # Fall back on normal behavior of func, interpreting input 639 | # as a single image. 640 | return cls(*(tuple([a] for a in ancestors) + args), **kwargs)[0] 641 | 642 | if not retain_doc: 643 | if process.__doc__ is None: 644 | process.__doc__ = '' 645 | process.__doc__ = ("This function has been made lazy. When passed\n" 646 | "a Slicerator, it will return a \n" 647 | "Pipeline of the results. When passed \n" 648 | "any other objects, its behavior is " 649 | "unchanged.\n\n") + process.__doc__ 650 | process.__name__ = cls.__name__ 651 | return process 652 | 653 | 654 | def _pipeline_fromfunc(func, retain_doc=False, ancestor_count=1): 655 | """Actual `pipeline` implementation 656 | 657 | Parameters 658 | ---------- 659 | func : callable 660 | Function for lazy evaluation 661 | retain_doc : bool 662 | If True, don't modify `func`'s doc string to say that it has been 663 | made lazy 664 | ancestor_count : int or 'all', optional 665 | Number of inputs to the pipeline. Defaults to 1. 666 | 667 | Returns 668 | ------- 669 | Pipeline 670 | Lazy function evaluation :py:class:`Pipeline` for `func`. 671 | """ 672 | if ancestor_count == 'all': 673 | ancestor_count = len(inspect.getfullargspec(func).args) 674 | 675 | @wraps(func) 676 | def process(*args, **kwargs): 677 | ancestors = args[:ancestor_count] 678 | args = args[ancestor_count:] 679 | all_pipe = all(hasattr(a, '_slicerator_flag') or 680 | isinstance(a, Slicerator) or 681 | isinstance(a, Pipeline) for a in ancestors) 682 | if all_pipe: 683 | def proc_func(*x): 684 | return func(*(x + args), **kwargs) 685 | 686 | return Pipeline(proc_func, *ancestors) 687 | else: 688 | # Fall back on normal behavior of func, interpreting input 689 | # as a single image. 690 | return func(*(ancestors + args), **kwargs) 691 | 692 | if not retain_doc: 693 | if process.__doc__ is None: 694 | process.__doc__ = '' 695 | process.__doc__ = ("This function has been made lazy. When passed\n" 696 | "a Slicerator, it will return a \n" 697 | "Pipeline of the results. When passed \n" 698 | "any other objects, its behavior is " 699 | "unchanged.\n\n") + process.__doc__ 700 | process.__name__ = func.__name__ 701 | return process 702 | 703 | 704 | def propagate_attr(func): 705 | func._propagate_flag = True 706 | return func 707 | 708 | 709 | def index_attr(func): 710 | @wraps(func) 711 | def wrapper(obj, key, *args, **kwargs): 712 | indices = key_to_indices(key, len(obj))[0] 713 | if isinstance(indices, collections.abc.Iterable): 714 | return (func(obj, i, *args, **kwargs) for i in indices) 715 | else: 716 | return func(obj, indices, *args, **kwargs) 717 | wrapper._index_flag = True 718 | return wrapper 719 | 720 | 721 | class SliceableAttribute(object): 722 | """This class enables index-taking methods that are linked to a Slicerator 723 | object to remap their indices according to the Slicerator indices. 724 | 725 | It also enables fancy indexing, exactly like the Slicerator itself. The new 726 | attribute supports both calling and indexing to give identical results.""" 727 | 728 | def __init__(self, slicerator, attribute): 729 | self._ancestor = slicerator._ancestor 730 | self._len = slicerator._len 731 | self._get = attribute 732 | self._indices = slicerator.indices # make an independent copy 733 | 734 | @property 735 | def indices(self): 736 | # Advancing indices won't affect this new copy of self._indices. 737 | indices, self._indices = itertools.tee(iter(self._indices)) 738 | return indices 739 | 740 | def _map_index(self, key): 741 | if key < -self._len or key >= self._len: 742 | raise IndexError("Key out of range") 743 | try: 744 | abs_key = self._indices[key] 745 | except TypeError: 746 | key = key if key >= 0 else self._len + key 747 | for _, i in zip(range(key + 1), self.indices): 748 | abs_key = i 749 | return abs_key 750 | 751 | def __iter__(self): 752 | return (self._get(i) for i in self.indices) 753 | 754 | def __len__(self): 755 | return self._len 756 | 757 | def __call__(self, key, *args, **kwargs): 758 | if not (isinstance(key, slice) or 759 | isinstance(key, collections.abc.Iterable)): 760 | return self._get(self._map_index(key), *args, **kwargs) 761 | else: 762 | rel_indices, new_length = key_to_indices(key, len(self)) 763 | return (self[k] for k in rel_indices) 764 | 765 | def __getitem__(self, key): 766 | return self(key) 767 | -------------------------------------------------------------------------------- /slicerator/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> main)" 27 | git_full = "8669c2606aae25f66ed94b1b16ef73132e4b2047" 28 | git_date = "2022-04-08 16:31:31 -0400" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440" 44 | cfg.tag_prefix = "v" 45 | cfg.parentdir_prefix = "None" 46 | cfg.versionfile_source = "slicerator/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /sphinx-requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc 2 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, print_function, 2 | unicode_literals) 3 | 4 | import os 5 | import numpy as np 6 | import random 7 | import types 8 | from io import BytesIO 9 | import pickle 10 | import pytest 11 | from numpy.testing import assert_array_equal 12 | from slicerator import Slicerator, Pipeline, pipeline, index_attr, propagate_attr 13 | 14 | path, _ = os.path.split(os.path.abspath(__file__)) 15 | path = os.path.join(path, 'data') 16 | 17 | 18 | def assert_letters_equal(actual, expected): 19 | # check if both lengths are equal 20 | assert len(actual) == len(expected) 21 | for actual_, expected_ in zip(actual, expected): 22 | assert actual_ == expected_ 23 | 24 | 25 | def compare_slice_to_list(actual, expected): 26 | assert_letters_equal(actual, expected) 27 | indices = list(range(len(actual))) 28 | for i in indices: 29 | # test positive indexing 30 | assert_letters_equal(actual[i], expected[i]) 31 | # test negative indexing 32 | assert_letters_equal(actual[-i + 1], expected[-i + 1]) 33 | # in reverse order 34 | for i in indices[::-1]: 35 | assert_letters_equal(actual[i], expected[i]) 36 | assert_letters_equal(actual[-i + 1], expected[-i + 1]) 37 | # in shuffled order (using a consistent random seed) 38 | r = random.Random(5) 39 | r.shuffle(indices) 40 | for i in indices: 41 | assert_letters_equal(actual[i], expected[i]) 42 | assert_letters_equal(actual[-i + 1], expected[-i + 1]) 43 | # test list indexing 44 | some_indices = [r.choice(indices) for _ in range(2)] 45 | assert_letters_equal([actual[i] for i in some_indices], 46 | [expected[i] for i in some_indices]) 47 | # mixing positive and negative indices 48 | some_indices = [r.choice(indices + [-i-1 for i in indices]) 49 | for _ in range(2)] 50 | assert_letters_equal([actual[i] for i in some_indices], 51 | [expected[i] for i in some_indices]) 52 | # test slices 53 | assert_letters_equal(actual[::2], expected[::2]) 54 | assert_letters_equal(actual[1::2], expected[1::2]) 55 | assert_letters_equal(actual[::3], expected[::3]) 56 | assert_letters_equal(actual[1:], expected[1:]) 57 | assert_letters_equal(actual[:], expected[:]) 58 | assert_letters_equal(actual[:-1], expected[:-1]) 59 | 60 | 61 | v = Slicerator(list('abcdefghij')) 62 | n = Slicerator(list(range(10))) 63 | 64 | 65 | def test_bool_mask(): 66 | mask = np.array([True, False] * 5) 67 | s = v[mask] 68 | assert_letters_equal(s, list('acegi')) 69 | 70 | 71 | def test_slice_of_slice(): 72 | slice1 = v[4:] 73 | compare_slice_to_list(slice1, list('efghij')) 74 | slice2 = slice1[-3:] 75 | compare_slice_to_list(slice2, list('hij')) 76 | slice1a = v[[3, 4, 5, 6, 7, 8, 9]] 77 | compare_slice_to_list(slice1a, list('defghij')) 78 | slice2a = slice1a[::2] 79 | compare_slice_to_list(slice2a, list('dfhj')) 80 | slice2b = slice1a[::-1] 81 | compare_slice_to_list(slice2b, list('jihgfed')) 82 | slice2c = slice1a[::-2] 83 | compare_slice_to_list(slice2c, list('jhfd')) 84 | slice2d = slice1a[:0:-1] 85 | compare_slice_to_list(slice2d, list('jihgfe')) 86 | slice2e = slice1a[-1:1:-1] 87 | compare_slice_to_list(slice2e, list('jihgf')) 88 | slice2f = slice1a[-2:1:-1] 89 | compare_slice_to_list(slice2f, list('ihgf')) 90 | slice2g = slice1a[::-3] 91 | compare_slice_to_list(slice2g, list('jgd')) 92 | slice2h = slice1a[[5, 6, 2, -1, 3, 3, 3, 0]] 93 | compare_slice_to_list(slice2h, list('ijfjgggd')) 94 | 95 | 96 | def test_slice_of_slice_of_slice(): 97 | slice1 = v[4:] 98 | compare_slice_to_list(slice1, list('efghij')) 99 | slice2 = slice1[1:-1] 100 | compare_slice_to_list(slice2, list('fghi')) 101 | slice2a = slice1[[2, 3, 4]] 102 | compare_slice_to_list(slice2a, list('ghi')) 103 | slice3 = slice2[1::2] 104 | compare_slice_to_list(slice3, list('gi')) 105 | 106 | 107 | def test_slice_of_slice_of_slice_of_slice(): 108 | # Take the red pill. It's slices all the way down! 109 | slice1 = v[4:] 110 | compare_slice_to_list(slice1, list('efghij')) 111 | slice2 = slice1[1:-1] 112 | compare_slice_to_list(slice2, list('fghi')) 113 | slice3 = slice2[1:] 114 | compare_slice_to_list(slice3, list('ghi')) 115 | slice4 = slice3[1:] 116 | compare_slice_to_list(slice4, list('hi')) 117 | 118 | # Give me another! 119 | slice1 = v[2:] 120 | compare_slice_to_list(slice1, list('cdefghij')) 121 | slice2 = slice1[0::2] 122 | compare_slice_to_list(slice2, list('cegi')) 123 | slice3 = slice2[:] 124 | compare_slice_to_list(slice3, list('cegi')) 125 | slice4 = slice3[:-1] 126 | compare_slice_to_list(slice4, list('ceg')) 127 | slice4a = slice3[::-1] 128 | compare_slice_to_list(slice4a, list('igec')) 129 | 130 | 131 | def test_slice_with_generator(): 132 | slice1 = v[1:] 133 | compare_slice_to_list(slice1, list('bcdefghij')) 134 | slice2 = slice1[(i for i in range(2, 5))] 135 | assert_letters_equal(list(slice2), list('def')) 136 | assert isinstance(slice2, types.GeneratorType) 137 | 138 | 139 | def test_no_len_raises(): 140 | with pytest.raises(ValueError): 141 | Slicerator((i for i in range(5)), (i for i in range(5))) 142 | 143 | def test_from_func(): 144 | v = Slicerator.from_func(lambda x: 'abcdefghij'[x], length=10) 145 | compare_slice_to_list(v, list('abcdefghij')) 146 | compare_slice_to_list(v[1:], list('bcdefghij')) 147 | compare_slice_to_list(v[1:][:4], list('bcde')) 148 | 149 | def _capitalize(letter): 150 | return letter.upper() 151 | 152 | 153 | def _capitalize_if_equal(letter, other_letter): 154 | if letter == other_letter: 155 | return letter.upper() 156 | else: 157 | return letter 158 | 159 | 160 | def _a_to_z(letter): 161 | if letter == 'a': 162 | return 'z' 163 | else: 164 | return letter 165 | 166 | @pipeline 167 | def append_zero_inplace(list_obj): 168 | list_obj.append(0) 169 | return list_obj 170 | 171 | 172 | def test_inplace_pipeline(): 173 | n_mutable = Slicerator([list([i]) for i in range(10)]) 174 | appended = append_zero_inplace(n_mutable) 175 | 176 | assert appended[5] == [5, 0] # execute the function 177 | assert n_mutable[5] == [5] # check the original 178 | 179 | 180 | def test_pipeline_simple(): 181 | capitalize = pipeline(_capitalize) 182 | cap_v = capitalize(v[:1]) 183 | 184 | assert_letters_equal([cap_v[0]], [_capitalize(v[0])]) 185 | 186 | 187 | def test_pipeline_propagation(): 188 | capitalize = pipeline(_capitalize) 189 | cap_v = capitalize(v) 190 | 191 | assert_letters_equal([cap_v[:1][0]], ['A']) 192 | assert_letters_equal([cap_v[:1][:2][0]], ['A']) 193 | 194 | 195 | def test_pipeline_nesting(): 196 | capitalize = pipeline(_capitalize) 197 | a_to_z = pipeline(_a_to_z) 198 | nested_v = capitalize(a_to_z(v)) 199 | 200 | assert_letters_equal([nested_v[0]], ['Z']) 201 | assert_letters_equal([nested_v[:1][0]], ['Z']) 202 | 203 | 204 | def _add_one(number): 205 | return number + 1 206 | 207 | 208 | def test_pipeline_nesting_numeric(): 209 | add_one = pipeline(_add_one) 210 | triple_nested = add_one(add_one(add_one(n))) 211 | assert_letters_equal([triple_nested[0]], [3]) 212 | assert_letters_equal([triple_nested[:1][0]], [3]) 213 | 214 | 215 | def test_repr(): 216 | repr(v) 217 | 218 | 219 | def test_getattr(): 220 | class MyList(list): 221 | attr1 = 'hello' 222 | attr2 = 'hello again' 223 | 224 | @index_attr 225 | def s(self, i): 226 | return list('ABCDEFGHIJ')[i] 227 | 228 | def close(self): 229 | pass 230 | 231 | a = Slicerator(MyList('abcdefghij'), propagate_attrs=['attr1', 's']) 232 | assert_letters_equal(a, list('abcdefghij')) 233 | assert hasattr(a, 'attr1') 234 | assert not hasattr(a, 'attr2') 235 | assert hasattr(a, 's') 236 | assert not hasattr(a, 'close') 237 | assert a.attr1 == 'hello' 238 | with pytest.raises(AttributeError): 239 | a[:5].nonexistent_attr 240 | 241 | compare_slice_to_list(list(a.s), list('ABCDEFGHIJ')) 242 | compare_slice_to_list(list(a[::2].s), list('ACEGI')) 243 | compare_slice_to_list(list(a[::2][1:].s), list('CEGI')) 244 | 245 | capitalize = pipeline(_capitalize) 246 | b = capitalize(a) 247 | assert_letters_equal(b, list('ABCDEFGHIJ')) 248 | assert hasattr(b, 'attr1') 249 | assert not hasattr(b, 'attr2') 250 | assert hasattr(b, 's') 251 | assert not hasattr(b, 'close') 252 | assert b.attr1 == 'hello' 253 | with pytest.raises(AttributeError): 254 | b[:5].nonexistent_attr 255 | 256 | compare_slice_to_list(list(b.s), list('ABCDEFGHIJ')) 257 | compare_slice_to_list(list(b[::2].s), list('ACEGI')) 258 | compare_slice_to_list(list(b[::2][1:].s), list('CEGI')) 259 | 260 | 261 | def test_getattr_subclass(): 262 | @Slicerator.from_class 263 | class Dummy(object): 264 | propagate_attrs = ['attr1'] 265 | def __init__(self): 266 | self.frame = list('abcdefghij') 267 | 268 | def __len__(self): 269 | return len(self.frame) 270 | 271 | def __getitem__(self, i): 272 | return self.frame[i] 273 | 274 | def attr1(self): 275 | # propagates through slices of Dummy 276 | return 'sliced' 277 | 278 | @propagate_attr 279 | def attr2(self): 280 | # propagates through slices of Dummy and subclasses 281 | return 'also in subclasses' 282 | 283 | def attr3(self): 284 | # does not propagate 285 | return 'only unsliced' 286 | 287 | 288 | class SubClass(Dummy): 289 | propagate_attrs = ['attr4'] # overwrites propagated attrs from Dummy 290 | 291 | def __len__(self): 292 | return len(self.frame) 293 | 294 | @property 295 | def attr4(self): 296 | # propagates through slices of SubClass 297 | return 'only subclass' 298 | 299 | dummy = Dummy() 300 | subclass = SubClass() 301 | assert hasattr(dummy, 'attr1') 302 | assert hasattr(dummy, 'attr2') 303 | assert hasattr(dummy, 'attr3') 304 | assert not hasattr(dummy, 'attr4') 305 | 306 | assert hasattr(dummy[1:], 'attr1') 307 | assert hasattr(dummy[1:], 'attr2') 308 | assert not hasattr(dummy[1:], 'attr3') 309 | assert not hasattr(dummy[1:], 'attr4') 310 | 311 | assert hasattr(dummy[1:][1:], 'attr1') 312 | assert hasattr(dummy[1:][1:], 'attr2') 313 | assert not hasattr(dummy[1:][1:], 'attr3') 314 | assert not hasattr(dummy[1:][1:], 'attr4') 315 | 316 | assert hasattr(subclass, 'attr1') 317 | assert hasattr(subclass, 'attr2') 318 | assert hasattr(subclass, 'attr3') 319 | assert hasattr(subclass, 'attr4') 320 | 321 | assert not hasattr(subclass[1:], 'attr1') 322 | assert hasattr(subclass[1:], 'attr2') 323 | assert not hasattr(subclass[1:], 'attr3') 324 | assert hasattr(subclass[1:], 'attr4') 325 | 326 | assert not hasattr(subclass[1:][1:], 'attr1') 327 | assert hasattr(subclass[1:][1:], 'attr2') 328 | assert not hasattr(subclass[1:][1:], 'attr3') 329 | assert hasattr(subclass[1:][1:], 'attr4') 330 | 331 | 332 | def test_pipeline_with_args(): 333 | capitalize = pipeline(_capitalize_if_equal) 334 | cap_a = capitalize(v, 'a') 335 | cap_b = capitalize(v, 'b') 336 | 337 | assert_letters_equal(cap_a, 'Abcdefghij') 338 | assert_letters_equal(cap_b, 'aBcdefghij') 339 | assert_letters_equal([cap_a[0]], ['A']) 340 | assert_letters_equal([cap_b[0]], ['a']) 341 | assert_letters_equal([cap_a[0]], ['A']) 342 | 343 | 344 | def test_composed_pipelines(): 345 | a_to_z = pipeline(_a_to_z) 346 | capitalize = pipeline(_capitalize_if_equal) 347 | 348 | composed = capitalize(a_to_z(v), 'c') 349 | 350 | assert_letters_equal(composed, 'zbCdefghij') 351 | 352 | 353 | def test_pipeline_class(): 354 | sli = Slicerator(np.empty((10, 32, 64))) 355 | 356 | @pipeline 357 | class crop(Pipeline): 358 | def __init__(self, reader, bbox): 359 | self.bbox = bbox 360 | Pipeline.__init__(self, None, reader) 361 | 362 | def _get(self, key): 363 | bbox = self.bbox 364 | return self._ancestors[0][key][bbox[0]:bbox[2], bbox[1]:bbox[3]] 365 | 366 | @property 367 | def frame_shape(self): 368 | bbox = self.bbox 369 | return (bbox[2] - bbox[0], bbox[3] - bbox[1]) 370 | 371 | cropped = crop(sli, (5, 5, 10, 20)) 372 | assert_array_equal(cropped[0], sli[0][5:10, 5:20]) 373 | assert_array_equal(cropped.frame_shape, (5, 15)) 374 | 375 | 376 | def test_serialize(): 377 | # dump Slicerator 378 | stream = BytesIO() 379 | pickle.dump(v, stream) 380 | stream.seek(0) 381 | v2 = pickle.load(stream) 382 | stream.close() 383 | compare_slice_to_list(v2, list('abcdefghij')) 384 | compare_slice_to_list(v2[4:], list('efghij')) 385 | compare_slice_to_list(v2[4:][:-1], list('efghi')) 386 | 387 | # dump sliced Slicerator 388 | stream = BytesIO() 389 | pickle.dump(v[4:], stream) 390 | stream.seek(0) 391 | v2 = pickle.load(stream) 392 | stream.close() 393 | compare_slice_to_list(v2, list('efghij')) 394 | compare_slice_to_list(v2[2:], list('ghij')) 395 | compare_slice_to_list(v2[2:][:-1], list('ghi')) 396 | 397 | # dump sliced sliced Slicerator 398 | stream = BytesIO() 399 | pickle.dump(v[4:][:-1], stream) 400 | stream.seek(0) 401 | v2 = pickle.load(stream) 402 | stream.close() 403 | compare_slice_to_list(v2, list('efghi')) 404 | compare_slice_to_list(v2[2:], list('ghi')) 405 | compare_slice_to_list(v2[2:][:-1], list('gh')) 406 | 407 | # test pipeline 408 | capitalize = pipeline(_capitalize_if_equal) 409 | stream = BytesIO() 410 | pickle.dump(capitalize(v, 'a'), stream) 411 | stream.seek(0) 412 | v2 = pickle.load(stream) 413 | stream.close() 414 | compare_slice_to_list(v2, list('Abcdefghij')) 415 | 416 | 417 | def test_from_class(): 418 | class Dummy(object): 419 | """DocString""" 420 | def __init__(self): 421 | self.frame = list('abcdefghij') 422 | 423 | def __len__(self): 424 | return len(self.frame) 425 | 426 | def __getitem__(self, i): 427 | """Other Docstring""" 428 | return self.frame[i] # actual code of get_frame 429 | 430 | def __repr__(self): 431 | return 'Repr' 432 | 433 | DummySli = Slicerator.from_class(Dummy) 434 | assert Dummy()[:2] == ['a', 'b'] # Dummy is unaffected 435 | 436 | # class slots propagate 437 | assert DummySli.__name__ == Dummy.__name__ 438 | assert DummySli.__doc__ == Dummy.__doc__ 439 | assert DummySli.__module__ == Dummy.__module__ 440 | 441 | dummy = DummySli() 442 | assert isinstance(dummy, Dummy) # still instance of Dummy 443 | assert repr(dummy) == 'Repr' # repr propagates 444 | 445 | compare_slice_to_list(dummy, 'abcdefghij') 446 | compare_slice_to_list(dummy[1:], 'bcdefghij') 447 | compare_slice_to_list(dummy[1:][2:], 'defghij') 448 | 449 | capitalize = pipeline(_capitalize_if_equal) 450 | cap_b = capitalize(dummy, 'b') 451 | assert_letters_equal(cap_b, 'aBcdefghij') 452 | 453 | 454 | def test_lazy_hasattr(): 455 | # this ensures that the Slicerator init does not evaluate all properties 456 | class Dummy(object): 457 | """DocString""" 458 | def __init__(self): 459 | self.frame = list('abcdefghij') 460 | 461 | def __len__(self): 462 | return len(self.frame) 463 | 464 | def __getitem__(self, i): 465 | """Other Docstring""" 466 | return self.frame[i] # actual code of get_frame 467 | 468 | @property 469 | def forbidden_property(self): 470 | raise RuntimeError() 471 | 472 | DummySli = Slicerator.from_class(Dummy) 473 | 474 | 475 | def test_pipeline_multi_input(): 476 | @pipeline(ancestor_count=2) 477 | def sum_offset(p1, p2, o): 478 | return p1 + p2 + o 479 | 480 | p1 = Slicerator(list(range(10))) 481 | p2 = Slicerator(list(range(10, 20))) 482 | o = 100 483 | 484 | res = sum_offset(p1, p2, o) 485 | assert(isinstance(res, Pipeline)) 486 | assert_array_equal(res, list(range(110, 129, 2))) 487 | assert(len(res) == len(p1)) 488 | 489 | resi = sum_offset(1, 2, 3) 490 | assert(isinstance(resi, int)) 491 | assert(resi == 6) 492 | 493 | p3 = Slicerator(list(range(20))) 494 | try: 495 | sum_offset(p1, p3) 496 | except ValueError: 497 | pass 498 | else: 499 | raise AssertionError("Should be unable to create pipeline with " 500 | "ancestors having different lengths.") 501 | 502 | 503 | def test_pipeline_propagate_attrs(): 504 | a1 = Slicerator(list(range(10))) 505 | a1.attr1 = 10 506 | a2 = Slicerator(list(range(10, 20))) 507 | a2.attr1 = 20 508 | a2.attr2 = 30 509 | 510 | p1 = Pipeline(lambda x, y: x + y, a1, a2, 511 | propagate_attrs={"attr1", "attr2"}, propagate_how=0) 512 | assert(p1.attr1 == 10) 513 | try: 514 | p1.attr2 515 | except AttributeError: 516 | pass 517 | else: 518 | raise AssertionError("attr2 should not exist") 519 | 520 | p2 = Pipeline(lambda x, y: x + y, a1, a2, 521 | propagate_attrs={"attr1", "attr2"}, propagate_how=1) 522 | assert(p2.attr1 == 20) 523 | assert(p2.attr2 == 30) 524 | 525 | p3 = Pipeline(lambda x, y: x + y, a1, a2, 526 | propagate_attrs={"attr1", "attr2"}, propagate_how="first") 527 | assert(p3.attr1 == 10) 528 | assert(p3.attr2 == 30) 529 | 530 | p4 = Pipeline(lambda x, y: x + y, a1, a2, 531 | propagate_attrs={"attr1", "attr2"}, propagate_how="last") 532 | assert(p4.attr1 == 20) 533 | assert(p4.attr2 == 30) 534 | 535 | a1.attr3 = 40 536 | a1.attr4 = 50 537 | a1._propagate_attrs = {"attr3"} 538 | a1.propagate_attrs = {"attr4"} 539 | p5 = Pipeline(lambda x, y: x + y, a1, a2, propagate_how="first") 540 | assert(p5.attr3 == 40) 541 | assert(p5.attr4 == 50) 542 | try: 543 | p5.attr1 544 | except AttributeError: 545 | pass 546 | else: 547 | raise AssertionError("attr1 should not exist") 548 | try: 549 | p5.attr2 550 | except AttributeError: 551 | pass 552 | else: 553 | raise AssertionError("attr2 should not exist") 554 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.18 3 | 4 | """The Versioneer - like a rocketeer, but for versions. 5 | 6 | The Versioneer 7 | ============== 8 | 9 | * like a rocketeer, but for versions! 10 | * https://github.com/warner/python-versioneer 11 | * Brian Warner 12 | * License: Public Domain 13 | * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy 14 | * [![Latest Version] 15 | (https://pypip.in/version/versioneer/badge.svg?style=flat) 16 | ](https://pypi.python.org/pypi/versioneer/) 17 | * [![Build Status] 18 | (https://travis-ci.org/warner/python-versioneer.png?branch=master) 19 | ](https://travis-ci.org/warner/python-versioneer) 20 | 21 | This is a tool for managing a recorded version number in distutils-based 22 | python projects. The goal is to remove the tedious and error-prone "update 23 | the embedded version string" step from your release process. Making a new 24 | release should be as easy as recording a new tag in your version-control 25 | system, and maybe making new tarballs. 26 | 27 | 28 | ## Quick Install 29 | 30 | * `pip install versioneer` to somewhere to your $PATH 31 | * add a `[versioneer]` section to your setup.cfg (see below) 32 | * run `versioneer install` in your source tree, commit the results 33 | 34 | ## Version Identifiers 35 | 36 | Source trees come from a variety of places: 37 | 38 | * a version-control system checkout (mostly used by developers) 39 | * a nightly tarball, produced by build automation 40 | * a snapshot tarball, produced by a web-based VCS browser, like github's 41 | "tarball from tag" feature 42 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 43 | 44 | Within each source tree, the version identifier (either a string or a number, 45 | this tool is format-agnostic) can come from a variety of places: 46 | 47 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 48 | about recent "tags" and an absolute revision-id 49 | * the name of the directory into which the tarball was unpacked 50 | * an expanded VCS keyword ($Id$, etc) 51 | * a `_version.py` created by some earlier build step 52 | 53 | For released software, the version identifier is closely related to a VCS 54 | tag. Some projects use tag names that include more than just the version 55 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 56 | needs to strip the tag prefix to extract the version identifier. For 57 | unreleased software (between tags), the version identifier should provide 58 | enough information to help developers recreate the same tree, while also 59 | giving them an idea of roughly how old the tree is (after version 1.2, before 60 | version 1.3). Many VCS systems can report a description that captures this, 61 | for example `git describe --tags --dirty --always` reports things like 62 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 63 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 64 | uncommitted changes. 65 | 66 | The version identifier is used for multiple purposes: 67 | 68 | * to allow the module to self-identify its version: `myproject.__version__` 69 | * to choose a name and prefix for a 'setup.py sdist' tarball 70 | 71 | ## Theory of Operation 72 | 73 | Versioneer works by adding a special `_version.py` file into your source 74 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 75 | dynamically ask the VCS tool for version information at import time. 76 | 77 | `_version.py` also contains `$Revision$` markers, and the installation 78 | process marks `_version.py` to have this marker rewritten with a tag name 79 | during the `git archive` command. As a result, generated tarballs will 80 | contain enough information to get the proper version. 81 | 82 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 83 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 84 | that configures it. This overrides several distutils/setuptools commands to 85 | compute the version when invoked, and changes `setup.py build` and `setup.py 86 | sdist` to replace `_version.py` with a small static file that contains just 87 | the generated version data. 88 | 89 | ## Installation 90 | 91 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 92 | 93 | ## Version-String Flavors 94 | 95 | Code which uses Versioneer can learn about its version string at runtime by 96 | importing `_version` from your main `__init__.py` file and running the 97 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 98 | import the top-level `versioneer.py` and run `get_versions()`. 99 | 100 | Both functions return a dictionary with different flavors of version 101 | information: 102 | 103 | * `['version']`: A condensed version string, rendered using the selected 104 | style. This is the most commonly used value for the project's version 105 | string. The default "pep440" style yields strings like `0.11`, 106 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 107 | below for alternative styles. 108 | 109 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 110 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 111 | 112 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 113 | commit date in ISO 8601 format. This will be None if the date is not 114 | available. 115 | 116 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 117 | this is only accurate if run in a VCS checkout, otherwise it is likely to 118 | be False or None 119 | 120 | * `['error']`: if the version string could not be computed, this will be set 121 | to a string describing the problem, otherwise it will be None. It may be 122 | useful to throw an exception in setup.py if this is set, to avoid e.g. 123 | creating tarballs with a version string of "unknown". 124 | 125 | Some variants are more useful than others. Including `full-revisionid` in a 126 | bug report should allow developers to reconstruct the exact code being tested 127 | (or indicate the presence of local changes that should be shared with the 128 | developers). `version` is suitable for display in an "about" box or a CLI 129 | `--version` output: it can be easily compared against release notes and lists 130 | of bugs fixed in various releases. 131 | 132 | The installer adds the following text to your `__init__.py` to place a basic 133 | version in `YOURPROJECT.__version__`: 134 | 135 | from ._version import get_versions 136 | __version__ = get_versions()['version'] 137 | del get_versions 138 | 139 | ## Styles 140 | 141 | The setup.cfg `style=` configuration controls how the VCS information is 142 | rendered into a version string. 143 | 144 | The default style, "pep440", produces a PEP440-compliant string, equal to the 145 | un-prefixed tag name for actual releases, and containing an additional "local 146 | version" section with more detail for in-between builds. For Git, this is 147 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 148 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 149 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 150 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 151 | software (exactly equal to a known tag), the identifier will only contain the 152 | stripped tag, e.g. "0.11". 153 | 154 | Other styles are available. See [details.md](details.md) in the Versioneer 155 | source tree for descriptions. 156 | 157 | ## Debugging 158 | 159 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 160 | to return a version of "0+unknown". To investigate the problem, run `setup.py 161 | version`, which will run the version-lookup code in a verbose mode, and will 162 | display the full contents of `get_versions()` (including the `error` string, 163 | which may help identify what went wrong). 164 | 165 | ## Known Limitations 166 | 167 | Some situations are known to cause problems for Versioneer. This details the 168 | most significant ones. More can be found on Github 169 | [issues page](https://github.com/warner/python-versioneer/issues). 170 | 171 | ### Subprojects 172 | 173 | Versioneer has limited support for source trees in which `setup.py` is not in 174 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 175 | two common reasons why `setup.py` might not be in the root: 176 | 177 | * Source trees which contain multiple subprojects, such as 178 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 179 | "master" and "slave" subprojects, each with their own `setup.py`, 180 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 181 | distributions (and upload multiple independently-installable tarballs). 182 | * Source trees whose main purpose is to contain a C library, but which also 183 | provide bindings to Python (and perhaps other langauges) in subdirectories. 184 | 185 | Versioneer will look for `.git` in parent directories, and most operations 186 | should get the right version string. However `pip` and `setuptools` have bugs 187 | and implementation details which frequently cause `pip install .` from a 188 | subproject directory to fail to find a correct version string (so it usually 189 | defaults to `0+unknown`). 190 | 191 | `pip install --editable .` should work correctly. `setup.py install` might 192 | work too. 193 | 194 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 195 | some later version. 196 | 197 | [Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking 198 | this issue. The discussion in 199 | [PR #61](https://github.com/warner/python-versioneer/pull/61) describes the 200 | issue from the Versioneer side in more detail. 201 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 202 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 203 | pip to let Versioneer work correctly. 204 | 205 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 206 | `setup.cfg`, so subprojects were completely unsupported with those releases. 207 | 208 | ### Editable installs with setuptools <= 18.5 209 | 210 | `setup.py develop` and `pip install --editable .` allow you to install a 211 | project into a virtualenv once, then continue editing the source code (and 212 | test) without re-installing after every change. 213 | 214 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 215 | convenient way to specify executable scripts that should be installed along 216 | with the python package. 217 | 218 | These both work as expected when using modern setuptools. When using 219 | setuptools-18.5 or earlier, however, certain operations will cause 220 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 221 | script, which must be resolved by re-installing the package. This happens 222 | when the install happens with one version, then the egg_info data is 223 | regenerated while a different version is checked out. Many setup.py commands 224 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 225 | a different virtualenv), so this can be surprising. 226 | 227 | [Bug #83](https://github.com/warner/python-versioneer/issues/83) describes 228 | this one, but upgrading to a newer version of setuptools should probably 229 | resolve it. 230 | 231 | ### Unicode version strings 232 | 233 | While Versioneer works (and is continually tested) with both Python 2 and 234 | Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. 235 | Newer releases probably generate unicode version strings on py2. It's not 236 | clear that this is wrong, but it may be surprising for applications when then 237 | write these strings to a network connection or include them in bytes-oriented 238 | APIs like cryptographic checksums. 239 | 240 | [Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates 241 | this question. 242 | 243 | 244 | ## Updating Versioneer 245 | 246 | To upgrade your project to a new release of Versioneer, do the following: 247 | 248 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 249 | * edit `setup.cfg`, if necessary, to include any new configuration settings 250 | indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. 251 | * re-run `versioneer install` in your source tree, to replace 252 | `SRC/_version.py` 253 | * commit any changed files 254 | 255 | ## Future Directions 256 | 257 | This tool is designed to make it easily extended to other version-control 258 | systems: all VCS-specific components are in separate directories like 259 | src/git/ . The top-level `versioneer.py` script is assembled from these 260 | components by running make-versioneer.py . In the future, make-versioneer.py 261 | will take a VCS name as an argument, and will construct a version of 262 | `versioneer.py` that is specific to the given VCS. It might also take the 263 | configuration arguments that are currently provided manually during 264 | installation by editing setup.py . Alternatively, it might go the other 265 | direction and include code from all supported VCS systems, reducing the 266 | number of intermediate scripts. 267 | 268 | 269 | ## License 270 | 271 | To make Versioneer easier to embed, all its code is dedicated to the public 272 | domain. The `_version.py` that it creates is also in the public domain. 273 | Specifically, both are released under the Creative Commons "Public Domain 274 | Dedication" license (CC0-1.0), as described in 275 | https://creativecommons.org/publicdomain/zero/1.0/ . 276 | 277 | """ 278 | 279 | from __future__ import print_function 280 | try: 281 | import configparser 282 | except ImportError: 283 | import ConfigParser as configparser 284 | import errno 285 | import json 286 | import os 287 | import re 288 | import subprocess 289 | import sys 290 | 291 | 292 | class VersioneerConfig: 293 | """Container for Versioneer configuration parameters.""" 294 | 295 | 296 | def get_root(): 297 | """Get the project root directory. 298 | 299 | We require that all commands are run from the project root, i.e. the 300 | directory that contains setup.py, setup.cfg, and versioneer.py . 301 | """ 302 | root = os.path.realpath(os.path.abspath(os.getcwd())) 303 | setup_py = os.path.join(root, "setup.py") 304 | versioneer_py = os.path.join(root, "versioneer.py") 305 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 306 | # allow 'python path/to/setup.py COMMAND' 307 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 308 | setup_py = os.path.join(root, "setup.py") 309 | versioneer_py = os.path.join(root, "versioneer.py") 310 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 311 | err = ("Versioneer was unable to run the project root directory. " 312 | "Versioneer requires setup.py to be executed from " 313 | "its immediate directory (like 'python setup.py COMMAND'), " 314 | "or in a way that lets it use sys.argv[0] to find the root " 315 | "(like 'python path/to/setup.py COMMAND').") 316 | raise VersioneerBadRootError(err) 317 | try: 318 | # Certain runtime workflows (setup.py install/develop in a setuptools 319 | # tree) execute all dependencies in a single python process, so 320 | # "versioneer" may be imported multiple times, and python's shared 321 | # module-import table will cache the first one. So we can't use 322 | # os.path.dirname(__file__), as that will find whichever 323 | # versioneer.py was first imported, even in later projects. 324 | me = os.path.realpath(os.path.abspath(__file__)) 325 | me_dir = os.path.normcase(os.path.splitext(me)[0]) 326 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 327 | if me_dir != vsr_dir: 328 | print("Warning: build in %s is using versioneer.py from %s" 329 | % (os.path.dirname(me), versioneer_py)) 330 | except NameError: 331 | pass 332 | return root 333 | 334 | 335 | def get_config_from_root(root): 336 | """Read the project setup.cfg file to determine Versioneer config.""" 337 | # This might raise EnvironmentError (if setup.cfg is missing), or 338 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 339 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 340 | # the top of versioneer.py for instructions on writing your setup.cfg . 341 | setup_cfg = os.path.join(root, "setup.cfg") 342 | parser = configparser.ConfigParser() 343 | parser.read(setup_cfg) 344 | VCS = parser.get("versioneer", "VCS") # mandatory 345 | 346 | def get(parser, name): 347 | if parser.has_option("versioneer", name): 348 | return parser.get("versioneer", name) 349 | return None 350 | cfg = VersioneerConfig() 351 | cfg.VCS = VCS 352 | cfg.style = get(parser, "style") or "" 353 | cfg.versionfile_source = get(parser, "versionfile_source") 354 | cfg.versionfile_build = get(parser, "versionfile_build") 355 | cfg.tag_prefix = get(parser, "tag_prefix") 356 | if cfg.tag_prefix in ("''", '""'): 357 | cfg.tag_prefix = "" 358 | cfg.parentdir_prefix = get(parser, "parentdir_prefix") 359 | cfg.verbose = get(parser, "verbose") 360 | return cfg 361 | 362 | 363 | class NotThisMethod(Exception): 364 | """Exception raised if a method is not valid for the current scenario.""" 365 | 366 | 367 | # these dictionaries contain VCS-specific tools 368 | LONG_VERSION_PY = {} 369 | HANDLERS = {} 370 | 371 | 372 | def register_vcs_handler(vcs, method): # decorator 373 | """Decorator to mark a method as the handler for a particular VCS.""" 374 | def decorate(f): 375 | """Store f in HANDLERS[vcs][method].""" 376 | if vcs not in HANDLERS: 377 | HANDLERS[vcs] = {} 378 | HANDLERS[vcs][method] = f 379 | return f 380 | return decorate 381 | 382 | 383 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 384 | env=None): 385 | """Call the given command(s).""" 386 | assert isinstance(commands, list) 387 | p = None 388 | for c in commands: 389 | try: 390 | dispcmd = str([c] + args) 391 | # remember shell=False, so use git.cmd on windows, not just git 392 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 393 | stdout=subprocess.PIPE, 394 | stderr=(subprocess.PIPE if hide_stderr 395 | else None)) 396 | break 397 | except EnvironmentError: 398 | e = sys.exc_info()[1] 399 | if e.errno == errno.ENOENT: 400 | continue 401 | if verbose: 402 | print("unable to run %s" % dispcmd) 403 | print(e) 404 | return None, None 405 | else: 406 | if verbose: 407 | print("unable to find command, tried %s" % (commands,)) 408 | return None, None 409 | stdout = p.communicate()[0].strip() 410 | if sys.version_info[0] >= 3: 411 | stdout = stdout.decode() 412 | if p.returncode != 0: 413 | if verbose: 414 | print("unable to run %s (error)" % dispcmd) 415 | print("stdout was %s" % stdout) 416 | return None, p.returncode 417 | return stdout, p.returncode 418 | 419 | 420 | LONG_VERSION_PY['git'] = ''' 421 | # This file helps to compute a version number in source trees obtained from 422 | # git-archive tarball (such as those provided by githubs download-from-tag 423 | # feature). Distribution tarballs (built by setup.py sdist) and build 424 | # directories (produced by setup.py build) will contain a much shorter file 425 | # that just contains the computed version number. 426 | 427 | # This file is released into the public domain. Generated by 428 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 429 | 430 | """Git implementation of _version.py.""" 431 | 432 | import errno 433 | import os 434 | import re 435 | import subprocess 436 | import sys 437 | 438 | 439 | def get_keywords(): 440 | """Get the keywords needed to look up the version information.""" 441 | # these strings will be replaced by git during git-archive. 442 | # setup.py/versioneer.py will grep for the variable names, so they must 443 | # each be defined on a line of their own. _version.py will just call 444 | # get_keywords(). 445 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 446 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 447 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 448 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 449 | return keywords 450 | 451 | 452 | class VersioneerConfig: 453 | """Container for Versioneer configuration parameters.""" 454 | 455 | 456 | def get_config(): 457 | """Create, populate and return the VersioneerConfig() object.""" 458 | # these strings are filled in when 'setup.py versioneer' creates 459 | # _version.py 460 | cfg = VersioneerConfig() 461 | cfg.VCS = "git" 462 | cfg.style = "%(STYLE)s" 463 | cfg.tag_prefix = "%(TAG_PREFIX)s" 464 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 465 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 466 | cfg.verbose = False 467 | return cfg 468 | 469 | 470 | class NotThisMethod(Exception): 471 | """Exception raised if a method is not valid for the current scenario.""" 472 | 473 | 474 | LONG_VERSION_PY = {} 475 | HANDLERS = {} 476 | 477 | 478 | def register_vcs_handler(vcs, method): # decorator 479 | """Decorator to mark a method as the handler for a particular VCS.""" 480 | def decorate(f): 481 | """Store f in HANDLERS[vcs][method].""" 482 | if vcs not in HANDLERS: 483 | HANDLERS[vcs] = {} 484 | HANDLERS[vcs][method] = f 485 | return f 486 | return decorate 487 | 488 | 489 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 490 | env=None): 491 | """Call the given command(s).""" 492 | assert isinstance(commands, list) 493 | p = None 494 | for c in commands: 495 | try: 496 | dispcmd = str([c] + args) 497 | # remember shell=False, so use git.cmd on windows, not just git 498 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 499 | stdout=subprocess.PIPE, 500 | stderr=(subprocess.PIPE if hide_stderr 501 | else None)) 502 | break 503 | except EnvironmentError: 504 | e = sys.exc_info()[1] 505 | if e.errno == errno.ENOENT: 506 | continue 507 | if verbose: 508 | print("unable to run %%s" %% dispcmd) 509 | print(e) 510 | return None, None 511 | else: 512 | if verbose: 513 | print("unable to find command, tried %%s" %% (commands,)) 514 | return None, None 515 | stdout = p.communicate()[0].strip() 516 | if sys.version_info[0] >= 3: 517 | stdout = stdout.decode() 518 | if p.returncode != 0: 519 | if verbose: 520 | print("unable to run %%s (error)" %% dispcmd) 521 | print("stdout was %%s" %% stdout) 522 | return None, p.returncode 523 | return stdout, p.returncode 524 | 525 | 526 | def versions_from_parentdir(parentdir_prefix, root, verbose): 527 | """Try to determine the version from the parent directory name. 528 | 529 | Source tarballs conventionally unpack into a directory that includes both 530 | the project name and a version string. We will also support searching up 531 | two directory levels for an appropriately named parent directory 532 | """ 533 | rootdirs = [] 534 | 535 | for i in range(3): 536 | dirname = os.path.basename(root) 537 | if dirname.startswith(parentdir_prefix): 538 | return {"version": dirname[len(parentdir_prefix):], 539 | "full-revisionid": None, 540 | "dirty": False, "error": None, "date": None} 541 | else: 542 | rootdirs.append(root) 543 | root = os.path.dirname(root) # up a level 544 | 545 | if verbose: 546 | print("Tried directories %%s but none started with prefix %%s" %% 547 | (str(rootdirs), parentdir_prefix)) 548 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 549 | 550 | 551 | @register_vcs_handler("git", "get_keywords") 552 | def git_get_keywords(versionfile_abs): 553 | """Extract version information from the given file.""" 554 | # the code embedded in _version.py can just fetch the value of these 555 | # keywords. When used from setup.py, we don't want to import _version.py, 556 | # so we do it with a regexp instead. This function is not used from 557 | # _version.py. 558 | keywords = {} 559 | try: 560 | f = open(versionfile_abs, "r") 561 | for line in f.readlines(): 562 | if line.strip().startswith("git_refnames ="): 563 | mo = re.search(r'=\s*"(.*)"', line) 564 | if mo: 565 | keywords["refnames"] = mo.group(1) 566 | if line.strip().startswith("git_full ="): 567 | mo = re.search(r'=\s*"(.*)"', line) 568 | if mo: 569 | keywords["full"] = mo.group(1) 570 | if line.strip().startswith("git_date ="): 571 | mo = re.search(r'=\s*"(.*)"', line) 572 | if mo: 573 | keywords["date"] = mo.group(1) 574 | f.close() 575 | except EnvironmentError: 576 | pass 577 | return keywords 578 | 579 | 580 | @register_vcs_handler("git", "keywords") 581 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 582 | """Get version information from git keywords.""" 583 | if not keywords: 584 | raise NotThisMethod("no keywords at all, weird") 585 | date = keywords.get("date") 586 | if date is not None: 587 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 588 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 589 | # -like" string, which we must then edit to make compliant), because 590 | # it's been around since git-1.5.3, and it's too difficult to 591 | # discover which version we're using, or to work around using an 592 | # older one. 593 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 594 | refnames = keywords["refnames"].strip() 595 | if refnames.startswith("$Format"): 596 | if verbose: 597 | print("keywords are unexpanded, not using") 598 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 599 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 600 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 601 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 602 | TAG = "tag: " 603 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 604 | if not tags: 605 | # Either we're using git < 1.8.3, or there really are no tags. We use 606 | # a heuristic: assume all version tags have a digit. The old git %%d 607 | # expansion behaves like git log --decorate=short and strips out the 608 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 609 | # between branches and tags. By ignoring refnames without digits, we 610 | # filter out many common branch names like "release" and 611 | # "stabilization", as well as "HEAD" and "master". 612 | tags = set([r for r in refs if re.search(r'\d', r)]) 613 | if verbose: 614 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 615 | if verbose: 616 | print("likely tags: %%s" %% ",".join(sorted(tags))) 617 | for ref in sorted(tags): 618 | # sorting will prefer e.g. "2.0" over "2.0rc1" 619 | if ref.startswith(tag_prefix): 620 | r = ref[len(tag_prefix):] 621 | if verbose: 622 | print("picking %%s" %% r) 623 | return {"version": r, 624 | "full-revisionid": keywords["full"].strip(), 625 | "dirty": False, "error": None, 626 | "date": date} 627 | # no suitable tags, so version is "0+unknown", but full hex is still there 628 | if verbose: 629 | print("no suitable tags, using unknown + full revision id") 630 | return {"version": "0+unknown", 631 | "full-revisionid": keywords["full"].strip(), 632 | "dirty": False, "error": "no suitable tags", "date": None} 633 | 634 | 635 | @register_vcs_handler("git", "pieces_from_vcs") 636 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 637 | """Get version from 'git describe' in the root of the source tree. 638 | 639 | This only gets called if the git-archive 'subst' keywords were *not* 640 | expanded, and _version.py hasn't already been rewritten with a short 641 | version string, meaning we're inside a checked out source tree. 642 | """ 643 | GITS = ["git"] 644 | if sys.platform == "win32": 645 | GITS = ["git.cmd", "git.exe"] 646 | 647 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 648 | hide_stderr=True) 649 | if rc != 0: 650 | if verbose: 651 | print("Directory %%s not under git control" %% root) 652 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 653 | 654 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 655 | # if there isn't one, this yields HEX[-dirty] (no NUM) 656 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 657 | "--always", "--long", 658 | "--match", "%%s*" %% tag_prefix], 659 | cwd=root) 660 | # --long was added in git-1.5.5 661 | if describe_out is None: 662 | raise NotThisMethod("'git describe' failed") 663 | describe_out = describe_out.strip() 664 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 665 | if full_out is None: 666 | raise NotThisMethod("'git rev-parse' failed") 667 | full_out = full_out.strip() 668 | 669 | pieces = {} 670 | pieces["long"] = full_out 671 | pieces["short"] = full_out[:7] # maybe improved later 672 | pieces["error"] = None 673 | 674 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 675 | # TAG might have hyphens. 676 | git_describe = describe_out 677 | 678 | # look for -dirty suffix 679 | dirty = git_describe.endswith("-dirty") 680 | pieces["dirty"] = dirty 681 | if dirty: 682 | git_describe = git_describe[:git_describe.rindex("-dirty")] 683 | 684 | # now we have TAG-NUM-gHEX or HEX 685 | 686 | if "-" in git_describe: 687 | # TAG-NUM-gHEX 688 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 689 | if not mo: 690 | # unparseable. Maybe git-describe is misbehaving? 691 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 692 | %% describe_out) 693 | return pieces 694 | 695 | # tag 696 | full_tag = mo.group(1) 697 | if not full_tag.startswith(tag_prefix): 698 | if verbose: 699 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 700 | print(fmt %% (full_tag, tag_prefix)) 701 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 702 | %% (full_tag, tag_prefix)) 703 | return pieces 704 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 705 | 706 | # distance: number of commits since tag 707 | pieces["distance"] = int(mo.group(2)) 708 | 709 | # commit: short hex revision ID 710 | pieces["short"] = mo.group(3) 711 | 712 | else: 713 | # HEX: no tags 714 | pieces["closest-tag"] = None 715 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 716 | cwd=root) 717 | pieces["distance"] = int(count_out) # total number of commits 718 | 719 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 720 | date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], 721 | cwd=root)[0].strip() 722 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 723 | 724 | return pieces 725 | 726 | 727 | def plus_or_dot(pieces): 728 | """Return a + if we don't already have one, else return a .""" 729 | if "+" in pieces.get("closest-tag", ""): 730 | return "." 731 | return "+" 732 | 733 | 734 | def render_pep440(pieces): 735 | """Build up version string, with post-release "local version identifier". 736 | 737 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 738 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 739 | 740 | Exceptions: 741 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 742 | """ 743 | if pieces["closest-tag"]: 744 | rendered = pieces["closest-tag"] 745 | if pieces["distance"] or pieces["dirty"]: 746 | rendered += plus_or_dot(pieces) 747 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 748 | if pieces["dirty"]: 749 | rendered += ".dirty" 750 | else: 751 | # exception #1 752 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 753 | pieces["short"]) 754 | if pieces["dirty"]: 755 | rendered += ".dirty" 756 | return rendered 757 | 758 | 759 | def render_pep440_pre(pieces): 760 | """TAG[.post.devDISTANCE] -- No -dirty. 761 | 762 | Exceptions: 763 | 1: no tags. 0.post.devDISTANCE 764 | """ 765 | if pieces["closest-tag"]: 766 | rendered = pieces["closest-tag"] 767 | if pieces["distance"]: 768 | rendered += ".post.dev%%d" %% pieces["distance"] 769 | else: 770 | # exception #1 771 | rendered = "0.post.dev%%d" %% pieces["distance"] 772 | return rendered 773 | 774 | 775 | def render_pep440_post(pieces): 776 | """TAG[.postDISTANCE[.dev0]+gHEX] . 777 | 778 | The ".dev0" means dirty. Note that .dev0 sorts backwards 779 | (a dirty tree will appear "older" than the corresponding clean one), 780 | but you shouldn't be releasing software with -dirty anyways. 781 | 782 | Exceptions: 783 | 1: no tags. 0.postDISTANCE[.dev0] 784 | """ 785 | if pieces["closest-tag"]: 786 | rendered = pieces["closest-tag"] 787 | if pieces["distance"] or pieces["dirty"]: 788 | rendered += ".post%%d" %% pieces["distance"] 789 | if pieces["dirty"]: 790 | rendered += ".dev0" 791 | rendered += plus_or_dot(pieces) 792 | rendered += "g%%s" %% pieces["short"] 793 | else: 794 | # exception #1 795 | rendered = "0.post%%d" %% pieces["distance"] 796 | if pieces["dirty"]: 797 | rendered += ".dev0" 798 | rendered += "+g%%s" %% pieces["short"] 799 | return rendered 800 | 801 | 802 | def render_pep440_old(pieces): 803 | """TAG[.postDISTANCE[.dev0]] . 804 | 805 | The ".dev0" means dirty. 806 | 807 | Eexceptions: 808 | 1: no tags. 0.postDISTANCE[.dev0] 809 | """ 810 | if pieces["closest-tag"]: 811 | rendered = pieces["closest-tag"] 812 | if pieces["distance"] or pieces["dirty"]: 813 | rendered += ".post%%d" %% pieces["distance"] 814 | if pieces["dirty"]: 815 | rendered += ".dev0" 816 | else: 817 | # exception #1 818 | rendered = "0.post%%d" %% pieces["distance"] 819 | if pieces["dirty"]: 820 | rendered += ".dev0" 821 | return rendered 822 | 823 | 824 | def render_git_describe(pieces): 825 | """TAG[-DISTANCE-gHEX][-dirty]. 826 | 827 | Like 'git describe --tags --dirty --always'. 828 | 829 | Exceptions: 830 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 831 | """ 832 | if pieces["closest-tag"]: 833 | rendered = pieces["closest-tag"] 834 | if pieces["distance"]: 835 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 836 | else: 837 | # exception #1 838 | rendered = pieces["short"] 839 | if pieces["dirty"]: 840 | rendered += "-dirty" 841 | return rendered 842 | 843 | 844 | def render_git_describe_long(pieces): 845 | """TAG-DISTANCE-gHEX[-dirty]. 846 | 847 | Like 'git describe --tags --dirty --always -long'. 848 | The distance/hash is unconditional. 849 | 850 | Exceptions: 851 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 852 | """ 853 | if pieces["closest-tag"]: 854 | rendered = pieces["closest-tag"] 855 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 856 | else: 857 | # exception #1 858 | rendered = pieces["short"] 859 | if pieces["dirty"]: 860 | rendered += "-dirty" 861 | return rendered 862 | 863 | 864 | def render(pieces, style): 865 | """Render the given version pieces into the requested style.""" 866 | if pieces["error"]: 867 | return {"version": "unknown", 868 | "full-revisionid": pieces.get("long"), 869 | "dirty": None, 870 | "error": pieces["error"], 871 | "date": None} 872 | 873 | if not style or style == "default": 874 | style = "pep440" # the default 875 | 876 | if style == "pep440": 877 | rendered = render_pep440(pieces) 878 | elif style == "pep440-pre": 879 | rendered = render_pep440_pre(pieces) 880 | elif style == "pep440-post": 881 | rendered = render_pep440_post(pieces) 882 | elif style == "pep440-old": 883 | rendered = render_pep440_old(pieces) 884 | elif style == "git-describe": 885 | rendered = render_git_describe(pieces) 886 | elif style == "git-describe-long": 887 | rendered = render_git_describe_long(pieces) 888 | else: 889 | raise ValueError("unknown style '%%s'" %% style) 890 | 891 | return {"version": rendered, "full-revisionid": pieces["long"], 892 | "dirty": pieces["dirty"], "error": None, 893 | "date": pieces.get("date")} 894 | 895 | 896 | def get_versions(): 897 | """Get version information or return default if unable to do so.""" 898 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 899 | # __file__, we can work backwards from there to the root. Some 900 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 901 | # case we can only use expanded keywords. 902 | 903 | cfg = get_config() 904 | verbose = cfg.verbose 905 | 906 | try: 907 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 908 | verbose) 909 | except NotThisMethod: 910 | pass 911 | 912 | try: 913 | root = os.path.realpath(__file__) 914 | # versionfile_source is the relative path from the top of the source 915 | # tree (where the .git directory might live) to this file. Invert 916 | # this to find the root from __file__. 917 | for i in cfg.versionfile_source.split('/'): 918 | root = os.path.dirname(root) 919 | except NameError: 920 | return {"version": "0+unknown", "full-revisionid": None, 921 | "dirty": None, 922 | "error": "unable to find root of source tree", 923 | "date": None} 924 | 925 | try: 926 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 927 | return render(pieces, cfg.style) 928 | except NotThisMethod: 929 | pass 930 | 931 | try: 932 | if cfg.parentdir_prefix: 933 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 934 | except NotThisMethod: 935 | pass 936 | 937 | return {"version": "0+unknown", "full-revisionid": None, 938 | "dirty": None, 939 | "error": "unable to compute version", "date": None} 940 | ''' 941 | 942 | 943 | @register_vcs_handler("git", "get_keywords") 944 | def git_get_keywords(versionfile_abs): 945 | """Extract version information from the given file.""" 946 | # the code embedded in _version.py can just fetch the value of these 947 | # keywords. When used from setup.py, we don't want to import _version.py, 948 | # so we do it with a regexp instead. This function is not used from 949 | # _version.py. 950 | keywords = {} 951 | try: 952 | f = open(versionfile_abs, "r") 953 | for line in f.readlines(): 954 | if line.strip().startswith("git_refnames ="): 955 | mo = re.search(r'=\s*"(.*)"', line) 956 | if mo: 957 | keywords["refnames"] = mo.group(1) 958 | if line.strip().startswith("git_full ="): 959 | mo = re.search(r'=\s*"(.*)"', line) 960 | if mo: 961 | keywords["full"] = mo.group(1) 962 | if line.strip().startswith("git_date ="): 963 | mo = re.search(r'=\s*"(.*)"', line) 964 | if mo: 965 | keywords["date"] = mo.group(1) 966 | f.close() 967 | except EnvironmentError: 968 | pass 969 | return keywords 970 | 971 | 972 | @register_vcs_handler("git", "keywords") 973 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 974 | """Get version information from git keywords.""" 975 | if not keywords: 976 | raise NotThisMethod("no keywords at all, weird") 977 | date = keywords.get("date") 978 | if date is not None: 979 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 980 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 981 | # -like" string, which we must then edit to make compliant), because 982 | # it's been around since git-1.5.3, and it's too difficult to 983 | # discover which version we're using, or to work around using an 984 | # older one. 985 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 986 | refnames = keywords["refnames"].strip() 987 | if refnames.startswith("$Format"): 988 | if verbose: 989 | print("keywords are unexpanded, not using") 990 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 991 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 992 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 993 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 994 | TAG = "tag: " 995 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 996 | if not tags: 997 | # Either we're using git < 1.8.3, or there really are no tags. We use 998 | # a heuristic: assume all version tags have a digit. The old git %d 999 | # expansion behaves like git log --decorate=short and strips out the 1000 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 1001 | # between branches and tags. By ignoring refnames without digits, we 1002 | # filter out many common branch names like "release" and 1003 | # "stabilization", as well as "HEAD" and "master". 1004 | tags = set([r for r in refs if re.search(r'\d', r)]) 1005 | if verbose: 1006 | print("discarding '%s', no digits" % ",".join(refs - tags)) 1007 | if verbose: 1008 | print("likely tags: %s" % ",".join(sorted(tags))) 1009 | for ref in sorted(tags): 1010 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1011 | if ref.startswith(tag_prefix): 1012 | r = ref[len(tag_prefix):] 1013 | if verbose: 1014 | print("picking %s" % r) 1015 | return {"version": r, 1016 | "full-revisionid": keywords["full"].strip(), 1017 | "dirty": False, "error": None, 1018 | "date": date} 1019 | # no suitable tags, so version is "0+unknown", but full hex is still there 1020 | if verbose: 1021 | print("no suitable tags, using unknown + full revision id") 1022 | return {"version": "0+unknown", 1023 | "full-revisionid": keywords["full"].strip(), 1024 | "dirty": False, "error": "no suitable tags", "date": None} 1025 | 1026 | 1027 | @register_vcs_handler("git", "pieces_from_vcs") 1028 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 1029 | """Get version from 'git describe' in the root of the source tree. 1030 | 1031 | This only gets called if the git-archive 'subst' keywords were *not* 1032 | expanded, and _version.py hasn't already been rewritten with a short 1033 | version string, meaning we're inside a checked out source tree. 1034 | """ 1035 | GITS = ["git"] 1036 | if sys.platform == "win32": 1037 | GITS = ["git.cmd", "git.exe"] 1038 | 1039 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 1040 | hide_stderr=True) 1041 | if rc != 0: 1042 | if verbose: 1043 | print("Directory %s not under git control" % root) 1044 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 1045 | 1046 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1047 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1048 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 1049 | "--always", "--long", 1050 | "--match", "%s*" % tag_prefix], 1051 | cwd=root) 1052 | # --long was added in git-1.5.5 1053 | if describe_out is None: 1054 | raise NotThisMethod("'git describe' failed") 1055 | describe_out = describe_out.strip() 1056 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 1057 | if full_out is None: 1058 | raise NotThisMethod("'git rev-parse' failed") 1059 | full_out = full_out.strip() 1060 | 1061 | pieces = {} 1062 | pieces["long"] = full_out 1063 | pieces["short"] = full_out[:7] # maybe improved later 1064 | pieces["error"] = None 1065 | 1066 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1067 | # TAG might have hyphens. 1068 | git_describe = describe_out 1069 | 1070 | # look for -dirty suffix 1071 | dirty = git_describe.endswith("-dirty") 1072 | pieces["dirty"] = dirty 1073 | if dirty: 1074 | git_describe = git_describe[:git_describe.rindex("-dirty")] 1075 | 1076 | # now we have TAG-NUM-gHEX or HEX 1077 | 1078 | if "-" in git_describe: 1079 | # TAG-NUM-gHEX 1080 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1081 | if not mo: 1082 | # unparseable. Maybe git-describe is misbehaving? 1083 | pieces["error"] = ("unable to parse git-describe output: '%s'" 1084 | % describe_out) 1085 | return pieces 1086 | 1087 | # tag 1088 | full_tag = mo.group(1) 1089 | if not full_tag.startswith(tag_prefix): 1090 | if verbose: 1091 | fmt = "tag '%s' doesn't start with prefix '%s'" 1092 | print(fmt % (full_tag, tag_prefix)) 1093 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1094 | % (full_tag, tag_prefix)) 1095 | return pieces 1096 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 1097 | 1098 | # distance: number of commits since tag 1099 | pieces["distance"] = int(mo.group(2)) 1100 | 1101 | # commit: short hex revision ID 1102 | pieces["short"] = mo.group(3) 1103 | 1104 | else: 1105 | # HEX: no tags 1106 | pieces["closest-tag"] = None 1107 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 1108 | cwd=root) 1109 | pieces["distance"] = int(count_out) # total number of commits 1110 | 1111 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 1112 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 1113 | cwd=root)[0].strip() 1114 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1115 | 1116 | return pieces 1117 | 1118 | 1119 | def do_vcs_install(manifest_in, versionfile_source, ipy): 1120 | """Git-specific installation logic for Versioneer. 1121 | 1122 | For Git, this means creating/changing .gitattributes to mark _version.py 1123 | for export-subst keyword substitution. 1124 | """ 1125 | GITS = ["git"] 1126 | if sys.platform == "win32": 1127 | GITS = ["git.cmd", "git.exe"] 1128 | files = [manifest_in, versionfile_source] 1129 | if ipy: 1130 | files.append(ipy) 1131 | try: 1132 | me = __file__ 1133 | if me.endswith(".pyc") or me.endswith(".pyo"): 1134 | me = os.path.splitext(me)[0] + ".py" 1135 | versioneer_file = os.path.relpath(me) 1136 | except NameError: 1137 | versioneer_file = "versioneer.py" 1138 | files.append(versioneer_file) 1139 | present = False 1140 | try: 1141 | f = open(".gitattributes", "r") 1142 | for line in f.readlines(): 1143 | if line.strip().startswith(versionfile_source): 1144 | if "export-subst" in line.strip().split()[1:]: 1145 | present = True 1146 | f.close() 1147 | except EnvironmentError: 1148 | pass 1149 | if not present: 1150 | f = open(".gitattributes", "a+") 1151 | f.write("%s export-subst\n" % versionfile_source) 1152 | f.close() 1153 | files.append(".gitattributes") 1154 | run_command(GITS, ["add", "--"] + files) 1155 | 1156 | 1157 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1158 | """Try to determine the version from the parent directory name. 1159 | 1160 | Source tarballs conventionally unpack into a directory that includes both 1161 | the project name and a version string. We will also support searching up 1162 | two directory levels for an appropriately named parent directory 1163 | """ 1164 | rootdirs = [] 1165 | 1166 | for i in range(3): 1167 | dirname = os.path.basename(root) 1168 | if dirname.startswith(parentdir_prefix): 1169 | return {"version": dirname[len(parentdir_prefix):], 1170 | "full-revisionid": None, 1171 | "dirty": False, "error": None, "date": None} 1172 | else: 1173 | rootdirs.append(root) 1174 | root = os.path.dirname(root) # up a level 1175 | 1176 | if verbose: 1177 | print("Tried directories %s but none started with prefix %s" % 1178 | (str(rootdirs), parentdir_prefix)) 1179 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1180 | 1181 | 1182 | SHORT_VERSION_PY = """ 1183 | # This file was generated by 'versioneer.py' (0.18) from 1184 | # revision-control system data, or from the parent directory name of an 1185 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1186 | # of this file. 1187 | 1188 | import json 1189 | 1190 | version_json = ''' 1191 | %s 1192 | ''' # END VERSION_JSON 1193 | 1194 | 1195 | def get_versions(): 1196 | return json.loads(version_json) 1197 | """ 1198 | 1199 | 1200 | def versions_from_file(filename): 1201 | """Try to determine the version from _version.py if present.""" 1202 | try: 1203 | with open(filename) as f: 1204 | contents = f.read() 1205 | except EnvironmentError: 1206 | raise NotThisMethod("unable to read _version.py") 1207 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1208 | contents, re.M | re.S) 1209 | if not mo: 1210 | mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1211 | contents, re.M | re.S) 1212 | if not mo: 1213 | raise NotThisMethod("no version_json in _version.py") 1214 | return json.loads(mo.group(1)) 1215 | 1216 | 1217 | def write_to_version_file(filename, versions): 1218 | """Write the given version number to the given _version.py file.""" 1219 | os.unlink(filename) 1220 | contents = json.dumps(versions, sort_keys=True, 1221 | indent=1, separators=(",", ": ")) 1222 | with open(filename, "w") as f: 1223 | f.write(SHORT_VERSION_PY % contents) 1224 | 1225 | print("set %s to '%s'" % (filename, versions["version"])) 1226 | 1227 | 1228 | def plus_or_dot(pieces): 1229 | """Return a + if we don't already have one, else return a .""" 1230 | if "+" in pieces.get("closest-tag", ""): 1231 | return "." 1232 | return "+" 1233 | 1234 | 1235 | def render_pep440(pieces): 1236 | """Build up version string, with post-release "local version identifier". 1237 | 1238 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1239 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1240 | 1241 | Exceptions: 1242 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1243 | """ 1244 | if pieces["closest-tag"]: 1245 | rendered = pieces["closest-tag"] 1246 | if pieces["distance"] or pieces["dirty"]: 1247 | rendered += plus_or_dot(pieces) 1248 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1249 | if pieces["dirty"]: 1250 | rendered += ".dirty" 1251 | else: 1252 | # exception #1 1253 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1254 | pieces["short"]) 1255 | if pieces["dirty"]: 1256 | rendered += ".dirty" 1257 | return rendered 1258 | 1259 | 1260 | def render_pep440_pre(pieces): 1261 | """TAG[.post.devDISTANCE] -- No -dirty. 1262 | 1263 | Exceptions: 1264 | 1: no tags. 0.post.devDISTANCE 1265 | """ 1266 | if pieces["closest-tag"]: 1267 | rendered = pieces["closest-tag"] 1268 | if pieces["distance"]: 1269 | rendered += ".post.dev%d" % pieces["distance"] 1270 | else: 1271 | # exception #1 1272 | rendered = "0.post.dev%d" % pieces["distance"] 1273 | return rendered 1274 | 1275 | 1276 | def render_pep440_post(pieces): 1277 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1278 | 1279 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1280 | (a dirty tree will appear "older" than the corresponding clean one), 1281 | but you shouldn't be releasing software with -dirty anyways. 1282 | 1283 | Exceptions: 1284 | 1: no tags. 0.postDISTANCE[.dev0] 1285 | """ 1286 | if pieces["closest-tag"]: 1287 | rendered = pieces["closest-tag"] 1288 | if pieces["distance"] or pieces["dirty"]: 1289 | rendered += ".post%d" % pieces["distance"] 1290 | if pieces["dirty"]: 1291 | rendered += ".dev0" 1292 | rendered += plus_or_dot(pieces) 1293 | rendered += "g%s" % pieces["short"] 1294 | else: 1295 | # exception #1 1296 | rendered = "0.post%d" % pieces["distance"] 1297 | if pieces["dirty"]: 1298 | rendered += ".dev0" 1299 | rendered += "+g%s" % pieces["short"] 1300 | return rendered 1301 | 1302 | 1303 | def render_pep440_old(pieces): 1304 | """TAG[.postDISTANCE[.dev0]] . 1305 | 1306 | The ".dev0" means dirty. 1307 | 1308 | Eexceptions: 1309 | 1: no tags. 0.postDISTANCE[.dev0] 1310 | """ 1311 | if pieces["closest-tag"]: 1312 | rendered = pieces["closest-tag"] 1313 | if pieces["distance"] or pieces["dirty"]: 1314 | rendered += ".post%d" % pieces["distance"] 1315 | if pieces["dirty"]: 1316 | rendered += ".dev0" 1317 | else: 1318 | # exception #1 1319 | rendered = "0.post%d" % pieces["distance"] 1320 | if pieces["dirty"]: 1321 | rendered += ".dev0" 1322 | return rendered 1323 | 1324 | 1325 | def render_git_describe(pieces): 1326 | """TAG[-DISTANCE-gHEX][-dirty]. 1327 | 1328 | Like 'git describe --tags --dirty --always'. 1329 | 1330 | Exceptions: 1331 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1332 | """ 1333 | if pieces["closest-tag"]: 1334 | rendered = pieces["closest-tag"] 1335 | if pieces["distance"]: 1336 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1337 | else: 1338 | # exception #1 1339 | rendered = pieces["short"] 1340 | if pieces["dirty"]: 1341 | rendered += "-dirty" 1342 | return rendered 1343 | 1344 | 1345 | def render_git_describe_long(pieces): 1346 | """TAG-DISTANCE-gHEX[-dirty]. 1347 | 1348 | Like 'git describe --tags --dirty --always -long'. 1349 | The distance/hash is unconditional. 1350 | 1351 | Exceptions: 1352 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1353 | """ 1354 | if pieces["closest-tag"]: 1355 | rendered = pieces["closest-tag"] 1356 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1357 | else: 1358 | # exception #1 1359 | rendered = pieces["short"] 1360 | if pieces["dirty"]: 1361 | rendered += "-dirty" 1362 | return rendered 1363 | 1364 | 1365 | def render(pieces, style): 1366 | """Render the given version pieces into the requested style.""" 1367 | if pieces["error"]: 1368 | return {"version": "unknown", 1369 | "full-revisionid": pieces.get("long"), 1370 | "dirty": None, 1371 | "error": pieces["error"], 1372 | "date": None} 1373 | 1374 | if not style or style == "default": 1375 | style = "pep440" # the default 1376 | 1377 | if style == "pep440": 1378 | rendered = render_pep440(pieces) 1379 | elif style == "pep440-pre": 1380 | rendered = render_pep440_pre(pieces) 1381 | elif style == "pep440-post": 1382 | rendered = render_pep440_post(pieces) 1383 | elif style == "pep440-old": 1384 | rendered = render_pep440_old(pieces) 1385 | elif style == "git-describe": 1386 | rendered = render_git_describe(pieces) 1387 | elif style == "git-describe-long": 1388 | rendered = render_git_describe_long(pieces) 1389 | else: 1390 | raise ValueError("unknown style '%s'" % style) 1391 | 1392 | return {"version": rendered, "full-revisionid": pieces["long"], 1393 | "dirty": pieces["dirty"], "error": None, 1394 | "date": pieces.get("date")} 1395 | 1396 | 1397 | class VersioneerBadRootError(Exception): 1398 | """The project root directory is unknown or missing key files.""" 1399 | 1400 | 1401 | def get_versions(verbose=False): 1402 | """Get the project version from whatever source is available. 1403 | 1404 | Returns dict with two keys: 'version' and 'full'. 1405 | """ 1406 | if "versioneer" in sys.modules: 1407 | # see the discussion in cmdclass.py:get_cmdclass() 1408 | del sys.modules["versioneer"] 1409 | 1410 | root = get_root() 1411 | cfg = get_config_from_root(root) 1412 | 1413 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1414 | handlers = HANDLERS.get(cfg.VCS) 1415 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1416 | verbose = verbose or cfg.verbose 1417 | assert cfg.versionfile_source is not None, \ 1418 | "please set versioneer.versionfile_source" 1419 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1420 | 1421 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1422 | 1423 | # extract version from first of: _version.py, VCS command (e.g. 'git 1424 | # describe'), parentdir. This is meant to work for developers using a 1425 | # source checkout, for users of a tarball created by 'setup.py sdist', 1426 | # and for users of a tarball/zipball created by 'git archive' or github's 1427 | # download-from-tag feature or the equivalent in other VCSes. 1428 | 1429 | get_keywords_f = handlers.get("get_keywords") 1430 | from_keywords_f = handlers.get("keywords") 1431 | if get_keywords_f and from_keywords_f: 1432 | try: 1433 | keywords = get_keywords_f(versionfile_abs) 1434 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1435 | if verbose: 1436 | print("got version from expanded keyword %s" % ver) 1437 | return ver 1438 | except NotThisMethod: 1439 | pass 1440 | 1441 | try: 1442 | ver = versions_from_file(versionfile_abs) 1443 | if verbose: 1444 | print("got version from file %s %s" % (versionfile_abs, ver)) 1445 | return ver 1446 | except NotThisMethod: 1447 | pass 1448 | 1449 | from_vcs_f = handlers.get("pieces_from_vcs") 1450 | if from_vcs_f: 1451 | try: 1452 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1453 | ver = render(pieces, cfg.style) 1454 | if verbose: 1455 | print("got version from VCS %s" % ver) 1456 | return ver 1457 | except NotThisMethod: 1458 | pass 1459 | 1460 | try: 1461 | if cfg.parentdir_prefix: 1462 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1463 | if verbose: 1464 | print("got version from parentdir %s" % ver) 1465 | return ver 1466 | except NotThisMethod: 1467 | pass 1468 | 1469 | if verbose: 1470 | print("unable to compute version") 1471 | 1472 | return {"version": "0+unknown", "full-revisionid": None, 1473 | "dirty": None, "error": "unable to compute version", 1474 | "date": None} 1475 | 1476 | 1477 | def get_version(): 1478 | """Get the short version string for this project.""" 1479 | return get_versions()["version"] 1480 | 1481 | 1482 | def get_cmdclass(): 1483 | """Get the custom setuptools/distutils subclasses used by Versioneer.""" 1484 | if "versioneer" in sys.modules: 1485 | del sys.modules["versioneer"] 1486 | # this fixes the "python setup.py develop" case (also 'install' and 1487 | # 'easy_install .'), in which subdependencies of the main project are 1488 | # built (using setup.py bdist_egg) in the same python process. Assume 1489 | # a main project A and a dependency B, which use different versions 1490 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1491 | # sys.modules by the time B's setup.py is executed, causing B to run 1492 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1493 | # sandbox that restores sys.modules to it's pre-build state, so the 1494 | # parent is protected against the child's "import versioneer". By 1495 | # removing ourselves from sys.modules here, before the child build 1496 | # happens, we protect the child from the parent's versioneer too. 1497 | # Also see https://github.com/warner/python-versioneer/issues/52 1498 | 1499 | cmds = {} 1500 | 1501 | # we add "version" to both distutils and setuptools 1502 | from distutils.core import Command 1503 | 1504 | class cmd_version(Command): 1505 | description = "report generated version string" 1506 | user_options = [] 1507 | boolean_options = [] 1508 | 1509 | def initialize_options(self): 1510 | pass 1511 | 1512 | def finalize_options(self): 1513 | pass 1514 | 1515 | def run(self): 1516 | vers = get_versions(verbose=True) 1517 | print("Version: %s" % vers["version"]) 1518 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1519 | print(" dirty: %s" % vers.get("dirty")) 1520 | print(" date: %s" % vers.get("date")) 1521 | if vers["error"]: 1522 | print(" error: %s" % vers["error"]) 1523 | cmds["version"] = cmd_version 1524 | 1525 | # we override "build_py" in both distutils and setuptools 1526 | # 1527 | # most invocation pathways end up running build_py: 1528 | # distutils/build -> build_py 1529 | # distutils/install -> distutils/build ->.. 1530 | # setuptools/bdist_wheel -> distutils/install ->.. 1531 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1532 | # setuptools/install -> bdist_egg ->.. 1533 | # setuptools/develop -> ? 1534 | # pip install: 1535 | # copies source tree to a tempdir before running egg_info/etc 1536 | # if .git isn't copied too, 'git describe' will fail 1537 | # then does setup.py bdist_wheel, or sometimes setup.py install 1538 | # setup.py egg_info -> ? 1539 | 1540 | # we override different "build_py" commands for both environments 1541 | if "setuptools" in sys.modules: 1542 | from setuptools.command.build_py import build_py as _build_py 1543 | else: 1544 | from distutils.command.build_py import build_py as _build_py 1545 | 1546 | class cmd_build_py(_build_py): 1547 | def run(self): 1548 | root = get_root() 1549 | cfg = get_config_from_root(root) 1550 | versions = get_versions() 1551 | _build_py.run(self) 1552 | # now locate _version.py in the new build/ directory and replace 1553 | # it with an updated value 1554 | if cfg.versionfile_build: 1555 | target_versionfile = os.path.join(self.build_lib, 1556 | cfg.versionfile_build) 1557 | print("UPDATING %s" % target_versionfile) 1558 | write_to_version_file(target_versionfile, versions) 1559 | cmds["build_py"] = cmd_build_py 1560 | 1561 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1562 | from cx_Freeze.dist import build_exe as _build_exe 1563 | # nczeczulin reports that py2exe won't like the pep440-style string 1564 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1565 | # setup(console=[{ 1566 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1567 | # "product_version": versioneer.get_version(), 1568 | # ... 1569 | 1570 | class cmd_build_exe(_build_exe): 1571 | def run(self): 1572 | root = get_root() 1573 | cfg = get_config_from_root(root) 1574 | versions = get_versions() 1575 | target_versionfile = cfg.versionfile_source 1576 | print("UPDATING %s" % target_versionfile) 1577 | write_to_version_file(target_versionfile, versions) 1578 | 1579 | _build_exe.run(self) 1580 | os.unlink(target_versionfile) 1581 | with open(cfg.versionfile_source, "w") as f: 1582 | LONG = LONG_VERSION_PY[cfg.VCS] 1583 | f.write(LONG % 1584 | {"DOLLAR": "$", 1585 | "STYLE": cfg.style, 1586 | "TAG_PREFIX": cfg.tag_prefix, 1587 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1588 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1589 | }) 1590 | cmds["build_exe"] = cmd_build_exe 1591 | del cmds["build_py"] 1592 | 1593 | if 'py2exe' in sys.modules: # py2exe enabled? 1594 | try: 1595 | from py2exe.distutils_buildexe import py2exe as _py2exe # py3 1596 | except ImportError: 1597 | from py2exe.build_exe import py2exe as _py2exe # py2 1598 | 1599 | class cmd_py2exe(_py2exe): 1600 | def run(self): 1601 | root = get_root() 1602 | cfg = get_config_from_root(root) 1603 | versions = get_versions() 1604 | target_versionfile = cfg.versionfile_source 1605 | print("UPDATING %s" % target_versionfile) 1606 | write_to_version_file(target_versionfile, versions) 1607 | 1608 | _py2exe.run(self) 1609 | os.unlink(target_versionfile) 1610 | with open(cfg.versionfile_source, "w") as f: 1611 | LONG = LONG_VERSION_PY[cfg.VCS] 1612 | f.write(LONG % 1613 | {"DOLLAR": "$", 1614 | "STYLE": cfg.style, 1615 | "TAG_PREFIX": cfg.tag_prefix, 1616 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1617 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1618 | }) 1619 | cmds["py2exe"] = cmd_py2exe 1620 | 1621 | # we override different "sdist" commands for both environments 1622 | if "setuptools" in sys.modules: 1623 | from setuptools.command.sdist import sdist as _sdist 1624 | else: 1625 | from distutils.command.sdist import sdist as _sdist 1626 | 1627 | class cmd_sdist(_sdist): 1628 | def run(self): 1629 | versions = get_versions() 1630 | self._versioneer_generated_versions = versions 1631 | # unless we update this, the command will keep using the old 1632 | # version 1633 | self.distribution.metadata.version = versions["version"] 1634 | return _sdist.run(self) 1635 | 1636 | def make_release_tree(self, base_dir, files): 1637 | root = get_root() 1638 | cfg = get_config_from_root(root) 1639 | _sdist.make_release_tree(self, base_dir, files) 1640 | # now locate _version.py in the new base_dir directory 1641 | # (remembering that it may be a hardlink) and replace it with an 1642 | # updated value 1643 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 1644 | print("UPDATING %s" % target_versionfile) 1645 | write_to_version_file(target_versionfile, 1646 | self._versioneer_generated_versions) 1647 | cmds["sdist"] = cmd_sdist 1648 | 1649 | return cmds 1650 | 1651 | 1652 | CONFIG_ERROR = """ 1653 | setup.cfg is missing the necessary Versioneer configuration. You need 1654 | a section like: 1655 | 1656 | [versioneer] 1657 | VCS = git 1658 | style = pep440 1659 | versionfile_source = src/myproject/_version.py 1660 | versionfile_build = myproject/_version.py 1661 | tag_prefix = 1662 | parentdir_prefix = myproject- 1663 | 1664 | You will also need to edit your setup.py to use the results: 1665 | 1666 | import versioneer 1667 | setup(version=versioneer.get_version(), 1668 | cmdclass=versioneer.get_cmdclass(), ...) 1669 | 1670 | Please read the docstring in ./versioneer.py for configuration instructions, 1671 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 1672 | """ 1673 | 1674 | SAMPLE_CONFIG = """ 1675 | # See the docstring in versioneer.py for instructions. Note that you must 1676 | # re-run 'versioneer.py setup' after changing this section, and commit the 1677 | # resulting files. 1678 | 1679 | [versioneer] 1680 | #VCS = git 1681 | #style = pep440 1682 | #versionfile_source = 1683 | #versionfile_build = 1684 | #tag_prefix = 1685 | #parentdir_prefix = 1686 | 1687 | """ 1688 | 1689 | INIT_PY_SNIPPET = """ 1690 | from ._version import get_versions 1691 | __version__ = get_versions()['version'] 1692 | del get_versions 1693 | """ 1694 | 1695 | 1696 | def do_setup(): 1697 | """Main VCS-independent setup function for installing Versioneer.""" 1698 | root = get_root() 1699 | try: 1700 | cfg = get_config_from_root(root) 1701 | except (EnvironmentError, configparser.NoSectionError, 1702 | configparser.NoOptionError) as e: 1703 | if isinstance(e, (EnvironmentError, configparser.NoSectionError)): 1704 | print("Adding sample versioneer config to setup.cfg", 1705 | file=sys.stderr) 1706 | with open(os.path.join(root, "setup.cfg"), "a") as f: 1707 | f.write(SAMPLE_CONFIG) 1708 | print(CONFIG_ERROR, file=sys.stderr) 1709 | return 1 1710 | 1711 | print(" creating %s" % cfg.versionfile_source) 1712 | with open(cfg.versionfile_source, "w") as f: 1713 | LONG = LONG_VERSION_PY[cfg.VCS] 1714 | f.write(LONG % {"DOLLAR": "$", 1715 | "STYLE": cfg.style, 1716 | "TAG_PREFIX": cfg.tag_prefix, 1717 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1718 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1719 | }) 1720 | 1721 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 1722 | "__init__.py") 1723 | if os.path.exists(ipy): 1724 | try: 1725 | with open(ipy, "r") as f: 1726 | old = f.read() 1727 | except EnvironmentError: 1728 | old = "" 1729 | if INIT_PY_SNIPPET not in old: 1730 | print(" appending to %s" % ipy) 1731 | with open(ipy, "a") as f: 1732 | f.write(INIT_PY_SNIPPET) 1733 | else: 1734 | print(" %s unmodified" % ipy) 1735 | else: 1736 | print(" %s doesn't exist, ok" % ipy) 1737 | ipy = None 1738 | 1739 | # Make sure both the top-level "versioneer.py" and versionfile_source 1740 | # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so 1741 | # they'll be copied into source distributions. Pip won't be able to 1742 | # install the package without this. 1743 | manifest_in = os.path.join(root, "MANIFEST.in") 1744 | simple_includes = set() 1745 | try: 1746 | with open(manifest_in, "r") as f: 1747 | for line in f: 1748 | if line.startswith("include "): 1749 | for include in line.split()[1:]: 1750 | simple_includes.add(include) 1751 | except EnvironmentError: 1752 | pass 1753 | # That doesn't cover everything MANIFEST.in can do 1754 | # (http://docs.python.org/2/distutils/sourcedist.html#commands), so 1755 | # it might give some false negatives. Appending redundant 'include' 1756 | # lines is safe, though. 1757 | if "versioneer.py" not in simple_includes: 1758 | print(" appending 'versioneer.py' to MANIFEST.in") 1759 | with open(manifest_in, "a") as f: 1760 | f.write("include versioneer.py\n") 1761 | else: 1762 | print(" 'versioneer.py' already in MANIFEST.in") 1763 | if cfg.versionfile_source not in simple_includes: 1764 | print(" appending versionfile_source ('%s') to MANIFEST.in" % 1765 | cfg.versionfile_source) 1766 | with open(manifest_in, "a") as f: 1767 | f.write("include %s\n" % cfg.versionfile_source) 1768 | else: 1769 | print(" versionfile_source already in MANIFEST.in") 1770 | 1771 | # Make VCS-specific changes. For git, this means creating/changing 1772 | # .gitattributes to mark _version.py for export-subst keyword 1773 | # substitution. 1774 | do_vcs_install(manifest_in, cfg.versionfile_source, ipy) 1775 | return 0 1776 | 1777 | 1778 | def scan_setup_py(): 1779 | """Validate the contents of setup.py against Versioneer's expectations.""" 1780 | found = set() 1781 | setters = False 1782 | errors = 0 1783 | with open("setup.py", "r") as f: 1784 | for line in f.readlines(): 1785 | if "import versioneer" in line: 1786 | found.add("import") 1787 | if "versioneer.get_cmdclass()" in line: 1788 | found.add("cmdclass") 1789 | if "versioneer.get_version()" in line: 1790 | found.add("get_version") 1791 | if "versioneer.VCS" in line: 1792 | setters = True 1793 | if "versioneer.versionfile_source" in line: 1794 | setters = True 1795 | if len(found) != 3: 1796 | print("") 1797 | print("Your setup.py appears to be missing some important items") 1798 | print("(but I might be wrong). Please make sure it has something") 1799 | print("roughly like the following:") 1800 | print("") 1801 | print(" import versioneer") 1802 | print(" setup( version=versioneer.get_version(),") 1803 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 1804 | print("") 1805 | errors += 1 1806 | if setters: 1807 | print("You should remove lines like 'versioneer.VCS = ' and") 1808 | print("'versioneer.versionfile_source = ' . This configuration") 1809 | print("now lives in setup.cfg, and should be removed from setup.py") 1810 | print("") 1811 | errors += 1 1812 | return errors 1813 | 1814 | 1815 | if __name__ == "__main__": 1816 | cmd = sys.argv[1] 1817 | if cmd == "setup": 1818 | errors = do_setup() 1819 | errors += scan_setup_py() 1820 | if errors: 1821 | sys.exit(1) 1822 | --------------------------------------------------------------------------------