├── MANIFEST.in ├── .travis.yml ├── .gitignore ├── tox.ini ├── setup.py ├── LICENSE-MIT ├── value.py ├── test_value.py └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE-MIT 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: pip install tox --use-mirrors 3 | script: tox 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Vim 4 | *.swp 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | 19 | # Installer logs 20 | pip-log.txt 21 | 22 | # Unit test / coverage reports 23 | .coverage 24 | .tox 25 | nosetests.xml 26 | 27 | #Translations 28 | *.mo 29 | 30 | #Mr Developer 31 | .mr.developer.cfg 32 | 33 | # Sphinx 34 | docs/_* 35 | 36 | # jython 37 | *$py.class 38 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests in 2 | # multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip 4 | # install tox" and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py25, py26, py27, py32, py33, pypy 8 | 9 | [testenv] 10 | commands = py.test 11 | deps = pytest 12 | 13 | [testenv:py27] 14 | commands = py.test --doctest-glob README.rst 15 | deps = pytest 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | import value 4 | 5 | 6 | setup( 7 | name=value.__name__, 8 | version=value.__version__, 9 | author='Vladimir Keleshev', 10 | author_email='vladimir@keleshev.com', 11 | description='Implementation of Value Object pattern', 12 | license='MIT', 13 | keywords='value object pattern', 14 | url='http://github.com/halst/value', 15 | py_modules=[value.__name__], 16 | long_description=open('README.rst').read(), 17 | classifiers=[ 18 | 'Development Status :: 3 - Alpha', 19 | 'Topic :: Utilities', 20 | 'Programming Language :: Python :: 2.5', 21 | 'Programming Language :: Python :: 2.6', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3.2', 24 | 'Programming Language :: Python :: 3.3', 25 | 'License :: OSI Approved :: MIT License', 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Vladimir Keleshev, 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software 6 | without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to 9 | whom the Software is furnished to do so, subject to the 10 | following conditions: 11 | 12 | The above copyright notice and this permission notice shall 13 | be included in all copies or substantial portions of the 14 | Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 17 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 19 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 20 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /value.py: -------------------------------------------------------------------------------- 1 | from inspect import getargspec 2 | 3 | 4 | __version__ = '0.1.0' 5 | 6 | 7 | class Value(object): 8 | 9 | def __new__(class_, *args, **kwargs): 10 | self = object.__new__(class_, *args, **kwargs) 11 | para, varargs, keywords, defaults = getargspec(self.__init__) 12 | if varargs: 13 | raise ValueError('`*args` are not allowed in __init__') 14 | if keywords: 15 | raise ValueError('`**kwargs` are not allowed in __init__') 16 | if not all(type(p) is str for p in para): 17 | raise ValueError('parameter unpacking is not allowed in __init__') 18 | defaults = () if not defaults else defaults 19 | self.__dict__.update(dict(zip(para[:0:-1], defaults[::-1]))) 20 | self.__dict__.update(dict(zip(para[1:], args) + kwargs.items())) 21 | return self 22 | 23 | def __repr__(self): 24 | null = object() 25 | para, _, _, defaults = getargspec(self.__init__) 26 | para = para[1:] 27 | actuals = [getattr(self, p) for p in para] 28 | defaults = () if not defaults else defaults 29 | defaults = defaults[1:] if len(defaults) == len(para) + 1 else defaults 30 | defaults = (null,) * (len(para) - len(defaults)) + defaults 31 | return '%s(%s)' % (self.__class__.__name__, 32 | ', '.join(repr(a) for a, d in zip(actuals, defaults) if a != d)) 33 | 34 | def __hash__(self): 35 | return hash(repr(self)) 36 | 37 | def __eq__(self, other): 38 | return repr(self) == repr(other) 39 | 40 | def __ne__(self, other): 41 | return repr(self) != repr(other) 42 | -------------------------------------------------------------------------------- /test_value.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from pytest import raises 4 | 5 | from value import Value 6 | 7 | 8 | def fixture(init): 9 | class Option(Value): 10 | __init__ = init 11 | return Option 12 | 13 | 14 | def test_primitive_value(): 15 | Option = fixture(lambda self: None) 16 | assert repr(Option()) == 'Option()' 17 | assert hash(Option()) == hash('Option()') 18 | assert Option() == Option() 19 | assert not Option() != Option() 20 | assert 'self' not in Option().__dict__ 21 | 22 | 23 | def test_init_without_defaults(): 24 | Option = fixture(lambda self, short, long: None) 25 | for option in [Option('-a', '--all'), 26 | Option(short='-a', long='--all'), 27 | Option('-a', long='--all')]: 28 | assert repr(option) == "Option('-a', '--all')" 29 | assert hash(option) == hash("Option('-a', '--all')") 30 | assert option == Option('-a', '--all') 31 | assert option.short == '-a' 32 | assert option.long == '--all' 33 | assert 'self' not in option.__dict__ 34 | 35 | 36 | def test_init_with_defaults(): 37 | Option = fixture(lambda self, short, long='--default': None) 38 | assert repr(Option('-a')) == "Option('-a')" 39 | assert repr(Option('-a', '--default')) == "Option('-a')" 40 | assert repr(Option('-a', '--all')) == "Option('-a', '--all')" 41 | assert 'self' not in Option('-a').__dict__ 42 | 43 | 44 | def test_corner_case_of_self_with_default(): 45 | Option = fixture(lambda self='', short='-a': None) 46 | assert repr(Option('-a')) == "Option()" 47 | assert repr(Option('-b')) == "Option('-b')" 48 | assert 'self' not in Option('-a').__dict__ 49 | 50 | 51 | def test_varargs_and_kwargs_are_not_allowed(): 52 | Option = fixture(lambda self, *args: None) 53 | with raises(ValueError): 54 | Option() 55 | Option = fixture(lambda self, **kwargs: None) 56 | with raises(ValueError): 57 | Option() 58 | 59 | def test_tuple_parameter_unpacking_is_not_allowed(): 60 | Option = fixture(lambda self, (one, two): None) 61 | with raises(ValueError): 62 | Option() 63 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Implementation of Value Object Pattern for Python 2 | ================================================= 3 | 4 | `From Wikipedia `_: 5 | 6 | A **value object** is a small object that represents a 7 | simple entity whose equality isn't based on identity: i.e. 8 | two value objects are equal when they have the same value, 9 | not necessarily being the same object. 10 | 11 | By default (if you subclass from ``object``) Python follows 12 | "reference semantics", i.e. two objects are equal if they 13 | are the same instance. ``Value`` class implements "value 14 | semantics", i.e. if you subclass it your objects will be 15 | equall if they hold the same data. 16 | 17 | This implementation will also inspect your ``__init__`` 18 | signature to automatically assign instance variables and 19 | produce a nice ``__repr__`` for your objects, dogether with 20 | a suitable ``__hash__`` implementation. 21 | 22 | Instead of asigning each instance variable manually: 23 | 24 | .. code:: python 25 | 26 | >>> class Date(object): 27 | ... 28 | ... def __init__(self, year, month=1, day=1): 29 | ... self.year = year 30 | ... self.month = month 31 | ... self.day = day 32 | 33 | ``Value`` defines ``__new__`` that will look at your 34 | ``__init__`` signature and assign instance variables based 35 | on it: 36 | 37 | .. code:: python 38 | 39 | >>> from value import Value 40 | ... 41 | >>> class Date(Value): 42 | ... 43 | ... def __init__(self, year, month=1, day=1): 44 | ... pass 45 | ... 46 | >>> Date(2013, 3).year == 2013 47 | True 48 | >>> Date(2013, 3).month == 3 49 | True 50 | >>> Date(2013, 3).day == 1 51 | True 52 | 53 | ``Value`` defines ``__eq__`` and ``__ne__`` to implement 54 | value object semantics, i.e. objects holding the same data 55 | are compared equal: 56 | 57 | .. code:: python 58 | 59 | >>> Date(2013, 3, 18) == Date(2013, 3, 18) 60 | True 61 | >>> Date(2013, 3, 18) != Date(1988) 62 | True 63 | 64 | ``Value`` also defines ``__repr__`` for you based on 65 | ``__init__`` signature: 66 | 67 | .. code:: python 68 | 69 | >>> repr(Date(2013, 3, 18)) 70 | 'Date(2013, 3, 18)' 71 | >>> repr(Date(1988, 1, 1)) 72 | 'Date(1988)' 73 | 74 | ``Value`` also defines ``__hash__`` for you, so that 75 | instances could be used in sets and as dictionary keys. 76 | 77 | Installation 78 | ------------------------------------------------------------ 79 | 80 | Use `pip `_ or easy_install:: 81 | 82 | pip install value==0.1.0 83 | 84 | Alternatively, you can just drop ``value.py`` file into your 85 | project—it is self-contained. 86 | 87 | - **value** is tested with Python 2.6, 2.7, 3.2, 3.3 and PyPy. 88 | - **value** follows `semantic versioning `_. 89 | --------------------------------------------------------------------------------