From 6c18cf940afc78e5fbe6ea179262da6ba756a0c2 Mon Sep 17 00:00:00 2001 From: team-python Date: Fri, 5 Feb 2016 17:07:05 +1100 Subject: [PATCH 1/4] update for relative imports --- jsonweb/__init__.py | 5 +- jsonweb/decode.py | 196 +++++++++++++++++++++--------------------- jsonweb/validators.py | 8 +- 3 files changed, 104 insertions(+), 105 deletions(-) diff --git a/jsonweb/__init__.py b/jsonweb/__init__.py index 76f1e3d..610aa2b 100644 --- a/jsonweb/__init__.py +++ b/jsonweb/__init__.py @@ -1,3 +1,2 @@ -from jsonweb.decode import loader, from_object -from jsonweb.encode import dumper, to_object - +from .decode import loader, from_object +from .encode import dumper, to_object diff --git a/jsonweb/decode.py b/jsonweb/decode.py index 8f77eee..7e7ef76 100644 --- a/jsonweb/decode.py +++ b/jsonweb/decode.py @@ -9,7 +9,7 @@ "last_name": "Adams" } ''') - + it would be pretty cool if instead of ``person`` being a :class:`dict` it was an instance of a class we defined called :class:`Person`. Luckily the python standard :mod:`json` module provides support for *class hinting* in the form @@ -49,11 +49,11 @@ import inspect import json from contextlib import contextmanager -from jsonweb.py3k import items +from .py3k import items -from jsonweb.validators import EnsureType -from jsonweb.exceptions import JsonWebError -from jsonweb._local import LocalStack +from .validators import EnsureType +from .exceptions import JsonWebError +from ._local import LocalStack _DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S" @@ -84,7 +84,7 @@ def __init__(self, obj_type, attr): ObjectDecodeError.__init__( self, "Missing {0} attribute for {1}.".format(attr, obj_type), - obj_type=obj_type, + obj_type=obj_type, attribute=attr ) @@ -102,18 +102,18 @@ class JsonWebObjectHandler(object): def __init__(self, args, kw_args=None): self.args = args self.kw_args = kw_args - + def __call__(self, cls, obj): cls_args = [] cls_kw_args = {} - + for arg in self.args: cls_args.append(obj[arg]) - + if self.kw_args: for key, default in self.kw_args: cls_kw_args[key] = obj.get(key, default) - + return cls(*cls_args, **cls_kw_args) @@ -121,38 +121,38 @@ class _ObjectHandlers(object): def __init__(self): self.__handlers = {} self.__deferred_updates = {} - + def add_handler(self, cls, handler, type_name=None, schema=None): name = type_name or cls.__name__ self.__handlers[name] = self.__merge_tuples( - self.__deferred_updates.get(name, (None,)*3), + self.__deferred_updates.get(name, (None,)*3), (handler, cls, schema) ) - + def get(self, name): """ Get a handler tuple. Return None if no such handler. """ return self.__handlers.get(name) - + def set(self, name, handler_tuple): """ Add a handler tuple (handler, cls, schema) """ self.__handlers[name] = handler_tuple - + def clear(self): self.__handlers = {} self.__deferred_updates = {} - + def update_handler(self, name, cls=None, handler=None, schema=None): """ - Modify cls, handler and schema for a decorated class. + Modify cls, handler and schema for a decorated class. """ handler_tuple = self.__handlers[name] - self.set(name, self.__merge_tuples((handler, cls, schema), + self.set(name, self.__merge_tuples((handler, cls, schema), handler_tuple)) - + def update_handler_deferred(self, name, cls=None, handler=None, schema=None): """ @@ -167,26 +167,26 @@ def update_handler_deferred(self, name, cls=None, d = self.__deferred_updates.get(name, (None,)*3) self.__deferred_updates[name] = self.__merge_tuples( (handler, cls, schema), d) - + def copy(self): handler_copy = _ObjectHandlers() [handler_copy.set(n, t) for n, t in self] return handler_copy - + def __merge_tuples(self, a_tuple, b_tuple): """ "Merge" two tuples of the same length. a takes precedence over b. - """ + """ if len(a_tuple) != len(b_tuple): raise ValueError("Iterators differ in length.") return tuple([(a or b) for a, b in zip(a_tuple, b_tuple)]) - + def __contains__(self, handler_name): return handler_name in self.__handlers - + def __getitem__(self, handler): return self.__handlers[handler] - + def __iter__(self): for name, handler_tuple in items(self.__handlers): yield name, handler_tuple @@ -202,9 +202,9 @@ class ObjectHook(object): def __init__(self, handlers, validate=True): self.handlers = handlers self.validate = validate - + def decode_obj(self, obj): - """ + """ This method is called for every dict decoded in a json string. The presence of the key ``__type__`` in ``obj`` will trigger a lookup in ``self.handlers``. If a handler is not found for ``__type__`` then an @@ -215,39 +215,39 @@ def decode_obj(self, obj): """ if "__type__" not in obj: return obj - + obj_type = obj["__type__"] try: factory, cls, schema = self.handlers[obj_type] except KeyError: raise ObjectNotFoundError(obj_type) - + if schema and self.validate: obj = schema().validate(obj) try: return factory(cls, obj) except KeyError as e: raise ObjectAttributeError(obj_type, e.args[0]) - + def get_arg_spec(func): arg_spec = inspect.getargspec(func) args = arg_spec.args - - try: + + try: if args[0] == "self": del args[0] except IndexError: pass - + if not args: - return None - + return None + kw_args = [] if arg_spec.defaults: for default in reversed(arg_spec.defaults): kw_args.append((args.pop(), default)) - + return args, kw_args @@ -267,7 +267,7 @@ def from_object(handler=None, type_name=None, schema=None): """ Decorating a class with :func:`from_object` will allow :func:`json.loads` to return instances of that class. - + ``handler`` is a callable that should return your class instance. It receives two arguments, your class and a python dict. Here is an example:: @@ -279,7 +279,7 @@ def from_object(handler=None, type_name=None, schema=None): ... obj["last_name"] ... ) ... - >>> @from_object(person_decoder) + >>> @from_object(person_decoder) ... class Person(object): ... def __init__(self, first_name, last_name): ... self.first_name @@ -291,37 +291,37 @@ def from_object(handler=None, type_name=None, schema=None): >>> person.first_name 'Shawn' - + The ``__type__`` key is very important. Without it :mod:`jsonweb` would not know which handler to delegate the python dict to. By default :func:`from_object` assumes ``__type__`` will be the class's ``__name__`` attribute. You can specify your own value by setting the ``type_name`` keyword argument :: - + @from_object(person_decoder, type_name="PersonObject") - + Which means the json string would need to be modified to look like this:: - + '{"__type__": "PersonObject", "first_name": "Shawn", "last_name": "Adams"}' - + If a handler cannot be found for ``__type__`` an exception is raised :: >>> luke = loader('{"__type__": "Jedi", "name": "Luke"}') Traceback (most recent call last): ... ObjectNotFoundError: Cannot decode object Jedi. No such object. - + You may have noticed that ``handler`` is optional. If you do not specify a ``handler`` :mod:`jsonweb` will attempt to generate one. It will inspect your class's ``__init__`` method. Any positional arguments will be considered required while keyword arguments will be optional. - + .. warning:: - + A handler cannot be generated from a method signature containing only ``*args`` and ``**kwargs``. The handler would not know which keys to pull out of the python dict. - + Lets look at a few examples:: >>> from jsonweb import from_object @@ -334,19 +334,19 @@ def from_object(handler=None, type_name=None, schema=None): >>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams", "gender": "male"}' >>> person = loader(person_json) - + What happens if we dont want to specify ``gender``:: - + >>> person_json = '''{ - ... "__type__": "Person", - ... "first_name": "Shawn", + ... "__type__": "Person", + ... "first_name": "Shawn", ... "last_name": "Adams" ... }''' >>> person = loader(person_json) Traceback (most recent call last): - ... + ... ObjectAttributeError: Missing gender attribute for Person. - + To make ``gender`` optional it must be a keyword argument:: >>> from jsonweb import from_object @@ -356,13 +356,13 @@ def from_object(handler=None, type_name=None, schema=None): ... self.first_name = first_name ... self.last_name = last_name ... self.gender = gender - + >>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}' >>> person = loader(person_json) >>> print person.gender None - - You can specify a json validator for a class with the ``schema`` keyword agrument. + + You can specify a json validator for a class with the ``schema`` keyword agrument. Here is a quick example:: >>> from jsonweb import from_object @@ -386,7 +386,7 @@ def from_object(handler=None, type_name=None, schema=None): ... except ValidationError, e: ... print e.errors["first_name"].message Expected str got int instead. - + Schemas are useful for validating user supplied json in web services or other web applications. For a detailed explanation on using schemas see the :mod:`jsonweb.schema`. @@ -408,7 +408,7 @@ def object_hook(handlers=None, as_type=None, validate=True): If you need to decode a JSON string that does not contain a ``__type__`` key and you know that the JSON represents a certain object or list of objects you can use ``as_type`` to specify it :: - + >>> json_str = '{"first_name": "bob", "last_name": "smith"}' >>> loader(json_str, as_type="Person") @@ -419,35 +419,35 @@ def object_hook(handlers=None, as_type=None, validate=True): ... ]''' >>> loader(json_str, as_type="Person") [, ] - + .. note:: - - Assumes every object WITHOUT a ``__type__`` kw is of + + Assumes every object WITHOUT a ``__type__`` kw is of the type specified by ``as_type`` . - + ``handlers`` is a dict with this format:: - + {"Person": {"cls": Person, "handler": person_decoder, "schema": PersonSchema)} - + If you do not wish to decorate your classes with :func:`from_object` you can specify the same parameters via the ``handlers`` keyword argument. Here is an example:: - + >>> class Person(object): ... def __init__(self, first_name, last_name): ... self.first_name = first_name ... self.last_name = last_name - ... + ... >>> def person_decoder(cls, obj): ... return cls(obj["first_name"], obj["last_name"]) - + >>> handlers = {"Person": {"cls": Person, "handler": person_decoder}} >>> person = loader(json_str, handlers=handlers) >>> # Or invoking the object_hook interface ourselves >>> person = json.loads(json_str, object_hook=object_hook(handlers)) - - .. note:: - + + .. note:: + If you decorate a class with :func:`from_object` you can override the ``handler`` and ``schema`` values later. Here is an example of overriding a schema you defined with :func:`from_object` (some code @@ -457,20 +457,20 @@ def object_hook(handlers=None, as_type=None, validate=True): >>> @from_object(schema=PersonSchema) >>> class Person(object): ... - + >>> # and later on in the code... >>> handlers = {"Person": {"schema": NewPersonSchema}} >>> person = loader(json_str, handlers=handlers) - + If you need to use ``as_type`` or ``handlers`` many times in your code you can forgo using :func:`loader` in favor of configuring a "custom" object hook callable. Here is an example :: - + >>> my_obj_hook = object_hook(handlers) >>> # this call uses custom handlers >>> person = json.loads(json_str, object_hook=my_obj_hook) >>> # and so does this one ... - >>> another_person = json.loads(json_str, object_hook=my_obj_hook) + >>> another_person = json.loads(json_str, object_hook=my_obj_hook) """ if handlers: _object_handlers = _default_object_handlers.copy() @@ -484,7 +484,7 @@ def object_hook(handlers=None, as_type=None, validate=True): ) else: _object_handlers = _default_object_handlers - + decode = ObjectHook(_object_handlers, validate) def handler(obj): @@ -500,32 +500,32 @@ def loader(json_str, **kw): Call this function as you would call :func:`json.loads`. It wraps the :ref:`object_hook` interface and returns python class instances from JSON strings. - + :param ensure_type: Check that the resulting object is of type - ``ensure_type``. Raise a ValidationError otherwise. - :param handlers: is a dict of handlers. see :func:`object_hook`. + ``ensure_type``. Raise a ValidationError otherwise. + :param handlers: is a dict of handlers. see :func:`object_hook`. :param as_type: explicitly specify the type of object the JSON - represents. see :func:`object_hook` + represents. see :func:`object_hook` :param validate: Set to False to turn off validation (ie dont run the - schemas) during this load operation. Defaults to True. + schemas) during this load operation. Defaults to True. :param kw: the rest of the kw args will be passed to the underlying :func:`json.loads` calls. - - + + """ kw["object_hook"] = object_hook( kw.pop("handlers", None), kw.pop("as_type", None), kw.pop("validate", True) ) - + ensure_type = kw.pop("ensure_type", _as_type_context.top) - + try: obj = json.loads(json_str, **kw) except ValueError as e: raise JsonDecodeError(e.args[0]) - + if ensure_type: return EnsureType(ensure_type).validate(obj) return obj @@ -538,24 +538,24 @@ def ensure_type(cls): :func:`loader` calls made in the active context. This will allow a :class:`~jsonweb.schema.ValidationError` to bubble up from the underlying :func:`loader` call if the resultant type is not of type ``ensure_type``. - + Here is an example :: - + # example_app.model.py from jsonweb.decode import from_object # import db model stuff from example_app import db - + @from_object() class Person(db.Base): first_name = db.Column(String) last_name = db.Column(String) - + def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name - + # example_app.__init__.py from example_app.model import session, Person from jsonweb.decode import from_object, ensure_type @@ -565,14 +565,14 @@ def __init__(self, first_name, last_name): app.errorhandler(ValidationError) def json_validation_error(e): return json_response({"error": e}) - - + + def load_request_json(): if request.headers.get('content-type') == 'application/json': return loader(request.data) abort(400) - - + + @app.route("/person", methods=["POST", "PUT"]) def add_person(): with ensure_type(Person): @@ -580,14 +580,14 @@ def add_person(): session.add(person) session.commit() return "ok" - - + + The above example is pretty contrived. We could have just made ``load_json_request`` - accept an ``ensure_type`` kw, but imagine if the call to :func:`loader` was burried + accept an ``ensure_type`` kw, but imagine if the call to :func:`loader` was burried deeper in our api and such a thing was not possible. """ _as_type_context.push(cls) try: yield None finally: - _as_type_context.pop() \ No newline at end of file + _as_type_context.pop() diff --git a/jsonweb/validators.py b/jsonweb/validators.py index ed3d844..e4e97c2 100644 --- a/jsonweb/validators.py +++ b/jsonweb/validators.py @@ -2,10 +2,10 @@ import inspect import re from datetime import datetime -from jsonweb import encode +from . import encode -from jsonweb.py3k import basestring, items -from jsonweb.exceptions import JsonWebError +from .py3k import basestring, items +from .exceptions import JsonWebError class _Errors(object): @@ -529,4 +529,4 @@ def isinstance_or_raise(obj, cls): def cls_name(obj): if inspect.isclass(obj): return obj.__name__ - return obj.__class__.__name__ \ No newline at end of file + return obj.__class__.__name__ From 2237ddd65c9b83355c7e0e60701d469dbdced483 Mon Sep 17 00:00:00 2001 From: team-python Date: Sat, 6 Feb 2016 15:41:29 +1100 Subject: [PATCH 2/4] compatibility with decoding 'dicts' as 'OrderedDicts'. Allow for 'object_pairs_hook' to be in call, sense this and adjust automatically. Previously object_pairs_hook would disable object decoding for __type__ objects completely. --- jsonweb/decode.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/jsonweb/decode.py b/jsonweb/decode.py index 7e7ef76..640a43a 100644 --- a/jsonweb/decode.py +++ b/jsonweb/decode.py @@ -199,9 +199,11 @@ class ObjectHook(object): directly. :func:`object_hook` is responsible for instantiating and using it. """ - def __init__(self, handlers, validate=True): + def __init__(self, handlers, validate=True,baseHook=None): self.handlers = handlers self.validate = validate + self.baseHook = baseHook + def decode_obj(self, obj): """ @@ -213,7 +215,14 @@ def decode_obj(self, obj): was supplied for the class, ``obj`` will first be validated then passed to handler. The handler should return a new python instant of type ``__type__``. """ - if "__type__" not in obj: + if isinstance(obj,list): + dobj=dict(obj) + if "__type__" not in dobj: + return self.baseHook(obj) + obj=dobj + elif "__type__" not in obj: + if self.baseHook: + return self.baseHook(obj) return obj obj_type = obj["__type__"] @@ -399,7 +408,8 @@ def wrapper(cls): return wrapper -def object_hook(handlers=None, as_type=None, validate=True): +def object_hook(handlers=None, as_type=None, validate=True + , baseHook=None): """ Wrapper around :class:`ObjectHook`. Calling this function will configure an instance of :class:`ObjectHook` and return a callable suitable for @@ -485,7 +495,7 @@ def object_hook(handlers=None, as_type=None, validate=True): else: _object_handlers = _default_object_handlers - decode = ObjectHook(_object_handlers, validate) + decode = ObjectHook(_object_handlers, validate, baseHook) def handler(obj): if as_type and "__type__" not in obj: @@ -513,10 +523,13 @@ def loader(json_str, **kw): """ - kw["object_hook"] = object_hook( + baseword="object_pairs_hook" + hookword= baseword if baseword in kw else "object_hook" + kw[hookword] = object_hook( kw.pop("handlers", None), kw.pop("as_type", None), - kw.pop("validate", True) + kw.pop("validate", True), + kw.pop(baseword, kw.pop("object_hook", None)) ) ensure_type = kw.pop("ensure_type", _as_type_context.top) From fefef82a08447b71fe3443b6266255ba7d426a53 Mon Sep 17 00:00:00 2001 From: team-python Date: Mon, 8 Feb 2016 12:43:31 +1100 Subject: [PATCH 3/4] updated noted style lapses - no functional change --- jsonweb/decode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonweb/decode.py b/jsonweb/decode.py index 640a43a..1f342b8 100644 --- a/jsonweb/decode.py +++ b/jsonweb/decode.py @@ -199,10 +199,10 @@ class ObjectHook(object): directly. :func:`object_hook` is responsible for instantiating and using it. """ - def __init__(self, handlers, validate=True,baseHook=None): + def __init__(self, handlers, validate=True, base_hook=None): self.handlers = handlers self.validate = validate - self.baseHook = baseHook + self.baseHook = base_hook def decode_obj(self, obj): From d90cea74f34662a4c660ec9da4563e9d778f90d6 Mon Sep 17 00:00:00 2001 From: team-python Date: Fri, 12 Feb 2016 14:53:01 +1100 Subject: [PATCH 4/4] update to allow descriptor pattern attributes --- jsonweb/encode.py | 157 ++++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 75 deletions(-) diff --git a/jsonweb/encode.py b/jsonweb/encode.py index 524489e..8ac4fd9 100644 --- a/jsonweb/encode.py +++ b/jsonweb/encode.py @@ -3,14 +3,14 @@ described by some sort of data model or resource in the form of a class object. This module provides an easy way to encode your python class instances to JSON. Here is a quick example:: - + >>> from jsonweb.encode import to_object, dumper >>> @to_object() ... class DataModel(object): ... def __init__(self, id, value): ... self.id = id ... self.value = value - + >>> data = DataModel(5, "foo") >>> dumper(data) '{"__type__": "DataModel", "id": 5, "value": "foo"}' @@ -38,7 +38,7 @@ def handler(func): encode handler. It will be called any time your class is serialized to a JSON string. :: - >>> from jsonweb import encode + >>> from jsonweb import encode >>> @encode.to_object() ... class Person(object): ... def __init__(self, first_name, last_name): @@ -46,13 +46,13 @@ def handler(func): ... self.last_name = last_name ... @encode.handler ... def to_obj(self): - ... return {"FirstName": person.first_name, + ... return {"FirstName": person.first_name, ... "LastName": person.last_name} ... >>> @encode.to_list() ... class People(object): ... def __init__(self, *persons): - ... self.persons = persons + ... self.persons = persons ... @encode.handler ... def to_list(self): ... return self.persons @@ -66,15 +66,15 @@ def handler(func): >>> print dumper(people, indent=2) [ { - "FirstName": "Luke", + "FirstName": "Luke", "LastName": "Skywalker" - }, + }, { - "FirstName": "Darth", + "FirstName": "Darth", "LastName": "Vader" - }, + }, { - "FirstName": "Obi-Wan", + "FirstName": "Obi-Wan", "LastName": "Kenobi" } ] @@ -91,10 +91,18 @@ def __inspect_for_handler(cls): for attr in dir(cls): if attr.startswith("_"): continue - obj = getattr(cls, attr) + if attr in cls.__dict__: + obj = cls.__dict__[attr] + # using getattr on descriptor pattern object prior to 'init' + # may break code break code + # and may invoke considerable code and even e.g. database access + # so avoid if possible + else: + # may still need to consider complex inherited descriptor patterns + obj = getattr(cls, attr) if hasattr(obj, "_jsonweb_encode_handler"): cls._encode.handler_is_instance_method = True - # we store the handler as a string name here. This is + # we store the handler as a string name here. This is # because obj is an unbound method. When its time to # encode the class instance we want to call the bound # instance method. @@ -110,7 +118,7 @@ def to_object(cls_type=None, suppress=None, handler=None, exclude_nulls=False): class instance to retrieve key/value pairs that will make up the JSON object (*Minus any attributes that start with an underscore or any attributes that were specified via the* ``suppress`` *keyword argument*). - + Here is an example:: >>> from jsonweb import to_object @@ -119,11 +127,11 @@ class instance to retrieve key/value pairs that will make up the JSON ... def __init__(self, first_name, last_name): ... self.first_name = first_name ... self.last_name = last_name - + >>> person = Person("Shawn", "Adams") >>> dumper(person) '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}' - + A ``__type__`` key is automatically added to the JSON object. Its value should represent the object type being encoded. By default it is set to the value of the decorated class's ``__name__`` attribute. You can @@ -135,11 +143,11 @@ class instance to retrieve key/value pairs that will make up the JSON ... def __init__(self, first_name, last_name): ... self.first_name = first_name ... self.last_name = last_name - + >>> person = Person("Shawn", "Adams") >>> dumper(person) '{"__type__": "PersonObject", "first_name": "Shawn", "last_name": "Adams"}' - + If you would like to leave some attributes out of the resulting JSON simply use the ``suppress`` kw argument to pass a list of attribute names:: @@ -150,16 +158,16 @@ class instance to retrieve key/value pairs that will make up the JSON ... def __init__(self, first_name, last_name): ... self.first_name = first_name ... self.last_name = last_name - + >>> person = Person("Shawn", "Adams") >>> dumper(person) - '{"__type__": "Person", "first_name": "Shawn"}' - + '{"__type__": "Person", "first_name": "Shawn"}' + You can even suppress the ``__type__`` attribute :: - + @to_object(suppress=["last_name", "__type__"]) ... - + Sometimes it's useful to suppress ``None`` values from your JSON output. Setting ``exclude_nulls`` to ``True`` will accomplish this :: @@ -169,27 +177,27 @@ class instance to retrieve key/value pairs that will make up the JSON ... def __init__(self, first_name, last_name): ... self.first_name = first_name ... self.last_name = last_name - + >>> person = Person("Shawn", None) >>> dumper(person) '{"__type__": "Person", "first_name": "Shawn"}' - - .. note:: - + + .. note:: + You can also pass most of these arguments to :func:`dumper`. They will take precedence over what you passed to :func:`to_object` and only effects that one call. - + If you need greater control over how your object is encoded you can specify a ``handler`` callable. It should accept one argument, which is the object to encode, and it should return a dict. This would override the default object handler :func:`JsonWebEncoder.object_handler`. - + Here is an example:: >>> from jsonweb import to_object >>> def person_encoder(person): - ... return {"FirstName": person.first_name, + ... return {"FirstName": person.first_name, ... "LastName": person.last_name} ... >>> @to_object(handler=person_encoder) @@ -198,15 +206,15 @@ class instance to retrieve key/value pairs that will make up the JSON ... self.guid = 12334 ... self.first_name = first_name ... self.last_name = last_name - + >>> person = Person("Shawn", "Adams") >>> dumper(person) '{"FirstName": "Shawn", "LastName": "Adams"}' - - + + You can also use the alternate decorator syntax to accomplish this. See :func:`jsonweb.encode.handler`. - + """ def wrapper(cls): cls._encode = EncodeArgs() @@ -214,7 +222,7 @@ def wrapper(cls): cls._encode.handler = handler cls._encode.suppress = suppress or [] cls._encode.exclude_nulls = exclude_nulls - cls._encode.__type__ = cls_type or cls.__name__ + cls._encode.__type__ = cls_type or cls.__name__ return __inspect_for_handler(cls) return wrapper @@ -225,49 +233,49 @@ def to_list(handler=None): with :func:`to_list`. By default The python built in :class:`list` will be called with your class instance as its argument. ie **list(obj)**. This means your class needs to define the ``__iter__`` method. - + Here is an example:: - + @to_object(suppress=["__type__"]) class Person(object): def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name - + @to_list() class People(object): def __init__(self, *persons): self.persons = persons - + def __iter__(self): for p in self.persons: yield p - + people = People( Person("Luke", "Skywalker"), Person("Darth", "Vader"), Person("Obi-Wan" "Kenobi") ) - + Encoding ``people`` produces this JSON:: - + [ - {"first_name": "Luke", "last_name": "Skywalker"}, - {"first_name": "Darth", "last_name": "Vader"}, + {"first_name": "Luke", "last_name": "Skywalker"}, + {"first_name": "Darth", "last_name": "Vader"}, {"first_name": "Obi-Wan", "last_name": "Kenobi"} - ] - + ] + .. versionadded:: 0.6.0 You can now specify a custom handler callable with the ``handler`` kw argument. It should accept one argument, your class instance. You can also use the :func:`jsonweb.encode.handler` decorator to mark one of the class's methods as the list handler. - + """ def wrapper(cls): cls._encode = EncodeArgs() cls._encode.serialize_as = "json_list" cls._encode.handler = handler - cls._encode.__type__ = cls.__name__ + cls._encode.__type__ = cls.__name__ return __inspect_for_handler(cls) return wrapper @@ -278,18 +286,18 @@ class JsonWebEncoder(json.JSONEncoder): instances of classes that have been decorated with :func:`to_object` or :func:`to_list`. Pass :class:`JsonWebEncoder` as the value for the ``cls`` keyword argument to :func:`json.dump` or :func:`json.dumps`. - + Example:: - + json.dumps(obj_instance, cls=JsonWebEncoder) - + Using :func:`dumper` is a shortcut for the above call to :func:`json.dumps` :: - + dumper(obj_instance) #much nicer! - + """ - + _DT_FORMAT = "%Y-%m-%dT%H:%M:%S" _D_FORMAT = "%Y-%m-%d" @@ -300,8 +308,8 @@ def __init__(self, **kw): if not isinstance(self.__hard_suppress, list): self.__hard_suppress = [self.__hard_suppress] json.JSONEncoder.__init__(self, **kw) - - def default(self, o): + + def default(self, o): try: e_args = getattr(o, "_encode") except AttributeError: @@ -318,29 +326,29 @@ def default(self, o): return self.object_handler(o) elif e_args.serialize_as == "json_list": return self.list_handler(o) - + if isinstance(o, datetime.datetime): return o.strftime(self._DT_FORMAT) if isinstance(o, datetime.date): return o.strftime(self._D_FORMAT) return json.JSONEncoder.default(self, o) - + def object_handler(self, obj): """ Handles encoding instance objects of classes decorated by :func:`to_object`. Returns a dict containing all the key/value pairs in ``obj.__dict__``. Excluding attributes that - + * start with an underscore. * were specified with the ``suppress`` keyword argument. - + The returned dict will be encoded into JSON. - + .. note:: - + Override this method if you wish to change how ALL objects are encoded into JSON objects. - + """ suppress = obj._encode.suppress if self.__exclude_nulls is not None: @@ -348,10 +356,10 @@ def object_handler(self, obj): else: exclude_nulls = obj._encode.exclude_nulls json_obj = {} - + def suppressed(key): - return key in suppress or key in self.__hard_suppress - + return key in suppress or key in self.__hard_suppress + for attr in dir(obj): if not attr.startswith("_") and not suppressed(attr): value = getattr(obj, attr) @@ -367,13 +375,13 @@ def list_handler(self, obj): """ Handles encoding instance objects of classes decorated by :func:`to_list`. Simply calls :class:`list` on ``obj``. - + .. note:: - + Override this method if you wish to change how ALL objects are encoded into JSON lists. - - """ + + """ return list(obj) @@ -383,17 +391,16 @@ def dumper(obj, **kw): call :func:`json.dumps`. ``kw`` args will be passed to the underlying json.dumps call. - :param handlers: A dict of type name/handler callable to use. + :param handlers: A dict of type name/handler callable to use. ie {"Person:" person_handler} - - :param cls: To override the given encoder. Should be a subclass + + :param cls: To override the given encoder. Should be a subclass of :class:`JsonWebEncoder`. - + :param suppress: A list of extra fields to suppress (as well as those suppressed by the class). - + :param exclude_nulls: Set True to suppress keys with null (None) values from the JSON output. Defaults to False. """ return json.dumps(obj, cls=kw.pop("cls", JsonWebEncoder), **kw) -