Skip to content

Commit 8a7ec83

Browse files
Store precise version number, and append suffixes as-needed to ensure exact version of code can be known: release[+gitsha]+localmod.
If git is available, the release will be based on the last tag in the git history that starts with "2". [+gitsha] will be ignored if this is exactly a release. +localmod will be dropped if there are no uncommitted modifications to the code. If the git is unavailable or the attempt to get more precise information fails for whatever reason, the base version as recorded in version.py will be used. Also, save the installed switch version in the output directory for improved record keeping and reproducibility, especially during active software development.
1 parent b5b1a28 commit 8a7ec83

File tree

6 files changed

+151
-7
lines changed

6 files changed

+151
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ venv
88
build*/
99
dist*/
1010
autodoc*
11+
switch_model/data/installed_version.txt

get_and_record_version.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from __future__ import print_function
2+
import argparse
3+
import logging
4+
import os
5+
import subprocess
6+
7+
"""
8+
Define a precise package version that includes any git digests for any commits
9+
made subsequently to a package release.
10+
11+
Example:
12+
1) Some commits have been made subsequent to an official release (possibly on
13+
a branch), plus some uncommitted modifications. The version would be:
14+
v1.0.4+{gitsha}+localmod
15+
2) Same scenario, but no uncommitted modifications: v1.0.4+{gitsha}
16+
3) No commits since the last official release: v1.0.4
17+
18+
These functions are encoded into a separate file from setup.py to support
19+
including precise versions in docker tags.
20+
"""
21+
22+
def get_git_version():
23+
"""
24+
Try to get git version like '{tag}+{gitsha}', with the added suffix
25+
"+localmod" if the git repo has had any uncommitted modifications.
26+
The "+{gitsha}" suffix will be dropped if this is the tagged version.
27+
Code adapted from setuptools_git_version which has an MIT license.
28+
https://pypi.org/project/setuptools-git-version/
29+
Note: Only look for tags that start with "2." to avoid tags like "demo-v1.0.1".
30+
"""
31+
git_command = "git describe --tags --long --match '2.*' --dirty --always"
32+
fmt = '{tag}+{gitsha}{dirty}'
33+
34+
git_version = subprocess.check_output(git_command, shell=True).decode('utf-8').strip()
35+
parts = git_version.split('-')
36+
# FYI, if it can't find a tag for whatever reason, len may be 1 or 2
37+
assert len(parts) in (3, 4), (
38+
"Trouble parsing git version output. Got {}, expected 3 or 4 things "
39+
"separated by dashes. This has been caused by the repository having no "
40+
"available tags, which was solved by fetching from the main repo:\n"
41+
"`git remote add main https://github.com/switch-model/switch.git && "
42+
"git fetch --all`".format(git_version)
43+
)
44+
if len(parts) == 4:
45+
dirty = '+localmod'
46+
else:
47+
dirty = ''
48+
tag, count, sha = parts[:3]
49+
if count == '0' and not dirty:
50+
return tag
51+
return fmt.format(tag=tag, gitsha=sha.lstrip('g'), dirty=dirty)
52+
53+
def get_and_record_version(repo_path):
54+
"""
55+
Attempt to get an absolute version number that includes commits made since
56+
the last release. If that succeeds, record the absolute version and use it
57+
for the pip catalog. If that fails, fall back to something reasonable and
58+
vague for the pip catalog, using the data from base_version.py.
59+
"""
60+
pkg_dir = os.path.join(repo_path , 'switch_model' )
61+
data_dir = os.path.join(pkg_dir, 'data' )
62+
__version__ = None
63+
try:
64+
__version__ = get_git_version()
65+
with open(os.path.join(data_dir, 'installed_version.txt'), 'w+') as f:
66+
f.write(__version__)
67+
except subprocess.CalledProcessError as e:
68+
logging.warning(
69+
"Could not call git as a subprocess to determine precise version."
70+
"Falling back to using the static version from version.py")
71+
logging.exception(e)
72+
except AssertionError as e:
73+
logging.warning("Trouble parsing git output.")
74+
logging.exception(e)
75+
except Exception as e:
76+
logging.warning(
77+
"Trouble getting precise version from git repository; "
78+
"using base version from switch_model/version.py. "
79+
"Error was: {}".format(e)
80+
)
81+
if __version__ is None:
82+
module_dat = {}
83+
with open(os.path.join(pkg_dir, 'version.py')) as fp:
84+
exec(fp.read(), module_dat)
85+
__version__ = module_dat['__version__']
86+
return __version__
87+
88+
def get_args():
89+
parser = argparse.ArgumentParser(
90+
description='Get a precise local version of this git repository',
91+
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
92+
parser.add_argument(
93+
'--verbose', '-v', dest='verbose', default=False,
94+
action='store_const', const=logging.WARNING,
95+
help='Show information about model preparation and solution')
96+
parser.add_argument(
97+
'--very-verbose', '-vv', dest='verbose', default=False,
98+
action='store_const', const=logging.INFO,
99+
help='Show more information about model preparation and solution')
100+
parser.add_argument(
101+
'--very-very-verbose', '-vvv', dest='verbose', default=False,
102+
action='store_const', const=logging.DEBUG,
103+
help='Show debugging-level information about model preparation and solution')
104+
parser.add_argument(
105+
'--quiet', '-q', dest='verbose', action='store_false',
106+
help="Don't show information about model preparation and solution "
107+
"(cancels --verbose setting)")
108+
109+
args = parser.parse_args()
110+
return args
111+
112+
def main():
113+
args = get_args()
114+
if args.verbose:
115+
logging.basicConfig(format='%(levelname)s:%(message)s', level=args.verbose)
116+
repo_path = os.path.dirname(os.path.realpath(__file__))
117+
__version__ = get_and_record_version(repo_path)
118+
print(__version__)
119+
120+
if __name__ == "__main__":
121+
main()

setup.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515
import os
1616
from setuptools import setup, find_packages
1717

18-
# Get the version number. Strategy #3 from https://packaging.python.org/single_source_version/
19-
version_path = os.path.join(os.path.dirname(__file__), 'switch_model', 'version.py')
20-
version = {}
21-
with open(version_path) as f:
22-
exec(f.read(), version)
23-
__version__ = version['__version__']
18+
from get_and_record_version import get_and_record_version
19+
20+
repo_path = os.path.dirname(os.path.realpath(__file__))
21+
__version__ = get_and_record_version(repo_path)
2422

2523
def read(*rnames):
2624
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
@@ -55,6 +53,9 @@ def read(*rnames):
5553
'Topic :: Software Development :: Libraries :: Python Modules'
5654
],
5755
packages=find_packages(include=['switch_model', 'switch_model.*']),
56+
package_data = {
57+
'switch_model': ['data/*']
58+
},
5859
keywords=[
5960
'renewable', 'power', 'energy', 'electricity',
6061
'production cost', 'capacity expansion',
@@ -65,6 +66,7 @@ def read(*rnames):
6566
'pint', # needed by Pyomo when we run our tests, but not included
6667
'testfixtures', # used for standard tests
6768
'pandas', # used for input upgrades and testing that functionality
69+
'setuptools', # For parsing version numbers; it is part of almost all python distributions, but not guaranteed.
6870
],
6971
extras_require={
7072
# packages used for advanced demand response, progressive hedging

switch_model/data/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""This directory contains any necessary package data or default configuration
2+
files.
3+
"""

switch_model/utilities.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from pyomo.environ import *
1313
import pyomo.opt
1414

15+
import switch_model
16+
1517
# Define string_types (same as six.string_types). This is useful for
1618
# distinguishing between strings and other iterables.
1719
try:
@@ -263,6 +265,12 @@ def post_solve(instance, outputs_dir=None):
263265
if hasattr(module, 'post_solve'):
264266
module.post_solve(instance, outputs_dir)
265267

268+
# Save the precise version used to solve this problem.
269+
version_path = os.path.join(outputs_dir, 'software_version.txt')
270+
with open(version_path, 'w') as f:
271+
f.write("This problem was solved with switch version {}.{}".format(
272+
switch_model.__version__, os.linesep))
273+
266274

267275
def min_data_check(model, *mandatory_model_components):
268276
"""

switch_model/version.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@
55
distribution because it needs to be executed before Switch (and its
66
dependencies) are installed.
77
"""
8-
__version__='2.0.6-dev'
8+
import os
9+
10+
base_version = '2.0.6-dev'
11+
12+
try:
13+
DATA_ROOT = os.path.join(os.path.dirname(__file__), 'data')
14+
with open(os.path.join(DATA_ROOT, 'installed_version.txt'), 'r') as f:
15+
__version__ = f.read().strip()
16+
except (IOError, NameError):
17+
__version__ = base_version

0 commit comments

Comments
 (0)