{ "info": { "author": "Anthon van der Neut", "author_email": "a.van.der.neut@ruamel.eu", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python" ], "description": "Python Object Notation\n======================\n\n.. image:: https://bitbucket.org/ruamel/pon/raw/default/_doc/_static/license.svg\n :target: https://opensource.org/licenses/MIT\n\n.. image:: https://bitbucket.org/ruamel/pon/raw/default/_doc/_static/pypi.svg\n :target: https://pypi.org/project/pon/\n\n.. image:: https://bitbucket.org/ruamel/oitnb/raw/default/_doc/_static/oitnb.svg\n :target: https://bitbucket.org/ruamel/oitnb/\n\n\nPython Object Notation (PON) fills the need for simple, human usable\nand maintainable, configuration data file format for Python.\n\nA basic example of a PON file::\n\n dict(\n version = [1, 2, 3], # Major, Minor and Micro version number\n email = \"avdn@europython.org\",\n score = 8.7,\n restrictions = None,\n published = True,\n install_requires=dict( # nested structure\n any=['requests', 'beautifulsoup'],\n py26=['ordereddict'],\n ),\n oddkeys = { \"2\": 2, \"s p a c e d\": \"out\"}\n )\n\n\n\nPON:\n\n- is readable and maintainable through familiarity for all Python programmers\n- allows comments\n- is editable by humans without breaking things every other time\n- is efficiently and securely loadable\n- Has several basic types: string, numbers (including simple math on\n the numbers), ``True``, ``False``, ``None``, ``date()``, datetime\n (in UTC, no timezone info),\n- basic structures: dict (``dict()`` / ``{}``) and list (``[]``),\n- extended structures: set (``set()`` / ``{}`` (the latter only on\n 2.7+) and tuples (``( )``)\n- basic and extended structures which can be nested (within its own\n type as well as in the other type) with the following restrictions:\n\n - top level must be a dict\n - no syntax for self references\n - ``{ }`` can only having strings as keys\n - children of the extended structures (set, tuple) cannot be\n referenced for substitutions\n\n- has substitutions/interpolations on string values, with the\n substitute value, being the ``str()`` result of a possible complex value\n- can be embedded in a Python file as it is valid Python code, and\n provides routines to extract such an embedded configuration.\n- expects UTF-8 based input\n- has support for multi-line strings through normal triple-quoting, as\n well as for ``dedent``-ing those multi-line strings\n- has a read-only parser with a small enough footprint that you do not\n require a package to be installed before you can use it\n (e.g. ~100 line parser in several ``setup.py`` files in\n packages on PyPI)\n- with a few restrictions, supports round-tripping *without the loss of\n comments* and *without the loss of carefully ordered keys*\n- Output is PEP8 compliant, except for allowing (but not requiring)\n spaces around the ``=`` following a ``dict()`` key\n\n\nWhy not YAML/XML/INI-CFG/JSON\n+++++++++++++++++++++++++++++\n\n\nYAML\n^^^^\n\nYAML fulfils all of the requirements of the PON except for the string\nsubstitution on values. And it can fulfil more complex tasks, like\nhandling (self) references and complex dict/mapping keys. However the\nbasics are easily readable, not all of its syntactical variations\nmight be familiar.\n\nThe main problem is that it requires an external library (ruamel.yaml\nfor YAML1.2 (with round-trip preservation of comments), PyYAML for 1.1\n(with guaranteed loss of comments)) and e.g. cannot be used within a\n``setup.py``.\n\nXML\n^^^\n\nAn XML parser ships with Python, so using that from any program and from\n``setup.py`` is possible. XML combines the inefficiency of ASCII with the\nillegibility of binary. 'nuff said.\n\nINI\n^^^\n\nINI/CFG files are familiar to a lot of people, but unfortunately there are\nall kinds of variations and extensions. Python's built-in ``configparser``\n(``ConfigParser`` in < Python 3.0), provides only limited structural\ninformation. It is essentially a dict, with the sections as keys for values\nthat are a dict (the key = value pairs). It supports multi-line strings in a\ntraditional way (indented continuation lines), comments and has interpolation\nin two different forms.\n\nJSON\n^^^^\n\nJSON is a format familiar to many for data exchange. However several\nthings make it unusableq for configuration files that are manipulated by\nhumans:\n\n - the inability to handle comments in most implementations\n (including Python's ``json`` module), although the inventor specified\n these should be ignored\n - humans generating an invalid file on every other edit, because\n commas are separators, and are not allowed before list/dict terminal\n ``]`` resp. ``}``\n - JSON requires even simple dict keys (strings without spaces) to be\n obfuscated by quotes\n\nPython has most of the batteries for PON included\n+++++++++++++++++++++++++++++++++++++++++++++++++\n\nIn Python you can already do::\n\n config = dict(\n version = [1, 2, 3], # Major, Minor and Micro version number\n email = \"avdn@europython.org\",\n score = 8.7,\n restrictions = None,\n published = True,\n install_requires=dict( # nested structure\n any=['requests', 'beautifulsoup',],\n py26=['ordereddict']\n ),\n )\n\n\nThis **is** valid Python code, and the embedding requirement is\nimplicitly taken care of. The problem is you don't always want to\nevaluation/include/import this *as code*, because of security\nimplications. You need to be able to parse this and reject invalid or\ndangerous constructs.\n\n(Also note that you can delete the whole line containing ``py26=``\nin the above without breaking things, although you end up with a comma\nbefore the closing parenthesis.)\n\nParsing out PON (almost) of the box\n+++++++++++++++++++++++++++++++++++\n\nThe function ``literal_eval`` from the ``ast`` module can parse a more\nJSON like variation of the previous config. E.g. the contents of the\nfollowing file::\n\n {\n \"version\": [1, 2, 3], # Major, Minor and Micro version number\n \"email\": \"avdn@europython.org\",\n \"score\": 8.7,\n \"restrictions\": None,\n \"published\": True,\n \"install_requires\": { # nested structure\n \"any\": ['requests', 'beautifulsoup',],\n \"py26\": ['ordereddict']\n },\n }\n\n\nusing the following::\n\n python -c 'import ast; ast.literal_eval(open(\"input2.pon\").read())\n\nThe above can also be relatively easily parsed from a larger (Python\nsource) file by looking for the assignment to a known variable,\n``config = {`` and the corresponding ``}`` (usually at the same indentation level).\n\nThis is almost JSON, but to be able to include JSON in Python, as well as\nbeing able to parse that, you need to change it some more::\n\n true = True\n null = None\n config = {\n \"version\": [1, 2, 3],\n \"email\": \"avdn@europython.org\",\n \"score\": 8.7,\n \"restrictions\": null,\n \"published\": true,\n \"install_requires\": {\n \"any\": [\"requests\", \"beautifulsoup\",],\n \"py26\": [\"ordereddict\"]\n }\n }\n\n\nYou have to define ``null`` and ``true`` for the Python parser to accept\nthis. For most JSON parsers you also have to remove the comments, and\nconsistently use double quotes for strings. And above all you have to remove\ntrailing commas, which is most often forgotten when deleting whole key-value\nlines at the end of a dictionary/mapping in JSON (resulting in non-running\nprograms unless you use ruamel.yaml/PyYAML to load your JSON files).\n\nActually, the above is not valid JSON (did you see the trailing comma\nin the list on the ``any`` line?). These problems don't make JSON a\nbad format. It is fine for information interchange between\nprograms. JSON files should just never be edited, and preferably not\neven have to be read, by humans.\n\n\nA replacement for literal_eval\n++++++++++++++++++++++++++++++\n\n``ast.literal_eval`` cannot deal with ``dict()``, so using that you\ncannot have keys that are strings without quotes. It\nalso throws a useless generic ValueError, when it is fed invalid\nstrings, making it difficult to provide meaningful feedback to the\nhuman editor of the (invalid) configuration data. And finally it\nhappily tries and fails to do its job when you feed it nonsense data\nlike a float.\n\n``ast.literal_eval`` is a good example how you can make a minimal\nevaluator around the ``ast`` facilities. A small adaptation can handle\nthe extras like ``dict``, ``date`` and ``datetime``. Thus allowing\nnon-quoted simple keys, while disallowing non string keys for ``{}``,\nforcing a toplevel dictionary. The code, including adaptations for 2.6\nand later support (2.6's cannot handle ``{}`` type sets)::\n\n import sys # NOQA\n import platform # NOQA\n import datetime # NOQA\n from textwrap import dedent # NOQA\n from _ast import * # NOQA\n\n if sys.version_info < (3, ):\n string_type = basestring\n else:\n string_type = str\n\n if sys.version_info < (3, 4):\n class Bytes():\n pass\n\n class NameConstant:\n pass\n\n if sys.version_info < (2, 7) or platform.python_implementation() == 'Jython':\n class Set():\n pass\n\n\n def loads(node_or_string, dict_typ=dict, return_ast=False, file_name=None):\n \"\"\"\n Safely evaluate an expression node or a string containing a Python\n expression. The string or node provided may only consist of the following\n Python literal structures: strings, bytes, numbers, tuples, lists, dicts,\n sets, booleans, and None.\n \"\"\"\n if sys.version_info < (3, 4):\n _safe_names = {'None': None, 'True': True, 'False': False}\n if isinstance(node_or_string, string_type):\n node_or_string = compile(\n node_or_string,\n '' if file_name is None else file_name, 'eval', PyCF_ONLY_AST)\n if isinstance(node_or_string, Expression):\n node_or_string = node_or_string.body\n else:\n raise TypeError(\"only string or AST nodes supported\")\n\n def _convert(node, expect_string=False):\n if isinstance(node, (Str, Bytes)):\n return node.s\n if expect_string:\n pass\n elif isinstance(node, Num):\n return node.n\n elif isinstance(node, Tuple):\n return tuple(map(_convert, node.elts))\n elif isinstance(node, List):\n return list(map(_convert, node.elts))\n elif isinstance(node, Set):\n return set(map(_convert, node.elts))\n elif isinstance(node, Dict):\n return dict_typ((_convert(k, expect_string=True), _convert(v)) for k, v\n in zip(node.keys, node.values))\n elif isinstance(node, NameConstant):\n return node.value\n elif sys.version_info < (3, 4) and isinstance(node, Name):\n if node.id in _safe_names:\n return _safe_names[node.id]\n elif isinstance(node, UnaryOp) and \\\n isinstance(node.op, (UAdd, USub)) and \\\n isinstance(node.operand, (Num, UnaryOp, BinOp)): # NOQA\n operand = _convert(node.operand)\n if isinstance(node.op, UAdd):\n return + operand\n else:\n return - operand\n elif isinstance(node, BinOp) and \\\n isinstance(node.op, (Add, Sub, Mult)) and \\\n isinstance(node.right, (Num, UnaryOp, BinOp)) and \\\n isinstance(node.left, (Num, UnaryOp, BinOp)): # NOQA\n left = _convert(node.left)\n right = _convert(node.right)\n if isinstance(node.op, Add):\n return left + right\n elif isinstance(node.op, Mult):\n return left * right\n else:\n return left - right\n elif isinstance(node, Call):\n func_id = getattr(node.func, 'id', None)\n if func_id == 'dict':\n return dict_typ((k.arg, _convert(k.value)) for k in node.keywords)\n elif func_id == 'set':\n return set(_convert(node.args[0]))\n elif func_id == 'date':\n return datetime.date(*[_convert(k) for k in node.args])\n elif func_id == 'datetime':\n return datetime.datetime(*[_convert(k) for k in node.args])\n elif func_id == 'dedent':\n return dedent(*[_convert(k) for k in node.args])\n elif isinstance(node, Name):\n return node.s\n err = SyntaxError('malformed node or string: ' + repr(node))\n err.filename = ''\n err.lineno = node.lineno\n err.offset = node.col_offset\n err.text = repr(node)\n err.node = node\n raise err\n res = _convert(node_or_string)\n if not isinstance(res, dict_typ):\n raise SyntaxError(\"Top level must be dict not \" + repr(type(res)))\n if return_ast:\n return res, node_or_string\n return res\n\n\nThe above 109 lines of Python **is** the actual code, that loads the full PON from an iterable.\n\n\nThis code can be further reduced if you only need to support later\nPython versions, and if you know your input is restricted (no math, no\nset/tuples/``{}``, no datetime, etc)\n\n\nSyntaxError\n^^^^^^^^^^^\n\nThe ``ast.literal_eval`` gives you a generic ValueError without any\nindication of what might be wrong nor where things are wrong. From the\n``SyntaxError`` that is raised on erroreous input by ``loads()`` you\ncan retrieve useful line information::\n\n error_str = u\"\"\"\n dict(\n a= u\"\u03b1\",\n b= False,\n c= date(2015, 9, 12),\n d= 1.37,\n )\n \"\"\"\n\n try:\n loads(error_str)\n except SyntaxError as e:\n context = 2\n from_line = e.lineno - (context + 1)\n to_line = e.lineno + (context - 1)\n w = len(str(to_line))\n for index, line in enumerate(error_str.splitlines(True)):\n if from_line <= index <= to_line:\n print(u\"{:{}}: {}\".format(index, w, line), end=u'')\n if index == e.lineno - 1:\n print(u\"{:{}} {}^--- {}\".format(\n u' ', w, u' ' * e.offset, e.node))\n\ngiving you::\n\n 2: a= u\"\u03b1\",\n 3: b= False,\n 4: c= date(2015, 9, 12),\n ^--- <_ast.Call object at 0x7f1598d20950>\n 5: d= 1.37,\n 6: )\n\n(the PON parser as indicated above extends ``ast.literal_eval`` with ``date()``\nand doesn't throw an eror on that input)\n\nMotivation\n++++++++++\n\nThe development of the ``literal_eval`` extension/replacement was\nmotivated by cleaning providing version and other information from the\n``__init__.py`` of a package to its ``setup.py`` file, thereby\nminimising the clutter of extra configuration files in the base\ndirectory (it is bad enough with ``setup.py``, ``dist`` and\n``tox.ini`` as non hidden files/directories.\n\nA version number can be easily parsed from an ``__init__.py`` file.\nBut allowing for more complex and complete configuration data allows\n``setup.py`` to be the same for all of my projects.\n\nUsing the ``pon`` package\n+++++++++++++++++++++++++\n\nThe ``pon`` package provides the the main parser\n``loads()``, the utility functions ``get()``, ``store()`` and\n``extract()`` and the PON class (for which the utilities are shortcuts).\n\n\n``get()`` and ``store()``\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\nIf you have a configuration::\n\n dict(\n a = dict(\n b = 24,\n c = [1, 3.14, {'d': 'klm'}],\n }\n\nloaded into a variable ``config``, you can access the value ``klm`` in\nthe normal Python way by using ``config['a']['c'][2]['d']``. PON also\nprovides the function ``get()`` with which you can access the same\nvalue using ``get(config, 'a.c.2.d')``.\n\nBased on the nested structure of ``config`` the \"2\" in that sequence is\nconverted to an index. As indicated before, integers as dict keys, are not allowed,\nkeys have to strings.\n\nComplementary there is the ``store()`` function (``set()`` being a reserved word in Python)\nthat takes as third parameter a value, to set or overwrite an existing one:\n``store(config, 'a.c.2.d', 'xyz')``\n\n\nSubstitution with ``get()``\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nSubstitution (called interpolation in\n``ConfigParser``/``configparser``) is done by accessing a value of\nyour configuration with with ``get()``, and providing the extra\nkeyword ``expand``. Substitution is done recursively on the expanded value.\nYou can provide the ``config`` object itself to expand::\n\n val = get(config, 'some.path', expand=config)\n\nand since this is such a common use case, you can specify ``expand=True``\ninstead of actually passing in ``config`` twice.\n\nThe syntax for substitution is the usual Python,\n``\"{key}\".format(key=value)``, string formatting but the key can be a\ndotted sequence valid for ``get()``::\n\n import pon\n\n config = pon.loads(\"\"\"\\\n dict(\n a = dict(\n image = \"http://{domain}/images\",\n alt = \"europython.eu\",\n dd = (2011, 10, 2) # this is a tuple\n ),\n domain = 'python.{tld.organisations}',\n datestr = 'date{a.dd}',\n tld = {\"organisations\": \"org\", \"commmercian\": \"com\"}\n )\n \"\"\")\n\n for key in ['a.image', 'datestr']:\n print(key, '->', pon.get(config, key, expand=True))\n\n\ngiving::\n\n a.image -> http://python.org/images\n datestr -> date(2011, 10, 2)\n\n\nThe recursion for this is restricted to 10 levels.\n\nThe separator (by default \".\") can be set on the ``PON`` class. Since \":\"\nis special in format strings, that character cannot be used as separator.\n\nRoundTripping PON\n+++++++++++++++++\n\nWith some restrictions it is possible to round trip PON, while\npreserving comments, in the smae way ruamel.yaml can for YAML:\n\n- you will not lose any data\n- on the first round-trip your formatting might change\n- a second round-trip will result in the same output as the first round-trip\n\nIn order to facilitate round-tripping extra information needs to be\nkept that is not available in the normal dict you get from the loading\nof your PON data structure into Python. This extra processing can be\ndone up-front, after which the original configuration data is no\nlonger necessary in text form, but wastes time during loading in case\nthe round-tripping is never needed. It can be extracted on demand, but\nin that case the original textual data needs to be available. This\ntime vs storage trade of is currently done at load time, and only when\nusing the PON class (and not when using utility function ``loads()``).\n\nIf you create a PON object from ``input`` (a file, a string or a list\nof strings) using ``PON(input)`` the resulting object will have\ninformation about the dict keys and list elements and has comment\ninformation associated with these keys.\n\nThe primary purpose for round-tripping is updating existing\ninformation in the configuration: updating one of the tuple values for\nkey \"version\", adding a dependency package to the list necessary for\n``py26``. If a whole new configuration file, including comments, needs\nto be generated, this can generally be done more easily by using (or\nstarting from) a text template than to try and procedurally built the structure.\n\nComments\n^^^^^^^^\n\nDumping the loaded PON structure as text, assuming some formatting\ncriteria, is relatively easy, if we could just ignore the fact that\ncomments are important for future readers of the dumped information.\n\nThe Python built-in ``compile()`` function generating the AST\ninformation from which the object holding the configuration\ninformation is extracted, throws away the comments. So the comment\ninformation has to be re-associated with the object, and in addition to\ndetermining what comment belongs where, this requires that the\nelements in the object tree can be extended (requiring more complex\nobjects that behave like dicts/lists, but have extra slots for comment\ninformation, a method which is also used in ruamel.yaml), or that a shadow\nstructure is kept in the same form as the configuration object.\n\nPON follows a hybrid by requiring the dictionaries to have key\ninsertion ordering (the ordering of the keys in the source\nconfiguration data) as well as keeping a shadow structure. The shadow\nstructure is extracted from the AST tree (used for generating/checking\nthe configuration loaded) with tokenization information (which\nprovides the comments).\n\nComments are associated with dictionary keys or list elements as far\nas these are \"on their own line\". A full line comment belongs, or\nconsecutive comments belong, to the next key/element if it is on a\nline of its own after the previous key/element. An end-of-line comment\nfollows a key/element at the end of a line (and there can be only\none). Additionally track is kept of comments before the initial, top\nlevel, ``dict(``, after the final key (there is no following one to\nhook it up to) and after the dict closing ``)`` token.\n\nAn example of a heavily commented PON file::\n\n # this is the configuration driving setup\n # initials comments going before the configuration information\n dict( # the top level dict can also have an end-of-line comment\n # full comment associated with the key version\n # this doesn't explain its usage that much, also associates with version\n version=(1, 2, 3) # end-of-line comment for version\n alt = dict( # this is end-of-line for alt\n # associated with place\n place=u\"D\u00fcsseldorf\",\n taste=\"awful\",\n # the next key is klm, this comment will move down on round-trip.\n ), # this is assocated with klm as well, even though not a end-of-line\n \"klm\" = [ 3, 5, 6 ], # although list elements, belongs to klm\n \"xyz\" = [ # belong to xyz\n 42, # the answer (associated with 42)\n 196,\n ],\n # trailing comment, special\n )\n # still more to say, special as well\n\nIf you change dictionary keys, comments associated with these will\ngenerally get lost. So do comments associated with key/element that\nget deleted.\n\nChanges on first round trip\n^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nPartly due to the ``pprint`` code on which the dumper is based, partly\ndue to arbitrary decisions on what kind of formatting info is\npreserved and partly depending on your input, the following happens on\nthe first round-trip:\n\n- the last element on a multi-line dict/list gets a trailing comma\n- comments that cannot be associated with an dict key or list element\n on the same line get moved to the next key/element (or the end of\n the main dict if no following elements)\n- strings are single quoted unless they contain a single quote (and no\n double quotes)\n- indent levels are a at 4 spaces\n- space around the equal sign between dict keys and values is removed\n- sets and tuples elements cannot be associated with comments, hence comments\n wander if they are not on the same line as a dict-key/list-element\n- sets and tuples are dumped on a single line, any dicts and lines\n underneath them are currently inaccessible for ``get()`` and\n therefore keys/elements for such dicts/lists cannot be associated\n with comments\n- extra lines with white space are silently dropped\n- add/subtract/multiply is not preserved\n- the datetime ``repr()`` drops trailing milliseconds if and seconds if\n they equal zero.\n\nNo data or comments get lost, unless you manipulate dict keys and/or\nlist length. And if the output from a dump is taken as source there\nshould be no further \"wandering\". The following input::\n\n try:\n from cStringIO import StringIO as _StringIO\n except ImportError:\n from io import StringIO as _StringIO\n\n from pon import PON\n\n input = \"\"\"\n dict(\n pckgs = dict(\n any=['package1', 'package2'],\n py26=['another package', 'and one with a long name',\n 'and on a new line'] # where do you go?\n ),\n )\n \"\"\"\n\n out1 = _StringIO()\n p1 = PON(input)\n p1.dump(out1)\n print(out1.getvalue())\n\n out2 = _StringIO()\n p2 = PON(out1.getvalue())\n p2.dump(out2)\n\n\n print('roundtrip 1: {0}, roundtrip 2: {1}'.format(\n input == out1.getvalue(),\n out1.getvalue() == out2.getvalue()))\n\n\ngives the following output::\n\n dict(\n pckgs=dict(\n any=['package1', 'package2'],\n py26=['another package', 'and one with a long name',\n 'and on a new line', # where do you go?\n ],\n ),\n )\n \n roundtrip 1: False, roundtrip 2: True\n\n\nFurther improvements\n^^^^^^^^^^^^^^^^^^^^\n\n- The set and tuple elements could be indexed, and then comments could\n be associated with their elements, and multi-line dumping would be\n better preserved.\n\n- The ``dump()`` could take parameters about indentation depth and on\n string quoting information.\n\n- The ``dump()`` output should be PEP8 compliant in principle. But IMO\n the removal of spaces around the `=` in a multi-line keyword\n argument assignment for ``dict()`` doesn't make thing more\n readable. A parameter to select one or the other would be useful::\n\n\n dict(\n a='1234324',\n b=['xyz', 'klm'],\n )\n\n is less easy to read than::\n\n dict(\n a = '1234324',\n b = ['xyz', 'klm'],\n )\n\n- Keeping the ``#`` of comments on multiple consecutive lines aligned,\n even if a value was changed before dumping and has become longer.\n\nShowcase\n++++++++\n\nThe following program contains most (if not all) of the facilities\nand round-trips::\n\n from io import StringIO as _StringIO\n from pon import PON\n\n configs = u'''\\\n # example config\n # should contain all types and facilities\n dict(\n s='abc', # single line string\n # multiline string\n mls=\"\"\"one\n two\n three\"\"\",\n mls_dedent=dedent(\"\"\"\n abc\n def\n \"\"\"),\n ghi={'A': 1, 'B': 2},\n klm=['Airbus 370', 'Fokker 100'],\n opq=set([2, 3, 5, 7, 9]),\n rst=(0, 1, 1, 2, 3, 5, 8, 13), # Fibonacci\n m={u'\u03c0': 3.14},\n anniversary=date(2011, 10, 2),\n dts=datetime(1919, 12, 1, 13, 45, 4),\n milisec=datetime(1922, 10, 19, 17, 55, 23, 321),\n six=2 + 4,\n secs_per_day=24 * 60 * 60,\n two=-2 - -4,\n # if you want to extend, do it here\n ) # and it's over\n '''\n\n out = _StringIO()\n p = PON(configs)\n p.dump(out)\n conf_adjust_for_calc = configs\n # calculations are not preserved, they don't round trip, so adjust here\n for x, y in (('2 + 4', '6'),\n ('24 * 60 * 60', \"{}\".format(24 * 60 * 60)),\n ('-2 - -4', '2')):\n conf_adjust_for_calc = conf_adjust_for_calc.replace(x, y)\n outl = out.getvalue().splitlines(True)\n orgl = conf_adjust_for_calc.splitlines(True)\n if outl == orgl:\n print('roundtrip 1: equal')\n else:\n import difflib\n diff = difflib.unified_diff(orgl, outl, 'input', 'output')\n for line in diff:\n print(line, end='')\n\n\nwith output::\n\n roundtrip 1: equal", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://bitbucket.org/ruamel/pon", "keywords": "pon object-notation json yaml xml", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "pon", "package_url": "https://pypi.org/project/pon/", "platform": "", "project_url": "https://pypi.org/project/pon/", "project_urls": { "Homepage": "https://bitbucket.org/ruamel/pon" }, "release_url": "https://pypi.org/project/pon/0.2.3/", "requires_dist": null, "requires_python": "", "summary": "Python Object Notation", "version": "0.2.3" }, "last_serial": 4159759, "releases": { "0.1": [], "0.1.0": [ { "comment_text": "", "digests": { "md5": "270de572b5f4c237844b2f0dc6233813", "sha256": "c4899f863d9edb5bec09eea24e7255d006940ed65b9364aeb2d1992af1d58358" }, "downloads": -1, "filename": "pon-0.1.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "270de572b5f4c237844b2f0dc6233813", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 29978, "upload_time": "2015-09-22T14:04:52", "url": "https://files.pythonhosted.org/packages/1c/c6/c800d02c59b0361ecc9ce0c372cab0c3cd284d992ea18f191be4ec2a536f/pon-0.1.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d3ea8fc7e7624b1330f915c8eee224d8", "sha256": "534fe3f9a9662f4381cd9a93871b17c845a832545bbda34d44925f6cf15f0a08" }, "downloads": -1, "filename": "pon-0.1.0.tar.gz", "has_sig": false, "md5_digest": "d3ea8fc7e7624b1330f915c8eee224d8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 40137, "upload_time": "2015-09-22T14:04:47", "url": "https://files.pythonhosted.org/packages/27/3b/b23500fd2158a84082993c23d91fdadfeb014095483947be74c8dbe6b207/pon-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "4a0a659065860cacf08ee87d00eb39f2", "sha256": "874bbd620563d43b83b1bd8348c4bac5f4b1722a556e24ea69c014fdb693875a" }, "downloads": -1, "filename": "pon-0.1.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "4a0a659065860cacf08ee87d00eb39f2", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 31062, "upload_time": "2016-01-25T09:08:28", "url": "https://files.pythonhosted.org/packages/7d/18/46b40e8f8108ee08ee5b09c4a1d2cf780975deed453ba16f3283fe8d54ff/pon-0.1.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0452c51228712d27a5587e48fba0670a", "sha256": "906e87443cf9eff494e080c244e875002cc0d395aeecd5dc293f0cd7a12dd08f" }, "downloads": -1, "filename": "pon-0.1.1.tar.gz", "has_sig": false, "md5_digest": "0452c51228712d27a5587e48fba0670a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42276, "upload_time": "2016-01-25T09:05:14", "url": "https://files.pythonhosted.org/packages/9e/fa/7d9423070de67a92e0fd54e87cc955ede956e7763e27e6988b5ec2132cc8/pon-0.1.1.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "a8f4f181e4bc9983cee77c49be5e436d", "sha256": "73afbfe15154c54279bf7b55f1401f9a2c89b23f3cdef8d2019ad54efcaf142e" }, "downloads": -1, "filename": "pon-0.2.1.tar.gz", "has_sig": false, "md5_digest": "a8f4f181e4bc9983cee77c49be5e436d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42860, "upload_time": "2016-02-22T07:33:11", "url": "https://files.pythonhosted.org/packages/32/42/2397cebfd1b6fff09033887797df45f1697438c8349c2ef6b671eb1d2ce5/pon-0.2.1.tar.gz" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "e36b14998c91b72b2ff081fc6fe9aae1", "sha256": "f2a3705908a98117436833f3999cae26d7679085f7f94a30d8f91bd33fbec425" }, "downloads": -1, "filename": "pon-0.2.2.tar.gz", "has_sig": false, "md5_digest": "e36b14998c91b72b2ff081fc6fe9aae1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42857, "upload_time": "2017-03-21T20:48:47", "url": "https://files.pythonhosted.org/packages/43/1f/b1a13d1be71673e10745c9b0888209b863462e8bfe36d1900385e748c145/pon-0.2.2.tar.gz" } ], "0.2.2.dev0": [ { "comment_text": "", "digests": { "md5": "4065981f5cc6d8e12d2b11f0db6e6557", "sha256": "c07d47719ee818111f3eb93ede16385f08712702fb48e0bb9ab89da2f444b7cc" }, "downloads": -1, "filename": "pon-0.2.2.dev0.tar.gz", "has_sig": false, "md5_digest": "4065981f5cc6d8e12d2b11f0db6e6557", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42882, "upload_time": "2016-02-22T07:32:49", "url": "https://files.pythonhosted.org/packages/0d/4b/8b17ff951dcdae7e7357bcbcb78e82f6dad35d7b6fa97de2347fe4cb8881/pon-0.2.2.dev0.tar.gz" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "8ae9632b2d381c95fd7579c84b01fad7", "sha256": "d12f151501fc0c58536e15256916211b8133955ba93f5bbfdd09fb8556d3327b" }, "downloads": -1, "filename": "pon-0.2.3.tar.gz", "has_sig": false, "md5_digest": "8ae9632b2d381c95fd7579c84b01fad7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47374, "upload_time": "2018-08-11T14:42:02", "url": "https://files.pythonhosted.org/packages/e6/0e/4e0c0d6e8f0f16cba2d8134c0da03689baab7440d4185f5d3fabc10ea3eb/pon-0.2.3.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "8ae9632b2d381c95fd7579c84b01fad7", "sha256": "d12f151501fc0c58536e15256916211b8133955ba93f5bbfdd09fb8556d3327b" }, "downloads": -1, "filename": "pon-0.2.3.tar.gz", "has_sig": false, "md5_digest": "8ae9632b2d381c95fd7579c84b01fad7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47374, "upload_time": "2018-08-11T14:42:02", "url": "https://files.pythonhosted.org/packages/e6/0e/4e0c0d6e8f0f16cba2d8134c0da03689baab7440d4185f5d3fabc10ea3eb/pon-0.2.3.tar.gz" } ] }