├── .bumpfile ├── emojificate ├── templatetags │ ├── __init__.py │ └── emojificate.py ├── defaults.py ├── __init__.py ├── __main__.py ├── filter.py └── _version.py ├── tests ├── requirements.txt ├── conftest.py ├── test_nochange.py ├── test_graphemes.py ├── test_templatetags.py └── test_parsing.py ├── .gitattributes ├── .gitignore ├── MANIFEST.in ├── setup.cfg ├── .github └── workflows │ ├── bumpfile.yml │ ├── pytest.yml │ └── python-publish.yml ├── setup.py ├── LICENSE ├── README.rst └── versioneer.py /.bumpfile: -------------------------------------------------------------------------------- 1 | 2025-12-02T00:12:48 2 | -------------------------------------------------------------------------------- /emojificate/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-tldr 2 | django -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "pytester" 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | emojificate/_version.py export-subst 2 | -------------------------------------------------------------------------------- /emojificate/defaults.py: -------------------------------------------------------------------------------- 1 | # Default settings 2 | 3 | DEFAULT_FILETYPE = "png" 4 | DEFAULT_CSS_CLASS = "emojificate" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | *~ 4 | .*.sw[op] 5 | *.egg-info 6 | *.egg 7 | .eggs 8 | .tox 9 | local 10 | dist 11 | build 12 | _build 13 | distribute-* 14 | -------------------------------------------------------------------------------- /emojificate/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import get_versions 2 | 3 | __version__ = get_versions()["version"] 4 | del get_versions 5 | 6 | from . import _version 7 | __version__ = _version.get_versions()['version'] 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | include LICENSE 3 | recursive-include tests *.py 4 | recursive-include docs Makefile 5 | recursive-include docs *.py 6 | recursive-include docs *.bat 7 | recursive-include docs *.rstinclude versioneer.py 8 | include emojificate/_version.py 9 | include versioneer.py 10 | -------------------------------------------------------------------------------- /tests/test_nochange.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from emojificate.filter import emojificate 3 | 4 | TEST_NOCHANGE = [ 5 | "☆*:.。. (づ ◕‿◕ )づ .。.:*☆", 6 | "This is a test of the emojification system!", 7 | ] 8 | 9 | 10 | def test_nochange(): 11 | for phrase in TEST_NOCHANGE: 12 | parsed = emojificate(phrase) 13 | assert phrase == parsed 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | # See the docstring in versioneer.py for instructions. Note that you must 5 | # re-run 'versioneer.py setup' after changing this section, and commit the 6 | # resulting files. 7 | 8 | [versioneer] 9 | VCS = git 10 | style = pep440-pre 11 | versionfile_source = emojificate/_version.py 12 | versionfile_build = emojificate/_version.py 13 | tag_prefix = 14 | parentdir_prefix = emojificate- 15 | 16 | -------------------------------------------------------------------------------- /tests/test_graphemes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from emojificate.filter import emojificate 3 | 4 | 5 | def valid(emoji, title, fuzzy=False): 6 | parsed = emojificate(emoji) 7 | 8 | assert emoji in parsed 9 | assert 'alt="{}'.format(emoji) in parsed 10 | 11 | assert title in parsed 12 | if not fuzzy: 13 | assert 'aria-label="Emoji: {}'.format(title) in parsed 14 | 15 | 16 | def test_flag(): 17 | valid("🇦🇺", "Australia", fuzzy=True) 18 | 19 | 20 | def test_pride(): 21 | valid("🏳️‍🌈", "Rainbow Flag") 22 | 23 | 24 | def test_farmer(): 25 | valid("👩🏼‍🌾", "Woman Farmer Medium-Light Skin Tone") 26 | -------------------------------------------------------------------------------- /.github/workflows/bumpfile.yml: -------------------------------------------------------------------------------- 1 | name: bumpfile 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 2 * *' 6 | 7 | # manual 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: get time 16 | run: echo "NOW=$(date +'%Y-%m-%dT%H:%M:%S')" >> $GITHUB_ENV 17 | - name: write time 18 | run: echo $NOW > .bumpfile 19 | - name: save time 20 | run: | 21 | git diff 22 | git config --global user.email "bumpfile-bot@example.com" 23 | git config --global user.name "BumpFile-bot" 24 | git add -A 25 | git commit -m "🤖 Bumped bumpfile" || exit 0 26 | git push -------------------------------------------------------------------------------- /emojificate/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import click 3 | 4 | from .filter import emojificate 5 | from .defaults import DEFAULT_CSS_CLASS, DEFAULT_FILETYPE 6 | 7 | 8 | @click.command() 9 | @click.argument("input_string") 10 | @click.option( 11 | "--filetype", help="File type for image (PNG, SVG)", default=DEFAULT_FILETYPE 12 | ) 13 | @click.option( 14 | "--css-class", help="CSS class to use for images", default=DEFAULT_CSS_CLASS 15 | ) 16 | def cli(input_string, filetype, css_class): 17 | """emojificate.py -- turns text with emoji into text with accessible emoji""" 18 | print(emojificate(input_string, filetype=filetype.lower(), css_class=css_class)) 19 | 20 | 21 | if __name__ == "__main__": 22 | cli() 23 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: 4 | push: 5 | branches: [ latest ] 6 | pull_request: 7 | branches: [ latest ] 8 | 9 | schedule: 10 | - cron: '0 0 1 * *' 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: 19 | - "3.7" 20 | - "3.8" 21 | - "3.9" 22 | - "3.10" 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install -r tests/requirements.txt 34 | pip install -e . 35 | - name: Test with pytest 36 | run: | 37 | pytest 38 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | publish: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install setuptools wheel twine build 27 | - name: Build and publish 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 31 | run: | 32 | python -m build 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | import django 2 | import os 3 | 4 | from django.template import Context, Template 5 | from django.conf import settings 6 | 7 | settings.configure( 8 | INSTALLED_APPS=["emojificate"], 9 | TEMPLATES=[ 10 | { 11 | "BACKEND": "django.template.backends.django.DjangoTemplates", 12 | "DIRS": [os.path.join(os.path.dirname(__file__), "templates")], 13 | } 14 | ], 15 | ) 16 | django.setup() 17 | 18 | 19 | TEST_FILTER = """ 20 | {% load emojificate %} 21 | This is some {{ user_content|emojificate }} that has emoji in it. 22 | """ 23 | 24 | TEST_TAG = """ 25 | {% load emojificate %} 26 | {% emojified %} 27 | This is some template content that 💜 emoji as well. 28 | {% endemojified %} 29 | """ 30 | 31 | 32 | def valid(data): 33 | assert "emoji" in data 34 | assert "alt" in data 35 | 36 | 37 | def test_filter(): 38 | context = Context({"user_content": "✨"}) 39 | 40 | parsed = Template(TEST_FILTER).render(context) 41 | valid(parsed) 42 | 43 | 44 | def test_tag(): 45 | parsed = Template(TEST_TAG).render(Context()) 46 | valid(parsed) 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from setuptools import setup, find_packages 3 | from os import path 4 | import versioneer 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | # Get the long description from the relevant file 9 | with open(path.join(here, "README.rst"), encoding="utf-8") as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name="emojificate", 14 | version=versioneer.get_version(), 15 | cmdclass=versioneer.get_cmdclass(), 16 | description="Convert emoji in HTML to fallback images, alt text, title text, and aria labels.", 17 | long_description=long_description, 18 | url="https://github.com/glasnt/emojificate", 19 | author="Katie McLaughlin", 20 | author_email="katie@glasnt.com", 21 | license="New BSD", 22 | install_requires=["emoji", "grapheme", "requests", "click"], 23 | classifiers=[ 24 | "Development Status :: 3 - Alpha", 25 | "Environment :: Web Environment", 26 | "Intended Audience :: Developers", 27 | "License :: OSI Approved :: BSD License", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Topic :: Text Processing :: Filters", 34 | "Topic :: Utilities", 35 | ], 36 | keywords="emoji accessibility a11y", 37 | packages=find_packages(exclude=["docs", "test"]), 38 | ) 39 | -------------------------------------------------------------------------------- /emojificate/templatetags/emojificate.py: -------------------------------------------------------------------------------- 1 | from django.template import Library, Node 2 | from django.utils.safestring import mark_safe 3 | from django.utils.html import conditional_escape 4 | from django.conf import settings 5 | 6 | from ..filter import emojificate 7 | from ..defaults import DEFAULT_CSS_CLASS, DEFAULT_FILETYPE 8 | 9 | register = Library() 10 | 11 | filetype = DEFAULT_FILETYPE 12 | if hasattr(settings, "EMOJIFICATE_FILETYPE"): 13 | filetype = settings.EMOJIFICATE_FILETYPE.lower() 14 | 15 | css_class = DEFAULT_CSS_CLASS 16 | if hasattr(settings, "EMOJIFICATE_CSS_CLASS"): 17 | css_class = settings.EMOJIFICATE_CSS_CLASS 18 | 19 | 20 | @register.filter("emojificate", needs_autoescape=True) 21 | def emojificate_filter(content, autoescape=True): 22 | """Convert any emoji in a string into accessible content.""" 23 | if autoescape: 24 | esc = conditional_escape 25 | else: 26 | esc = lambda x: x 27 | return mark_safe(emojificate(esc(content), filetype=filetype, css_class=css_class)) 28 | 29 | 30 | @register.tag("emojified") 31 | def do_emojified(parser, token): 32 | nodelist = parser.parse(("endemojified",)) 33 | parser.delete_first_token() 34 | return EmojifiedNode(nodelist) 35 | 36 | 37 | class EmojifiedNode(Node): 38 | def __init__(self, nodelist): 39 | self.nodelist = nodelist 40 | 41 | def render(self, context): 42 | output = self.nodelist.render(context) 43 | return emojificate(output, filetype=filetype, css_class=css_class) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Katie McLaughlin 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 octohatrack 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 | -------------------------------------------------------------------------------- /tests/test_parsing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys, os 3 | 4 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 5 | from emojificate.filter import emojificate 6 | 7 | # A list of new emoji introduced in the unicodedata set for that version of Python 8 | # Emoji introduced in later versions won't be available in earlier ones. 9 | PYTHON_37 = {"alt": "🥰", "title": "Smiling Face With Smiling Eyes And Three Hearts"} 10 | PYTHON_38 = {"alt": "🥱", "title": "Yawning Face"} # Unicode 12.0 11 | PYTHON_39 = {"alt": "🥸", "title": "Disguised Face"} # Unicode 13.0 12 | PYTHON_310 = {"alt": "🥲", "title": "Smiling Face With Tear"} # Unicode 13.0, also. 13 | 14 | 15 | def valid(data): 16 | alt = data["alt"] 17 | title = data["title"] 18 | 19 | for filetype in ["png", "svg"]: 20 | parsed = emojificate(alt, filetype=filetype) 21 | assert alt in parsed 22 | assert 'alt="{}"'.format(alt) in parsed 23 | 24 | assert title in parsed 25 | assert 'aria-label="Emoji: {}"'.format(title) in parsed 26 | 27 | 28 | @pytest.mark.skipif(sys.version_info.minor < 7, reason="requires Python 3.7 or higher") 29 | def test_python_37_char(): 30 | valid(PYTHON_37) 31 | 32 | 33 | @pytest.mark.skipif(sys.version_info.minor < 8, reason="requires Python 3.8 or higher") 34 | def test_python_38_char(): 35 | valid(PYTHON_38) 36 | 37 | 38 | @pytest.mark.skipif(sys.version_info.minor < 9, reason="requires Python 3.9 or higher") 39 | def test_python_39_char(): 40 | valid(PYTHON_39) 41 | 42 | 43 | @pytest.mark.skipif(sys.version_info.minor < 10, reason="requires Python 3.10 or higher") 44 | def test_python_310_char(): 45 | valid(PYTHON_310) 46 | -------------------------------------------------------------------------------- /emojificate/filter.py: -------------------------------------------------------------------------------- 1 | import unicodedata 2 | from grapheme import graphemes 3 | import emoji 4 | import requests 5 | 6 | 7 | __all__ = ["emojificate"] 8 | 9 | TWITTER_CDN = "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2" 10 | TWITTER_TYPE = {"png": "/72x72/{codepoint}.png", "svg": "/svg/{codepoint}.svg"} 11 | 12 | 13 | def valid_src(src): 14 | """Check to see if a source URL is hosted in the CDN""" 15 | req = requests.head(src) 16 | return req.status_code == 200 17 | 18 | 19 | def valid_category(char): 20 | try: 21 | return unicodedata.category(char) == "So" 22 | except TypeError: 23 | return False 24 | 25 | 26 | def get_best_name(char): 27 | """ 28 | unicode data does not recognise the grapheme, 29 | so try and parse something from emoji instead. 30 | """ 31 | shortcode = emoji.demojize(char, language="alias") 32 | 33 | # Roughly convert shortcode to screenreader-friendly sentence. 34 | return shortcode.replace(":", "").replace("_", " ").replace("selector", "").title() 35 | 36 | 37 | def convert(char, filetype, css_class): 38 | def tag(a, b): 39 | return '%s="%s"' % (a, b) 40 | 41 | def codepoint(codes): 42 | # See https://github.com/twitter/twemoji/issues/419#issuecomment-637360325 43 | if "200d" not in codes: 44 | return "-".join([c for c in codes if c != "fe0f"]) 45 | return "-".join(codes) 46 | 47 | if valid_category(char): 48 | # Is a Char, and a Symbol 49 | name = unicodedata.name(char).title() 50 | else: 51 | if len(char) == 1: 52 | # Is a Char, not a Symbol, we don't care. 53 | return char 54 | else: 55 | # Is probably a grapheme 56 | name = get_best_name(char) 57 | 58 | src = TWITTER_CDN 59 | src += TWITTER_TYPE[filetype] 60 | 61 | src = src.format(codepoint=codepoint(["{cp:x}".format(cp=ord(c)) for c in char])) 62 | 63 | # If twitter doesn't have an image for it, pretend it's not an emoji. 64 | if valid_src(src): 65 | return "".join( 66 | [ 67 | "", 74 | ] 75 | ) 76 | else: 77 | return char 78 | 79 | 80 | def emojificate(string, filetype="png", css_class="emojificate"): 81 | return "".join(convert(ch, filetype, css_class) for ch in graphemes(string)) 82 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | emojificate 2 | =========== 3 | 4 | |status| |release| |date| 5 | 6 | .. |status| image:: https://img.shields.io/github/actions/workflow/status/glasnt/emojificate/pytest.yml?branch=latest&label=pytest&style=flat-square :alt: GitHub Workflow Status 7 | 8 | .. |release| image:: https://img.shields.io/github/v/release/glasnt/emojificate?sort=semver&style=flat-square :alt: GitHub release (latest SemVer) 9 | 10 | .. |date| image:: https://img.shields.io/github/release-date/glasnt/emojificate?style=flat-square :alt: GitHub Release Date 11 | 12 | Emojificate is a Python implementation of a concept of using fallback images, alt text, title text and aria labels to represent emoji in HTML, a more accessible method than browser defaults. 13 | 14 | Installation 15 | ------------ 16 | 17 | emojificate is available on pypi:: 18 | 19 | pip install emojificate 20 | 21 | Usage 22 | ----- 23 | 24 | To convert a string from the command line:: 25 | 26 | $ python3 -m emojificate "I 💜 emoji 😊" 27 | I 28 | 💜 31 | emoji 32 | 😊 35 | 36 | Change the class with ``--css-class`` (default "emojificate"). To get SVG instead of PNG, use ``--filetype svg``. 37 | 38 | 39 | Or, if you've got a Django project, put ``emojificate`` into your ``INSTALLED_APPS``, and then use the following in a template:: 40 | 41 | {% load emojificate %} 42 | This is some {{ user_content|emojificate }} that has emoji in it. 43 | 44 | {% emojified %} 45 | This is some template content that 💜 emoji as well. 46 | {% endemojified %} 47 | 48 | Configure with ``EMOJIFICATE_FILETYPE`` and ``EMOJIFICIATE_CSS_CLASS`` in your ``settings.py``, and add some css to make the emoji not huge. 49 | 50 | Implementation 51 | -------------- 52 | 53 | TL;DR: Take a string, split it into tokens, and if a token is emoji, process it into a nice format. 54 | 55 | As of 0.4.0, string-splitting is now handled by `grapheme `__. 56 | 57 | Given a list of tokens, we can leverage the native `unicodedata `__ to: 58 | 59 | * see if a token is a unicode Symbol (an emoji) 60 | * get the codepoint for the emoji, and 61 | * get the name of the emoji. 62 | 63 | If a token is a grapheme and not a character, there won't be a record of what it is in unicodedata. In that case emojificate defaults to a human-readable version of the shortcode provided by `emoji `__. 64 | 65 | From there, we construct an ```` replacement for the emoji: 66 | 67 | * Use images from `twemoji `__, Twitter's emoji set (if the URL exists) 68 | * Have an ``alt`` parameter containing the original emoji. This allows for copying-pasting. 69 | * Use the name of the emoji in the ``title`` parameter. This allows for hover-tooltips. 70 | * Add an ``aria-label`` for screen-reader accessibility. 71 | 72 | For more information, see `Solve For Emoji `__. 73 | 74 | Implementation in other languages 75 | --------------------------------- 76 | 77 | Ruby 78 | ~~~~~ 79 | 80 | .. code-block:: ruby 81 | 82 | require 'gemoji' 83 | 84 | def cdn 85 | "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/" 86 | end 87 | 88 | def emojificate(string) 89 | string.split("").each do |s| 90 | e = Emoji.find_by_unicode(s) 91 | if e then 92 | u = s.ord.to_s(16) # e.g. 1f431 93 | d = e.description # e.g. "cat face" 94 | img = "\"#{s}\"" 95 | print img 96 | else 97 | print s 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /emojificate/_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. 9 | # Generated by versioneer-0.27 10 | # https://github.com/python-versioneer/python-versioneer 11 | 12 | """Git implementation of _version.py.""" 13 | 14 | import errno 15 | import os 16 | import re 17 | import subprocess 18 | import sys 19 | from typing import Callable, Dict 20 | import functools 21 | 22 | 23 | def get_keywords(): 24 | """Get the keywords needed to look up the version information.""" 25 | # these strings will be replaced by git during git-archive. 26 | # setup.py/versioneer.py will grep for the variable names, so they must 27 | # each be defined on a line of their own. _version.py will just call 28 | # get_keywords(). 29 | git_refnames = " (HEAD -> latest)" 30 | git_full = "cdb018be970130a9d1c8bb8d5dfe48e6ce1ab1e4" 31 | git_date = "2025-12-02 00:12:48 +0000" 32 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 33 | return keywords 34 | 35 | 36 | class VersioneerConfig: 37 | """Container for Versioneer configuration parameters.""" 38 | 39 | 40 | def get_config(): 41 | """Create, populate and return the VersioneerConfig() object.""" 42 | # these strings are filled in when 'setup.py versioneer' creates 43 | # _version.py 44 | cfg = VersioneerConfig() 45 | cfg.VCS = "git" 46 | cfg.style = "pep440" 47 | cfg.tag_prefix = "" 48 | cfg.parentdir_prefix = "emojificate-" 49 | cfg.versionfile_source = "emojificate/_version.py" 50 | cfg.verbose = False 51 | return cfg 52 | 53 | 54 | class NotThisMethod(Exception): 55 | """Exception raised if a method is not valid for the current scenario.""" 56 | 57 | 58 | LONG_VERSION_PY: Dict[str, str] = {} 59 | HANDLERS: Dict[str, Dict[str, Callable]] = {} 60 | 61 | 62 | def register_vcs_handler(vcs, method): # decorator 63 | """Create decorator to mark a method as the handler of a VCS.""" 64 | def decorate(f): 65 | """Store f in HANDLERS[vcs][method].""" 66 | if vcs not in HANDLERS: 67 | HANDLERS[vcs] = {} 68 | HANDLERS[vcs][method] = f 69 | return f 70 | return decorate 71 | 72 | 73 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 74 | env=None): 75 | """Call the given command(s).""" 76 | assert isinstance(commands, list) 77 | process = None 78 | 79 | popen_kwargs = {} 80 | if sys.platform == "win32": 81 | # This hides the console window if pythonw.exe is used 82 | startupinfo = subprocess.STARTUPINFO() 83 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 84 | popen_kwargs["startupinfo"] = startupinfo 85 | 86 | for command in commands: 87 | try: 88 | dispcmd = str([command] + args) 89 | # remember shell=False, so use git.cmd on windows, not just git 90 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 91 | stdout=subprocess.PIPE, 92 | stderr=(subprocess.PIPE if hide_stderr 93 | else None), **popen_kwargs) 94 | break 95 | except OSError: 96 | e = sys.exc_info()[1] 97 | if e.errno == errno.ENOENT: 98 | continue 99 | if verbose: 100 | print("unable to run %s" % dispcmd) 101 | print(e) 102 | return None, None 103 | else: 104 | if verbose: 105 | print("unable to find command, tried %s" % (commands,)) 106 | return None, None 107 | stdout = process.communicate()[0].strip().decode() 108 | if process.returncode != 0: 109 | if verbose: 110 | print("unable to run %s (error)" % dispcmd) 111 | print("stdout was %s" % stdout) 112 | return None, process.returncode 113 | return stdout, process.returncode 114 | 115 | 116 | def versions_from_parentdir(parentdir_prefix, root, verbose): 117 | """Try to determine the version from the parent directory name. 118 | 119 | Source tarballs conventionally unpack into a directory that includes both 120 | the project name and a version string. We will also support searching up 121 | two directory levels for an appropriately named parent directory 122 | """ 123 | rootdirs = [] 124 | 125 | for _ in range(3): 126 | dirname = os.path.basename(root) 127 | if dirname.startswith(parentdir_prefix): 128 | return {"version": dirname[len(parentdir_prefix):], 129 | "full-revisionid": None, 130 | "dirty": False, "error": None, "date": None} 131 | rootdirs.append(root) 132 | root = os.path.dirname(root) # up a level 133 | 134 | if verbose: 135 | print("Tried directories %s but none started with prefix %s" % 136 | (str(rootdirs), parentdir_prefix)) 137 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 138 | 139 | 140 | @register_vcs_handler("git", "get_keywords") 141 | def git_get_keywords(versionfile_abs): 142 | """Extract version information from the given file.""" 143 | # the code embedded in _version.py can just fetch the value of these 144 | # keywords. When used from setup.py, we don't want to import _version.py, 145 | # so we do it with a regexp instead. This function is not used from 146 | # _version.py. 147 | keywords = {} 148 | try: 149 | with open(versionfile_abs, "r") as fobj: 150 | for line in fobj: 151 | if line.strip().startswith("git_refnames ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["refnames"] = mo.group(1) 155 | if line.strip().startswith("git_full ="): 156 | mo = re.search(r'=\s*"(.*)"', line) 157 | if mo: 158 | keywords["full"] = mo.group(1) 159 | if line.strip().startswith("git_date ="): 160 | mo = re.search(r'=\s*"(.*)"', line) 161 | if mo: 162 | keywords["date"] = mo.group(1) 163 | except OSError: 164 | pass 165 | return keywords 166 | 167 | 168 | @register_vcs_handler("git", "keywords") 169 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 170 | """Get version information from git keywords.""" 171 | if "refnames" not in keywords: 172 | raise NotThisMethod("Short version file found") 173 | date = keywords.get("date") 174 | if date is not None: 175 | # Use only the last line. Previous lines may contain GPG signature 176 | # information. 177 | date = date.splitlines()[-1] 178 | 179 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 180 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 181 | # -like" string, which we must then edit to make compliant), because 182 | # it's been around since git-1.5.3, and it's too difficult to 183 | # discover which version we're using, or to work around using an 184 | # older one. 185 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 186 | refnames = keywords["refnames"].strip() 187 | if refnames.startswith("$Format"): 188 | if verbose: 189 | print("keywords are unexpanded, not using") 190 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 191 | refs = {r.strip() for r in refnames.strip("()").split(",")} 192 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 193 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 194 | TAG = "tag: " 195 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 196 | if not tags: 197 | # Either we're using git < 1.8.3, or there really are no tags. We use 198 | # a heuristic: assume all version tags have a digit. The old git %d 199 | # expansion behaves like git log --decorate=short and strips out the 200 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 201 | # between branches and tags. By ignoring refnames without digits, we 202 | # filter out many common branch names like "release" and 203 | # "stabilization", as well as "HEAD" and "master". 204 | tags = {r for r in refs if re.search(r'\d', r)} 205 | if verbose: 206 | print("discarding '%s', no digits" % ",".join(refs - tags)) 207 | if verbose: 208 | print("likely tags: %s" % ",".join(sorted(tags))) 209 | for ref in sorted(tags): 210 | # sorting will prefer e.g. "2.0" over "2.0rc1" 211 | if ref.startswith(tag_prefix): 212 | r = ref[len(tag_prefix):] 213 | # Filter out refs that exactly match prefix or that don't start 214 | # with a number once the prefix is stripped (mostly a concern 215 | # when prefix is '') 216 | if not re.match(r'\d', r): 217 | continue 218 | if verbose: 219 | print("picking %s" % r) 220 | return {"version": r, 221 | "full-revisionid": keywords["full"].strip(), 222 | "dirty": False, "error": None, 223 | "date": date} 224 | # no suitable tags, so version is "0+unknown", but full hex is still there 225 | if verbose: 226 | print("no suitable tags, using unknown + full revision id") 227 | return {"version": "0+unknown", 228 | "full-revisionid": keywords["full"].strip(), 229 | "dirty": False, "error": "no suitable tags", "date": None} 230 | 231 | 232 | @register_vcs_handler("git", "pieces_from_vcs") 233 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 234 | """Get version from 'git describe' in the root of the source tree. 235 | 236 | This only gets called if the git-archive 'subst' keywords were *not* 237 | expanded, and _version.py hasn't already been rewritten with a short 238 | version string, meaning we're inside a checked out source tree. 239 | """ 240 | GITS = ["git"] 241 | if sys.platform == "win32": 242 | GITS = ["git.cmd", "git.exe"] 243 | 244 | # GIT_DIR can interfere with correct operation of Versioneer. 245 | # It may be intended to be passed to the Versioneer-versioned project, 246 | # but that should not change where we get our version from. 247 | env = os.environ.copy() 248 | env.pop("GIT_DIR", None) 249 | runner = functools.partial(runner, env=env) 250 | 251 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 252 | hide_stderr=not verbose) 253 | if rc != 0: 254 | if verbose: 255 | print("Directory %s not under git control" % root) 256 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 257 | 258 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 259 | # if there isn't one, this yields HEX[-dirty] (no NUM) 260 | describe_out, rc = runner(GITS, [ 261 | "describe", "--tags", "--dirty", "--always", "--long", 262 | "--match", f"{tag_prefix}[[:digit:]]*" 263 | ], cwd=root) 264 | # --long was added in git-1.5.5 265 | if describe_out is None: 266 | raise NotThisMethod("'git describe' failed") 267 | describe_out = describe_out.strip() 268 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 269 | if full_out is None: 270 | raise NotThisMethod("'git rev-parse' failed") 271 | full_out = full_out.strip() 272 | 273 | pieces = {} 274 | pieces["long"] = full_out 275 | pieces["short"] = full_out[:7] # maybe improved later 276 | pieces["error"] = None 277 | 278 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 279 | cwd=root) 280 | # --abbrev-ref was added in git-1.6.3 281 | if rc != 0 or branch_name is None: 282 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 283 | branch_name = branch_name.strip() 284 | 285 | if branch_name == "HEAD": 286 | # If we aren't exactly on a branch, pick a branch which represents 287 | # the current commit. If all else fails, we are on a branchless 288 | # commit. 289 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 290 | # --contains was added in git-1.5.4 291 | if rc != 0 or branches is None: 292 | raise NotThisMethod("'git branch --contains' returned error") 293 | branches = branches.split("\n") 294 | 295 | # Remove the first line if we're running detached 296 | if "(" in branches[0]: 297 | branches.pop(0) 298 | 299 | # Strip off the leading "* " from the list of branches. 300 | branches = [branch[2:] for branch in branches] 301 | if "master" in branches: 302 | branch_name = "master" 303 | elif not branches: 304 | branch_name = None 305 | else: 306 | # Pick the first branch that is returned. Good or bad. 307 | branch_name = branches[0] 308 | 309 | pieces["branch"] = branch_name 310 | 311 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 312 | # TAG might have hyphens. 313 | git_describe = describe_out 314 | 315 | # look for -dirty suffix 316 | dirty = git_describe.endswith("-dirty") 317 | pieces["dirty"] = dirty 318 | if dirty: 319 | git_describe = git_describe[:git_describe.rindex("-dirty")] 320 | 321 | # now we have TAG-NUM-gHEX or HEX 322 | 323 | if "-" in git_describe: 324 | # TAG-NUM-gHEX 325 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 326 | if not mo: 327 | # unparsable. Maybe git-describe is misbehaving? 328 | pieces["error"] = ("unable to parse git-describe output: '%s'" 329 | % describe_out) 330 | return pieces 331 | 332 | # tag 333 | full_tag = mo.group(1) 334 | if not full_tag.startswith(tag_prefix): 335 | if verbose: 336 | fmt = "tag '%s' doesn't start with prefix '%s'" 337 | print(fmt % (full_tag, tag_prefix)) 338 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 339 | % (full_tag, tag_prefix)) 340 | return pieces 341 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 342 | 343 | # distance: number of commits since tag 344 | pieces["distance"] = int(mo.group(2)) 345 | 346 | # commit: short hex revision ID 347 | pieces["short"] = mo.group(3) 348 | 349 | else: 350 | # HEX: no tags 351 | pieces["closest-tag"] = None 352 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 353 | pieces["distance"] = len(out.split()) # total number of commits 354 | 355 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 356 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 357 | # Use only the last line. Previous lines may contain GPG signature 358 | # information. 359 | date = date.splitlines()[-1] 360 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 361 | 362 | return pieces 363 | 364 | 365 | def plus_or_dot(pieces): 366 | """Return a + if we don't already have one, else return a .""" 367 | if "+" in pieces.get("closest-tag", ""): 368 | return "." 369 | return "+" 370 | 371 | 372 | def render_pep440(pieces): 373 | """Build up version string, with post-release "local version identifier". 374 | 375 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 376 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 377 | 378 | Exceptions: 379 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 380 | """ 381 | if pieces["closest-tag"]: 382 | rendered = pieces["closest-tag"] 383 | if pieces["distance"] or pieces["dirty"]: 384 | rendered += plus_or_dot(pieces) 385 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 386 | if pieces["dirty"]: 387 | rendered += ".dirty" 388 | else: 389 | # exception #1 390 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 391 | pieces["short"]) 392 | if pieces["dirty"]: 393 | rendered += ".dirty" 394 | return rendered 395 | 396 | 397 | def render_pep440_branch(pieces): 398 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 399 | 400 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 401 | (a feature branch will appear "older" than the master branch). 402 | 403 | Exceptions: 404 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 405 | """ 406 | if pieces["closest-tag"]: 407 | rendered = pieces["closest-tag"] 408 | if pieces["distance"] or pieces["dirty"]: 409 | if pieces["branch"] != "master": 410 | rendered += ".dev0" 411 | rendered += plus_or_dot(pieces) 412 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 413 | if pieces["dirty"]: 414 | rendered += ".dirty" 415 | else: 416 | # exception #1 417 | rendered = "0" 418 | if pieces["branch"] != "master": 419 | rendered += ".dev0" 420 | rendered += "+untagged.%d.g%s" % (pieces["distance"], 421 | pieces["short"]) 422 | if pieces["dirty"]: 423 | rendered += ".dirty" 424 | return rendered 425 | 426 | 427 | def pep440_split_post(ver): 428 | """Split pep440 version string at the post-release segment. 429 | 430 | Returns the release segments before the post-release and the 431 | post-release version number (or -1 if no post-release segment is present). 432 | """ 433 | vc = str.split(ver, ".post") 434 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 435 | 436 | 437 | def render_pep440_pre(pieces): 438 | """TAG[.postN.devDISTANCE] -- No -dirty. 439 | 440 | Exceptions: 441 | 1: no tags. 0.post0.devDISTANCE 442 | """ 443 | if pieces["closest-tag"]: 444 | if pieces["distance"]: 445 | # update the post release segment 446 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 447 | rendered = tag_version 448 | if post_version is not None: 449 | rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 450 | else: 451 | rendered += ".post0.dev%d" % (pieces["distance"]) 452 | else: 453 | # no commits, use the tag as the version 454 | rendered = pieces["closest-tag"] 455 | else: 456 | # exception #1 457 | rendered = "0.post0.dev%d" % pieces["distance"] 458 | return rendered 459 | 460 | 461 | def render_pep440_post(pieces): 462 | """TAG[.postDISTANCE[.dev0]+gHEX] . 463 | 464 | The ".dev0" means dirty. Note that .dev0 sorts backwards 465 | (a dirty tree will appear "older" than the corresponding clean one), 466 | but you shouldn't be releasing software with -dirty anyways. 467 | 468 | Exceptions: 469 | 1: no tags. 0.postDISTANCE[.dev0] 470 | """ 471 | if pieces["closest-tag"]: 472 | rendered = pieces["closest-tag"] 473 | if pieces["distance"] or pieces["dirty"]: 474 | rendered += ".post%d" % pieces["distance"] 475 | if pieces["dirty"]: 476 | rendered += ".dev0" 477 | rendered += plus_or_dot(pieces) 478 | rendered += "g%s" % pieces["short"] 479 | else: 480 | # exception #1 481 | rendered = "0.post%d" % pieces["distance"] 482 | if pieces["dirty"]: 483 | rendered += ".dev0" 484 | rendered += "+g%s" % pieces["short"] 485 | return rendered 486 | 487 | 488 | def render_pep440_post_branch(pieces): 489 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 490 | 491 | The ".dev0" means not master branch. 492 | 493 | Exceptions: 494 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 495 | """ 496 | if pieces["closest-tag"]: 497 | rendered = pieces["closest-tag"] 498 | if pieces["distance"] or pieces["dirty"]: 499 | rendered += ".post%d" % pieces["distance"] 500 | if pieces["branch"] != "master": 501 | rendered += ".dev0" 502 | rendered += plus_or_dot(pieces) 503 | rendered += "g%s" % pieces["short"] 504 | if pieces["dirty"]: 505 | rendered += ".dirty" 506 | else: 507 | # exception #1 508 | rendered = "0.post%d" % pieces["distance"] 509 | if pieces["branch"] != "master": 510 | rendered += ".dev0" 511 | rendered += "+g%s" % pieces["short"] 512 | if pieces["dirty"]: 513 | rendered += ".dirty" 514 | return rendered 515 | 516 | 517 | def render_pep440_old(pieces): 518 | """TAG[.postDISTANCE[.dev0]] . 519 | 520 | The ".dev0" means dirty. 521 | 522 | Exceptions: 523 | 1: no tags. 0.postDISTANCE[.dev0] 524 | """ 525 | if pieces["closest-tag"]: 526 | rendered = pieces["closest-tag"] 527 | if pieces["distance"] or pieces["dirty"]: 528 | rendered += ".post%d" % pieces["distance"] 529 | if pieces["dirty"]: 530 | rendered += ".dev0" 531 | else: 532 | # exception #1 533 | rendered = "0.post%d" % pieces["distance"] 534 | if pieces["dirty"]: 535 | rendered += ".dev0" 536 | return rendered 537 | 538 | 539 | def render_git_describe(pieces): 540 | """TAG[-DISTANCE-gHEX][-dirty]. 541 | 542 | Like 'git describe --tags --dirty --always'. 543 | 544 | Exceptions: 545 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 546 | """ 547 | if pieces["closest-tag"]: 548 | rendered = pieces["closest-tag"] 549 | if pieces["distance"]: 550 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 551 | else: 552 | # exception #1 553 | rendered = pieces["short"] 554 | if pieces["dirty"]: 555 | rendered += "-dirty" 556 | return rendered 557 | 558 | 559 | def render_git_describe_long(pieces): 560 | """TAG-DISTANCE-gHEX[-dirty]. 561 | 562 | Like 'git describe --tags --dirty --always -long'. 563 | The distance/hash is unconditional. 564 | 565 | Exceptions: 566 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 567 | """ 568 | if pieces["closest-tag"]: 569 | rendered = pieces["closest-tag"] 570 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 571 | else: 572 | # exception #1 573 | rendered = pieces["short"] 574 | if pieces["dirty"]: 575 | rendered += "-dirty" 576 | return rendered 577 | 578 | 579 | def render(pieces, style): 580 | """Render the given version pieces into the requested style.""" 581 | if pieces["error"]: 582 | return {"version": "unknown", 583 | "full-revisionid": pieces.get("long"), 584 | "dirty": None, 585 | "error": pieces["error"], 586 | "date": None} 587 | 588 | if not style or style == "default": 589 | style = "pep440" # the default 590 | 591 | if style == "pep440": 592 | rendered = render_pep440(pieces) 593 | elif style == "pep440-branch": 594 | rendered = render_pep440_branch(pieces) 595 | elif style == "pep440-pre": 596 | rendered = render_pep440_pre(pieces) 597 | elif style == "pep440-post": 598 | rendered = render_pep440_post(pieces) 599 | elif style == "pep440-post-branch": 600 | rendered = render_pep440_post_branch(pieces) 601 | elif style == "pep440-old": 602 | rendered = render_pep440_old(pieces) 603 | elif style == "git-describe": 604 | rendered = render_git_describe(pieces) 605 | elif style == "git-describe-long": 606 | rendered = render_git_describe_long(pieces) 607 | else: 608 | raise ValueError("unknown style '%s'" % style) 609 | 610 | return {"version": rendered, "full-revisionid": pieces["long"], 611 | "dirty": pieces["dirty"], "error": None, 612 | "date": pieces.get("date")} 613 | 614 | 615 | def get_versions(): 616 | """Get version information or return default if unable to do so.""" 617 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 618 | # __file__, we can work backwards from there to the root. Some 619 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 620 | # case we can only use expanded keywords. 621 | 622 | cfg = get_config() 623 | verbose = cfg.verbose 624 | 625 | try: 626 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 627 | verbose) 628 | except NotThisMethod: 629 | pass 630 | 631 | try: 632 | root = os.path.realpath(__file__) 633 | # versionfile_source is the relative path from the top of the source 634 | # tree (where the .git directory might live) to this file. Invert 635 | # this to find the root from __file__. 636 | for _ in cfg.versionfile_source.split('/'): 637 | root = os.path.dirname(root) 638 | except NameError: 639 | return {"version": "0+unknown", "full-revisionid": None, 640 | "dirty": None, 641 | "error": "unable to find root of source tree", 642 | "date": None} 643 | 644 | try: 645 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 646 | return render(pieces, cfg.style) 647 | except NotThisMethod: 648 | pass 649 | 650 | try: 651 | if cfg.parentdir_prefix: 652 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 653 | except NotThisMethod: 654 | pass 655 | 656 | return {"version": "0+unknown", "full-revisionid": None, 657 | "dirty": None, 658 | "error": "unable to compute version", "date": None} 659 | -------------------------------------------------------------------------------- /versioneer.py: -------------------------------------------------------------------------------- 1 | 2 | # Version: 0.27 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/python-versioneer/python-versioneer 11 | * Brian Warner 12 | * License: Public Domain (Unlicense) 13 | * Compatible with: Python 3.7, 3.8, 3.9, 3.10 and pypy3 14 | * [![Latest Version][pypi-image]][pypi-url] 15 | * [![Build Status][travis-image]][travis-url] 16 | 17 | This is a tool for managing a recorded version number in setuptools-based 18 | python projects. The goal is to remove the tedious and error-prone "update 19 | the embedded version string" step from your release process. Making a new 20 | release should be as easy as recording a new tag in your version-control 21 | system, and maybe making new tarballs. 22 | 23 | 24 | ## Quick Install 25 | 26 | Versioneer provides two installation modes. The "classic" vendored mode installs 27 | a copy of versioneer into your repository. The experimental build-time dependency mode 28 | is intended to allow you to skip this step and simplify the process of upgrading. 29 | 30 | ### Vendored mode 31 | 32 | * `pip install versioneer` to somewhere in your $PATH 33 | * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is 34 | available, so you can also use `conda install -c conda-forge versioneer` 35 | * add a `[tool.versioneer]` section to your `pyproject.toml` or a 36 | `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) 37 | * Note that you will need to add `tomli` to your build-time dependencies if you 38 | use `pyproject.toml` 39 | * run `versioneer install --vendor` in your source tree, commit the results 40 | * verify version information with `python setup.py version` 41 | 42 | ### Build-time dependency mode 43 | 44 | * `pip install versioneer` to somewhere in your $PATH 45 | * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is 46 | available, so you can also use `conda install -c conda-forge versioneer` 47 | * add a `[tool.versioneer]` section to your `pyproject.toml` or a 48 | `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) 49 | * add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) 50 | to the `requires` key of the `build-system` table in `pyproject.toml`: 51 | ```toml 52 | [build-system] 53 | requires = ["setuptools", "versioneer[toml]"] 54 | build-backend = "setuptools.build_meta" 55 | ``` 56 | * run `versioneer install --no-vendor` in your source tree, commit the results 57 | * verify version information with `python setup.py version` 58 | 59 | ## Version Identifiers 60 | 61 | Source trees come from a variety of places: 62 | 63 | * a version-control system checkout (mostly used by developers) 64 | * a nightly tarball, produced by build automation 65 | * a snapshot tarball, produced by a web-based VCS browser, like github's 66 | "tarball from tag" feature 67 | * a release tarball, produced by "setup.py sdist", distributed through PyPI 68 | 69 | Within each source tree, the version identifier (either a string or a number, 70 | this tool is format-agnostic) can come from a variety of places: 71 | 72 | * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows 73 | about recent "tags" and an absolute revision-id 74 | * the name of the directory into which the tarball was unpacked 75 | * an expanded VCS keyword ($Id$, etc) 76 | * a `_version.py` created by some earlier build step 77 | 78 | For released software, the version identifier is closely related to a VCS 79 | tag. Some projects use tag names that include more than just the version 80 | string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool 81 | needs to strip the tag prefix to extract the version identifier. For 82 | unreleased software (between tags), the version identifier should provide 83 | enough information to help developers recreate the same tree, while also 84 | giving them an idea of roughly how old the tree is (after version 1.2, before 85 | version 1.3). Many VCS systems can report a description that captures this, 86 | for example `git describe --tags --dirty --always` reports things like 87 | "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 88 | 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has 89 | uncommitted changes). 90 | 91 | The version identifier is used for multiple purposes: 92 | 93 | * to allow the module to self-identify its version: `myproject.__version__` 94 | * to choose a name and prefix for a 'setup.py sdist' tarball 95 | 96 | ## Theory of Operation 97 | 98 | Versioneer works by adding a special `_version.py` file into your source 99 | tree, where your `__init__.py` can import it. This `_version.py` knows how to 100 | dynamically ask the VCS tool for version information at import time. 101 | 102 | `_version.py` also contains `$Revision$` markers, and the installation 103 | process marks `_version.py` to have this marker rewritten with a tag name 104 | during the `git archive` command. As a result, generated tarballs will 105 | contain enough information to get the proper version. 106 | 107 | To allow `setup.py` to compute a version too, a `versioneer.py` is added to 108 | the top level of your source tree, next to `setup.py` and the `setup.cfg` 109 | that configures it. This overrides several distutils/setuptools commands to 110 | compute the version when invoked, and changes `setup.py build` and `setup.py 111 | sdist` to replace `_version.py` with a small static file that contains just 112 | the generated version data. 113 | 114 | ## Installation 115 | 116 | See [INSTALL.md](./INSTALL.md) for detailed installation instructions. 117 | 118 | ## Version-String Flavors 119 | 120 | Code which uses Versioneer can learn about its version string at runtime by 121 | importing `_version` from your main `__init__.py` file and running the 122 | `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can 123 | import the top-level `versioneer.py` and run `get_versions()`. 124 | 125 | Both functions return a dictionary with different flavors of version 126 | information: 127 | 128 | * `['version']`: A condensed version string, rendered using the selected 129 | style. This is the most commonly used value for the project's version 130 | string. The default "pep440" style yields strings like `0.11`, 131 | `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section 132 | below for alternative styles. 133 | 134 | * `['full-revisionid']`: detailed revision identifier. For Git, this is the 135 | full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". 136 | 137 | * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the 138 | commit date in ISO 8601 format. This will be None if the date is not 139 | available. 140 | 141 | * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that 142 | this is only accurate if run in a VCS checkout, otherwise it is likely to 143 | be False or None 144 | 145 | * `['error']`: if the version string could not be computed, this will be set 146 | to a string describing the problem, otherwise it will be None. It may be 147 | useful to throw an exception in setup.py if this is set, to avoid e.g. 148 | creating tarballs with a version string of "unknown". 149 | 150 | Some variants are more useful than others. Including `full-revisionid` in a 151 | bug report should allow developers to reconstruct the exact code being tested 152 | (or indicate the presence of local changes that should be shared with the 153 | developers). `version` is suitable for display in an "about" box or a CLI 154 | `--version` output: it can be easily compared against release notes and lists 155 | of bugs fixed in various releases. 156 | 157 | The installer adds the following text to your `__init__.py` to place a basic 158 | version in `YOURPROJECT.__version__`: 159 | 160 | from ._version import get_versions 161 | __version__ = get_versions()['version'] 162 | del get_versions 163 | 164 | ## Styles 165 | 166 | The setup.cfg `style=` configuration controls how the VCS information is 167 | rendered into a version string. 168 | 169 | The default style, "pep440", produces a PEP440-compliant string, equal to the 170 | un-prefixed tag name for actual releases, and containing an additional "local 171 | version" section with more detail for in-between builds. For Git, this is 172 | TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags 173 | --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the 174 | tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and 175 | that this commit is two revisions ("+2") beyond the "0.11" tag. For released 176 | software (exactly equal to a known tag), the identifier will only contain the 177 | stripped tag, e.g. "0.11". 178 | 179 | Other styles are available. See [details.md](details.md) in the Versioneer 180 | source tree for descriptions. 181 | 182 | ## Debugging 183 | 184 | Versioneer tries to avoid fatal errors: if something goes wrong, it will tend 185 | to return a version of "0+unknown". To investigate the problem, run `setup.py 186 | version`, which will run the version-lookup code in a verbose mode, and will 187 | display the full contents of `get_versions()` (including the `error` string, 188 | which may help identify what went wrong). 189 | 190 | ## Known Limitations 191 | 192 | Some situations are known to cause problems for Versioneer. This details the 193 | most significant ones. More can be found on Github 194 | [issues page](https://github.com/python-versioneer/python-versioneer/issues). 195 | 196 | ### Subprojects 197 | 198 | Versioneer has limited support for source trees in which `setup.py` is not in 199 | the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are 200 | two common reasons why `setup.py` might not be in the root: 201 | 202 | * Source trees which contain multiple subprojects, such as 203 | [Buildbot](https://github.com/buildbot/buildbot), which contains both 204 | "master" and "slave" subprojects, each with their own `setup.py`, 205 | `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI 206 | distributions (and upload multiple independently-installable tarballs). 207 | * Source trees whose main purpose is to contain a C library, but which also 208 | provide bindings to Python (and perhaps other languages) in subdirectories. 209 | 210 | Versioneer will look for `.git` in parent directories, and most operations 211 | should get the right version string. However `pip` and `setuptools` have bugs 212 | and implementation details which frequently cause `pip install .` from a 213 | subproject directory to fail to find a correct version string (so it usually 214 | defaults to `0+unknown`). 215 | 216 | `pip install --editable .` should work correctly. `setup.py install` might 217 | work too. 218 | 219 | Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in 220 | some later version. 221 | 222 | [Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking 223 | this issue. The discussion in 224 | [PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the 225 | issue from the Versioneer side in more detail. 226 | [pip PR#3176](https://github.com/pypa/pip/pull/3176) and 227 | [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve 228 | pip to let Versioneer work correctly. 229 | 230 | Versioneer-0.16 and earlier only looked for a `.git` directory next to the 231 | `setup.cfg`, so subprojects were completely unsupported with those releases. 232 | 233 | ### Editable installs with setuptools <= 18.5 234 | 235 | `setup.py develop` and `pip install --editable .` allow you to install a 236 | project into a virtualenv once, then continue editing the source code (and 237 | test) without re-installing after every change. 238 | 239 | "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a 240 | convenient way to specify executable scripts that should be installed along 241 | with the python package. 242 | 243 | These both work as expected when using modern setuptools. When using 244 | setuptools-18.5 or earlier, however, certain operations will cause 245 | `pkg_resources.DistributionNotFound` errors when running the entrypoint 246 | script, which must be resolved by re-installing the package. This happens 247 | when the install happens with one version, then the egg_info data is 248 | regenerated while a different version is checked out. Many setup.py commands 249 | cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into 250 | a different virtualenv), so this can be surprising. 251 | 252 | [Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes 253 | this one, but upgrading to a newer version of setuptools should probably 254 | resolve it. 255 | 256 | 257 | ## Updating Versioneer 258 | 259 | To upgrade your project to a new release of Versioneer, do the following: 260 | 261 | * install the new Versioneer (`pip install -U versioneer` or equivalent) 262 | * edit `setup.cfg` and `pyproject.toml`, if necessary, 263 | to include any new configuration settings indicated by the release notes. 264 | See [UPGRADING](./UPGRADING.md) for details. 265 | * re-run `versioneer install --[no-]vendor` in your source tree, to replace 266 | `SRC/_version.py` 267 | * commit any changed files 268 | 269 | ## Future Directions 270 | 271 | This tool is designed to make it easily extended to other version-control 272 | systems: all VCS-specific components are in separate directories like 273 | src/git/ . The top-level `versioneer.py` script is assembled from these 274 | components by running make-versioneer.py . In the future, make-versioneer.py 275 | will take a VCS name as an argument, and will construct a version of 276 | `versioneer.py` that is specific to the given VCS. It might also take the 277 | configuration arguments that are currently provided manually during 278 | installation by editing setup.py . Alternatively, it might go the other 279 | direction and include code from all supported VCS systems, reducing the 280 | number of intermediate scripts. 281 | 282 | ## Similar projects 283 | 284 | * [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time 285 | dependency 286 | * [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of 287 | versioneer 288 | * [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools 289 | plugin 290 | 291 | ## License 292 | 293 | To make Versioneer easier to embed, all its code is dedicated to the public 294 | domain. The `_version.py` that it creates is also in the public domain. 295 | Specifically, both are released under the "Unlicense", as described in 296 | https://unlicense.org/. 297 | 298 | [pypi-image]: https://img.shields.io/pypi/v/versioneer.svg 299 | [pypi-url]: https://pypi.python.org/pypi/versioneer/ 300 | [travis-image]: 301 | https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg 302 | [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer 303 | 304 | """ 305 | # pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring 306 | # pylint:disable=missing-class-docstring,too-many-branches,too-many-statements 307 | # pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error 308 | # pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with 309 | # pylint:disable=attribute-defined-outside-init,too-many-arguments 310 | 311 | import configparser 312 | import errno 313 | import json 314 | import os 315 | import re 316 | import subprocess 317 | import sys 318 | from pathlib import Path 319 | from typing import Callable, Dict 320 | import functools 321 | try: 322 | import tomli 323 | have_tomli = True 324 | except ImportError: 325 | have_tomli = False 326 | 327 | 328 | class VersioneerConfig: 329 | """Container for Versioneer configuration parameters.""" 330 | 331 | 332 | def get_root(): 333 | """Get the project root directory. 334 | 335 | We require that all commands are run from the project root, i.e. the 336 | directory that contains setup.py, setup.cfg, and versioneer.py . 337 | """ 338 | root = os.path.realpath(os.path.abspath(os.getcwd())) 339 | setup_py = os.path.join(root, "setup.py") 340 | versioneer_py = os.path.join(root, "versioneer.py") 341 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 342 | # allow 'python path/to/setup.py COMMAND' 343 | root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) 344 | setup_py = os.path.join(root, "setup.py") 345 | versioneer_py = os.path.join(root, "versioneer.py") 346 | if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): 347 | err = ("Versioneer was unable to run the project root directory. " 348 | "Versioneer requires setup.py to be executed from " 349 | "its immediate directory (like 'python setup.py COMMAND'), " 350 | "or in a way that lets it use sys.argv[0] to find the root " 351 | "(like 'python path/to/setup.py COMMAND').") 352 | raise VersioneerBadRootError(err) 353 | try: 354 | # Certain runtime workflows (setup.py install/develop in a setuptools 355 | # tree) execute all dependencies in a single python process, so 356 | # "versioneer" may be imported multiple times, and python's shared 357 | # module-import table will cache the first one. So we can't use 358 | # os.path.dirname(__file__), as that will find whichever 359 | # versioneer.py was first imported, even in later projects. 360 | my_path = os.path.realpath(os.path.abspath(__file__)) 361 | me_dir = os.path.normcase(os.path.splitext(my_path)[0]) 362 | vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) 363 | if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): 364 | print("Warning: build in %s is using versioneer.py from %s" 365 | % (os.path.dirname(my_path), versioneer_py)) 366 | except NameError: 367 | pass 368 | return root 369 | 370 | 371 | def get_config_from_root(root): 372 | """Read the project setup.cfg file to determine Versioneer config.""" 373 | # This might raise OSError (if setup.cfg is missing), or 374 | # configparser.NoSectionError (if it lacks a [versioneer] section), or 375 | # configparser.NoOptionError (if it lacks "VCS="). See the docstring at 376 | # the top of versioneer.py for instructions on writing your setup.cfg . 377 | root = Path(root) 378 | pyproject_toml = root / "pyproject.toml" 379 | setup_cfg = root / "setup.cfg" 380 | section = None 381 | if pyproject_toml.exists() and have_tomli: 382 | try: 383 | with open(pyproject_toml, 'rb') as fobj: 384 | pp = tomli.load(fobj) 385 | section = pp['tool']['versioneer'] 386 | except (tomli.TOMLDecodeError, KeyError): 387 | pass 388 | if not section: 389 | parser = configparser.ConfigParser() 390 | with open(setup_cfg) as cfg_file: 391 | parser.read_file(cfg_file) 392 | parser.get("versioneer", "VCS") # raise error if missing 393 | 394 | section = parser["versioneer"] 395 | 396 | cfg = VersioneerConfig() 397 | cfg.VCS = section['VCS'] 398 | cfg.style = section.get("style", "") 399 | cfg.versionfile_source = section.get("versionfile_source") 400 | cfg.versionfile_build = section.get("versionfile_build") 401 | cfg.tag_prefix = section.get("tag_prefix") 402 | if cfg.tag_prefix in ("''", '""', None): 403 | cfg.tag_prefix = "" 404 | cfg.parentdir_prefix = section.get("parentdir_prefix") 405 | cfg.verbose = section.get("verbose") 406 | return cfg 407 | 408 | 409 | class NotThisMethod(Exception): 410 | """Exception raised if a method is not valid for the current scenario.""" 411 | 412 | 413 | # these dictionaries contain VCS-specific tools 414 | LONG_VERSION_PY: Dict[str, str] = {} 415 | HANDLERS: Dict[str, Dict[str, Callable]] = {} 416 | 417 | 418 | def register_vcs_handler(vcs, method): # decorator 419 | """Create decorator to mark a method as the handler of a VCS.""" 420 | def decorate(f): 421 | """Store f in HANDLERS[vcs][method].""" 422 | HANDLERS.setdefault(vcs, {})[method] = f 423 | return f 424 | return decorate 425 | 426 | 427 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 428 | env=None): 429 | """Call the given command(s).""" 430 | assert isinstance(commands, list) 431 | process = None 432 | 433 | popen_kwargs = {} 434 | if sys.platform == "win32": 435 | # This hides the console window if pythonw.exe is used 436 | startupinfo = subprocess.STARTUPINFO() 437 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 438 | popen_kwargs["startupinfo"] = startupinfo 439 | 440 | for command in commands: 441 | try: 442 | dispcmd = str([command] + args) 443 | # remember shell=False, so use git.cmd on windows, not just git 444 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 445 | stdout=subprocess.PIPE, 446 | stderr=(subprocess.PIPE if hide_stderr 447 | else None), **popen_kwargs) 448 | break 449 | except OSError: 450 | e = sys.exc_info()[1] 451 | if e.errno == errno.ENOENT: 452 | continue 453 | if verbose: 454 | print("unable to run %s" % dispcmd) 455 | print(e) 456 | return None, None 457 | else: 458 | if verbose: 459 | print("unable to find command, tried %s" % (commands,)) 460 | return None, None 461 | stdout = process.communicate()[0].strip().decode() 462 | if process.returncode != 0: 463 | if verbose: 464 | print("unable to run %s (error)" % dispcmd) 465 | print("stdout was %s" % stdout) 466 | return None, process.returncode 467 | return stdout, process.returncode 468 | 469 | 470 | LONG_VERSION_PY['git'] = r''' 471 | # This file helps to compute a version number in source trees obtained from 472 | # git-archive tarball (such as those provided by githubs download-from-tag 473 | # feature). Distribution tarballs (built by setup.py sdist) and build 474 | # directories (produced by setup.py build) will contain a much shorter file 475 | # that just contains the computed version number. 476 | 477 | # This file is released into the public domain. 478 | # Generated by versioneer-0.27 479 | # https://github.com/python-versioneer/python-versioneer 480 | 481 | """Git implementation of _version.py.""" 482 | 483 | import errno 484 | import os 485 | import re 486 | import subprocess 487 | import sys 488 | from typing import Callable, Dict 489 | import functools 490 | 491 | 492 | def get_keywords(): 493 | """Get the keywords needed to look up the version information.""" 494 | # these strings will be replaced by git during git-archive. 495 | # setup.py/versioneer.py will grep for the variable names, so they must 496 | # each be defined on a line of their own. _version.py will just call 497 | # get_keywords(). 498 | git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" 499 | git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" 500 | git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" 501 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 502 | return keywords 503 | 504 | 505 | class VersioneerConfig: 506 | """Container for Versioneer configuration parameters.""" 507 | 508 | 509 | def get_config(): 510 | """Create, populate and return the VersioneerConfig() object.""" 511 | # these strings are filled in when 'setup.py versioneer' creates 512 | # _version.py 513 | cfg = VersioneerConfig() 514 | cfg.VCS = "git" 515 | cfg.style = "%(STYLE)s" 516 | cfg.tag_prefix = "%(TAG_PREFIX)s" 517 | cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" 518 | cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" 519 | cfg.verbose = False 520 | return cfg 521 | 522 | 523 | class NotThisMethod(Exception): 524 | """Exception raised if a method is not valid for the current scenario.""" 525 | 526 | 527 | LONG_VERSION_PY: Dict[str, str] = {} 528 | HANDLERS: Dict[str, Dict[str, Callable]] = {} 529 | 530 | 531 | def register_vcs_handler(vcs, method): # decorator 532 | """Create decorator to mark a method as the handler of a VCS.""" 533 | def decorate(f): 534 | """Store f in HANDLERS[vcs][method].""" 535 | if vcs not in HANDLERS: 536 | HANDLERS[vcs] = {} 537 | HANDLERS[vcs][method] = f 538 | return f 539 | return decorate 540 | 541 | 542 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 543 | env=None): 544 | """Call the given command(s).""" 545 | assert isinstance(commands, list) 546 | process = None 547 | 548 | popen_kwargs = {} 549 | if sys.platform == "win32": 550 | # This hides the console window if pythonw.exe is used 551 | startupinfo = subprocess.STARTUPINFO() 552 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 553 | popen_kwargs["startupinfo"] = startupinfo 554 | 555 | for command in commands: 556 | try: 557 | dispcmd = str([command] + args) 558 | # remember shell=False, so use git.cmd on windows, not just git 559 | process = subprocess.Popen([command] + args, cwd=cwd, env=env, 560 | stdout=subprocess.PIPE, 561 | stderr=(subprocess.PIPE if hide_stderr 562 | else None), **popen_kwargs) 563 | break 564 | except OSError: 565 | e = sys.exc_info()[1] 566 | if e.errno == errno.ENOENT: 567 | continue 568 | if verbose: 569 | print("unable to run %%s" %% dispcmd) 570 | print(e) 571 | return None, None 572 | else: 573 | if verbose: 574 | print("unable to find command, tried %%s" %% (commands,)) 575 | return None, None 576 | stdout = process.communicate()[0].strip().decode() 577 | if process.returncode != 0: 578 | if verbose: 579 | print("unable to run %%s (error)" %% dispcmd) 580 | print("stdout was %%s" %% stdout) 581 | return None, process.returncode 582 | return stdout, process.returncode 583 | 584 | 585 | def versions_from_parentdir(parentdir_prefix, root, verbose): 586 | """Try to determine the version from the parent directory name. 587 | 588 | Source tarballs conventionally unpack into a directory that includes both 589 | the project name and a version string. We will also support searching up 590 | two directory levels for an appropriately named parent directory 591 | """ 592 | rootdirs = [] 593 | 594 | for _ in range(3): 595 | dirname = os.path.basename(root) 596 | if dirname.startswith(parentdir_prefix): 597 | return {"version": dirname[len(parentdir_prefix):], 598 | "full-revisionid": None, 599 | "dirty": False, "error": None, "date": None} 600 | rootdirs.append(root) 601 | root = os.path.dirname(root) # up a level 602 | 603 | if verbose: 604 | print("Tried directories %%s but none started with prefix %%s" %% 605 | (str(rootdirs), parentdir_prefix)) 606 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 607 | 608 | 609 | @register_vcs_handler("git", "get_keywords") 610 | def git_get_keywords(versionfile_abs): 611 | """Extract version information from the given file.""" 612 | # the code embedded in _version.py can just fetch the value of these 613 | # keywords. When used from setup.py, we don't want to import _version.py, 614 | # so we do it with a regexp instead. This function is not used from 615 | # _version.py. 616 | keywords = {} 617 | try: 618 | with open(versionfile_abs, "r") as fobj: 619 | for line in fobj: 620 | if line.strip().startswith("git_refnames ="): 621 | mo = re.search(r'=\s*"(.*)"', line) 622 | if mo: 623 | keywords["refnames"] = mo.group(1) 624 | if line.strip().startswith("git_full ="): 625 | mo = re.search(r'=\s*"(.*)"', line) 626 | if mo: 627 | keywords["full"] = mo.group(1) 628 | if line.strip().startswith("git_date ="): 629 | mo = re.search(r'=\s*"(.*)"', line) 630 | if mo: 631 | keywords["date"] = mo.group(1) 632 | except OSError: 633 | pass 634 | return keywords 635 | 636 | 637 | @register_vcs_handler("git", "keywords") 638 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 639 | """Get version information from git keywords.""" 640 | if "refnames" not in keywords: 641 | raise NotThisMethod("Short version file found") 642 | date = keywords.get("date") 643 | if date is not None: 644 | # Use only the last line. Previous lines may contain GPG signature 645 | # information. 646 | date = date.splitlines()[-1] 647 | 648 | # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant 649 | # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 650 | # -like" string, which we must then edit to make compliant), because 651 | # it's been around since git-1.5.3, and it's too difficult to 652 | # discover which version we're using, or to work around using an 653 | # older one. 654 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 655 | refnames = keywords["refnames"].strip() 656 | if refnames.startswith("$Format"): 657 | if verbose: 658 | print("keywords are unexpanded, not using") 659 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 660 | refs = {r.strip() for r in refnames.strip("()").split(",")} 661 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 662 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 663 | TAG = "tag: " 664 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 665 | if not tags: 666 | # Either we're using git < 1.8.3, or there really are no tags. We use 667 | # a heuristic: assume all version tags have a digit. The old git %%d 668 | # expansion behaves like git log --decorate=short and strips out the 669 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 670 | # between branches and tags. By ignoring refnames without digits, we 671 | # filter out many common branch names like "release" and 672 | # "stabilization", as well as "HEAD" and "master". 673 | tags = {r for r in refs if re.search(r'\d', r)} 674 | if verbose: 675 | print("discarding '%%s', no digits" %% ",".join(refs - tags)) 676 | if verbose: 677 | print("likely tags: %%s" %% ",".join(sorted(tags))) 678 | for ref in sorted(tags): 679 | # sorting will prefer e.g. "2.0" over "2.0rc1" 680 | if ref.startswith(tag_prefix): 681 | r = ref[len(tag_prefix):] 682 | # Filter out refs that exactly match prefix or that don't start 683 | # with a number once the prefix is stripped (mostly a concern 684 | # when prefix is '') 685 | if not re.match(r'\d', r): 686 | continue 687 | if verbose: 688 | print("picking %%s" %% r) 689 | return {"version": r, 690 | "full-revisionid": keywords["full"].strip(), 691 | "dirty": False, "error": None, 692 | "date": date} 693 | # no suitable tags, so version is "0+unknown", but full hex is still there 694 | if verbose: 695 | print("no suitable tags, using unknown + full revision id") 696 | return {"version": "0+unknown", 697 | "full-revisionid": keywords["full"].strip(), 698 | "dirty": False, "error": "no suitable tags", "date": None} 699 | 700 | 701 | @register_vcs_handler("git", "pieces_from_vcs") 702 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 703 | """Get version from 'git describe' in the root of the source tree. 704 | 705 | This only gets called if the git-archive 'subst' keywords were *not* 706 | expanded, and _version.py hasn't already been rewritten with a short 707 | version string, meaning we're inside a checked out source tree. 708 | """ 709 | GITS = ["git"] 710 | if sys.platform == "win32": 711 | GITS = ["git.cmd", "git.exe"] 712 | 713 | # GIT_DIR can interfere with correct operation of Versioneer. 714 | # It may be intended to be passed to the Versioneer-versioned project, 715 | # but that should not change where we get our version from. 716 | env = os.environ.copy() 717 | env.pop("GIT_DIR", None) 718 | runner = functools.partial(runner, env=env) 719 | 720 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 721 | hide_stderr=not verbose) 722 | if rc != 0: 723 | if verbose: 724 | print("Directory %%s not under git control" %% root) 725 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 726 | 727 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 728 | # if there isn't one, this yields HEX[-dirty] (no NUM) 729 | describe_out, rc = runner(GITS, [ 730 | "describe", "--tags", "--dirty", "--always", "--long", 731 | "--match", f"{tag_prefix}[[:digit:]]*" 732 | ], cwd=root) 733 | # --long was added in git-1.5.5 734 | if describe_out is None: 735 | raise NotThisMethod("'git describe' failed") 736 | describe_out = describe_out.strip() 737 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 738 | if full_out is None: 739 | raise NotThisMethod("'git rev-parse' failed") 740 | full_out = full_out.strip() 741 | 742 | pieces = {} 743 | pieces["long"] = full_out 744 | pieces["short"] = full_out[:7] # maybe improved later 745 | pieces["error"] = None 746 | 747 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 748 | cwd=root) 749 | # --abbrev-ref was added in git-1.6.3 750 | if rc != 0 or branch_name is None: 751 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 752 | branch_name = branch_name.strip() 753 | 754 | if branch_name == "HEAD": 755 | # If we aren't exactly on a branch, pick a branch which represents 756 | # the current commit. If all else fails, we are on a branchless 757 | # commit. 758 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 759 | # --contains was added in git-1.5.4 760 | if rc != 0 or branches is None: 761 | raise NotThisMethod("'git branch --contains' returned error") 762 | branches = branches.split("\n") 763 | 764 | # Remove the first line if we're running detached 765 | if "(" in branches[0]: 766 | branches.pop(0) 767 | 768 | # Strip off the leading "* " from the list of branches. 769 | branches = [branch[2:] for branch in branches] 770 | if "master" in branches: 771 | branch_name = "master" 772 | elif not branches: 773 | branch_name = None 774 | else: 775 | # Pick the first branch that is returned. Good or bad. 776 | branch_name = branches[0] 777 | 778 | pieces["branch"] = branch_name 779 | 780 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 781 | # TAG might have hyphens. 782 | git_describe = describe_out 783 | 784 | # look for -dirty suffix 785 | dirty = git_describe.endswith("-dirty") 786 | pieces["dirty"] = dirty 787 | if dirty: 788 | git_describe = git_describe[:git_describe.rindex("-dirty")] 789 | 790 | # now we have TAG-NUM-gHEX or HEX 791 | 792 | if "-" in git_describe: 793 | # TAG-NUM-gHEX 794 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 795 | if not mo: 796 | # unparsable. Maybe git-describe is misbehaving? 797 | pieces["error"] = ("unable to parse git-describe output: '%%s'" 798 | %% describe_out) 799 | return pieces 800 | 801 | # tag 802 | full_tag = mo.group(1) 803 | if not full_tag.startswith(tag_prefix): 804 | if verbose: 805 | fmt = "tag '%%s' doesn't start with prefix '%%s'" 806 | print(fmt %% (full_tag, tag_prefix)) 807 | pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" 808 | %% (full_tag, tag_prefix)) 809 | return pieces 810 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 811 | 812 | # distance: number of commits since tag 813 | pieces["distance"] = int(mo.group(2)) 814 | 815 | # commit: short hex revision ID 816 | pieces["short"] = mo.group(3) 817 | 818 | else: 819 | # HEX: no tags 820 | pieces["closest-tag"] = None 821 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 822 | pieces["distance"] = len(out.split()) # total number of commits 823 | 824 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 825 | date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() 826 | # Use only the last line. Previous lines may contain GPG signature 827 | # information. 828 | date = date.splitlines()[-1] 829 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 830 | 831 | return pieces 832 | 833 | 834 | def plus_or_dot(pieces): 835 | """Return a + if we don't already have one, else return a .""" 836 | if "+" in pieces.get("closest-tag", ""): 837 | return "." 838 | return "+" 839 | 840 | 841 | def render_pep440(pieces): 842 | """Build up version string, with post-release "local version identifier". 843 | 844 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 845 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 846 | 847 | Exceptions: 848 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 849 | """ 850 | if pieces["closest-tag"]: 851 | rendered = pieces["closest-tag"] 852 | if pieces["distance"] or pieces["dirty"]: 853 | rendered += plus_or_dot(pieces) 854 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 855 | if pieces["dirty"]: 856 | rendered += ".dirty" 857 | else: 858 | # exception #1 859 | rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], 860 | pieces["short"]) 861 | if pieces["dirty"]: 862 | rendered += ".dirty" 863 | return rendered 864 | 865 | 866 | def render_pep440_branch(pieces): 867 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 868 | 869 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 870 | (a feature branch will appear "older" than the master branch). 871 | 872 | Exceptions: 873 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 874 | """ 875 | if pieces["closest-tag"]: 876 | rendered = pieces["closest-tag"] 877 | if pieces["distance"] or pieces["dirty"]: 878 | if pieces["branch"] != "master": 879 | rendered += ".dev0" 880 | rendered += plus_or_dot(pieces) 881 | rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) 882 | if pieces["dirty"]: 883 | rendered += ".dirty" 884 | else: 885 | # exception #1 886 | rendered = "0" 887 | if pieces["branch"] != "master": 888 | rendered += ".dev0" 889 | rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], 890 | pieces["short"]) 891 | if pieces["dirty"]: 892 | rendered += ".dirty" 893 | return rendered 894 | 895 | 896 | def pep440_split_post(ver): 897 | """Split pep440 version string at the post-release segment. 898 | 899 | Returns the release segments before the post-release and the 900 | post-release version number (or -1 if no post-release segment is present). 901 | """ 902 | vc = str.split(ver, ".post") 903 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 904 | 905 | 906 | def render_pep440_pre(pieces): 907 | """TAG[.postN.devDISTANCE] -- No -dirty. 908 | 909 | Exceptions: 910 | 1: no tags. 0.post0.devDISTANCE 911 | """ 912 | if pieces["closest-tag"]: 913 | if pieces["distance"]: 914 | # update the post release segment 915 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 916 | rendered = tag_version 917 | if post_version is not None: 918 | rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) 919 | else: 920 | rendered += ".post0.dev%%d" %% (pieces["distance"]) 921 | else: 922 | # no commits, use the tag as the version 923 | rendered = pieces["closest-tag"] 924 | else: 925 | # exception #1 926 | rendered = "0.post0.dev%%d" %% pieces["distance"] 927 | return rendered 928 | 929 | 930 | def render_pep440_post(pieces): 931 | """TAG[.postDISTANCE[.dev0]+gHEX] . 932 | 933 | The ".dev0" means dirty. Note that .dev0 sorts backwards 934 | (a dirty tree will appear "older" than the corresponding clean one), 935 | but you shouldn't be releasing software with -dirty anyways. 936 | 937 | Exceptions: 938 | 1: no tags. 0.postDISTANCE[.dev0] 939 | """ 940 | if pieces["closest-tag"]: 941 | rendered = pieces["closest-tag"] 942 | if pieces["distance"] or pieces["dirty"]: 943 | rendered += ".post%%d" %% pieces["distance"] 944 | if pieces["dirty"]: 945 | rendered += ".dev0" 946 | rendered += plus_or_dot(pieces) 947 | rendered += "g%%s" %% pieces["short"] 948 | else: 949 | # exception #1 950 | rendered = "0.post%%d" %% pieces["distance"] 951 | if pieces["dirty"]: 952 | rendered += ".dev0" 953 | rendered += "+g%%s" %% pieces["short"] 954 | return rendered 955 | 956 | 957 | def render_pep440_post_branch(pieces): 958 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 959 | 960 | The ".dev0" means not master branch. 961 | 962 | Exceptions: 963 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 964 | """ 965 | if pieces["closest-tag"]: 966 | rendered = pieces["closest-tag"] 967 | if pieces["distance"] or pieces["dirty"]: 968 | rendered += ".post%%d" %% pieces["distance"] 969 | if pieces["branch"] != "master": 970 | rendered += ".dev0" 971 | rendered += plus_or_dot(pieces) 972 | rendered += "g%%s" %% pieces["short"] 973 | if pieces["dirty"]: 974 | rendered += ".dirty" 975 | else: 976 | # exception #1 977 | rendered = "0.post%%d" %% pieces["distance"] 978 | if pieces["branch"] != "master": 979 | rendered += ".dev0" 980 | rendered += "+g%%s" %% pieces["short"] 981 | if pieces["dirty"]: 982 | rendered += ".dirty" 983 | return rendered 984 | 985 | 986 | def render_pep440_old(pieces): 987 | """TAG[.postDISTANCE[.dev0]] . 988 | 989 | The ".dev0" means dirty. 990 | 991 | Exceptions: 992 | 1: no tags. 0.postDISTANCE[.dev0] 993 | """ 994 | if pieces["closest-tag"]: 995 | rendered = pieces["closest-tag"] 996 | if pieces["distance"] or pieces["dirty"]: 997 | rendered += ".post%%d" %% pieces["distance"] 998 | if pieces["dirty"]: 999 | rendered += ".dev0" 1000 | else: 1001 | # exception #1 1002 | rendered = "0.post%%d" %% pieces["distance"] 1003 | if pieces["dirty"]: 1004 | rendered += ".dev0" 1005 | return rendered 1006 | 1007 | 1008 | def render_git_describe(pieces): 1009 | """TAG[-DISTANCE-gHEX][-dirty]. 1010 | 1011 | Like 'git describe --tags --dirty --always'. 1012 | 1013 | Exceptions: 1014 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1015 | """ 1016 | if pieces["closest-tag"]: 1017 | rendered = pieces["closest-tag"] 1018 | if pieces["distance"]: 1019 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 1020 | else: 1021 | # exception #1 1022 | rendered = pieces["short"] 1023 | if pieces["dirty"]: 1024 | rendered += "-dirty" 1025 | return rendered 1026 | 1027 | 1028 | def render_git_describe_long(pieces): 1029 | """TAG-DISTANCE-gHEX[-dirty]. 1030 | 1031 | Like 'git describe --tags --dirty --always -long'. 1032 | The distance/hash is unconditional. 1033 | 1034 | Exceptions: 1035 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1036 | """ 1037 | if pieces["closest-tag"]: 1038 | rendered = pieces["closest-tag"] 1039 | rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) 1040 | else: 1041 | # exception #1 1042 | rendered = pieces["short"] 1043 | if pieces["dirty"]: 1044 | rendered += "-dirty" 1045 | return rendered 1046 | 1047 | 1048 | def render(pieces, style): 1049 | """Render the given version pieces into the requested style.""" 1050 | if pieces["error"]: 1051 | return {"version": "unknown", 1052 | "full-revisionid": pieces.get("long"), 1053 | "dirty": None, 1054 | "error": pieces["error"], 1055 | "date": None} 1056 | 1057 | if not style or style == "default": 1058 | style = "pep440" # the default 1059 | 1060 | if style == "pep440": 1061 | rendered = render_pep440(pieces) 1062 | elif style == "pep440-branch": 1063 | rendered = render_pep440_branch(pieces) 1064 | elif style == "pep440-pre": 1065 | rendered = render_pep440_pre(pieces) 1066 | elif style == "pep440-post": 1067 | rendered = render_pep440_post(pieces) 1068 | elif style == "pep440-post-branch": 1069 | rendered = render_pep440_post_branch(pieces) 1070 | elif style == "pep440-old": 1071 | rendered = render_pep440_old(pieces) 1072 | elif style == "git-describe": 1073 | rendered = render_git_describe(pieces) 1074 | elif style == "git-describe-long": 1075 | rendered = render_git_describe_long(pieces) 1076 | else: 1077 | raise ValueError("unknown style '%%s'" %% style) 1078 | 1079 | return {"version": rendered, "full-revisionid": pieces["long"], 1080 | "dirty": pieces["dirty"], "error": None, 1081 | "date": pieces.get("date")} 1082 | 1083 | 1084 | def get_versions(): 1085 | """Get version information or return default if unable to do so.""" 1086 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 1087 | # __file__, we can work backwards from there to the root. Some 1088 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 1089 | # case we can only use expanded keywords. 1090 | 1091 | cfg = get_config() 1092 | verbose = cfg.verbose 1093 | 1094 | try: 1095 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 1096 | verbose) 1097 | except NotThisMethod: 1098 | pass 1099 | 1100 | try: 1101 | root = os.path.realpath(__file__) 1102 | # versionfile_source is the relative path from the top of the source 1103 | # tree (where the .git directory might live) to this file. Invert 1104 | # this to find the root from __file__. 1105 | for _ in cfg.versionfile_source.split('/'): 1106 | root = os.path.dirname(root) 1107 | except NameError: 1108 | return {"version": "0+unknown", "full-revisionid": None, 1109 | "dirty": None, 1110 | "error": "unable to find root of source tree", 1111 | "date": None} 1112 | 1113 | try: 1114 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 1115 | return render(pieces, cfg.style) 1116 | except NotThisMethod: 1117 | pass 1118 | 1119 | try: 1120 | if cfg.parentdir_prefix: 1121 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1122 | except NotThisMethod: 1123 | pass 1124 | 1125 | return {"version": "0+unknown", "full-revisionid": None, 1126 | "dirty": None, 1127 | "error": "unable to compute version", "date": None} 1128 | ''' 1129 | 1130 | 1131 | @register_vcs_handler("git", "get_keywords") 1132 | def git_get_keywords(versionfile_abs): 1133 | """Extract version information from the given file.""" 1134 | # the code embedded in _version.py can just fetch the value of these 1135 | # keywords. When used from setup.py, we don't want to import _version.py, 1136 | # so we do it with a regexp instead. This function is not used from 1137 | # _version.py. 1138 | keywords = {} 1139 | try: 1140 | with open(versionfile_abs, "r") as fobj: 1141 | for line in fobj: 1142 | if line.strip().startswith("git_refnames ="): 1143 | mo = re.search(r'=\s*"(.*)"', line) 1144 | if mo: 1145 | keywords["refnames"] = mo.group(1) 1146 | if line.strip().startswith("git_full ="): 1147 | mo = re.search(r'=\s*"(.*)"', line) 1148 | if mo: 1149 | keywords["full"] = mo.group(1) 1150 | if line.strip().startswith("git_date ="): 1151 | mo = re.search(r'=\s*"(.*)"', line) 1152 | if mo: 1153 | keywords["date"] = mo.group(1) 1154 | except OSError: 1155 | pass 1156 | return keywords 1157 | 1158 | 1159 | @register_vcs_handler("git", "keywords") 1160 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 1161 | """Get version information from git keywords.""" 1162 | if "refnames" not in keywords: 1163 | raise NotThisMethod("Short version file found") 1164 | date = keywords.get("date") 1165 | if date is not None: 1166 | # Use only the last line. Previous lines may contain GPG signature 1167 | # information. 1168 | date = date.splitlines()[-1] 1169 | 1170 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 1171 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 1172 | # -like" string, which we must then edit to make compliant), because 1173 | # it's been around since git-1.5.3, and it's too difficult to 1174 | # discover which version we're using, or to work around using an 1175 | # older one. 1176 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1177 | refnames = keywords["refnames"].strip() 1178 | if refnames.startswith("$Format"): 1179 | if verbose: 1180 | print("keywords are unexpanded, not using") 1181 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 1182 | refs = {r.strip() for r in refnames.strip("()").split(",")} 1183 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 1184 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 1185 | TAG = "tag: " 1186 | tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} 1187 | if not tags: 1188 | # Either we're using git < 1.8.3, or there really are no tags. We use 1189 | # a heuristic: assume all version tags have a digit. The old git %d 1190 | # expansion behaves like git log --decorate=short and strips out the 1191 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 1192 | # between branches and tags. By ignoring refnames without digits, we 1193 | # filter out many common branch names like "release" and 1194 | # "stabilization", as well as "HEAD" and "master". 1195 | tags = {r for r in refs if re.search(r'\d', r)} 1196 | if verbose: 1197 | print("discarding '%s', no digits" % ",".join(refs - tags)) 1198 | if verbose: 1199 | print("likely tags: %s" % ",".join(sorted(tags))) 1200 | for ref in sorted(tags): 1201 | # sorting will prefer e.g. "2.0" over "2.0rc1" 1202 | if ref.startswith(tag_prefix): 1203 | r = ref[len(tag_prefix):] 1204 | # Filter out refs that exactly match prefix or that don't start 1205 | # with a number once the prefix is stripped (mostly a concern 1206 | # when prefix is '') 1207 | if not re.match(r'\d', r): 1208 | continue 1209 | if verbose: 1210 | print("picking %s" % r) 1211 | return {"version": r, 1212 | "full-revisionid": keywords["full"].strip(), 1213 | "dirty": False, "error": None, 1214 | "date": date} 1215 | # no suitable tags, so version is "0+unknown", but full hex is still there 1216 | if verbose: 1217 | print("no suitable tags, using unknown + full revision id") 1218 | return {"version": "0+unknown", 1219 | "full-revisionid": keywords["full"].strip(), 1220 | "dirty": False, "error": "no suitable tags", "date": None} 1221 | 1222 | 1223 | @register_vcs_handler("git", "pieces_from_vcs") 1224 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 1225 | """Get version from 'git describe' in the root of the source tree. 1226 | 1227 | This only gets called if the git-archive 'subst' keywords were *not* 1228 | expanded, and _version.py hasn't already been rewritten with a short 1229 | version string, meaning we're inside a checked out source tree. 1230 | """ 1231 | GITS = ["git"] 1232 | if sys.platform == "win32": 1233 | GITS = ["git.cmd", "git.exe"] 1234 | 1235 | # GIT_DIR can interfere with correct operation of Versioneer. 1236 | # It may be intended to be passed to the Versioneer-versioned project, 1237 | # but that should not change where we get our version from. 1238 | env = os.environ.copy() 1239 | env.pop("GIT_DIR", None) 1240 | runner = functools.partial(runner, env=env) 1241 | 1242 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, 1243 | hide_stderr=not verbose) 1244 | if rc != 0: 1245 | if verbose: 1246 | print("Directory %s not under git control" % root) 1247 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 1248 | 1249 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 1250 | # if there isn't one, this yields HEX[-dirty] (no NUM) 1251 | describe_out, rc = runner(GITS, [ 1252 | "describe", "--tags", "--dirty", "--always", "--long", 1253 | "--match", f"{tag_prefix}[[:digit:]]*" 1254 | ], cwd=root) 1255 | # --long was added in git-1.5.5 1256 | if describe_out is None: 1257 | raise NotThisMethod("'git describe' failed") 1258 | describe_out = describe_out.strip() 1259 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 1260 | if full_out is None: 1261 | raise NotThisMethod("'git rev-parse' failed") 1262 | full_out = full_out.strip() 1263 | 1264 | pieces = {} 1265 | pieces["long"] = full_out 1266 | pieces["short"] = full_out[:7] # maybe improved later 1267 | pieces["error"] = None 1268 | 1269 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], 1270 | cwd=root) 1271 | # --abbrev-ref was added in git-1.6.3 1272 | if rc != 0 or branch_name is None: 1273 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 1274 | branch_name = branch_name.strip() 1275 | 1276 | if branch_name == "HEAD": 1277 | # If we aren't exactly on a branch, pick a branch which represents 1278 | # the current commit. If all else fails, we are on a branchless 1279 | # commit. 1280 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 1281 | # --contains was added in git-1.5.4 1282 | if rc != 0 or branches is None: 1283 | raise NotThisMethod("'git branch --contains' returned error") 1284 | branches = branches.split("\n") 1285 | 1286 | # Remove the first line if we're running detached 1287 | if "(" in branches[0]: 1288 | branches.pop(0) 1289 | 1290 | # Strip off the leading "* " from the list of branches. 1291 | branches = [branch[2:] for branch in branches] 1292 | if "master" in branches: 1293 | branch_name = "master" 1294 | elif not branches: 1295 | branch_name = None 1296 | else: 1297 | # Pick the first branch that is returned. Good or bad. 1298 | branch_name = branches[0] 1299 | 1300 | pieces["branch"] = branch_name 1301 | 1302 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 1303 | # TAG might have hyphens. 1304 | git_describe = describe_out 1305 | 1306 | # look for -dirty suffix 1307 | dirty = git_describe.endswith("-dirty") 1308 | pieces["dirty"] = dirty 1309 | if dirty: 1310 | git_describe = git_describe[:git_describe.rindex("-dirty")] 1311 | 1312 | # now we have TAG-NUM-gHEX or HEX 1313 | 1314 | if "-" in git_describe: 1315 | # TAG-NUM-gHEX 1316 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 1317 | if not mo: 1318 | # unparsable. Maybe git-describe is misbehaving? 1319 | pieces["error"] = ("unable to parse git-describe output: '%s'" 1320 | % describe_out) 1321 | return pieces 1322 | 1323 | # tag 1324 | full_tag = mo.group(1) 1325 | if not full_tag.startswith(tag_prefix): 1326 | if verbose: 1327 | fmt = "tag '%s' doesn't start with prefix '%s'" 1328 | print(fmt % (full_tag, tag_prefix)) 1329 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 1330 | % (full_tag, tag_prefix)) 1331 | return pieces 1332 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 1333 | 1334 | # distance: number of commits since tag 1335 | pieces["distance"] = int(mo.group(2)) 1336 | 1337 | # commit: short hex revision ID 1338 | pieces["short"] = mo.group(3) 1339 | 1340 | else: 1341 | # HEX: no tags 1342 | pieces["closest-tag"] = None 1343 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 1344 | pieces["distance"] = len(out.split()) # total number of commits 1345 | 1346 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 1347 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 1348 | # Use only the last line. Previous lines may contain GPG signature 1349 | # information. 1350 | date = date.splitlines()[-1] 1351 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 1352 | 1353 | return pieces 1354 | 1355 | 1356 | def do_vcs_install(versionfile_source, ipy): 1357 | """Git-specific installation logic for Versioneer. 1358 | 1359 | For Git, this means creating/changing .gitattributes to mark _version.py 1360 | for export-subst keyword substitution. 1361 | """ 1362 | GITS = ["git"] 1363 | if sys.platform == "win32": 1364 | GITS = ["git.cmd", "git.exe"] 1365 | files = [versionfile_source] 1366 | if ipy: 1367 | files.append(ipy) 1368 | if "VERSIONEER_PEP518" not in globals(): 1369 | try: 1370 | my_path = __file__ 1371 | if my_path.endswith((".pyc", ".pyo")): 1372 | my_path = os.path.splitext(my_path)[0] + ".py" 1373 | versioneer_file = os.path.relpath(my_path) 1374 | except NameError: 1375 | versioneer_file = "versioneer.py" 1376 | files.append(versioneer_file) 1377 | present = False 1378 | try: 1379 | with open(".gitattributes", "r") as fobj: 1380 | for line in fobj: 1381 | if line.strip().startswith(versionfile_source): 1382 | if "export-subst" in line.strip().split()[1:]: 1383 | present = True 1384 | break 1385 | except OSError: 1386 | pass 1387 | if not present: 1388 | with open(".gitattributes", "a+") as fobj: 1389 | fobj.write(f"{versionfile_source} export-subst\n") 1390 | files.append(".gitattributes") 1391 | run_command(GITS, ["add", "--"] + files) 1392 | 1393 | 1394 | def versions_from_parentdir(parentdir_prefix, root, verbose): 1395 | """Try to determine the version from the parent directory name. 1396 | 1397 | Source tarballs conventionally unpack into a directory that includes both 1398 | the project name and a version string. We will also support searching up 1399 | two directory levels for an appropriately named parent directory 1400 | """ 1401 | rootdirs = [] 1402 | 1403 | for _ in range(3): 1404 | dirname = os.path.basename(root) 1405 | if dirname.startswith(parentdir_prefix): 1406 | return {"version": dirname[len(parentdir_prefix):], 1407 | "full-revisionid": None, 1408 | "dirty": False, "error": None, "date": None} 1409 | rootdirs.append(root) 1410 | root = os.path.dirname(root) # up a level 1411 | 1412 | if verbose: 1413 | print("Tried directories %s but none started with prefix %s" % 1414 | (str(rootdirs), parentdir_prefix)) 1415 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 1416 | 1417 | 1418 | SHORT_VERSION_PY = """ 1419 | # This file was generated by 'versioneer.py' (0.27) from 1420 | # revision-control system data, or from the parent directory name of an 1421 | # unpacked source archive. Distribution tarballs contain a pre-generated copy 1422 | # of this file. 1423 | 1424 | import json 1425 | 1426 | version_json = ''' 1427 | %s 1428 | ''' # END VERSION_JSON 1429 | 1430 | 1431 | def get_versions(): 1432 | return json.loads(version_json) 1433 | """ 1434 | 1435 | 1436 | def versions_from_file(filename): 1437 | """Try to determine the version from _version.py if present.""" 1438 | try: 1439 | with open(filename) as f: 1440 | contents = f.read() 1441 | except OSError: 1442 | raise NotThisMethod("unable to read _version.py") 1443 | mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", 1444 | contents, re.M | re.S) 1445 | if not mo: 1446 | mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", 1447 | contents, re.M | re.S) 1448 | if not mo: 1449 | raise NotThisMethod("no version_json in _version.py") 1450 | return json.loads(mo.group(1)) 1451 | 1452 | 1453 | def write_to_version_file(filename, versions): 1454 | """Write the given version number to the given _version.py file.""" 1455 | os.unlink(filename) 1456 | contents = json.dumps(versions, sort_keys=True, 1457 | indent=1, separators=(",", ": ")) 1458 | with open(filename, "w") as f: 1459 | f.write(SHORT_VERSION_PY % contents) 1460 | 1461 | print("set %s to '%s'" % (filename, versions["version"])) 1462 | 1463 | 1464 | def plus_or_dot(pieces): 1465 | """Return a + if we don't already have one, else return a .""" 1466 | if "+" in pieces.get("closest-tag", ""): 1467 | return "." 1468 | return "+" 1469 | 1470 | 1471 | def render_pep440(pieces): 1472 | """Build up version string, with post-release "local version identifier". 1473 | 1474 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 1475 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 1476 | 1477 | Exceptions: 1478 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 1479 | """ 1480 | if pieces["closest-tag"]: 1481 | rendered = pieces["closest-tag"] 1482 | if pieces["distance"] or pieces["dirty"]: 1483 | rendered += plus_or_dot(pieces) 1484 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1485 | if pieces["dirty"]: 1486 | rendered += ".dirty" 1487 | else: 1488 | # exception #1 1489 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 1490 | pieces["short"]) 1491 | if pieces["dirty"]: 1492 | rendered += ".dirty" 1493 | return rendered 1494 | 1495 | 1496 | def render_pep440_branch(pieces): 1497 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 1498 | 1499 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 1500 | (a feature branch will appear "older" than the master branch). 1501 | 1502 | Exceptions: 1503 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 1504 | """ 1505 | if pieces["closest-tag"]: 1506 | rendered = pieces["closest-tag"] 1507 | if pieces["distance"] or pieces["dirty"]: 1508 | if pieces["branch"] != "master": 1509 | rendered += ".dev0" 1510 | rendered += plus_or_dot(pieces) 1511 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 1512 | if pieces["dirty"]: 1513 | rendered += ".dirty" 1514 | else: 1515 | # exception #1 1516 | rendered = "0" 1517 | if pieces["branch"] != "master": 1518 | rendered += ".dev0" 1519 | rendered += "+untagged.%d.g%s" % (pieces["distance"], 1520 | pieces["short"]) 1521 | if pieces["dirty"]: 1522 | rendered += ".dirty" 1523 | return rendered 1524 | 1525 | 1526 | def pep440_split_post(ver): 1527 | """Split pep440 version string at the post-release segment. 1528 | 1529 | Returns the release segments before the post-release and the 1530 | post-release version number (or -1 if no post-release segment is present). 1531 | """ 1532 | vc = str.split(ver, ".post") 1533 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 1534 | 1535 | 1536 | def render_pep440_pre(pieces): 1537 | """TAG[.postN.devDISTANCE] -- No -dirty. 1538 | 1539 | Exceptions: 1540 | 1: no tags. 0.post0.devDISTANCE 1541 | """ 1542 | if pieces["closest-tag"]: 1543 | if pieces["distance"]: 1544 | # update the post release segment 1545 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 1546 | rendered = tag_version 1547 | if post_version is not None: 1548 | rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 1549 | else: 1550 | rendered += ".post0.dev%d" % (pieces["distance"]) 1551 | else: 1552 | # no commits, use the tag as the version 1553 | rendered = pieces["closest-tag"] 1554 | else: 1555 | # exception #1 1556 | rendered = "0.post0.dev%d" % pieces["distance"] 1557 | return rendered 1558 | 1559 | 1560 | def render_pep440_post(pieces): 1561 | """TAG[.postDISTANCE[.dev0]+gHEX] . 1562 | 1563 | The ".dev0" means dirty. Note that .dev0 sorts backwards 1564 | (a dirty tree will appear "older" than the corresponding clean one), 1565 | but you shouldn't be releasing software with -dirty anyways. 1566 | 1567 | Exceptions: 1568 | 1: no tags. 0.postDISTANCE[.dev0] 1569 | """ 1570 | if pieces["closest-tag"]: 1571 | rendered = pieces["closest-tag"] 1572 | if pieces["distance"] or pieces["dirty"]: 1573 | rendered += ".post%d" % pieces["distance"] 1574 | if pieces["dirty"]: 1575 | rendered += ".dev0" 1576 | rendered += plus_or_dot(pieces) 1577 | rendered += "g%s" % pieces["short"] 1578 | else: 1579 | # exception #1 1580 | rendered = "0.post%d" % pieces["distance"] 1581 | if pieces["dirty"]: 1582 | rendered += ".dev0" 1583 | rendered += "+g%s" % pieces["short"] 1584 | return rendered 1585 | 1586 | 1587 | def render_pep440_post_branch(pieces): 1588 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 1589 | 1590 | The ".dev0" means not master branch. 1591 | 1592 | Exceptions: 1593 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 1594 | """ 1595 | if pieces["closest-tag"]: 1596 | rendered = pieces["closest-tag"] 1597 | if pieces["distance"] or pieces["dirty"]: 1598 | rendered += ".post%d" % pieces["distance"] 1599 | if pieces["branch"] != "master": 1600 | rendered += ".dev0" 1601 | rendered += plus_or_dot(pieces) 1602 | rendered += "g%s" % pieces["short"] 1603 | if pieces["dirty"]: 1604 | rendered += ".dirty" 1605 | else: 1606 | # exception #1 1607 | rendered = "0.post%d" % pieces["distance"] 1608 | if pieces["branch"] != "master": 1609 | rendered += ".dev0" 1610 | rendered += "+g%s" % pieces["short"] 1611 | if pieces["dirty"]: 1612 | rendered += ".dirty" 1613 | return rendered 1614 | 1615 | 1616 | def render_pep440_old(pieces): 1617 | """TAG[.postDISTANCE[.dev0]] . 1618 | 1619 | The ".dev0" means dirty. 1620 | 1621 | Exceptions: 1622 | 1: no tags. 0.postDISTANCE[.dev0] 1623 | """ 1624 | if pieces["closest-tag"]: 1625 | rendered = pieces["closest-tag"] 1626 | if pieces["distance"] or pieces["dirty"]: 1627 | rendered += ".post%d" % pieces["distance"] 1628 | if pieces["dirty"]: 1629 | rendered += ".dev0" 1630 | else: 1631 | # exception #1 1632 | rendered = "0.post%d" % pieces["distance"] 1633 | if pieces["dirty"]: 1634 | rendered += ".dev0" 1635 | return rendered 1636 | 1637 | 1638 | def render_git_describe(pieces): 1639 | """TAG[-DISTANCE-gHEX][-dirty]. 1640 | 1641 | Like 'git describe --tags --dirty --always'. 1642 | 1643 | Exceptions: 1644 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1645 | """ 1646 | if pieces["closest-tag"]: 1647 | rendered = pieces["closest-tag"] 1648 | if pieces["distance"]: 1649 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1650 | else: 1651 | # exception #1 1652 | rendered = pieces["short"] 1653 | if pieces["dirty"]: 1654 | rendered += "-dirty" 1655 | return rendered 1656 | 1657 | 1658 | def render_git_describe_long(pieces): 1659 | """TAG-DISTANCE-gHEX[-dirty]. 1660 | 1661 | Like 'git describe --tags --dirty --always -long'. 1662 | The distance/hash is unconditional. 1663 | 1664 | Exceptions: 1665 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 1666 | """ 1667 | if pieces["closest-tag"]: 1668 | rendered = pieces["closest-tag"] 1669 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 1670 | else: 1671 | # exception #1 1672 | rendered = pieces["short"] 1673 | if pieces["dirty"]: 1674 | rendered += "-dirty" 1675 | return rendered 1676 | 1677 | 1678 | def render(pieces, style): 1679 | """Render the given version pieces into the requested style.""" 1680 | if pieces["error"]: 1681 | return {"version": "unknown", 1682 | "full-revisionid": pieces.get("long"), 1683 | "dirty": None, 1684 | "error": pieces["error"], 1685 | "date": None} 1686 | 1687 | if not style or style == "default": 1688 | style = "pep440" # the default 1689 | 1690 | if style == "pep440": 1691 | rendered = render_pep440(pieces) 1692 | elif style == "pep440-branch": 1693 | rendered = render_pep440_branch(pieces) 1694 | elif style == "pep440-pre": 1695 | rendered = render_pep440_pre(pieces) 1696 | elif style == "pep440-post": 1697 | rendered = render_pep440_post(pieces) 1698 | elif style == "pep440-post-branch": 1699 | rendered = render_pep440_post_branch(pieces) 1700 | elif style == "pep440-old": 1701 | rendered = render_pep440_old(pieces) 1702 | elif style == "git-describe": 1703 | rendered = render_git_describe(pieces) 1704 | elif style == "git-describe-long": 1705 | rendered = render_git_describe_long(pieces) 1706 | else: 1707 | raise ValueError("unknown style '%s'" % style) 1708 | 1709 | return {"version": rendered, "full-revisionid": pieces["long"], 1710 | "dirty": pieces["dirty"], "error": None, 1711 | "date": pieces.get("date")} 1712 | 1713 | 1714 | class VersioneerBadRootError(Exception): 1715 | """The project root directory is unknown or missing key files.""" 1716 | 1717 | 1718 | def get_versions(verbose=False): 1719 | """Get the project version from whatever source is available. 1720 | 1721 | Returns dict with two keys: 'version' and 'full'. 1722 | """ 1723 | if "versioneer" in sys.modules: 1724 | # see the discussion in cmdclass.py:get_cmdclass() 1725 | del sys.modules["versioneer"] 1726 | 1727 | root = get_root() 1728 | cfg = get_config_from_root(root) 1729 | 1730 | assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" 1731 | handlers = HANDLERS.get(cfg.VCS) 1732 | assert handlers, "unrecognized VCS '%s'" % cfg.VCS 1733 | verbose = verbose or cfg.verbose 1734 | assert cfg.versionfile_source is not None, \ 1735 | "please set versioneer.versionfile_source" 1736 | assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" 1737 | 1738 | versionfile_abs = os.path.join(root, cfg.versionfile_source) 1739 | 1740 | # extract version from first of: _version.py, VCS command (e.g. 'git 1741 | # describe'), parentdir. This is meant to work for developers using a 1742 | # source checkout, for users of a tarball created by 'setup.py sdist', 1743 | # and for users of a tarball/zipball created by 'git archive' or github's 1744 | # download-from-tag feature or the equivalent in other VCSes. 1745 | 1746 | get_keywords_f = handlers.get("get_keywords") 1747 | from_keywords_f = handlers.get("keywords") 1748 | if get_keywords_f and from_keywords_f: 1749 | try: 1750 | keywords = get_keywords_f(versionfile_abs) 1751 | ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) 1752 | if verbose: 1753 | print("got version from expanded keyword %s" % ver) 1754 | return ver 1755 | except NotThisMethod: 1756 | pass 1757 | 1758 | try: 1759 | ver = versions_from_file(versionfile_abs) 1760 | if verbose: 1761 | print("got version from file %s %s" % (versionfile_abs, ver)) 1762 | return ver 1763 | except NotThisMethod: 1764 | pass 1765 | 1766 | from_vcs_f = handlers.get("pieces_from_vcs") 1767 | if from_vcs_f: 1768 | try: 1769 | pieces = from_vcs_f(cfg.tag_prefix, root, verbose) 1770 | ver = render(pieces, cfg.style) 1771 | if verbose: 1772 | print("got version from VCS %s" % ver) 1773 | return ver 1774 | except NotThisMethod: 1775 | pass 1776 | 1777 | try: 1778 | if cfg.parentdir_prefix: 1779 | ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 1780 | if verbose: 1781 | print("got version from parentdir %s" % ver) 1782 | return ver 1783 | except NotThisMethod: 1784 | pass 1785 | 1786 | if verbose: 1787 | print("unable to compute version") 1788 | 1789 | return {"version": "0+unknown", "full-revisionid": None, 1790 | "dirty": None, "error": "unable to compute version", 1791 | "date": None} 1792 | 1793 | 1794 | def get_version(): 1795 | """Get the short version string for this project.""" 1796 | return get_versions()["version"] 1797 | 1798 | 1799 | def get_cmdclass(cmdclass=None): 1800 | """Get the custom setuptools subclasses used by Versioneer. 1801 | 1802 | If the package uses a different cmdclass (e.g. one from numpy), it 1803 | should be provide as an argument. 1804 | """ 1805 | if "versioneer" in sys.modules: 1806 | del sys.modules["versioneer"] 1807 | # this fixes the "python setup.py develop" case (also 'install' and 1808 | # 'easy_install .'), in which subdependencies of the main project are 1809 | # built (using setup.py bdist_egg) in the same python process. Assume 1810 | # a main project A and a dependency B, which use different versions 1811 | # of Versioneer. A's setup.py imports A's Versioneer, leaving it in 1812 | # sys.modules by the time B's setup.py is executed, causing B to run 1813 | # with the wrong versioneer. Setuptools wraps the sub-dep builds in a 1814 | # sandbox that restores sys.modules to it's pre-build state, so the 1815 | # parent is protected against the child's "import versioneer". By 1816 | # removing ourselves from sys.modules here, before the child build 1817 | # happens, we protect the child from the parent's versioneer too. 1818 | # Also see https://github.com/python-versioneer/python-versioneer/issues/52 1819 | 1820 | cmds = {} if cmdclass is None else cmdclass.copy() 1821 | 1822 | # we add "version" to setuptools 1823 | from setuptools import Command 1824 | 1825 | class cmd_version(Command): 1826 | description = "report generated version string" 1827 | user_options = [] 1828 | boolean_options = [] 1829 | 1830 | def initialize_options(self): 1831 | pass 1832 | 1833 | def finalize_options(self): 1834 | pass 1835 | 1836 | def run(self): 1837 | vers = get_versions(verbose=True) 1838 | print("Version: %s" % vers["version"]) 1839 | print(" full-revisionid: %s" % vers.get("full-revisionid")) 1840 | print(" dirty: %s" % vers.get("dirty")) 1841 | print(" date: %s" % vers.get("date")) 1842 | if vers["error"]: 1843 | print(" error: %s" % vers["error"]) 1844 | cmds["version"] = cmd_version 1845 | 1846 | # we override "build_py" in setuptools 1847 | # 1848 | # most invocation pathways end up running build_py: 1849 | # distutils/build -> build_py 1850 | # distutils/install -> distutils/build ->.. 1851 | # setuptools/bdist_wheel -> distutils/install ->.. 1852 | # setuptools/bdist_egg -> distutils/install_lib -> build_py 1853 | # setuptools/install -> bdist_egg ->.. 1854 | # setuptools/develop -> ? 1855 | # pip install: 1856 | # copies source tree to a tempdir before running egg_info/etc 1857 | # if .git isn't copied too, 'git describe' will fail 1858 | # then does setup.py bdist_wheel, or sometimes setup.py install 1859 | # setup.py egg_info -> ? 1860 | 1861 | # pip install -e . and setuptool/editable_wheel will invoke build_py 1862 | # but the build_py command is not expected to copy any files. 1863 | 1864 | # we override different "build_py" commands for both environments 1865 | if 'build_py' in cmds: 1866 | _build_py = cmds['build_py'] 1867 | else: 1868 | from setuptools.command.build_py import build_py as _build_py 1869 | 1870 | class cmd_build_py(_build_py): 1871 | def run(self): 1872 | root = get_root() 1873 | cfg = get_config_from_root(root) 1874 | versions = get_versions() 1875 | _build_py.run(self) 1876 | if getattr(self, "editable_mode", False): 1877 | # During editable installs `.py` and data files are 1878 | # not copied to build_lib 1879 | return 1880 | # now locate _version.py in the new build/ directory and replace 1881 | # it with an updated value 1882 | if cfg.versionfile_build: 1883 | target_versionfile = os.path.join(self.build_lib, 1884 | cfg.versionfile_build) 1885 | print("UPDATING %s" % target_versionfile) 1886 | write_to_version_file(target_versionfile, versions) 1887 | cmds["build_py"] = cmd_build_py 1888 | 1889 | if 'build_ext' in cmds: 1890 | _build_ext = cmds['build_ext'] 1891 | else: 1892 | from setuptools.command.build_ext import build_ext as _build_ext 1893 | 1894 | class cmd_build_ext(_build_ext): 1895 | def run(self): 1896 | root = get_root() 1897 | cfg = get_config_from_root(root) 1898 | versions = get_versions() 1899 | _build_ext.run(self) 1900 | if self.inplace: 1901 | # build_ext --inplace will only build extensions in 1902 | # build/lib<..> dir with no _version.py to write to. 1903 | # As in place builds will already have a _version.py 1904 | # in the module dir, we do not need to write one. 1905 | return 1906 | # now locate _version.py in the new build/ directory and replace 1907 | # it with an updated value 1908 | target_versionfile = os.path.join(self.build_lib, 1909 | cfg.versionfile_build) 1910 | if not os.path.exists(target_versionfile): 1911 | print(f"Warning: {target_versionfile} does not exist, skipping " 1912 | "version update. This can happen if you are running build_ext " 1913 | "without first running build_py.") 1914 | return 1915 | print("UPDATING %s" % target_versionfile) 1916 | write_to_version_file(target_versionfile, versions) 1917 | cmds["build_ext"] = cmd_build_ext 1918 | 1919 | if "cx_Freeze" in sys.modules: # cx_freeze enabled? 1920 | from cx_Freeze.dist import build_exe as _build_exe 1921 | # nczeczulin reports that py2exe won't like the pep440-style string 1922 | # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. 1923 | # setup(console=[{ 1924 | # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION 1925 | # "product_version": versioneer.get_version(), 1926 | # ... 1927 | 1928 | class cmd_build_exe(_build_exe): 1929 | def run(self): 1930 | root = get_root() 1931 | cfg = get_config_from_root(root) 1932 | versions = get_versions() 1933 | target_versionfile = cfg.versionfile_source 1934 | print("UPDATING %s" % target_versionfile) 1935 | write_to_version_file(target_versionfile, versions) 1936 | 1937 | _build_exe.run(self) 1938 | os.unlink(target_versionfile) 1939 | with open(cfg.versionfile_source, "w") as f: 1940 | LONG = LONG_VERSION_PY[cfg.VCS] 1941 | f.write(LONG % 1942 | {"DOLLAR": "$", 1943 | "STYLE": cfg.style, 1944 | "TAG_PREFIX": cfg.tag_prefix, 1945 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1946 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1947 | }) 1948 | cmds["build_exe"] = cmd_build_exe 1949 | del cmds["build_py"] 1950 | 1951 | if 'py2exe' in sys.modules: # py2exe enabled? 1952 | try: 1953 | from py2exe.setuptools_buildexe import py2exe as _py2exe 1954 | except ImportError: 1955 | from py2exe.distutils_buildexe import py2exe as _py2exe 1956 | 1957 | class cmd_py2exe(_py2exe): 1958 | def run(self): 1959 | root = get_root() 1960 | cfg = get_config_from_root(root) 1961 | versions = get_versions() 1962 | target_versionfile = cfg.versionfile_source 1963 | print("UPDATING %s" % target_versionfile) 1964 | write_to_version_file(target_versionfile, versions) 1965 | 1966 | _py2exe.run(self) 1967 | os.unlink(target_versionfile) 1968 | with open(cfg.versionfile_source, "w") as f: 1969 | LONG = LONG_VERSION_PY[cfg.VCS] 1970 | f.write(LONG % 1971 | {"DOLLAR": "$", 1972 | "STYLE": cfg.style, 1973 | "TAG_PREFIX": cfg.tag_prefix, 1974 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 1975 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 1976 | }) 1977 | cmds["py2exe"] = cmd_py2exe 1978 | 1979 | # sdist farms its file list building out to egg_info 1980 | if 'egg_info' in cmds: 1981 | _egg_info = cmds['egg_info'] 1982 | else: 1983 | from setuptools.command.egg_info import egg_info as _egg_info 1984 | 1985 | class cmd_egg_info(_egg_info): 1986 | def find_sources(self): 1987 | # egg_info.find_sources builds the manifest list and writes it 1988 | # in one shot 1989 | super().find_sources() 1990 | 1991 | # Modify the filelist and normalize it 1992 | root = get_root() 1993 | cfg = get_config_from_root(root) 1994 | self.filelist.append('versioneer.py') 1995 | if cfg.versionfile_source: 1996 | # There are rare cases where versionfile_source might not be 1997 | # included by default, so we must be explicit 1998 | self.filelist.append(cfg.versionfile_source) 1999 | self.filelist.sort() 2000 | self.filelist.remove_duplicates() 2001 | 2002 | # The write method is hidden in the manifest_maker instance that 2003 | # generated the filelist and was thrown away 2004 | # We will instead replicate their final normalization (to unicode, 2005 | # and POSIX-style paths) 2006 | from setuptools import unicode_utils 2007 | normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') 2008 | for f in self.filelist.files] 2009 | 2010 | manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') 2011 | with open(manifest_filename, 'w') as fobj: 2012 | fobj.write('\n'.join(normalized)) 2013 | 2014 | cmds['egg_info'] = cmd_egg_info 2015 | 2016 | # we override different "sdist" commands for both environments 2017 | if 'sdist' in cmds: 2018 | _sdist = cmds['sdist'] 2019 | else: 2020 | from setuptools.command.sdist import sdist as _sdist 2021 | 2022 | class cmd_sdist(_sdist): 2023 | def run(self): 2024 | versions = get_versions() 2025 | self._versioneer_generated_versions = versions 2026 | # unless we update this, the command will keep using the old 2027 | # version 2028 | self.distribution.metadata.version = versions["version"] 2029 | return _sdist.run(self) 2030 | 2031 | def make_release_tree(self, base_dir, files): 2032 | root = get_root() 2033 | cfg = get_config_from_root(root) 2034 | _sdist.make_release_tree(self, base_dir, files) 2035 | # now locate _version.py in the new base_dir directory 2036 | # (remembering that it may be a hardlink) and replace it with an 2037 | # updated value 2038 | target_versionfile = os.path.join(base_dir, cfg.versionfile_source) 2039 | print("UPDATING %s" % target_versionfile) 2040 | write_to_version_file(target_versionfile, 2041 | self._versioneer_generated_versions) 2042 | cmds["sdist"] = cmd_sdist 2043 | 2044 | return cmds 2045 | 2046 | 2047 | CONFIG_ERROR = """ 2048 | setup.cfg is missing the necessary Versioneer configuration. You need 2049 | a section like: 2050 | 2051 | [versioneer] 2052 | VCS = git 2053 | style = pep440 2054 | versionfile_source = src/myproject/_version.py 2055 | versionfile_build = myproject/_version.py 2056 | tag_prefix = 2057 | parentdir_prefix = myproject- 2058 | 2059 | You will also need to edit your setup.py to use the results: 2060 | 2061 | import versioneer 2062 | setup(version=versioneer.get_version(), 2063 | cmdclass=versioneer.get_cmdclass(), ...) 2064 | 2065 | Please read the docstring in ./versioneer.py for configuration instructions, 2066 | edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. 2067 | """ 2068 | 2069 | SAMPLE_CONFIG = """ 2070 | # See the docstring in versioneer.py for instructions. Note that you must 2071 | # re-run 'versioneer.py setup' after changing this section, and commit the 2072 | # resulting files. 2073 | 2074 | [versioneer] 2075 | #VCS = git 2076 | #style = pep440 2077 | #versionfile_source = 2078 | #versionfile_build = 2079 | #tag_prefix = 2080 | #parentdir_prefix = 2081 | 2082 | """ 2083 | 2084 | OLD_SNIPPET = """ 2085 | from ._version import get_versions 2086 | __version__ = get_versions()['version'] 2087 | del get_versions 2088 | """ 2089 | 2090 | INIT_PY_SNIPPET = """ 2091 | from . import {0} 2092 | __version__ = {0}.get_versions()['version'] 2093 | """ 2094 | 2095 | 2096 | def do_setup(): 2097 | """Do main VCS-independent setup function for installing Versioneer.""" 2098 | root = get_root() 2099 | try: 2100 | cfg = get_config_from_root(root) 2101 | except (OSError, configparser.NoSectionError, 2102 | configparser.NoOptionError) as e: 2103 | if isinstance(e, (OSError, configparser.NoSectionError)): 2104 | print("Adding sample versioneer config to setup.cfg", 2105 | file=sys.stderr) 2106 | with open(os.path.join(root, "setup.cfg"), "a") as f: 2107 | f.write(SAMPLE_CONFIG) 2108 | print(CONFIG_ERROR, file=sys.stderr) 2109 | return 1 2110 | 2111 | print(" creating %s" % cfg.versionfile_source) 2112 | with open(cfg.versionfile_source, "w") as f: 2113 | LONG = LONG_VERSION_PY[cfg.VCS] 2114 | f.write(LONG % {"DOLLAR": "$", 2115 | "STYLE": cfg.style, 2116 | "TAG_PREFIX": cfg.tag_prefix, 2117 | "PARENTDIR_PREFIX": cfg.parentdir_prefix, 2118 | "VERSIONFILE_SOURCE": cfg.versionfile_source, 2119 | }) 2120 | 2121 | ipy = os.path.join(os.path.dirname(cfg.versionfile_source), 2122 | "__init__.py") 2123 | if os.path.exists(ipy): 2124 | try: 2125 | with open(ipy, "r") as f: 2126 | old = f.read() 2127 | except OSError: 2128 | old = "" 2129 | module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] 2130 | snippet = INIT_PY_SNIPPET.format(module) 2131 | if OLD_SNIPPET in old: 2132 | print(" replacing boilerplate in %s" % ipy) 2133 | with open(ipy, "w") as f: 2134 | f.write(old.replace(OLD_SNIPPET, snippet)) 2135 | elif snippet not in old: 2136 | print(" appending to %s" % ipy) 2137 | with open(ipy, "a") as f: 2138 | f.write(snippet) 2139 | else: 2140 | print(" %s unmodified" % ipy) 2141 | else: 2142 | print(" %s doesn't exist, ok" % ipy) 2143 | ipy = None 2144 | 2145 | # Make VCS-specific changes. For git, this means creating/changing 2146 | # .gitattributes to mark _version.py for export-subst keyword 2147 | # substitution. 2148 | do_vcs_install(cfg.versionfile_source, ipy) 2149 | return 0 2150 | 2151 | 2152 | def scan_setup_py(): 2153 | """Validate the contents of setup.py against Versioneer's expectations.""" 2154 | found = set() 2155 | setters = False 2156 | errors = 0 2157 | with open("setup.py", "r") as f: 2158 | for line in f.readlines(): 2159 | if "import versioneer" in line: 2160 | found.add("import") 2161 | if "versioneer.get_cmdclass()" in line: 2162 | found.add("cmdclass") 2163 | if "versioneer.get_version()" in line: 2164 | found.add("get_version") 2165 | if "versioneer.VCS" in line: 2166 | setters = True 2167 | if "versioneer.versionfile_source" in line: 2168 | setters = True 2169 | if len(found) != 3: 2170 | print("") 2171 | print("Your setup.py appears to be missing some important items") 2172 | print("(but I might be wrong). Please make sure it has something") 2173 | print("roughly like the following:") 2174 | print("") 2175 | print(" import versioneer") 2176 | print(" setup( version=versioneer.get_version(),") 2177 | print(" cmdclass=versioneer.get_cmdclass(), ...)") 2178 | print("") 2179 | errors += 1 2180 | if setters: 2181 | print("You should remove lines like 'versioneer.VCS = ' and") 2182 | print("'versioneer.versionfile_source = ' . This configuration") 2183 | print("now lives in setup.cfg, and should be removed from setup.py") 2184 | print("") 2185 | errors += 1 2186 | return errors 2187 | 2188 | 2189 | def setup_command(): 2190 | """Set up Versioneer and exit with appropriate error code.""" 2191 | errors = do_setup() 2192 | errors += scan_setup_py() 2193 | sys.exit(1 if errors else 0) 2194 | 2195 | 2196 | if __name__ == "__main__": 2197 | cmd = sys.argv[1] 2198 | if cmd == "setup": 2199 | setup_command() 2200 | --------------------------------------------------------------------------------