├── .codacy.yml ├── .gitignore ├── .landscape.yml ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── setup.py ├── src └── flags.py └── tests ├── __init__.py ├── test_arithmetic.py ├── test_base.py ├── test_decorators.py ├── test_flags.py └── test_pickling.py /.codacy.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | pylint: 3 | enabled: true 4 | python_version: 3 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.eggs/ 2 | /.idea/ 3 | /build/ 4 | /dist/ 5 | /src/py_flags.egg-info/ 6 | 7 | 8 | /.coverage 9 | /htmlcov/ 10 | 11 | /README.html 12 | /CHANGES.html 13 | 14 | *.py[cod] 15 | -------------------------------------------------------------------------------- /.landscape.yml: -------------------------------------------------------------------------------- 1 | pylint: 2 | disable: 3 | - unused-argument 4 | max-line-length: 120 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.5" 5 | - "3.6" 6 | - "3.7" 7 | - "3.8" 8 | - "3.9" 9 | - "pypy3" 10 | install: 11 | - pip install coveralls 12 | # Under python3.2 the latest coverage fails with a syntax error this is why we downgrade to 4.0a5 13 | - if [[ $TRAVIS_PYTHON_VERSION == 3.2 ]]; then pip install --upgrade coverage==4.0a5; fi 14 | script: 15 | - python -m compileall -f . 16 | - coverage run --source=flags setup.py test 17 | after_success: 18 | coveralls 19 | notifications: 20 | email: false 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 István Pásztor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | recursive-include tests *.py 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | py-flags 3 | ======== 4 | 5 | Type-safe (bit)flags 6 | """""""""""""""""""" 7 | 8 | 9 | .. image:: https://img.shields.io/travis/pasztorpisti/py-flags.svg?style=flat 10 | :target: https://travis-ci.org/pasztorpisti/py-flags 11 | :alt: build 12 | 13 | .. image:: https://img.shields.io/codacy/grade/6a7d15a84f524e98943678875d2f0dd8/master.svg?style=flat 14 | :target: https://www.codacy.com/app/pasztorpisti/py-flags 15 | :alt: code quality 16 | 17 | .. image:: https://img.shields.io/coveralls/pasztorpisti/py-flags/master.svg?style=flat 18 | :target: https://coveralls.io/r/pasztorpisti/py-flags?branch=master 19 | :alt: coverage 20 | 21 | .. image:: https://img.shields.io/pypi/v/py-flags.svg?style=flat 22 | :target: https://pypi.python.org/pypi/py-flags 23 | :alt: pypi 24 | 25 | .. image:: https://img.shields.io/github/tag/pasztorpisti/py-flags.svg?style=flat 26 | :target: https://github.com/pasztorpisti/py-flags 27 | :alt: github 28 | 29 | .. image:: https://img.shields.io/github/license/pasztorpisti/py-flags.svg?style=flat 30 | :target: https://github.com/pasztorpisti/py-flags/blob/master/LICENSE.txt 31 | :alt: license: MIT 32 | 33 | 34 | .. note:: 35 | 36 | Python 3.6+ users should consider using the ``Flag`` and ``IntFlag`` classes of the standard ``enum`` module. 37 | Those are very similar to the ``flags.Flags`` class of this repo. 38 | 39 | It's enough to read only the short Installation_ and `Quick Overview`_ sections to start using this module. 40 | The rest is about details. 41 | 42 | 43 | .. contents:: 44 | 45 | 46 | Installation 47 | ============ 48 | 49 | .. code-block:: sh 50 | 51 | pip install py-flags 52 | 53 | Alternatively you can download the distribution from the following places: 54 | 55 | - https://pypi.python.org/pypi/py-flags#downloads 56 | - https://github.com/pasztorpisti/py-flags/releases 57 | 58 | 59 | Quick Overview 60 | ============== 61 | 62 | With this module you can define type-safe (bit)flags. The style of the flag definition is very similar to the enum 63 | definitions you can create using the standard ``enum`` module of python 3. 64 | 65 | 66 | Defining flags with the ``class`` syntax: 67 | 68 | .. code-block:: python 69 | 70 | >>> from flags import Flags 71 | >>> 72 | >>> class TextStyle(Flags): 73 | >>> bold = 1 # value = 1 << 0 74 | >>> italic = 2 # value = 1 << 1 75 | >>> underline = 4 # value = 1 << 2 76 | 77 | 78 | In most cases you just want to use the flags as a set (of ``bool`` variables) and the actual flag values aren't 79 | important. To avoid manually setting unique flag values you can use auto assignment. To auto-assign a unique flag value 80 | use an empty iterable (for example empty tuple or list) as the value of the flag. Auto-assignment picks the first 81 | unused least significant bit for each auto-assignable flag in top-to-bottom order. 82 | 83 | .. code-block:: python 84 | 85 | >>> class TextStyle(Flags): 86 | >>> bold = () # value = 1 << 0 87 | >>> italic = () # value = 1 << 1 88 | >>> underline = () # value = 1 << 2 89 | 90 | 91 | As a shortcut you can call a flags class to create a subclass of it. This pattern has also been stolen from the 92 | standard ``enum`` module. The following flags definition is equivalent to the previous definition that uses the 93 | ``class`` syntax: 94 | 95 | 96 | .. code-block:: python 97 | 98 | >>> TextStyle = Flags('TextStyle', 'bold italic underline') 99 | 100 | 101 | Flags have human readable string representations and ``repr`` with more info: 102 | 103 | .. code-block:: python 104 | 105 | >>> print(TextStyle.bold) 106 | TextStyle.bold 107 | >>> print(repr(TextStyle.bold)) 108 | 109 | 110 | The type of a flag is the flags class it belongs to: 111 | 112 | .. code-block:: python 113 | 114 | >>> type(TextStyle.bold) 115 | 116 | >>> isinstance(TextStyle.bold, TextStyle) 117 | True 118 | 119 | 120 | You can combine flags with bool operators. The result is also an instance of the flags class with the previously 121 | described properties. 122 | 123 | .. code-block:: python 124 | 125 | >>> result = TextStyle.bold | TextStyle.italic 126 | >>> 127 | >>> print(result) 128 | TextStyle(bold|italic) 129 | >>> print(repr(result)) 130 | 131 | 132 | 133 | Operators work in a type-safe way: you can combine only flags of the same type. Trying to combine them with instances 134 | of other types results in error: 135 | 136 | .. code-block:: python 137 | 138 | >> result = TextStyle.bold | 1 139 | Traceback (most recent call last): 140 | File "", line 1, in 141 | TypeError: unsupported operand type(s) for |: 'TextStyle' and 'int' 142 | >>> 143 | >>> class OtherFlags(Flags): 144 | ... flag0 = () 145 | ... 146 | >>> result = TextStyle.bold | OtherFlags.flag0 147 | Traceback (most recent call last): 148 | File "", line 1, in 149 | TypeError: unsupported operand type(s) for |: 'TextStyle' and 'OtherFlags' 150 | 151 | 152 | Flags and their combinations (basically the instances of the flags class) are immutable and hashable so they can be 153 | used as set members and dictionary keys: 154 | 155 | .. code-block:: python 156 | 157 | >>> font_files = {} 158 | >>> font_files[TextStyle.bold] = 'bold.ttf' 159 | >>> font_files[TextStyle.italic] = 'italic.ttf' 160 | >>> font_files == {TextStyle.bold: 'bold.ttf', TextStyle.italic: 'italic.ttf'} 161 | True 162 | 163 | 164 | The flags you define automatically have two "virtual" flags: ``no_flags`` and ``all_flags``. ``no_flags`` is basically 165 | the zero flag and ``all_flags`` is the combination of all flags you've defined: 166 | 167 | .. code-block:: python 168 | 169 | >>> TextStyle.no_flags 170 | 171 | >>> TextStyle.all_flags 172 | 173 | 174 | 175 | Testing whether specific flags are set: 176 | 177 | .. code-block:: python 178 | 179 | >>> result = TextStyle.bold | TextStyle.italic 180 | >>> bool(result & TextStyle.bold) # 1. oldschool bit twiddling 181 | True 182 | >>> TextStyle.bold in result # 2. in operator 183 | True 184 | >>> result.bold # 3. attribute-style access 185 | True 186 | 187 | 188 | From the above testing methods the attribute-style access can check only the presence of a single flag. With the 189 | ``&`` and ``in`` operators you can check the presence of multiple flags at the same time: 190 | 191 | .. code-block:: python 192 | 193 | >>> result = TextStyle.bold | TextStyle.italic 194 | >>> 195 | >>> # True if at least one of the bold and underline flags is set 196 | >>> bool((TextStyle.bold | TextStyle.underline) & result) 197 | True 198 | >>> # True only when both the bold and underline flags are set 199 | >>> (TextStyle.bold | TextStyle.underline) in result 200 | False 201 | 202 | 203 | If for some reason you need the actual integer value of the flags then you can cast them to ``int``: 204 | 205 | .. code-block:: python 206 | 207 | >>> int(TextStyle.bold) 208 | 1 209 | 210 | 211 | You can convert the ``int()`` and ``str()`` representations of flags back into flags instances: 212 | 213 | .. code-block:: python 214 | 215 | >>> TextStyle(2) 216 | 217 | >>> TextStyle('TextStyle.bold') 218 | 219 | 220 | 221 | Flags type VS builtin python types 222 | ================================== 223 | 224 | You can find several discussions online questioning the pythonicity of using flags. The reason for this is that 225 | python provides several builtin types that provide flags-like functionality. Despite this you can still see some 226 | libraries (like the ``re`` module of python) that make use of flags usually in the form of an ``int`` value. 227 | 228 | I think that a flags type provides an interesting combination of the properties of the native python solutions 229 | that can make your code better in some cases. 230 | 231 | 232 | Instead of a flags type you can use the following solutions if you want to work with builtin python types: 233 | 234 | +------------------------------+-------------------------------------------------------------------------+ 235 | | Builtin type | How can we use it as flags? | 236 | +==============================+=========================================================================+ 237 | | ``int`` | Closes sibling of a full-featured flags class. No need for explanation. | 238 | +------------------------------+-------------------------------------------------------------------------+ 239 | | ``set``, ``frozenset`` | By giving each flag an id/name we can represent a set of flags by | 240 | | | putting only the name of the active bits/flags into the set. | 241 | +------------------------------+-------------------------------------------------------------------------+ 242 | | Several ``bool`` variables | We can store bits of a flag in separate ``bool`` variables: | 243 | | | | 244 | | | - as function args and locals | 245 | | | - as named ``bool`` values in dictionaries | 246 | | | - as attributes of an arbitrary object | 247 | +------------------------------+-------------------------------------------------------------------------+ 248 | 249 | A purpose-built flags type can provide all of the following features while all builtin python types lack at least some: 250 | 251 | - Easy to store and pass around as a single object (e.g.: as a function arg). 252 | - Easy way to combine "a set of ``bool`` variables"/flags with a single bitwise bool operation. 253 | - Flag with integer representation possibly with several bits set (sometimes comes in handy for FFI code). 254 | - Human readable ``str()`` and ``repr()`` for debugging and error messages. 255 | - Type safety: we should be able to combine only instances of the same flags type. 256 | - Immutability. 257 | 258 | Based on the above info it's easier to decide when it makes sense to use flags. In some cases the ``flags`` module 259 | absolutely rocks: 260 | 261 | - FFI code. 262 | - Having a lot of related ``bool`` variables that you often pass around in function calls. In this case using flags 263 | can simplify your function declarations (and other parts of the code) while adding/removing flags requires no change 264 | in function signatures. 265 | 266 | 267 | Flags class declaration 268 | ======================= 269 | 270 | 271 | Class attributes: flags VS your helper methods, properties and attributes 272 | ------------------------------------------------------------------------- 273 | 274 | A flags class attribute is treated as a flag if it isn't a descriptor and its name doesn't start with ``_``. 275 | For those who don't know what python descriptors are: methods and properties are descriptors so you 276 | can safely define helper methods and properties without being afraid that they are treated as flags. 277 | 278 | .. code-block:: python 279 | 280 | >>> from flags import Flags 281 | >>> 282 | >>> class TextStyle(Flags): 283 | >>> bold = 1 # value = 1 << 0 284 | >>> italic = 2 # value = 1 << 1 285 | >>> underline = 4 # value = 1 << 2 286 | >>> 287 | >>> # this isn't treated as a flag because of the '_' prefix 288 | >>> _extra_data = 42 289 | >>> 290 | >>> @property 291 | >>> def helper_property(self): 292 | >>> ... 293 | >>> 294 | >>> def helper_method(self): 295 | >>> ... 296 | 297 | 298 | Possible ways to define flag values 299 | ----------------------------------- 300 | 301 | Each flag in your flags class has an integer value (bitmask) and also an optional user defined app-specific data object. 302 | Class attributes that define your flags can have the following values: 303 | 304 | 1. An integer value: bits=integer_value, data=\ ``flags.UNDEFINED`` 305 | 2. An iterable of ... 306 | 1. 0 items: bits=, data=\ ``flags.UNDEFINED`` 307 | 2. 1 item: bits=, data=iterable[0] 308 | 3. 2 items: bits=iterable[0], data=iterable[1] 309 | 310 | .. code-block:: python 311 | 312 | >>> from flags import Flags 313 | >>> 314 | >>> class FlagValueAssignmentExample(Flags): 315 | >>> # 1. bits=42, data=flags.UNDEFINED 316 | >>> flag1 = 42 317 | >>> 318 | >>> # 2.1. bits=, data=flags.UNDEFINED 319 | >>> flag21_1 = () 320 | >>> flag21_2 = [] 321 | >>> 322 | >>> # 2.2. bits=, data='my_data' 323 | >>> flag22_1 = 'my_data', # a tuple with 1 item 324 | >>> flag22_2 = ('my_data',) 325 | >>> flag22_3 = ['my_data'] 326 | >>> 327 | >>> # 2.3. bits=42, data='my_data' 328 | >>> flag23_1 = 42, 'my_data' # a tuple with 2 items 329 | >>> flag23_2 = (42, 'my_data') 330 | >>> flag23_3 = [42, 'my_data'] 331 | 332 | 333 | Auto-assignment processes auto-assignable flag definitions in top-to-bottom order and picks the first unused least 334 | significant bit for each. We treat a bit as used if it has been used by any flags that aren't auto-assignable 335 | including those that are defined below the currently auto-assigned flag. 336 | 337 | See the `Instance methods and properties`_ section to find out how to access the bits and the user defined 338 | data of flag members. 339 | 340 | 341 | Aliases 342 | ------- 343 | 344 | If you define more than one flags with the same bits then these flags are aliases to the first flag that has 345 | been defined with the given bits. In this case only the first flag member is allowed to define user data. 346 | Trying to define data in aliases results in error. 347 | 348 | .. code-block:: python 349 | 350 | >>> class AliasExample(Flags): 351 | >>> flag1 = 1, 'user_data1' 352 | >>> flag2 = 2, 'user_data2' 353 | >>> 354 | >>> # Alias for flag1 because it has the same bit value (1) 355 | >>> flag1_alias1 = 1 356 | >>> 357 | >>> # The flag definition below would cause an error because 358 | >>> # aliases aren't allowed to define user data. 359 | >>> # flag1_alias2 = 1, 'alias_user_data' 360 | 361 | 362 | Inheritance 363 | ----------- 364 | 365 | If a flags class has already defined at least one flag then it is considered to be final. Trying to subclass it 366 | results in error. Extending an existing flags class with additional flag members and behavior through subclassing 367 | is semantically undesired (just like in case of enums). 368 | 369 | You can however define and subclass your own customized flags base class given that it doesn't define any flags. 370 | This is useful if you want to share utility functions/properties between your flags classes or if you want to 371 | customize some special class attributes (like `__no_flags_name__`_ and `__all_flags_name__`_) for multiple flags 372 | classes in one base class. 373 | 374 | .. code-block:: python 375 | 376 | >>> # defining a project-wide customized flags base class 377 | >>> class BaseFlags(Flags): 378 | >>> # setting the project-wide pickle serialization mode 379 | >>> __pickle_int_flags__ = True 380 | >>> 381 | >>> # changing the default 'no_flags' to 'none' 382 | >>> __no_flags_name__ = 'none' 383 | >>> 384 | >>> # changing the default 'all_flags' to 'all' 385 | >>> __all_flags_name__ = 'all' 386 | >>> 387 | >>> @property 388 | >>> def helper_property_shared_by_subclasses(self): 389 | >>> ... 390 | 391 | 392 | Subclassing with the function call syntax 393 | ----------------------------------------- 394 | 395 | To create a subclass of an existing (non-final) flags class you can also call it. In this case the flags class 396 | provides the following signature: 397 | 398 | **FlagsClass**\ *(class_name, flags, \*, mixins=(), module=None, qualname=None, no_flags_name=flags.UNDEFINED, all_flags_name=flags.UNDEFINED)* 399 | 400 | The return value of this function call is the newly created subclass. 401 | 402 | The format of the ``flags`` parameter can be one of the following: 403 | 404 | - A space and/or comma separated list of flag names. E.g.: ``'flag0 flag1 flag2'`` or ``'flag0, flag1, flag2'`` 405 | - An iterable of flag names. E.g.: ``['flag0', 'flag1']`` 406 | - An iterable of ``(name, value)`` pairs where value defines the bits and/or the data for this flag as described in 407 | the `Possible ways to define flag values`_ section. 408 | - A mapping (e.g.: ``dict``) where the keys are flag names and the values define the bits and/or data for the flags 409 | as described in the `Possible ways to define flag values`_ section. 410 | 411 | The ``module`` and ``qualname`` parameters have to be specified only if you want to use the the created flags class 412 | with pickle. In this case ``module`` and ``qualname`` should point to a place from where pickle can import the 413 | created flags class. For flags classes that reside at module level it's enough to define only ``module`` and 414 | ``class_name`` for pickle support. ``qualname`` is optional and works only with python 3.4+ with pickle protocol 4. 415 | 416 | 417 | .. code-block:: 418 | 419 | >>> class MyBaseFlags(Flags): 420 | ... __no_flags_name__ = 'none' 421 | ... __all_flags_name__ = 'all' 422 | ... 423 | >>> FlagsClass1 = Flags('FlagsClass1', 'flag0 flag1') 424 | >>> FlagsClass2 = MyBaseFlags('FlagsClass2', ['flag0', 'flag1']) 425 | >>> FlagsClass3 = Flags('FlagsClass3', '', no_flags_name='zero', all_flags_name='all') 426 | >>> FlagsClass4 = FlagsClass3('FlagsClass4', dict(flag4=4, flag8=8)) 427 | 428 | 429 | Supported operations 430 | ==================== 431 | 432 | Instance methods and properties 433 | ------------------------------- 434 | 435 | *property* Flags.\ **properties** 436 | 437 | If this instance has the same bits as one of the flags you have defined in the flags class then this property 438 | is an object with some extra info for that flag member definition otherwise ``None``. Note that if you are using 439 | flag aliases then all aliases share the same properties object. 440 | 441 | The returned object has the following readonly attributes: 442 | 443 | ``name`` 444 | 445 | The name of the flag. 446 | 447 | ``bits`` 448 | 449 | The integer value associated with this flag. 450 | 451 | ``data`` 452 | 453 | The user defined application-specific data for this flag. The value of this is ``flags.UNDEFINED`` if you 454 | haven't defined any user-data for this flag. 455 | 456 | ``index`` 457 | 458 | The zero based index of this flag in the flags class. 459 | 460 | ``index_without_aliases`` 461 | 462 | The zero based index of this flag in the flags class excluding the aliases. 463 | 464 | *property* Flags.\ **name** 465 | 466 | Returns ``None`` if the ``properties`` property is ``None`` otherwise returns ``properties.name``. 467 | 468 | *property* Flags.\ **data** 469 | 470 | Returns ``flags.UNDEFINED`` if the ``properties`` property is ``None`` otherwise returns ``properties.data``. 471 | 472 | .. _`Flags.to_simple_str()`: 473 | 474 | Flags.\ **to_simple_str**\ *()* 475 | 476 | While ``Flags.__str__()`` returns a long string representation that always contains the flags class name 477 | (e.g.: ``'TextStyle()'``, ``'TextStyle.bold'`` or ``'TextStyle(bold|italic)'``) this method returns a simplified 478 | string without the classname. This simple string is an empty string for the zero flag or the ``'|'`` concatenated 479 | list of flag names otherwise. Examples: ``''``, ``'bold'``, ``'bold|italic'`` 480 | 481 | Flags.\ **__iter__**\ *()* and Flags.\ **__len__**\ *()* 482 | 483 | Iterating over a flags class instance yields all flags class members that are part of this flag instance. 484 | Flag aliases are excluded from the yielded items. 485 | A flags class member is part of this flag instance if the ``flags_class_member in flags_instance`` expression is 486 | ``True``. ``len(flags_instance)`` returns the number of items returned by iteration. 487 | 488 | .. code-block:: python 489 | 490 | >>> from flags import Flags 491 | >>> 492 | >>> class Example(Flags): 493 | ... flag_1 = 1 494 | ... flag_2 = 2 495 | ... # Note: flag_3 is the combination of flag_1 and flag_2 496 | ... flag_3 = 3 497 | ... flag_4 = 4 498 | ... # Alias for flag_4 499 | ... flag_4_alias = 4 500 | ... 501 | >>> list(iter(Example.no_flags)) 502 | [] 503 | >>> len(Example.no_flags) 504 | 0 505 | 506 | >>> list(Example.all_flags) 507 | [, , 508 | , ] 509 | >>> len(Example.all_flags) 510 | 4 511 | 512 | >>> list(Example.flag_1) 513 | [] 514 | >>> len(Example.flag_1) 515 | 1 516 | 517 | >>> list(Example.flag_2) 518 | [] 519 | >>> len(Example.flag_2) 520 | 1 521 | 522 | >>> list(Example.flag_3) 523 | [, , 524 | ] 525 | >>> len(Example.flag_3) 526 | 3 527 | 528 | >>> list(Example.flag_4) 529 | [] 530 | >>> len(Example.flag_4) 531 | 1 532 | 533 | >>> list(Example.flag_4_alias) 534 | [] 535 | >>> len(Example.flag_4_alias) 536 | 1 537 | 538 | >>> list(Example.flag_1 | Example.flag_4) 539 | [, ] 540 | >>> len(Example.flag_1 | Example.flag_4) 541 | 2 542 | 543 | 544 | .. note:: 545 | 546 | Under the hood ``__len__()`` uses iteration to count the number of contained flag members. 547 | 548 | 549 | Flags.\ **__hash__**\ *()* 550 | 551 | Flags class instances are immutable and hashable. You can use the builtin ``hash()`` function to hash them and 552 | you can use them as set members and mapping keys. 553 | 554 | 555 | Flags.\ **__eq__**\ *()*, Flags.\ **__ne__**\ *()*, Flags.\ **__ge__**\ *()*, Flags.\ **__gt__**\ *()*, 556 | Flags.\ **__le__**\ *()*, Flags.\ **__lt__**\ *()* 557 | 558 | Comparison operators on flag instances work similarly as in case of native python ``set``\ s. 559 | Two flag instances are equal only if their bits are the same. A flags instance is less than or equal to another 560 | flags instance only if its bits are a subset of the bits of the other one. The first flags instance is less than 561 | the second one if its bits are a **proper/strict** subset (is subset, but not equal) of the bits of the other one. 562 | 563 | Flags.\ **__int__**\ *()* 564 | 565 | A flags instance can be converted to an ``int`` using the ``int(flags_instance)`` expression. This conversion 566 | returns the bits of the flags instance. 567 | 568 | Flags.\ **__bool__**\ *()* 569 | 570 | A flags instance can be converted to a ``bool`` value using the ``bool(flags_instance)`` expression. The result 571 | is ``False`` only if the instance is the zero flag. 572 | 573 | Flags.\ **__contains__**\ *()* 574 | 575 | A flags instance is contained by another instance if the bits of the first one is a subset of the second one. 576 | The ``flags_instance1 in flags_instance2`` expression has the same value as the 577 | ``flags_instance1 <= flags_instance2`` expression. 578 | 579 | Flags.\ **is_disjoint**\ *(\*flags_instances)* 580 | 581 | The return value is ``True`` only if the flags instance on which we called ``is_dijoint()`` has no common bit 582 | with any of the flags instances passed as a parameters. 583 | 584 | Flags.\ **__or__**\ *()*, Flags.\ **__xor__**\ *()*, Flags.\ **__and__**\ *()* 585 | 586 | Bitwise bool operators (``|``, ``^``, ``&``) combine the bits of two flags instances and return a new immutable 587 | flags instance that wraps the combined bits. 588 | 589 | Flags.\ **__invert__**\ *()* 590 | 591 | Applying the unary ``~`` operator returns a new immutable flags instance that contains the inverted bits of the 592 | original flags instance. Note that inversion affects only those bits that are included in the ``__all_flags__`` 593 | of this flag type. 594 | 595 | Flags.\ **__sub__**\ *()* 596 | 597 | Subtracting flags instances is similar to subtracting native python ``set`` instances. The result of 598 | ``flags1 - flags2`` is a new flags instance that contains all bits that are set in ``flags1`` but aren't set 599 | in ``flags2``. We could also say that ``flags1 - flags2`` is the same as ``flags1 & ~flags2``. 600 | 601 | 602 | Class methods 603 | ------------- 604 | 605 | *classmethod* Flags.\ **__iter__**\ *()* and Flags.\ **__len__**\ *()* 606 | 607 | Iterating a flags class yields all non-alias flags you've declared for the class. 608 | ``len(flags_class)`` returns the number of non-alias flags declared for the class. 609 | 610 | *classmethod* Flags.\ **__getitem__**\ *()* 611 | 612 | You can access the members of a flags class not only as class attributes (``FlagsClass.flag``) but also 613 | with the subscript notation (``FlagsClass['flag']``). 614 | 615 | *classmethod* Flags.\ **from_simple_str**\ *(s)* 616 | 617 | Converts the output of `Flags.to_simple_str()`_ into a flags instance. 618 | 619 | *classmethod* Flags.\ **from_str**\ *(s)* 620 | 621 | Converts the output of `Flags.to_simple_str()`_ or ``Flags.__str__()`` into a flags instance. 622 | 623 | *classmethod* Flags.\ **bits_from_simple_str**\ *(s)* 624 | 625 | Converts the output of `Flags.to_simple_str()`_ into an integer (bits). 626 | 627 | *classmethod* Flags.\ **bits_from_str**\ *(s)* 628 | 629 | Converts the output of `Flags.to_simple_str()`_ or ``Flags.__str__()`` into an integer (bits). 630 | 631 | 632 | The ``@unique`` and ``@unique_bits`` decorators 633 | =============================================== 634 | 635 | You can apply the ``@unique`` and ``@unique_bits`` operators only to "final" flags classes that have flag members 636 | defined. Trying to apply them onto base classes without any flag members results in error. 637 | 638 | ``@unique`` forbids the declaration of aliases. In fact, originally I wanted to call this decorator ``@no_aliases`` 639 | but decided to use ``@unique`` to follow the conventions used by the standard ``enum`` module. 640 | A flags class with this decorator can not have two flags defined with the exact same bits (but a few overlapping 641 | bits are still allowed). 642 | 643 | ``@unique_bits`` ensures that there isn't a single bit that is shared by any two members of the flags class. 644 | Note that ``@unique_bits`` is a much stricter requirement than ``@unique`` and applying ``@unique`` along with this 645 | decorator is unnecessary and redundant (but not harmful or forbidden). 646 | 647 | 648 | Serialization 649 | ============= 650 | 651 | 652 | Pickle 653 | ------ 654 | 655 | Flags class instances are pickle serializable. In case of python 3.3 and lower the picklable flags class has to 656 | be declared at module level in order to make it importable for pickle. From python 3.4 pickle protocol 4 can 657 | deal with ``__qualname__`` so can declare serializable flags classes at a deeper scope. 658 | 659 | Note that the pickle support by default saves the flags class (name) along with the output of `Flags.to_simple_str()`_ 660 | to the pickled stream. To save the bits of instances (an integer) instead of the `Flags.to_simple_str()`_ output 661 | set the `__pickle_int_flags__`_ class attribute to ``True``. 662 | 663 | 664 | Custom serialization 665 | -------------------- 666 | 667 | If you want to roll your own serializer instead of using pickle then it is recommended to use the same 668 | strategy as pickle - your serializer should remember: 669 | 670 | 1. the flags class 671 | 2. the ``int`` or ``string`` representation of the flags class instances 672 | 673 | You can retrieve the ``int`` representation of a flags instance with ``int(flags_instance)`` while the recommended 674 | string representation for serialization can be acquired using `Flags.to_simple_str()`_. ``str(flags_instance)`` 675 | would also work but it is unnecessarily verbose compared to the ``to_simple_str()`` output. 676 | 677 | You can convert the integer and string representations back to flags instances by calling the flags class itself 678 | with the given integer or string as a single argument. E.g.: ``flags_instance = flags_class(int_representation)`` 679 | 680 | 681 | Implementation details 682 | ====================== 683 | 684 | 685 | Introspection 686 | ------------- 687 | 688 | 689 | Flags classes have some special attributes that may come in handy for introspection. 690 | 691 | ``__all_members__`` 692 | 693 | This is a readonly ordered dictionary that contains all members including the aliases and also the special 694 | ``no_flags`` and ``all_flags`` members. The dictionary keys store member names and the values are flags class 695 | instances. 696 | 697 | .. note:: 698 | 699 | If you customize the names of special members through the ``__no_flag_name__`` and ``__all_flag_name__`` 700 | class attributes then this dictionary contains the customized names. 701 | 702 | ``__members__`` 703 | 704 | Same as ``__all_members__`` but this doesn't contain the special ``no_flags`` and ``all_flags`` members. 705 | This dictionary contains only the members including the aliases. 706 | 707 | ``__members_without_aliases__`` 708 | 709 | Same as ``__members__`` but without the aliases. This doesn't contain the special ``no_flags`` and ``all_flags`` 710 | or any aliases. 711 | 712 | ``__member_aliases__`` 713 | 714 | An ordered dictionary in which each key is the name of an alias and the associated value is the name of the 715 | aliased member. 716 | 717 | ``__no_flags__`` 718 | 719 | An instance of the flags class: the zero flag. 720 | 721 | ``__all_flags__`` 722 | 723 | The bitwise or combination of all members that have been declared in this class. 724 | 725 | .. _`__no_flags_name__`: 726 | 727 | ``__no_flags_name__`` 728 | 729 | A string that specifies the name of an alias for the ``__no_flags__`` class attribute. 730 | By default the value of ``__no_flags_name__`` is ``'no_flags'`` which means that the zero flag can be accessed 731 | not only through the ``__no_flags__`` class attribute but also as ``no_flags``. 732 | 733 | The interesting thing about ``__no_flags_name__`` is that it can be customized during flags class declaration 734 | so the name of this alias can be used to give the zero flag a name that is 735 | specific to a flags class (e.g.: ``'Unknown'``). A project can also use this name to customize the name of the 736 | zero flag in a project specific flags base class to match the flags class member naming convention of the project 737 | (if the default ``'no_flags'`` isn't good). By setting ``__no_flags_name__`` to ``None`` we can prevent the 738 | creation of an alias for ``__no_flags__``. 739 | 740 | .. _`__all_flags_name__`: 741 | 742 | ``__all_flags_name__`` 743 | 744 | A string that specifies the name of an alias for ``__all_flags__``. Works in a similar way as ``__no_flags_name__``. 745 | 746 | .. _`__pickle_int_flags__`: 747 | 748 | ``__pickle_int_flags__`` 749 | 750 | By default the pickle serializer support saves the names of flags. By setting ``__pickle_int_flags__`` to ``True`` 751 | you can ask the pickle support to save the ``int`` value of serialized flags instead of the names. 752 | 753 | ``__dotted_single_flag_str__`` 754 | 755 | By default ``__str__()`` handles flag instances with only a single flag set specially. For the zero flag it 756 | outputs ``'FlagsClass()'``, for a single flag it outputs ``'FlagsClass.flag1'`` and for multiple flags it's 757 | ``'FlagsClass(flag1|flag2)'``. If you set ``__dotted_single_flag_str__`` to ``False`` then the output for 758 | a single flag changes to ``'FlagsClass(flag1)'``. This matches the format of the output for zero and 759 | multiple flags. 760 | 761 | 762 | Efficiency 763 | ---------- 764 | 765 | A flag object has only a single instance attribute that stores an integer (flags). 766 | The storage of this instance attribute is optimized using ``__slots__``. Your flags classes aren't allowed to add 767 | or use instance variables and you can not define ``__slots__``. Trying to do so results in error. 768 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import codecs 3 | import os 4 | import re 5 | 6 | from setuptools import setup 7 | 8 | 9 | script_dir = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def read_text_file(path): 13 | with codecs.open(path, 'r', 'utf-8') as f: 14 | return f.read() 15 | 16 | 17 | def find_version(*path): 18 | contents = read_text_file(os.path.join(script_dir, *path)) 19 | 20 | # The version line must have the form 21 | # version_info = (X, Y, Z) 22 | m = re.search( 23 | r'^version_info\s*=\s*\(\s*(?P\d+)\s*,\s*(?P\d+)\s*,\s*(?P\d+)\s*\)\s*$', 24 | contents, 25 | re.MULTILINE, 26 | ) 27 | if m: 28 | return '%s.%s.%s' % (m.group('v0'), m.group('v1'), m.group('v2')) 29 | raise RuntimeError('Unable to determine package version.') 30 | 31 | 32 | setup( 33 | name='py-flags', 34 | version=find_version('src', 'flags.py'), 35 | description='Type-safe (bit)flags for python 3', 36 | long_description=read_text_file(os.path.join(script_dir, 'README.rst')), 37 | keywords='flags bit flag set bitfield bool arithmetic', 38 | 39 | url='https://github.com/pasztorpisti/py-flags', 40 | 41 | author='István Pásztor', 42 | author_email='pasztorpisti@gmail.com', 43 | 44 | license='MIT', 45 | 46 | classifiers=[ 47 | 'License :: OSI Approved :: MIT License', 48 | 49 | 'Development Status :: 5 - Production/Stable', 50 | 'Intended Audience :: Developers', 51 | 'Topic :: Software Development :: Libraries :: Python Modules', 52 | 53 | 'Programming Language :: Python :: 3', 54 | 'Programming Language :: Python :: Implementation :: CPython', 55 | 'Programming Language :: Python :: Implementation :: PyPy', 56 | ], 57 | 58 | install_requires=['dictionaries==0.0.2'], 59 | py_modules=['flags'], 60 | package_dir={'': 'src'}, 61 | 62 | test_suite='tests', 63 | ) 64 | -------------------------------------------------------------------------------- /src/flags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import collections 3 | import functools 4 | import pickle 5 | 6 | from collections.abc import Iterable, Mapping, Set 7 | 8 | from dictionaries import ReadonlyDictProxy 9 | 10 | __all__ = ['Flags', 'FlagsMeta', 'FlagData', 'UNDEFINED', 'unique', 'unique_bits'] 11 | 12 | 13 | # version_info[0]: Increase in case of large milestones/releases. 14 | # version_info[1]: Increase this and zero out version_info[2] if you have explicitly modified 15 | # a previously existing behavior/interface. 16 | # If the behavior of an existing feature changes as a result of a bugfix 17 | # and the new (bugfixed) behavior is that meets the expectations of the 18 | # previous interface documentation then you shouldn't increase this, in that 19 | # case increase only version_info[2]. 20 | # version_info[2]: Increase in case of bugfixes. Also use this if you added new features 21 | # without modifying the behavior of the previously existing ones. 22 | version_info = (1, 1, 4) 23 | __version__ = '.'.join(str(n) for n in version_info) 24 | __author__ = 'István Pásztor' 25 | __license__ = 'MIT' 26 | 27 | 28 | def unique(flags_class): 29 | """ A decorator for flags classes to forbid flag aliases. """ 30 | if not is_flags_class_final(flags_class): 31 | raise TypeError('unique check can be applied only to flags classes that have members') 32 | if not flags_class.__member_aliases__: 33 | return flags_class 34 | aliases = ', '.join('%s -> %s' % (alias, name) for alias, name in flags_class.__member_aliases__.items()) 35 | raise ValueError('duplicate values found in %r: %s' % (flags_class, aliases)) 36 | 37 | 38 | def unique_bits(flags_class): 39 | """ A decorator for flags classes to forbid declaring flags with overlapping bits. """ 40 | flags_class = unique(flags_class) 41 | other_bits = 0 42 | for name, member in flags_class.__members_without_aliases__.items(): 43 | bits = int(member) 44 | if other_bits & bits: 45 | for other_name, other_member in flags_class.__members_without_aliases__.items(): 46 | if int(other_member) & bits: 47 | raise ValueError("%r: '%s' and '%s' have overlapping bits" % (flags_class, other_name, name)) 48 | else: 49 | other_bits |= bits 50 | return flags_class 51 | 52 | 53 | def is_descriptor(obj): 54 | return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__') 55 | 56 | 57 | class Const: 58 | def __init__(self, name): 59 | self.__name = name 60 | 61 | def __repr__(self): 62 | return self.__name 63 | 64 | 65 | # "singleton" to be used as a const value with identity checks 66 | UNDEFINED = Const('UNDEFINED') 67 | 68 | 69 | def create_flags_subclass(base_enum_class, class_name, flags, *, mixins=(), module=None, qualname=None, 70 | no_flags_name=UNDEFINED, all_flags_name=UNDEFINED): 71 | meta_class = type(base_enum_class) 72 | bases = tuple(mixins) + (base_enum_class,) 73 | class_dict = {'__members__': flags} 74 | if no_flags_name is not UNDEFINED: 75 | class_dict['__no_flags_name__'] = no_flags_name 76 | if all_flags_name is not UNDEFINED: 77 | class_dict['__all_flags_name__'] = all_flags_name 78 | flags_class = meta_class(class_name, bases, class_dict) 79 | 80 | # disabling on enabling pickle on the new class based on our module parameter 81 | if module is None: 82 | # Making the class unpicklable. 83 | def disabled_reduce_ex(self, proto): 84 | raise pickle.PicklingError("'%s' is unpicklable" % (type(self).__name__,)) 85 | flags_class.__reduce_ex__ = disabled_reduce_ex 86 | 87 | # For pickle module==None means the __main__ module so let's change it to a non-existing name. 88 | # This will cause a failure while trying to pickle the class. 89 | module = '' 90 | flags_class.__module__ = module 91 | 92 | if qualname is not None: 93 | flags_class.__qualname__ = qualname 94 | 95 | return flags_class 96 | 97 | 98 | def process_inline_members_definition(members): 99 | """ 100 | :param members: this can be any of the following: 101 | - a string containing a space and/or comma separated list of names: e.g.: 102 | "item1 item2 item3" OR "item1,item2,item3" OR "item1, item2, item3" 103 | - tuple/list/Set of strings (names) 104 | - Mapping of (name, data) pairs 105 | - any kind of iterable that yields (name, data) pairs 106 | :return: An iterable of (name, data) pairs. 107 | """ 108 | if isinstance(members, str): 109 | members = ((name, UNDEFINED) for name in members.replace(',', ' ').split()) 110 | elif isinstance(members, (tuple, list, Set)): 111 | if members and isinstance(next(iter(members)), str): 112 | members = ((name, UNDEFINED) for name in members) 113 | elif isinstance(members, Mapping): 114 | members = members.items() 115 | return members 116 | 117 | 118 | def is_member_definition_class_attribute(name, value): 119 | """ Returns True if the given class attribute with the specified 120 | name and value should be treated as a flag member definition. """ 121 | return not name.startswith('_') and not is_descriptor(value) 122 | 123 | 124 | def extract_member_definitions_from_class_attributes(class_dict): 125 | members = [(name, value) for name, value in class_dict.items() 126 | if is_member_definition_class_attribute(name, value)] 127 | for name, _ in members: 128 | del class_dict[name] 129 | 130 | members.extend(process_inline_members_definition(class_dict.pop('__members__', ()))) 131 | return members 132 | 133 | 134 | class ReadonlyzerMixin: 135 | """ Makes instance attributes readonly after setting readonly=True. """ 136 | __slots__ = ('__readonly',) 137 | 138 | def __init__(self, *args, readonly=False, **kwargs): 139 | # Calling super() before setting readonly. 140 | # This way super().__init__ can set attributes even if readonly==True 141 | super().__init__(*args, **kwargs) 142 | self.__readonly = readonly 143 | 144 | @property 145 | def readonly(self): 146 | try: 147 | return self.__readonly 148 | except AttributeError: 149 | return False 150 | 151 | @readonly.setter 152 | def readonly(self, value): 153 | self.__readonly = value 154 | 155 | def __setattr__(self, key, value): 156 | if self.readonly: 157 | raise AttributeError("Can't set attribute '%s' of readonly '%s' object" % (key, type(self).__name__)) 158 | super().__setattr__(key, value) 159 | 160 | def __delattr__(self, key): 161 | if self.readonly: 162 | raise AttributeError("Can't delete attribute '%s' of readonly '%s' object" % (key, type(self).__name__)) 163 | super().__delattr__(key) 164 | 165 | 166 | class FlagProperties(ReadonlyzerMixin): 167 | __slots__ = ('name', 'data', 'bits', 'index', 'index_without_aliases') 168 | 169 | def __init__(self, *, name, bits, data=None, index=None, index_without_aliases=None): 170 | self.name = name 171 | self.data = data 172 | self.bits = bits 173 | self.index = index 174 | self.index_without_aliases = index_without_aliases 175 | super().__init__() 176 | 177 | 178 | READONLY_PROTECTED_FLAGS_CLASS_ATTRIBUTES = frozenset([ 179 | '__writable_protected_flags_class_attributes__', '__all_members__', '__members__', '__members_without_aliases__', 180 | '__member_aliases__', '__bits_to_properties__', '__bits_to_instance__', '__pickle_int_flags__', 181 | ]) 182 | 183 | # these attributes are writable when __writable_protected_flags_class_attributes__ is set to True on the class. 184 | TEMPORARILY_WRITABLE_PROTECTED_FLAGS_CLASS_ATTRIBUTES = frozenset([ 185 | '__all_bits__', '__no_flags__', '__all_flags__', '__no_flags_name__', '__all_flags_name__', 186 | ]) 187 | 188 | PROTECTED_FLAGS_CLASS_ATTRIBUTES = READONLY_PROTECTED_FLAGS_CLASS_ATTRIBUTES | \ 189 | TEMPORARILY_WRITABLE_PROTECTED_FLAGS_CLASS_ATTRIBUTES 190 | 191 | 192 | def is_valid_bits_value(bits): 193 | return isinstance(bits, int) and not isinstance(bits, bool) 194 | 195 | 196 | def initialize_class_dict_and_create_flags_class(class_dict, class_name, create_flags_class): 197 | # all_members is used by __getattribute__ and __setattr__. It contains all items 198 | # from members and also the no_flags and all_flags special members if they are defined. 199 | all_members = collections.OrderedDict() 200 | members = collections.OrderedDict() 201 | members_without_aliases = collections.OrderedDict() 202 | bits_to_properties = collections.OrderedDict() 203 | bits_to_instance = collections.OrderedDict() 204 | member_aliases = collections.OrderedDict() 205 | class_dict['__all_members__'] = ReadonlyDictProxy(all_members) 206 | class_dict['__members__'] = ReadonlyDictProxy(members) 207 | class_dict['__members_without_aliases__'] = ReadonlyDictProxy(members_without_aliases) 208 | class_dict['__bits_to_properties__'] = ReadonlyDictProxy(bits_to_properties) 209 | class_dict['__bits_to_instance__'] = ReadonlyDictProxy(bits_to_instance) 210 | class_dict['__member_aliases__'] = ReadonlyDictProxy(member_aliases) 211 | 212 | flags_class = create_flags_class(class_dict) 213 | 214 | def instantiate_member(name, bits, special): 215 | if not isinstance(name, str): 216 | raise TypeError('Flag name should be an str but it is %r' % (name,)) 217 | if not is_valid_bits_value(bits): 218 | raise TypeError("Bits for flag '%s' should be an int but it is %r" % (name, bits)) 219 | if not special and bits == 0: 220 | raise ValueError("Flag '%s' has the invalid value of zero" % name) 221 | member = flags_class(bits) 222 | if int(member) != bits: 223 | raise RuntimeError("%s has altered the assigned bits of member '%s' from %r to %r" % ( 224 | class_name, name, bits, int(member))) 225 | return member 226 | 227 | def register_member(member, name, bits, data, special): 228 | # special members (like no_flags, and all_flags) have no index 229 | # and they appear only in the __all_members__ collection. 230 | if all_members.setdefault(name, member) is not member: 231 | raise ValueError('Duplicate flag name: %r' % name) 232 | 233 | # It isn't a problem if an instance with the same bits already exists in bits_to_instance because 234 | # a member contains only the bits so our new member is equivalent with the replaced one. 235 | bits_to_instance[bits] = member 236 | 237 | if special: 238 | return 239 | 240 | members[name] = member 241 | properties = FlagProperties(name=name, bits=bits, data=data, index=len(members)) 242 | properties_for_bits = bits_to_properties.setdefault(bits, properties) 243 | is_alias = properties_for_bits is not properties 244 | if is_alias: 245 | if data is not UNDEFINED: 246 | raise ValueError("You aren't allowed to associate data with alias '%s'" % name) 247 | member_aliases[name] = properties_for_bits.name 248 | else: 249 | properties.index_without_aliases = len(members_without_aliases) 250 | members_without_aliases[name] = member 251 | properties.readonly = True 252 | 253 | def instantiate_and_register_member(*, name, bits, data=None, special_member=False): 254 | member = instantiate_member(name, bits, special_member) 255 | register_member(member, name, bits, data, special_member) 256 | return member 257 | 258 | return flags_class, instantiate_and_register_member 259 | 260 | 261 | def create_flags_class_with_members(class_name, class_dict, member_definitions, create_flags_class): 262 | class_dict['__writable_protected_flags_class_attributes__'] = True 263 | 264 | flags_class, instantiate_and_register_member = initialize_class_dict_and_create_flags_class( 265 | class_dict, class_name, create_flags_class) 266 | 267 | member_definitions = [(name, data) for name, data in member_definitions] 268 | member_definitions = flags_class.process_member_definitions(member_definitions) 269 | # member_definitions has to be an iterable of iterables yielding (name, bits, data) 270 | 271 | all_bits = 0 272 | for name, bits, data in member_definitions: 273 | instantiate_and_register_member(name=name, bits=bits, data=data) 274 | all_bits |= bits 275 | 276 | if len(flags_class) == 0: 277 | # In this case process_member_definitions() returned an empty iterable which isn't allowed. 278 | raise RuntimeError("%s.%s returned an empty iterable" % 279 | (flags_class.__name__, flags_class.process_member_definitions.__name__)) 280 | 281 | def instantiate_special_member(name, default_name, bits): 282 | name = default_name if name is None else name 283 | return instantiate_and_register_member(name=name, bits=bits, special_member=True) 284 | 285 | flags_class.__no_flags__ = instantiate_special_member(flags_class.__no_flags_name__, '__no_flags__', 0) 286 | flags_class.__all_flags__ = instantiate_special_member(flags_class.__all_flags_name__, '__all_flags__', all_bits) 287 | 288 | flags_class.__all_bits__ = all_bits 289 | 290 | del flags_class.__writable_protected_flags_class_attributes__ 291 | return flags_class 292 | 293 | 294 | class FlagData: 295 | pass 296 | 297 | 298 | def is_flags_class_final(flags_class): 299 | return hasattr(flags_class, '__members__') 300 | 301 | 302 | class FlagsMeta(type): 303 | def __new__(mcs, class_name, bases, class_dict): 304 | if '__slots__' in class_dict: 305 | raise RuntimeError("You aren't allowed to use __slots__ in your Flags subclasses") 306 | class_dict['__slots__'] = () 307 | 308 | def create_flags_class(custom_class_dict=None): 309 | return super(FlagsMeta, mcs).__new__(mcs, class_name, bases, custom_class_dict or class_dict) 310 | 311 | if Flags is None: 312 | # This __new__ call is creating the Flags class of this module. 313 | return create_flags_class() 314 | 315 | flags_bases = [base for base in bases if issubclass(base, Flags)] 316 | for base in flags_bases: 317 | # pylint: disable=protected-access 318 | if is_flags_class_final(base): 319 | raise RuntimeError("You can't subclass '%s' because it has already defined flag members" % 320 | (base.__name__,)) 321 | 322 | member_definitions = extract_member_definitions_from_class_attributes(class_dict) 323 | if not member_definitions: 324 | return create_flags_class() 325 | return create_flags_class_with_members(class_name, class_dict, member_definitions, create_flags_class) 326 | 327 | def __call__(cls, *args, **kwargs): 328 | if kwargs or len(args) >= 2: 329 | # The Flags class or one of its subclasses was "called" as a 330 | # utility function to create a subclass of the called class. 331 | return create_flags_subclass(cls, *args, **kwargs) 332 | 333 | # We have zero or one positional argument and we have to create and/or return an exact instance of cls. 334 | # 1. Zero argument means we have to return a zero flag. 335 | # 2. A single positional argument can be one of the following cases: 336 | # 1. An object whose class is exactly cls. 337 | # 2. An str object that comes from Flags.__str__() or Flags.to_simple_str() 338 | # 3. An int object that specifies the bits of the Flags instance to be created. 339 | 340 | if not is_flags_class_final(cls): 341 | raise RuntimeError("Instantiation of abstract flags class '%s.%s' isn't allowed." % ( 342 | cls.__module__, cls.__name__)) 343 | 344 | if not args: 345 | # case 1 - zero positional arguments, we have to return a zero flag 346 | return cls.__no_flags__ 347 | 348 | value = args[0] 349 | 350 | if type(value) is cls: 351 | # case 2.1 352 | return value 353 | 354 | if isinstance(value, str): 355 | # case 2.2 356 | bits = cls.bits_from_str(value) 357 | elif is_valid_bits_value(value): 358 | # case 2.3 359 | bits = cls.__all_bits__ & value 360 | else: 361 | raise TypeError("Can't instantiate flags class '%s' from value %r" % (cls.__name__, value)) 362 | 363 | instance = cls.__bits_to_instance__.get(bits) 364 | if instance: 365 | return instance 366 | return super().__call__(bits) 367 | 368 | @classmethod 369 | def __prepare__(mcs, class_name, bases): 370 | return collections.OrderedDict() 371 | 372 | def __delattr__(cls, name): 373 | if (name in PROTECTED_FLAGS_CLASS_ATTRIBUTES and name != '__writable_protected_flags_class_attributes__') or\ 374 | (name in getattr(cls, '__all_members__', {})): 375 | raise AttributeError("Can't delete protected attribute '%s'" % name) 376 | super().__delattr__(name) 377 | 378 | def __setattr__(cls, name, value): 379 | if name in PROTECTED_FLAGS_CLASS_ATTRIBUTES: 380 | if name in READONLY_PROTECTED_FLAGS_CLASS_ATTRIBUTES or\ 381 | not getattr(cls, '__writable_protected_flags_class_attributes__', False): 382 | raise AttributeError("Can't assign protected attribute '%s'" % name) 383 | elif name in getattr(cls, '__all_members__', {}): 384 | raise AttributeError("Can't assign protected attribute '%s'" % name) 385 | super().__setattr__(name, value) 386 | 387 | def __getattr__(cls, name): 388 | try: 389 | return super().__getattribute__('__all_members__')[name] 390 | except KeyError: 391 | raise AttributeError(name) 392 | 393 | def __getitem__(cls, name): 394 | return cls.__all_members__[name] 395 | 396 | def __iter__(cls): 397 | return iter(cls.__members_without_aliases__.values()) 398 | 399 | def __reversed__(cls): 400 | return reversed(list(cls.__members_without_aliases__.values())) 401 | 402 | def __bool__(cls): 403 | return True 404 | 405 | def __len__(cls): 406 | members = getattr(cls, '__members_without_aliases__', ()) 407 | return len(members) 408 | 409 | def flag_attribute_value_to_bits_and_data(cls, name, value): 410 | if value is UNDEFINED: 411 | return UNDEFINED, UNDEFINED 412 | elif isinstance(value, FlagData): 413 | return UNDEFINED, value 414 | elif is_valid_bits_value(value): 415 | return value, UNDEFINED 416 | elif isinstance(value, Iterable): 417 | arr = tuple(value) 418 | if len(arr) == 0: 419 | return UNDEFINED, UNDEFINED 420 | if len(arr) == 1: 421 | return UNDEFINED, arr[0] 422 | if len(arr) == 2: 423 | return arr 424 | raise ValueError("Iterable is expected to have at most 2 items instead of %s " 425 | "for flag '%s', iterable: %r" % (len(arr), name, value)) 426 | raise TypeError("Expected an int or an iterable of at most 2 items " 427 | "for flag '%s', received %r" % (name, value)) 428 | 429 | def process_member_definitions(cls, member_definitions): 430 | """ 431 | The incoming member_definitions contains the class attributes (with their values) that are 432 | used to define the flag members. This method can do anything to the incoming list and has to 433 | return a final set of flag definitions that assigns bits to the members. The returned member 434 | definitions can be completely different or unrelated to the incoming ones. 435 | :param member_definitions: A list of (name, data) tuples. 436 | :return: An iterable of iterables yielding 3 items: name, bits, data 437 | """ 438 | members = [] 439 | auto_flags = [] 440 | all_bits = 0 441 | for name, data in member_definitions: 442 | bits, data = cls.flag_attribute_value_to_bits_and_data(name, data) 443 | if bits is UNDEFINED: 444 | auto_flags.append(len(members)) 445 | members.append((name, data)) 446 | elif is_valid_bits_value(bits): 447 | all_bits |= bits 448 | members.append((name, bits, data)) 449 | else: 450 | raise TypeError("Expected an int value as the bits of flag '%s', received %r" % (name, bits)) 451 | 452 | # auto-assigning unused bits to members without custom defined bits 453 | bit = 1 454 | for index in auto_flags: 455 | while bit & all_bits: 456 | bit <<= 1 457 | name, data = members[index] 458 | members[index] = name, bit, data 459 | bit <<= 1 460 | 461 | return members 462 | 463 | def __repr__(cls): 464 | return "" % cls.__name__ 465 | 466 | __no_flags_name__ = 'no_flags' 467 | __all_flags_name__ = 'all_flags' 468 | __dotted_single_flag_str__ = True 469 | __pickle_int_flags__ = False 470 | __all_bits__ = -1 471 | 472 | # TODO: utility method to fill the flag members to a namespace, and another utility that can fill 473 | # them to a module (a specific case of namespaces) 474 | 475 | 476 | def operator_requires_type_identity(wrapped): 477 | @functools.wraps(wrapped) 478 | def wrapper(self, other): 479 | if type(other) is not type(self): 480 | return NotImplemented 481 | return wrapped(self, other) 482 | return wrapper 483 | 484 | 485 | class FlagsArithmeticMixin: 486 | __slots__ = ('__bits',) 487 | 488 | def __new__(cls, bits): 489 | instance = super().__new__(cls) 490 | # pylint: disable=protected-access 491 | instance.__bits = bits & cls.__all_bits__ 492 | return instance 493 | 494 | def __int__(self): 495 | return self.__bits 496 | 497 | def __bool__(self): 498 | return self.__bits != 0 499 | 500 | def __contains__(self, item): 501 | if type(item) is not type(self): 502 | return False 503 | # this logic is equivalent to that of __ge__(self, item) and __le__(item, self) 504 | # pylint: disable=protected-access 505 | return item.__bits == (self.__bits & item.__bits) 506 | 507 | def is_disjoint(self, *flags_instances): 508 | for flags in flags_instances: 509 | if self & flags: 510 | return False 511 | return True 512 | 513 | def __create_flags_instance(self, bits): 514 | # optimization, exploiting immutability 515 | if bits == self.__bits: 516 | return self 517 | return type(self)(bits) 518 | 519 | @operator_requires_type_identity 520 | def __or__(self, other): 521 | # pylint: disable=protected-access 522 | return self.__create_flags_instance(self.__bits | other.__bits) 523 | 524 | @operator_requires_type_identity 525 | def __xor__(self, other): 526 | # pylint: disable=protected-access 527 | return self.__create_flags_instance(self.__bits ^ other.__bits) 528 | 529 | @operator_requires_type_identity 530 | def __and__(self, other): 531 | # pylint: disable=protected-access 532 | return self.__create_flags_instance(self.__bits & other.__bits) 533 | 534 | @operator_requires_type_identity 535 | def __sub__(self, other): 536 | # pylint: disable=protected-access 537 | bits = self.__bits ^ (self.__bits & other.__bits) 538 | return self.__create_flags_instance(bits) 539 | 540 | @operator_requires_type_identity 541 | def __eq__(self, other): 542 | # pylint: disable=protected-access 543 | return self.__bits == other.__bits 544 | 545 | @operator_requires_type_identity 546 | def __ne__(self, other): 547 | # pylint: disable=protected-access 548 | return self.__bits != other.__bits 549 | 550 | @operator_requires_type_identity 551 | def __ge__(self, other): 552 | # pylint: disable=protected-access 553 | return other.__bits == (self.__bits & other.__bits) 554 | 555 | @operator_requires_type_identity 556 | def __gt__(self, other): 557 | # pylint: disable=protected-access 558 | return (self.__bits != other.__bits) and (other.__bits == (self.__bits & other.__bits)) 559 | 560 | @operator_requires_type_identity 561 | def __le__(self, other): 562 | # pylint: disable=protected-access 563 | return self.__bits == (self.__bits & other.__bits) 564 | 565 | @operator_requires_type_identity 566 | def __lt__(self, other): 567 | # pylint: disable=protected-access 568 | return (self.__bits != other.__bits) and (self.__bits == (self.__bits & other.__bits)) 569 | 570 | def __invert__(self): 571 | return self.__create_flags_instance(self.__bits ^ type(self).__all_bits__) 572 | 573 | 574 | # This is used by FlagsMeta to detect whether the flags class currently being created is Flags. 575 | Flags = None 576 | 577 | 578 | class Flags(FlagsArithmeticMixin, metaclass=FlagsMeta): 579 | @property 580 | def is_member(self): 581 | """ `flags.is_member` is a shorthand for `flags.properties is not None`. 582 | If this property is False then this Flags instance has either zero bits or holds a combination 583 | of flag member bits. 584 | If this property is True then the bits of this Flags instance match exactly the bits associated 585 | with one of the members. This however doesn't necessarily mean that this flag instance isn't a 586 | combination of several flags because the bits of a member can be the subset of another member. 587 | For example if member0_bits=0x1 and member1_bits=0x3 then the bits of member0 are a subset of 588 | the bits of member1. If a flag instance holds the bits of member1 then Flags.is_member returns 589 | True and Flags.properties returns the properties of member1 but __len__() returns 2 and 590 | __iter__() yields both member0 and member1. 591 | """ 592 | return type(self).__bits_to_properties__.get(int(self)) is not None 593 | 594 | @property 595 | def properties(self): 596 | """ 597 | :return: Returns None if this flag isn't an exact member of a flags class but a combination of flags, 598 | returns an object holding the properties (e.g.: name, data, index, ...) of the flag otherwise. 599 | We don't store flag properties directly in Flags instances because this way Flags instances that are 600 | the (temporary) result of flags arithmetic don't have to maintain these fields and it also has some 601 | benefits regarding memory usage. """ 602 | return type(self).__bits_to_properties__.get(int(self)) 603 | 604 | @property 605 | def name(self): 606 | properties = self.properties 607 | return self.properties.name if properties else None 608 | 609 | @property 610 | def data(self): 611 | properties = self.properties 612 | return self.properties.data if properties else UNDEFINED 613 | 614 | def __getattr__(self, name): 615 | try: 616 | member = type(self).__members__[name] 617 | except KeyError: 618 | raise AttributeError(name) 619 | return member in self 620 | 621 | def __iter__(self): 622 | members = type(self).__members_without_aliases__.values() 623 | return (member for member in members if member in self) 624 | 625 | def __reversed__(self): 626 | members = reversed(list(type(self).__members_without_aliases__.values())) 627 | return (member for member in members if member in self) 628 | 629 | def __len__(self): 630 | return sum(1 for _ in self) 631 | 632 | def __hash__(self): 633 | return int(self) ^ hash(type(self)) 634 | 635 | def __reduce_ex__(self, proto): 636 | value = int(self) if type(self).__pickle_int_flags__ else self.to_simple_str() 637 | return type(self), (value,) 638 | 639 | def __str__(self): 640 | # Warning: The output of this method has to be a string that can be processed by bits_from_str() 641 | return self.__internal_str() 642 | 643 | def __internal_str(self): 644 | if not type(self).__dotted_single_flag_str__: 645 | return '%s(%s)' % (type(self).__name__, self.to_simple_str()) 646 | contained_flags = list(self) 647 | if len(contained_flags) != 1: 648 | # This is the zero flag or a set of flags (as a result of arithmetic) 649 | # or a flags class member that is a superset of another flags member. 650 | return '%s(%s)' % (type(self).__name__, '|'.join(member.name for member in contained_flags)) 651 | return '%s.%s' % (type(self).__name__, contained_flags[0].properties.name) 652 | 653 | def __repr__(self): 654 | contained_flags = list(self) 655 | if len(contained_flags) != 1: 656 | # This is the zero flag or a set of flags (as a result of arithmetic) 657 | # or a flags class member that is a superset of another flags member. 658 | return '<%s bits=0x%04X>' % (self.__internal_str(), int(self)) 659 | return '<%s bits=0x%04X data=%r>' % (self.__internal_str(), contained_flags[0].properties.bits, 660 | contained_flags[0].properties.data) 661 | 662 | def to_simple_str(self): 663 | return '|'.join(member.name for member in self) 664 | 665 | @classmethod 666 | def from_simple_str(cls, s): 667 | """ Accepts only the output of to_simple_str(). The output of __str__() is invalid as input. """ 668 | if not isinstance(s, str): 669 | raise TypeError("Expected an str instance, received %r" % (s,)) 670 | return cls(cls.bits_from_simple_str(s)) 671 | 672 | @classmethod 673 | def from_str(cls, s): 674 | """ Accepts both the output of to_simple_str() and __str__(). """ 675 | if not isinstance(s, str): 676 | raise TypeError("Expected an str instance, received %r" % (s,)) 677 | return cls(cls.bits_from_str(s)) 678 | 679 | @classmethod 680 | def bits_from_simple_str(cls, s): 681 | member_names = (name.strip() for name in s.split('|')) 682 | member_names = filter(None, member_names) 683 | bits = 0 684 | for member_name in filter(None, member_names): 685 | member = cls.__all_members__.get(member_name) 686 | if member is None: 687 | raise ValueError("Invalid flag '%s.%s' in string %r" % (cls.__name__, member_name, s)) 688 | bits |= int(member) 689 | return bits 690 | 691 | @classmethod 692 | def bits_from_str(cls, s): 693 | """ Converts the output of __str__ into an integer. """ 694 | try: 695 | if len(s) <= len(cls.__name__) or not s.startswith(cls.__name__): 696 | return cls.bits_from_simple_str(s) 697 | c = s[len(cls.__name__)] 698 | if c == '(': 699 | if not s.endswith(')'): 700 | raise ValueError 701 | return cls.bits_from_simple_str(s[len(cls.__name__)+1:-1]) 702 | elif c == '.': 703 | member_name = s[len(cls.__name__)+1:] 704 | return int(cls.__all_members__[member_name]) 705 | else: 706 | raise ValueError 707 | except ValueError as ex: 708 | if ex.args: 709 | raise 710 | raise ValueError("%s.%s: invalid input: %r" % (cls.__name__, cls.bits_from_str.__name__, s)) 711 | except KeyError as ex: 712 | raise ValueError("%s.%s: Invalid flag name '%s' in input: %r" % (cls.__name__, cls.bits_from_str.__name__, 713 | ex.args[0], s)) 714 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pasztorpisti/py-flags/16706c13fe040964f0ece6b48fc4c054aa8be8ed/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_arithmetic.py: -------------------------------------------------------------------------------- 1 | """ Testing flag combining operators on our flags instances. """ 2 | import operator 3 | from unittest import TestCase 4 | 5 | from flags import Flags 6 | 7 | 8 | class MyOtherFlags(Flags): 9 | of0 = () 10 | 11 | 12 | class MyFlags(Flags): 13 | f0 = () 14 | f1 = () 15 | f2 = () 16 | 17 | 18 | no_flags = MyFlags.no_flags 19 | all_flags = MyFlags.all_flags 20 | f0 = MyFlags.f0 21 | f1 = MyFlags.f1 22 | f2 = MyFlags.f2 23 | f01 = MyFlags.f0 | MyFlags.f1 24 | f02 = MyFlags.f0 | MyFlags.f2 25 | f12 = MyFlags.f1 | MyFlags.f2 26 | 27 | 28 | class TestArithmetic(TestCase): 29 | def test_member_bits(self): 30 | self.assertEqual(int(MyOtherFlags.of0), 1) 31 | 32 | self.assertEqual(int(no_flags), 0) 33 | self.assertEqual(int(all_flags), 7) 34 | self.assertEqual(int(f0), 1) 35 | self.assertEqual(int(f1), 2) 36 | self.assertEqual(int(f2), 4) 37 | self.assertEqual(int(f01), 3) 38 | self.assertEqual(int(f02), 5) 39 | self.assertEqual(int(f12), 6) 40 | 41 | def test_contains(self): 42 | self.assertNotIn(MyOtherFlags.of0, MyFlags.all_flags) 43 | self.assertNotIn(MyOtherFlags.of0, MyFlags.f0) 44 | self.assertNotIn(False, MyFlags.f0) 45 | self.assertNotIn(True, MyFlags.f0) 46 | self.assertNotIn('', MyFlags.f0) 47 | self.assertNotIn('my_string', MyFlags.f0) 48 | self.assertNotIn(4, MyFlags.f0) 49 | self.assertNotIn(5.5, MyFlags.f0) 50 | self.assertNotIn(None, MyFlags.f0) 51 | 52 | # same test cases as in case of operator.__le__(item, flags) 53 | self.assertTrue(no_flags in no_flags) 54 | self.assertTrue(no_flags in all_flags) 55 | self.assertTrue(no_flags in f0) 56 | self.assertTrue(no_flags in f1) 57 | self.assertTrue(no_flags in f2) 58 | self.assertTrue(no_flags in f01) 59 | self.assertTrue(no_flags in f02) 60 | self.assertTrue(no_flags in f12) 61 | 62 | self.assertFalse(f0 in no_flags) 63 | self.assertTrue(f0 in all_flags) 64 | self.assertTrue(f0 in f0) 65 | self.assertFalse(f0 in f1) 66 | self.assertFalse(f0 in f2) 67 | self.assertTrue(f0 in f01) 68 | self.assertTrue(f0 in f02) 69 | self.assertFalse(f0 in f12) 70 | 71 | self.assertFalse(f01 in no_flags) 72 | self.assertTrue(f01 in all_flags) 73 | self.assertFalse(f01 in f0) 74 | self.assertFalse(f01 in f1) 75 | self.assertFalse(f01 in f2) 76 | self.assertTrue(f01 in f01) 77 | self.assertFalse(f01 in f02) 78 | self.assertFalse(f01 in f12) 79 | 80 | self.assertTrue(no_flags in all_flags) 81 | self.assertTrue(all_flags in all_flags) 82 | self.assertTrue(f0 in all_flags) 83 | self.assertTrue(f1 in all_flags) 84 | self.assertTrue(f2 in all_flags) 85 | self.assertTrue(f01 in all_flags) 86 | self.assertTrue(f02 in all_flags) 87 | self.assertTrue(f12 in all_flags) 88 | 89 | def test_is_disjoint(self): 90 | self.assertTrue(no_flags.is_disjoint(no_flags)) 91 | self.assertTrue(no_flags.is_disjoint(all_flags)) 92 | self.assertTrue(no_flags.is_disjoint(f0)) 93 | self.assertTrue(no_flags.is_disjoint(f1)) 94 | self.assertTrue(no_flags.is_disjoint(f2)) 95 | self.assertTrue(no_flags.is_disjoint(f01)) 96 | self.assertTrue(no_flags.is_disjoint(f02)) 97 | self.assertTrue(no_flags.is_disjoint(f12)) 98 | 99 | self.assertTrue(f0.is_disjoint(no_flags)) 100 | self.assertFalse(f0.is_disjoint(all_flags)) 101 | self.assertFalse(f0.is_disjoint(f0)) 102 | self.assertTrue(f0.is_disjoint(f1)) 103 | self.assertTrue(f0.is_disjoint(f2)) 104 | self.assertFalse(f0.is_disjoint(f01)) 105 | self.assertFalse(f0.is_disjoint(f02)) 106 | self.assertTrue(f0.is_disjoint(f12)) 107 | 108 | self.assertTrue(f01.is_disjoint(no_flags)) 109 | self.assertFalse(f01.is_disjoint(all_flags)) 110 | self.assertFalse(f01.is_disjoint(f0)) 111 | self.assertFalse(f01.is_disjoint(f1)) 112 | self.assertTrue(f01.is_disjoint(f2)) 113 | self.assertFalse(f01.is_disjoint(f01)) 114 | self.assertFalse(f01.is_disjoint(f02)) 115 | self.assertFalse(f01.is_disjoint(f12)) 116 | 117 | self.assertTrue(all_flags.is_disjoint(no_flags)) 118 | self.assertFalse(all_flags.is_disjoint(all_flags)) 119 | self.assertFalse(all_flags.is_disjoint(f0)) 120 | self.assertFalse(all_flags.is_disjoint(f1)) 121 | self.assertFalse(all_flags.is_disjoint(f2)) 122 | self.assertFalse(all_flags.is_disjoint(f01)) 123 | self.assertFalse(all_flags.is_disjoint(f02)) 124 | self.assertFalse(all_flags.is_disjoint(f12)) 125 | 126 | def _test_incompatible_types_fail(self, operator_): 127 | for other in (MyOtherFlags.of0, False, True, '', 'my_string', 4, 5.5, None): 128 | with self.assertRaises(TypeError, msg='other operand: %r' % other): 129 | operator_(f0, other) 130 | 131 | def test_or(self): 132 | self._test_incompatible_types_fail(operator.__or__) 133 | 134 | self.assertEqual(no_flags | no_flags, no_flags) 135 | self.assertEqual(no_flags | all_flags, all_flags) 136 | self.assertEqual(no_flags | f0, f0) 137 | self.assertEqual(no_flags | f1, f1) 138 | self.assertEqual(no_flags | f2, f2) 139 | self.assertEqual(no_flags | f01, f01) 140 | self.assertEqual(no_flags | f02, f02) 141 | self.assertEqual(no_flags | f12, f12) 142 | 143 | self.assertEqual(f0 | no_flags, f0) 144 | self.assertEqual(f0 | all_flags, all_flags) 145 | self.assertEqual(f0 | f0, f0) 146 | self.assertEqual(f0 | f1, f01) 147 | self.assertEqual(f0 | f2, f02) 148 | self.assertEqual(f0 | f01, f01) 149 | self.assertEqual(f0 | f02, f02) 150 | self.assertEqual(f0 | f12, all_flags) 151 | 152 | self.assertEqual(f01 | no_flags, f01) 153 | self.assertEqual(f01 | all_flags, all_flags) 154 | self.assertEqual(f01 | f0, f01) 155 | self.assertEqual(f01 | f1, f01) 156 | self.assertEqual(f01 | f2, all_flags) 157 | self.assertEqual(f01 | f01, f01) 158 | self.assertEqual(f01 | f02, all_flags) 159 | self.assertEqual(f01 | f12, all_flags) 160 | 161 | self.assertEqual(all_flags | no_flags, all_flags) 162 | self.assertEqual(all_flags | all_flags, all_flags) 163 | self.assertEqual(all_flags | f0, all_flags) 164 | self.assertEqual(all_flags | f1, all_flags) 165 | self.assertEqual(all_flags | f2, all_flags) 166 | self.assertEqual(all_flags | f01, all_flags) 167 | self.assertEqual(all_flags | f02, all_flags) 168 | self.assertEqual(all_flags | f12, all_flags) 169 | 170 | def test_xor(self): 171 | self._test_incompatible_types_fail(operator.__xor__) 172 | 173 | self.assertEqual(no_flags ^ no_flags, no_flags) 174 | self.assertEqual(no_flags ^ all_flags, all_flags) 175 | self.assertEqual(no_flags ^ f0, f0) 176 | self.assertEqual(no_flags ^ f1, f1) 177 | self.assertEqual(no_flags ^ f2, f2) 178 | self.assertEqual(no_flags ^ f01, f01) 179 | self.assertEqual(no_flags ^ f02, f02) 180 | self.assertEqual(no_flags ^ f12, f12) 181 | 182 | self.assertEqual(f0 ^ no_flags, f0) 183 | self.assertEqual(f0 ^ all_flags, f12) 184 | self.assertEqual(f0 ^ f0, no_flags) 185 | self.assertEqual(f0 ^ f1, f01) 186 | self.assertEqual(f0 ^ f2, f02) 187 | self.assertEqual(f0 ^ f01, f1) 188 | self.assertEqual(f0 ^ f02, f2) 189 | self.assertEqual(f0 ^ f12, all_flags) 190 | 191 | self.assertEqual(f01 ^ no_flags, f01) 192 | self.assertEqual(f01 ^ all_flags, f2) 193 | self.assertEqual(f01 ^ f0, f1) 194 | self.assertEqual(f01 ^ f1, f0) 195 | self.assertEqual(f01 ^ f2, all_flags) 196 | self.assertEqual(f01 ^ f01, no_flags) 197 | self.assertEqual(f01 ^ f02, f12) 198 | self.assertEqual(f01 ^ f12, f02) 199 | 200 | self.assertEqual(all_flags ^ no_flags, all_flags) 201 | self.assertEqual(all_flags ^ all_flags, no_flags) 202 | self.assertEqual(all_flags ^ f0, f12) 203 | self.assertEqual(all_flags ^ f1, f02) 204 | self.assertEqual(all_flags ^ f2, f01) 205 | self.assertEqual(all_flags ^ f01, f2) 206 | self.assertEqual(all_flags ^ f02, f1) 207 | self.assertEqual(all_flags ^ f12, f0) 208 | 209 | def test_and(self): 210 | self._test_incompatible_types_fail(operator.__and__) 211 | 212 | self.assertEqual(no_flags & no_flags, no_flags) 213 | self.assertEqual(no_flags & all_flags, no_flags) 214 | self.assertEqual(no_flags & f0, no_flags) 215 | self.assertEqual(no_flags & f1, no_flags) 216 | self.assertEqual(no_flags & f2, no_flags) 217 | self.assertEqual(no_flags & f01, no_flags) 218 | self.assertEqual(no_flags & f02, no_flags) 219 | self.assertEqual(no_flags & f12, no_flags) 220 | 221 | self.assertEqual(f0 & no_flags, no_flags) 222 | self.assertEqual(f0 & all_flags, f0) 223 | self.assertEqual(f0 & f0, f0) 224 | self.assertEqual(f0 & f1, no_flags) 225 | self.assertEqual(f0 & f2, no_flags) 226 | self.assertEqual(f0 & f01, f0) 227 | self.assertEqual(f0 & f02, f0) 228 | self.assertEqual(f0 & f12, no_flags) 229 | 230 | self.assertEqual(f01 & no_flags, no_flags) 231 | self.assertEqual(f01 & all_flags, f01) 232 | self.assertEqual(f01 & f0, f0) 233 | self.assertEqual(f01 & f1, f1) 234 | self.assertEqual(f01 & f2, no_flags) 235 | self.assertEqual(f01 & f01, f01) 236 | self.assertEqual(f01 & f02, f0) 237 | self.assertEqual(f01 & f12, f1) 238 | 239 | self.assertEqual(all_flags & no_flags, no_flags) 240 | self.assertEqual(all_flags & all_flags, all_flags) 241 | self.assertEqual(all_flags & f0, f0) 242 | self.assertEqual(all_flags & f1, f1) 243 | self.assertEqual(all_flags & f2, f2) 244 | self.assertEqual(all_flags & f01, f01) 245 | self.assertEqual(all_flags & f02, f02) 246 | self.assertEqual(all_flags & f12, f12) 247 | 248 | def test_sub(self): 249 | self._test_incompatible_types_fail(operator.__sub__) 250 | 251 | self.assertEqual(no_flags - no_flags, no_flags) 252 | self.assertEqual(no_flags - all_flags, no_flags) 253 | self.assertEqual(no_flags - f0, no_flags) 254 | self.assertEqual(no_flags - f1, no_flags) 255 | self.assertEqual(no_flags - f2, no_flags) 256 | self.assertEqual(no_flags - f01, no_flags) 257 | self.assertEqual(no_flags - f02, no_flags) 258 | self.assertEqual(no_flags - f12, no_flags) 259 | 260 | self.assertEqual(f0 - no_flags, f0) 261 | self.assertEqual(f0 - all_flags, no_flags) 262 | self.assertEqual(f0 - f0, no_flags) 263 | self.assertEqual(f0 - f1, f0) 264 | self.assertEqual(f0 - f2, f0) 265 | self.assertEqual(f0 - f01, no_flags) 266 | self.assertEqual(f0 - f02, no_flags) 267 | self.assertEqual(f0 - f12, f0) 268 | 269 | self.assertEqual(f01 - no_flags, f01) 270 | self.assertEqual(f01 - all_flags, no_flags) 271 | self.assertEqual(f01 - f0, f1) 272 | self.assertEqual(f01 - f1, f0) 273 | self.assertEqual(f01 - f2, f01) 274 | self.assertEqual(f01 - f01, no_flags) 275 | self.assertEqual(f01 - f02, f1) 276 | self.assertEqual(f01 - f12, f0) 277 | 278 | self.assertEqual(all_flags - no_flags, all_flags) 279 | self.assertEqual(all_flags - all_flags, no_flags) 280 | self.assertEqual(all_flags - f0, f12) 281 | self.assertEqual(all_flags - f1, f02) 282 | self.assertEqual(all_flags - f2, f01) 283 | self.assertEqual(all_flags - f01, f2) 284 | self.assertEqual(all_flags - f02, f1) 285 | self.assertEqual(all_flags - f12, f0) 286 | 287 | def test_eq(self): 288 | self.assertFalse(MyFlags.f0 == MyOtherFlags.of0) 289 | self.assertFalse(MyFlags.f0 == False) 290 | self.assertFalse(MyFlags.f0 == True) 291 | self.assertFalse(MyFlags.f0 == '') 292 | self.assertFalse(MyFlags.f0 == 'my_string') 293 | self.assertFalse(MyFlags.f0 == None) 294 | 295 | self.assertTrue(no_flags == no_flags) 296 | self.assertTrue(all_flags == all_flags) 297 | self.assertTrue(f0 == f0) 298 | self.assertTrue(f1 == f1) 299 | self.assertTrue(f2 == f2) 300 | self.assertTrue(f01 == f01) 301 | self.assertTrue(f02 == f02) 302 | self.assertTrue(f12 == f12) 303 | 304 | self.assertFalse(f0 == no_flags) 305 | self.assertFalse(f0 == all_flags) 306 | self.assertTrue(f0 == f0) 307 | self.assertFalse(f0 == f1) 308 | self.assertFalse(f0 == f2) 309 | self.assertFalse(f0 == f01) 310 | self.assertFalse(f0 == f02) 311 | self.assertFalse(f0 == f12) 312 | 313 | self.assertFalse(f01 == no_flags) 314 | self.assertFalse(f01 == all_flags) 315 | self.assertFalse(f01 == f0) 316 | self.assertFalse(f01 == f1) 317 | self.assertFalse(f01 == f2) 318 | self.assertTrue(f01 == f01) 319 | self.assertFalse(f01 == f02) 320 | self.assertFalse(f01 == f12) 321 | 322 | def test_ne(self): 323 | self.assertTrue(MyFlags.f0 != MyOtherFlags.of0) 324 | self.assertTrue(MyFlags.f0 != False) 325 | self.assertTrue(MyFlags.f0 != True) 326 | self.assertTrue(MyFlags.f0 != '') 327 | self.assertTrue(MyFlags.f0 != 'my_string') 328 | self.assertTrue(MyFlags.f0 != None) 329 | 330 | self.assertFalse(no_flags != no_flags) 331 | self.assertFalse(all_flags != all_flags) 332 | self.assertFalse(f0 != f0) 333 | self.assertFalse(f1 != f1) 334 | self.assertFalse(f2 != f2) 335 | self.assertFalse(f01 != f01) 336 | self.assertFalse(f02 != f02) 337 | self.assertFalse(f12 != f12) 338 | 339 | self.assertTrue(f0 != no_flags) 340 | self.assertTrue(f0 != all_flags) 341 | self.assertFalse(f0 != f0) 342 | self.assertTrue(f0 != f1) 343 | self.assertTrue(f0 != f2) 344 | self.assertTrue(f0 != f01) 345 | self.assertTrue(f0 != f02) 346 | self.assertTrue(f0 != f12) 347 | 348 | self.assertTrue(f01 != no_flags) 349 | self.assertTrue(f01 != all_flags) 350 | self.assertTrue(f01 != f0) 351 | self.assertTrue(f01 != f1) 352 | self.assertTrue(f01 != f2) 353 | self.assertFalse(f01 != f01) 354 | self.assertTrue(f01 != f02) 355 | self.assertTrue(f01 != f12) 356 | 357 | def test_ge(self): 358 | self._test_incompatible_types_fail(operator.__ge__) 359 | 360 | self.assertTrue(no_flags >= no_flags) 361 | self.assertFalse(no_flags >= all_flags) 362 | self.assertFalse(no_flags >= f0) 363 | self.assertFalse(no_flags >= f1) 364 | self.assertFalse(no_flags >= f2) 365 | self.assertFalse(no_flags >= f01) 366 | self.assertFalse(no_flags >= f02) 367 | self.assertFalse(no_flags >= f12) 368 | 369 | self.assertTrue(f0 >= no_flags) 370 | self.assertFalse(f0 >= all_flags) 371 | self.assertTrue(f0 >= f0) 372 | self.assertFalse(f0 >= f1) 373 | self.assertFalse(f0 >= f2) 374 | self.assertFalse(f0 >= f01) 375 | self.assertFalse(f0 >= f02) 376 | self.assertFalse(f0 >= f12) 377 | 378 | self.assertTrue(f01 >= no_flags) 379 | self.assertFalse(f01 >= all_flags) 380 | self.assertTrue(f01 >= f0) 381 | self.assertTrue(f01 >= f1) 382 | self.assertFalse(f01 >= f2) 383 | self.assertTrue(f01 >= f01) 384 | self.assertFalse(f01 >= f02) 385 | self.assertFalse(f01 >= f12) 386 | 387 | self.assertFalse(no_flags >= all_flags) 388 | self.assertTrue(all_flags >= all_flags) 389 | self.assertFalse(f0 >= all_flags) 390 | self.assertFalse(f1 >= all_flags) 391 | self.assertFalse(f2 >= all_flags) 392 | self.assertFalse(f01 >= all_flags) 393 | self.assertFalse(f02 >= all_flags) 394 | self.assertFalse(f12 >= all_flags) 395 | 396 | def test_gt(self): 397 | self._test_incompatible_types_fail(operator.__gt__) 398 | 399 | self.assertFalse(no_flags > no_flags) 400 | self.assertFalse(no_flags > all_flags) 401 | self.assertFalse(no_flags > f0) 402 | self.assertFalse(no_flags > f1) 403 | self.assertFalse(no_flags > f2) 404 | self.assertFalse(no_flags > f01) 405 | self.assertFalse(no_flags > f02) 406 | self.assertFalse(no_flags > f12) 407 | 408 | self.assertTrue(f0 > no_flags) 409 | self.assertFalse(f0 > all_flags) 410 | self.assertFalse(f0 > f0) 411 | self.assertFalse(f0 > f1) 412 | self.assertFalse(f0 > f2) 413 | self.assertFalse(f0 > f01) 414 | self.assertFalse(f0 > f02) 415 | self.assertFalse(f0 > f12) 416 | 417 | self.assertTrue(f01 > no_flags) 418 | self.assertFalse(f01 > all_flags) 419 | self.assertTrue(f01 > f0) 420 | self.assertTrue(f01 > f1) 421 | self.assertFalse(f01 > f2) 422 | self.assertFalse(f01 > f01) 423 | self.assertFalse(f01 > f02) 424 | self.assertFalse(f01 > f12) 425 | 426 | self.assertFalse(no_flags > all_flags) 427 | self.assertFalse(all_flags > all_flags) 428 | self.assertFalse(f0 > all_flags) 429 | self.assertFalse(f1 > all_flags) 430 | self.assertFalse(f2 > all_flags) 431 | self.assertFalse(f01 > all_flags) 432 | self.assertFalse(f02 > all_flags) 433 | self.assertFalse(f12 > all_flags) 434 | 435 | def test_le(self): 436 | self._test_incompatible_types_fail(operator.__le__) 437 | 438 | self.assertTrue(no_flags <= no_flags) 439 | self.assertTrue(no_flags <= all_flags) 440 | self.assertTrue(no_flags <= f0) 441 | self.assertTrue(no_flags <= f1) 442 | self.assertTrue(no_flags <= f2) 443 | self.assertTrue(no_flags <= f01) 444 | self.assertTrue(no_flags <= f02) 445 | self.assertTrue(no_flags <= f12) 446 | 447 | self.assertFalse(f0 <= no_flags) 448 | self.assertTrue(f0 <= all_flags) 449 | self.assertTrue(f0 <= f0) 450 | self.assertFalse(f0 <= f1) 451 | self.assertFalse(f0 <= f2) 452 | self.assertTrue(f0 <= f01) 453 | self.assertTrue(f0 <= f02) 454 | self.assertFalse(f0 <= f12) 455 | 456 | self.assertFalse(f01 <= no_flags) 457 | self.assertTrue(f01 <= all_flags) 458 | self.assertFalse(f01 <= f0) 459 | self.assertFalse(f01 <= f1) 460 | self.assertFalse(f01 <= f2) 461 | self.assertTrue(f01 <= f01) 462 | self.assertFalse(f01 <= f02) 463 | self.assertFalse(f01 <= f12) 464 | 465 | self.assertTrue(no_flags <= all_flags) 466 | self.assertTrue(all_flags <= all_flags) 467 | self.assertTrue(f0 <= all_flags) 468 | self.assertTrue(f1 <= all_flags) 469 | self.assertTrue(f2 <= all_flags) 470 | self.assertTrue(f01 <= all_flags) 471 | self.assertTrue(f02 <= all_flags) 472 | self.assertTrue(f12 <= all_flags) 473 | 474 | def test_lt(self): 475 | self._test_incompatible_types_fail(operator.__lt__) 476 | 477 | self.assertFalse(no_flags < no_flags) 478 | self.assertTrue(no_flags < all_flags) 479 | self.assertTrue(no_flags < f0) 480 | self.assertTrue(no_flags < f1) 481 | self.assertTrue(no_flags < f2) 482 | self.assertTrue(no_flags < f01) 483 | self.assertTrue(no_flags < f02) 484 | self.assertTrue(no_flags < f12) 485 | 486 | self.assertFalse(f0 < no_flags) 487 | self.assertTrue(f0 < all_flags) 488 | self.assertFalse(f0 < f0) 489 | self.assertFalse(f0 < f1) 490 | self.assertFalse(f0 < f2) 491 | self.assertTrue(f0 < f01) 492 | self.assertTrue(f0 < f02) 493 | self.assertFalse(f0 < f12) 494 | 495 | self.assertFalse(f01 < no_flags) 496 | self.assertTrue(f01 < all_flags) 497 | self.assertFalse(f01 < f0) 498 | self.assertFalse(f01 < f1) 499 | self.assertFalse(f01 < f2) 500 | self.assertFalse(f01 < f01) 501 | self.assertFalse(f01 < f02) 502 | self.assertFalse(f01 < f12) 503 | 504 | self.assertTrue(no_flags < all_flags) 505 | self.assertFalse(all_flags < all_flags) 506 | self.assertTrue(f0 < all_flags) 507 | self.assertTrue(f1 < all_flags) 508 | self.assertTrue(f2 < all_flags) 509 | self.assertTrue(f01 < all_flags) 510 | self.assertTrue(f02 < all_flags) 511 | self.assertTrue(f12 < all_flags) 512 | 513 | def test_invert(self): 514 | self.assertEqual(~no_flags, all_flags) 515 | self.assertEqual(~all_flags, no_flags) 516 | self.assertEqual(~f0, f12) 517 | self.assertEqual(~f1, f02) 518 | self.assertEqual(~f2, f01) 519 | self.assertEqual(~f01, f2) 520 | self.assertEqual(~f02, f1) 521 | self.assertEqual(~f12, f0) 522 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is excluded from the test discovery by the load_tests() function in this file. 3 | You can use this module to declare reusable test base classes/mixins. 4 | """ 5 | 6 | import pickle 7 | import sys 8 | from unittest import TestCase, TestSuite 9 | 10 | 11 | class PicklingTestCase(TestCase): 12 | def _pickle_and_unpickle(self, flags): 13 | # Python3.3 and earlier pickle doesn't seem to handle __qualname__. 14 | # In python3.4 we already have protocol 4 that handles the __qualname__ 15 | # of inner classes but we have to specify protocol 4 explicitly. 16 | # In python3.5 pickling inner classes works without specifying the protocol. 17 | if sys.version_info[:2] == (3, 4): 18 | pickled = pickle.dumps(flags, 4) 19 | else: 20 | pickled = pickle.dumps(flags) 21 | unpickled = pickle.loads(pickled) 22 | self.assertEqual(unpickled, flags) 23 | # Just making sure... 24 | self.assertEqual(int(unpickled), int(flags)) 25 | 26 | 27 | class PicklingSuccessTestBase(PicklingTestCase): 28 | FlagsClass = None 29 | 30 | def test_pickling_zero_flags(self): 31 | self._pickle_and_unpickle(self.FlagsClass.no_flags) 32 | 33 | def test_pickling_all_flags(self): 34 | self._pickle_and_unpickle(self.FlagsClass.all_flags) 35 | 36 | def test_pickling_single_flag(self): 37 | self._pickle_and_unpickle(self.FlagsClass.f0) 38 | self._pickle_and_unpickle(self.FlagsClass.f1) 39 | self._pickle_and_unpickle(self.FlagsClass.f2) 40 | self._pickle_and_unpickle(self.FlagsClass.f3) 41 | 42 | def test_pickling_two_flags(self): 43 | self._pickle_and_unpickle(self.FlagsClass.f1 | self.FlagsClass.f2) 44 | 45 | 46 | def load_tests(loader, tests, pattern): 47 | return TestSuite() 48 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | import re 2 | from unittest import TestCase 3 | 4 | from flags import Flags, unique, unique_bits 5 | 6 | 7 | class TestUniqueDecorator(TestCase): 8 | def test_with_no_duplicates(self): 9 | @unique 10 | class MyFlags(Flags): 11 | f0 = 1 12 | f1 = 2 13 | f2 = 4 14 | f3 = 8 15 | f4 = f2 | f3 16 | 17 | # make sure that the flags still work after decorating 18 | self.assertEqual(int(MyFlags.f0 | MyFlags.f2), 5) 19 | self.assertEqual(int(MyFlags.f0 & MyFlags.f2), 0) 20 | 21 | def test_with_duplicates(self): 22 | with self.assertRaisesRegex(ValueError, 23 | re.escape(r"duplicate values found in : f1 -> f0, f4 -> f2")): 24 | @unique 25 | class MyFlags(Flags): 26 | f0 = 1 27 | f1 = 1 28 | f2 = 4 29 | f3 = 8 30 | f4 = f2 31 | 32 | def test_all_flags_is_excluded_from_unique_check(self): 33 | @unique 34 | class MyFlags(Flags): 35 | # all_flags is also 1 in this case 36 | f0 = 1 37 | 38 | @unique 39 | class MyFlags2(Flags): 40 | f0 = 1 41 | f1 = 2 42 | # all_flags is also 3 in this case 43 | f2 = 3 44 | 45 | def test_decorator_fails_with_non_final_flags_class(self): 46 | with self.assertRaisesRegex(TypeError, 47 | re.escape(r"unique check can be applied only to flags classes that have members")): 48 | @unique 49 | class MyFlags(Flags): 50 | pass 51 | 52 | 53 | class TestUniqueBitsDecorator(TestCase): 54 | def test_with_no_overlapping_bits(self): 55 | @unique_bits 56 | class MyFlags(Flags): 57 | f0 = 1 58 | f1 = 2 59 | f2 = 4 60 | f3 = 8 61 | 62 | # make sure that the flags still work after decorating 63 | self.assertEqual(int(MyFlags.f0 | MyFlags.f2), 5) 64 | self.assertEqual(int(MyFlags.f0 & MyFlags.f2), 0) 65 | 66 | def test_with_overlapping_bits(self): 67 | with self.assertRaisesRegex(ValueError, 68 | r": '\w+' and '\w+' have overlapping bits"): 69 | @unique_bits 70 | class MyFlags(Flags): 71 | f0 = 1 72 | f1 = 2 73 | f2 = 3 74 | 75 | with self.assertRaisesRegex(ValueError, 76 | r": '\w+' and '\w+' have overlapping bits"): 77 | @unique_bits 78 | class MyFlags2(Flags): 79 | f0 = 1 80 | f1 = 2 81 | f2 = 5 82 | 83 | def test_decorator_fails_with_non_final_flags_class(self): 84 | with self.assertRaisesRegex(TypeError, 85 | re.escape(r"unique check can be applied only to flags classes that have members")): 86 | @unique_bits 87 | class MyFlags(Flags): 88 | pass 89 | -------------------------------------------------------------------------------- /tests/test_flags.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import re 3 | from unittest import TestCase 4 | 5 | from flags import Flags, FlagProperties, FlagData, Const, PROTECTED_FLAGS_CLASS_ATTRIBUTES, UNDEFINED 6 | 7 | 8 | class TestUtilities(TestCase): 9 | """ Testing utility functions and other random stuff to satisfy coverage and make its output more useful. """ 10 | def test_const_repr(self): 11 | self.assertEqual(repr(Const('name1')), 'name1') 12 | self.assertEqual(repr(Const('name2')), 'name2') 13 | 14 | def test_readonly_attribute_of_flag_properties(self): 15 | properties = FlagProperties(name='name', bits=1) 16 | # setting attributes shouldn't fail because properties.readonly==False 17 | properties.name = 'name2' 18 | properties.bits = 2 19 | 20 | del properties.index 21 | self.assertFalse(hasattr(properties, 'index')) 22 | 23 | # We set readonly=True. This disables modifying and deleting attributes of the object. 24 | properties.readonly = True 25 | 26 | with self.assertRaisesRegex(AttributeError, 27 | re.escape(r"Can't set attribute 'name' of readonly 'FlagProperties' object")): 28 | properties.name = 'name3' 29 | 30 | with self.assertRaisesRegex(AttributeError, 31 | re.escape(r"Can't set attribute 'readonly' of readonly 'FlagProperties' object")): 32 | properties.readonly = False 33 | 34 | with self.assertRaisesRegex(AttributeError, 35 | re.escape(r"Can't set attribute 'bits' of readonly 'FlagProperties' object")): 36 | properties.bits = 4 37 | 38 | with self.assertRaisesRegex(AttributeError, 39 | re.escape(r"Can't delete attribute 'index_without_aliases' of " 40 | r"readonly 'FlagProperties' object")): 41 | del properties.index_without_aliases 42 | 43 | # We hope that attempts to modify and delete our readonly attributes have failed. 44 | self.assertTrue(hasattr(properties, 'index_without_aliases')) 45 | self.assertEqual(properties.name, 'name2') 46 | self.assertEqual(properties.bits, 2) 47 | self.assertTrue(properties.readonly) 48 | 49 | 50 | class TestFlagsMemberDeclaration(TestCase): 51 | """ Tests different ways of declaring the members/flags of a flags class. """ 52 | def _test_flags_class(self, flags_class, *, unordered_members=False, has_data=False): 53 | def test_member(name, bits): 54 | member = getattr(flags_class, name) 55 | self.assertIs(type(member), flags_class) 56 | self.assertEqual(int(member), bits) 57 | 58 | if unordered_members: 59 | self.assertSetEqual({int(flags_class.f0), int(flags_class.f1), int(flags_class.f2)}, {1, 2, 4}) 60 | for member_name in ('f0', 'f1', 'f2'): 61 | test_member(member_name, int(getattr(flags_class, member_name))) 62 | else: 63 | test_member('f0', 1) 64 | test_member('f1', 2) 65 | test_member('f2', 4) 66 | 67 | # special/virtual members 68 | test_member('no_flags', 0) 69 | test_member('all_flags', 7) 70 | 71 | self.assertEqual(len(flags_class), 3) 72 | self.assertSetEqual(set(flags_class), {flags_class.f0, flags_class.f1, flags_class.f2}) 73 | 74 | if has_data: 75 | self.assertEqual(flags_class.f0.data, 'data0') 76 | self.assertEqual(flags_class.f1.data, 'data1') 77 | self.assertEqual(flags_class.f2.data, 'data2') 78 | 79 | # TODO: more checks 80 | 81 | def test_flag_names_as_class_members(self): 82 | class MyFlags(Flags): 83 | f0 = ['data0'] 84 | f1 = ['data1'] 85 | f2 = ['data2'] 86 | self._test_flags_class(MyFlags, has_data=True) 87 | 88 | def test_flag_names_as_space_separated_list(self): 89 | class MyFlags(Flags): 90 | __members__ = 'f0 f1 f2' 91 | self._test_flags_class(MyFlags) 92 | 93 | def test_flag_names_as_comma_separated_list(self): 94 | class MyFlags(Flags): 95 | __members__ = ' f0,f1, f2' 96 | self._test_flags_class(MyFlags) 97 | 98 | def test_flag_names_as_tuple(self): 99 | class MyFlags(Flags): 100 | __members__ = ('f0', 'f1', 'f2') 101 | self._test_flags_class(MyFlags) 102 | 103 | def test_flag_names_as_list(self): 104 | class MyFlags(Flags): 105 | __members__ = ['f0', 'f1', 'f2'] 106 | self._test_flags_class(MyFlags) 107 | 108 | def test_flag_names_as_set(self): 109 | class MyFlags(Flags): 110 | __members__ = {'f0', 'f1', 'f2'} 111 | self._test_flags_class(MyFlags, unordered_members=True) 112 | 113 | def test_flag_names_as_frozenset(self): 114 | class MyFlags(Flags): 115 | __members__ = frozenset(['f0', 'f1', 'f2']) 116 | self._test_flags_class(MyFlags, unordered_members=True) 117 | 118 | def test_flags_with_data_as_tuple(self): 119 | class MyFlags(Flags): 120 | __members__ = (('f0', ['data0']), ['f1', ['data1']], ('f2', ['data2'])) 121 | self._test_flags_class(MyFlags, has_data=True) 122 | 123 | def test_flags_with_data_as_list(self): 124 | class MyFlags(Flags): 125 | __members__ = [('f0', ['data0']), ['f1', ['data1']], ('f2', ['data2'])] 126 | self._test_flags_class(MyFlags, has_data=True) 127 | 128 | def test_flags_with_data_as_set(self): 129 | class MyFlags(Flags): 130 | __members__ = {('f0', ('data0',)), ('f1', ('data1',)), ('f2', ('data2',))} 131 | self._test_flags_class(MyFlags, has_data=True, unordered_members=True) 132 | 133 | def test_flags_with_data_as_frozenset(self): 134 | class MyFlags(Flags): 135 | __members__ = frozenset([('f0', ('data0',)), ('f1', ('data1',)), ('f2', ('data2',))]) 136 | self._test_flags_class(MyFlags, has_data=True, unordered_members=True) 137 | 138 | def test_flags_with_data_as_dict(self): 139 | class MyFlags(Flags): 140 | __members__ = dict(f0=['data0'], f1=['data1'], f2=['data2']) 141 | self._test_flags_class(MyFlags, has_data=True, unordered_members=True) 142 | 143 | def test_flags_with_data_as_ordered_dict(self): 144 | class MyFlags(Flags): 145 | __members__ = collections.OrderedDict([('f0', ['data0']), ('f1', ['data1']), ('f2', ['data2'])]) 146 | self._test_flags_class(MyFlags, has_data=True) 147 | 148 | def test_flags_with_data_as_iterable(self): 149 | class MyFlags(Flags): 150 | __members__ = iter([('f0', ['data0']), ('f1', ['data1']), ('f2', ['data2'])]) 151 | self._test_flags_class(MyFlags, has_data=True) 152 | 153 | def _test_special_flags(self, flags_class, *, no_flags_name='no_flags', all_flags_name='all_flags'): 154 | self.assertEqual(flags_class.__no_flags_name__, no_flags_name) 155 | self.assertEqual(flags_class.__all_flags_name__, all_flags_name) 156 | self.assertEqual(len(flags_class), 2) 157 | self.assertEqual(int(getattr(flags_class, no_flags_name)), 0) 158 | self.assertEqual(int(getattr(flags_class, all_flags_name)), 3) 159 | self.assertEqual(flags_class.__all_bits__, 3) 160 | self.assertIn(no_flags_name, flags_class.__all_members__) 161 | self.assertIn(all_flags_name, flags_class.__all_members__) 162 | self.assertNotIn(no_flags_name, flags_class.__members__) 163 | self.assertNotIn(all_flags_name, flags_class.__members__) 164 | 165 | def test_special_flags_with_class_declaration(self): 166 | class MyFlags(Flags): 167 | f0 = () 168 | f1 = () 169 | self._test_special_flags(MyFlags) 170 | 171 | def test_special_flags_with_dynamic_class_creation(self): 172 | flags_class = Flags('MyFlags', 'f0 f1') 173 | self._test_special_flags(flags_class) 174 | 175 | def test_special_flags_with_class_declaration_and_custom_flag_names(self): 176 | class MyFlags(Flags): 177 | __no_flags_name__ = 'custom_no_flags_name' 178 | __all_flags_name__ = 'custom_all_flags_name' 179 | f0 = () 180 | f1 = () 181 | self._test_special_flags(MyFlags, no_flags_name='custom_no_flags_name', 182 | all_flags_name='custom_all_flags_name') 183 | 184 | def test_special_flags_disabled_with_class_declaration(self): 185 | # First we check the non-disabled version 186 | class NoDisable(Flags): 187 | f0 = () 188 | 189 | self.assertTrue(hasattr(NoDisable, 'no_flags')) 190 | self.assertTrue(hasattr(NoDisable, 'all_flags')) 191 | self.assertTrue(hasattr(NoDisable, '__no_flags__')) 192 | self.assertTrue(hasattr(NoDisable, '__all_flags__')) 193 | 194 | # Now let's check the disabled versions 195 | class DisabledNoFlags(Flags): 196 | __no_flags_name__ = None 197 | f0 = () 198 | 199 | self.assertFalse(hasattr(DisabledNoFlags, 'no_flags')) 200 | self.assertTrue(hasattr(DisabledNoFlags, 'all_flags')) 201 | self.assertTrue(hasattr(DisabledNoFlags, '__no_flags__')) 202 | self.assertTrue(hasattr(DisabledNoFlags, '__all_flags__')) 203 | 204 | class DisabledAllFlags(Flags): 205 | __all_flags_name__ = None 206 | f0 = () 207 | 208 | self.assertTrue(hasattr(DisabledAllFlags, 'no_flags')) 209 | self.assertFalse(hasattr(DisabledAllFlags, 'all_flags')) 210 | self.assertTrue(hasattr(DisabledAllFlags, '__no_flags__')) 211 | self.assertTrue(hasattr(DisabledAllFlags, '__all_flags__')) 212 | 213 | class BothDisabled(Flags): 214 | __no_flags_name__ = None 215 | __all_flags_name__ = None 216 | f0 = () 217 | 218 | self.assertFalse(hasattr(BothDisabled, 'no_flags')) 219 | self.assertFalse(hasattr(BothDisabled, 'all_flags')) 220 | self.assertTrue(hasattr(BothDisabled, '__no_flags__')) 221 | self.assertTrue(hasattr(BothDisabled, '__all_flags__')) 222 | 223 | def test_special_flags_with_dynamic_class_creation_and_custom_flag_names(self): 224 | flags_class = Flags('MyFlags', 'f0 f1', no_flags_name='custom_no_flags_name', 225 | all_flags_name='custom_all_flags_name') 226 | self._test_special_flags(flags_class, no_flags_name='custom_no_flags_name', 227 | all_flags_name='custom_all_flags_name') 228 | 229 | def test_special_flags_with_class_declaration_and_custom_flag_names_inherited_from_base_class(self): 230 | class MyFlagsBase(Flags): 231 | __no_flags_name__ = 'custom_no_flags_name' 232 | __all_flags_name__ = 'custom_all_flags_name' 233 | 234 | class MyFlags(MyFlagsBase): 235 | f0 = () 236 | f1 = () 237 | self._test_special_flags(MyFlags, no_flags_name='custom_no_flags_name', 238 | all_flags_name='custom_all_flags_name') 239 | 240 | def test_special_flags_with_dynamic_class_creation_and_custom_flag_names_inherited_from_base_class(self): 241 | class MyFlagsBase(Flags): 242 | __no_flags_name__ = 'custom_no_flags_name' 243 | __all_flags_name__ = 'custom_all_flags_name' 244 | 245 | flags_class = MyFlagsBase('MyFlags', 'f0 f1') 246 | self._test_special_flags(flags_class, no_flags_name='custom_no_flags_name', 247 | all_flags_name='custom_all_flags_name') 248 | 249 | def test_aliases(self): 250 | class MyFlags(Flags): 251 | f1 = 1 252 | f1_alias = 1 253 | f2 = 2 254 | f2_alias = 2 255 | 256 | # all_members includes the aliases and also no_flags and all_flags special members 257 | self.assertEqual(len(MyFlags.__all_members__), 6) 258 | self.assertEqual(len(MyFlags.__members__), 4) 259 | self.assertEqual(len(MyFlags.__members_without_aliases__), 2) 260 | self.assertSetEqual(set(MyFlags.__member_aliases__.items()), {('f1_alias', 'f1'), ('f2_alias', 'f2')}) 261 | 262 | 263 | class TestFlagsDeclarationErrors(TestCase): 264 | def test_alias_declares_data(self): 265 | with self.assertRaisesRegex(ValueError, re.escape(r"You aren't allowed to associate data with alias 'f2'")): 266 | class MyFlags(Flags): 267 | f0 = 1 268 | f2 = 1, None 269 | 270 | with self.assertRaisesRegex(ValueError, re.escape(r"You aren't allowed to associate data with alias 'f2'")): 271 | class MyFlags2(Flags): 272 | f0 = 1 273 | f2 = 1, 'data' 274 | 275 | def test_duplicate_flag_name_in_member_names_string(self): 276 | with self.assertRaisesRegex(ValueError, re.escape(r"Duplicate flag name: 'f1'")): 277 | Flags('MyFlags', 'f0 f1 f1') 278 | 279 | def test_default_special_flag_name_conflicts_with_a_declared_flag(self): 280 | with self.assertRaisesRegex(ValueError, re.escape(r"Duplicate flag name: 'no_flags'")): 281 | class MyFlags(Flags): 282 | no_flags = () 283 | 284 | def test_custom_special_flag_name_conflicts_with_a_declared_flag(self): 285 | with self.assertRaisesRegex(ValueError, re.escape(r"Duplicate flag name: 'f0'")): 286 | class MyFlags(Flags): 287 | __no_flags_name__ = 'f0' 288 | f0 = () 289 | 290 | def test_custom_special_flag_name_conflicts_with_anoter_custom_special_flag_name(self): 291 | with self.assertRaisesRegex(ValueError, re.escape(r"Duplicate flag name: 'special_flag'")): 292 | class MyFlags(Flags): 293 | __no_flags_name__ = 'special_flag' 294 | __all_flags_name__ = 'special_flag' 295 | f0 = () 296 | 297 | def test_custom_special_flag_name_conflicts_with_anoter_default_special_flag_name(self): 298 | with self.assertRaisesRegex(ValueError, re.escape(r"Duplicate flag name: 'all_flags'")): 299 | class MyFlags(Flags): 300 | __no_flags_name__ = 'all_flags' 301 | f0 = () 302 | 303 | def test_flags_class_new_alters_the_value_of_bits(self): 304 | with self.assertRaisesRegex(RuntimeError, 305 | r"MyFlags has altered the assigned bits of member 'f0' from 1 to 2"): 306 | class MyFlags(Flags): 307 | f0 = () 308 | 309 | def __new__(cls, bits): 310 | bits = 2 311 | return Flags.__new__(cls, bits) 312 | 313 | def test_slots_usage_isnt_allowed(self): 314 | with self.assertRaisesRegex(RuntimeError, r"You aren't allowed to use __slots__ in your Flags subclasses"): 315 | class MyFlags(Flags): 316 | __slots__ = ('my_slot',) 317 | f0 = () 318 | 319 | def test_iterable_longer_than_2(self): 320 | with self.assertRaisesRegex( 321 | ValueError, 322 | re.escape(r"Iterable is expected to have at most 2 items instead of 3 " 323 | r"for flag 'f0', iterable: (None, None, None)")): 324 | class MyFlags(Flags): 325 | f0 = (None, None, None) 326 | 327 | def test_bits_is_str(self): 328 | with self.assertRaisesRegex( 329 | ValueError, 330 | re.escape(r"Iterable is expected to have at most 2 items instead of 8 " 331 | r"for flag 'f0', iterable: 'str_bits'")): 332 | class MyFlags(Flags): 333 | f0 = 'str_bits' 334 | 335 | def test_bits_is_bool(self): 336 | with self.assertRaisesRegex( 337 | TypeError, 338 | re.escape(r"Expected an int or an iterable of at most 2 items for flag 'f0', received False")): 339 | class MyFlags(Flags): 340 | f0 = False 341 | 342 | def test_bits_is_none(self): 343 | with self.assertRaisesRegex( 344 | TypeError, 345 | re.escape(r"Expected an int or an iterable of at most 2 items for flag 'f0', received None")): 346 | class MyFlags(Flags): 347 | f0 = None 348 | 349 | def test_bits_is_bool_in_2_long_iterable(self): 350 | with self.assertRaisesRegex( 351 | TypeError, 352 | re.escape(r"Expected an int value as the bits of flag 'f0', received False")): 353 | class MyFlags(Flags): 354 | f0 = (False, 'data0') 355 | 356 | def test_bits_is_str_in_2_long_iterable(self): 357 | with self.assertRaisesRegex( 358 | TypeError, 359 | re.escape(r"Expected an int value as the bits of flag 'f0', received 5.5")): 360 | class MyFlags(Flags): 361 | f0 = 5.5, 'data0' 362 | 363 | def test_bits_is_none_in_2_long_iterable(self): 364 | with self.assertRaisesRegex( 365 | TypeError, 366 | re.escape(r"Expected an int value as the bits of flag 'f1', received None")): 367 | class MyFlags(Flags): 368 | f0 = 0b0011 369 | f1 = (None, 'data1') 370 | f2 = 0b0110 371 | 372 | 373 | class TestAutoAssignedBits(TestCase): 374 | def test_auto_assign_with_flag_data(self): 375 | class MyFlagData(FlagData): 376 | pass 377 | 378 | my_flag_data = MyFlagData() 379 | 380 | class MyFlags(Flags): 381 | f0 = 0b001 382 | f1 = my_flag_data 383 | f2 = 0b010 384 | 385 | self.assertListEqual([int(MyFlags.f0), int(MyFlags.f1), int(MyFlags.f2)], [0b001, 0b100, 0b010]) 386 | self.assertIs(MyFlags.f0.data, UNDEFINED) 387 | self.assertIs(MyFlags.f1.data, my_flag_data) 388 | self.assertIs(MyFlags.f2.data, UNDEFINED) 389 | 390 | def test_auto_assign_with_empty_tuple(self): 391 | class MyFlags(Flags): 392 | f0 = 0b0011 393 | f1 = () 394 | f2 = 0b1000 395 | 396 | self.assertListEqual([int(MyFlags.f0), int(MyFlags.f1), int(MyFlags.f2)], [0b0011, 0b0100, 0b1000]) 397 | self.assertIs(MyFlags.f0.data, UNDEFINED) 398 | self.assertIs(MyFlags.f1.data, UNDEFINED) 399 | self.assertIs(MyFlags.f2.data, UNDEFINED) 400 | 401 | def test_auto_assign_with_empty_list(self): 402 | class MyFlags(Flags): 403 | f0 = 0b0011 404 | f1 = [] 405 | f2 = 0b0110 406 | 407 | self.assertListEqual([int(MyFlags.f0), int(MyFlags.f1), int(MyFlags.f2)], [0b0011, 0b1000, 0b0110]) 408 | self.assertIs(MyFlags.f0.data, UNDEFINED) 409 | self.assertIs(MyFlags.f1.data, UNDEFINED) 410 | self.assertIs(MyFlags.f2.data, UNDEFINED) 411 | 412 | 413 | class TestFlagsInheritance(TestCase): 414 | def test_dynamic_flags_class_creation_subclasses_the_called_flags_class(self): 415 | MyFlags = Flags('MyFlags', '') 416 | self.assertTrue(issubclass(MyFlags, Flags)) 417 | 418 | MyFlags2 = MyFlags('MyFlags2', '') 419 | self.assertTrue(issubclass(MyFlags2, MyFlags)) 420 | 421 | def test_subclassing_more_than_one_abstract_flags_classes_works(self): 422 | class FlagsBase1(Flags): 423 | __no_flags_name__ = 'custom_no_flags_name' 424 | 425 | class FlagsBase2(Flags): 426 | __all_flags_name__ = 'custom_all_flags_name' 427 | 428 | class MyFlags(FlagsBase1, FlagsBase2): 429 | f0 = () 430 | f1 = () 431 | 432 | self.assertEqual(MyFlags.__no_flags_name__, 'custom_no_flags_name') 433 | self.assertEqual(MyFlags.__all_flags_name__, 'custom_all_flags_name') 434 | 435 | def test_subclassing_fails_if_at_least_one_flags_base_class_isnt_abstract(self): 436 | class NonAbstract(Flags): 437 | f0 = () 438 | 439 | class Abstract1(Flags): 440 | pass 441 | 442 | class Abstract2(Flags): 443 | pass 444 | 445 | with self.assertRaisesRegex( 446 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 447 | NonAbstract('MyFlags', 'flag1 flag2') 448 | 449 | with self.assertRaisesRegex( 450 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 451 | NonAbstract('MyFlags', 'flag1 flag2', mixins=[Abstract1]) 452 | 453 | with self.assertRaisesRegex( 454 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 455 | Flags('MyFlags', 'flag1 flag2', mixins=[NonAbstract]) 456 | 457 | with self.assertRaisesRegex( 458 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 459 | Flags('MyFlags', 'flag1 flag2', mixins=[Abstract1, NonAbstract]) 460 | 461 | with self.assertRaisesRegex( 462 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 463 | Flags('MyFlags', 'flag1 flag2', mixins=[Abstract1, NonAbstract, Abstract2]) 464 | 465 | with self.assertRaisesRegex( 466 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 467 | Abstract1('MyFlags', 'flag1 flag2', mixins=[NonAbstract]) 468 | 469 | with self.assertRaisesRegex( 470 | RuntimeError, re.escape("You can't subclass 'NonAbstract' because it has already defined flag members")): 471 | Abstract1('MyFlags', 'flag1 flag2', mixins=[Abstract2, NonAbstract]) 472 | 473 | 474 | class TestFlagsClassInstantiationFromValue(TestCase): 475 | def test_instantiation_of_abstract_flags_class_fails(self): 476 | # A flags class is considered to be "abstract" if it doesn't define any members. 477 | with self.assertRaisesRegex(RuntimeError, r"Instantiation of abstract flags class '.+\.Flags' isn't allowed."): 478 | Flags() 479 | 480 | class MyAbstractFlagsClass(Flags): 481 | pass 482 | 483 | with self.assertRaisesRegex(RuntimeError, 484 | r"Instantiation of abstract flags class " 485 | r"'.+\.MyAbstractFlagsClass' isn't allowed."): 486 | MyAbstractFlagsClass() 487 | 488 | # Subclassing MyAbstractFlagsClass by calling it: 489 | MyAbstractFlagsClass_2 = MyAbstractFlagsClass('MyAbstractFlagsClass_2', '') 490 | 491 | with self.assertRaisesRegex(RuntimeError, 492 | r"Instantiation of abstract flags class " 493 | r"'.+\.MyAbstractFlagsClass_2' isn't allowed."): 494 | MyAbstractFlagsClass_2() 495 | 496 | def test_no_arg_to_create_zero_flag(self): 497 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 498 | flag = MyFlags() 499 | self.assertIsInstance(flag, MyFlags) 500 | self.assertEqual(flag, MyFlags.no_flags) 501 | self.assertEqual(int(flag), 0) 502 | 503 | def test_value_is_flags_instance(self): 504 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 505 | flag = MyFlags(MyFlags.f1) 506 | self.assertIsInstance(flag, MyFlags) 507 | self.assertEqual(flag, MyFlags.f1) 508 | 509 | def test_int_value_zero(self): 510 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 511 | flag = MyFlags(0) 512 | self.assertIsInstance(flag, MyFlags) 513 | self.assertEqual(flag, MyFlags.no_flags) 514 | self.assertEqual(int(flag), 0) 515 | 516 | def test_int_value_single_member(self): 517 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 518 | flag = MyFlags(int(MyFlags.f1)) 519 | self.assertIsInstance(flag, MyFlags) 520 | self.assertEqual(flag, MyFlags.f1) 521 | 522 | def test_int_value_multiple_members(self): 523 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 524 | flag = MyFlags(int(MyFlags.f1) | int(MyFlags.f2)) 525 | self.assertIsInstance(flag, MyFlags) 526 | self.assertEqual(flag, MyFlags.f1 | MyFlags.f2) 527 | 528 | def test_str_value_zero(self): 529 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 530 | flag = MyFlags(str(MyFlags.no_flags)) 531 | self.assertIsInstance(flag, MyFlags) 532 | self.assertEqual(flag, MyFlags.no_flags) 533 | 534 | def test_str_value_single_member(self): 535 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 536 | flag = MyFlags(str(MyFlags.f1)) 537 | self.assertIsInstance(flag, MyFlags) 538 | self.assertEqual(flag, MyFlags.f1) 539 | 540 | def test_str_value_multiple_members(self): 541 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 542 | flag = MyFlags(str(MyFlags.f1 | MyFlags.f2)) 543 | self.assertIsInstance(flag, MyFlags) 544 | self.assertEqual(flag, MyFlags.f1 | MyFlags.f2) 545 | 546 | def test_simple_str_value_zero(self): 547 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 548 | flag = MyFlags(MyFlags.no_flags.to_simple_str()) 549 | self.assertIsInstance(flag, MyFlags) 550 | self.assertEqual(flag, MyFlags.no_flags) 551 | 552 | def test_simple_str_value_single_member(self): 553 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 554 | flag = MyFlags(MyFlags.f1.to_simple_str()) 555 | self.assertIsInstance(flag, MyFlags) 556 | self.assertEqual(flag, MyFlags.f1) 557 | 558 | def test_simple_str_value_multiple_members(self): 559 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 560 | flag = MyFlags((MyFlags.f1 | MyFlags.f2).to_simple_str()) 561 | self.assertIsInstance(flag, MyFlags) 562 | self.assertEqual(flag, MyFlags.f1 | MyFlags.f2) 563 | 564 | def test_float_value_fails(self): 565 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 566 | with self.assertRaisesRegex(TypeError, re.escape(r"Can't instantiate flags class 'MyFlags' from value 0.5")): 567 | MyFlags(0.5) 568 | 569 | def test_bool_value_fails(self): 570 | MyFlags = Flags('MyFlags', 'f0 f1 f2') 571 | with self.assertRaisesRegex(TypeError, re.escape(r"Can't instantiate flags class 'MyFlags' from value False")): 572 | MyFlags(False) 573 | 574 | 575 | class TestProcessMemberDefinitions(TestCase): 576 | """ Tests the process_member_definitions() method of the flags class. """ 577 | def test_returned_name_isnt_a_string(self): 578 | with self.assertRaisesRegex(TypeError, r"Flag name should be an str but it is 123"): 579 | class MyFlags(Flags): 580 | f0 = () 581 | 582 | @classmethod 583 | def process_member_definitions(cls, member_definitions): 584 | return [(123, 1, None)] 585 | 586 | def test_returned_bits_isnt_an_int(self): 587 | with self.assertRaisesRegex(TypeError, r"Bits for flag 'f0' should be an int but it is 'invalid_bits'"): 588 | class MyFlags(Flags): 589 | f0 = () 590 | 591 | @classmethod 592 | def process_member_definitions(cls, member_defintions): 593 | return [['f0', 'invalid_bits', None]] 594 | 595 | def test_returned_bits_is_zero(self): 596 | with self.assertRaisesRegex(ValueError, r"Flag 'f0' has the invalid value of zero"): 597 | class MyFlags(Flags): 598 | f0 = () 599 | 600 | @classmethod 601 | def process_member_definitions(cls, member_defintions): 602 | yield 'f0', 0, None 603 | 604 | def test_returning_empty_iterable_fails(self): 605 | with self.assertRaisesRegex(RuntimeError, 606 | re.escape("MyFlags.process_member_definitions returned an empty iterable")): 607 | class MyFlags(Flags): 608 | f0 = () 609 | 610 | @classmethod 611 | def process_member_definitions(cls, member_definitions): 612 | return () 613 | 614 | 615 | class TestFlagsClassMethods(TestCase): 616 | def setUp(self): 617 | self.MyFlags = Flags('MyFlags', 'f0 f1 f2') 618 | 619 | def test_iter(self): 620 | self.assertListEqual(list(iter(self.MyFlags)), [self.MyFlags.f0, self.MyFlags.f1, self.MyFlags.f2]) 621 | 622 | def test_reversed(self): 623 | self.assertListEqual(list(reversed(self.MyFlags)), [self.MyFlags.f2, self.MyFlags.f1, self.MyFlags.f0]) 624 | 625 | def test_getitem(self): 626 | self.assertEqual(self.MyFlags['f0'], self.MyFlags.f0) 627 | self.assertEqual(self.MyFlags['f1'], self.MyFlags.f1) 628 | self.assertNotEqual(self.MyFlags['f0'], self.MyFlags.f1) 629 | 630 | def test_setitem_fails(self): 631 | with self.assertRaisesRegex(TypeError, r"does not support item assignment"): 632 | self.MyFlags['f0'] = 'whatever' 633 | 634 | def test_bool(self): 635 | self.assertTrue(Flags) 636 | self.assertTrue(self.MyFlags) 637 | 638 | def test_len(self): 639 | self.assertEqual(len(Flags), 0) 640 | self.assertEqual(len(self.MyFlags), 3) 641 | 642 | def test_repr(self): 643 | self.assertEqual(repr(self.MyFlags), '') 644 | 645 | def test_setattr_fails_with_protected_class_members(self): 646 | for attribute in PROTECTED_FLAGS_CLASS_ATTRIBUTES | set(self.MyFlags.__all_members__.keys()): 647 | if attribute in ('no_flags', 'all_flags', '__writable_protected_flags_class_attributes__'): 648 | regex = re.escape(attribute) 649 | else: 650 | regex = re.escape(r"Can't delete protected attribute '%s'" % attribute) 651 | 652 | with self.assertRaisesRegex(AttributeError, regex): 653 | delattr(self.MyFlags, attribute) 654 | 655 | def test_delattr_fails_with_protected_class_members(self): 656 | for attribute in PROTECTED_FLAGS_CLASS_ATTRIBUTES | set(self.MyFlags.__all_members__.keys()): 657 | with self.assertRaisesRegex( 658 | AttributeError, re.escape(r"Can't assign protected attribute '%s'" % attribute)): 659 | setattr(self.MyFlags, attribute, 'new_value') 660 | 661 | def test_setattr_and_delattr_work_with_non_protected_class_members(self): 662 | self.assertFalse(hasattr(self.MyFlags, 'non_protected_member')) 663 | self.MyFlags.non_protected_member = 42 664 | self.assertEqual(self.MyFlags.non_protected_member, 42) 665 | del self.MyFlags.non_protected_member 666 | self.assertFalse(hasattr(self.MyFlags, 'non_protected_member')) 667 | 668 | 669 | class TestFlagsInstanceMethods(TestCase): 670 | class MyFlags(Flags): 671 | f0 = ['data0'] 672 | f1 = ['data1'] 673 | f2 = ['data2'] 674 | 675 | class NoDottedSingleFlagStr(Flags): 676 | __dotted_single_flag_str__ = False 677 | f0 = () 678 | f1 = () 679 | 680 | class SubsetFlag(Flags): 681 | # f1 is a proper/strict subset of f3 682 | f1 = 1 683 | f3 = 3 684 | 685 | def test_is_member(self): 686 | self.assertFalse(self.MyFlags.no_flags.is_member) 687 | self.assertFalse(self.MyFlags.all_flags.is_member) 688 | self.assertTrue(self.MyFlags.f0.is_member) 689 | self.assertTrue(self.MyFlags.f1.is_member) 690 | self.assertTrue(self.MyFlags.f2.is_member) 691 | self.assertFalse((self.MyFlags.f0 | self.MyFlags.f1).is_member) 692 | self.assertFalse((self.MyFlags.f0 | self.MyFlags.f2).is_member) 693 | self.assertFalse((self.MyFlags.f1 | self.MyFlags.f2).is_member) 694 | 695 | def test_properties(self): 696 | self.assertIsNone(self.MyFlags.no_flags.properties) 697 | self.assertIsNone(self.MyFlags.all_flags.properties) 698 | self.assertIsNotNone(self.MyFlags.f0.properties) 699 | self.assertIsNotNone(self.MyFlags.f1.properties) 700 | self.assertIsNotNone(self.MyFlags.f2.properties) 701 | self.assertIsNone((self.MyFlags.f0 | self.MyFlags.f1).properties) 702 | self.assertIsNone((self.MyFlags.f0 | self.MyFlags.f2).properties) 703 | self.assertIsNone((self.MyFlags.f1 | self.MyFlags.f2).properties) 704 | 705 | def test_name(self): 706 | self.assertIsNone(self.MyFlags.no_flags.name) 707 | self.assertIsNone(self.MyFlags.all_flags.name) 708 | self.assertEqual(self.MyFlags.f0.name, 'f0') 709 | self.assertEqual(self.MyFlags.f1.name, 'f1') 710 | self.assertEqual(self.MyFlags.f2.name, 'f2') 711 | self.assertIsNone((self.MyFlags.f0 | self.MyFlags.f1).name) 712 | self.assertIsNone((self.MyFlags.f0 | self.MyFlags.f2).name) 713 | self.assertIsNone((self.MyFlags.f1 | self.MyFlags.f2).name) 714 | 715 | def test_data(self): 716 | self.assertIs(self.MyFlags.no_flags.data, UNDEFINED) 717 | self.assertIs(self.MyFlags.all_flags.data, UNDEFINED) 718 | self.assertEqual(self.MyFlags.f0.data, 'data0') 719 | self.assertEqual(self.MyFlags.f1.data, 'data1') 720 | self.assertEqual(self.MyFlags.f2.data, 'data2') 721 | self.assertIs((self.MyFlags.f0 | self.MyFlags.f1).data, UNDEFINED) 722 | self.assertIs((self.MyFlags.f0 | self.MyFlags.f2).data, UNDEFINED) 723 | self.assertIs((self.MyFlags.f1 | self.MyFlags.f2).data, UNDEFINED) 724 | 725 | def test_getattr(self): 726 | flags = self.MyFlags.f0 727 | 728 | with self.assertRaises(AttributeError): 729 | _ = flags.no_flags 730 | with self.assertRaises(AttributeError): 731 | _ = flags.all_flags 732 | 733 | self.assertTrue(flags.f0) 734 | self.assertFalse(flags.f1) 735 | self.assertFalse(flags.f2) 736 | 737 | def test_int(self): 738 | self.assertEqual(int(self.MyFlags.no_flags), 0) 739 | self.assertEqual(int(self.MyFlags.all_flags), 7) 740 | self.assertEqual(int(self.MyFlags.f0), 1) 741 | self.assertEqual(int(self.MyFlags.f1), 2) 742 | self.assertEqual(int(self.MyFlags.f2), 4) 743 | self.assertEqual(int(self.MyFlags.f0 | self.MyFlags.f1), 3) 744 | self.assertEqual(int(self.MyFlags.f0 | self.MyFlags.f2), 5) 745 | self.assertEqual(int(self.MyFlags.f1 | self.MyFlags.f2), 6) 746 | 747 | def test_bool(self): 748 | self.assertFalse(self.MyFlags.no_flags) 749 | self.assertTrue(self.MyFlags.all_flags) 750 | self.assertTrue(self.MyFlags.f0) 751 | self.assertTrue(self.MyFlags.f1) 752 | self.assertTrue(self.MyFlags.f2) 753 | self.assertTrue(self.MyFlags.f0 | self.MyFlags.f1) 754 | self.assertTrue(self.MyFlags.f0 | self.MyFlags.f2) 755 | self.assertTrue(self.MyFlags.f1 | self.MyFlags.f2) 756 | 757 | def test_len(self): 758 | self.assertEqual(len(self.MyFlags.no_flags), 0) 759 | self.assertEqual(len(self.MyFlags.all_flags), 3) 760 | self.assertEqual(len(self.MyFlags.f0), 1) 761 | self.assertEqual(len(self.MyFlags.f1), 1) 762 | self.assertEqual(len(self.MyFlags.f2), 1) 763 | self.assertEqual(len(self.MyFlags.f0 | self.MyFlags.f1), 2) 764 | self.assertEqual(len(self.MyFlags.f0 | self.MyFlags.f2), 2) 765 | self.assertEqual(len(self.MyFlags.f1 | self.MyFlags.f2), 2) 766 | 767 | def test_iter(self): 768 | self.assertEqual(list(self.MyFlags.no_flags), []) 769 | self.assertEqual(list(self.MyFlags.all_flags), [self.MyFlags.f0, self.MyFlags.f1, self.MyFlags.f2]) 770 | self.assertEqual(list(self.MyFlags.f0), [self.MyFlags.f0]) 771 | self.assertEqual(list(self.MyFlags.f1), [self.MyFlags.f1]) 772 | self.assertEqual(list(self.MyFlags.f2), [self.MyFlags.f2]) 773 | self.assertEqual(list(self.MyFlags.f0 | self.MyFlags.f1), [self.MyFlags.f0, self.MyFlags.f1]) 774 | self.assertEqual(list(self.MyFlags.f0 | self.MyFlags.f2), [self.MyFlags.f0, self.MyFlags.f2]) 775 | self.assertEqual(list(self.MyFlags.f1 | self.MyFlags.f2), [self.MyFlags.f1, self.MyFlags.f2]) 776 | 777 | def test_reversed(self): 778 | self.assertEqual(list(reversed(self.MyFlags.no_flags)), []) 779 | self.assertEqual(list(reversed(self.MyFlags.all_flags)), [self.MyFlags.f2, self.MyFlags.f1, self.MyFlags.f0]) 780 | self.assertEqual(list(reversed(self.MyFlags.f0)), [self.MyFlags.f0]) 781 | self.assertEqual(list(reversed(self.MyFlags.f1)), [self.MyFlags.f1]) 782 | self.assertEqual(list(reversed(self.MyFlags.f2)), [self.MyFlags.f2]) 783 | self.assertEqual(list(reversed(self.MyFlags.f0 | self.MyFlags.f1)), [self.MyFlags.f1, self.MyFlags.f0]) 784 | self.assertEqual(list(reversed(self.MyFlags.f0 | self.MyFlags.f2)), [self.MyFlags.f2, self.MyFlags.f0]) 785 | self.assertEqual(list(reversed(self.MyFlags.f1 | self.MyFlags.f2)), [self.MyFlags.f2, self.MyFlags.f1]) 786 | 787 | def test_repr(self): 788 | self.assertEqual(repr(self.MyFlags.no_flags), '') 789 | self.assertEqual(repr(self.MyFlags.all_flags), '') 790 | self.assertEqual(repr(self.MyFlags.f0), "") 791 | self.assertEqual(repr(self.MyFlags.f1), "") 792 | self.assertEqual(repr(self.MyFlags.f2), "") 793 | self.assertEqual(repr(self.MyFlags.f0 | self.MyFlags.f1), '') 794 | self.assertEqual(repr(self.MyFlags.f0 | self.MyFlags.f2), '') 795 | self.assertEqual(repr(self.MyFlags.f1 | self.MyFlags.f2), '') 796 | 797 | self.assertEqual(repr(self.NoDottedSingleFlagStr.no_flags), '') 798 | self.assertEqual(repr(self.NoDottedSingleFlagStr.all_flags), '') 799 | self.assertEqual(repr(self.NoDottedSingleFlagStr.f0), "") 800 | self.assertEqual(repr(self.NoDottedSingleFlagStr.f1), "") 801 | self.assertEqual(repr(self.NoDottedSingleFlagStr.f0 | self.NoDottedSingleFlagStr.f1), 802 | '') 803 | 804 | self.assertEqual(repr(self.SubsetFlag.f3), '') 805 | 806 | def test_str(self): 807 | self.assertEqual(str(self.MyFlags.no_flags), 'MyFlags()') 808 | self.assertEqual(str(self.MyFlags.all_flags), 'MyFlags(f0|f1|f2)') 809 | self.assertEqual(str(self.MyFlags.f0), 'MyFlags.f0') 810 | self.assertEqual(str(self.MyFlags.f1), 'MyFlags.f1') 811 | self.assertEqual(str(self.MyFlags.f2), 'MyFlags.f2') 812 | self.assertEqual(str(self.MyFlags.f0 | self.MyFlags.f1), 'MyFlags(f0|f1)') 813 | self.assertEqual(str(self.MyFlags.f0 | self.MyFlags.f2), 'MyFlags(f0|f2)') 814 | self.assertEqual(str(self.MyFlags.f1 | self.MyFlags.f2), 'MyFlags(f1|f2)') 815 | 816 | self.assertEqual(str(self.NoDottedSingleFlagStr.no_flags), 'NoDottedSingleFlagStr()') 817 | self.assertEqual(str(self.NoDottedSingleFlagStr.all_flags), 'NoDottedSingleFlagStr(f0|f1)') 818 | self.assertEqual(str(self.NoDottedSingleFlagStr.f0), 'NoDottedSingleFlagStr(f0)') 819 | self.assertEqual(str(self.NoDottedSingleFlagStr.f1), 'NoDottedSingleFlagStr(f1)') 820 | self.assertEqual(str(self.NoDottedSingleFlagStr.f0 | self.NoDottedSingleFlagStr.f1), 821 | 'NoDottedSingleFlagStr(f0|f1)') 822 | 823 | self.assertEqual(str(self.SubsetFlag.f3), 'SubsetFlag(f1|f3)') 824 | 825 | def test_from_str(self): 826 | self.assertEqual(self.MyFlags.no_flags, self.MyFlags.from_str('MyFlags()')) 827 | self.assertEqual(self.MyFlags.all_flags, self.MyFlags.from_str('MyFlags(f0|f1|f2)')) 828 | self.assertEqual(self.MyFlags.f0, self.MyFlags.from_str('MyFlags.f0')) 829 | self.assertEqual(self.MyFlags.f1, self.MyFlags.from_str('MyFlags.f1')) 830 | self.assertEqual(self.MyFlags.f2, self.MyFlags.from_str('MyFlags.f2')) 831 | self.assertEqual(self.MyFlags.f0 | self.MyFlags.f1, self.MyFlags.from_str('MyFlags(f0|f1)')) 832 | self.assertEqual(self.MyFlags.f0 | self.MyFlags.f2, self.MyFlags.from_str('MyFlags(f0|f2)')) 833 | self.assertEqual(self.MyFlags.f1 | self.MyFlags.f2, self.MyFlags.from_str('MyFlags(f1|f2)')) 834 | 835 | with self.assertRaisesRegex(TypeError, re.escape(r"Expected an str instance, received 42")): 836 | self.MyFlags.from_str(42) 837 | 838 | def test_bits_from_str(self): 839 | self.assertEqual(0, self.MyFlags.bits_from_str('MyFlags()')) 840 | self.assertEqual(7, self.MyFlags.bits_from_str('MyFlags(f0|f1|f2)')) 841 | self.assertEqual(1, self.MyFlags.bits_from_str('MyFlags.f0')) 842 | self.assertEqual(2, self.MyFlags.bits_from_str('MyFlags.f1')) 843 | self.assertEqual(4, self.MyFlags.bits_from_str('MyFlags.f2')) 844 | self.assertEqual(3, self.MyFlags.bits_from_str('MyFlags(f0|f1)')) 845 | self.assertEqual(5, self.MyFlags.bits_from_str('MyFlags(f0|f2)')) 846 | self.assertEqual(6, self.MyFlags.bits_from_str('MyFlags(f1|f2)')) 847 | 848 | with self.assertRaisesRegex(ValueError, re.escape(r"Invalid flag 'MyFlags.invalid' in string 'f0|invalid|f1'")): 849 | self.MyFlags.bits_from_str('f0|invalid|f1') 850 | 851 | with self.assertRaisesRegex(ValueError, re.escape("MyFlags.bits_from_str: invalid input: 'MyFlags(f0'")): 852 | self.MyFlags.bits_from_str('MyFlags(f0') 853 | 854 | with self.assertRaisesRegex(ValueError, re.escape("MyFlags.bits_from_str: invalid input: 'MyFlags