├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── miniver ├── __init__.py ├── _static_version.py ├── _version.py └── app.py ├── unannotated-tags.patch ├── CHANGELOG.md ├── RELEASE.md ├── ci ├── test_package.sh └── create_package.py ├── .github └── workflows │ └── test.yml ├── setup.py ├── README.md └── LICENSE /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | build 3 | dist 4 | **/*/__pycache__ 5 | *.pyc 6 | -------------------------------------------------------------------------------- /miniver/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["__version__"] 2 | from ._version import __version__ 3 | 4 | del _version # remove to avoid confusion with __version__ 5 | -------------------------------------------------------------------------------- /miniver/_static_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of 'miniver': https://github.com/jbweston/miniver 3 | # 4 | # This file will be overwritten by setup.py when a source or binary 5 | # distribution is made. The magic value "__use_git__" is interpreted by 6 | # _version.py. 7 | 8 | version = "__use_git__" 9 | 10 | # These values are only set if the distribution was created with 'git archive' 11 | refnames = "$Format:%D$" 12 | git_hash = "$Format:%h$" 13 | -------------------------------------------------------------------------------- /unannotated-tags.patch: -------------------------------------------------------------------------------- 1 | Apply this patch to "_version.py" to get Miniver to calculate the 2 | version using unannotated tags in addition to annotated tags. 3 | @@ -68,7 +68,7 @@ def get_version_from_git(): 4 | for opts in [["--first-parent"], []]: 5 | try: 6 | p = subprocess.Popen( 7 | - ["git", "describe", "--long", "--always"] + opts, 8 | + ["git", "describe", "--long", "--always", "--tags"] + opts, 9 | cwd=package_root, 10 | stdout=subprocess.PIPE, 11 | stderr=subprocess.PIPE, 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to miniver will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [unreleased] 8 | 9 | ## [0.8.0] - 2021-10-13 10 | ### Added 11 | - Add a 'ver' command that prints the detected version 12 | - Running 'miniver' without any arguments invokes the 'ver' command 13 | - Miniver now works with namespace packages 14 | 15 | ## [0.7.0] - 2020-08-15 16 | ### Added 17 | - Allow distributions that place packages in a "src" directory 18 | ### Changed 19 | - Replace tool "install-miniver" with a tool "miniver" with a command "install" 20 | ### Fixed 21 | - Use "build_py" from setuptools, rather than distutils, which prevents a warning 22 | being displayed when using more recent setuptools versions 23 | 24 | ## [0.6.0] - 2019-02-17 25 | ### Fixed 26 | - Typos in generated files (comments only) 27 | - Dedented template code produced by 'install-miniver' to make it copy-pasteable. 28 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a miniver release 2 | These instructions can also be used as a starting point for packages that use miniver. 3 | 4 | ## Preflight checks 5 | 6 | 1. Verify that all issues/pull requests pertinent for this release are closed/merged 7 | 2. Verify that all changes are recorded in the changelog 8 | 3. Verify that any attribution files (e.g AUTHORS) are up to date 9 | 4. Verify that copyright notices are up to date 10 | 5. Verify that the builds are passing on `master` 11 | 12 | ## Prepare the release 13 | 14 | 1. Restore the git repository to a pristine state: `git checkout master && git reset --hard HEAD && git clean -xd -f` 15 | 2. Create a *signed*, *annotated* release tag: `git tag -as vX.Y.Z -m 'version X.Y.Z'` 16 | 3. Create source and binary distributions: `python setup.py sdist bdist_wheel` 17 | 4. Create an empty commit to start development towards the next release: `git commit --allow-empty -m 'start development towards A.B.C'` 18 | 5. Create a *signed*, *annotated* pre-release tag: `git tag -as vA.B.C-dev -m 'work towards A.B.C'` 19 | 20 | ## Publish the release 21 | 1. Push the new version and development tags: `git push upstream vX.Y.Z vA.B.C-dev` 22 | 2. Upload the distributions to PyPI: `twine upload dist/*` 23 | -------------------------------------------------------------------------------- /ci/test_package.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | distr=$1 4 | pkg=$2 5 | 6 | function test_version() { 7 | echo "Testing: $pkg.__version__$1" 8 | python -c "import $pkg; assert $pkg.__version__$1" 9 | } 10 | 11 | # "./" to ensure we don't pull from PyPI 12 | pip install -e ./$distr 13 | 14 | test_version "== 'unknown'" 15 | 16 | pushd $distr 17 | git add . 18 | git commit -m "First commit" 19 | git tag -a 0.0.0 -m "0.0.0" 20 | echo "Tagged 0.0.0" 21 | popd 22 | 23 | test_version "== '0.0.0'" 24 | 25 | pushd $distr 26 | echo "# Extra comment" >> setup.py 27 | echo "Modified working directory" 28 | popd 29 | 30 | test_version ".startswith('0.0.0')" 31 | test_version ".endswith('dirty')" 32 | 33 | # Test staged modifications result in a dirty tree 34 | pushd $distr 35 | git add . 36 | echo "Staged modifications" 37 | popd 38 | 39 | test_version ".startswith('0.0.0')" 40 | test_version ".endswith('dirty')" 41 | 42 | pushd $distr 43 | git commit -a -m "new comment" 44 | echo "Committed changes" 45 | popd 46 | 47 | test_version ".startswith('0.0.0.dev1')" 48 | 49 | pushd $distr 50 | git tag -a 0.0.1 -m "0.0.1" 51 | echo "Tagged 0.0.1" 52 | popd 53 | 54 | test_version "== '0.0.1'" 55 | 56 | # Now test against "real" (non-editable) installations 57 | pip uninstall -y $distr 58 | 59 | pushd $distr 60 | git commit --allow-empty -m 'next commit' 61 | git tag -a 0.0.2 -m "0.0.2" 62 | echo "Tagged 0.0.2" 63 | popd 64 | 65 | # First a source distribution 66 | 67 | echo "Testing setup.py sdist" 68 | pushd $distr 69 | python setup.py sdist 70 | pip install dist/*.tar.gz 71 | popd 72 | 73 | test_version "== '0.0.2'" 74 | 75 | pip uninstall -y $distr 76 | 77 | pushd $distr 78 | git commit --allow-empty -m 'final commit' 79 | git tag -a 0.0.3 -m "0.0.3" 80 | echo "Tagged 0.0.3" 81 | popd 82 | 83 | # Then a wheel distribution 84 | 85 | echo "Testing setup.py bdist_wheel" 86 | pushd $distr 87 | python setup.py bdist_wheel 88 | pip install dist/*.whl 89 | popd 90 | 91 | test_version "== '0.0.3'" 92 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | max-parallel: 4 10 | matrix: 11 | operating-system: [ubuntu-latest, macos-latest, windows-latest] 12 | python-version: [3.5, 3.6, 3.7, 3.8] 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Set up Git and Pip 19 | run: | 20 | git config --global user.email "you@example.com" 21 | git config --global user.name "Your Name" 22 | python -m pip install --upgrade pip 23 | python -m pip install wheel 24 | git --version 25 | python --version 26 | - name: Install miniver 27 | run: | 28 | pip install . 29 | - name: Set up minimal python packages 30 | run: | 31 | cd .. 32 | # simple package 33 | python miniver/ci/create_package.py simple-distr simple_pkg 34 | # simple package in 'src' layout 35 | python miniver/ci/create_package.py simple-src-distr simple_src_pkg --src-layout 36 | # namespace package 37 | python miniver/ci/create_package.py ns-distr nspkg.simple_pkg 38 | # namespace package in 'src' layout 39 | python miniver/ci/create_package.py ns-src-distr nspkg.simple_src_pkg --src-layout 40 | - name: Test versioning of simple package 41 | shell: bash 42 | run: cd .. && miniver/ci/test_package.sh simple-distr simple_pkg 43 | - name: Test versioning of simple src-layout package 44 | shell: bash 45 | run: cd .. && miniver/ci/test_package.sh simple-src-distr simple_src_pkg 46 | - name: Test versioning of namespace package 47 | shell: bash 48 | run: cd .. && miniver/ci/test_package.sh ns-distr nspkg.simple_pkg 49 | - name: Test versioning of namespace src-layout package 50 | shell: bash 51 | run: cd .. && miniver/ci/test_package.sh ns-src-distr nspkg.simple_src_pkg 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | import sys 5 | 6 | if sys.version_info < (3, 5): 7 | print("Miniver needs at least Python 3.5.") 8 | sys.exit(1) 9 | 10 | 11 | # Loads version.py module without importing the whole package. 12 | def get_version_and_cmdclass(pkg_path): 13 | import os 14 | from importlib.util import module_from_spec, spec_from_file_location 15 | 16 | spec = spec_from_file_location("version", os.path.join(pkg_path, "_version.py")) 17 | module = module_from_spec(spec) 18 | spec.loader.exec_module(module) 19 | return module.__version__, module.get_cmdclass(pkg_path) 20 | 21 | 22 | version, cmdclass = get_version_and_cmdclass("miniver") 23 | 24 | with open("README.md") as readme_file: 25 | long_description = readme_file.read() 26 | 27 | setup( 28 | name="miniver", 29 | description="minimal versioning tool", 30 | long_description=long_description, 31 | long_description_content_type="text/markdown", 32 | version=version, 33 | url="https://github.com/jbweston/miniver", 34 | author="Joseph Weston and Christoph Groth", 35 | author_email="joseph@weston.cloud", 36 | license="CC0", 37 | classifiers=[ 38 | "Development Status :: 4 - Beta", 39 | "License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication", 40 | "Topic :: Software Development :: Version Control :: Git", 41 | "Intended Audience :: Developers", 42 | "Programming Language :: Python :: 3 :: Only", 43 | "Programming Language :: Python :: 3.5", 44 | "Programming Language :: Python :: 3.6", 45 | "Programming Language :: Python :: 3.7", 46 | "Programming Language :: Python :: 3.8", 47 | "Operating System :: POSIX :: Linux", 48 | "Operating System :: MacOS :: MacOS X", 49 | "Operating System :: Microsoft :: Windows", 50 | ], 51 | packages=find_packages("."), 52 | cmdclass=cmdclass, 53 | entry_points={ 54 | "console_scripts": [ 55 | "miniver=miniver.app:main", 56 | ] 57 | }, 58 | ) 59 | -------------------------------------------------------------------------------- /ci/create_package.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from contextlib import contextmanager 3 | from functools import partial 4 | import os.path 5 | from os import chdir, makedirs 6 | from shutil import rmtree 7 | from subprocess import run, PIPE, CalledProcessError 8 | from sys import exit, stderr 9 | from textwrap import indent 10 | 11 | 12 | @contextmanager 13 | def log(msg, where=stderr): 14 | pp = partial(print, file=where) 15 | pp("{}...".format(msg), end="") 16 | try: 17 | yield 18 | except KeyboardInterrupt: 19 | pp("INTERRUPTED") 20 | exit(2) 21 | except CalledProcessError as e: 22 | pp("FAILED") 23 | pp("Subprocess '{}' failed with exit code {}".format(e.cmd, e.returncode)) 24 | if e.stdout: 25 | pp("---- stdout ----") 26 | pp(e.stdout.decode()) 27 | pp("----------------") 28 | if e.stderr: 29 | pp("---- stderr ----") 30 | pp(e.stderr.decode()) 31 | pp("----------------") 32 | exit(e.returncode) 33 | except Exception as e: 34 | print("FAILED", file=where) 35 | print(str(e), file=where) 36 | exit(1) 37 | else: 38 | print("OK", file=where) 39 | 40 | 41 | def main(): 42 | parser = argparse.ArgumentParser() 43 | parser.add_argument("distribution", help="Distribution package name") 44 | parser.add_argument("package", help="Dotted package name") 45 | parser.add_argument("--src-layout", action="store_true") 46 | args = parser.parse_args() 47 | 48 | distr, pkg, src_pkg = (getattr(args, x) for x in ("distribution", "package", "src_layout")) 49 | 50 | path = os.path.join("src" if src_pkg else "", pkg.replace(".", os.path.sep)) 51 | 52 | with log("Ensuring '{}' is removed".format(distr)): 53 | rmtree(args.distribution, ignore_errors=True) 54 | 55 | with log("Initializing git repository in '{}'".format(distr)): 56 | run("git init {}".format(distr), shell=True, check=True, stdout=PIPE, stderr=PIPE) 57 | chdir(distr) 58 | makedirs(path) 59 | 60 | with log("Installing miniver in '{}'".format(os.path.join(distr, path))): 61 | r = run( 62 | "miniver install {}".format(path), 63 | shell=True, 64 | check=True, 65 | stdout=PIPE, 66 | stderr=PIPE, 67 | ) 68 | setup_template = r.stdout.decode("utf8") 69 | 70 | with log("Writing gitignore"): 71 | with open(".gitignore", "w") as f: 72 | f.write("\n".join([ 73 | "dist", 74 | "build", 75 | "__pycache__", 76 | "*.egg-info", 77 | ])) 78 | 79 | with log("Writing setup.py"): 80 | lines = [ 81 | "name='{}',".format(distr), 82 | "packages=['{}'],".format(pkg), 83 | ] 84 | if src_pkg: 85 | lines.append("package_dir={'': 'src'},") 86 | replacement = indent("\n".join(lines), " ") 87 | 88 | with open("setup.py", "w") as f: 89 | # This is tightly coupled to the setup.py template: there is a 90 | # call to 'setup()' with an ellipsis on a single line. 91 | f.write(setup_template.replace(" ...,", replacement)) 92 | 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miniver 2 | [![license: CC0-1.0](https://img.shields.io/pypi/l/miniver.svg)][cc0] 3 | [![PyPI version](https://img.shields.io/pypi/v/miniver.svg)][pypi] 4 | [![CI status](https://github.com/jbweston/miniver/workflows/test/badge.svg)][ci] 5 | 6 | 7 | **Like [versioneer][versioneer], but smaller** 8 | 9 | Miniver is a **mini**mal **ver**sioning tool that serves the same purpose 10 | as [Versioneer][versioneer], except that it only works with Git and 11 | multiplatform support is still experimental. 12 | 13 | #### Why would I use this? 14 | If you are developing a Python package inside a Git repository and 15 | want to get the version directly from Git tags, rather than hard-coding 16 | version strings everywhere. 17 | 18 | This is the same problem that Versioneer solves, but Miniver is less 19 | than 200 lines of code, whereas Versioneer is over 2000. The tradeoff 20 | is that Miniver only works with Git and Python 3.5 (or above). 21 | 22 | Support for Python 2 is not a goal, as Python 2 is fast approaching its 23 | end of life (2020), and we want to encourage people to use Python 3! 24 | That being said, Christian Marquardt has a [fork that also 25 | works with Python 2](https://github.com/cmarquardt/miniver2) 26 | 27 | [versioneer]: https://github.com/warner/python-versioneer 28 | [cc0]: http://creativecommons.org/publicdomain/zero/1.0/ 29 | [pypi]: https://pypi.org/project/miniver/ 30 | [ci]: https://github.com/jbweston/miniver/actions?query=workflow%3Atest 31 | 32 | ## Usage 33 | The simplest way to use Miniver is to run the following in your project root: 34 | ``` 35 | curl https://raw.githubusercontent.com/jbweston/miniver/master/miniver/app.py | python - install 36 | ``` 37 | This will grab the latest files from GitHub and set up Miniver for your project. 38 | 39 | ### I get an `unknown` version! 40 | The version is reported as `unknown` (plus the current git hash) when there are no valid tags 41 | in the git history. You should create an [*annotated tag*](https://git-scm.com/book/en/v2/Git-Basics-Tagging) 42 | so that Miniver reports a reasonable version. 43 | 44 | If your project uses *unannotated tags* for versioning (though this is not the 45 | [recommended way](https://stackoverflow.com/questions/11514075/what-is-the-difference-between-an-annotated-and-unannotated-tag)) 46 | then you'll need to run the following in order to modify Miniver's behaviour: 47 | ``` 48 | curl https://raw.githubusercontent.com/jbweston/miniver/master/unannotated-tags.patch | patch /_version.py 49 | ``` 50 | 51 | ### I don't want to type that URL every time I use this 52 | You can `pip install miniver`, which will give you the `miniver` command. 53 | Then you can simply run the following from your project root to use Miniver: 54 | ``` 55 | miniver install 56 | ``` 57 | 58 | ### Can I use this without executing random code from the internet? 59 | Sure! Copy `miniver/_version.py` and `miniver/_static_version.py` from this 60 | repository into your package directory, then copy the following snippets into 61 | the appropriate files: 62 | 63 | ```python 64 | # Your package's __init__.py 65 | from ._version import __version__ 66 | del _version 67 | ``` 68 | 69 | ```python 70 | # Your project's setup.py 71 | 72 | from setuptools import setup 73 | 74 | # Loads _version.py module without importing the whole package. 75 | def get_version_and_cmdclass(pkg_path): 76 | import os 77 | from importlib.util import module_from_spec, spec_from_file_location 78 | spec = spec_from_file_location( 79 | 'version', os.path.join(pkg_path, '_version.py'), 80 | ) 81 | module = module_from_spec(spec) 82 | spec.loader.exec_module(module) 83 | return module.__version__, module.get_cmdclass(pkg_path) 84 | 85 | 86 | version, cmdclass = get_version_and_cmdclass('my_package') 87 | 88 | setup( 89 | name='my_package', 90 | version=version, 91 | cmdclass=cmdclass, 92 | ) 93 | ``` 94 | 95 | ``` 96 | # Your project's .gitattributes 97 | my_package/_static_version.py export-subst 98 | ``` 99 | 100 | replacing `'my_package'` in the above with the name of your package 101 | (this should be the same as the name of the directory into 102 | which you copied the contents of `miniver`). 103 | 104 | That's it! 105 | 106 | ## License 107 | Miniver is in the public domain under a CC0 license. 108 | -------------------------------------------------------------------------------- /miniver/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of 'miniver': https://github.com/jbweston/miniver 3 | # 4 | from collections import namedtuple 5 | import os 6 | 7 | Version = namedtuple("Version", ("release", "dev", "labels")) 8 | 9 | # No public API 10 | __all__ = [] 11 | 12 | package_root = os.path.dirname(os.path.realpath(__file__)) 13 | package_name = os.path.basename(package_root) 14 | 15 | STATIC_VERSION_FILE = "_static_version.py" 16 | 17 | 18 | def get_version(version_file=STATIC_VERSION_FILE): 19 | version_info = get_static_version_info(version_file) 20 | version = version_info["version"] 21 | if version == "__use_git__": 22 | version = get_version_from_git() 23 | if not version: 24 | version = get_version_from_git_archive(version_info) 25 | if not version: 26 | version = Version("unknown", None, None) 27 | return pep440_format(version) 28 | else: 29 | return version 30 | 31 | 32 | def get_static_version_info(version_file=STATIC_VERSION_FILE): 33 | version_info = {} 34 | with open(os.path.join(package_root, version_file), "rb") as f: 35 | exec(f.read(), {}, version_info) 36 | return version_info 37 | 38 | 39 | def version_is_from_git(version_file=STATIC_VERSION_FILE): 40 | return get_static_version_info(version_file)["version"] == "__use_git__" 41 | 42 | 43 | def pep440_format(version_info): 44 | release, dev, labels = version_info 45 | 46 | version_parts = [release] 47 | if dev: 48 | if release.endswith("-dev") or release.endswith(".dev"): 49 | version_parts.append(dev) 50 | else: # prefer PEP440 over strict adhesion to semver 51 | version_parts.append(".dev{}".format(dev)) 52 | 53 | if labels: 54 | version_parts.append("+") 55 | version_parts.append(".".join(labels)) 56 | 57 | return "".join(version_parts) 58 | 59 | 60 | def get_version_from_git(): 61 | import subprocess 62 | 63 | # git describe --first-parent does not take into account tags from branches 64 | # that were merged-in. The '--long' flag gets us the 'dev' version and 65 | # git hash, '--always' returns the git hash even if there are no tags. 66 | for opts in [["--first-parent"], []]: 67 | try: 68 | p = subprocess.Popen( 69 | ["git", "describe", "--long", "--always"] + opts, 70 | cwd=package_root, 71 | stdout=subprocess.PIPE, 72 | stderr=subprocess.PIPE, 73 | ) 74 | except OSError: 75 | return 76 | if p.wait() == 0: 77 | break 78 | else: 79 | return 80 | 81 | description = ( 82 | p.communicate()[0] 83 | .decode() 84 | .strip("v") # Tags can have a leading 'v', but the version should not 85 | .rstrip("\n") 86 | .rsplit("-", 2) # Split the latest tag, commits since tag, and hash 87 | ) 88 | 89 | try: 90 | release, dev, git = description 91 | except ValueError: # No tags, only the git hash 92 | # prepend 'g' to match with format returned by 'git describe' 93 | git = "g{}".format(*description) 94 | release = "unknown" 95 | dev = None 96 | 97 | labels = [] 98 | if dev == "0": 99 | dev = None 100 | else: 101 | labels.append(git) 102 | 103 | try: 104 | p = subprocess.Popen( 105 | ["git", "describe", "--dirty"], 106 | cwd=package_root, 107 | stdout=subprocess.PIPE, 108 | stderr=subprocess.PIPE, 109 | ) 110 | except OSError: 111 | labels.append("confused") # This should never happen. 112 | else: 113 | dirty_output = p.communicate()[0].decode().strip("\n") 114 | if dirty_output.endswith("dirty"): 115 | labels.append("dirty") 116 | 117 | return Version(release, dev, labels) 118 | 119 | 120 | # TODO: change this logic when there is a git pretty-format 121 | # that gives the same output as 'git describe'. 122 | # Currently we can only tell the tag the current commit is 123 | # pointing to, or its hash (with no version info) 124 | # if it is not tagged. 125 | def get_version_from_git_archive(version_info): 126 | try: 127 | refnames = version_info["refnames"] 128 | git_hash = version_info["git_hash"] 129 | except KeyError: 130 | # These fields are not present if we are running from an sdist. 131 | # Execution should never reach here, though 132 | return None 133 | 134 | if git_hash.startswith("$Format") or refnames.startswith("$Format"): 135 | # variables not expanded during 'git archive' 136 | return None 137 | 138 | VTAG = "tag: v" 139 | refs = set(r.strip() for r in refnames.split(",")) 140 | version_tags = set(r[len(VTAG) :] for r in refs if r.startswith(VTAG)) 141 | if version_tags: 142 | release, *_ = sorted(version_tags) # prefer e.g. "2.0" over "2.0rc1" 143 | return Version(release, dev=None, labels=None) 144 | else: 145 | return Version("unknown", dev=None, labels=["g{}".format(git_hash)]) 146 | 147 | 148 | __version__ = get_version() 149 | 150 | 151 | # The following section defines a 'get_cmdclass' function 152 | # that can be used from setup.py. The '__version__' module 153 | # global is used (but not modified). 154 | 155 | 156 | def _write_version(fname): 157 | # This could be a hard link, so try to delete it first. Is there any way 158 | # to do this atomically together with opening? 159 | try: 160 | os.remove(fname) 161 | except OSError: 162 | pass 163 | with open(fname, "w") as f: 164 | f.write( 165 | "# This file has been created by setup.py.\n" 166 | "version = '{}'\n".format(__version__) 167 | ) 168 | 169 | 170 | def get_cmdclass(pkg_source_path): 171 | from setuptools.command.build_py import build_py as build_py_orig 172 | from setuptools.command.sdist import sdist as sdist_orig 173 | 174 | class _build_py(build_py_orig): 175 | def run(self): 176 | super().run() 177 | 178 | src_marker = "".join(["src", os.path.sep]) 179 | 180 | if pkg_source_path.startswith(src_marker): 181 | path = pkg_source_path[len(src_marker) :] 182 | else: 183 | path = pkg_source_path 184 | _write_version( 185 | os.path.join(self.build_lib, path, STATIC_VERSION_FILE) 186 | ) 187 | 188 | class _sdist(sdist_orig): 189 | def make_release_tree(self, base_dir, files): 190 | super().make_release_tree(base_dir, files) 191 | _write_version( 192 | os.path.join(base_dir, pkg_source_path, STATIC_VERSION_FILE) 193 | ) 194 | 195 | return dict(sdist=_sdist, build_py=_build_py) 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /miniver/app.py: -------------------------------------------------------------------------------- 1 | # This file is part of 'miniver': https://github.com/jbweston/miniver 2 | 3 | import sys 4 | import os 5 | import os.path 6 | import argparse 7 | import tempfile 8 | import shutil 9 | import textwrap 10 | import glob 11 | from zipfile import ZipFile 12 | from importlib.util import find_spec, spec_from_file_location, module_from_spec 13 | from urllib.request import urlretrieve 14 | 15 | if sys.version_info < (3, 5): 16 | print("Miniver needs at least Python 3.5") 17 | sys.exit(1) 18 | 19 | try: 20 | import miniver 21 | 22 | _miniver_version = miniver.__version__ 23 | del miniver 24 | _miniver_is_installed = True 25 | except ImportError: 26 | _miniver_version = "unknown" 27 | _miniver_is_installed = False 28 | 29 | # When we fetch miniver from local files 30 | _miniver_modules = ("_version",) 31 | 32 | 33 | # When we fetch miniver from GitHub 34 | _miniver_zip_url = "https://github.com/jbweston/miniver/archive/master.zip" 35 | _zipfile_root = "miniver-master" # tied to the fact that we fetch master.zip 36 | 37 | # File templates 38 | _setup_template = textwrap.dedent( 39 | ''' 40 | from setuptools import setup 41 | 42 | def get_version_and_cmdclass(pkg_path): 43 | """Load version.py module without importing the whole package. 44 | 45 | Template code from miniver 46 | """ 47 | import os 48 | from importlib.util import module_from_spec, spec_from_file_location 49 | 50 | spec = spec_from_file_location("version", os.path.join(pkg_path, "_version.py")) 51 | module = module_from_spec(spec) 52 | spec.loader.exec_module(module) 53 | return module.__version__, module.get_cmdclass(pkg_path) 54 | 55 | 56 | version, cmdclass = get_version_and_cmdclass(r"{package_dir}") 57 | 58 | 59 | setup( 60 | ..., 61 | version=version, 62 | cmdclass=cmdclass, 63 | ) 64 | ''' 65 | ) 66 | 67 | _static_version_template = textwrap.dedent( 68 | """\ 69 | # -*- coding: utf-8 -*- 70 | # This file is part of 'miniver': https://github.com/jbweston/miniver 71 | # 72 | # This file will be overwritten by setup.py when a source or binary 73 | # distribution is made. The magic value "__use_git__" is interpreted by 74 | # _version.py. 75 | 76 | version = "__use_git__" 77 | 78 | # These values are only set if the distribution was created with 'git archive' 79 | refnames = "$Format:%D$" 80 | git_hash = "$Format:%h$" 81 | """ 82 | ) 83 | 84 | _init_template = "from ._version import __version__" 85 | _gitattribute_template = "{package_dir}/_static_version.py export-subst" 86 | 87 | 88 | def _line_in_file(to_find, filename): 89 | """Return True if the specified line exists in the named file.""" 90 | assert "\n" not in to_find 91 | try: 92 | with open(filename) as f: 93 | return any(to_find in line for line in f) 94 | except FileNotFoundError: 95 | return False 96 | 97 | 98 | def _write_line(content, filename): 99 | assert "\n" not in content 100 | if not _line_in_file(content, filename): 101 | with open(filename, "a") as f: 102 | f.write(content) 103 | 104 | 105 | def _write_content(content, filename): 106 | with open(filename, "w") as f: 107 | f.write(content) 108 | 109 | 110 | def _fail(msg): 111 | print(msg, file=sys.stderr) 112 | print("Miniver was not installed", file=sys.stderr) 113 | sys.exit(1) 114 | 115 | 116 | def extract_miniver_from_github(): 117 | filename, _ = urlretrieve(_miniver_zip_url) 118 | z = ZipFile(filename) 119 | tmpdir = tempfile.mkdtemp() 120 | input_paths = [ 121 | "/".join((_zipfile_root, "miniver", module + ".py")) 122 | for module in _miniver_modules 123 | ] 124 | for p in input_paths: 125 | z.extract(p, path=tmpdir) 126 | return [os.path.join(tmpdir, *p.split()) for p in input_paths] 127 | 128 | 129 | def extract_miniver_from_local(): 130 | return [ 131 | find_spec("." + module, package="miniver").origin for module in _miniver_modules 132 | ] 133 | 134 | 135 | def get_parser(): 136 | parser = argparse.ArgumentParser(description="Interact with miniver") 137 | parser.add_argument("-v", "--version", action="version", version=_miniver_version) 138 | # TODO: when we can depend on Python 3.7 make this "add_subparsers(required=True)" 139 | subparsers = parser.add_subparsers() 140 | # 'install' command 141 | install_parser = subparsers.add_parser( 142 | "install", help="Install miniver into the current Python package" 143 | ) 144 | install_parser.add_argument( 145 | "package_directory", help="Directory to install 'miniver' into." 146 | ) 147 | install_parser.set_defaults(dispatch=install) 148 | # 'ver' command 149 | ver_parser = subparsers.add_parser( 150 | "ver", help="Display generated version", 151 | ) 152 | ver_parser.add_argument( 153 | "search_path", 154 | help="Path to begin search for version files", 155 | nargs="?", 156 | default=".", 157 | ) 158 | ver_parser.set_defaults(dispatch=ver) 159 | return parser 160 | 161 | 162 | def install(args): 163 | package_dir = args.package_directory 164 | if not os.path.isdir(package_dir): 165 | _fail("Directory '{}' does not exist".format(package_dir)) 166 | if package_dir != os.path.relpath(package_dir): 167 | _fail("'{}' is not a relative directory".format(package_dir)) 168 | 169 | # Get miniver files 170 | if _miniver_is_installed: 171 | miniver_paths = extract_miniver_from_local() 172 | else: 173 | miniver_paths = extract_miniver_from_github() 174 | output_paths = [ 175 | os.path.join(package_dir, os.path.basename(path)) for path in miniver_paths 176 | ] 177 | 178 | for path in output_paths: 179 | if os.path.exists(path): 180 | _fail("'{}' already exists".format(path)) 181 | 182 | # Write content to local package directory 183 | for path, output_path in zip(miniver_paths, output_paths): 184 | shutil.copy(path, output_path) 185 | _write_content( 186 | _static_version_template, os.path.join(package_dir, "_static_version.py") 187 | ) 188 | _write_line( 189 | _gitattribute_template.format(package_dir=package_dir), ".gitattributes" 190 | ) 191 | _write_line( 192 | _init_template.format(package_dir=package_dir), 193 | os.path.join(package_dir, "__init__.py"), 194 | ) 195 | 196 | msg = "\n".join( 197 | textwrap.wrap( 198 | "Miniver is installed into '{package_dir}/'. " 199 | "You still have to copy the following snippet into your 'setup.py':" 200 | ) 201 | ) 202 | # Use stdout for setup template only, so it can be redirected to 'setup.py' 203 | print(msg.format(package_dir=package_dir), file=sys.stderr) 204 | print(_setup_template.format(package_dir=package_dir), file=sys.stdout) 205 | 206 | 207 | def ver(args): 208 | search_path = os.path.realpath(args.search_path) 209 | try: 210 | version_location, = glob.glob( 211 | os.path.join(search_path, "**", "_version.py"), 212 | recursive=True, 213 | ) 214 | except ValueError as err: 215 | if "not enough" in str(err): 216 | print( 217 | "'_version.py' not found in '{}'".format(search_path), 218 | file=sys.stderr, 219 | ) 220 | sys.exit(1) 221 | elif "too many" in str(err): 222 | print( 223 | "More than 1 '_version.py' found in '{}'".format(search_path), 224 | file=sys.stderr, 225 | ) 226 | sys.exit(1) 227 | else: 228 | raise 229 | version_spec = spec_from_file_location("version", version_location) 230 | version = module_from_spec(version_spec) 231 | version_spec.loader.exec_module(version) 232 | print(version.get_version()) 233 | 234 | 235 | def main(): 236 | parser = get_parser() 237 | args = parser.parse_args() 238 | # TODO: remove this check when we can rely on Python 3.7 and 239 | # can make subparsers required. 240 | if "dispatch" in args: 241 | args.dispatch(args) 242 | else: 243 | args = parser.parse_args(["ver"]) 244 | args.dispatch(args) 245 | 246 | 247 | # This is needed when using the script directly from GitHub, but not if 248 | # miniver is installed. 249 | if __name__ == "__main__": 250 | main() 251 | --------------------------------------------------------------------------------