diff --git a/.gitignore b/.gitignore index 26209e9..6c5ddc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store -*.pyc \ No newline at end of file +*.pyc +*.egg-info diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..ccdb913 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import sys + +try: + import django +except ImportError: + print("Error: missing test dependency:") + print(" django library is needed to run test suite") + print(" you can install it with 'pip install django'") + sys.exit(1) + +from django.conf import settings + + +def main(): + # Dynamically configure the Django settings with the minimum necessary to + # get Django running tests. + + settings.configure( + INSTALLED_APPS=[ + 'versionfield', + ], + DATABASE_ENGINE='django.db.backends.sqlite3', + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } + }, + DEBUG=True, + TEMPLATE_DEBUG=True, + ) + + if django.VERSION[:2] >= (1, 7): + django.setup() + + apps = ['versionfield'] + + from django.test.utils import get_runner + + DjangoTestRunner = get_runner(settings) + + failures = DjangoTestRunner(verbosity=2, interactive=True).run_tests(apps) + sys.exit(failures) + + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 19bdb09..7ef40aa 100644 --- a/setup.py +++ b/setup.py @@ -5,21 +5,24 @@ from setuptools import setup, find_packages except ImportError: import ez_setup + 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-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, + tests_require=['Django'], + test_suite='run_tests.main', + classifiers=[ 'Development Status :: 3 - Alpha', 'Framework :: Django', 'Intended Audience :: Developers', diff --git a/versionfield/__init__.py b/versionfield/__init__.py index f589268..18e4764 100644 --- a/versionfield/__init__.py +++ b/versionfield/__init__.py @@ -5,65 +5,65 @@ import forms -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)" +class VersionField(models.BigIntegerField): + """ + 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)" - __metaclass__ = models.SubfieldBase + __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 to_python(self,value): - if isinstance(value, Version): - return value + def to_python(self, value): + if isinstance(value, Version): + return int(value) - if isinstance(value,basestring): - return Version(value,self.number_bits) + if isinstance(value, basestring): + return Version(value, self.number_bits) - if value is None: - return None + if value is None: + return None - 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 get_prep_value(self, value): + if isinstance(value, basestring): + return int(Version(value, self.number_bits)) - if value is None: - return None + if value is None: + return None - return int(value) + return int(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 __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 __unicode__(self, value): + return unicode(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..7f70a68 100644 --- a/versionfield/forms.py +++ b/versionfield/forms.py @@ -1,21 +1,30 @@ from django import forms +from django.forms.widgets import TextInput from version import Version from constants import DEFAULT_NUMBER_BITS -from utils import convert_version_string_to_int +from utils import convert_version_int_to_string + class VersionField(forms.IntegerField): - def __init__(self,number_bits=DEFAULT_NUMBER_BITS,**kwargs): - self.number_bits = number_bits - return super(VersionField, self).__init__(**kwargs) + widget = TextInput + + 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 - 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 int(Version(value, self.number_bits)) - if isinstance(value,basestring): - 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) \ No newline at end of file + def widget_attrs(self, widget): + attrs = super(VersionField, self).widget_attrs(widget) + attrs['pattern'] = '^' + (r'(\d+\.)?' * (len(self.number_bits) - 1)) + r'(\*|\d+)$' + return attrs diff --git a/versionfield/tests/__init__.py b/versionfield/tests/__init__.py index 1214ff1..813cf0d 100644 --- a/versionfield/tests/__init__.py +++ b/versionfield/tests/__init__.py @@ -1,56 +1 @@ -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") +# coding: utf8 \ No newline at end of file diff --git a/versionfield/tests/test_versionfield.py b/versionfield/tests/test_versionfield.py new file mode 100644 index 0000000..2433626 --- /dev/null +++ b/versionfield/tests/test_versionfield.py @@ -0,0 +1,60 @@ +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..2f59694 100644 --- a/versionfield/utils.py +++ b/versionfield/utils.py @@ -1,50 +1,57 @@ -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) +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) + 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..9bb94bc 100644 --- a/versionfield/version.py +++ b/versionfield/version.py @@ -1,30 +1,33 @@ +# coding: utf8 + from .utils import convert_version_string_to_int, convert_version_int_to_string -from .constants import DEFAULT_NUMBER_BITS + + 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 __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)