Browse Source

Fix version handling compatible with python-semanticversion 2.8

It seems the behavior of 'partial' argument of Version class __init__
has been changed between semantic-version 2.6.0 and 2.8.1
(though I could not identify the root cause).
'partial' argument is marked as deprecated in semantic-vesion 2.7.0,
so it is a good chance not to depend on 'partial' argument in horizon.

This commit uses Version.coerce() [1] instead to convert non-semver
version into a valid semver version.
We also need to keep the original version information as it is passed
when initializing python-*client (cinderclient and keystoneclient).

In addition, the previous implementation based on semantic-version 2.6.0
returns True for "Version("3.55") == 3". It depends on the behavior
of "partial" argument of semantic_version.Version. It was not
documented and it looks tricky to depend on this behavior.
"major" and "minor" properties are now introduced and api/
is updated accordingly. I believe this approach is clearer and stable.

Unit test coverage on dict behavior is improved.
Variable names in the unit tests are adjusted to more meaningful ones.


Change-Id: If0deee9d0289ff91d58d942b9612f7736356ae18
Akihiro Motoki 9 months ago
3 changed files with 70 additions and 18 deletions
  1. +28
  2. +2
  3. +40

+ 28
- 6
openstack_dashboard/api/ View File

@@ -32,23 +32,45 @@ __all__ = ('APIResourceWrapper', 'APIDictWrapper',

class Version(object):
"""A class to handle API version.

The current OpenStack APIs use the versioning of "<major>.<minor>",
so this class supports this style only.
# NOTE(amotoki): The implementation depends on the semantic_version library
# but we don't care the patch version in this class.

def __init__(self, version):
self.version = semantic_version.Version(str(version), partial=True)
# NOTE(amotoki):
# All comparisons should use self.sem_ver as we would like to
# compare versions in the semantic versioning way.
# self.orig_ver should be used only in __str__ and __repr__
# to keep the original version information.
self.orig_ver = str(version)
self.sem_ver = semantic_version.Version.coerce(str(version))

def major(self):
return self.sem_ver.major

def minor(self):
return self.sem_ver.minor

def __eq__(self, other):
return self.version == Version(other).version
return self.sem_ver == Version(other).sem_ver

def __lt__(self, other):
return self.version < Version(other).version
return self.sem_ver < Version(other).sem_ver

def __repr__(self):
return "Version('%s')" % self.version
return "Version('%s')" % self.orig_ver

def __str__(self):
return str(self.version)
return str(self.orig_ver)

def __hash__(self):
return hash(str(self.version))
return hash(str(self.sem_ver))

class APIVersionManager(object):

+ 2
- 2
openstack_dashboard/api/ View File

@@ -233,9 +233,9 @@ def cinderclient(request, version=None):
(username, token_id, tenant_id, cinder_urls,
auth_url) = get_auth_params_from_request(request)
version = base.Version(version)
if version == 2:
if version.major == 2:
service_names = ('volumev2', 'volume')
elif version == 3:
elif version.major == 3:
service_names = ('volumev3', 'volume')
service_names = ('volume',)

+ 40
- 10
openstack_dashboard/test/unit/api/ View File

@@ -83,25 +83,55 @@ class APIVersionTests(test.TestCase):
self.assertEqual(api_base.Version('1.0'), version)

def test_greater(self):
version1 = api_base.Version('1.0')
version10 = api_base.Version('1.0')
version12 = api_base.Version('1.2')
version120 = api_base.Version('1.20')
self.assertGreater(version12, version1)
self.assertGreater(version12, version10)
self.assertGreater(version120, version12)
self.assertEqual(version12, 1) # sic!
self.assertGreater(1.2, version1)
self.assertGreater(version12, 1)
self.assertGreater(1.2, version10)
self.assertGreater(version120, 1.2)
self.assertGreater('1.20', version12)

def test_dict(self):
version1 = api_base.Version('1.0')
version1b = api_base.Version('1.0')
self.assertIn(version1, {version1b: 1})
test_dict = {api_base.Version('1.0'): 1}

self.assertIn(api_base.Version('1'), test_dict)
self.assertIn(api_base.Version('1.0'), test_dict)
self.assertIn(api_base.Version('1.0.0'), test_dict)
self.assertIn(api_base.Version(1), test_dict)
self.assertIn(api_base.Version(1.0), test_dict)

self.assertNotIn(api_base.Version('1.2'), test_dict)
self.assertNotIn(api_base.Version('1.20'), test_dict)

test_dict = {api_base.Version('1.2'): 1}
self.assertIn(api_base.Version('1.2'), test_dict)
self.assertIn(api_base.Version('1.2.0'), test_dict)
self.assertIn(api_base.Version(1.2), test_dict)

self.assertNotIn(api_base.Version('1'), test_dict)
self.assertNotIn(api_base.Version('1.0'), test_dict)
self.assertNotIn(api_base.Version('1.0.0'), test_dict)
self.assertNotIn(api_base.Version(1), test_dict)
self.assertNotIn(api_base.Version(1.0), test_dict)
self.assertNotIn(api_base.Version('1.20'), test_dict)

def test_text(self):
version1 = api_base.Version('1.0')
self.assertEqual("1.0", str(version1))
self.assertEqual("Version('1.0')", repr(version1))
version10 = api_base.Version('1.0')
self.assertEqual("1.0", str(version10))
self.assertEqual("Version('1.0')", repr(version10))

def test_major_minor(self):
version1 = api_base.Version('1')
version10 = api_base.Version('1.0')
version120 = api_base.Version('1.20')
self.assertEqual(1, version1.major)
self.assertEqual(0, version1.minor)
self.assertEqual(1, version10.major)
self.assertEqual(0, version10.minor)
self.assertEqual(1, version120.major)
self.assertEqual(20, version120.minor)

# Wrapper classes that only define _attrs don't need extra testing.