diff --git a/.gitignore b/.gitignore index 26209e9..6b7a409 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ .DS_Store -*.pyc \ No newline at end of file +*.pyc + +# Setuptools distribution folder. +/dist/ + +# Python egg metadata, regenerated from source files by setuptools. +/*.egg-info diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..82155de --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +sudo: false +language: python +python: + - "2.7" + +before_install: + - pip install codecov + +install: + - python setup.py -q develop + +script: + - cd test_project + - coverage run --source ../versionfield manage.py test versionfield + +after_success: + - codecov diff --git a/README.md b/README.md index f1d6d13..cef8340 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ django-versionfield ==== +[![Build Status](https://travis-ci.org/tonioo/django-versionfield.svg?branch=master)](https://travis-ci.org/tonioo/django-versionfield) +[![codecov](https://codecov.io/gh/tonioo/django-versionfield/branch/master/graph/badge.svg)](https://codecov.io/gh/tonioo/django-versionfield) + Usage: from versionfield import VersionField @@ -16,6 +19,7 @@ License django-versionfield is distributed under a BSD-style license. + Copyright (c) 2014-2017 Antoine Nguyen Copyright (c) 2011-2013 MindSnacks (http://mindsnacks.com/) Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/setup.py b/setup.py index 19bdb09..c01121d 100644 --- a/setup.py +++ b/setup.py @@ -8,18 +8,21 @@ ez_setup.use_setuptools() from setuptools import setup, find_packages -import os - setup( - name = "django-versionfield", - version = "0.3.2", - url = 'https://github.com/mindsnacks/django-versionfield', - license = 'BSD', - description = "A DB Independent Custom Django Field for storing Version numbers for fast indexing", - author = 'Tom Hoddes', - packages = find_packages(), - include_package_data = True, - classifiers = [ + name="django-versionfield2", + version="0.5.0", + url='https://github.com/tonioo/django-versionfield', + license='BSD', + description="A DB Independent Custom Django Field for storing Version numbers for fast indexing", + author='Antoine Nguyen', + author_email='tonio@ngyn.org', + packages=find_packages(), + include_package_data=True, + install_requires=[ + 'Django<=1.10.99', + 'six>=1.9.0', + ], + classifiers=[ 'Development Status :: 3 - Alpha', 'Framework :: Django', 'Intended Audience :: Developers', diff --git a/test_project/manage.py b/test_project/manage.py new file mode 100755 index 0000000..0fc36a3 --- /dev/null +++ b/test_project/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/test_project/test_project/__init__.py b/test_project/test_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py new file mode 100644 index 0000000..b60d23f --- /dev/null +++ b/test_project/test_project/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for test_project project. + +Generated by 'django-admin startproject' using Django 1.9.7. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '33%!r(6)*y1@f0=(2(!*fwoz5@=mi#$6&cc-ze@2#ws3(gx(5p' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'versionfield', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'test_project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'test_project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/test_project/test_project/urls.py b/test_project/test_project/urls.py new file mode 100644 index 0000000..b03753d --- /dev/null +++ b/test_project/test_project/urls.py @@ -0,0 +1,21 @@ +"""test_project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/test_project/test_project/wsgi.py b/test_project/test_project/wsgi.py new file mode 100644 index 0000000..fc2e14e --- /dev/null +++ b/test_project/test_project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for test_project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + +application = get_wsgi_application() diff --git a/versionfield/__init__.py b/versionfield/__init__.py index f589268..ce0d739 100644 --- a/versionfield/__init__.py +++ b/versionfield/__init__.py @@ -1,69 +1,88 @@ +from __future__ import unicode_literals + +import six + from django.db import models +from django.utils.encoding import python_2_unicode_compatible + +from . import forms from .constants import DEFAULT_NUMBER_BITS from .version import Version -from utils import convert_version_string_to_int, convert_version_int_to_string -import forms +from .utils import convert_version_int_to_string + + +@python_2_unicode_compatible +class VersionField(models.Field): + """ + A Field where version numbers are input/output as strings (e.g. 3.0.1) + but stored in the db as converted integers for fast indexing + """ -class VersionField(models.PositiveIntegerField): - """ - A Field where version numbers are input/output as strings (e.g. 3.0.1) - but stored in the db as converted integers for fast indexing - """ - description = "A version number (e.g. 3.0.1)" + description = "A version number (e.g. 3.0.1)" - __metaclass__ = models.SubfieldBase + def __init__(self, number_bits=DEFAULT_NUMBER_BITS, *args, **kwargs): + self.number_bits = number_bits + super(VersionField, self).__init__(*args, **kwargs) - def __init__(self, number_bits=DEFAULT_NUMBER_BITS, *args, **kwargs): - self.number_bits = number_bits - super(VersionField, self).__init__(*args, **kwargs) + def db_type(self, connection): + """Use integer as internal representation.""" + return "integer" - def to_python(self,value): - if isinstance(value, Version): - return value + def to_python(self, value): + if not value: + return None - if isinstance(value,basestring): - return Version(value,self.number_bits) + if isinstance(value, Version): + return value - if value is None: - return None + if isinstance(value, six.string_types): + return Version(value, self.number_bits) - return Version(convert_version_int_to_string(value,self.number_bits),self.number_bits) + return Version( + convert_version_int_to_string(value, self.number_bits), + self.number_bits + ) - def get_prep_value(self,value): - if isinstance(value,basestring): - return int(Version(value,self.number_bits)) + def from_db_value(self, value, expression, connection, context): + """Convert data from database.""" + if value is None: + return value + return Version( + convert_version_int_to_string(value, self.number_bits), + self.number_bits) - if value is None: - return None + def get_prep_value(self, value): + if isinstance(value, six.string_types): + return int(Version(value, self.number_bits)) - return int(value) + if value is None: + return None - def formfield(self, **kwargs): - defaults = { - 'form_class': forms.VersionField, - 'number_bits': self.number_bits - } - defaults.update(kwargs) - return super(VersionField, self).formfield(**defaults) + return int(value) - def __unicode__(self, value): - return unicode(value) + def formfield(self, **kwargs): + defaults = { + 'form_class': forms.VersionField, + 'number_bits': self.number_bits + } + defaults.update(kwargs) + return super(VersionField, self).formfield(**defaults) + def __str__(self, value): + return six.text_type(value) try: - from south.modelsinspector import add_introspection_rules - rules = [ - ( - (VersionField,), - [], - { - "number_bits": ["number_bits", {"default": DEFAULT_NUMBER_BITS}], - }, - ) - ] - add_introspection_rules(rules, ["^versionfield"]) + from south.modelsinspector import add_introspection_rules + rules = [( + (VersionField,), + [], + { + "number_bits": ["number_bits", {"default": DEFAULT_NUMBER_BITS}], + }, + )] + add_introspection_rules(rules, ["^versionfield"]) except ImportError: - # looks like we aren't using south - pass + # looks like we aren't using south + pass diff --git a/versionfield/constants.py b/versionfield/constants.py index 6f44d59..e8ad689 100644 --- a/versionfield/constants.py +++ b/versionfield/constants.py @@ -1 +1 @@ -DEFAULT_NUMBER_BITS = (8,8,16) \ No newline at end of file +DEFAULT_NUMBER_BITS = (8, 8, 16) diff --git a/versionfield/forms.py b/versionfield/forms.py index a64be58..cff57d5 100644 --- a/versionfield/forms.py +++ b/versionfield/forms.py @@ -1,21 +1,60 @@ +"""Custom form fields.""" + +import six + from django import forms -from version import Version -from constants import DEFAULT_NUMBER_BITS -from utils import convert_version_string_to_int - -class VersionField(forms.IntegerField): - def __init__(self,number_bits=DEFAULT_NUMBER_BITS,**kwargs): - self.number_bits = number_bits - return super(VersionField, self).__init__(**kwargs) - - def to_python(self,value): - """ - Verifies that value can be converted to a Version object - """ - if not value: - return None - - if isinstance(value,basestring): - return Version(value,self.number_bits) - - return Version(convert_version_int_to_string(value,self.number_bits),self.number_bits) \ No newline at end of file + +from .version import Version +from .constants import DEFAULT_NUMBER_BITS +from .utils import convert_version_int_to_string + + +class VersionField(forms.CharField): + + """A form field dedicated to version numbers.""" + + def __init__(self, number_bits=DEFAULT_NUMBER_BITS, **kwargs): + self.number_bits = number_bits + return super(VersionField, self).__init__(**kwargs) + + def check_format(self, string): + """Check that value contains no more than N decimal numbers.""" + parts = string.split(".") + actual_len = len(parts) + allowed_len = len(self.number_bits) + if actual_len > allowed_len: + raise forms.ValidationError( + "Version has %(actual)d components; only %(allowed)d " + "components are allowed", + code="too_long_version", + params=dict(actual=actual_len, allowed=allowed_len)) + for i, (part, bits) in enumerate(zip(parts, self.number_bits), 1): + if not part.isdigit(): + raise forms.ValidationError( + "Version's %(index)d component (%(part)s) is not numeric; " + "only numeric values are allowed", + code="not_numeric_version", + params=dict(index=i, part=part)) + num = int(part) + max_allowed = (1 << bits) - 1 + if num > max_allowed: + raise forms.ValidationError( + "Version's %(index)d component (%(part)s) is too big; " + "maximum allowed value for this component is %(allowed)d", + code="version_component_too_big", + params=dict(index=i, part=part, allowed=max_allowed)) + + def to_python(self, value): + """Verifies that value can be converted to a Version object.""" + if not value: + return None + + self.check_format(value) + + if isinstance(value, six.string_types): + return Version(value, self.number_bits) + + return Version( + convert_version_int_to_string(value, self.number_bits), + self.number_bits + ) diff --git a/versionfield/tests.py b/versionfield/tests.py new file mode 100644 index 0000000..0bad4ac --- /dev/null +++ b/versionfield/tests.py @@ -0,0 +1,162 @@ +from __future__ import unicode_literals + +import unittest + +from django.test import TestCase +from django.db import models +from django.forms import ValidationError + +from . import VersionField +from .constants import DEFAULT_NUMBER_BITS +from .version import Version +from . import forms + + +class DummyModel(models.Model): + version = VersionField() + + +class VersionFieldTest(TestCase): + + def setUp(self): + DummyModel.objects.create(version="0.1") + DummyModel.objects.create(version="1.0") + DummyModel.objects.create(version="1.0.1") + + def test_get_by_exact_version(self): + thing = DummyModel.objects.get(version="0.1") + self.assertEqual(thing.version, "0.1") + self.assertEqual(thing.version, "0.1.0") + + def test_filter_by_greater_than_version(self): + things = DummyModel.objects.filter(version__gt="0.1") + self.assertEqual(len(things), 2) + + things = DummyModel.objects.filter(version__gt="1.0") + self.assertEqual(len(things), 1) + + things = DummyModel.objects.filter(version__gt="1.0.1") + self.assertEqual(len(things), 0) + + def test_filter_by_less_than_version(self): + things = DummyModel.objects.filter(version__lt="0.1") + self.assertEqual(len(things), 0) + + things = DummyModel.objects.filter(version__lt="1.0") + self.assertEqual(len(things), 1) + + things = DummyModel.objects.filter(version__lt="1.0.1") + self.assertEqual(len(things), 2) + + def test_overflow_number(self): + error_occured = False + try: + DummyModel.objects.create(version="1.999.1") + except ValueError: + error_occured = True + self.assertTrue(error_occured) + + def test_validate_positive(self): + field = forms.VersionField() + field.check_format("10.11.12") + + def test_validate_too_long(self): + error_occured = False + correct_error = False + field = forms.VersionField() + try: + field.check_format("10.11.12.13") + except ValidationError as e: + error_occured = True + if e.code == "too_long_version": + correct_error = True + self.assertTrue(error_occured) + self.assertTrue(correct_error) + + def test_validate_not_numeric(self): + error_occured = False + correct_error = False + field = forms.VersionField() + try: + field.check_format("10.x.1") + except ValidationError as e: + error_occured = True + if e.code == "not_numeric_version": + correct_error = True + self.assertTrue(error_occured) + self.assertTrue(correct_error) + + def test_validate_too_big(self): + error_occured = False + correct_error = False + field = forms.VersionField() + try: + field.check_format("10.999.1") + except ValidationError as e: + error_occured = True + if e.code == "version_component_too_big": + correct_error = True + self.assertTrue(error_occured) + self.assertTrue(correct_error) + + +class DummyModelCustomBit(models.Model): + version = VersionField(number_bits=(8, 16, 8)) + + +class VersionFieldCustomBitsTest(TestCase): + def setUp(self): + DummyModelCustomBit.objects.create(version="1.999.1") + + def test_get_by_exact_version(self): + thing = DummyModelCustomBit.objects.get(version="1.999.1") + self.assertEqual(thing.version, "1.999.1") + + +class VersionObjectTestCase(unittest.TestCase): + + def test_equal_operator(self): + self.assertEqual( + Version("1.2.3", DEFAULT_NUMBER_BITS), + Version("1.2.3", DEFAULT_NUMBER_BITS), + ) + + def test_lt_operator(self): + self.assertTrue( + Version("1.2.3", DEFAULT_NUMBER_BITS) < + Version("1.2.4", DEFAULT_NUMBER_BITS) + ) + self.assertFalse( + Version("1.2.4", DEFAULT_NUMBER_BITS) < + Version("1.2.3", DEFAULT_NUMBER_BITS) + ) + + def test_le_operator(self): + self.assertTrue( + Version("1.2.3", DEFAULT_NUMBER_BITS) <= + Version("1.2.4", DEFAULT_NUMBER_BITS) + ) + self.assertTrue( + Version("1.2.4", DEFAULT_NUMBER_BITS) <= + Version("1.2.4", DEFAULT_NUMBER_BITS) + ) + + def test_gt_operator(self): + self.assertTrue( + Version("1.2.4", DEFAULT_NUMBER_BITS) > + Version("1.2.3", DEFAULT_NUMBER_BITS) + ) + self.assertFalse( + Version("2.3.4", DEFAULT_NUMBER_BITS) > + Version("3.2.1", DEFAULT_NUMBER_BITS) + ) + + def test_ge_operator(self): + self.assertTrue( + Version("1.2.4", DEFAULT_NUMBER_BITS) >= + Version("1.2.3", DEFAULT_NUMBER_BITS) + ) + self.assertTrue( + Version("3.3.3", DEFAULT_NUMBER_BITS) <= + Version("3.3.3", DEFAULT_NUMBER_BITS) + ) diff --git a/versionfield/tests/__init__.py b/versionfield/tests/__init__.py deleted file mode 100644 index 1214ff1..0000000 --- a/versionfield/tests/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.test import TestCase -from django.db import models -from .. import VersionField - -class DummyModel(models.Model): - version = VersionField() - -class VersionFieldTest(TestCase): - def setUp(self): - DummyModel.objects.create(version="0.1") - DummyModel.objects.create(version="1.0") - DummyModel.objects.create(version="1.0.1") - - def test_get_by_exact_version(self): - thing = DummyModel.objects.get(version="0.1") - self.assertEqual(thing.version,"0.1") - self.assertEqual(thing.version,"0.1.0") - - def test_filter_by_greater_than_version(self): - things = DummyModel.objects.filter(version__gt="0.1") - self.assertEqual(len(things),2) - - things = DummyModel.objects.filter(version__gt="1.0") - self.assertEqual(len(things),1) - - things = DummyModel.objects.filter(version__gt="1.0.1") - self.assertEqual(len(things),0) - - def test_filter_by_less_than_version(self): - things = DummyModel.objects.filter(version__lt="0.1") - self.assertEqual(len(things),0) - - things = DummyModel.objects.filter(version__lt="1.0") - self.assertEqual(len(things),1) - - things = DummyModel.objects.filter(version__lt="1.0.1") - self.assertEqual(len(things),2) - - def test_overflow_number(self): - error_occured = False - try: - overflow = DummyModel.objects.create(version="1.999.1") - except ValueError: - error_occured = True - self.assertTrue(error_occured) - -class DummyModelCustomBit(models.Model): - version = VersionField(number_bits=(8,16,8)) - -class VersionFieldCustomBitsTest(TestCase): - def setUp(self): - DummyModelCustomBit.objects.create(version="1.999.1") - - def test_get_by_exact_version(self): - thing = DummyModelCustomBit.objects.get(version="1.999.1") - self.assertEqual(thing.version,"1.999.1") diff --git a/versionfield/utils.py b/versionfield/utils.py index 52332df..5dd1f4d 100644 --- a/versionfield/utils.py +++ b/versionfield/utils.py @@ -1,50 +1,65 @@ -def convert_version_string_to_int(string,number_bits): - """ - Take in a verison string e.g. '3.0.1' - Store it as a converted int: 3*(2**number_bits[0])+0*(2**number_bits[1])+1*(2**number_bits[2]) - - >>> convert_version_string_to_int('3.0.1',[8,8,16]) - 50331649 - """ - numbers = [int(number_string) for number_string in string.split(".")] - - if len(numbers) > len(number_bits): - raise NotImplementedError("Versions with more than {0} decimal places are not supported".format(len(number_bits)-1)) - - #add 0s for missing numbers - numbers.extend([0]*(len(number_bits) - len(numbers))) - - #convert to single int and return - number = 0 - total_bits = 0 - for num,bits in reversed(zip(numbers,number_bits)): - max_num = (bits+1)-1 - if num >= 1 << max_num: - raise ValueError("Number {0} cannot be stored with only {1} bits. Max is {2}".format(num,bits,max_num)) - number += num << total_bits - total_bits += bits - - return number - -def convert_version_int_to_string(number,number_bits): - """ - Take in a verison string e.g. '3.0.1' - Store it as a converted int: 3*(2**number_bits[0])+0*(2**number_bits[1])+1*(2**number_bits[2]) - - >>> convert_version_int_to_string(50331649,[8,8,16]) - '3.0.1' - """ - number_strings = [] - total_bits = sum(number_bits) - for bits in number_bits: - shift_amount = (total_bits-bits) - number_segment = number >> shift_amount - number_strings.append(str(number_segment)) - total_bits = total_bits - bits - number = number - (number_segment << shift_amount) - - return ".".join(number_strings) +"""Conversion functions.""" + + +import six + + +def convert_version_string_to_int(string, number_bits): + """ + Take in a verison string e.g. '3.0.1' + Store it as a converted int: + 3 * (2**number_bits[0]) + 0 * (2**number_bits[1]) + 1 * (2**number_bits[2]) + + >>> convert_version_string_to_int('3.0.1',[8,8,16]) + 50331649 + """ + numbers = [int(number_string) for number_string in string.split(".")] + + if len(numbers) > len(number_bits): + raise NotImplementedError( + "Versions with more than {0} decimal places are not supported" + .format(len(number_bits) - 1) + ) + + # add 0s for missing numbers + numbers.extend([0] * (len(number_bits) - len(numbers))) + + # convert to single int and return + number = 0 + total_bits = 0 + for num, bits in reversed(list(zip(numbers, number_bits))): + max_num = (bits + 1) - 1 + if num >= 1 << max_num: + raise ValueError( + "Number {0} cannot be stored with only {1} bits. Max is {2}" + .format(num, bits, max_num) + ) + number += num << total_bits + total_bits += bits + + return number + + +def convert_version_int_to_string(number, number_bits): + """ + Take in a verison string e.g. '3.0.1' + Store it as a converted int: + 3 * (2**number_bits[0]) + 0 * (2**number_bits[1]) + 1 * (2**number_bits[2]) + + >>> convert_version_int_to_string(50331649,[8,8,16]) + '3.0.1' + """ + number_strings = [] + total_bits = sum(number_bits) + for bits in number_bits: + shift_amount = (total_bits - bits) + number_segment = number >> shift_amount + number_strings.append(six.text_type(number_segment)) + total_bits = total_bits - bits + number = number - (number_segment << shift_amount) + return ".".join(number_strings) + if __name__ == "__main__": - import doctest - doctest.testmod() \ No newline at end of file + import doctest + doctest.testmod() diff --git a/versionfield/version.py b/versionfield/version.py index ce7a766..e0e2da4 100644 --- a/versionfield/version.py +++ b/versionfield/version.py @@ -1,30 +1,62 @@ +import six + +from django.utils.encoding import python_2_unicode_compatible + from .utils import convert_version_string_to_int, convert_version_int_to_string -from .constants import DEFAULT_NUMBER_BITS + + +@python_2_unicode_compatible class Version(object): - def __init__(self,string,number_bits): - """ - Take in a verison string e.g. '3.0.1' - Store it as a converted int - """ - self.number_bits = number_bits - self.internal_integer = convert_version_string_to_int(string,number_bits) - - def __unicode__(self): - return unicode(convert_version_int_to_string(self.internal_integer,self.number_bits)) - - def __str__(self): - return self.__unicode__() - - def __repr__(self): - return self.__unicode__() - - def __int__(self): - return self.internal_integer - - def __eq__(self,other): - if not other: - return False # we are obviously a valid Version, but 'other' isn't - if isinstance(other,basestring): - return self == Version(other,self.number_bits) - else: - return int(self) == int(other) \ No newline at end of file + def __init__(self, string, number_bits): + """ + Take in a verison string e.g. '3.0.1' + Store it as a converted int + """ + self.number_bits = number_bits + self.internal_integer = convert_version_string_to_int( + string, number_bits) + + def __str__(self): + return six.text_type(convert_version_int_to_string( + self.internal_integer, self.number_bits)) + + def __repr__(self): + return self.__str__() + + def __int__(self): + return self.internal_integer + + def __eq__(self, other): + if not other: + return False # we are obviously a valid Version, but 'other' isn't + if isinstance(other, six.string_types): + other = Version(other, self.number_bits) + return int(self) == int(other) + + def __lt__(self, other): + if not other: + return False + if isinstance(other, six.string_types): + other = Version(other, self.number_bits) + return int(self) < int(other) + + def __le__(self, other): + if not other: + return False + if isinstance(other, six.string_types): + other = Version(other, self.number_bits) + return int(self) <= int(other) + + def __gt__(self, other): + if not other: + return False + if isinstance(other, six.string_types): + other = Version(other, self.number_bits) + return int(self) > int(other) + + def __ge__(self, other): + if not other: + return False + if isinstance(other, six.string_types): + other = Version(other, self.number_bits) + return int(self) >= int(other)