pax_global_header00006660000000000000000000000064136471432730014524gustar00rootroot0000000000000052 comment=86b6fb6aea3546fd86eb319bd06a82a655a94a97 itypes-1.2.0/000077500000000000000000000000001364714327300130415ustar00rootroot00000000000000itypes-1.2.0/.gitignore000066400000000000000000000001131364714327300150240ustar00rootroot00000000000000*.pyc *~ .* /*.egg-info/ /dist/ /env/ /htmlcov/ !.gitignore !.travis.yml itypes-1.2.0/.travis.yml000066400000000000000000000002231364714327300151470ustar00rootroot00000000000000language: python cache: pip python: - "2.7" - "3.4" - "3.5" - "3.6" install: - pip install -r requirements.txt script: - ./runtests itypes-1.2.0/LICENSE.md000066400000000000000000000027451364714327300144550ustar00rootroot00000000000000# License Copyright © 2017-present, Tom Christie. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. itypes-1.2.0/README.md000066400000000000000000000076621364714327300143330ustar00rootroot00000000000000# itypes [![Build Status](https://travis-ci.org/PavanTatikonda/itypes.svg?branch=master)](https://travis-ci.org/PavanTatikonda/itypes) Basic immutable container types for Python. A simple implementation that's designed for simplicity over performance. Use these in circumstances where it may result in more comprehensible code, or when you want to create custom types with restricted, immutable interfaces. For an alternative implementation designed for performance, please see [pyrsistent](https://github.com/tobgu/pyrsistent). ### Installation Install using `pip`: pip install itypes ### Instantiating dictionaries and lists. >>> import itypes >>> d = itypes.Dict({'a': 1, 'b': 2, 'c': 3}) >>> l = itypes.List(['a', 'b', 'c']) ### On instantiation, nested types are coerced to immutables. >>> d = itypes.Dict({'a': 123, 'b': ['a', 'b', 'c']}) >>> d['b'] List(['a', 'b', 'c']) ### Assignments and deletions return new copies. Methods: `set(key, value)`, `delete(key)` >>> d2 = d.set('c', 456) >>> d2 Dict({'a': 123, 'b': ['a', 'b', 'c'], 'c': 456}) >>> d3 = d2.delete('a') >>> d3 Dict({'b': ['a', 'b', 'c'], 'c': 456}) ### Standard assignments and deletions fail. >>> d['z'] = 123 TypeError: 'Dict' object doesn't support item assignment >>> del(d['c']) TypeError: 'Dict' object doesn't support item deletion ### Nested lookups. Method: `get_in(keys, default=None)` >>> d['b'][-1] 'c' >>> d['b'][5] IndexError: list index out of range >>> d.get_in(['b', -1]) 'c' >>> d.get_in(['b', 5]) None ### Nested assignments and deletions. Methods: `set_in(keys, value)`, `delete_in(keys)` >>> d2 = d.set_in(['b', 1], 'xxx') >>> d2 Dict({'a': 123, 'b': ['a', 'xxx', 'c']}) >>> d3 = d2.delete_in(['b', 0]) >>> d3 Dict({'a': 123, 'b': ['xxx', 'c']}) ### Equality works against standard types. >>> d = itypes.Dict({'a': 1, 'b': 2, 'c': 3}) >>> d == {'a': 1, 'b': 2, 'c': 3} True ### Objects are hashable. >>> hash(d) 277752239 ### Shortcuts for switching between mutable and immutable types. Functions: `to_mutable(instance)`, `to_immutable(value)` >>> value = itypes.to_mutable(d) >>> value {'a': 123, 'b': ['a', 'b', 'c']} >>> itypes.to_immutable(value) Dict({'a': 123, 'b': ['a', 'b', 'c']}) ### Subclassing. Only private attribute names may be set on instances. Use `@property` for attribute access. Define a `.clone(self, data)` method if objects have additional state. Example: class Configuration(itypes.Dict): def __init__(self, title, *args, **kwargs): self._title = title super(Configuration, self).__init__(*args, **kwargs) @property def title(self): return self._title def clone(self, data): return Configuration(self._title, data) Using the custom class: >>> config = Configuration('worker-process', {'hostname': 'example.com', 'dynos': 4}) >>> config.title 'worker-process' >>> new = config.set('dynos', 2) >>> new Configuration({'dynos': 2, 'hostname': 'example.com'}) >>> new.title 'worker-process' ### Custom immutable objects. Subclass `itypes.Object` for an object that prevents setting public attributes. >>> class Custom(itypes.Object): ... pass Only private attribute names may be set on instances. Use `@property` for attribute access. >>> class Document(itypes.Object): ... def __init__(self, title, content): ... self._title = title ... self._content = title ... @property ... def title(self): ... return self._title ... @property ... def content(self): ... return self._content Using immutable objects: >>> doc = Document(title='Immutability', content='For simplicity') >>> doc.title 'Immutability' >>> doc.title = 'Changed' TypeError: 'Document' object doesn't support property assignment. itypes-1.2.0/itypes.py000066400000000000000000000130271364714327300147330ustar00rootroot00000000000000# coding: utf-8 try: from collections.abc import Mapping, Sequence except ImportError: # support for python 2.x from collections import Mapping, Sequence __version__ = '1.2.0' def to_mutable(instance): if isinstance(instance, Dict): return { key: to_mutable(value) for key, value in instance.items() } elif isinstance(instance, List): return [ to_mutable(value) for value in instance ] return instance def to_immutable(value): if isinstance(value, dict): return Dict(value) elif isinstance(value, list): return List(value) return value def _to_hashable(instance): if isinstance(instance, Dict): items = sorted(instance.items(), key=lambda item: item[0]) return ( (key, _to_hashable(value)) for key, value in items ) elif isinstance(instance, List): return [ _to_hashable(value) for value in instance ] return instance def _set_in(node, keys, value): if not keys: return value elif len(keys) == 1: return node.set(keys[0], value) key = keys[0] child = node[key] if not isinstance(child, (Dict, List)): msg = "Expected a container type at key '%s', but got '%s'" raise KeyError(msg % type(child)) child = child.set_in(keys[1:], value) return node.set(key, child) def _delete_in(node, keys): if not keys: return elif len(keys) == 1: return node.delete(keys[0]) key = keys[0] child = node[key] if not isinstance(child, (Dict, List)): msg = "Expected a container type at key '%s', but got '%s'" raise KeyError(msg % type(child)) child = child.delete_in(keys[1:]) return node.set(key, child) def _get_in(node, keys, default=None): if not keys: return default key = keys[0] try: child = node[key] except (KeyError, IndexError): return default if len(keys) == 1: return child return child.get_in(keys[1:], default=default) class Object(object): def __setattr__(self, key, value): if key.startswith('_'): return object.__setattr__(self, key, value) msg = "'%s' object doesn't support property assignment." raise TypeError(msg % self.__class__.__name__) class Dict(Mapping): def __init__(self, *args, **kwargs): self._data = { key: to_immutable(value) for key, value in dict(*args, **kwargs).items() } def __setattr__(self, key, value): if key.startswith('_'): return object.__setattr__(self, key, value) msg = "'%s' object doesn't support property assignment." raise TypeError(msg % self.__class__.__name__) def __getitem__(self, key): return self._data[key] def __iter__(self): return iter(self._data) def __len__(self): return len(self._data) def __eq__(self, other): if isinstance(other, self.__class__): return self._data == other._data return self._data == other def __hash__(self): return hash(_to_hashable(self)) def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, to_mutable(self) ) def __str__(self): return str(self._data) def set(self, key, value): data = dict(self._data) data[key] = value if hasattr(self, 'clone'): return self.clone(data) return type(self)(data) def delete(self, key): data = dict(self._data) data.pop(key) if hasattr(self, 'clone'): return self.clone(data) return type(self)(data) def get_in(self, keys, default=None): return _get_in(self, keys, default=default) def set_in(self, keys, value): return _set_in(self, keys, value) def delete_in(self, keys): return _delete_in(self, keys) class List(Sequence): def __init__(self, *args): self._data = [ to_immutable(value) for value in list(*args) ] def __setattr__(self, key, value): if key == '_data': return object.__setattr__(self, key, value) msg = "'%s' object doesn't support property assignment." raise TypeError(msg % self.__class__.__name__) def __getitem__(self, key): return self._data[key] def __iter__(self): return iter(self._data) def __len__(self): return len(self._data) def __eq__(self, other): if isinstance(other, self.__class__): return self._data == other._data return self._data == other def __hash__(self): return hash(_to_hashable(self)) def __repr__(self): return "%s(%s)" % ( self.__class__.__name__, to_mutable(self) ) def __str__(self): return str(self._data) def set(self, key, value): data = list(self._data) data[key] = value if hasattr(self, 'clone'): return self.clone(data) return type(self)(data) def delete(self, key): data = list(self._data) data.pop(key) if hasattr(self, 'clone'): return self.clone(data) return type(self)(data) def get_in(self, keys, default=None): return _get_in(self, keys, default=default) def set_in(self, keys, value): return _set_in(self, keys, value) def delete_in(self, keys): return _delete_in(self, keys) itypes-1.2.0/requirements.txt000066400000000000000000000001051364714327300163210ustar00rootroot00000000000000# Testing requirements flake8 pytest # Packaging requirements wheel itypes-1.2.0/runtests000077500000000000000000000045211364714327300146600ustar00rootroot00000000000000#!/usr/bin/env python import os import pytest import subprocess import sys PYTEST_ARGS = ['tests.py', '--tb=short'] FLAKE8_ARGS = ['itypes.py', 'tests.py', '--ignore=E501'] COVERAGE_OPTIONS = { 'include': ['itypes.py', 'tests.py'], } sys.path.append(os.path.dirname(__file__)) class NullFile(object): def write(self, data): pass def exit_on_failure(ret, message=None): if ret: sys.exit(ret) def flake8_main(args): print('Running flake8 code linting') ret = subprocess.call(['flake8'] + args) print('flake8 failed' if ret else 'flake8 passed') return ret def fail_if_lacking_coverage(cov): precent_covered = cov.report( file=NullFile(), **COVERAGE_OPTIONS ) if precent_covered == 100: print('100% coverage') return print('Tests passed, but not 100% coverage.') cov.report(**COVERAGE_OPTIONS) cov.html_report(**COVERAGE_OPTIONS) sys.exit(1) def split_class_and_function(string): class_string, function_string = string.split('.', 1) return "%s and %s" % (class_string, function_string) def is_function(string): # `True` if it looks like a test function is included in the string. return string.startswith('test_') or '.test_' in string def is_class(string): # `True` if first character is uppercase - assume it's a class name. return string[0] == string[0].upper() if __name__ == "__main__": if len(sys.argv) > 1: pytest_args = sys.argv[1:] first_arg = pytest_args[0] if first_arg.startswith('-'): # `runtests.py [flags]` pytest_args = PYTEST_ARGS + pytest_args elif is_class(first_arg) and is_function(first_arg): # `runtests.py TestCase.test_function [flags]` expression = split_class_and_function(first_arg) pytest_args = PYTEST_ARGS + ['-k', expression] + pytest_args[1:] elif is_class(first_arg) or is_function(first_arg): # `runtests.py TestCase [flags]` # `runtests.py test_function [flags]` pytest_args = PYTEST_ARGS + ['-k', pytest_args[0]] + pytest_args[1:] else: pytest_args = PYTEST_ARGS # cov = coverage.coverage() # cov.start() exit_on_failure(pytest.main(pytest_args)) # cov.stop() exit_on_failure(flake8_main(FLAKE8_ARGS)) # fail_if_lacking_coverage(cov) itypes-1.2.0/setup.py000077500000000000000000000032321364714327300145560ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup import re import os import sys def get_version(): """ Return package version as listed in `__version__` in `init.py`. """ init_py = open('itypes.py').read() return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) version = get_version() if sys.argv[-1] == 'publish': os.system("python setup.py sdist upload") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") sys.exit() def read(fname): with open(fname) as fp: content = fp.read() return content setup( name='itypes', version=version, url='http://github.com/PavanTatikonda/itypes', license='BSD', description='Simple immutable types for python.', long_description=read('README.md'), long_description_content_type="text/markdown", author='Tom Christie', author_email='tom@tomchristie.com', py_modules=['itypes'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', ] ) itypes-1.2.0/tests.py000066400000000000000000000044431364714327300145620ustar00rootroot00000000000000import itypes import pytest # [], .get() def test_dict_get(): orig = itypes.Dict({'a': 1, 'b': 2, 'c': 3}) assert orig.get('a') == 1 assert orig.get('z') is None def test_dict_lookup(): orig = itypes.Dict({'a': 1, 'b': 2, 'c': 3}) assert orig['a'] == 1 with pytest.raises(KeyError): orig['zzz'] def test_list_lookup(): orig = itypes.List(['a', 'b', 'c']) assert orig[1] == 'b' with pytest.raises(IndexError): orig[999] # .delete(), .set() def test_dict_delete(): orig = itypes.Dict({'a': 1, 'b': 2, 'c': 3}) new = orig.delete('a') assert new == {'b': 2, 'c': 3} def test_dict_set(): orig = itypes.Dict({'a': 1, 'b': 2, 'c': 3}) new = orig.set('d', 4) assert new == {'a': 1, 'b': 2, 'c': 3, 'd': 4} def test_list_delete(): orig = itypes.List(['a', 'b', 'c']) new = orig.delete(1) assert new == ['a', 'c'] def test_list_set(): orig = itypes.List(['a', 'b', 'c']) new = orig.set(1, 'xxx') assert new == ['a', 'xxx', 'c'] # .get_in() def test_get_in(): orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) assert orig.get_in(['a', -1]) == 'z' assert orig.get_in(['dummy', -1]) is None assert orig.get_in(['a', 999]) is None # .delete_in(), .set_in() def test_delete_in(): orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) new = orig.delete_in(['a', 1]) assert new == {'a': ['x', 'z'], 'b': 2, 'c': 3} orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) new = orig.delete_in(['a']) assert new == {'b': 2, 'c': 3} orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) new = orig.delete_in([]) assert new is None def test_set_in(): orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) new = orig.set_in(['a', 1], 'yyy') assert new == {'a': ['x', 'yyy', 'z'], 'b': 2, 'c': 3} orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) new = orig.set_in(['a'], 'yyy') assert new == {'a': 'yyy', 'b': 2, 'c': 3} orig = itypes.Dict({'a': ['x', 'y', 'z'], 'b': 2, 'c': 3}) new = orig.set_in([], 'yyy') assert new == 'yyy' # Objects def test_setting_object_property(): class Example(itypes.Object): pass example = Example() with pytest.raises(TypeError): example.a = 123