From 2ea5e2a2dba24dbe439fd6be2360600266688353 Mon Sep 17 00:00:00 2001 From: Dwayne Pryce Date: Wed, 12 Feb 2020 12:28:53 -0800 Subject: [PATCH] New version process (#14) * The new version process Providing a version on build to a python library is remarkably complex. By python convention, your module should contain a __version__ in the root module (e.g. `topologic.__version__`). Separate from that, setuptools setup.py requests a version number as well - and you definitely want these to match. To make it more difficult, if one were to clone the repository and run setuptools themselves without installing the requirements, they won't even be able to import topologic to get it that way. What we ended up going with is a submodule for the version.py file to live in. This version.py looks in version.txt for a version number - and on pre-release and release, this file will contain the actual version as part of the Github workflow action. If this file is empty, however, it will generate a version number based on the manually-adjusted semver in `version.py`, the `dev` prerelease indicator, and the current timestamp. Thus, local builds via setuptools or pip (which is using setuptools in the background) will get a fixed version number on build. Lastly, if the repository is to be cloned and the package not installed, but a python interpreter opened in the root directory, it will generate a version number based on the manually-adjusted semver in `version.py`, the `dev` prerelease indicator, and the current timestamp _at the time of importing topologic to the interpreter_. * Slightly updated the build action file for validation, and renamed it to make a bit more sense (test_push could mean a few things, instead of 'test on push') * Rogue prints * Ignore any changes to version.txt - it should stay blank in version control, and it's easy to accidentally add fixed version numbers to that file and commit them. Also refer to version by relative path import not a pseudo-circular import within topologic's __init__.py file * Also verify that the docs can be built without errors --- .../{test_push.yml => validate_on_push.yml} | 8 +- .gitignore | 3 + MANIFEST.in | 2 +- docs/conf.py | 4 +- setup.py | 29 +++++-- topologic/__init__.py | 7 +- topologic/version.py | 75 ------------------- topologic/version/__init__.py | 2 + topologic/version/version.py | 36 +++++++++ topologic/{ => version}/version.txt | 0 10 files changed, 73 insertions(+), 93 deletions(-) rename .github/workflows/{test_push.yml => validate_on_push.yml} (83%) delete mode 100644 topologic/version.py create mode 100644 topologic/version/__init__.py create mode 100644 topologic/version/version.py rename topologic/{ => version}/version.txt (100%) diff --git a/.github/workflows/test_push.yml b/.github/workflows/validate_on_push.yml similarity index 83% rename from .github/workflows/test_push.yml rename to .github/workflows/validate_on_push.yml index 42dbf5d..9b0a500 100644 --- a/.github/workflows/test_push.yml +++ b/.github/workflows/validate_on_push.yml @@ -3,7 +3,7 @@ name: Type check, lint, and test on: [push] jobs: - build: + validate: runs-on: ubuntu-latest @@ -31,3 +31,9 @@ jobs: - name: Test with pytest run: | pytest tests topologic --doctest-modules + - name: Build with setuptools + run: | + python setup.py build sdist + - name: Generate docs with Sphinx + run: | + sphinx-build -W -a docs/ docs/_build/html diff --git a/.gitignore b/.gitignore index 07b997c..13a6e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,6 @@ venv.bak/ .vscode/ *.code-workspace + +# ignore any changes to topologic/version/version.txt (this is easy to do - we want this file to exist, but be empty and not include any changes) +topologic/version/version.txt diff --git a/MANIFEST.in b/MANIFEST.in index 3c599a9..094d4db 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -recursive-include topologic version.txt +recursive-include topologic/version version.txt prune tests diff --git a/docs/conf.py b/docs/conf.py index d921d0e..74f15ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,9 +35,9 @@ copyright = '(C) Microsoft Corporation. All rights reserved.' author = 'Microsoft Corporation' # The short X.Y version -version = topologic.version.__semver +version = topologic.version.version.__semver # The full version, including alpha/beta/rc tags -release = topologic.version.get_version() +release = topologic.version.version.version # -- General configuration --------------------------------------------------- diff --git a/setup.py b/setup.py index 33bfc33..17cb033 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,21 @@ # Copyright (C) Microsoft Corporation. All rights reserved. import setuptools +import sys import os -# Python 3 + build tools are required to install this library. To install the -# prerequisites on Ubuntu, run -# sudo apt-get install build-essential python3 python3-dev python3-venv -version_file_path = os.path.abspath("topologic/version.txt") -exec(open('topologic/version.py').read()) +def handle_version() -> str: + sys.path.insert(0, os.path.join("topologic", "version")) + from version import version + sys.path.pop(0) -version = get_version(version_file_path) + version_path = os.path.join("topologic", "version", "version.txt") + with open(version_path, "w") as version_file: + b = version_file.write(f"{version}") + return version + + +version = handle_version() setuptools.setup( name="topologic", @@ -19,13 +25,20 @@ setuptools.setup( "processing networkx graph objects and statistical analysis over these objects.", version=version, packages=setuptools.find_packages(exclude=["tests", "tests.*", "tests/*"]), - package_data={'': ['version.txt']}, + package_data={'version': [os.path.join('topologic', 'version', 'version.txt')]}, include_package_data=True, author="Dwayne Pryce", author_email="dwpryce@microsoft.com", + license="MIT", classifiers=[ - "Programming Language :: Python :: 3" + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License" ], + project_urls = { + "Github": "https://github.com/microsoft/topologic", + "Documentation": "https://topologic.readthedocs.io", + }, + url="https://github.com/microsoft/topologic", install_requires=[ 'networkx', 'python-louvain>=0.13', diff --git a/topologic/__init__.py b/topologic/__init__.py index c2534f2..d759f58 100644 --- a/topologic/__init__.py +++ b/topologic/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from .version import get_version +from .version.version import name, version as __version__ # VITAL NOTE: ORDER MATTERS from .exceptions import DialectException, InvalidGraphError, UnweightedGraphError @@ -58,8 +58,3 @@ __all__ = [ 'self_loop_augmentation', 'UnweightedGraphError' ] - -name = 'topologic' -# __build_version__ is defined by the VSTS build and put into version.py. Copying the variable here so that -# we define __version__ in the standard __init__.py instead of version.py -__version__ = get_version() diff --git a/topologic/version.py b/topologic/version.py deleted file mode 100644 index 5a1b568..0000000 --- a/topologic/version.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import datetime -from typing import List, Optional, Tuple -import pkg_resources - -__all__: List[str] = ["get_version_and_type", "get_version"] - -# manually updated -__semver = "0.1.0b1" -package_name = "topologic" -__version_file = "version.txt" - - -def _from_resource() -> str: - version_file = pkg_resources.resource_stream(package_name, __version_file) - version_file_contents = version_file.read() - return version_file_contents.decode("utf-8").strip() - - -def _from_path(path: str) -> str: - with open(path) as version_file: - version_line = version_file.readline() - return version_line.strip() - - -def version_file_as_str(version_file_path: Optional[str]) -> str: - # in most cases, we want to treat this as a package relative file - if version_file_path is None: - return _from_resource() - else: - # however, if we need to get the version prior to calling setuptools, we need to read from a file instead - return _from_path(version_file_path) - - -def local_build_number() -> str: - return datetime.datetime.today().strftime('%Y%m%d.0+local') - - -def version_from_file(version_file_path: Optional[str] = None) -> Tuple[str, str]: - build_type: str = "" - build_number: str = "" - try: - version_file_contents: str = version_file_as_str(version_file_path) - if len(version_file_contents) == 0: - raise ValueError("Empty file, fallback to local snapshot") - version_file_values = version_file_contents.strip().split(",") - if len(version_file_values) == 2: - temp_type = version_file_values[0].lower() - temp_build_number = version_file_values[1] - timestamp, daily_build_number = temp_build_number.split(".") - combined_build_number = f"{timestamp}{daily_build_number.rjust(3, '0')}" - - if temp_type == "snapshot" or temp_type == "release": - build_type = temp_type - build_number = combined_build_number - else: - raise ValueError("Unknown build type, fallback to local snapshot") - else: - raise ValueError("Unknown version file format, fallback to local snapshot") - except (FileNotFoundError, ValueError): - build_type = "snapshot" - build_number = local_build_number() - return build_type, build_number - - -def get_version_and_type(version_file_path: Optional[str] = None) -> Tuple[str, str]: - build_type, build_number = version_from_file(version_file_path) - return (f"{__semver}.dev{build_number}", build_type) if build_type == "snapshot" else (__semver, build_type) - - -def get_version(version_file_path: Optional[str] = None) -> str: - version, _ = get_version_and_type(version_file_path) - return version diff --git a/topologic/version/__init__.py b/topologic/version/__init__.py new file mode 100644 index 0000000..9a04545 --- /dev/null +++ b/topologic/version/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. diff --git a/topologic/version/version.py b/topologic/version/version.py new file mode 100644 index 0000000..d445263 --- /dev/null +++ b/topologic/version/version.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import datetime +from typing import List +import pkg_resources + +__all__: List[str] = ["version", "name"] + +name = "topologic" + +# manually updated +__semver = "0.1.0" +# full version (may be same as __semver on release) +__version_file = "version.txt" + + +def _from_resource() -> str: + version_file = pkg_resources.resource_stream(__name__, __version_file) + version_file_contents = version_file.read() + return version_file_contents.decode("utf-8").strip() + + +def local_build_number() -> str: + return datetime.datetime.today().strftime('%Y%m%d%H%M%S') + + +def get_version() -> str: + version_file_contents = _from_resource() + if len(version_file_contents) == 0: + return f"{__semver}.dev{local_build_number()}" + else: + return version_file_contents + + +version = get_version() diff --git a/topologic/version.txt b/topologic/version/version.txt similarity index 100% rename from topologic/version.txt rename to topologic/version/version.txt