Skip to content
Snippets Groups Projects
Commit 56244eae authored by Pavel Kácha's avatar Pavel Kácha
Browse files

Initial commit - split from IDEA repository, commit e341f9e836b6a8bca64704acdeb93e1ef40e810f

parents
No related branches found
No related tags found
No related merge requests found
!.gitignore
.directory
*.[oa]
*~
*.log
*.pem
*.cert
*.key
*.gpg
*.tar
*.gz
*.bz2
*.xz
*.tgz
*.tbz2
*.txz
*.deb
# Python related stuff.
*.pyc
*.egg-info
__pycache__
build
# Grunt automation tool related stuff
/node_modules/
LICENSE 0 → 100644
Copyright (c) 2016, CESNET, z. s. p. o.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
\ No newline at end of file
Makefile 0 → 100644
#-------------------------------------------------------------------------------
# Copyright (c) since 2016, CESNET, z. s. p. o.
# Authors: Pavel Kácha <pavel.kacha@cesnet.cz>
# Jan Mach <jan.mach@cesnet.cz>
# Use of this source is governed by an ISC license, see LICENSE file.
#-------------------------------------------------------------------------------
all: archive bdist deploy
build: archive bdist
help:
$(info List of possible make targets:)
$(info )
$(info * all: archive previous packages, build new distribution and deploy to PyPI [default])
$(info * build: archive previous packages and build new distribution)
$(info * test: run unit tests)
$(info * archive: archive previous packages)
$(info * bdist: build new distribution)
$(info * install: install distribution on local machine)
$(info * deploy: deploy to PyPI)
$(info )
# Perform unit tests
test: FORCE
$(info Testing source code)
nosetests
# Move old distribution files to archive directory
archive: FORCE
$(info Checking if dist archivation is needed)
@if ! [ `ls dist/typedcols* | wc -l` = "0" ]; then\
echo "Moving old distribution files to local archive";\
mv -f dist/typedcols* archive;\
fi
# Build various Python package distributions
bdist:
$(info Building distributions)
# Build and upload (insecure)
#python3 setup.py sdist bdist_wheel upload
# Build only
python3 setup.py sdist bdist_wheel --universal
# Perform installation from local files for both Python 2 and 3
install: FORCE
$(info Local installation)
pip install dist/typedcols*.whl
pip3 install dist/typedcols*.whl
# Deploy latest packages to PyPI
deploy: FORCE
$(info PyPI deployment)
# Secure upload with Twine
twine upload dist/typedcols*
# Empty rule as dependency wil force make to always perform target
# Source: https://www.gnu.org/software/make/manual/html_node/Force-Targets.html
FORCE:
typedcols
================================================================================
Python 2 and 3 compatible library providing typed collections.
This README file is work in progress, for more information please visit home page
at https://idea.cesnet.cz/en/index.
# Ignore everything in this directory
*
# Except this file
!.gitignore
# Ignore everything in this directory
*
# Except this file
!.gitignore
setup.py 0 → 100644
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Copyright (c) since 2016, CESNET, z. s. p. o.
# Authors: Pavel Kácha <pavel.kacha@cesnet.cz>
# Jan Mach <jan.mach@cesnet.cz>
# Use of this source is governed by an ISC license, see LICENSE file.
#-------------------------------------------------------------------------------
# Resources:
# https://packaging.python.org/en/latest/
# http://python-packaging-user-guide.readthedocs.io/distributing/
# Always prefer setuptools over distutils
from setuptools import setup, find_packages
# To use a consistent encoding
from codecs import open
from os import path
here = path.abspath(path.dirname(__file__))
# Get the long description from the README file
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()
setup(
name = 'typedcols',
version = '0.1.7',
description = 'Python library providing typed collections.',
long_description = long_description,
classifiers = [
'Development Status :: 4 - Beta',
'License :: OSI Approved :: ISC License (ISCL)',
'Programming Language :: Python',
],
keywords = 'library',
url = 'https://homeproj.cesnet.cz/git/idea.git',
author = 'Pavel Kacha',
author_email = 'pavel.kacha@cesnet.cz',
license = 'ISC',
py_modules = ['typedcols'],
test_suite = 'nose.collector',
tests_require = [
'nose'
],
zip_safe = True
)
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, CESNET, z. s. p. o.
# Use of this source is governed by an ISC license, see LICENSE file.
from typedcols import TypedDict, TypedList, KeyNotAllowed, KeysRequired, Discard, Any
from sys import version_info
import unittest
class AddressDict(TypedDict):
typedef = {
"street": {"type": str},
"num": {"type": int, "description": "Street number"},
"city": str,
"state": {"type": str, "required": True}
}
allow_unknown = True
def raise_discard(x=None):
raise Discard
class PersonDict(TypedDict):
typedef = {
"name": {"type": str, "default": "_Default_Value_"},
"age": int,
"address": {"type": AddressDict},
"tel": {"type": int, "required": True},
"note": {},
"discard1": Discard,
"discard2": {"type": Discard, "default": "asdf"},
"discard3": lambda x: Discard,
"discard4": raise_discard,
"discard_default1": {"type": Any, "default": Discard}, # Same as no default
"discard_default2": {"type": Any, "default": raise_discard}, # Same as no default
}
allow_unknown = False
# Monkeypatching for cheap Py 2 & 3 compatibility
if not hasattr(unittest.TestCase, "assertRaisesRegex"):
unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp
class TestTypedDict(unittest.TestCase):
def setUp(self):
self.person = PersonDict({
"age": "34",
"note": None,
"address": {
"street": "Vrchlikova",
"num": 12.3,
"city": "Kocourkov"
},
"discard1": "junk",
"discard2": "garbage",
"discard3": "rubbish",
"discard4": "scrap"
})
def testTypedefNormalization(self):
self.assertEqual(self.person.typedef["age"], {"type": int})
def testInit(self):
self.assertEqual(self.person, {
"name": "_Default_Value_",
"age": 34,
"note": None,
"address": {
"street": "Vrchlikova",
"num": 12,
"city": "Kocourkov"}})
def testSetGetKnownOk(self):
self.person["address"]["city"] = "Brno"
self.assertEqual(self.person["address"]["city"], "Brno")
def testSetKnownBadType(self):
with self.assertRaisesRegex(ValueError, r"\(\('num',\), \"invalid literal for int\(\) with base 10: 'WRONG'\", 'Street number'\)"):
self.person["address"]["num"] = "WRONG"
def testUpdate(self):
self.person.clear()
with self.assertRaisesRegex(ValueError, r"\(\('age',\), \"invalid literal for int\(\) with base 10: 'bla'\"\)"):
self.person.update({
"age": "bla"
})
def testUpdateNested(self):
self.person.clear()
with self.assertRaisesRegex(ValueError, r"\(\('address', 'num'\), \"invalid literal for int\(\) with base 10: 'asdf'\", 'Street number'\)"):
self.person.update({
"address": {
"num": "asdf"
}
})
def testRequired(self):
with self.assertRaises(KeysRequired):
self.person.checkRequired()
def testUnknown(self):
with self.assertRaisesRegex(KeyNotAllowed, r'unknown'):
self.person["unknown"] = "WRONG"
def testDel(self):
del self.person["address"]["city"]
with self.assertRaisesRegex(KeyError, r"'city'"):
self.person["address"]["city"]
def testDelDefault(self):
del self.person["name"]
self.assertEqual(self.person["name"], "_Default_Value_")
def testIter(self):
try:
it = self.person.iteritems()
except AttributeError:
it = self.person.items()
res = sorted([v for v in it])
self.assertEqual(res, [
("address", {"city": "Kocourkov", "street": "Vrchlikova", "num": 12}),
("age", 34),
("name", "_Default_Value_"),
("note", None)
])
class IntList(TypedList):
item_type = int
class TestTypedList(unittest.TestCase):
def setUp(self):
self.intlist = IntList((1, 2.1, "3"))
def testInit(self):
self.assertEqual(list(self.intlist), [1, 2, 3])
def testSetGetKnownOk(self):
self.intlist[2] = "4"
self.assertEqual(self.intlist[-1], 4)
def testSetKnownBadType(self):
with self.assertRaisesRegex(ValueError, r"invalid literal for int\(\) with base 10: 'WRONG'"):
self.intlist[2] = "WRONG"
def testDel(self):
del self.intlist[1]
self.assertEqual(list(self.intlist), [1, 3])
def testDelOutOfBounds(self):
with self.assertRaisesRegex(IndexError, r"list assignment index out of range"):
del self.intlist[3]
def testIter(self):
res = [val for val in self.intlist]
self.assertEqual(res, [1, 2, 3])
if __name__ == '__main__':
unittest.main()
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, CESNET, z. s. p. o.
# Use of this source is governed by an ISC license, see LICENSE file.
"""Simple typed collections library.
Defines TypedDict and TypedList, which enforce inserted types based on simple
type definition.
"""
__version__ = '0.1.7'
__author__ = 'Pavel Kácha <pavel.kacha@cesnet.cz>'
import collections
import abc
class KeyNotAllowed(LookupError):
""" Raised when untyped key is inserted on type, which does not allow
for untyped keys.
"""
class KeysRequired(LookupError):
""" Raised when required keys are missing in dictionary (usually on the
call of checkRequired method.
"""
class Discard(Exception):
""" Sentinel class to signal expected dropping of the key.
Can be returned or raised from type enforcing callable,
and can itself be used as type enforcing callable.
"""
@classmethod
def __call__(cls, x=None):
return cls
def Any(v):
return v
class TypedDictMetaclass(abc.ABCMeta):
""" Metaclass for TypedDict, allowing simplified typedefs - if typedef is not
dict, simple type object is assumed and correct dict is created.
Metaclassed to be run just once for the class, not for each instance.
"""
def dictifyTypedef(self, typedef):
for key in typedef:
tdef = typedef[key]
if not isinstance(tdef, collections.Mapping):
typedef[key] = {"type": tdef}
def __init__(cls, name, bases, dct):
super(TypedDictMetaclass, cls).__init__(name, bases, dct)
cls.dictifyTypedef(cls.typedef)
class TypedDict(collections.MutableMapping):
""" Dictionary type abstract class, which supports checking of inserted
types, based on simple type definition.
Must be subclassed, and subclass must populate 'typedef' dict, and may
also reassign allow_unknown and dict_class class attributes.
typedef: dictionary with keys and their type definitions. Type definition
may be simple callable (int, string, check_func,
AnotherTypedDict), or dict with the following members:
"type":
type enforcing callable. If callable returns, raises
or is Discard, key will be silently discarded
"default":
new TypedDict subclass will be initialized with keys
with this value; deleted keys will also revert to it
"required":
bool, checkRequired method will report the key if not present
"description":
string, explaining field type in human readable terms
(will be used in exception explanations)
Type enforcing callable must take one argument, and return value,
coerced to expected type. Coercion may even be conversion, for example
arbitrary date string, converted to DateTime.
allow_unknown: boolean, specifies whether dictionary allows unknown keys,
that means keys, which are not defined in 'typedef'
dict_class: class or factory for underlying dict implementation
"""
typedef = {}
allow_unknown = False
dict_class = dict
def __init__(self, init_data=None):
self.data = self.dict_class()
self.clear()
self.update(init_data or {})
def clear(self):
self.data.clear()
for key in self.typedef.keys():
self.initItemDefault(key)
def initItemDefault(self, key):
""" Sets 'key' to the default value from typedef (if defined) """
tdef = self.getTypedef(key)
try:
default = tdef["default"]
except KeyError:
pass
else:
if default is Discard:
return
try:
# Call if callable
default = default()
except Discard:
return
except TypeError:
pass
self[key] = default
def getTypedef(self, key):
""" Get type definition for 'key'.
If key is not defined and allow_unknown is True, empty
definition is returned, otherwise KeyNotAllowed gets raised.
"""
tdef = {}
try:
tdef = self.typedef[key]
except KeyError:
if not self.allow_unknown:
raise KeyNotAllowed(key)
return tdef
def checkRequired(self):
""" The class does not check missing items by itself (we need it to
be incomplete during creation and manipulation), so this checks
and return list of missing required keys explicitly.
Note that the check is not recursive (as instance dictionary
may contain another subclasses of TypedDict), so care must
be taken if there is such concern.
"""
missing = ()
for key, tdef in self.typedef.items():
if tdef.get("required", False) and not key in self.data:
missing = missing + (key,)
if missing:
raise KeysRequired(missing)
def __setitem__(self, key, value):
""" Setter with type coercion.
Any exception, raised from type enforcing callable, will get
modified - first .arg will be tuple of key hierarchy, last
.arg will be message from "description" field in type definition
"""
tdef = self.getTypedef(key)
valuetype = tdef.get("type", Any)
if valuetype is Discard:
return
try:
fvalue = valuetype(value)
except Discard:
return
except Exception as e:
if isinstance(e.args[0], tuple):
e.args = ((key,) + e.args[0],) + e.args[1:]
else:
e.args = ((key,),) + e.args
if len(e.args) < 3:
desc = tdef.get("description", None)
if desc is not None:
e.args = e.args + (desc,)
raise
if fvalue is Discard:
return
self.data[key] = fvalue
def __getitem__(self, key):
return self.data[key]
def __delitem__(self, key):
""" Deleter with reverting to defaults """
del self.data[key]
self.initItemDefault(key)
# Following definitions are not strictly necessary as MutableMapping
# already defines them, however we can override them by calling to
# possibly more optimized underlying implementations.
def __iter__(self): return iter(self.data)
def itervalues(self): return self.data.itervalues()
def iteritems(self): return self.data.iteritems()
def keys(self): return self.data.keys()
def values(self): return self.data.values()
def __len__(self): return len(self.data)
def __str__(self): return "%s(%s)" % (type(self).__name__, str(self.data))
def __repr__(self): return "%s(%s)" % (type(self).__name__, repr(self.data))
# Py 2 requires metaclassing by __metaclass__ attribute, whereas Py 3
# needs metaclass argument. What actually happens is the following,
# so we will do it explicitly, to be compatible with both versions.
TypedDict = TypedDictMetaclass("TypedDict", (TypedDict,), {})
class TypedList(collections.MutableSequence):
""" List type abstract class, which supports checking of inserted items
type.
Must be subclassed, and subclass must populate 'item_type' class
variable, and may also reassign list_class class attributes.
item_type: type enforcing callable, wich must take one argument, and
return value, coerced to expected type. Coercion may even be
conversion, for example arbitrary date string, converted to
DateTime. Because defined within class, Python authomatically
makes it object method, so it must be wrapped in staticmethod(...)
explicitly.
list_class: class or factory for underlying list implementation
"""
item_type = staticmethod(Any)
list_class = list
def __init__(self, iterable):
self.data = self.list_class()
self.extend(iterable)
def __getitem__(self, val): return self.data[val]
def __delitem__(self, val): del self.data[val]
def __len__(self): return len(self.data)
def __setitem__(self, idx, val):
tval = self.item_type(val)
self.data[idx] = tval
def insert(self, idx, val):
tval = self.item_type(val)
self.data.insert(idx, tval)
# Following definitions are not strictly necessary as MutableSequence
# already defines them, however we can override them by calling to
# possibly more optimized underlying implementations.
def __contains__(self, val):
tval = self.item_type(val)
return tval in self.data
def index(self, val):
tval = self.item_type(val)
return self.data.index(tval)
def count(self, val):
tval = self.item_type(val)
return self.data.count(tval)
def __iter__(self): return iter(self.data)
def reverse(self): return self.data.reverse()
def __reversed__(self): return reversed(self.data)
def pop(self, index=-1): return self.data.pop(index)
def __str__(self): return "%s(%s)" % (type(self).__name__, str(self.data))
def __repr__(self): return "%s(%s)" % (type(self).__name__, repr(self.data))
def typed_list(name, item_type):
""" Helper for oneshot type definition """
return type(name, (TypedList,), dict(item_type=staticmethod(item_type)))
def typed_dict(name, allow_unknown, typedef):
""" Helper for oneshot type definition """
return type(name, (TypedDict,), dict(allow_unknown=allow_unknown, typedef=typedef))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment