├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bitfield ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── models.py ├── query.py ├── tests │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── tests.py └── types.py ├── conftest.py ├── setup.cfg ├── setup.py └── tox.ini /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [master] 7 | tags: ["*"] 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | ci: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: 21 | - "3.8" 22 | - "3.9" 23 | - "3.10" 24 | - "3.11" 25 | steps: 26 | - name: Set up PostgreSQL 27 | run: | 28 | echo 'local all all peer map=mymap' | sudo tee /etc/postgresql/14/main/pg_hba.conf 29 | echo "mymap $(id -un) postgres" | sudo tee /etc/postgresql/14/main/pg_ident.conf 30 | sudo /etc/init.d/postgresql restart 14 31 | createdb -U postgres bitfield 32 | - uses: actions/checkout@v3 33 | - uses: actions/setup-python@v4 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | - run: pip install -e .[tests] tox tox-gh-actions 37 | - run: PYFLAKES_NODOCTEST=1 flake8 bitfield 38 | - run: tox 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.eggs 2 | *.pyc 3 | *.swp 4 | *.un~ 5 | *.egg-info 6 | *.egg/dist/ 7 | build/ 8 | dist/ 9 | .tox/ 10 | 11 | .idea/ 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2010 DISQUS 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README MANIFEST.in LICENSE 2 | global-exclude *~ 3 | exclude bitfield/tests/* 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-bitfield 2 | --------------- 3 | 4 | .. image:: https://github.com/disqus/django-bitfield/actions/workflows/ci.yml/badge.svg 5 | :target: https://github.com/disqus/django-bitfield/actions 6 | 7 | Provides a BitField like class (using a BigIntegerField) for your Django models. 8 | 9 | Requirements 10 | ============ 11 | 12 | * Django >= 1.11 (a newer version with current security support is 13 | highly recommended). 14 | * PostgreSQL (see notes) 15 | 16 | **Notes:** 17 | 18 | - SQLite does not support save operations using a ``Bit`` (per the example under Usage). 19 | - MySQL fails on most queries related to BitField's. 20 | 21 | Installation 22 | ============ 23 | 24 | Install it with pip (or easy_install):: 25 | 26 | pip install django-bitfield 27 | 28 | Usage 29 | ===== 30 | 31 | First you'll need to attach a BitField to your class. This acts as a BigIntegerField (BIGINT) in your database:: 32 | 33 | from bitfield import BitField 34 | 35 | class MyModel(models.Model): 36 | flags = BitField(flags=( 37 | 'awesome_flag', 38 | 'flaggy_foo', 39 | 'baz_bar', 40 | )) 41 | 42 | Flags can also be defined with labels:: 43 | 44 | class MyModel(models.Model): 45 | flags = BitField(flags=( 46 | ('awesome_flag', 'Awesome Flag!'), 47 | ('flaggy_foo', 'Flaggy Foo'), 48 | ('baz_bar', 'Baz (bar)'), 49 | )) 50 | 51 | Now you can use the field using very familiar Django operations:: 52 | 53 | # Create the model 54 | o = MyModel.objects.create(flags=0) 55 | 56 | # Add awesome_flag (does not work in SQLite) 57 | MyModel.objects.filter(pk=o.pk).update(flags=F('flags').bitor(MyModel.flags.awesome_flag)) 58 | 59 | # Set flags manually to [awesome_flag, flaggy_foo] 60 | MyModel.objects.filter(pk=o.pk).update(flags=MyModel.flags.awesome_flag | MyModel.flags.flaggy_foo) 61 | 62 | # Remove awesome_flag (does not work in SQLite) 63 | MyModel.objects.filter(pk=o.pk).update(flags=F('flags').bitand(~MyModel.flags.awesome_flag)) 64 | 65 | # Find by awesome_flag 66 | MyModel.objects.filter(flags=MyModel.flags.awesome_flag) 67 | 68 | # Exclude by awesome_flag 69 | MyModel.objects.filter(flags=~MyModel.flags.awesome_flag) 70 | 71 | # Test awesome_flag 72 | if o.flags.awesome_flag: 73 | print "Happy times!" 74 | 75 | # List all flags on the field 76 | for f in o.flags: 77 | print f 78 | 79 | # Get a flag label 80 | print o.flags.get_label('awesome_flag') 81 | 82 | Enjoy! 83 | 84 | Admin 85 | ===== 86 | 87 | To use the widget in the admin, you'll need to import the classes and then update or create 88 | a ModelAdmin with these formfield_overrides lines in your admin.py:: 89 | 90 | from bitfield import BitField 91 | from bitfield.forms import BitFieldCheckboxSelectMultiple 92 | 93 | class MyModelAdmin(admin.ModelAdmin): 94 | formfield_overrides = { 95 | BitField: {'widget': BitFieldCheckboxSelectMultiple}, 96 | } 97 | 98 | admin.site.register(MyModel, MyModelAdmin) 99 | 100 | 101 | There is also a ``BitFieldListFilter`` list filter (Django 1.4 or newer). 102 | To use it set ``list_filter`` ModelAdmin option:: 103 | 104 | list_filter = ( 105 | ('flags', BitFieldListFilter,) 106 | ) 107 | 108 | BitFieldListFilter is in ``bitfield.admin`` module:: 109 | 110 | from bitfield.admin import BitFieldListFilter 111 | 112 | Changelog 113 | ========= 114 | 115 | 2.2.0 - 2022-07-11: 116 | 117 | - Add support for Django 4.0. 118 | - Drop support for Django versions older than 1.11.29. 119 | - Drop support for Python 2.7. 120 | 121 | 2.1.0 - 2021-05-25: 122 | 123 | - Add support for Django 3.1, 3.2 (No changes needed). 124 | - Add support for Python 3.8, 3.9. 125 | - Fixed multiple bugs with use in the Django admin. 126 | - Removed dead compatibility code. 127 | 128 | 2.0.1 - 2020-01-25: 129 | 130 | - Add support for Django 3.0. 131 | 132 | 2.0.0 - 2020-01-24: 133 | 134 | - Drop support for Django versions below 1.10. 135 | - Use _meta.private_fields instead of deprecated _meta.virtual_fields in CompositeBitField. 136 | - Add testing with python 3.6, 3.7 and Django 2.x to travis configuration. 137 | -------------------------------------------------------------------------------- /bitfield/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-bitfield 3 | ~~~~~~~~~~~~~~~ 4 | """ 5 | from __future__ import absolute_import 6 | 7 | from bitfield.models import Bit, BitHandler, CompositeBitField, BitField # NOQA 8 | 9 | default_app_config = 'bitfield.apps.BitFieldAppConfig' 10 | 11 | VERSION = "2.2.0" 12 | -------------------------------------------------------------------------------- /bitfield/admin.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | from django.core.exceptions import ValidationError 4 | if django.VERSION < (2, 0): 5 | from django.utils.translation import ugettext_lazy as _ 6 | else: 7 | # Aliased since Django 2.0 https://github.com/django/django/blob/2.0/django/utils/translation/__init__.py#L80-L81 8 | from django.utils.translation import gettext_lazy as _ 9 | from django.contrib.admin import FieldListFilter 10 | from django.contrib.admin.options import IncorrectLookupParameters 11 | 12 | from bitfield import Bit, BitHandler 13 | 14 | 15 | class BitFieldListFilter(FieldListFilter): 16 | """ 17 | BitField list filter. 18 | """ 19 | 20 | def __init__(self, field, request, params, model, model_admin, field_path): 21 | self.lookup_kwarg = field_path 22 | self.lookup_val = int(request.GET.get(self.lookup_kwarg, 0)) 23 | self.flags = field.flags 24 | self.labels = field.labels 25 | super(BitFieldListFilter, self).__init__( 26 | field, request, params, model, model_admin, field_path) 27 | 28 | def queryset(self, request, queryset): 29 | filter_kwargs = dict( 30 | (p, BitHandler(v, ())) 31 | for p, v in self.used_parameters.items() 32 | ) 33 | if not filter_kwargs: 34 | return queryset 35 | try: 36 | return queryset.filter(**filter_kwargs) 37 | except ValidationError as e: 38 | raise IncorrectLookupParameters(e) 39 | 40 | def expected_parameters(self): 41 | return [self.lookup_kwarg] 42 | 43 | def choices(self, cl): 44 | yield { 45 | 'selected': self.lookup_val == 0, 46 | 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 47 | 'display': _('All'), 48 | } 49 | for number, flag in enumerate(self.flags): 50 | bit_mask = Bit(number).mask 51 | yield { 52 | 'selected': self.lookup_val == bit_mask, 53 | 'query_string': cl.get_query_string({self.lookup_kwarg: bit_mask}), 54 | 'display': self.labels[number], 55 | } 56 | -------------------------------------------------------------------------------- /bitfield/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BitFieldAppConfig(AppConfig): 5 | name = 'bitfield' 6 | verbose_name = "Bit Field" 7 | -------------------------------------------------------------------------------- /bitfield/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django.forms import CheckboxSelectMultiple, IntegerField, ValidationError 4 | 5 | from django.utils.encoding import force_str 6 | 7 | from bitfield.types import BitHandler 8 | 9 | 10 | class BitFieldCheckboxSelectMultiple(CheckboxSelectMultiple): 11 | def render(self, name, value, attrs=None, choices=(), renderer=None): 12 | if isinstance(value, BitHandler): 13 | value = [k for k, v in value if v] 14 | elif isinstance(value, int): 15 | real_value = [] 16 | div = 2 17 | for (k, v) in self.choices: 18 | if value % div != 0: 19 | real_value.append(k) 20 | value -= (value % div) 21 | div *= 2 22 | value = real_value 23 | return super(BitFieldCheckboxSelectMultiple, self).render( 24 | name, value, attrs=attrs) 25 | 26 | def has_changed(self, initial, data): 27 | if initial is None: 28 | initial = [] 29 | if data is None: 30 | data = [] 31 | if initial != data: 32 | return True 33 | initial_set = set([force_str(value) for value in initial]) 34 | data_set = set([force_str(value) for value in data]) 35 | return data_set != initial_set 36 | 37 | 38 | class BitFormField(IntegerField): 39 | def __init__(self, choices=(), widget=BitFieldCheckboxSelectMultiple, *args, **kwargs): 40 | 41 | if isinstance(kwargs['initial'], int): 42 | iv = kwargs['initial'] 43 | iv_list = [] 44 | for i in range(0, min(len(choices), 63)): 45 | if (1 << i) & iv > 0: 46 | iv_list += [choices[i][0]] 47 | kwargs['initial'] = iv_list 48 | self.widget = widget 49 | super(BitFormField, self).__init__(widget=widget, *args, **kwargs) 50 | self.choices = self.widget.choices = choices 51 | 52 | def clean(self, value): 53 | if not value: 54 | return 0 55 | 56 | # Assume an iterable which contains an item per flag that's enabled 57 | result = BitHandler(0, [k for k, v in self.choices]) 58 | for k in value: 59 | try: 60 | setattr(result, str(k), True) 61 | except AttributeError: 62 | raise ValidationError('Unknown choice: %r' % (k,)) 63 | return int(result) 64 | -------------------------------------------------------------------------------- /bitfield/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import signals 2 | from django.db.models.fields import Field, BigIntegerField 3 | 4 | from bitfield.forms import BitFormField 5 | from bitfield.query import BitQueryLookupWrapper 6 | from bitfield.types import BitHandler, Bit 7 | 8 | # Count binary capacity. Truncate "0b" prefix from binary form. 9 | # Twice faster than bin(i)[2:] or math.floor(math.log(i)) 10 | MAX_FLAG_COUNT = int(len(bin(BigIntegerField.MAX_BIGINT)) - 2) 11 | 12 | 13 | class BitFieldFlags(object): 14 | def __init__(self, flags): 15 | if len(flags) > MAX_FLAG_COUNT: 16 | raise ValueError('Too many flags') 17 | self._flags = flags 18 | 19 | def __repr__(self): 20 | return repr(self._flags) 21 | 22 | def __iter__(self): 23 | for flag in self._flags: 24 | yield flag 25 | 26 | def __getattr__(self, key): 27 | if key == '_flags': 28 | # Since __getattr__ is for fallback, reaching here from Python 29 | # means that there's no '_flags' attribute in this object, 30 | # which may be caused by intermediate state while copying etc. 31 | raise AttributeError( 32 | "'%s' object has no attribute '%s'" % (self.__class__.__name__, key) 33 | ) 34 | try: 35 | flags = self._flags 36 | except AttributeError: 37 | raise AttributeError( 38 | "'%s' object has no attribute '%s'" % (self.__class__.__name__, key) 39 | ) 40 | try: 41 | flag = flags.index(key) 42 | except ValueError: 43 | raise AttributeError("flag {} is not registered".format(key)) 44 | return Bit(flag) 45 | 46 | def iteritems(self): 47 | for flag in self._flags: 48 | yield flag, Bit(self._flags.index(flag)) 49 | 50 | def iterkeys(self): 51 | for flag in self._flags: 52 | yield flag 53 | 54 | def itervalues(self): 55 | for flag in self._flags: 56 | yield Bit(self._flags.index(flag)) 57 | 58 | def items(self): 59 | return list(self.iteritems()) 60 | 61 | def keys(self): 62 | return list(self.iterkeys()) 63 | 64 | def values(self): 65 | return list(self.itervalues()) 66 | 67 | 68 | class BitFieldCreator(object): 69 | """ 70 | A placeholder class that provides a way to set the attribute on the model. 71 | Descriptor for BitFields. Checks to make sure that all flags of the 72 | instance match the class. This is to handle the case when caching 73 | an older version of the instance and a newer version of the class is 74 | available (usually during deploys). 75 | """ 76 | def __init__(self, field): 77 | self.field = field 78 | 79 | def __set__(self, obj, value): 80 | obj.__dict__[self.field.name] = self.field.to_python(value) 81 | 82 | def __get__(self, obj, type=None): 83 | if obj is None: 84 | return BitFieldFlags(self.field.flags) 85 | retval = obj.__dict__[self.field.name] 86 | if self.field.__class__ is BitField: 87 | # Update flags from class in case they've changed. 88 | retval._keys = self.field.flags 89 | return retval 90 | 91 | 92 | class BitField(BigIntegerField): 93 | 94 | def contribute_to_class(self, cls, name, **kwargs): 95 | super(BitField, self).contribute_to_class(cls, name, **kwargs) 96 | setattr(cls, self.name, BitFieldCreator(self)) 97 | 98 | def __init__(self, flags, default=None, *args, **kwargs): 99 | if isinstance(flags, dict): 100 | # Get only integer keys in correct range 101 | valid_keys = (k for k in flags.keys() if isinstance(k, int) and (0 <= k < MAX_FLAG_COUNT)) 102 | if not valid_keys: 103 | raise ValueError('Wrong keys or empty dictionary') 104 | # Fill list with values from dict or with empty values 105 | flags = [flags.get(i, '') for i in range(max(valid_keys) + 1)] 106 | 107 | if len(flags) > MAX_FLAG_COUNT: 108 | raise ValueError('Too many flags') 109 | 110 | self._arg_flags = flags 111 | flags = list(flags) 112 | labels = [] 113 | for num, flag in enumerate(flags): 114 | if isinstance(flag, (tuple, list)): 115 | flags[num] = flag[0] 116 | labels.append(flag[1]) 117 | else: 118 | labels.append(flag) 119 | 120 | if isinstance(default, (list, tuple, set, frozenset)): 121 | new_value = 0 122 | for flag in default: 123 | new_value |= Bit(flags.index(flag)) 124 | default = new_value 125 | 126 | BigIntegerField.__init__(self, default=default, *args, **kwargs) 127 | self.flags = flags 128 | self.labels = labels 129 | 130 | def formfield(self, form_class=BitFormField, **kwargs): 131 | choices = [(k, self.labels[self.flags.index(k)]) for k in self.flags] 132 | return Field.formfield(self, form_class, choices=choices, **kwargs) 133 | 134 | def get_prep_value(self, value): 135 | if value is None: 136 | return None 137 | if isinstance(value, (BitHandler, Bit)): 138 | value = value.mask 139 | return int(value) 140 | 141 | # def get_db_prep_save(self, value, connection): 142 | # if isinstance(value, Bit): 143 | # return BitQuerySaveWrapper(self.model._meta.db_table, self.name, value) 144 | # return super(BitField, self).get_db_prep_save(value, connection=connection) 145 | 146 | def to_python(self, value): 147 | if isinstance(value, Bit): 148 | value = value.mask 149 | if not isinstance(value, BitHandler): 150 | # Regression for #1425: fix bad data that was created resulting 151 | # in negative values for flags. Compute the value that would 152 | # have been visible ot the application to preserve compatibility. 153 | if isinstance(value, int) and value < 0: 154 | new_value = 0 155 | for bit_number, _ in enumerate(self.flags): 156 | new_value |= (value & (2 ** bit_number)) 157 | value = new_value 158 | 159 | value = BitHandler(value, self.flags, self.labels) 160 | else: 161 | # Ensure flags are consistent for unpickling 162 | value._keys = self.flags 163 | return value 164 | 165 | def deconstruct(self): 166 | name, path, args, kwargs = super(BitField, self).deconstruct() 167 | args.insert(0, self._arg_flags) 168 | return name, path, args, kwargs 169 | 170 | 171 | BitField.register_lookup(BitQueryLookupWrapper) 172 | 173 | 174 | class CompositeBitFieldWrapper(object): 175 | def __init__(self, fields): 176 | self.fields = fields 177 | 178 | def __getattr__(self, attr): 179 | if attr == 'fields': 180 | return super(CompositeBitFieldWrapper, self).__getattr__(attr) 181 | 182 | for field in self.fields: 183 | if hasattr(field, attr): 184 | return getattr(field, attr) 185 | raise AttributeError('%s is not a valid flag' % attr) 186 | 187 | def __hasattr__(self, attr): 188 | if attr == 'fields': 189 | return super(CompositeBitFieldWrapper, self).__hasattr__(attr) 190 | 191 | for field in self.fields: 192 | if hasattr(field, attr): 193 | return True 194 | return False 195 | 196 | def __setattr__(self, attr, value): 197 | if attr == 'fields': 198 | super(CompositeBitFieldWrapper, self).__setattr__(attr, value) 199 | return 200 | 201 | for field in self.fields: 202 | if hasattr(field, attr): 203 | setattr(field, attr, value) 204 | return 205 | raise AttributeError('%s is not a valid flag' % attr) 206 | 207 | 208 | class CompositeBitField(object): 209 | is_relation = False 210 | many_to_many = False 211 | concrete = False 212 | 213 | def __init__(self, fields): 214 | self.fields = fields 215 | 216 | def contribute_to_class(self, cls, name): 217 | self.name = name 218 | self.model = cls 219 | cls._meta.private_fields.append(self) 220 | 221 | signals.class_prepared.connect(self.validate_fields, sender=cls) 222 | 223 | setattr(cls, name, self) 224 | 225 | def validate_fields(self, sender, **kwargs): 226 | cls = sender 227 | model_fields = dict([ 228 | (f.name, f) for f in cls._meta.fields if f.name in self.fields]) 229 | all_flags = sum([model_fields[f].flags for f in self.fields], []) 230 | if len(all_flags) != len(set(all_flags)): 231 | raise ValueError('BitField flags must be unique.') 232 | 233 | def __get__(self, instance, instance_type=None): 234 | fields = [getattr(instance, f) for f in self.fields] 235 | return CompositeBitFieldWrapper(fields) 236 | 237 | def __set__(self, *args, **kwargs): 238 | raise NotImplementedError('CompositeBitField cannot be set.') 239 | -------------------------------------------------------------------------------- /bitfield/query.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from bitfield.types import Bit, BitHandler 4 | from django.db.models.lookups import Exact 5 | 6 | 7 | class BitQueryLookupWrapper(Exact): # NOQA 8 | def process_lhs(self, compiler, connection, lhs=None): 9 | lhs_sql, lhs_params = super(BitQueryLookupWrapper, self).process_lhs( 10 | compiler, connection, lhs) 11 | 12 | if not isinstance(self.rhs, (BitHandler, Bit)): 13 | return lhs_sql, lhs_params 14 | 15 | op = ' & ' if self.rhs else ' | ' 16 | rhs_sql, rhs_params = self.process_rhs(compiler, connection) 17 | params = list(lhs_params) 18 | params.extend(rhs_params) 19 | 20 | return op.join((lhs_sql, rhs_sql)), params 21 | 22 | def get_db_prep_lookup(self, value, connection): 23 | v = value.mask if isinstance(value, (BitHandler, Bit)) else value 24 | return super(BitQueryLookupWrapper, self).get_db_prep_lookup(v, connection) 25 | 26 | def get_prep_lookup(self): 27 | if isinstance(self.rhs, (BitHandler, Bit)): 28 | return self.rhs # resolve at later stage, in get_db_prep_lookup 29 | return super(BitQueryLookupWrapper, self).get_prep_lookup() 30 | 31 | 32 | class BitQuerySaveWrapper(BitQueryLookupWrapper): 33 | def as_sql(self, qn, connection): 34 | """ 35 | Create the proper SQL fragment. This inserts something like 36 | "(T0.flags & value) != 0". 37 | 38 | This will be called by Where.as_sql() 39 | """ 40 | engine = connection.settings_dict['ENGINE'].rsplit('.', -1)[-1] 41 | if engine.startswith('postgres'): 42 | XOR_OPERATOR = '#' 43 | elif engine.startswith('sqlite'): 44 | raise NotImplementedError 45 | else: 46 | XOR_OPERATOR = '^' 47 | 48 | if self.bit: 49 | return ("%s.%s | %d" % (qn(self.table_alias), qn(self.column), self.bit.mask), 50 | []) 51 | return ("%s.%s %s %d" % (qn(self.table_alias), qn(self.column), XOR_OPERATOR, self.bit.mask), 52 | []) 53 | -------------------------------------------------------------------------------- /bitfield/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disqus/django-bitfield/f287c0972764488463d60cf8aaa696ff00a764f3/bitfield/tests/__init__.py -------------------------------------------------------------------------------- /bitfield/tests/forms.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django import forms 4 | 5 | from bitfield.tests.models import BitFieldTestModel 6 | 7 | 8 | class BitFieldTestModelForm(forms.ModelForm): 9 | class Meta: 10 | model = BitFieldTestModel 11 | exclude = tuple() 12 | -------------------------------------------------------------------------------- /bitfield/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from bitfield import BitField, CompositeBitField 4 | 5 | 6 | class BitFieldTestModel(models.Model): 7 | flags = BitField(flags=( 8 | 'FLAG_0', 9 | 'FLAG_1', 10 | 'FLAG_2', 11 | 'FLAG_3', 12 | ), default=3, db_column='another_name') 13 | 14 | 15 | class CompositeBitFieldTestModel(models.Model): 16 | flags_1 = BitField(flags=( 17 | 'FLAG_0', 18 | 'FLAG_1', 19 | 'FLAG_2', 20 | 'FLAG_3', 21 | ), default=0) 22 | flags_2 = BitField(flags=( 23 | 'FLAG_4', 24 | 'FLAG_5', 25 | 'FLAG_6', 26 | 'FLAG_7', 27 | ), default=0) 28 | flags = CompositeBitField(( 29 | 'flags_1', 30 | 'flags_2', 31 | )) 32 | -------------------------------------------------------------------------------- /bitfield/tests/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import pickle 4 | 5 | from django.db import connection, models 6 | from django.db.models import F 7 | from django.test import TestCase 8 | 9 | from bitfield import BitHandler, Bit, BitField 10 | 11 | from .forms import BitFieldTestModelForm 12 | from .models import BitFieldTestModel, CompositeBitFieldTestModel 13 | 14 | 15 | class BitHandlerTest(TestCase): 16 | def test_comparison(self): 17 | bithandler_1 = BitHandler(0, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 18 | bithandler_2 = BitHandler(1, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 19 | bithandler_3 = BitHandler(0, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 20 | assert bithandler_1 == bithandler_1 21 | assert bithandler_1 != bithandler_2 22 | assert bithandler_1 == bithandler_3 23 | 24 | def test_defaults(self): 25 | bithandler = BitHandler(0, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 26 | # Default value of 0. 27 | self.assertEqual(int(bithandler), 0) 28 | # Test bit numbers. 29 | self.assertEqual(int(bithandler.FLAG_0.number), 0) 30 | self.assertEqual(int(bithandler.FLAG_1.number), 1) 31 | self.assertEqual(int(bithandler.FLAG_2.number), 2) 32 | self.assertEqual(int(bithandler.FLAG_3.number), 3) 33 | # Negative test non-existent key. 34 | self.assertRaises(AttributeError, lambda: bithandler.FLAG_4) 35 | # Test bool(). 36 | self.assertEqual(bool(bithandler.FLAG_0), False) 37 | self.assertEqual(bool(bithandler.FLAG_1), False) 38 | self.assertEqual(bool(bithandler.FLAG_2), False) 39 | self.assertEqual(bool(bithandler.FLAG_3), False) 40 | 41 | def test_nonzero_default(self): 42 | bithandler = BitHandler(1, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 43 | self.assertEqual(bool(bithandler.FLAG_0), True) 44 | self.assertEqual(bool(bithandler.FLAG_1), False) 45 | self.assertEqual(bool(bithandler.FLAG_2), False) 46 | self.assertEqual(bool(bithandler.FLAG_3), False) 47 | 48 | bithandler = BitHandler(2, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 49 | self.assertEqual(bool(bithandler.FLAG_0), False) 50 | self.assertEqual(bool(bithandler.FLAG_1), True) 51 | self.assertEqual(bool(bithandler.FLAG_2), False) 52 | self.assertEqual(bool(bithandler.FLAG_3), False) 53 | 54 | bithandler = BitHandler(3, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 55 | self.assertEqual(bool(bithandler.FLAG_0), True) 56 | self.assertEqual(bool(bithandler.FLAG_1), True) 57 | self.assertEqual(bool(bithandler.FLAG_2), False) 58 | self.assertEqual(bool(bithandler.FLAG_3), False) 59 | 60 | bithandler = BitHandler(4, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 61 | self.assertEqual(bool(bithandler.FLAG_0), False) 62 | self.assertEqual(bool(bithandler.FLAG_1), False) 63 | self.assertEqual(bool(bithandler.FLAG_2), True) 64 | self.assertEqual(bool(bithandler.FLAG_3), False) 65 | 66 | def test_mutation(self): 67 | bithandler = BitHandler(0, ('FLAG_0', 'FLAG_1', 'FLAG_2', 'FLAG_3')) 68 | self.assertEqual(bool(bithandler.FLAG_0), False) 69 | self.assertEqual(bool(bithandler.FLAG_1), False) 70 | self.assertEqual(bool(bithandler.FLAG_2), False) 71 | self.assertEqual(bool(bithandler.FLAG_3), False) 72 | 73 | bithandler = BitHandler(bithandler | 1, bithandler._keys) 74 | self.assertEqual(bool(bithandler.FLAG_0), True) 75 | self.assertEqual(bool(bithandler.FLAG_1), False) 76 | self.assertEqual(bool(bithandler.FLAG_2), False) 77 | self.assertEqual(bool(bithandler.FLAG_3), False) 78 | 79 | bithandler ^= 3 80 | self.assertEqual(int(bithandler), 2) 81 | 82 | self.assertEqual(bool(bithandler & 1), False) 83 | 84 | bithandler.FLAG_0 = False 85 | self.assertEqual(bithandler.FLAG_0, False) 86 | 87 | bithandler.FLAG_1 = True 88 | self.assertEqual(bithandler.FLAG_0, False) 89 | self.assertEqual(bithandler.FLAG_1, True) 90 | 91 | bithandler.FLAG_2 = False 92 | self.assertEqual(bithandler.FLAG_0, False) 93 | self.assertEqual(bithandler.FLAG_1, True) 94 | self.assertEqual(bithandler.FLAG_2, False) 95 | 96 | 97 | class BitTest(TestCase): 98 | def test_int(self): 99 | bit = Bit(0) 100 | self.assertEqual(int(bit), 1) 101 | self.assertEqual(bool(bit), True) 102 | self.assertFalse(not bit) 103 | 104 | def test_comparison(self): 105 | self.assertEqual(Bit(0), Bit(0)) 106 | self.assertNotEqual(Bit(1), Bit(0)) 107 | self.assertNotEqual(Bit(0, 0), Bit(0, 1)) 108 | self.assertEqual(Bit(0, 1), Bit(0, 1)) 109 | self.assertEqual(Bit(0), 1) 110 | 111 | def test_and(self): 112 | self.assertEqual(1 & Bit(2), 0) 113 | self.assertEqual(1 & Bit(0), 1) 114 | self.assertEqual(1 & ~Bit(0), 0) 115 | self.assertEqual(Bit(0) & Bit(2), 0) 116 | self.assertEqual(Bit(0) & Bit(0), 1) 117 | self.assertEqual(Bit(0) & ~Bit(0), 0) 118 | 119 | def test_or(self): 120 | self.assertEqual(1 | Bit(2), 5) 121 | self.assertEqual(1 | Bit(5), 33) 122 | self.assertEqual(1 | ~Bit(2), -5) 123 | self.assertEqual(Bit(0) | Bit(2), 5) 124 | self.assertEqual(Bit(0) | Bit(5), 33) 125 | self.assertEqual(Bit(0) | ~Bit(2), -5) 126 | 127 | def test_xor(self): 128 | self.assertEqual(1 ^ Bit(2), 5) 129 | self.assertEqual(1 ^ Bit(0), 0) 130 | self.assertEqual(1 ^ Bit(1), 3) 131 | self.assertEqual(1 ^ Bit(5), 33) 132 | self.assertEqual(1 ^ ~Bit(2), -6) 133 | self.assertEqual(Bit(0) ^ Bit(2), 5) 134 | self.assertEqual(Bit(0) ^ Bit(0), 0) 135 | self.assertEqual(Bit(0) ^ Bit(1), 3) 136 | self.assertEqual(Bit(0) ^ Bit(5), 33) 137 | self.assertEqual(Bit(0) ^ ~Bit(2), -6) 138 | 139 | 140 | class BitFieldTest(TestCase): 141 | def test_basic(self): 142 | # Create instance and make sure flags are working properly. 143 | instance = BitFieldTestModel.objects.create(flags=1) 144 | self.assertTrue(instance.flags.FLAG_0) 145 | self.assertFalse(instance.flags.FLAG_1) 146 | self.assertFalse(instance.flags.FLAG_2) 147 | self.assertFalse(instance.flags.FLAG_3) 148 | 149 | def test_regression_1425(self): 150 | # Creating new instances shouldn't allow negative values. 151 | instance = BitFieldTestModel.objects.create(flags=-1) 152 | self.assertEqual(instance.flags._value, 15) 153 | self.assertTrue(instance.flags.FLAG_0) 154 | self.assertTrue(instance.flags.FLAG_1) 155 | self.assertTrue(instance.flags.FLAG_2) 156 | self.assertTrue(instance.flags.FLAG_3) 157 | 158 | cursor = connection.cursor() 159 | flags_field = BitFieldTestModel._meta.get_field('flags') 160 | flags_db_column = flags_field.db_column or flags_field.name 161 | cursor.execute("INSERT INTO %s (%s) VALUES (-1)" % (BitFieldTestModel._meta.db_table, flags_db_column)) 162 | # There should only be the one row we inserted through the cursor. 163 | instance = BitFieldTestModel.objects.get(flags=-1) 164 | self.assertTrue(instance.flags.FLAG_0) 165 | self.assertTrue(instance.flags.FLAG_1) 166 | self.assertTrue(instance.flags.FLAG_2) 167 | self.assertTrue(instance.flags.FLAG_3) 168 | instance.save() 169 | 170 | self.assertEqual(BitFieldTestModel.objects.filter(flags=15).count(), 2) 171 | self.assertEqual(BitFieldTestModel.objects.filter(flags__lt=0).count(), 0) 172 | 173 | def test_select(self): 174 | BitFieldTestModel.objects.create(flags=3) 175 | self.assertTrue(BitFieldTestModel.objects.filter(flags=BitFieldTestModel.flags.FLAG_1).exists()) 176 | self.assertTrue(BitFieldTestModel.objects.filter(flags=BitFieldTestModel.flags.FLAG_0).exists()) 177 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=BitFieldTestModel.flags.FLAG_0).exists()) 178 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=BitFieldTestModel.flags.FLAG_1).exists()) 179 | 180 | def test_select_complex_expression(self): 181 | BitFieldTestModel.objects.create(flags=3) 182 | self.assertTrue(BitFieldTestModel.objects.filter(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_1)).exists()) 183 | self.assertTrue(BitFieldTestModel.objects.filter(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_0)).exists()) 184 | self.assertTrue(BitFieldTestModel.objects.filter(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_0).bitor(BitFieldTestModel.flags.FLAG_1)).exists()) 185 | self.assertTrue(BitFieldTestModel.objects.filter(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_0 | BitFieldTestModel.flags.FLAG_1)).exists()) 186 | self.assertTrue(BitFieldTestModel.objects.filter(flags=F('flags').bitand(15)).exists()) 187 | self.assertTrue(BitFieldTestModel.objects.exclude(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_2)).exists()) 188 | self.assertTrue(BitFieldTestModel.objects.exclude(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_3)).exists()) 189 | self.assertTrue(BitFieldTestModel.objects.exclude(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_2 | BitFieldTestModel.flags.FLAG_3)).exists()) 190 | self.assertTrue(BitFieldTestModel.objects.exclude(flags=F('flags').bitand(12)).exists()) 191 | 192 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_1)).exists()) 193 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_0)).exists()) 194 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_0).bitor(BitFieldTestModel.flags.FLAG_1)).exists()) 195 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_0 | BitFieldTestModel.flags.FLAG_1)).exists()) 196 | self.assertFalse(BitFieldTestModel.objects.exclude(flags=F('flags').bitand(15)).exists()) 197 | self.assertFalse(BitFieldTestModel.objects.filter(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_2)).exists()) 198 | self.assertFalse(BitFieldTestModel.objects.filter(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_3)).exists()) 199 | self.assertFalse(BitFieldTestModel.objects.filter(flags=F('flags').bitand(BitFieldTestModel.flags.FLAG_2 | BitFieldTestModel.flags.FLAG_3)).exists()) 200 | self.assertFalse(BitFieldTestModel.objects.filter(flags=F('flags').bitand(12)).exists()) 201 | 202 | def test_update(self): 203 | instance = BitFieldTestModel.objects.create(flags=0) 204 | self.assertFalse(instance.flags.FLAG_0) 205 | 206 | BitFieldTestModel.objects.filter(pk=instance.pk).update(flags=F('flags').bitor(BitFieldTestModel.flags.FLAG_1)) 207 | instance = BitFieldTestModel.objects.get(pk=instance.pk) 208 | self.assertTrue(instance.flags.FLAG_1) 209 | 210 | BitFieldTestModel.objects.filter(pk=instance.pk).update(flags=F('flags').bitor(((~BitFieldTestModel.flags.FLAG_0 | BitFieldTestModel.flags.FLAG_3)))) 211 | instance = BitFieldTestModel.objects.get(pk=instance.pk) 212 | self.assertFalse(instance.flags.FLAG_0) 213 | self.assertTrue(instance.flags.FLAG_1) 214 | self.assertTrue(instance.flags.FLAG_3) 215 | self.assertFalse(BitFieldTestModel.objects.filter(flags=BitFieldTestModel.flags.FLAG_0).exists()) 216 | 217 | BitFieldTestModel.objects.filter(pk=instance.pk).update(flags=F('flags').bitand(~BitFieldTestModel.flags.FLAG_3)) 218 | instance = BitFieldTestModel.objects.get(pk=instance.pk) 219 | self.assertFalse(instance.flags.FLAG_0) 220 | self.assertTrue(instance.flags.FLAG_1) 221 | self.assertFalse(instance.flags.FLAG_3) 222 | 223 | def test_update_with_handler(self): 224 | instance = BitFieldTestModel.objects.create(flags=0) 225 | self.assertFalse(instance.flags.FLAG_0) 226 | 227 | instance.flags.FLAG_1 = True 228 | 229 | BitFieldTestModel.objects.filter(pk=instance.pk).update(flags=F('flags').bitor(instance.flags)) 230 | instance = BitFieldTestModel.objects.get(pk=instance.pk) 231 | self.assertTrue(instance.flags.FLAG_1) 232 | 233 | def test_negate(self): 234 | BitFieldTestModel.objects.create(flags=BitFieldTestModel.flags.FLAG_0 | BitFieldTestModel.flags.FLAG_1) 235 | BitFieldTestModel.objects.create(flags=BitFieldTestModel.flags.FLAG_1) 236 | self.assertEqual(BitFieldTestModel.objects.filter(flags=~BitFieldTestModel.flags.FLAG_0).count(), 1) 237 | self.assertEqual(BitFieldTestModel.objects.filter(flags=~BitFieldTestModel.flags.FLAG_1).count(), 0) 238 | self.assertEqual(BitFieldTestModel.objects.filter(flags=~BitFieldTestModel.flags.FLAG_2).count(), 2) 239 | 240 | def test_default_value(self): 241 | instance = BitFieldTestModel.objects.create() 242 | self.assertTrue(instance.flags.FLAG_0) 243 | self.assertTrue(instance.flags.FLAG_1) 244 | self.assertFalse(instance.flags.FLAG_2) 245 | self.assertFalse(instance.flags.FLAG_3) 246 | 247 | def test_binary_capacity(self): 248 | import math 249 | from django.db.models.fields import BigIntegerField 250 | # Local maximum value, slow canonical algorithm 251 | MAX_COUNT = int(math.floor(math.log(BigIntegerField.MAX_BIGINT, 2))) 252 | 253 | # Big flags list 254 | flags = ['f' + str(i) for i in range(100)] 255 | 256 | try: 257 | BitField(flags=flags[:MAX_COUNT]) 258 | except ValueError: 259 | self.fail("It should work well with these flags") 260 | 261 | self.assertRaises(ValueError, BitField, flags=flags[:(MAX_COUNT + 1)]) 262 | 263 | def test_dictionary_init(self): 264 | flags = { 265 | 0: 'zero', 266 | 1: 'first', 267 | 10: 'tenth', 268 | 2: 'second', 269 | 270 | 'wrongkey': 'wrongkey', 271 | 100: 'bigkey', 272 | -100: 'smallkey', 273 | } 274 | 275 | try: 276 | bf = BitField(flags) 277 | except ValueError: 278 | self.fail("It should work well with these flags") 279 | 280 | self.assertEqual(bf.flags, ['zero', 'first', 'second', '', '', '', '', '', '', '', 'tenth']) 281 | self.assertRaises(ValueError, BitField, flags={}) 282 | self.assertRaises(ValueError, BitField, flags={'wrongkey': 'wrongkey'}) 283 | self.assertRaises(ValueError, BitField, flags={'1': 'non_int_key'}) 284 | 285 | def test_defaults_as_key_names(self): 286 | class TestModel(models.Model): 287 | flags = BitField(flags=( 288 | 'FLAG_0', 289 | 'FLAG_1', 290 | 'FLAG_2', 291 | 'FLAG_3', 292 | ), default=('FLAG_1', 'FLAG_2')) 293 | field = TestModel._meta.get_field('flags') 294 | self.assertEqual(field.default, TestModel.flags.FLAG_1 | TestModel.flags.FLAG_2) 295 | 296 | 297 | class BitFieldSerializationTest(TestCase): 298 | def test_can_unserialize_bithandler(self): 299 | bf = BitFieldTestModel() 300 | bf.flags.FLAG_0 = 1 301 | bf.flags.FLAG_1 = 0 302 | data = pickle.dumps(bf) 303 | inst = pickle.loads(data) 304 | self.assertTrue(inst.flags.FLAG_0) 305 | self.assertFalse(inst.flags.FLAG_1) 306 | 307 | def test_pickle_integration(self): 308 | inst = BitFieldTestModel.objects.create(flags=1) 309 | data = pickle.dumps(inst) 310 | inst = pickle.loads(data) 311 | self.assertEqual(type(inst.flags), BitHandler) 312 | self.assertEqual(int(inst.flags), 1) 313 | 314 | def test_added_field(self): 315 | bf = BitFieldTestModel() 316 | bf.flags.FLAG_0 = 1 317 | bf.flags.FLAG_1 = 0 318 | bf.flags.FLAG_3 = 0 319 | data = pickle.dumps(bf) 320 | inst = pickle.loads(data) 321 | self.assertTrue('FLAG_3' in inst.flags.keys()) 322 | 323 | 324 | class CompositeBitFieldTest(TestCase): 325 | def test_get_flag(self): 326 | inst = CompositeBitFieldTestModel() 327 | self.assertEqual(inst.flags.FLAG_0, inst.flags_1.FLAG_0) 328 | self.assertEqual(inst.flags.FLAG_4, inst.flags_2.FLAG_4) 329 | self.assertRaises(AttributeError, lambda: inst.flags.flag_NA) 330 | 331 | def test_set_flag(self): 332 | inst = CompositeBitFieldTestModel() 333 | 334 | flag_0_original = bool(inst.flags.FLAG_0) 335 | self.assertEqual(bool(inst.flags_1.FLAG_0), flag_0_original) 336 | flag_4_original = bool(inst.flags.FLAG_4) 337 | self.assertEqual(bool(inst.flags_2.FLAG_4), flag_4_original) 338 | 339 | # flip flags' bits 340 | inst.flags.FLAG_0 = not flag_0_original 341 | inst.flags.FLAG_4 = not flag_4_original 342 | 343 | # check to make sure the bit flips took effect 344 | self.assertNotEqual(bool(inst.flags.FLAG_0), flag_0_original) 345 | self.assertNotEqual(bool(inst.flags_1.FLAG_0), flag_0_original) 346 | self.assertNotEqual(bool(inst.flags.FLAG_4), flag_4_original) 347 | self.assertNotEqual(bool(inst.flags_2.FLAG_4), flag_4_original) 348 | 349 | def set_flag(): 350 | inst.flags.flag_NA = False 351 | self.assertRaises(AttributeError, set_flag) 352 | 353 | def test_hasattr(self): 354 | inst = CompositeBitFieldTestModel() 355 | self.assertEqual(hasattr(inst.flags, 'flag_0'), 356 | hasattr(inst.flags_1, 'flag_0')) 357 | self.assertEqual(hasattr(inst.flags, 'flag_4'), 358 | hasattr(inst.flags_2, 'flag_4')) 359 | 360 | 361 | class BitFormFieldTest(TestCase): 362 | def test_form_new_invalid(self): 363 | invalid_data_dicts = [ 364 | {'flags': ['FLAG_0', 'FLAG_FLAG']}, 365 | {'flags': ['FLAG_4']}, 366 | {'flags': [1, 2]} 367 | ] 368 | for invalid_data in invalid_data_dicts: 369 | form = BitFieldTestModelForm(data=invalid_data) 370 | self.assertFalse(form.is_valid()) 371 | 372 | def test_form_new(self): 373 | data_dicts = [ 374 | {'flags': ['FLAG_0', 'FLAG_1']}, 375 | {'flags': ['FLAG_3']}, 376 | {'flags': []}, 377 | {} 378 | ] 379 | for data in data_dicts: 380 | form = BitFieldTestModelForm(data=data) 381 | self.assertTrue(form.is_valid()) 382 | instance = form.save() 383 | flags = data['flags'] if 'flags' in data else [] 384 | for k in BitFieldTestModel.flags: 385 | self.assertEqual(bool(getattr(instance.flags, k)), k in flags) 386 | 387 | def test_form_update(self): 388 | instance = BitFieldTestModel.objects.create(flags=0) 389 | for k in BitFieldTestModel.flags: 390 | self.assertFalse(bool(getattr(instance.flags, k))) 391 | 392 | data = {'flags': ['FLAG_0', 'FLAG_1']} 393 | form = BitFieldTestModelForm(data=data, instance=instance) 394 | self.assertTrue(form.is_valid()) 395 | instance = form.save() 396 | for k in BitFieldTestModel.flags: 397 | self.assertEqual(bool(getattr(instance.flags, k)), k in data['flags']) 398 | 399 | data = {'flags': ['FLAG_2', 'FLAG_3']} 400 | form = BitFieldTestModelForm(data=data, instance=instance) 401 | self.assertTrue(form.is_valid()) 402 | instance = form.save() 403 | for k in BitFieldTestModel.flags: 404 | self.assertEqual(bool(getattr(instance.flags, k)), k in data['flags']) 405 | 406 | data = {'flags': []} 407 | form = BitFieldTestModelForm(data=data, instance=instance) 408 | self.assertTrue(form.is_valid()) 409 | instance = form.save() 410 | for k in BitFieldTestModel.flags: 411 | self.assertFalse(bool(getattr(instance.flags, k))) 412 | -------------------------------------------------------------------------------- /bitfield/types.py: -------------------------------------------------------------------------------- 1 | def cmp(a, b): 2 | return (a > b) - (a < b) 3 | 4 | 5 | class Bit(object): 6 | """ 7 | Represents a single Bit. 8 | """ 9 | def __init__(self, number, is_set=True): 10 | self.number = number 11 | self.is_set = bool(is_set) 12 | self.mask = 2 ** int(number) 13 | self.children = [] 14 | if not self.is_set: 15 | self.mask = ~self.mask 16 | 17 | def __repr__(self): 18 | return '<%s: number=%d, is_set=%s>' % (self.__class__.__name__, self.number, self.is_set) 19 | 20 | # def __str__(self): 21 | # if self.is_set: 22 | # return 'Yes' 23 | # return 'No' 24 | 25 | def __int__(self): 26 | return self.mask 27 | 28 | def __bool__(self): 29 | return self.is_set 30 | 31 | __nonzero__ = __bool__ 32 | 33 | def __eq__(self, value): 34 | if isinstance(value, Bit): 35 | return value.number == self.number and value.is_set == self.is_set 36 | elif isinstance(value, bool): 37 | return value == self.is_set 38 | elif isinstance(value, int): 39 | return value == self.mask 40 | return value == self.is_set 41 | 42 | def __ne__(self, value): 43 | return not self == value 44 | 45 | def __coerce__(self, value): 46 | return (self.is_set, bool(value)) 47 | 48 | def __invert__(self): 49 | return self.__class__(self.number, not self.is_set) 50 | 51 | def __and__(self, value): 52 | if isinstance(value, Bit): 53 | value = value.mask 54 | return value & self.mask 55 | 56 | def __rand__(self, value): 57 | if isinstance(value, Bit): 58 | value = value.mask 59 | return self.mask & value 60 | 61 | def __or__(self, value): 62 | if isinstance(value, Bit): 63 | value = value.mask 64 | return value | self.mask 65 | 66 | def __ror__(self, value): 67 | if isinstance(value, Bit): 68 | value = value.mask 69 | return self.mask | value 70 | 71 | def __lshift__(self, value): 72 | if isinstance(value, Bit): 73 | value = value.mask 74 | return value << self.mask 75 | 76 | def __rlshift__(self, value): 77 | if isinstance(value, Bit): 78 | value = value.mask 79 | return self.mask << value 80 | 81 | def __rshift__(self, value): 82 | if isinstance(value, Bit): 83 | value = value.mask 84 | return value >> self.mask 85 | 86 | def __rrshift__(self, value): 87 | if isinstance(value, Bit): 88 | value = value.mask 89 | return self.mask >> value 90 | 91 | def __xor__(self, value): 92 | if isinstance(value, Bit): 93 | value = value.mask 94 | return value ^ self.mask 95 | 96 | def __rxor__(self, value): 97 | if isinstance(value, Bit): 98 | value = value.mask 99 | return self.mask ^ value 100 | 101 | def __sentry__(self): 102 | return repr(self) 103 | 104 | def evaluate(self, evaluator, qn, connection): 105 | return self.mask, [] 106 | 107 | def prepare(self, evaluator, query, allow_joins): 108 | return evaluator.prepare_node(self, query, allow_joins) 109 | 110 | 111 | class BitHandler(object): 112 | """ 113 | Represents an array of bits, each as a ``Bit`` object. 114 | """ 115 | def __init__(self, value, keys, labels=None): 116 | # TODO: change to bitarray? 117 | if value: 118 | self._value = int(value) 119 | else: 120 | self._value = 0 121 | self._keys = keys 122 | self._labels = labels is not None and labels or keys 123 | 124 | def __eq__(self, other): 125 | if not isinstance(other, BitHandler): 126 | return False 127 | return self._value == other._value 128 | 129 | def __lt__(self, other): 130 | return int(self._value) < other 131 | 132 | def __le__(self, other): 133 | return int(self._value) <= other 134 | 135 | def __gt__(self, other): 136 | return int(self._value) > other 137 | 138 | def __ge__(self, other): 139 | return int(self._value) >= other 140 | 141 | def __cmp__(self, other): 142 | return cmp(self._value, other) 143 | 144 | def __repr__(self): 145 | return '<%s: %s>' % (self.__class__.__name__, ', '.join('%s=%s' % (k, self.get_bit(n).is_set) for n, k in enumerate(self._keys)),) 146 | 147 | def __str__(self): 148 | return str(self._value) 149 | 150 | def __int__(self): 151 | return self._value 152 | 153 | def __bool__(self): 154 | return bool(self._value) 155 | 156 | __nonzero__ = __bool__ 157 | 158 | def __and__(self, value): 159 | return BitHandler(self._value & int(value), self._keys) 160 | 161 | def __or__(self, value): 162 | return BitHandler(self._value | int(value), self._keys) 163 | 164 | def __add__(self, value): 165 | return BitHandler(self._value + int(value), self._keys) 166 | 167 | def __sub__(self, value): 168 | return BitHandler(self._value - int(value), self._keys) 169 | 170 | def __lshift__(self, value): 171 | return BitHandler(self._value << int(value), self._keys) 172 | 173 | def __rshift__(self, value): 174 | return BitHandler(self._value >> int(value), self._keys) 175 | 176 | def __xor__(self, value): 177 | return BitHandler(self._value ^ int(value), self._keys) 178 | 179 | def __contains__(self, key): 180 | bit_number = self._keys.index(key) 181 | return bool(self.get_bit(bit_number)) 182 | 183 | def __getattr__(self, key): 184 | if key.startswith('_'): 185 | return object.__getattribute__(self, key) 186 | if key not in self._keys: 187 | raise AttributeError('%s is not a valid flag' % key) 188 | return self.get_bit(self._keys.index(key)) 189 | 190 | def __setattr__(self, key, value): 191 | if key.startswith('_'): 192 | return object.__setattr__(self, key, value) 193 | if key not in self._keys: 194 | raise AttributeError('%s is not a valid flag' % key) 195 | self.set_bit(self._keys.index(key), value) 196 | 197 | def __iter__(self): 198 | return self.iteritems() 199 | 200 | def __sentry__(self): 201 | return repr(self) 202 | 203 | def _get_mask(self): 204 | return self._value 205 | mask = property(_get_mask) 206 | 207 | def evaluate(self, evaluator, qn, connection): 208 | return self.mask, [] 209 | 210 | def get_bit(self, bit_number): 211 | mask = 2 ** int(bit_number) 212 | return Bit(bit_number, self._value & mask != 0) 213 | 214 | def set_bit(self, bit_number, true_or_false): 215 | mask = 2 ** int(bit_number) 216 | if true_or_false: 217 | self._value |= mask 218 | else: 219 | self._value &= (~mask) 220 | return Bit(bit_number, self._value & mask != 0) 221 | 222 | def keys(self): 223 | return self._keys 224 | 225 | def iterkeys(self): 226 | return iter(self._keys) 227 | 228 | def items(self): 229 | return list(self.iteritems()) 230 | 231 | def iteritems(self): 232 | for k in self._keys: 233 | yield (k, getattr(self, k).is_set) 234 | 235 | def get_label(self, flag): 236 | if isinstance(flag, str): 237 | flag = self._keys.index(flag) 238 | if isinstance(flag, Bit): 239 | flag = flag.number 240 | return self._labels[flag] 241 | 242 | 243 | from django.core.exceptions import ImproperlyConfigured 244 | 245 | # We need to register adapters in order to prevent 246 | # "ProgrammingError: can't adapt type" 247 | try: 248 | from django.db.backends.sqlite3.base import Database 249 | Database.register_adapter(Bit, lambda x: int(x)) 250 | Database.register_adapter(BitHandler, lambda x: int(x)) 251 | except ImproperlyConfigured: 252 | pass 253 | 254 | try: 255 | from django.db.backends.postgresql.base import Database 256 | Database.extensions.register_adapter(Bit, lambda x: Database.extensions.AsIs(int(x))) 257 | Database.extensions.register_adapter(BitHandler, lambda x: Database.extensions.AsIs(int(x))) 258 | except ImproperlyConfigured: 259 | pass 260 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os.path 4 | import sys 5 | 6 | from django.conf import settings 7 | 8 | 9 | sys.path.insert(0, os.path.dirname(__file__)) 10 | 11 | 12 | def pytest_configure(config): 13 | if not settings.configured: 14 | test_db = os.environ.get('DB', 'sqlite') 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'NAME': 'bitfield', 19 | } 20 | } 21 | 22 | if test_db == 'postgres': 23 | DATABASES['default'].update({ 24 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 25 | 'USER': 'postgres', 26 | }) 27 | elif test_db == 'mysql': 28 | DATABASES['default'].update({ 29 | 'ENGINE': 'django.db.backends.mysql', 30 | 'USER': 'root', 31 | }) 32 | elif test_db == 'sqlite': 33 | DATABASES['default'].update({ 34 | 'ENGINE': 'django.db.backends.sqlite3', 35 | 'NAME': ':memory:', 36 | }) 37 | else: 38 | raise NotImplementedError 39 | 40 | settings.configure( 41 | DATABASES=DATABASES, 42 | INSTALLED_APPS=[ 43 | 'django.contrib.contenttypes', 44 | 'bitfield', 45 | 'bitfield.tests', 46 | ], 47 | ROOT_URLCONF='', 48 | DEBUG=False, 49 | ) 50 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | python_files=test*.py 3 | addopts=--tb=native -p no:doctest 4 | norecursedirs=bin dist docs htmlcov hooks node_modules .* {args} 5 | 6 | [flake8] 7 | ignore = F999,E501,E128,E124,E402,W503,E731,F841 8 | max-line-length = 100 9 | exclude = .tox,.git,docs 10 | 11 | [wheel] 12 | universal = 1 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import ast 4 | import os.path 5 | from setuptools import setup, find_packages 6 | 7 | class GetVersion(ast.NodeVisitor): 8 | def __init__(self, path): 9 | with open(path) as f: 10 | self.visit(ast.parse(f.read(), path)) 11 | 12 | def visit_Assign(self, node): 13 | if any(target.id == 'VERSION' for target in node.targets): 14 | assert not hasattr(self, 'VERSION') 15 | self.VERSION = node.value.value 16 | 17 | this_directory = os.path.abspath(os.path.dirname(__file__)) 18 | with open(os.path.join(this_directory, 'README.rst')) as f: 19 | long_description = f.read() 20 | 21 | setup( 22 | name='django-bitfield', 23 | version=GetVersion(os.path.join(os.path.dirname(__file__), 'bitfield', '__init__.py')).VERSION, 24 | author='Disqus', 25 | author_email='opensource@disqus.com', 26 | url='https://github.com/disqus/django-bitfield', 27 | description='BitField in Django', 28 | long_description=long_description, 29 | long_description_content_type='text/x-rst', 30 | packages=find_packages(), 31 | zip_safe=False, 32 | install_requires=[ 33 | 'Django>=1.11.29', 34 | ], 35 | extras_require={ 36 | 'tests': [ 37 | 'flake8', 38 | 'mysqlclient', 39 | 'psycopg2>=2.3', 40 | 'pytest-django', 41 | ], 42 | }, 43 | include_package_data=True, 44 | classifiers=[ 45 | 'Framework :: Django', 46 | 'Intended Audience :: Developers', 47 | 'Intended Audience :: System Administrators', 48 | 'Operating System :: OS Independent', 49 | 'Topic :: Software Development', 50 | "Programming Language :: Python", 51 | "Programming Language :: Python :: 3", 52 | "Programming Language :: Python :: 3.4", 53 | "Programming Language :: Python :: 3.5", 54 | "Programming Language :: Python :: 3.6", 55 | "Programming Language :: Python :: 3.7", 56 | "Programming Language :: Python :: 3.8", 57 | "Programming Language :: Python :: 3.9", 58 | ], 59 | ) 60 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Run Django versions with the Python versions that they support. 3 | # Taken from: 4 | # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django 5 | envlist = 6 | py{38,39,310}-django{32,40,41}-{sqlite,postgres}, 7 | py{38,39,310,311}-django{41}-{sqlite,postgres}, 8 | 9 | [gh-actions] 10 | python = 11 | 3.8: py38 12 | 3.9: py39 13 | 3.10: py310 14 | 3.11: py311 15 | 16 | [testenv] 17 | commands = 18 | py.test 19 | passenv = DB 20 | deps = 21 | pytest 22 | psycopg2-binary>=2.8 23 | django32: Django>=3.2,<3.3 24 | django32: pytest-django>=4.2 25 | django40: Django>=4.0,<4.1 26 | django40: pytest-django>=4.5 27 | django41: Django>=4.1,<4.2 28 | django41: pytest-django>=4.5 29 | setenv = 30 | sqlite: DB=sqlite 31 | postgres: DB=postgres 32 | --------------------------------------------------------------------------------