├── .gitignore ├── LICENSE ├── Makefile ├── README ├── phpserialize.py ├── setup.cfg ├── setup.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | .tox 5 | dist 6 | build 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python tests.py 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | a port of the serialize and unserialize functions of php to python. This module 2 | implements the python serialization interface (eg: provides dumps, loads and 3 | similar functions). 4 | -------------------------------------------------------------------------------- /phpserialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | r""" 3 | phpserialize 4 | ~~~~~~~~~~~~ 5 | 6 | a port of the ``serialize`` and ``unserialize`` functions of 7 | php to python. This module implements the python serialization 8 | interface (eg: provides `dumps`, `loads` and similar functions). 9 | 10 | Usage 11 | ===== 12 | 13 | >>> from phpserialize import * 14 | >>> obj = dumps("Hello World") 15 | >>> loads(obj) 16 | 'Hello World' 17 | 18 | Due to the fact that PHP doesn't know the concept of lists, lists 19 | are serialized like hash-maps in PHP. As a matter of fact the 20 | reverse value of a serialized list is a dict: 21 | 22 | >>> loads(dumps(range(2))) 23 | {0: 0, 1: 1} 24 | 25 | If you want to have a list again, you can use the `dict_to_list` 26 | helper function: 27 | 28 | >>> dict_to_list(loads(dumps(range(2)))) 29 | [0, 1] 30 | 31 | It's also possible to convert into a tuple by using the `dict_to_tuple` 32 | function: 33 | 34 | >>> dict_to_tuple(loads(dumps((1, 2, 3)))) 35 | (1, 2, 3) 36 | 37 | Another problem are unicode strings. By default unicode strings are 38 | encoded to 'utf-8' but not decoded on `unserialize`. The reason for 39 | this is that phpserialize can't guess if you have binary or text data 40 | in the strings: 41 | 42 | >>> loads(dumps(u'Hello W\xf6rld')) 43 | 'Hello W\xc3\xb6rld' 44 | 45 | If you know that you have only text data of a known charset in the result 46 | you can decode strings by setting `decode_strings` to True when calling 47 | loads: 48 | 49 | >>> loads(dumps(u'Hello W\xf6rld'), decode_strings=True) 50 | u'Hello W\xf6rld' 51 | 52 | Dictionary keys are limited to strings and integers. `None` is converted 53 | into an empty string and floats and booleans into integers for PHP 54 | compatibility: 55 | 56 | >>> loads(dumps({None: 14, 42.23: 'foo', True: [1, 2, 3]})) 57 | {'': 14, 1: {0: 1, 1: 2, 2: 3}, 42: 'foo'} 58 | 59 | It also provides functions to read from file-like objects: 60 | 61 | >>> from StringIO import StringIO 62 | >>> stream = StringIO('a:2:{i:0;i:1;i:1;i:2;}') 63 | >>> dict_to_list(load(stream)) 64 | [1, 2] 65 | 66 | And to write to those: 67 | 68 | >>> stream = StringIO() 69 | >>> dump([1, 2], stream) 70 | >>> stream.getvalue() 71 | 'a:2:{i:0;i:1;i:1;i:2;}' 72 | 73 | Like `pickle` chaining of objects is supported: 74 | 75 | >>> stream = StringIO() 76 | >>> dump([1, 2], stream) 77 | >>> dump("foo", stream) 78 | >>> stream.seek(0) 79 | >>> load(stream) 80 | {0: 1, 1: 2} 81 | >>> load(stream) 82 | 'foo' 83 | 84 | This feature however is not supported in PHP. PHP will only unserialize 85 | the first object. 86 | 87 | Array Serialization 88 | =================== 89 | 90 | Starting with 1.2 you can provide an array hook to the unserialization 91 | functions that are invoked with a list of pairs to return a real array 92 | object. By default `dict` is used as array object which however means 93 | that the information about the order is lost for associative arrays. 94 | 95 | For example you can pass the ordered dictionary to the unserilization 96 | functions: 97 | 98 | >>> from collections import OrderedDict 99 | >>> loads('a:2:{s:3:"foo";i:1;s:3:"bar";i:2;}', 100 | ... array_hook=OrderedDict) 101 | collections.OrderedDict([('foo', 1), ('bar', 2)]) 102 | 103 | Object Serialization 104 | ==================== 105 | 106 | PHP supports serialization of objects. Starting with 1.2 of phpserialize 107 | it is possible to both serialize and unserialize objects. Because class 108 | names in PHP and Python usually do not map, there is a separate 109 | `object_hook` parameter that is responsible for creating these classes. 110 | 111 | For a simple test example the `phpserialize.phpobject` class can be used: 112 | 113 | >>> data = 'O:7:"WP_User":1:{s:8:"username";s:5:"admin";}' 114 | >>> user = loads(data, object_hook=phpobject) 115 | >>> user.username 116 | 'admin' 117 | >>> user.__name__ 118 | 'WP_User' 119 | 120 | An object hook is a function that takes the name of the class and a dict 121 | with the instance data as arguments. The instance data keys are in PHP 122 | format which usually is not what you want. To convert it into Python 123 | identifiers you can use the `convert_member_dict` function. For more 124 | information about that, have a look at the next section. Here an 125 | example for a simple object hook: 126 | 127 | >>> class User(object): 128 | ... def __init__(self, username): 129 | ... self.username = username 130 | ... 131 | >>> def object_hook(name, d): 132 | ... cls = {'WP_User': User}[name] 133 | ... return cls(**d) 134 | ... 135 | >>> user = loads(data, object_hook=object_hook) 136 | >>> user.username 137 | 'admin' 138 | 139 | To serialize objects you can use the `object_hook` of the dump functions 140 | and return instances of `phpobject`: 141 | 142 | >>> def object_hook(obj): 143 | ... if isinstance(obj, User): 144 | ... return phpobject('WP_User', {'username': obj.username}) 145 | ... raise LookupError('unknown object') 146 | ... 147 | >>> dumps(user, object_hook=object_hook) 148 | 'O:7:"WP_User":1:{s:8:"username";s:5:"admin";}' 149 | 150 | PHP's Object System 151 | =================== 152 | 153 | The PHP object system is derived from compiled languages such as Java 154 | and C#. Attributes can be protected from external access by setting 155 | them to `protected` or `private`. This does not only serve the purpose 156 | to encapsulate internals but also to avoid name clashes. 157 | 158 | In PHP each class in the inheritance chain can have a private variable 159 | with the same name, without causing clashes. (This is similar to the 160 | Python `__var` name mangling system). 161 | 162 | This PHP class:: 163 | 164 | class WP_UserBase { 165 | protected $username; 166 | 167 | public function __construct($username) { 168 | $this->username = $username; 169 | } 170 | } 171 | 172 | class WP_User extends WP_UserBase { 173 | private $password; 174 | public $flag; 175 | 176 | public function __construct($username, $password) { 177 | parent::__construct($username); 178 | $this->password = $password; 179 | $this->flag = 0; 180 | } 181 | } 182 | 183 | Is serialized with a member data dict that looks like this: 184 | 185 | >>> data = { 186 | ... ' * username': 'the username', 187 | ... ' WP_User password': 'the password', 188 | ... 'flag': 'the flag' 189 | ... } 190 | 191 | Because this access system does not exist in Python, the 192 | `convert_member_dict` can convert this dict: 193 | 194 | >>> d = convert_member_dict(data) 195 | >>> d['username'] 196 | 'the username' 197 | >>> d['password'] 198 | 'the password' 199 | 200 | The `phpobject` class does this conversion on the fly. What is 201 | serialized is the special `__php_vars__` dict of the class: 202 | 203 | >>> user = phpobject('WP_User', data) 204 | >>> user.username 205 | 'the username' 206 | >>> user.username = 'admin' 207 | >>> user.__php_vars__[' * username'] 208 | 'admin' 209 | 210 | As you can see, reassigning attributes on a php object will try 211 | to change a private or protected attribute with the same name. 212 | Setting an unknown one will create a new public attribute: 213 | 214 | >>> user.is_admin = True 215 | >>> user.__php_vars__['is_admin'] 216 | True 217 | 218 | To convert the phpobject into a dict, you can use the `_asdict` 219 | method: 220 | 221 | >>> d = user._asdict() 222 | >>> d['username'] 223 | 'admin' 224 | 225 | Python 3 Notes 226 | ============== 227 | 228 | Because the unicode support in Python 3 no longer transparently 229 | handles bytes and unicode objects we had to change the way the 230 | decoding works. On Python 3 you most likely want to always 231 | decode strings. Because this would totally fail on binary data 232 | phpserialize uses the "surrogateescape" method to not fail on 233 | invalid data. See the documentation in Python 3 for more 234 | information. 235 | 236 | Changelog 237 | ========= 238 | 239 | 1.3 240 | - added support for Python 3 241 | 242 | 1.2 243 | - added support for object serialization 244 | - added support for array hooks 245 | 246 | 1.1 247 | - added `dict_to_list` and `dict_to_tuple` 248 | - added support for unicode 249 | - allowed chaining of objects like pickle does 250 | 251 | 252 | :copyright: 2007-2012 by Armin Ronacher. 253 | license: BSD 254 | """ 255 | import codecs 256 | try: 257 | codecs.lookup_error('surrogateescape') 258 | default_errors = 'surrogateescape' 259 | except LookupError: 260 | default_errors = 'strict' 261 | 262 | try: 263 | from StringIO import StringIO as BytesIO 264 | except ImportError: 265 | from io import BytesIO as BytesIO 266 | 267 | try: 268 | unicode 269 | except NameError: 270 | # Python 3 271 | unicode = str 272 | basestring = (bytes, str) 273 | 274 | try: 275 | long 276 | except NameError: 277 | # Python 3 278 | long = int 279 | 280 | try: 281 | xrange 282 | except NameError: 283 | xrange = range 284 | 285 | __author__ = 'Armin Ronacher ' 286 | __version__ = '1.3' 287 | __all__ = ('phpobject', 'convert_member_dict', 'dict_to_list', 'dict_to_tuple', 288 | 'load', 'loads', 'dump', 'dumps', 'serialize', 'unserialize') 289 | 290 | 291 | def _translate_member_name(name): 292 | if name[:1] == ' ': 293 | name = name.split(None, 2)[-1] 294 | return name 295 | 296 | 297 | class phpobject(object): 298 | """Simple representation for PHP objects. This is used """ 299 | __slots__ = ('__name__', '__php_vars__') 300 | 301 | def __init__(self, name, d=None): 302 | if d is None: 303 | d = {} 304 | object.__setattr__(self, '__name__', name) 305 | object.__setattr__(self, '__php_vars__', d) 306 | 307 | def _asdict(self): 308 | """Returns a new dictionary from the data with Python identifiers.""" 309 | return convert_member_dict(self.__php_vars__) 310 | 311 | def _lookup_php_var(self, name): 312 | for key, value in self.__php_vars__.items(): 313 | if _translate_member_name(key) == name: 314 | return key, value 315 | 316 | def __getattr__(self, name): 317 | rv = self._lookup_php_var(name) 318 | if rv is not None: 319 | return rv[1] 320 | raise AttributeError(name) 321 | 322 | def __setattr__(self, name, value): 323 | rv = self._lookup_php_var(name) 324 | if rv is not None: 325 | name = rv[0] 326 | self.__php_vars__[name] = value 327 | 328 | def __repr__(self): 329 | return '' % (self.__name__,) 330 | 331 | 332 | def convert_member_dict(d): 333 | """Converts the names of a member dict to Python syntax. PHP class data 334 | member names are not the plain identifiers but might be prefixed by the 335 | class name if private or a star if protected. This function converts them 336 | into standard Python identifiers: 337 | 338 | >>> convert_member_dict({"username": "user1", " User password": 339 | ... "default", " * is_active": True}) 340 | {'username': 'user1', 'password': 'default', 'is_active': True} 341 | """ 342 | return dict((_translate_member_name(k), v) for k, v in d.items()) 343 | 344 | 345 | def dumps(data, charset='utf-8', errors=default_errors, object_hook=None): 346 | """Return the PHP-serialized representation of the object as a string, 347 | instead of writing it to a file like `dump` does. On Python 3 348 | this returns bytes objects, on Python 3 this returns bytestrings. 349 | """ 350 | def _serialize(obj, keypos): 351 | if keypos: 352 | if isinstance(obj, (int, long, float, bool)): 353 | return ('i:%i;' % obj).encode('latin1') 354 | if isinstance(obj, basestring): 355 | encoded_obj = obj 356 | if isinstance(obj, unicode): 357 | encoded_obj = obj.encode(charset, errors) 358 | s = BytesIO() 359 | s.write(b's:') 360 | s.write(str(len(encoded_obj)).encode('latin1')) 361 | s.write(b':"') 362 | s.write(encoded_obj) 363 | s.write(b'";') 364 | return s.getvalue() 365 | if obj is None: 366 | return b's:0:"";' 367 | raise TypeError('can\'t serialize %r as key' % type(obj)) 368 | else: 369 | if obj is None: 370 | return b'N;' 371 | if isinstance(obj, bool): 372 | return ('b:%i;' % obj).encode('latin1') 373 | if isinstance(obj, (int, long)): 374 | return ('i:%s;' % obj).encode('latin1') 375 | if isinstance(obj, float): 376 | return ('d:%s;' % obj).encode('latin1') 377 | if isinstance(obj, basestring): 378 | encoded_obj = obj 379 | if isinstance(obj, unicode): 380 | encoded_obj = obj.encode(charset, errors) 381 | s = BytesIO() 382 | s.write(b's:') 383 | s.write(str(len(encoded_obj)).encode('latin1')) 384 | s.write(b':"') 385 | s.write(encoded_obj) 386 | s.write(b'";') 387 | return s.getvalue() 388 | if isinstance(obj, (list, tuple, dict)): 389 | out = [] 390 | if isinstance(obj, dict): 391 | iterable = obj.items() 392 | else: 393 | iterable = enumerate(obj) 394 | for key, value in iterable: 395 | out.append(_serialize(key, True)) 396 | out.append(_serialize(value, False)) 397 | return b''.join([ 398 | b'a:', 399 | str(len(obj)).encode('latin1'), 400 | b':{', 401 | b''.join(out), 402 | b'}' 403 | ]) 404 | if isinstance(obj, phpobject): 405 | return b'O' + _serialize(obj.__name__, True)[1:-1] + \ 406 | _serialize(obj.__php_vars__, False)[1:] 407 | if object_hook is not None: 408 | return _serialize(object_hook(obj), False) 409 | raise TypeError('can\'t serialize %r' % type(obj)) 410 | 411 | return _serialize(data, False) 412 | 413 | 414 | def load(fp, charset='utf-8', errors=default_errors, decode_strings=False, 415 | object_hook=None, array_hook=None): 416 | """Read a string from the open file object `fp` and interpret it as a 417 | data stream of PHP-serialized objects, reconstructing and returning 418 | the original object hierarchy. 419 | 420 | `fp` must provide a `read()` method that takes an integer argument. Both 421 | method should return strings. Thus `fp` can be a file object opened for 422 | reading, a `StringIO` object (`BytesIO` on Python 3), or any other custom 423 | object that meets this interface. 424 | 425 | `load` will read exactly one object from the stream. See the docstring of 426 | the module for this chained behavior. 427 | 428 | If an object hook is given object-opcodes are supported in the serilization 429 | format. The function is called with the class name and a dict of the 430 | class data members. The data member names are in PHP format which is 431 | usually not what you want. The `simple_object_hook` function can convert 432 | them to Python identifier names. 433 | 434 | If an `array_hook` is given that function is called with a list of pairs 435 | for all array items. This can for example be set to 436 | `collections.OrderedDict` for an ordered, hashed dictionary. 437 | """ 438 | if array_hook is None: 439 | array_hook = dict 440 | 441 | def _expect(e): 442 | v = fp.read(len(e)) 443 | if v != e: 444 | raise ValueError('failed expectation, expected %r got %r' % (e, v)) 445 | 446 | def _read_until(delim): 447 | buf = [] 448 | while 1: 449 | char = fp.read(1) 450 | if char == delim: 451 | break 452 | elif not char: 453 | raise ValueError('unexpected end of stream') 454 | buf.append(char) 455 | return b''.join(buf) 456 | 457 | def _load_array(): 458 | items = int(_read_until(b':')) * 2 459 | _expect(b'{') 460 | result = [] 461 | last_item = Ellipsis 462 | for idx in xrange(items): 463 | item = _unserialize() 464 | if last_item is Ellipsis: 465 | last_item = item 466 | else: 467 | result.append((last_item, item)) 468 | last_item = Ellipsis 469 | _expect(b'}') 470 | return result 471 | 472 | def _unserialize(): 473 | type_ = fp.read(1).lower() 474 | if type_ == b'n': 475 | _expect(b';') 476 | return None 477 | if type_ in b'idb': 478 | _expect(b':') 479 | data = _read_until(b';') 480 | if type_ == b'i': 481 | return int(data) 482 | if type_ == b'd': 483 | return float(data) 484 | return int(data) != 0 485 | if type_ == b's': 486 | _expect(b':') 487 | length = int(_read_until(b':')) 488 | _expect(b'"') 489 | data = fp.read(length) 490 | _expect(b'"') 491 | if decode_strings: 492 | data = data.decode(charset, errors) 493 | _expect(b';') 494 | return data 495 | if type_ == b'a': 496 | _expect(b':') 497 | return array_hook(_load_array()) 498 | if type_ == b'o': 499 | if object_hook is None: 500 | raise ValueError('object in serialization dump but ' 501 | 'object_hook not given.') 502 | _expect(b':') 503 | name_length = int(_read_until(b':')) 504 | _expect(b'"') 505 | name = fp.read(name_length) 506 | _expect(b'":') 507 | if decode_strings: 508 | name = name.decode(charset, errors) 509 | return object_hook(name, dict(_load_array())) 510 | raise ValueError('unexpected opcode') 511 | 512 | return _unserialize() 513 | 514 | 515 | def loads(data, charset='utf-8', errors=default_errors, decode_strings=False, 516 | object_hook=None, array_hook=None): 517 | """Read a PHP-serialized object hierarchy from a string. Characters in the 518 | string past the object's representation are ignored. On Python 3 the 519 | string must be a bytestring. 520 | """ 521 | return load(BytesIO(data), charset, errors, decode_strings, 522 | object_hook, array_hook) 523 | 524 | 525 | def dump(data, fp, charset='utf-8', errors=default_errors, object_hook=None): 526 | """Write a PHP-serialized representation of obj to the open file object 527 | `fp`. Unicode strings are encoded to `charset` with the error handling 528 | of `errors`. 529 | 530 | `fp` must have a `write()` method that accepts a single string argument. 531 | It can thus be a file object opened for writing, a `StringIO` object 532 | (or a `BytesIO` object on Python 3), or any other custom object that meets 533 | this interface. 534 | 535 | The `object_hook` is called for each unknown object and has to either 536 | raise an exception if it's unable to convert the object or return a 537 | value that is serializable (such as a `phpobject`). 538 | """ 539 | fp.write(dumps(data, charset, errors, object_hook)) 540 | 541 | 542 | def dict_to_list(d): 543 | """Converts an ordered dict into a list.""" 544 | # make sure it's a dict, that way dict_to_list can be used as an 545 | # array_hook. 546 | d = dict(d) 547 | try: 548 | return [d[x] for x in xrange(len(d))] 549 | except KeyError: 550 | raise ValueError('dict is not a sequence') 551 | 552 | 553 | def dict_to_tuple(d): 554 | """Converts an ordered dict into a tuple.""" 555 | return tuple(dict_to_list(d)) 556 | 557 | 558 | serialize = dumps 559 | unserialize = loads 560 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_date = 0 4 | tag_svn_revision = 0 5 | 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | def get_docs(): 5 | result = [] 6 | in_docs = False 7 | f = open(os.path.join(os.path.dirname(__file__), 'phpserialize.py')) 8 | try: 9 | for line in f: 10 | if in_docs: 11 | if line.lstrip().startswith(':copyright:'): 12 | break 13 | result.append(line[4:].rstrip()) 14 | elif line.strip() == 'r"""': 15 | in_docs = True 16 | finally: 17 | f.close() 18 | return '\n'.join(result) 19 | 20 | setup( 21 | name='phpserialize', 22 | author='Armin Ronacher', 23 | author_email='armin.ronacher@active-4.com', 24 | version='1.3', 25 | url='http://github.com/mitsuhiko/phpserialize', 26 | py_modules=['phpserialize'], 27 | description='a port of the serialize and unserialize ' 28 | 'functions of php to python.', 29 | long_description=get_docs(), 30 | zip_safe=False, 31 | test_suite='tests', 32 | classifiers=[ 33 | 'License :: OSI Approved :: BSD License', 34 | 'Programming Language :: PHP', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 3' 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import phpserialize 4 | 5 | 6 | class PhpSerializeTestCase(unittest.TestCase): 7 | 8 | def test_dumps_int(self): 9 | self.assertEqual(phpserialize.dumps(5), b'i:5;') 10 | 11 | def test_dumps_float(self): 12 | self.assertEqual(phpserialize.dumps(5.6), b'd:5.6;') 13 | 14 | def test_dumps_str(self): 15 | self.assertEqual(phpserialize.dumps('Hello world'), 16 | b's:11:"Hello world";') 17 | 18 | def test_dumps_unicode(self): 19 | self.assertEqual(phpserialize.dumps('Björk Guðmundsdóttir'), 20 | b's:23:"Bj\xc3\xb6rk Gu\xc3\xb0mundsd\xc3\xb3ttir";') 21 | 22 | def test_dumps_binary(self): 23 | self.assertEqual(phpserialize.dumps(b'\001\002\003'), 24 | b's:3:"\x01\x02\x03";') 25 | 26 | def test_dumps_list(self): 27 | self.assertEqual(phpserialize.dumps([7, 8, 9]), 28 | b'a:3:{i:0;i:7;i:1;i:8;i:2;i:9;}') 29 | 30 | def test_dumps_tuple(self): 31 | self.assertEqual(phpserialize.dumps((7, 8, 9)), 32 | b'a:3:{i:0;i:7;i:1;i:8;i:2;i:9;}') 33 | 34 | def test_dumps_dict(self): 35 | self.assertEqual(phpserialize.dumps({'a': 1, 'b': 2, 'c': 3}), 36 | b'a:3:{s:1:"a";i:1;s:1:"c";i:3;s:1:"b";i:2;}') 37 | 38 | def test_loads_dict(self): 39 | self.assertEqual(phpserialize.loads(b'a:3:{s:1:"a";i:1;s:1:"c";i:3;s:1:"b";i:2;}', 40 | decode_strings=True), {'a': 1, 'b': 2, 'c': 3}) 41 | 42 | def test_loads_unicode(self): 43 | self.assertEqual(phpserialize.loads(b's:23:"Bj\xc3\xb6rk Gu\xc3\xb0mundsd\xc3\xb3ttir";', 44 | decode_strings=True), b'Bj\xc3\xb6rk Gu\xc3\xb0mundsd\xc3\xb3ttir'.decode('utf-8')) 45 | 46 | def test_loads_binary(self): 47 | self.assertEqual(phpserialize.loads(b's:3:"\001\002\003";', decode_strings=False), 48 | b'\001\002\003') 49 | 50 | def test_dumps_and_loads_dict(self): 51 | self.assertEqual(phpserialize.loads(phpserialize.dumps({'a': 1, 'b': 2, 'c': 3}), 52 | decode_strings=True), {'a': 1, 'b': 2, 'c': 3}) 53 | 54 | def test_list_roundtrips(self): 55 | x = phpserialize.loads(phpserialize.dumps(list(range(2)))) 56 | self.assertEqual(x, {0: 0, 1: 1}) 57 | y = phpserialize.dict_to_list(x) 58 | self.assertEqual(y, [0, 1]) 59 | 60 | def test_tuple_roundtrips(self): 61 | x = phpserialize.loads(phpserialize.dumps(list(range(2)))) 62 | self.assertEqual(x, {0: 0, 1: 1}) 63 | y = phpserialize.dict_to_tuple(x) 64 | self.assertEqual(y, (0, 1)) 65 | 66 | def test_fileio_support_with_chaining_and_all(self): 67 | f = phpserialize.BytesIO() 68 | phpserialize.dump([1, 2], f) 69 | phpserialize.dump(42, f) 70 | f = phpserialize.BytesIO(f.getvalue()) 71 | self.assertEqual(phpserialize.load(f), {0: 1, 1: 2}) 72 | self.assertEqual(phpserialize.load(f), 42) 73 | 74 | def test_object_hook(self): 75 | class User(object): 76 | def __init__(self, username): 77 | self.username = username 78 | 79 | def load_object_hook(name, d): 80 | return {'WP_User': User}[name](**d) 81 | 82 | def dump_object_hook(obj): 83 | if isinstance(obj, User): 84 | return phpserialize.phpobject('WP_User', {'username': obj.username}) 85 | raise LookupError('unknown object') 86 | 87 | user = User('test') 88 | x = phpserialize.dumps(user, object_hook=dump_object_hook) 89 | y = phpserialize.loads(x, object_hook=load_object_hook, 90 | decode_strings=True) 91 | self.assert_(b'WP_User' in x) 92 | self.assertEqual(type(y), type(user)) 93 | self.assertEqual(y.username, user.username) 94 | 95 | def test_basic_object_hook(self): 96 | data = b'O:7:"WP_User":1:{s:8:"username";s:5:"admin";}' 97 | user = phpserialize.loads(data, object_hook=phpserialize.phpobject, 98 | decode_strings=True) 99 | self.assertEqual(user.username, 'admin') 100 | self.assertEqual(user.__name__, 'WP_User') 101 | 102 | 103 | if __name__ == '__main__': 104 | unittest.main() 105 | --------------------------------------------------------------------------------