diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..73f4741e7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# For archives, substitute the commit hash in the setup.py file. +/setup.py export-subst diff --git a/chaco/__init__.py b/chaco/__init__.py index 02ebaa757..45df0b907 100644 --- a/chaco/__init__.py +++ b/chaco/__init__.py @@ -3,6 +3,9 @@ """ Two-dimensional plotting application toolkit. Part of the Chaco project of the Enthought Tool Suite. """ -from ._version import full_version as __version__ # noqa +try: + from chaco._version import full_version as __version__ # noqa +except ImportError: + __version__ = "not-built" __requires__ = ["traits", "traitsui", "pyface", "numpy", "enable"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..06ae32aac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["cython", "numpy", "setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 0355e33a4..6214b0f53 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ # All rights reserved. import os import re +import runpy import subprocess from numpy import get_include @@ -11,38 +12,96 @@ MAJOR = 4 MINOR = 8 MICRO = 1 - +PRERELEASE = "" IS_RELEASED = False -VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO) +# If this file is part of a Git export (for example created with "git archive", +# or downloaded from GitHub), ARCHIVE_COMMIT_HASH gives the full hash of the +# commit that was exported. +ARCHIVE_COMMIT_HASH = "$Format:%H$" + +# Templates for version strings. +RELEASED_VERSION = "{major}.{minor}.{micro}{prerelease}" +UNRELEASED_VERSION = "{major}.{minor}.{micro}{prerelease}.dev{dev}" + +# Paths to the autogenerated version file and the Git directory. +HERE = os.path.abspath(os.path.dirname(__file__)) +VERSION_FILE = os.path.join(HERE, "chaco", "_version.py") +GIT_DIRECTORY = os.path.join(HERE, ".git") + +# Template for the autogenerated version file. +VERSION_FILE_TEMPLATE = """\ +# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +# THIS FILE IS GENERATED FROM SETUP.PY + +#: The full version of the package, including a development suffix +#: for unreleased versions of the package. +version = '{version}' + +#: The full version of the package, same as 'version' +#: Kept for backward compatibility +full_version = version + +#: The Git revision from which this release was made. +git_revision = '{git_revision}' + +#: Flag whether this is a final release +is_released = {is_released} +""" -# Name of the directory containing the package. -PKG_PATHNAME = 'chaco' -# Name of the file containing the version information. -_VERSION_FILENAME = os.path.join(PKG_PATHNAME, '_version.py') +def read_module(module, package='chaco'): + """ Read a simple .py file from chaco in a safe way. + It would be simpler to import the file, but that can be problematic in an + unknown system, so we exec the file instead and extract the variables. -def read_version_py(path): - """ Read a _version.py file in a safe way. """ - with open(path, 'r') as fp: - code = compile(fp.read(), 'chaco._version', 'exec') + This will fail if things get too complex in the file being read, but is + sufficient to get version and requirements information. + """ + base_dir = os.path.dirname(__file__) + module_name = package + '.' + module + path = os.path.join(base_dir, package, module+'.py') + with open(path, 'r', encoding='utf-8') as fp: + code = compile(fp.read(), module_name, 'exec') context = {} exec(code, context) - return context['git_revision'], context['full_version'] - + return context -def git_version(): - """ Parse version information from the current git commit. - Parse the output of `git describe` and return the git hash and the number - of commits since the last version tag. +# Return the git revision as a string +def _git_info(): """ + Get information about the given commit from Git. + + Returns + ------- + git_count : int + Number of revisions from this commit to the initial commit. + git_revision : str + Commit hash for HEAD. + Raises + ------ + EnvironmentError + If Git is not available. + subprocess.CalledProcessError + If Git is available, but the version command fails (most likely + because there's no Git repository here). + """ def _minimal_ext_cmd(cmd): # construct minimal environment env = {} - for k in ['SYSTEMROOT', 'PATH', 'HOME']: + for k in ['SYSTEMROOT', 'PATH']: v = os.environ.get(k) if v is not None: env[k] = v @@ -56,14 +115,7 @@ def _minimal_ext_cmd(cmd): return out try: - # We ask git to find the latest tag matching a glob expression. The - # intention is to find a release tag of the form '4.50.2'. Strictly - # speaking, the glob expression also matches tags of the form - # '4abc.5xyz.2gtluuu', but it's very difficult with glob expressions - # to distinguish between the two cases, and the likelihood of a - # problem is minimal. - out = _minimal_ext_cmd( - ['git', 'describe', '--match', '[0-9]*.[0-9]*.[0-9]*', '--tags']) + out = _minimal_ext_cmd(['git', 'describe', '--tags']) except OSError: out = '' @@ -75,61 +127,159 @@ def _minimal_ext_cmd(cmd): else: git_revision, git_count = match.group('hash'), match.group('count') - return git_revision, git_count + return git_count, git_revision -def write_version_py(filename=_VERSION_FILENAME): - """ Create a file containing the version information. """ +def git_version(): + """ + Construct version information from local variables and Git. - template = """\ -# This file was automatically generated from the `setup.py` script. -version = '{version}' -full_version = '{full_version}' -git_revision = '{git_revision}' -is_released = {is_released} + Returns + ------- + version : str + Package version. + git_revision : str + The full commit hash for the current Git revision. -if not is_released: - version = full_version -""" - # Adding the git rev number needs to be done inside - # write_version_py(), otherwise the import of _version messes - # up the build under Python 3. - fullversion = VERSION - chaco_version_path = os.path.join( - os.path.dirname(__file__), 'chaco', '_version.py') - if os.path.exists('.git'): - git_rev, dev_num = git_version() - elif os.path.exists(filename): - # must be a source distribution, use existing version file - try: - git_rev, fullversion = read_version_py(chaco_version_path) - except (SyntaxError, KeyError): - raise RuntimeError("Unable to read git_revision. Try removing " - "chaco/_version.py and the build directory " - "before building.") - - match = re.match(r'.*?\.dev(?P\d+)', fullversion) - if match is None: - dev_num = '0' - else: - dev_num = match.group('dev_num') - else: - git_rev = 'Unknown' - dev_num = '0' + Raises + ------ + EnvironmentError + If Git is not available. + subprocess.CalledProcessError + If Git is available, but the version command fails (most likely + because there's no Git repository here). + """ + git_count, git_revision = _git_info() + version_template = RELEASED_VERSION if IS_RELEASED else UNRELEASED_VERSION + version = version_template.format( + major=MAJOR, + minor=MINOR, + micro=MICRO, + prerelease=PRERELEASE, + dev=git_count, + ) + return version, git_revision + + +def archive_version(): + """ + Construct version information for an archive. + + Returns + ------- + version : str + Package version. + git_revision : str + The full commit hash for the current Git revision. + + Raises + ------ + ValueError + If this does not appear to be an archive. + """ + if "$" in ARCHIVE_COMMIT_HASH: + raise ValueError("This does not appear to be an archive.") - if not IS_RELEASED: - fullversion += '.dev{0}'.format(dev_num) + version_template = RELEASED_VERSION if IS_RELEASED else UNRELEASED_VERSION + version = version_template.format( + major=MAJOR, + minor=MINOR, + micro=MICRO, + prerelease=PRERELEASE, + dev="-unknown", + ) + return version, ARCHIVE_COMMIT_HASH + + +def write_version_file(version, git_revision, filename=VERSION_FILE): + """ + Write version information to the version file. + + Overwrites any existing version file. + + Parameters + ---------- + version : str + Package version. + git_revision : str + The full commit hash for the current Git revision. + filename : str + Path to the version file. + """ + with open(filename, "w", encoding="utf-8") as version_file: + version_file.write( + VERSION_FILE_TEMPLATE.format( + version=version, + git_revision=git_revision, + is_released=IS_RELEASED, + ) + ) + + +def read_version_file(): + """ + Read version information from the version file, if it exists. + + Returns + ------- + version : str + The full version, including any development suffix. + git_revision : str + The full commit hash for the current Git revision. + + Raises + ------ + EnvironmentError + If the version file does not exist. + """ + version_info = runpy.run_path(VERSION_FILE) + return (version_info["version"], version_info["git_revision"]) + + +def resolve_version(): + """ + Process version information and write a version file if necessary. + + Returns the current version information. + + Returns + ------- + version : str + Package version. + git_revision : str + The full commit hash for the current Git revision. + """ + if os.path.isdir(GIT_DIRECTORY): + # This is a local clone; compute version information and write + # it to the version file, overwriting any existing information. + version = git_version() + print("Computed package version: {}".format(version)) + print("Writing version to version file {}.".format(VERSION_FILE)) + write_version_file(*version) + elif "$" not in ARCHIVE_COMMIT_HASH: + # This is a source archive. + version = archive_version() + print("Archive package version: {}".format(version)) + print("Writing version to version file {}.".format(VERSION_FILE)) + write_version_file(*version) + elif os.path.isfile(VERSION_FILE): + # This is a source distribution. Read the version information. + print("Reading version file {}".format(VERSION_FILE)) + version = read_version_file() + print("Package version from version file: {}".format(version)) + else: + raise RuntimeError( + "Unable to determine package version. No local Git clone " + "detected, and no version file found at {}.".format(VERSION_FILE) + ) - with open(filename, "wt") as fp: - fp.write(template.format(version=VERSION, - full_version=fullversion, - git_revision=git_rev, - is_released=IS_RELEASED)) + return version if __name__ == "__main__": - write_version_py() - from chaco import __requires__, __version__ + __version__, _ = resolve_version() + data = read_module('__init__') + __requires__ = data['__requires__'] numpy_include_dir = get_include()